'''
Used for importing parts from other FreeCAD documents.
When update parts is executed, this library import or updates the parts in the assembly document.
'''

from assembly2.core import *
from assembly2.core import __dir__
from assembly2.lib3D import *
from assembly2.solvers import solveConstraints
from assembly2.utils.muxAssembly import (
    muxObjects,
    Proxy_muxAssemblyObj,
    muxMapColors
)
from .viewProviderProxy import ImportedPartViewProviderProxy, group_constraints_under_parts
import Part
from PySide import QtGui
import os, numpy, shutil, copy, time

from assembly2.importPart.fcstd_parser import Fcstd_File_Parser
from .importPath import *
from .selectionMigration import *

def importPart( filename, partName=None, doc_assembly=None ):
    if doc_assembly == None:
        doc_assembly = FreeCAD.ActiveDocument
    updateExistingPart = partName != None
    if updateExistingPart:
        FreeCAD.Console.PrintMessage("updating part %s from %s\n" % (partName,filename))
    else:
        FreeCAD.Console.PrintMessage("importing part from %s\n" % filename)
    doc_already_open = filename in [ d.FileName for d in FreeCAD.listDocuments().values() ]
    debugPrint(4, "%s open already %s" % (filename, doc_already_open))
    if doc_already_open:
        doc = [ d for d in FreeCAD.listDocuments().values() if d.FileName == filename][0]
        close_doc = False
    else:
        if filename.lower().endswith('.fcstd'):
            doc = Fcstd_File_Parser( filename )
            close_doc = False
        else: #trying shaping import http://forum.freecadweb.org/viewtopic.php?f=22&t=12434&p=99772#p99772x
            import ImportGui
            doc = FreeCAD.newDocument( os.path.basename(filename) )
            shapeobj = ImportGui.insert(filename,doc.Name)
            close_doc = True

    visibleObjects = [ obj for obj in doc.Objects
                       if hasattr(obj,'ViewObject') and obj.ViewObject.isVisible()
                       and hasattr(obj,'Shape') and len(obj.Shape.Faces) > 0 and 'Body' not in obj.Name] # len(obj.Shape.Faces) > 0 to avoid sketches, skip Body

    debugPrint(3, '%s objects %s' % (doc.Name, doc.Objects))
    if any([ 'importPart' in obj.Content for obj in doc.Objects]) and not len(visibleObjects) == 1:
        subAssemblyImport = True
        debugPrint(2, 'Importing subassembly from %s' % filename)
        tempPartName = 'import_temporary_part'
        obj_to_copy = doc_assembly.addObject("Part::FeaturePython",tempPartName)
        obj_to_copy.Proxy = Proxy_muxAssemblyObj()
        obj_to_copy.ViewObject.Proxy = ImportedPartViewProviderProxy()
        obj_to_copy.Shape =  muxObjects(doc)
        if (not updateExistingPart) or \
                (updateExistingPart and getattr( doc_assembly.getObject(partName),'updateColors',True)):
            muxMapColors(doc, obj_to_copy)
    else:
        subAssemblyImport = False
        if len(visibleObjects) != 1:
            if not updateExistingPart:
                msg = "A part can only be imported from a FreeCAD document with exactly one visible part. Aborting operation"
                QtGui.QMessageBox.information(  QtGui.QApplication.activeWindow(), "Value Error", msg )
            else:
                msg = "Error updating part from %s: A part can only be imported from a FreeCAD document with exactly one visible part. Aborting update of %s" % (partName, filename)
            QtGui.QMessageBox.information(  QtGui.QApplication.activeWindow(), "Value Error", msg )
        #QtGui.QMessageBox.warning( QtGui.QApplication.activeWindow(), "Value Error!", msg, QtGui.QMessageBox.StandardButton.Ok )
            return
        obj_to_copy  = visibleObjects[0]

    if updateExistingPart:
        obj = doc_assembly.getObject(partName)
        prevPlacement = obj.Placement
        if not hasattr(obj, 'updateColors'):
            obj.addProperty("App::PropertyBool","updateColors","importPart").updateColors = True
        importUpdateConstraintSubobjects( doc_assembly, obj, obj_to_copy )
    else:
        partName = findUnusedObjectName( doc.Label + '_', document=doc_assembly )
        try:
            obj = doc_assembly.addObject("Part::FeaturePython",partName)
        except UnicodeEncodeError:
            safeName = findUnusedObjectName('import_', document=doc_assembly)
            obj = doc_assembly.addObject("Part::FeaturePython", safeName)
            obj.Label = findUnusedLabel( doc.Label + '_', document=doc_assembly )
        obj.addProperty("App::PropertyFile",    "sourceFile",    "importPart").sourceFile = filename
        obj.addProperty("App::PropertyFloat", "timeLastImport","importPart")
        obj.setEditorMode("timeLastImport",1)
        obj.addProperty("App::PropertyBool","fixedPosition","importPart")
        obj.fixedPosition = not any([i.fixedPosition for i in doc_assembly.Objects if hasattr(i, 'fixedPosition') ])
        obj.addProperty("App::PropertyBool","updateColors","importPart").updateColors = True
    obj.Shape = obj_to_copy.Shape.copy()
    if updateExistingPart:
        obj.Placement = prevPlacement
    else:
        # for Fcstd_File_Parser not all properties are implemented...
        for p in obj_to_copy.ViewObject.PropertiesList: #assuming that the user may change the appearance of parts differently depending on the assembly.
            if hasattr(obj.ViewObject, p) and p not in ['DiffuseColor']:
                try:
                    setattr(obj.ViewObject, p, getattr(obj_to_copy.ViewObject, p))
                except Exception as msg:
                    FreeCAD.Console.PrintWarning('Unable to setattr(obj.ViewObject, %s, %s)\n' % (p, getattr(obj_to_copy.ViewObject, p) ))
        obj.ViewObject.Proxy = ImportedPartViewProviderProxy()
    if getattr(obj,'updateColors',True) and hasattr( obj_to_copy.ViewObject, 'DiffuseColor'):
        obj.ViewObject.DiffuseColor = copy.copy( obj_to_copy.ViewObject.DiffuseColor )
        #obj.ViewObject.Transparency = copy.copy( obj_to_copy.ViewObject.Transparency )   # .Transparency property
        tsp = copy.copy( obj_to_copy.ViewObject.Transparency )   #  .Transparency workaround for FC 0.17 @ Nov 2016
        if tsp < 100 and tsp!=0:
            obj.ViewObject.Transparency = tsp+1
        if tsp == 100:
            obj.ViewObject.Transparency = tsp-1
        obj.ViewObject.Transparency = tsp   # .Transparency workaround end
    obj.Proxy = Proxy_importPart()
    obj.timeLastImport = os.path.getmtime( filename )
    #clean up
    if subAssemblyImport:
        doc_assembly.removeObject(tempPartName)
    if close_doc: 
        FreeCAD.closeDocument( doc.Name )
        FreeCAD.setActiveDocument( doc_assembly.Name )
        FreeCAD.ActiveDocument = doc_assembly
    return obj

class Proxy_importPart:
    def execute(self, shape):
        pass

class ImportPartCommand:
    def Activated(self):
        if FreeCADGui.ActiveDocument == None:
            FreeCAD.newDocument()
        view = FreeCADGui.activeDocument().activeView()
        #filename, filetype = QtGui.QFileDialog.getOpenFileName(
        #    QtGui.QApplication.activeWindow(),
        #    "Select FreeCAD document to import part from",
        #    "",# "" is the default, os.path.dirname(FreeCAD.ActiveDocument.FileName),
        #    "FreeCAD Document (*.fcstd)"
        #    )
        dialog = QtGui.QFileDialog(
            QtGui.QApplication.activeWindow(),
            "Select FreeCAD document to import part from"
            )
        dialog.setNameFilter("Supported Formats (*.FCStd *.brep *.brp *.imp *.iges *.igs *.obj *.step *.stp);;All files (*.*)")
        if dialog.exec_():
            filename = dialog.selectedFiles()[0]
        else:
            return
        importedObject = importPart( filename )
        FreeCAD.ActiveDocument.recompute()
        if not importedObject.fixedPosition: #will be true for the first imported part
            PartMover( view, importedObject )
        else:
            from PySide import QtCore
            self.timer = QtCore.QTimer()
            QtCore.QObject.connect(self.timer, QtCore.SIGNAL("timeout()"), self.GuiViewFit)
            self.timer.start( 200 ) #0.2 seconds

    def GuiViewFit(self):
        FreeCADGui.SendMsgToActiveView("ViewFit")
        self.timer.stop()

    def GetResources(self):
        return {
            'Pixmap' : ':/assembly2/icons/importPart.svg',
            'MenuText': 'Import a part from another FreeCAD document',
            'ToolTip': 'Import a part from another FreeCAD document'
            }
FreeCADGui.addCommand('assembly2_importPart', ImportPartCommand())


class UpdateImportedPartsCommand:
    def Activated(self):
        #disable proxies solving the system as their objects are updated
        doc_assembly = FreeCAD.ActiveDocument
        solve_assembly_constraints = False
        YesToAll_clicked = False
        for obj in doc_assembly.Objects:
            if hasattr(obj, 'sourceFile'):
                if not hasattr( obj, 'timeLastImport'):
                    obj.addProperty("App::PropertyFloat", "timeLastImport","importPart") #should default to zero which will force update.
                    obj.setEditorMode("timeLastImport",1)
                if not os.path.exists( obj.sourceFile ) and  path_rel_to_abs( obj.sourceFile ) is None:
                    debugPrint( 3, '%s.sourceFile %s is missing, attempting to repair it' % (obj.Name,  obj.sourceFile) )
                    replacement = None
                    aFolder, aFilename = posixpath.split( doc_assembly.FileName )
                    sParts = path_split( posixpath, obj.sourceFile)
                    debugPrint( 3, '  obj.sourceFile parts %s' % sParts )
                    replacement = None
                    previousRejects = []
                    while replacement == None and aFilename != '':
                        for i in reversed(range(len(sParts))):
                            newFn = aFolder
                            for j in range(i,len(sParts)):
                                newFn = posixpath.join( newFn,sParts[j] )
                            debugPrint( 4, '    checking %s' % newFn )
                            if os.path.exists( newFn ) and not newFn in previousRejects :
                                if YesToAll_clicked:
                                    replacement = newFn
                                    break
                                reply = QtGui.QMessageBox.question(
                                    QtGui.QApplication.activeWindow(), "%s source file not found" % obj.Name,
                                    "Unable to find\n  %s \nUse \n  %s\n instead?" % (obj.sourceFile, newFn) ,
                                    QtGui.QMessageBox.Yes | QtGui.QMessageBox.YesToAll | QtGui.QMessageBox.No, QtGui.QMessageBox.Yes)
                                if reply == QtGui.QMessageBox.Yes:
                                    replacement = newFn
                                    break
                                if reply == QtGui.QMessageBox.YesToAll:
                                    replacement = newFn
                                    YesToAll_clicked = True
                                    break
                                else:
                                    previousRejects.append( newFn )
                        aFolder, aFilename = posixpath.split( aFolder )
                    if replacement != None:
                        obj.sourceFile = replacement
                    else:
                        QtGui.QMessageBox.critical(  QtGui.QApplication.activeWindow(), "Source file not found", "update of %s aborted!\nUnable to find %s" % (obj.Name, obj.sourceFile) )
                        obj.timeLastImport = 0 #force update if users repairs link
                if path_rel_to_abs( obj.sourceFile ) is not None:
                    absolutePath = path_rel_to_abs( obj.sourceFile )
                    if os.path.getmtime( absolutePath ) > obj.timeLastImport:
                        importPart( absolutePath, obj.Name,  doc_assembly )
                        solve_assembly_constraints = True
                if os.path.exists( obj.sourceFile ):
                    if os.path.getmtime( obj.sourceFile ) > obj.timeLastImport:
                        importPart( obj.sourceFile, obj.Name,  doc_assembly )
                        solve_assembly_constraints = True

        if solve_assembly_constraints:
            solveConstraints( doc_assembly )
        # constraint mirror house keeping

        for obj in doc_assembly.Objects: #for adding creating mirrored constraints in old files
            if 'ConstraintInfo' in obj.Content:
                if doc_assembly.getObject( obj.Object1 ) == None or doc_assembly.getObject( obj.Object2 ) == None:
                    debugPrint(2, 'removing %s which refers to non-existent objects' % obj.Name)
                    doc_assembly.removeObject( obj.Name ) #required for FreeCAD 0.15 which does not support the on-delete method
                if group_constraints_under_parts():
                    if not hasattr( obj.ViewObject.Proxy, 'mirror_name'):
                        if isinstance( doc_assembly.getObject( obj.Object1 ).Proxy, Proxy_importPart) \
                                or isinstance( doc_assembly.getObject( obj.Object2 ).Proxy, Proxy_importPart):
                            debugPrint(2, 'creating mirror of %s' % obj.Name)
                            doc_assembly.getObject( obj.Object2 ).touch()
                            obj.ViewObject.Proxy.mirror_name = create_constraint_mirror(  obj, obj.ViewObject.Proxy.iconPath )
            elif 'ConstraintNfo' in obj.Content: #constraint mirror
                if  doc_assembly.getObject( obj.ViewObject.Proxy.constraintObj_name ) == None:
                    debugPrint(2, 'removing %s which mirrors/links to a non-existent constraint' % obj.Name)
                    doc_assembly.removeObject( obj.Name ) #clean up for FreeCAD 0.15 which does not support the on-delete method
                elif not group_constraints_under_parts():
                     debugPrint(2, 'removing %s since group_constraints_under_parts=False' % obj.Name)
                     delattr( doc_assembly.getObject( obj.ViewObject.Proxy.constraintObj_name ),  'mirror_name' )
                     doc_assembly.removeObject( obj.Name )
            elif hasattr(obj,'Proxy') and isinstance( obj.Proxy, Proxy_importPart) and not isinstance( obj.ViewObject.Proxy, ImportedPartViewProviderProxy):
                obj.ViewObject.Proxy = ImportedPartViewProviderProxy()
                debugPrint(2, '%s.ViewObject.Proxy = ImportedPartViewProviderProxy()'%obj.Name)
        doc_assembly.recompute()


    def GetResources(self):
        return {
            'Pixmap' : ':/assembly2/icons/importPart_update.svg',
            'MenuText': 'Update parts imported into the assembly',
            'ToolTip': 'Update parts imported into the assembly'
            }


FreeCADGui.addCommand('assembly2_updateImportedPartsCommand', UpdateImportedPartsCommand())

def duplicateImportedPart( part ):
    nameBase = part.Label
    while nameBase[-1] in '0123456789' and len(nameBase) > 0:
        nameBase = nameBase[:-1]
    try:
        newObj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", findUnusedObjectName(nameBase))
    except UnicodeEncodeError:
        safeName = findUnusedObjectName('import_')
        newObj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", safeName)
        newObj.Label = findUnusedLabel( nameBase )
    newObj.addProperty("App::PropertyFile",    "sourceFile",    "importPart").sourceFile = part.sourceFile
    newObj.addProperty("App::PropertyFloat", "timeLastImport","importPart").timeLastImport =  part.timeLastImport
    newObj.setEditorMode("timeLastImport",1)
    newObj.addProperty("App::PropertyBool","fixedPosition","importPart").fixedPosition = False# part.fixedPosition
    newObj.addProperty("App::PropertyBool","updateColors","importPart").updateColors = getattr(part,'updateColors',True)
    newObj.Shape = part.Shape.copy()
    for p in part.ViewObject.PropertiesList: #assuming that the user may change the appearance of parts differently depending on their role in the assembly.
        if hasattr(newObj.ViewObject, p) and p not in ['DiffuseColor','Proxy']:
            setattr(newObj.ViewObject, p, getattr( part.ViewObject, p))
    newObj.ViewObject.DiffuseColor = copy.copy( part.ViewObject.DiffuseColor )
    newObj.Proxy = Proxy_importPart()
    newObj.ViewObject.Proxy = ImportedPartViewProviderProxy()
    newObj.Placement.Base = part.Placement.Base
    newObj.Placement.Rotation = part.Placement.Rotation
    return newObj


class PartMover:
    def __init__(self, view, obj):
        self.obj = obj
        self.initialPostion = self.obj.Placement.Base
        self.copiedObject = False
        self.view = view
        self.callbackMove = self.view.addEventCallback("SoLocation2Event",self.moveMouse)
        self.callbackClick = self.view.addEventCallback("SoMouseButtonEvent",self.clickMouse)
        self.callbackKey = self.view.addEventCallback("SoKeyboardEvent",self.KeyboardEvent)
    def moveMouse(self, info):
        newPos = self.view.getPoint( *info['Position'] )
        debugPrint(5, 'new position %s' % str(newPos))
        self.obj.Placement.Base = newPos
    def removeCallbacks(self):
        self.view.removeEventCallback("SoLocation2Event",self.callbackMove)
        self.view.removeEventCallback("SoMouseButtonEvent",self.callbackClick)
        self.view.removeEventCallback("SoKeyboardEvent",self.callbackKey)
    def clickMouse(self, info):
        debugPrint(4, 'clickMouse info %s' % str(info))
        if info['Button'] == 'BUTTON1' and info['State'] == 'DOWN':
            if not info['ShiftDown'] and not info['CtrlDown']:
                self.removeCallbacks()
            elif info['ShiftDown']: #copy object
                self.obj = duplicateImportedPart( self.obj )
                self.copiedObject = True
            elif info['CtrlDown']:
                azi   =  ( numpy.random.rand() - 0.5 )*numpy.pi*2
                ela   =  ( numpy.random.rand() - 0.5 )*numpy.pi
                theta =  ( numpy.random.rand() - 0.5 )*numpy.pi
                axis = azimuth_and_elevation_angles_to_axis( azi, ela )
                self.obj.Placement.Rotation.Q = quaternion( theta, *axis )

    def KeyboardEvent(self, info):
        debugPrint(4, 'KeyboardEvent info %s' % str(info))
        if info['State'] == 'UP' and info['Key'] == 'ESCAPE':
            if not self.copiedObject:
                self.obj.Placement.Base = self.initialPostion
            else:
                FreeCAD.ActiveDocument.removeObject(self.obj.Name)
            self.removeCallbacks()



class PartMoverSelectionObserver:
     def __init__(self):
         FreeCADGui.Selection.addObserver(self)
         FreeCADGui.Selection.removeSelectionGate()
     def addSelection( self, docName, objName, sub, pnt ):
         debugPrint(4,'addSelection: docName,objName,sub = %s,%s,%s' % (docName, objName, sub))
         FreeCADGui.Selection.removeObserver(self)
         obj = FreeCAD.ActiveDocument.getObject(objName)
         view = FreeCADGui.activeDocument().activeView()
         PartMover( view, obj )


class MovePartCommand:
    def Activated(self):
        selection = [s for s in FreeCADGui.Selection.getSelectionEx() if s.Document == FreeCAD.ActiveDocument ]
        if len(selection) == 1:
            PartMover(  FreeCADGui.activeDocument().activeView(), selection[0].Object )
        else:
            PartMoverSelectionObserver()

    def GetResources(self):
        return {
            'Pixmap' : ':/assembly2/icons/Draft_Move.svg',
            'MenuText': 'move',
            'ToolTip': 'move part  ( shift+click to copy )'
            }

FreeCADGui.addCommand('assembly2_movePart', MovePartCommand())

class DuplicatePartCommand:
    def Activated(self):
        selection = [s for s in FreeCADGui.Selection.getSelectionEx() if s.Document == FreeCAD.ActiveDocument ]
        if len(selection) == 1:
            PartMover(  FreeCADGui.activeDocument().activeView(), duplicateImportedPart( selection[0].Object ) )

    def GetResources(self):
        return {
            'MenuText': 'duplicate',
            'ToolTip': 'duplicate part (hold shift for multiple)'
            }

FreeCADGui.addCommand('assembly2_duplicatePart', DuplicatePartCommand())

#copy object


class EditPartCommand:
    def Activated(self):
        selection = [s for s in FreeCADGui.Selection.getSelection() if s.Document == FreeCAD.ActiveDocument ]
        obj = selection[0]
        docs = FreeCAD.listDocuments().values()
        docFilenames = [ d.FileName for d in docs ]
        if not obj.sourceFile in docFilenames :
            FreeCAD.open(obj.sourceFile)
            debugPrint(2, 'Openning %s' % str(obj.sourceFile))
        else:
            name = docs[docFilenames.index(obj.sourceFile)].Name
            debugPrint(2, 'Trying to set focus on %s, not working for some reason!' % str(obj.sourceFile))
            FreeCAD.setActiveDocument( name )
            FreeCAD.ActiveDocument=FreeCAD.getDocument( name )
            FreeCADGui.ActiveDocument=FreeCADGui.getDocument( name )
    def GetResources(self):
        return {
            'MenuText': 'edit',
            }
FreeCADGui.addCommand('assembly2_editImportedPart', EditPartCommand())

class ForkPartCommand:
    def Activated(self):
        selection = [s for s in FreeCADGui.Selection.getSelection() if s.Document == FreeCAD.ActiveDocument ]
        obj = selection[0]
        filename, filetype = QtGui.QFileDialog.getSaveFileName(
            QtGui.QApplication.activeWindow(),
            "Specify the filename for the fork of '%s'" % obj.Label[:obj.Label.find('_import')],
            os.path.dirname(FreeCAD.ActiveDocument.FileName),
            "FreeCAD Document (*.fcstd)"
            )
        if filename == '':
            return
        if not os.path.exists(filename):
            debugPrint(2, 'copying %s -> %s' % (obj.sourceFile, filename))
            shutil.copyfile(obj.sourceFile, filename)
            obj.sourceFile = filename
            FreeCAD.open(obj.sourceFile)
        else:
            QtGui.QMessageBox.critical(  QtGui.QApplication.activeWindow(), "Bad filename", "Specify a new filename!")

    def GetResources(self):
        return {
            'MenuText': 'fork',
            }
FreeCADGui.addCommand('assembly2_forkImportedPart', ForkPartCommand())


class DeletePartsConstraints:
    def Activated(self):
        selection = [s for s in FreeCADGui.Selection.getSelection() if s.Document == FreeCAD.ActiveDocument ]
        #if len(selection) == 1: not required as this check is done in initGui
        part = selection[0]
        deleteList = []
        for c in FreeCAD.ActiveDocument.Objects:
            if 'ConstraintInfo' in c.Content:
                if part.Name in [ c.Object1, c.Object2 ]:
                    deleteList.append(c)
        if len(deleteList) == 0:
            QtGui.QMessageBox.information(  QtGui.QApplication.activeWindow(), "Info", 'No constraints refer to "%s"' % part.Name)
        else:
            flags = QtGui.QMessageBox.StandardButton.Yes | QtGui.QMessageBox.StandardButton.No
            msg = "Delete %s's constraint(s):\n  - %s?" % ( part.Name, '\n  - '.join( c.Name for c in deleteList))
            response = QtGui.QMessageBox.critical(QtGui.QApplication.activeWindow(), "Delete constraints?", msg, flags )
            if response == QtGui.QMessageBox.Yes:
                for c in deleteList:
                    from assembly2.constraints import removeConstraint
                    removeConstraint(c)
    def GetResources(self):
        return {
            'MenuText': 'delete constraints',
            }
FreeCADGui.addCommand('assembly2_deletePartsConstraints', DeletePartsConstraints())