from future.utils import with_metaclass
from collections import namedtuple
import FreeCAD, FreeCADGui
import asm3
import asm3.utils as utils
from asm3.utils import objName,cstrlogger as logger, guilogger
from asm3.proxy import ProxyType, PropertyInfo, propGet, propGetValue

import os
_iconPath = os.path.join(utils.iconPath,'constraints')

def _p(solver,partInfo,subname,shape):
    'return a handle of a transformed point derived from "shape"'
    if not solver:
        if utils.hasCenter(shape):
            return
        return 'a vertex or circular edge/face'
    key = subname+'.p'
    h = partInfo.EntityMap.get(key,None)
    system = solver.system
    if h:
        system.log('cache {}: {}'.format(key,h))
    else:
        v = utils.getElementPos(shape)
        system.NameTag = subname
        e = system.addPoint3dV(*v)
        system.NameTag = partInfo.PartName
        h = system.addTransform(e,*partInfo.Params,group=partInfo.Group)
        system.log('{}: {},{}'.format(key,h,partInfo.Group))
        partInfo.EntityMap[key] = h
    return h

def _n(solver,partInfo,subname,shape):
    'return a handle of a transformed normal quaterion derived from shape'
    if not solver:
        if utils.isPlanar(shape):
            return
        return 'an edge or face with a surface normal'
    key = subname+'.n'
    h = partInfo.EntityMap.get(key,None)
    system = solver.system
    if h:
        system.log('cache {}: {}'.format(key,h))
    else:
        system.NameTag = subname
        e = system.addNormal3dV(*utils.getElementNormal(shape))
        system.NameTag = partInfo.PartName
        h = system.addTransform(e,*partInfo.Params,group=partInfo.Group)
        system.log('{}: {},{}'.format(key,h,partInfo.Group))
        partInfo.EntityMap[key] = h
    return h

def _l(solver,partInfo,subname,shape,retAll=False):
    'return a pair of handle of the end points of an edge in "shape"'
    if not solver:
        if utils.isLinearEdge(shape):
            return
        return 'a linear edge'
    key = subname+'.l'
    h = partInfo.EntityMap.get(key,None)
    system = solver.system
    if h:
        system.log('cache {}: {}'.format(key,h))
    else:
        system.NameTag = subname
        v = shape.Edges[0].Vertexes
        p1 = system.addPoint3dV(*v[0].Point)
        p2 = system.addPoint3dV(*v[-1].Point)
        system.NameTag = partInfo.PartName
        tp1 = system.addTransform(p1,*partInfo.Params,group=partInfo.Group)
        tp2 = system.addTransform(p2,*partInfo.Params,group=partInfo.Group)
        h = system.addLineSegment(tp1,tp2,group=partInfo.Group)
        h = (h,tp1,tp2,p1,p2)
        system.log('{}: {},{}'.format(key,h,partInfo.Group))
        partInfo.EntityMap[key] = h
    return h if retAll else h[0]

def _ln(solver,partInfo,subname,shape,retAll=False):
    'return a handle for either a line or a normal depends on the shape'
    if not solver:
        if utils.isLinearEdge(shape) or utils.isPlanar(shape):
            return
        return 'a linear edge or edge/face with planar surface'
    if utils.isLinearEdge(shape):
        return _l(solver,partInfo,subname,shape,retAll)
    return _n(solver,partInfo,subname,shape)

def _w(solver,partInfo,subname,shape,retAll=False):
    'return a handle of a transformed plane/workplane from "shape"'
    if not solver:
        if utils.isPlanar(shape):
            return
        return 'an edge/face with a planar surface'

    key = subname+'.w'
    h = partInfo.EntityMap.get(key,None)
    system = solver.system
    if h:
        system.log('cache {}: {}'.format(key,h))
    else:
        p = _p(solver,partInfo,subname,shape)
        n = _n(solver,partInfo,subname,shape)
        system.NameTag = partInfo.PartName
        h = system.addWorkplane(p,n,group=partInfo.Group)
        h = (h,p,n)
        system.log('{}: {},{}'.format(key,h,partInfo.Group))
        partInfo.EntityMap[key] = h
    return h if retAll else h[0]

def _wa(solver,partInfo,subname,shape):
    return _w(solver,partInfo,subname,shape,True)

def _c(solver,partInfo,subname,shape,requireArc=False):
    'return a handle of a transformed circle/arc derived from "shape"'
    if not solver:
        r = utils.getElementCircular(shape)
        if not r or (requireArc and not isinstance(r,list,tuple)):
            return
        return 'an cicular arc edge' if requireArc else 'a circular edge'
    key = subname+'.c'
    h = partInfo.EntityMap.get(key,None)
    system = solver.system
    if h:
        system.log('cache {}: {}'.format(key,h))
    else:
        h = [_w(solver,partInfo,subname,shape,False)]
        r = utils.getElementCircular(shape)
        if not r:
            raise RuntimeError('shape is not cicular')
        if isinstance(r,(list,tuple)):
            l = _l(solver,partInfo,subname,shape,True)
            h += l[1:]
            system.NameTag = partInfo.PartName
            h = system.addArcOfCircleV(*h,group=partInfo.Group)
        elif requireArc:
            raise RuntimeError('shape is not an arc')
        else:
            system.NameTag = partInfo.PartName
            h.append(solver.addDistanceV(r))
            h = system.addCircle(*h,group=partInfo.Group)
        system.log('{}: {},{}'.format(key,h,partInfo.Group))
        partInfo.EntityMap[key] = h
    return h

def _a(solver,partInfo,subname,shape):
    return _c(solver,partInfo,subname,shape,True)


class ConstraintCommand:
    _toolbarName = 'Assembly3 Constraints'
    _menuGroupName = ''

    def __init__(self,tp):
        self.tp = tp
        self._id = 100 + tp._id
        self._active = None

    def workbenchActivated(self):
        pass

    def workbenchDeactivated(self):
        self._active = None

    def getContextMenuName(self):
        pass

    def getName(self):
        return 'asm3Add'+self.tp.getName()

    def GetResources(self):
        return self.tp.GetResources()

    def Activated(self):
        guilogger.report('constraint "{}" command exception'.format(
            self.tp.getName()), asm3.assembly.AsmConstraint.make,self.tp._id)

    def IsActive(self):
        if not FreeCAD.ActiveDocument:
            return False
        if self._active is None:
            self.checkActive()
        return self._active

    def checkActive(self):
        from asm3.assembly import AsmConstraint
        if guilogger.catchTrace('selection "{}" exception'.format(
                self.tp.getName()), AsmConstraint.getSelection, self.tp._id):
            self._active = True
        else:
            self._active = False

    def onClearSelection(self):
        self._active = False

class Constraint(ProxyType):
    'constraint meta class'

    _typeID = '_ConstraintType'
    _typeEnum = 'ConstraintType'
    _disabled = 'Disabled'

    @classmethod
    def register(mcs,cls):
        super(Constraint,mcs).register(cls)
        if cls._id>=0 and cls._menuItem:
            asm3.gui.AsmCmdManager.register(ConstraintCommand(cls))

    @classmethod
    def attach(mcs,obj,checkType=True):
        if checkType:
            if not mcs._disabled in obj.PropertiesList:
                obj.addProperty("App::PropertyBool",mcs._disabled,"Base",'')
        return super(Constraint,mcs).attach(obj,checkType)

    @classmethod
    def onChanged(mcs,obj,prop):
        if prop == mcs._disabled:
            obj.ViewObject.signalChangeIcon()
            return
        return super(Constraint,mcs).onChanged(obj,prop)

    @classmethod
    def isDisabled(mcs,obj):
        return getattr(obj,mcs._disabled,False)

    @classmethod
    def check(mcs,tp,group,checkCount=False):
        mcs.getType(tp).check(group,checkCount)

    @classmethod
    def prepare(mcs,obj,solver):
        return mcs.getProxy(obj).prepare(obj,solver)

    @classmethod
    def getFixedParts(mcs,cstrs):
        firstPart = None
        firstPartName = None
        found = False
        ret = set()
        for obj in cstrs:
            cstr = mcs.getProxy(obj)
            if cstr.hasFixedPart(obj):
                found = True
                for info in cstr.getFixedParts(obj):
                    logger.debug('fixed part ' + info.PartName)
                    ret.add(info.Part)

            if not found and not firstPart:
                elements = obj.Proxy.getElements()
                if elements:
                    info = elements[0].Proxy.getInfo()
                    firstPart = info.Part
                    firstPartName = info.PartName

        if not found:
            if not firstPart:
                return None
            logger.debug('lock first part {}'.format(firstPartName))
            ret.add(firstPart)
        return ret

    @classmethod
    def getFixedTransform(mcs,cstrs):
        firstPart = None
        found = False
        ret = {}
        for obj in cstrs:
            cstr = mcs.getProxy(obj)
            if cstr.hasFixedPart(obj):
                for info in cstr.getFixedTransform(obj):
                    found = True
                    ret[info.Part] = info

            if not found and not firstPart:
                elements = obj.Proxy.getElements()
                if elements:
                    info = elements[0].Proxy.getInfo()
                    firstPart = info.Part
        if not found and firstPart:
            ret[firstPart] = False
        return ret

    @classmethod
    def getIcon(mcs,obj):
        cstr = mcs.getProxy(obj)
        if cstr:
            return cstr.getIcon(obj)


def _makeProp(name,tp,doc='',getter=propGet,internal=False):
    PropertyInfo(Constraint,name,tp,doc,getter=getter,
            group='Constraint',internal=internal)

_makeProp('Distance','App::PropertyDistance',getter=propGetValue)
_makeProp('Offset','App::PropertyDistance',getter=propGetValue)
_makeProp('Cascade','App::PropertyBool',internal=True)
_makeProp('Angle','App::PropertyAngle',getter=propGetValue)
_makeProp('Ratio','App::PropertyFloat')
_makeProp('Difference','App::PropertyFloat')
_makeProp('Diameter','App::PropertyFloat')
_makeProp('Radius','App::PropertyFloat')
_makeProp('Supplement','App::PropertyBool',
        'If True, then the second angle is calculated as 180-angle')
_makeProp('AtEnd','App::PropertyBool',
        'If True, then tangent at the end point, or else at the start point')

_ordinal = ('1st', '2nd', '3rd', '4th', '5th', '6th', '7th')

def cstrName(obj):
    return '{}<{}>'.format(objName(obj),Constraint.getTypeName(obj))


class Base(with_metaclass(Constraint,object)):
    _id = -1
    _entityDef = ()
    _workplane = False
    _props = []
    _iconName = 'Assembly_ConstraintGeneral.svg'

    _menuText = 'Create "{}" constraint'
    _menuItem = False

    def __init__(self,_obj):
        pass

    @classmethod
    def getPropertyInfoList(cls):
        return cls._props

    @classmethod
    def constraintFunc(cls,obj,solver):
        try:
            return getattr(solver.system,'add'+cls.getName())
        except AttributeError:
            logger.warn('{} not supported in solver "{}"'.format(
                cstrName(obj),solver.getName()))

    @classmethod
    def getEntityDef(cls,group,checkCount,obj=None):
        entities = cls._entityDef
        if len(group) == len(entities):
            return entities
        if cls._workplane and len(group)==len(entities)+1:
            return list(entities) + [_w]
        if not checkCount and len(group)<len(entities):
            return entities[:len(group)]
        if not obj:
            name = cls.getName()
        else:
            name += cstrName(obj)
        if len(group)<len(entities):
            msg = entities[len(group)](None,None,None,None)
            raise RuntimeError('Constraint {} expects a {} element of '
                '{}'.format(name,_ordinal[len(group)],msg))
        raise RuntimeError('Constraint {} has too many elements, expecting '
            'only {}'.format(name,len(entities)))

    @classmethod
    def check(cls,group,checkCount=False):
        entities = cls.getEntityDef(group,checkCount)
        for i,e in enumerate(entities):
            o = group[i]
            msg = e(None,None,None,o)
            if not msg:
                continue
            if i == len(cls._entityDef):
                raise RuntimeError('Constraint "{}" requires an optional {} '
                    'element to be a planar face for defining a '
                    'workplane'.format(cls.getName(), _ordinal[i], msg))
            raise RuntimeError('Constraint "{}" requires the {} element to be'
                    ' {}'.format(cls.getName(), _ordinal[i], msg))

    @classmethod
    def getIcon(cls,obj):
        return utils.getIcon(cls,Constraint.isDisabled(obj),_iconPath)

    @classmethod
    def getEntities(cls,obj,solver):
        '''maps fcad element shape to entities'''
        elements = obj.Proxy.getElements()
        entities = cls.getEntityDef(elements,True,obj)
        ret = []
        for e,o in zip(entities,elements):
            info = o.Proxy.getInfo()
            partInfo = solver.getPartInfo(info)
            ret.append(e(solver,partInfo,info.Subname,info.Shape))
        logger.debug('{} entities: {}'.format(cstrName(obj),ret))
        return ret

    @classmethod
    def prepare(cls,obj,solver):
        func = cls.constraintFunc(obj,solver)
        if func:
            params = cls.getPropertyValues(obj) + cls.getEntities(obj,solver)
            return func(*params,group=solver.group)
        else:
            logger.warn('{} no constraint func'.format(cstrName(obj)))

    @classmethod
    def hasFixedPart(cls,_obj):
        return False

    @classmethod
    def getMenuText(cls):
        return cls._menuText.format(cls.getName())

    @classmethod
    def getToolTip(cls):
        tooltip = getattr(cls,'_tooltip',None)
        if not tooltip:
            return cls.getMenuText()
        return tooltip.format(cls.getName())

    @classmethod
    def GetResources(cls):
        return {'Pixmap':utils.addIconToFCAD(cls._iconName,_iconPath),
                'MenuText':cls.getMenuText(),
                'ToolTip':cls.getToolTip()}


class Locked(Base):
    _id = 0
    _iconName = 'Assembly_ConstraintLock.svg'
    _menuItem = True
    _tooltip = 'Add a "{}" constraint to fix part(s)'

    @classmethod
    def getFixedParts(cls,obj):
        ret = []
        for e in obj.Proxy.getElements():
            info = e.Proxy.getInfo()
            if not utils.isVertex(info.Shape) and \
               not utils.isLinearEdge(info.Shape):
                ret.append(info)
        return ret

    Info = namedtuple('AsmCstrTransformInfo', ('Part', 'Shape'))

    @classmethod
    def getFixedTransform(cls,obj):
        ret = []
        for e in obj.Proxy.getElements():
            info = e.Proxy.getInfo()
            if not utils.isVertex(info.Shape) and \
               not utils.isLinearEdge(info.Shape):
                ret.append(cls.Info(Part=info.Part,Shape=None))
                continue
            ret.append(cls.Info(Part=info.Part,Shape=info.Shape))
        return ret

    @classmethod
    def hasFixedPart(cls,obj):
        return len(obj.Proxy.getElements())>0

    @classmethod
    def prepare(cls,obj,solver):
        ret = []
        for element in obj.Proxy.getElements():
            info = element.Proxy.getInfo()
            if not utils.isVertex(info.Shape) and \
               not utils.isLinearEdge(info.Shape):
                continue
            if solver.isFixedPart(info):
                logger.warn('redundant locking element "{}" in constraint '
                        '{}'.format(info.Subname,objName(obj)))
                continue
            partInfo = solver.getPartInfo(info)
            system = solver.system
            for i,v in enumerate(info.Shape.Vertexes):
                subname = info.Subname+'.'+str(i)
                system.NameTag = subname + '.tp'
                e1 = system.addPoint3dV(*info.Placement.multVec(v.Point))
                e2 = _p(solver,partInfo,subname,v)
                if i==0:
                    e0 = e1
                    ret.append(system.addPointsCoincident(
                        e1,e2,group=solver.group))
                else:
                    system.NameTag = info.Subname + 'tl'
                    l = system.addLineSegment(e0,e1)
                    ret.append(system.addPointOnLine(e2,l,group=solver.group))

        return ret

    @classmethod
    def check(cls,group,_checkCount=False):
        if not all([utils.isElement(o) for o in group]):
            raise RuntimeError('Constraint "{}" requires all children to be '
                    'of element (Vertex, Edge or Face)'.format(cls.getName()))


class BaseMulti(Base):
    _id = -1
    _entityDef = (_wa,)

    @classmethod
    def check(cls,group,_checkCount=False):
        if len(group)<2:
            raise RuntimeError('Constraint "{}" requires at least two '
                'elements'.format(cls.getName()))
        for o in group:
            msg = cls._entityDef[0](None,None,None,o)
            if msg:
                raise RuntimeError('Constraint "{}" requires all the element '
                    'to be of {}'.format(cls.getName()))
        return

    @classmethod
    def prepare(cls,obj,solver):
        func = cls.constraintFunc(obj,solver);
        if not func:
            logger.warn('{} no constraint func'.format(cstrName(obj)))
            return
        parts = set()
        ref = None
        elements = []
        props = cls.getPropertyValues(obj)

        for e in obj.Proxy.getElements():
            info = e.Proxy.getInfo()
            if info.Part in parts:
                logger.warn('{} skip duplicate parts {}'.format(
                    cstrName(obj),info.PartName))
                continue
            parts.add(info.Part)
            if solver.isFixedPart(info):
                if ref:
                    logger.warn('{} skip more than one fixed part {}'.format(
                        cstrName(obj),info.PartName))
                    continue
                ref = info
                elements.insert(0,e)
            else:
                elements.append(e)
        if len(elements)<=1:
            logger.warn('{} has no effective constraint'.format(cstrName(obj)))
            return
        e0 = None
        ret = []
        for e in elements:
            info = e.Proxy.getInfo()
            partInfo = solver.getPartInfo(info)
            if not e0:
                e0 = cls._entityDef[0](solver,partInfo,info.Subname,info.Shape)
            else:
                e = cls._entityDef[0](solver,partInfo,info.Subname,info.Shape)
                params = props + [e0,e]
                h = func(*params,group=solver.group)
                if isinstance(h,(list,tuple)):
                    ret += list(h)
                else:
                    ret.append(h)
        return ret


class BaseCascade(BaseMulti):
    @classmethod
    def prepare(cls,obj,solver):
        if not getattr(obj,'Cascade',True):
            return super(BaseCascade,cls).prepare(obj,solver)
        func = cls.constraintFunc(obj,solver);
        if not func:
            logger.warn('{} no constraint func'.format(cstrName(obj)))
            return
        props = cls.getPropertyValues(obj)
        prev = None
        ret = []
        for e in obj.Proxy.getElements():
            info = e.Proxy.getInfo()
            if not prev or prev.Part==info.Part:
                prev = info
                continue
            prevInfo = solver.getPartInfo(prev)
            e1 = cls._entityDef[0](solver,prevInfo,prev.Subname,prev.Shape)
            partInfo = solver.getPartInfo(info)
            e2 = cls._entityDef[0](solver,partInfo,info.Subname,info.Shape)
            prev = info
            if solver.isFixedPart(info):
                params = props + [e1,e2]
            else:
                params = props + [e2,e1]
            h = func(*params,group=solver.group)
            if isinstance(h,(list,tuple)):
                ret += list(h)
            else:
                ret.append(h)

        if not ret:
            logger.warn('{} has no effective constraint'.format(cstrName(obj)))
        return ret


class PlaneCoincident(BaseCascade):
    _id = 35
    _iconName = 'Assembly_ConstraintCoincidence.svg'
    _props = ['Cascade','Offset']
    _menuItem = True
    _tooltip = \
        'Add a "{}" constraint to conincide planes of two or more parts.\n'\
        'The planes are coincided at their centers with an optional distance.'


class PlaneAlignment(BaseCascade):
    _id = 37
    _iconName = 'Assembly_ConstraintAlignment.svg'
    _props = ['Cascade','Offset']
    _menuItem = True
    _tooltip = 'Add a "{}" constraint to rotate planes of two or more parts\n'\
               'into the same orientation'


class AxialAlignment(BaseMulti):
    _id = 36
    _iconName = 'Assembly_ConstraintAxial.svg'
    _menuItem = True
    _tooltip = 'Add a "{}" constraint to align planes of two or more parts.\n'\
        'The planes are aligned at the direction of their surface normal axis.'


class SameOrientation(BaseMulti):
    _id = 2
    _entityDef = (_n,)
    _iconName = 'Assembly_ConstraintOrientation.svg'
    _menuItem = True
    _tooltip = 'Add a "{}" constraint to align planes of two or more parts.\n'\
        'The planes are aligned to have the same orientation (i.e. rotation)'


class Angle(Base):
    _id = 27
    _entityDef = (_ln,_ln)
    _workplane = True
    _props = ["Angle","Supplement"]
    _iconName = 'Assembly_ConstraintAngle.svg'
    _menuItem = True
    _tooltip = 'Add a "{}" constraint to set the angle of planes or linear\n'\
               'edges of two parts.'


class Perpendicular(Base):
    _id = 28
    _entityDef = (_ln,_ln)
    _workplane = True
    _iconName = 'Assembly_ConstraintPerpendicular.svg'
    _menuItem = True
    _tooltip = 'Add a "{}" constraint to make planes or linear edges of two\n'\
               'parts perpendicular.'


class Parallel(Base):
    _id = -1
    _entityDef = (_ln,_ln)
    _workplane = True
    _iconName = 'Assembly_ConstraintParallel.svg'
    _menuItem = True
    _tooltip = 'Add a "{}" constraint to make planes or linear edges of two\n'\
               'parts parallel.'


class MultiParallel(BaseMulti):
    _id = 291
    _entityDef = (_ln,)
    _iconName = 'Assembly_ConstraintMultiParallel.svg'
    _menuItem = True
    _tooltip = 'Add a "{}" constraint to make planes or linear edges of two\n'\
               'or more parts parallel.'


class PointsCoincident(Base):
    _id = 1
    _entityDef = (_p,_p)
    _workplane = True


class PointInPlane(Base):
    _id = 3
    _entityDef = (_p,_w)


class PointOnLine(Base):
    _id = 4
    _entityDef = (_p,_l)
    _workplane = True


class PointsDistance(Base):
    _id = 5
    _entityDef = (_p,_p)
    _workplane = True
    _props = ["Distance"]


class PointsProjectDistance(Base):
    _id = 6
    _entityDef = (_p,_p,_l)
    _props = ["Distance"]


class PointPlaneDistance(Base):
    _id = 7
    _entityDef = (_p,_w)
    _props = ["Distance"]


class PointLineDistance(Base):
    _id = 8
    _entityDef = (_p,_l)
    _workplane = True
    _props = ["Distance"]


class EqualLength(Base):
    _id = 9
    _entityDef = (_l,_l)
    _workplane = True


class LengthRatio(Base):
    _id = 10
    _entityDef = (_l,_l)
    _workplane = True
    _props = ["Ratio"]


class LengthDifference(Base):
    _id = 11
    _entityDef = (_l,_l)
    _workplane = True
    _props = ["Difference"]


class EqualLengthPointLineDistance(Base):
    _id = 12
    _entityDef = (_p,_l,_l)
    _workplane = True


class EqualPointLineDistance(Base):
    _id = 13
    _entityDef = (_p,_l,_p,_l)
    _workplane = True


class EqualAngle(Base):
    _id = 14
    _entityDef = (_l,_l,_l,_l)
    _workplane = True
    _props = ["Supplement"]


class EqualLineArcLength(Base):
    _id = 15
    _entityDef = (_l,_a)
    _workplane = True


class Symmetric(Base):
    _id = 16
    _entityDef = (_p,_p,_w)
    _workplane = True


class SymmetricHorizontal(Base):
    _id = 17
    _entityDef = (_p,_p,_w)


class SymmetricVertical(Base):
    _id = 18
    _entityDef = (_p,_p,_w)


class SymmetricLine(Base):
    _id = 19
    _entityDef = (_p,_p,_l,_w)


class MidPoint(Base):
    _id = 20
    _entityDef = (_p,_p,_l)
    _workplane = True


class PointsHorizontal(Base):
    _id = 21
    _entityDef = (_p,_p,_w)


class PointsVertical(Base):
    _id = 22
    _entityDef = (_p,_p,_w)


class LineHorizontal(Base):
    _id = 23
    _entityDef = (_l,_w)


class LineVertical(Base):
    _id = 24
    _entityDef = (_l,_w)


class Diameter(Base):
    _id = 25
    _entityDef = (_c,)
    _prop = ("Diameter",)


class PointOnCircle(Base):
    _id = 26
    _entityDef = (_p,_c)


class ArcLineTangent(Base):
    _id = 30
    _entityDef = (_c,_l)
    _props = ["AtEnd"]


#  class CubicLineTangent(Base):
#      _id = 31
#
#
#  class CurvesTangent(Base):
#      _id = 32


class EqualRadius(Base):
    _id = 33
    _entityDef = (_c,_c)


class WhereDragged(Base):
    _id = 34
    _entityDef = (_p,)
    _workplane = True