import os
import FreeCAD
from .deps import with_metaclass
from .constraint import cstrName, PlaneInfo, NormalInfo
from .utils import getIcon, syslogger as logger, objName, project2D, getNormal
from .proxy import ProxyType, PropertyInfo

class System(ProxyType):
    'solver system meta class'

    _typeID = '_SolverType'
    _typeEnum = 'SolverType'
    _propGroup = 'Solver'
    _iconName = 'Assembly_Assembly_Tree.svg'

    @classmethod
    def setDefaultTypeID(mcs,obj,name=None):
        if not name:
            info = mcs.getInfo()
            idx = 1 if len(info.TypeNames)>1 else 0
            name = info.TypeNames[idx]
        super(System,mcs).setDefaultTypeID(obj,name)

    @classmethod
    def getIcon(mcs,obj):
        func = getattr(mcs.getProxy(obj),'getIcon',None)
        if func:
            icon = func(obj)
            if icon:
                return icon
        return getIcon(mcs,mcs.isDisabled(obj))

    @classmethod
    def isDisabled(mcs,obj):
        proxy = mcs.getProxy(obj)
        return not proxy or proxy.isDisabled(obj)

    @classmethod
    def isTouched(mcs,obj):
        proxy = mcs.getProxy(obj)
        return proxy and proxy.isTouched(obj)

    @classmethod
    def touch(mcs,obj,touched=True):
        proxy = mcs.getProxy(obj)
        if proxy:
            proxy.touch(obj,touched)

    @classmethod
    def onChanged(mcs,obj,prop):
        proxy = mcs.getProxy(obj)
        if proxy:
            proxy.onChanged(obj,prop)
        if super(System,mcs).onChanged(obj,prop):
            obj.Proxy.onSolverChanged()

    @classmethod
    def getSystem(mcs,obj):
        proxy = mcs.getProxy(obj)
        if proxy:
            system = proxy.getSystem(obj)
            if isinstance(system,SystemExtension):
                system.relax = obj.AutoRelax
            return system

    @classmethod
    def isConstraintSupported(mcs,obj,name):
        if name == 'Locked':
            return True
        proxy = mcs.getProxy(obj)
        if proxy:
            return proxy.isConstraintSupported(name)

def _makePropInfo(name,tp,doc='',default=None):
    PropertyInfo(System,name,tp,doc,group='Solver',default=default)

_makePropInfo('Verbose','App::PropertyBool')
_makePropInfo('AutoRelax','App::PropertyBool',default=True)

class SystemBase(with_metaclass(System, object)):
    _id = 0
    _props = ['Verbose','AutoRelax']

    def __init__(self,obj):
        self._touched = True
        self.verbose = obj.Verbose
        self.log = logger.info if self.verbose else logger.debug
        super(SystemBase,self).__init__()

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

    @classmethod
    def getName(cls):
        return 'None'

    def isConstraintSupported(self,_cstrName):
        return True

    def isDisabled(self,_obj):
        return True

    def isTouched(self,_obj):
        return getattr(self,'_touched',True)

    def touch(self,_obj,touched=True):
        self._touched = touched

    def onChanged(self,obj,prop):
        if prop == 'Verbose':
            self.verbose = obj.Verbose
            self.log = logger.info if obj.Verbose else logger.debug


class SystemExtension(object):
    def __init__(self):
        super(SystemExtension,self).__init__()
        self.NameTag = ''
        self.sketchPlane = None
        self.cstrObj = None
        self.firstInfo = None
        self.secondInfo = None
        self.relax = False

    def checkRedundancy(self,obj,firstInfo,secondInfo):
        self.cstrObj,self.firstInfo,self.secondInfo=obj,firstInfo,secondInfo

    def addSketchPlane(self,*args,**kargs):
        _ = kargs
        self.sketchPlane = args[0] if args else None
        return self.sketchPlane

    def setOrientation(self,h,lockAngle,yaw,pitch,roll,n1,n2,group):
        if not lockAngle:
            h.append(self.addParallel(n1.entity,n2.entity,group=group))
            return h
        if not yaw and not pitch and not roll:
            n = n2.entity
        else:
            rot = n2.rot.multiply(FreeCAD.Rotation(yaw,pitch,roll))
            e = self.addNormal3dV(*getNormal(rot))
            n = self.addTransform(e,*n2.params,group=group)
        h.append(self.addSameOrientation(n1.entity,n,group=group))
        return h

    def reportRedundancy(self,warn=False):
        msg = '{} between {} and {}'.format(cstrName(self.cstrObj),
                self.firstInfo.PartName, self.secondInfo.PartName)
        if warn:
            logger.warn('skip redundant {}', msg, frame=1)
        else:
            logger.debug('auto relax {}', msg, frame=1)

    def _countConstraints(self,increment,limit,*names):
        first,second = self.firstInfo,self.secondInfo
        if not first or not second:
            return []
        for name in names:
            cstrs = first.CstrMap.get(second.Part,{}).get(name,None)
            if not cstrs:
                if increment:
                    cstrs = second.CstrMap.setdefault(
                                first.Part,{}).setdefault(name,[])
                else:
                    cstrs = second.CstrMap.get(first.Part,{}).get(name,[])
            cstrs += [None]*increment
            count = len(cstrs)
            if limit and count>=limit:
                self.reportRedundancy(count>limit)
        return cstrs

    def countConstraints(self,increment,limit,*names):
        count = len(self._countConstraints(increment,limit,*names))
        if count>limit:
            return -1
        return count

    def addPlaneCoincident(
            self, d, dx, dy, lockAngle, yaw, pitch, roll, pln1, pln2, group=0):
        if not group:
            group = self.GroupHandle
        h = []

        count=self.countConstraints(2 if lockAngle else 1,2,'Coincident')
        if count < 0:
            return

        if d or dx or dy:
            dx,dy,d = pln2.normal.rot.multVec(FreeCAD.Vector(dx,dy,d))
            v = pln2.origin.vector+FreeCAD.Vector(dx,dy,d)
            e = self.addTransform(
                    self.addPoint3dV(*v),*pln2.origin.params,group=group)
        else:
            v = pln2.origin.vector
            e = pln2.origin.entity

        if not lockAngle and count==2:
            # if there is already some other plane coincident constraint set for
            # this pair of parts, we reduce this second constraint to either a
            # points horizontal or vertical constraint, i.e. reduce the
            # constraining DOF down to 1.
            #
            # We project the initial points to the first element plane, and
            # check for differences in x and y components of the points to
            # determine whether to use horizontal or vertical constraint.
            rot = pln1.normal.pla.Rotation.multiply(pln1.normal.rot)
            v1 = pln1.normal.pla.multVec(pln1.origin.vector)
            v2 = pln2.normal.pla.multVec(v)
            v1,v2 = project2D(rot, v1, v2)
            if abs(v1.x-v2.x) < abs(v1.y-v2.y):
                h.append(self.addPointsHorizontal(
                    pln1.origin.entity, e, pln1.entity, group=group))
            else:
                h.append(self.addPointsVertical(
                    pln1.origin.entity, e, pln1.entity, group=group))
            return h

        h.append(self.addPointsCoincident(pln1.origin.entity, e, group=group))

        return self.setOrientation(h, lockAngle, yaw, pitch, roll,
                                   pln1.normal, pln2.normal, group)

    def addPlaneAlignment(self,d,lockAngle,yaw,pitch,roll,pln1,pln2,group=0):
        if not group:
            group = self.GroupHandle
        h = []
        if self.relax:
            dof = 2 if lockAngle else 1
            cstrs = self._countConstraints(dof,3,'Alignment')
            count = len(cstrs)
            if count > 3:
                return
            if count == 1:
                cstrs[0] = pln1.entity
        else:
            count = 0
            cstrs = None

        if d:
            h.append(self.addPointPlaneDistance(
                d, pln2.origin.entity, pln1.entity, group=group))
        else:
            h.append(self.addPointInPlane(
                pln2.origin.entity, pln1.entity,group=group))
        if count<=2:
            n1,n2 = pln1.normal,pln2.normal
            if count==2 and not lockAngle:
                self.reportRedundancy()
                h.append(self.addParallel(n2.entity,n1.entity,cstrs[0],group))
            else:
                self.setOrientation(h,lockAngle,yaw,pitch,roll,n1,n2,group)
        return h

    def addAxialAlignment(self,lockAngle,yaw,pitch,roll,ln1,ln2,group=0):
        if not group:
            group = self.GroupHandle
        h = []
        if not isinstance(ln1,NormalInfo):
            if not isinstance(ln2,NormalInfo):
                lockAngle = False
            else:
                ln1,ln2 = ln2,ln1

        count = self.countConstraints(2 if lockAngle else 1,2,'Axial')
        if count < 0:
            return
        relax = count==2 and not lockAngle
        if isinstance(ln2,NormalInfo):
            ln = ln2.ln
            if not relax:
                h = self.setOrientation(
                        h,lockAngle,yaw,pitch,roll,ln1,ln2,group)
        else:
            ln = ln2.entity
            if not relax:
                h.append(self.addParallel(ln1.entity,ln,group=group))
        h.append(self.addPointOnLine(ln1.p0,ln,group=group))
        return h

    def addMultiParallel(self,lockAngle,yaw,pitch,raw,e1,e2,group=0):
        if not group:
            group = self.GroupHandle
        h = []
        isPlane = isinstance(e1,PlaneInfo),isinstance(e2,PlaneInfo)
        if all(isPlane):
            return self.setOrientation(h, lockAngle, yaw, pitch, raw,
                                       e1.normal, e2.normal, group);
        if not any(isPlane):
            h.append(self.addParallel(e1, e2, group=group))
        elif isPlane[0]:
            h.append(self.addPerpendicular(e1.normal.entity, e2, group=group))
        else:
            h.append(self.addPerpendicular(e1, e2.normal.entity, group=group))
        return h

    def addColinear(self,l1,l2,wrkpln=0,group=0):
        h = []
        if isinstance(l1,NormalInfo):
            pt = l1.p0
            l1 = l1.ln
        else:
            pt = l1.p0
            l1 = l1.entity
        if isinstance(l2,NormalInfo):
            l2 = l2.ln
        else:
            l2 = l2.entity
        h.append(self.addParallel(l1,l2,wrkpln=wrkpln,group=group))
        h.append(self.addPointOnLine(pt,l2,wrkpln=wrkpln,group=group))
        return h

    def addPlacement(self,pla,group=0):
        q = pla.Rotation.Q
        base = pla.Base
        nameTagSave = self.NameTag
        nameTag = nameTagSave+'.' if nameTagSave else 'pla.'
        ret = []
        for n,v in (('x',base.x),('y',base.y),('z',base.z),
                ('qw',q[3]),('qx',q[0]),('qy',q[1]),('qz',q[2])):
            self.NameTag = nameTag+n
            ret.append(self.addParamV(v,group))
        self.NameTag = nameTagSave
        return ret