import math
from collections import namedtuple
import FreeCAD, FreeCADGui
from PySide import QtCore, QtGui
from . import utils, gui
from .assembly import isTypeOf, Assembly, ViewProviderAssembly, \
    resolveAssembly, getElementInfo, setPlacement
from .utils import logger, objName
from .constraint import Constraint

MovingPartInfo = namedtuple('MovingPartInfo',
        ('Hierarchy','ElementInfo','SelObj','SelSubname'))

class AsmMovingPart(object):
    def __init__(self,hierarchy,info):
        self.objs = [h.Assembly for h in reversed(hierarchy)]
        self.assembly = resolveAssembly(info.Parent)
        self.viewObject = self.assembly.Object.ViewObject
        self.info = info
        self.undos = None

        fixed = Constraint.getFixedTransform(self.assembly.getConstraints())
        fixed = fixed.get(info.Part,None)
        self.fixedTransform = fixed
        if fixed and fixed.Shape:
            shape = fixed.Shape
        else:
            shape = info.Shape

        rot = utils.getElementRotation(shape)
        if not rot:
            # in case the shape has no normal, like a vertex, just use an empty
            # rotation, which means having the same rotation as the owner part.
            rot = FreeCAD.Rotation()

        hasBound = True
        if not utils.isVertex(shape):
            self.bbox = shape.BoundBox
        else:
            bbox = info.Object.ViewObject.getBoundingBox()
            if bbox.isValid():
                self.bbox = bbox
            else:
                logger.warn('empty bounding box of part {}',info.PartName)
                self.bbox = FreeCAD.BoundBox(0,0,0,5,5,5)
                hasBound = False

        pos = utils.getElementPos(shape)
        if not pos:
            if hasBound:
                pos = self.bbox.Center
            else:
                pos = shape.Placement.Base
        pla = FreeCAD.Placement(pos,rot)

        self.offset = pla.copy()
        self.offsetInv = pla.inverse()
        self.draggerPlacement = info.Placement.multiply(pla)
        self.trace = None
        self.tracePoint = None

    @classmethod
    def onRollback(cls):
        doc = FreeCADGui.editDocument()
        if not doc:
            return
        vobj = doc.getInEdit()
        if vobj and isTypeOf(vobj,ViewProviderAssembly):
            movingPart = getattr(vobj.Proxy,'_movingPart',None)
            if movingPart:
                vobj.Object.recompute(True)
                movingPart.tracePoint = movingPart.TracePosition

    def begin(self):
        self.tracePoint = self.TracePosition

    def update(self):
        info = getElementInfo(self.info.Parent,self.info.SubnameRef)
        self.info = info
        if utils.isDraftObject(info.Part):
            pos = utils.getElementPos(info.Shape)
            rot = utils.getElementRotation(info.Shape)
            pla = info.Placement.multiply(FreeCAD.Placement(pos,rot))
        else:
            pla = info.Placement.multiply(self.offset)
        logger.trace('part move update {}: {}',objName(info.Parent),pla)
        self.draggerPlacement = pla
        return pla

    @property
    def Movement(self):
        pla = self.viewObject.DraggingPlacement.multiply(
                self.draggerPlacement.inverse())
        return utils.roundPlacement(pla)

    @property
    def TracePosition(self):
        pos = gui.AsmCmdTrace.getPosition()
        if pos:
            return pos
        mat = FreeCADGui.editDocument().EditingTransform
        return mat.multiply(self.draggerPlacement.Base)

    def move(self):
        info = self.info
        part = info.Part
        obj = self.assembly.Object
        pla = self.viewObject.DraggingPlacement
        updatePla = True

        rollback = []
        if not info.Subname.startswith('Face') and utils.isDraftWire(part):
            updatePla = False
            if info.Subname.startswith('Vertex'):
                idx = utils.draftWireVertex2PointIndex(part,info.Subname)
                if idx is None:
                    logger.error('Invalid draft wire vertex {} {}',
                        info.Subname, info.PartName)
                    return
                change = [idx]
            else:
                change = utils.edge2VertexIndex(part,info.Subname,True)
                if change[0] is None or change[1] is None:
                    logger.error('Invalid draft wire edge {} {}',
                        info.Subname, info.PartName)
                    return

            movement = self.Movement
            points = part.Points
            for idx in change:
                pt = points[idx]
                rollback.append((info.PartName, part, (idx,pt)))
                points[idx] = movement.multVec(pt)
            part.Points = points

        elif info.Subname.startswith('Vertex') and \
             utils.isDraftCircle(part):
            updatePla = False
            a1 = part.FirstAngle
            a2 = part.LastAngle
            r = part.Radius
            rollback.append((info.PartName, part, (r,a1,a2)))
            pt = info.Placement.inverse().multVec(pla.Base)
            part.Radius = pt.Length
            if a1 != a2:
                pt.z = 0
                a = math.degrees(FreeCAD.Vector(1,0,0).getAngle(pt))
                if info.Subname.endswith('1'):
                    part.FirstAngle = a
                else:
                    part.LastAngle = a

        elif self.fixedTransform:
            fixed = self.fixedTransform
            movement = self.Movement
            if fixed.Shape:
                # fixed position, so reset translation
                movement.Base = FreeCAD.Vector()
                if not utils.isVertex(fixed.Shape):
                    yaw,_,_ = movement.Rotation.toEuler()
                    # when dragging with a fixed axis, we align the dragger Z
                    # axis with that fixed axis. So we shall only keep the yaw
                    # among the euler angles
                    movement.Rotation = FreeCAD.Rotation(yaw,0,0)
                pla = self.draggerPlacement.multiply(movement)

        if updatePla:
            # obtain and update the part placement
            pla = pla.multiply(self.offsetInv)
            setPlacement(info.Part,pla)
            rollback.append((info.PartName,info.Part,info.Placement.copy()))

        if not gui.AsmCmdManager.AutoRecompute or \
           QtGui.QApplication.keyboardModifiers()==QtCore.Qt.ControlModifier:
            # AsmCmdManager.AutoRecompute means auto re-solve the system. The
            # recompute() call below is only for updating linked element and
            # stuff
            obj.recompute(True)
            return

        # calls solver.solve(obj) and redirect all the exceptions message
        # to logger only.
        from . import solver
        if not logger.catch('solver exception when moving part',
               solver.solve, self.objs, dragPart=info.Part, rollback=rollback):
            obj.recompute(True)

        if gui.AsmCmdManager.Trace:
            pos = self.TracePosition
            if not self.tracePoint.isEqual(pos,1e-5):
                try:
                    # check if the object is deleted
                    self.trace.Name
                except Exception:
                    self.trace = None
                if not self.trace:
                    self.trace = FreeCAD.ActiveDocument.addObject(
                        'Part::Polygon','AsmTrace')
                    self.trace.Nodes = [self.tracePoint]
                self.tracePoint = pos
                self.trace.Nodes = {-1:pos}
                self.trace.recompute()

        # self.draggerPlacement, which holds the intended dragger placement, is
        # updated by the above solver call through the following chain, 
        #   solver.solve() -> (triggers dependent objects recompute when done)
        #   Assembly.execute() ->
        #   ViewProviderAssembly.onExecute() -> 
        #   AsmMovingPart.update()
        return self.draggerPlacement

def checkFixedPart(info):
    if not gui.AsmCmdManager.LockMover:
        return
    assembly = resolveAssembly(info.Parent)
    cstrs = assembly.getConstraints()
    partGroup = assembly.getPartGroup()
    if info.Part in Constraint.getFixedParts(None,cstrs,partGroup):
        raise RuntimeError('cannot move fixed part')

def getMovingElementInfo():
    '''Extract information from current selection for part moving

    It returns a tuple containing the selected assembly hierarchy (obtained from
    Assembly.findChildren()), and AsmElementInfo of the selected child part
    object. 
    
    If there is only one selection, then the moving part will be one belong to
    the highest level assembly in selected hierarchy.

    If there are two selections, then one selection must be a parent assembly
    containing the other child object. The moving object will then be the
    immediate child part object of the owner assembly. The actual selected sub
    element, i.e. vertex, edge, face will determine the dragger placement
    '''

    sels = FreeCADGui.Selection.getSelectionEx('',False)
    if not sels:
        raise RuntimeError('no selection')

    if not sels[0].SubElementNames:
        raise RuntimeError('no sub-object in selection')

    if len(sels)>1 or len(sels[0].SubElementNames)>2:
        raise RuntimeError('too many selection')

    selObj = sels[0].Object
    selSub = sels[0].SubElementNames[0]
    ret = Assembly.findChildren(selObj,selSub)
    if not ret:
        raise RuntimeError('invalid selection {}, subname {}'.format(
            objName(selObj),selSub))

    if len(sels[0].SubElementNames)==1:
        info = getElementInfo(ret[0].Assembly,
                ret[0].Subname, checkPlacement=True)
        return MovingPartInfo(SelObj=selObj,
                              SelSubname=selSub,
                              Hierarchy=ret,
                              ElementInfo=info)

    ret2 = Assembly.findChildren(selObj,sels[0].SubElementNames[1])
    if not ret2:
        raise RuntimeError('invalid selection {}, subname {}'.format(
            objName(selObj,sels[0].SubElementNames[1])))

    if len(ret) == len(ret2):
        if len(ret2[-1].Subname) < len(ret[-1].Subname):
            ret,ret2 = ret2,ret
        else:
            selSub = sels[0].SubElementNames[1]
    elif len(ret) > len(ret2):
        ret,ret2 = ret2,ret
    else:
        selSub = sels[0].SubElementNames[1]

    assembly = ret[-1].Assembly
    for r in ret2:
        if assembly == r.Assembly:
            info = getElementInfo(r.Assembly,r.Subname,checkPlacement=True)
            return MovingPartInfo(SelObj=selObj,
                            SelSubname=selSub,
                            Hierarchy=ret2,
                            ElementInfo=info)
    raise RuntimeError('not child parent selection')

def movePart(useCenterballDragger=None,moveInfo=None):
    if not moveInfo:
        moveInfo = logger.catch(
                'exception when moving part', getMovingElementInfo)
        if not moveInfo:
            return False
    info = moveInfo.ElementInfo
    doc = FreeCADGui.editDocument()
    if doc:
        doc.resetEdit()
    vobj = resolveAssembly(info.Parent).Object.ViewObject
    doc = info.Parent.ViewObject.Document
    if useCenterballDragger is not None:
        vobj.UseCenterballDragger = useCenterballDragger
    vobj.Proxy._movingPart = AsmMovingPart(moveInfo.Hierarchy,info)
    FreeCADGui.Selection.clearSelection()
    return doc.setEdit(vobj,1)

class AsmQuickMover:
    def __init__(self, info):
        self.info = info.ElementInfo
        idx = len(self.info.Subname)
        if idx:
            sub = info.SelSubname[:-idx]
        else:
            sub = info.SelSubname
        _,mat = info.SelObj.getSubObject(sub,1,FreeCAD.Matrix())
        pos = utils.getElementPos(self.info.Shape)
        if not pos:
            pos = self.info.Shape.BoundBox.Center
        pos = mat.multiply(pos)
        self.matrix = mat*self.info.Placement.inverse().toMatrix()
        base = self.matrix.multiply(self.info.Placement.Base)
        self.offset = pos - base

        self.matrix.invert()
        self.view = info.SelObj.ViewObject.Document.ActiveView
        self.callbackMove = self.view.addEventCallback(
                "SoLocation2Event",self.moveMouse)
        self.callbackClick = self.view.addEventCallback(
                "SoMouseButtonEvent",self.clickMouse)
        self.callbackKey = self.view.addEventCallback(
                "SoKeyboardEvent",self.keyboardEvent)
        FreeCAD.setActiveTransaction('Assembly quick move')
        self.active = True

    def moveMouse(self, info):
        pla = self.info.Placement
        pos = self.view.getPoint(*info['Position'])
        pla.Base = self.matrix.multiply(pos-self.offset)
        setPlacement(self.info.Part,pla)

    def removeCallbacks(self, abort=False):
        if not self.active:
            return
        self.view.removeEventCallback("SoLocation2Event",self.callbackMove)
        self.view.removeEventCallback("SoMouseButtonEvent",self.callbackClick)
        self.view.removeEventCallback("SoKeyboardEvent",self.callbackKey)
        FreeCAD.closeActiveTransaction(abort)
        self.active = False
        self.info = None
        self.view = None

    def clickMouse(self, info):
        if info['State'] == 'DOWN':
            self.removeCallbacks(info['Button']!='BUTTON1')

    def keyboardEvent(self, info):
        if info['Key'] == 'ESCAPE':
            self.removeCallbacks(True)

class AsmDocumentObserver:
    _quickMover = None

    def __init__(self):
        FreeCAD.addDocumentObserver(self)

    @classmethod
    def closeMover(cls):
        if cls._quickMover:
            cls._quickMover.removeCallbacks(True)
            cls._quickMover = None

    @classmethod
    def quickMove(cls,info):
        cls.closeMover()
        cls._quickMover = AsmQuickMover(info)

    def slotCreatedDocument(self,_doc):
        self.closeMover()

    def slotDeletedDocument(self,_doc):
        self.closeMover()

    def slotUndoDocument(self,_doc):
        self.closeMover()
        AsmMovingPart.onRollback()
        Assembly.cancelAutoSolve()

    def slotRedoDocument(self,_doc):
        self.slotUndoDocument(_doc)

    def slotAbortTransaction(self,_doc):
        self.slotUndoDocument(_doc)

    def slotChangedObject(self,obj,prop):
        Assembly.checkPartChange(obj,prop)

    def slotRecomputedDocument(self,_doc):
        Assembly.resumeSchedule()

    def slotBeforeRecomputeDocument(self,_doc):
        Assembly.pauseSchedule()


def quickMove():
    ret = logger.catch('exception when moving part', getMovingElementInfo)
    if not ret:
        return False
    doc = FreeCADGui.editDocument()
    if doc:
        doc.resetEdit()
    FreeCADGui.Selection.clearSelection()
    FreeCADGui.Selection.addSelection(ret.SelObj,ret.SelSubname)
    AsmDocumentObserver.quickMove(ret)