from maya import cmds, OpenMaya

from .utils import (
    attribute,
    curve, 
    cluster, 
    undo, 
    math, 
    colour, 
    control, 
    controlShape,
    motionPath
)

from .settings import (
    Settings
)


# ----------------------------------------------------------------------------


MATRIX_PLUGIN = "matrixNodes.mll"


# ----------------------------------------------------------------------------


class SplineIK(Settings):
    """
    The Spline IK module works on a curve and generates an joint chain
    that sticks to it's position on the curve. This means that stretch
    and squash will only occur in the areas that manipulates as opposed
    to it scaling as a whole.

    The other benefit of using this module over a regular spline IK is
    the fact that the twist is divided over the controls that are
    generated and not just limited to the beginning and end.

    Once the class is initialized the user can change attributes that
    are defined in the :class:`rjSplineIK.settings.Settings` class that 
    gets inherited.

    Once the user parameters are set the :func:`SplineIK.create` 
    can be ran.
    """
    def __init__(self):
        Settings.__init__(self)
        
        # variables
        self._name = None
        self._curve = None
        
        # control variables
        self._controls = []
        self._rootControl = None
        self._tangentControls = None
        
        # slide control variables
        self._slideControl = None
        self._slideMinControl = None
        self._slideMaxControl = None
        
        # joints variables
        self._joints = []
        self._rootJoint = None
        
        # load matrix nodes plugin
        if not cmds.pluginInfo(MATRIX_PLUGIN, query=True, loaded=True):
            cmds.loadPlugin(MATRIX_PLUGIN)
        
    # ------------------------------------------------------------------------

    @property
    def name(self):
        """
        :return: name to use while creating the spline ik
        :rtype: str
        """
        return self._name

    @name.setter
    def name(self, name):
        self._name = name

    # ------------------------------------------------------------------------

    @property
    def curve(self):
        """
        :return: name of the curve to attach the ik to
        :rtype: str
        """
        return self._curve

    @curve.setter
    def curve(self, curve):
        self._curve = curve

    @property
    def curveShape(self):
        """
        :return: first shape of the curve
        :rtype: str
        """
        return cmds.listRelatives(self.curve, s=True)[0]
        
    # ------------------------------------------------------------------------
        
    @property
    def rootControl(self):
        """
        :return: name of root control
        :rtype: str
        """
        return self._rootControl

    @property
    def controls(self):
        """
        :return: list of all tweak controls
        :rtype: list
        """
        return self._controls

    @property
    def tangentControls(self):
        """
        :return: list of all tangent controls
        :rtype: list
        """
        return self._tangentControls

    # --------------------------------------------------------------------

    @property
    def slideControl(self):
        """
        :return: name of slide control
        :rtype: str
        """
        return self._slideControl

    @property
    def slideMinControl(self):
        """
        :return: name of slide control ( min )
        :rtype: str
        """
        return self._slideMinControl

    @property
    def slideMaxControl(self):
        """
        :return: name of slide control ( max )
        :rtype: str
        """
        return self._slideMaxControl

    # --------------------------------------------------------------------

    @property
    def rootJoint(self):
        """
        :return: name of root joint
        :rtype: str
        """
        return self._rootJoint

    @property
    def joints(self):
        """
        :return: list of joints that are attached to the curve
        :rtype: list
        """
        return self._joints
        
    # ------------------------------------------------------------------------
    
    def __orientControl(self, offset):
        # get position
        pos = cmds.xform(offset, q=True, ws=True, t=True)

        # get closest parameter on curve
        parameter, _ = curve.nearestPointOnCurve(self.curveShape, pos)

        # get tangent
        forward = cmds.pointOnCurve(
            self.curveShape,
            pr=parameter,
            normalizedTangent=True
        )

        # get up
        forward = OpenMaya.MVector(*forward)
        up = OpenMaya.MVector(*self.upVector)

        right = up^forward
        right.normalize()

        up = right^forward
        up.normalize()

        orient = up*OpenMaya.MVector(*self.upVector)
        if orient <= 0:
            up = up * -1

        # construct quaternion
        quaternion = math.lookRotation(up, forward)

        # convert to euler
        euler = quaternion.asEulerRotation()

        rot = [
            math.degrees(euler.x),
            math.degrees(euler.y),
            math.degrees(euler.z),
        ]

        # set euler
        cmds.xform(offset, ws=True, ro=rot)
    
    def __createControl(self, cls, shape, clr, i=None, suffix=""):
        # create root control
        offset, ctrl = control.createControlShape(
            "{0}{1}".format(self.name, suffix),
            shape,
            clr,
            i
        )
        
        # position control
        pos = cluster.getClusterPosition(cls)
        cmds.setAttr("{0}.translate".format(offset), *pos)

        # parent cluster
        cmds.parent(cls, ctrl)

        return offset, ctrl
        
    # ------------------------------------------------------------------------

    def __createTangentControl(self, cls, i, side, parent):
        # create tangent control
        ctrlOffset, ctrl = self.__createControl(
            cls,
            self.tangentControlShape,
            self.tangentControlColour,
            i,
            side,
        )

        # connect vis
        cmds.connectAttr(
            "{0}.tangent_vis".format(parent),
            "{0}.visibility".format(ctrlOffset),
        )

        # create line
        name = ctrl.replace("ctrl", "line")
        line, _ = curve.createCurveShape(name, [(0,0,0),(0,0,0)])

        cmds.setAttr("{0}.overrideEnabled".format(line), 1)
        cmds.setAttr("{0}.overrideDisplayType".format(line), 1)
        cmds.setAttr("{0}.inheritsTransform".format(line), 0)

        cmds.parent(line, ctrl, relative=True)

        # connect line
        for i, driver in enumerate([ctrl, parent]):
            dm = cmds.createNode(
                "decomposeMatrix",
                n="{0}_dm_{1:03d}".format(name, i+1)
            )

            cmds.connectAttr(
                "{0}.worldMatrix".format(driver),
                "{0}.inputMatrix".format(dm)
            )
            cmds.connectAttr(
                "{0}.outputTranslate".format(dm),
                "{0}.controlPoints[{1}]".format(line, i)
            )

        return ctrlOffset, ctrl
        
    # ------------------------------------------------------------------------

    def __createControls(self):
        # create root control
        rootOffset, root = control.createControlShape(
            "{0}_root".format(self.name),
            self.rootControlShape,
            self.rootControlColour
        )

        # position root control
        pos = cluster.getClusterPosition(self.controlClusters[0])
        cmds.setAttr("{0}.translate".format(rootOffset), *pos)
        
        # orient root controls
        if self.orientRootToCurve:
            self.__orientControl(rootOffset)
        
        # create controls
        controls = []
        tangentControls = []
        
        for i, cls in enumerate(self.controlClusters):
            # before and after
            before = i*3-1
            after = i*3+1
            
            # create control
            ctrlOffset, ctrl = self.__createControl(
                cls,
                self.controlShape,
                self.controlColour,
                i,
            )
            
            # add to list
            controls.append(ctrl)
            
            # add tangent vis attribute
            attribute.addSpacerAttr(ctrl)
            attribute.addAttr(
                ctrl, 
                "tangent_vis", 
                at="long", 
                minValue=0, 
                maxValue=1
            )
            
            # orient root controls
            if self.orientToCurve:
                self.__orientControl(ctrlOffset)
                
            # create read group
            grp = cmds.group(
                world=True,
                empty=True,
                n="{0}_read_{1:03d}".format(self.name, i+1)
            )
            
            pos = cluster.getClusterPosition(cls)
            cmds.setAttr("{0}.translate".format(grp), *pos)
            
            # parent control
            cmds.parent(grp, ctrl)
            cmds.parent(ctrlOffset, root)  

            # create tangent controls
            for side, j, rot in zip(["a", "b"], [before, after], [180, 0]):
                if j <= 0 or j >= len(self.clusters):
                    continue
 
                # create tangent control
                tCtrlOffset, tCtrl = self.__createTangentControl(
                    self.clusters[j],
                    i,
                    "_{0}".format(side),
                    ctrl
                )
                
                # parent tangent control
                cmds.parent(tCtrlOffset, ctrl)
                
                # rotate tangent control
                rotate = [a*rot for a in self.aimVector]
                cmds.setAttr("{0}.rotate".format(tCtrlOffset), *rotate)
                
                # add to list
                tangentControls.append(tCtrl)

        return root, controls, tangentControls
        
    # ------------------------------------------------------------------------

    def __getParameters(self):
        # cluster parameters
        num = len(self.controlClusters)
        p1 = curve.splitCurveToParametersByParameter(
            self.curveShape, 
            num
        )

        # locator parameters
        p2 = curve.splitCurveToParametersByLength(
            self.curveShape,
            self.numJoints
        )

        return p1, p2

    def __getWeighting(self):
        return math.remapWeighting(
            self.jParameters,
            self.cParameters
        )
        
    # ------------------------------------------------------------------------
        
    def __createUpVectors(self):
        # variables
        ups = []
        blends = []

        # loop weights
        for i, weight in enumerate(self.weights):
            # create blend matrix
            bm = cmds.createNode(
                "wtAddMatrix",
                n="{0}_bm_{1:03d}".format(self.name, i+1)
            )
            
            # blend cluster weights
            for j, k in enumerate(weight.keys()):
                # get control
                control = self.controls[k]

                # get read group
                children = cmds.listRelatives(control, c=True, f=True)
                group = [c for c in children if c.count("_read_")][0]
                
                # set blend weight
                cmds.setAttr(
                    "{0}.wtMatrix[{1}].weightIn".format(bm, j), 
                    weight[k]
                )
                
                # connect to control
                cmds.connectAttr(
                    "{0}.worldMatrix[0]".format(group), 
                    "{0}.wtMatrix[{1}].matrixIn".format(bm, j)
                )

            # multiply up vector
            pmm = cmds.createNode(
                "pointMatrixMult",
                n="{0}_up_pmm_{1:03d}".format(self.name, i+1)
            )
            
            cmds.setAttr("{0}.vectorMultiply".format(pmm), 1)
            cmds.setAttr("{0}.inPoint{1}".format(pmm, self.upDirection.upper()), 100)
            cmds.connectAttr(
                "{0}.matrixSum".format(bm),
                "{0}.inMatrix".format(pmm),
            )

            # decompose blend matrix
            dm = cmds.createNode(
                "decomposeMatrix",
                n="{0}_up_dm_{1:03d}".format(self.name, i+1)
            )
            
            cmds.connectAttr(
                "{0}.matrixSum".format(bm),
                "{0}.inputMatrix".format(dm),
            )

            # add up with blend
            pma = cmds.createNode(
                "plusMinusAverage",
                n="{0}_up_pma_{1:03d}".format(self.name, i+1)
            )
            
            cmds.connectAttr(
                "{0}.output".format(pmm),
                "{0}.input3D[0]".format(pma),
            )
            
            cmds.connectAttr(
                "{0}.outputTranslate".format(dm),
                "{0}.input3D[1]".format(pma),
            )
            
            # store nodes
            blends.append(bm)
            ups.append(pma)

        return blends, ups
        
    # ------------------------------------------------------------------------
        
    def __createPointOnCurves(self):
        pocs = []
        aims = []

        for i, parameter in enumerate(self.jParameters):
            # create follicle
            loc, poc, aim = curve.createFollicle(
                "{0}_{1:03d}".format(self.name, i + 1),
                self.curve,
                parameter=parameter,
                upDirection=self.upDirection,
                forwardDirection=self.forwardDirection,
                overrideNormal="{0}.output3D".format(self.ups[i]),
                subtractPositionFromNormal=True
            )

            aims.append(aim)
            pocs.append(poc)

            cmds.parent(aim, world=True)

            # remove locator, will be replaced with joint later
            cmds.delete(loc)

        return pocs, aims
        
    # ------------------------------------------------------------------------
        
    def __createJoints(self):
        # variables
        joints = []
        
        # clear selection
        cmds.select(clear=True)

        # create root joint
        root = cmds.joint(n="{0}_root_jnt".format(self.name))
        cmds.setAttr("{0}.drawStyle".format(root), 2)

        # position root joint
        pos = cmds.getAttr("{0}.result.position".format(self.pointOnCurves[0]))
        cmds.setAttr("{0}.translate".format(root), *pos[0])

        # create curve joints
        for i, _ in enumerate(self.pointOnCurves):
            cmds.select(root)

            jnt = cmds.joint(n="{0}_jnt_{1:03d}".format(self.name, i + 1))
            #cmds.setAttr("{0}.displayLocalAxis".format(jnt), 1)
            cmds.setAttr("{0}.inheritsTransform".format(jnt), 0)
            cmds.setAttr("{0}.segmentScaleCompensate".format(jnt), 0)
            cmds.setAttr("{0}.radius".format(jnt), 0.1)

            joints.append(jnt)
            
        return root, joints
        
    # ------------------------------------------------------------------------
        
    def __connectTranslateJoints(self):
        # connect translate of joint
        for poc, jnt in zip(self.pointOnCurves, self.joints):
            cmds.connectAttr(
                "{0}.result.position".format(poc), 
                "{0}.translate".format(jnt)
            )

    def __connectRotateJoints(self):
        # connect rotation of joint
        for aim, jnt in zip(self.aimOnCurves, self.joints):
            cmds.parent(aim, jnt)
            cmds.connectAttr(
                "{0}.constraintRotate".format(aim), 
                "{0}.rotate".format(jnt)
            )
            
    def __scaleConstraintJoints(self):
        # variable
        constraints = []
        
        # loop clusters
        for i, weight in enumerate(self.weights):
            # get cluster drivers
            drivers = [self.controlClusters[k] for k, v in weight.iteritems()]

            # constraint grp to clusters
            c = cmds.scaleConstraint(
                drivers, 
                self.joints[i],
                n="{0}_scale_{1:03d}".format(self.name, i+1),
                mo=False
            )[0]

            # set weighting
            aliases = cmds.scaleConstraint(
                c, 
                query=True, 
                weightAliasList=True
            )
            aliasesData = [
                (aliases[i], weight.values()[i]) 
                for i in range(len(weight.keys()))
            ]

            # set scale constraint
            for attr, value in aliasesData:
                cmds.setAttr("{0}.{1}".format(c, attr), value)

            constraints.append(c)

        return constraints
            
    # ------------------------------------------------------------------------
        
    def __connectJoints(self):
        # constraint root
        cmds.parentConstraint(self.rootControl, self.rootJoint, mo=False)
        cmds.scaleConstraint(self.rootControl, self.rootJoint, mo=False)

        # constraint joints
        self.__connectTranslateJoints()
        self.__connectRotateJoints()

        return self.__scaleConstraintJoints()
        
    # ------------------------------------------------------------------------
        
    def __createScaleReaders(self):
        readers = []

        # get root offset position
        rootPos = cmds.getAttr("{0}.translate".format(self.rootJoint))[0]

        # loop locators
        for i, poc in enumerate(self.pointOnCurves):
            # multiply up vector
            pmm = cmds.createNode(
                "pointMatrixMult",
                n="{0}_scale_pmm_{1:03d}".format(self.name, i+1)
            )
            
            locPos = cmds.getAttr("{0}.result.position".format(poc))[0]
            pos = [
                locPos[0] - rootPos[0],
                locPos[1] - rootPos[1],
                locPos[2] - rootPos[2],
            ]

            cmds.setAttr("{0}.inPoint".format(pmm), *pos)
            cmds.connectAttr(
                "{0}.worldMatrix[0]".format(self.rootJoint), 
                "{0}.inMatrix".format(pmm), 
            )

            readers.append(pmm)
        
        return readers
        
    # ------------------------------------------------------------------------
    
    def __createDistanceBetween(self, nodes, suffix, attr):
        num = len(nodes)
        distances = []

        for i in range(num-1):
            # create node
            db = cmds.createNode(
                "distanceBetween",
                n="{0}_scale_{1}_db_{2:03d}".format(
                    self.name,
                    suffix,
                    i
                )
            )

            # connect input
            cmds.connectAttr(
                "{0}.{1}".format(nodes[i], attr),
                "{0}.point1".format(db)
            )
            cmds.connectAttr(
                "{0}.{1}".format(nodes[i + 1], attr),
                "{0}.point2".format(db)
            )

            # append attribute
            distances.append("{0}.distance".format(db))

        return distances
        
    def __createDistanceBetweenConnection(self, base, scale, i):
        # get scale average from distances
        mult = cmds.createNode(
            "multiplyDivide",
            n="{0}_scale_md_{1:03d}".format(self.name, i)
        )

        cmds.setAttr("{0}.operation".format(mult), 2)
        cmds.connectAttr(scale, "{0}.input1X".format(mult))
        cmds.connectAttr(base, "{0}.input2X".format(mult))

        # bring value down by one
        adl01 = cmds.createNode(
            "addDoubleLinear",
            n="{0}_scale_adl_a_{1:03d}".format(self.name, i)
        )
        
        cmds.setAttr("{0}.input2".format(adl01), -1)
        cmds.connectAttr(
            "{0}.outputX".format(mult), 
            "{0}.input1".format(adl01)
        )

        # multiply by user value
        mdl = cmds.createNode(
            "multDoubleLinear",
            n="{0}_scale_mdl_{1:03d}".format(self.name, i)
        )

        cmds.connectAttr(
            "{0}.output".format(adl01),
            "{0}.input1".format(mdl)
        )
        cmds.connectAttr(
            "{0}.scale_multiplier".format(self.rootControl),
            "{0}.input2".format(mdl)
        )

        # bring value up by one
        adl02 = cmds.createNode(
            "addDoubleLinear",
            n="{0}_scale_adl_b_{1:03d}".format(self.name, i)
        )
        
        cmds.setAttr("{0}.input2".format(adl02), 1)
        cmds.connectAttr(
            "{0}.output".format(mdl),
            "{0}.input1".format(adl02)
        )

        # clamp by user value
        clamp = cmds.createNode(
            "clamp",
            n="{0}_scale_clamp_{1:03d}".format(self.name, i)
        )

        cmds.connectAttr(
            "{0}.scale_clamp_min".format(self.rootControl),
            "{0}.minR".format(clamp)
        )
        cmds.connectAttr(
            "{0}.scale_clamp_max".format(self.rootControl),
            "{0}.maxR".format(clamp)
        )
        cmds.connectAttr(
            "{0}.output".format(adl02),
            "{0}.inputR".format(clamp)
        )

        return "{0}.outputR".format(clamp)
        
    def __createDistanceBetweenConnections(self):
        # connect scales
        connections = []
        indices = range(len(self.bDistances))

        # loop distances
        for i, base, scale in zip(indices, self.bDistances, self.sDistances):
            connections.append(
                self.__createDistanceBetweenConnection(
                    base, scale, i+1
                )
            )

        # duplicate last distance
        connections.append(connections[-1])
        return connections

    # ------------------------------------------------------------------------
        
    def __createStretchAndSquash(self):
        # add spacer attribute
        attribute.addSpacerAttr(self.rootControl)
        
        # add spacer stretch and squash attribute
        attribute.addAttr(
            self.rootControl, "scale_multiplier", defaultValue=1, minValue=0
        )
        attribute.addAttr(
            self.rootControl, "scale_clamp_min", defaultValue=0.1, minValue=0
        )
        attribute.addAttr(
            self.rootControl, "scale_clamp_max", defaultValue=2, minValue=0
        )
        
        # create distance between nodes
        self.bDistances = self.__createDistanceBetween(
            self.pointOnCurves, 
            "base", 
            "result.position"
        )
        self.sDistances = self.__createDistanceBetween(
            self.scaleReaders, 
            "scale", 
            "output"
        )

        # create user input hierarchy
        connections = self.__createDistanceBetweenConnections()

        # determine axis to scale
        axis = ["X", "Y", "Z"]
        axis.remove(self.forwardDirection.upper())

        # connect to scale constraint
        for i, connection in enumerate(connections):
            for a in axis:
                cmds.connectAttr(
                    connection,
                    "{0}.offset{1}".format(
                        self.scaleConstraints[i],
                        a
                    )
                )

    # ------------------------------------------------------------------------
    
    def __createSlideControls(self):
        # variables
        offsets = []
        controls = []
        
        # loop controls
        for suffix in ["slide", "slide_min", "slide_max"]:
            ctrlOffset, ctrl = control.createControlShape(
                "{0}_{1}".format(self.name, suffix),
                self.slideControlShape,
                self.slideControlColour
            )
            
            # scale constraint
            cmds.scaleConstraint(self.rootControl, ctrlOffset)

            # append to list
            offsets.append(ctrlOffset)
            controls.append(ctrl)
            
        # scale controls
        scale = [0.75, 0.5, 0.5]
        for ctrl, s in zip(controls, scale):
            cmds.setAttr("{0}.scale".format(ctrl), s, s, s)
            cmds.makeIdentity(ctrl, apply=True, scale=True)
        
        # parent controls
        cmds.parent(
            offsets,
            self.rootControl
        )

        return controls
        
    def __attachSlideControlsToMotionPath(self):
        # variables
        motionPaths = []
        
        # create motion path
        mpData = {
            "worldUpType":"Object Rotation Up",
            "worldUpObject":self.rootControl,
            "fractionMode":False,
            "worldUpVector":self.worldUpVector,
        }
        
        for ctrl in [
            self.slideControl, 
            self.slideMinControl, 
            self.slideMaxControl
        ]:
            # get control offset
            offset = cmds.listRelatives(ctrl, p=True, f=True)[0]
            
            # attach to motion path
            motionPaths.append(
                motionPath.attachToMotionPath(
                    self.curve, offset, **mpData
                )
            )
        
        return motionPaths
        
    def __normalizeSlideAttributes(self):
        normalized = []
        attributes = [
            "slide_center", 
            "slide_shift", 
            "slide_shift_min", 
            "slide_shift_max"
        ]
        
        # loop attributes
        for attr in attributes:
            mdl = cmds.createNode(
                "multDoubleLinear",
                n="{0}_{1}_norm_mdl".format(self.name, attr)
            )

            cmds.setAttr("{0}.input1".format(mdl), 0.1)
            cmds.connectAttr(
                "{0}.{1}".format(self.slideControl, attr),
                "{0}.input2".format(mdl)
            )

            normalized.append("{0}.output".format(mdl))

        return normalized
        
    def __connectSlideControls(self):
        # variables
        clampAttributes = []
        motionPathAttributes = []
        
        # reverse shift attribute
        mdl = cmds.createNode(
            "multDoubleLinear",
            n="{0}_slide_shift_reverse_mdl".format(self.name)
        )
        
        cmds.setAttr("{0}.input1".format(mdl), -1)
        cmds.connectAttr(self.shiftNorm, "{0}.input2".format(mdl))

        # add to center
        attributes = ["shift", "shift_ctrl", "shift_min", "shift_max"]
        inputs = [
            "{0}.output".format(mdl),
            self.shiftNorm, 
            self.shiftMinNorm, 
            self.shiftMaxNorm
        ]

        # get curve parameter length
        parameterLength = curve.parameterLength(self.curveShape)

        # loop attributes
        for attr, input in zip(attributes,inputs):
            # add value with center
            adl = cmds.createNode(
                "addDoubleLinear",
                n="{0}_slide_{1}_adl".format(self.name, attr)
            )
        
            cmds.connectAttr(self.centerNorm, "{0}.input1".format(adl))
            cmds.connectAttr(input, "{0}.input2".format(adl))

            # clamp value between 0-1
            clamp = cmds.createNode(
                "clamp",
                n="{0}_slide_{1}_clamp".format(self.name, attr)
            )
            
            cmds.setAttr("{0}.minR".format(clamp), 0)
            cmds.setAttr("{0}.maxR".format(clamp), 1)
            cmds.connectAttr(
                "{0}.output".format(adl),
                "{0}.inputR".format(clamp)
            )

            # adjust to parameter length
            mdl = cmds.createNode(
                "multDoubleLinear",
                n="{0}_slide_{1}_mdl".format(self.name, attr)
            )
            
            cmds.setAttr("{0}.input1".format(mdl), parameterLength)
            cmds.connectAttr(
                "{0}.outputR".format(clamp),
                "{0}.input2".format(mdl)
            )

            clampAttributes.append("{0}.outputR".format(clamp))
            motionPathAttributes.append("{0}.output".format(mdl))

        # get motion path attributes
        clamp, clampCtrl, clampMin, clampMax = motionPathAttributes

        # connect clamped values to motion path
        cmds.connectAttr(clampCtrl, "{0}.uValue".format(self.mp))
        cmds.connectAttr(clampMin, "{0}.uValue".format(self.mpMin))
        cmds.connectAttr(clampMax, "{0}.uValue".format(self.mpMax))

        # get clamp attributes
        clamp, clampCtrl, clampMin, clampMax = clampAttributes
        return clamp, clampMin, clampMax
        
    # ------------------------------------------------------------------------
    
    def __connectSlideToJoint(self, poc, i):
        # get parameter
        parameter = cmds.getAttr("{0}.parameter".format(poc))

        # create ramp node
        ramp = cmds.createNode(
            "ramp",
            n="{0}_slide_ramp_{1:03d}".format(self.name, i)
        )
        
        # set default colours and positions
        cmds.setAttr("{0}.colorEntryList[0].color".format(ramp), 0, 0, 0)
        cmds.setAttr("{0}.colorEntryList[0].position".format(ramp), 0)
        cmds.setAttr("{0}.colorEntryList[1].color".format(ramp), 1, 1, 1)
        cmds.setAttr("{0}.colorEntryList[1].position".format(ramp), 1)
        cmds.setAttr("{0}.colorEntryList[2].color".format(ramp), 0.5, 0.5, 0.5)
        cmds.setAttr("{0}.colorEntryList[2].position".format(ramp), 0.5)

        # set default uv parameters
        # connect them to solve maya bug or not setting attributs
        adl = cmds.createNode(
            "addDoubleLinear",
            n="{0}_slide_uv_adl_{1:03d}".format(self.name, i)
        )
        
        cmds.setAttr("{0}.input2".format(adl), parameter)
        cmds.connectAttr(
            "{0}.output".format(adl),
            "{0}.uCoord".format(ramp)
        )
        cmds.connectAttr(
            "{0}.output".format(adl),
            "{0}.vCoord".format(ramp)
        )

        # connect control values
        cmds.connectAttr(
            self.clamp, 
            "{0}.colorEntryList[2].position".format(ramp)
        )
        cmds.connectAttr(
            self.centerNorm, 
            "{0}.colorEntryList[2].colorR".format(ramp)
        )
        cmds.connectAttr(
            self.clampMin, 
            "{0}.colorEntryList[0].position".format(ramp)
        )
        cmds.connectAttr(
            self.clampMin, 
            "{0}.colorEntryList[0].colorR".format(ramp)
        )
        cmds.connectAttr(
            self.clampMax, 
            "{0}.colorEntryList[1].position".format(ramp)
        )
        cmds.connectAttr(
            self.clampMax, 
            "{0}.colorEntryList[1].colorR".format(ramp)
        )

        # check if value is between min and max parameter
        conditions = []
        suffixes = ["a", "b"]
        inputs = [self.clampMin, self.clampMax]
        operations = [5,3]

        for suffix, input, operation in zip(suffixes, inputs, operations):
            cd = cmds.createNode(
                "condition",
                n="{0}_slide_cd_{1}_{2:03d}".format(self.name, suffix, i)
            )

            cmds.setAttr("{0}.operation".format(cd), operation)
            cmds.setAttr("{0}.secondTerm".format(cd), parameter)
            cmds.connectAttr(input, "{0}.firstTerm".format(cd))
            
            cmds.setAttr("{0}.colorIfTrueR".format(cd), 1)
            cmds.setAttr("{0}.colorIfFalseR".format(cd), 0)

            conditions.append("{0}.outColorR".format(cd))

        # multiply output to see if value is between min and max parameter
        mdl = cmds.createNode(
            "multDoubleLinear",
            n="{0}_slide_mdl_{1:03d}".format(self.name, i)
        )
        
        cmds.connectAttr(conditions[0], "{0}.input1".format(mdl))
        cmds.connectAttr(conditions[1], "{0}.input2".format(mdl))

        # condition parameter to use ramped or default value
        cd = cmds.createNode(
            "condition",
            n="{0}_slide_cd_c_{1:03d}".format(self.name, i)
        )
        
        cmds.setAttr("{0}.colorIfTrueR".format(cd), parameter)
        cmds.connectAttr(
            "{0}.output".format(mdl), 
            "{0}.firstTerm".format(cd)
        )
        
        cmds.connectAttr(
            "{0}.outColorR".format(ramp),
            "{0}.colorIfFalseR".format(cd)
        )

        # connect result to point on curve node
        cmds.connectAttr(
            "{0}.outColorR".format(cd), 
            "{0}.parameter".format(poc)
        )
    
    def __connectSlideToJoints(self):
        for i, poc in enumerate(self.pointOnCurves[1:-1]):
            self.__connectSlideToJoint(poc, i+1)
    
    # ------------------------------------------------------------------------
    
    def __createSlide(self):
        # create controls
        self._slideControl, \
        self._slideMinControl, \
        self._slideMaxControl = self.__createSlideControls()

        # create attributes
        attribute.addSpacerAttr(self.slideControl)
        
        attribute.addAttr(
            self.slideControl, "slide_center", dv=5, min=0, max=10
        )
        attribute.addAttr(
            self.slideControl, "slide_shift", dv=0, min=-10, max=10
        )
        attribute.addAttr(
            self.slideControl, "slide_shift_min", dv=-10, min=-10, max=0
        )
        attribute.addAttr(
            self.slideControl, "slide_shift_max", dv=10, min=0, max=10
        )
        
        # attach to motionPath
        self.mp, \
        self.mpMin, \
        self.mpMax = self.__attachSlideControlsToMotionPath()
        
        # normalize attributes
        self.centerNorm, \
        self.shiftNorm, \
        self.shiftMinNorm, \
        self.shiftMaxNorm = self.__normalizeSlideAttributes()
        
        # connect controls
        self.clamp, \
        self.clampMin, \
        self.clampMax = self.__connectSlideControls()

        # connect to locators
        self.__connectSlideToJoints()
        
    # ------------------------------------------------------------------------
        
    def create(
            self, 
            name,
            curve_,
            numJoints, 
            upDirection="y", 
            worldUpDirection="y", 
            forwardDirection="x"
        ):
        """
        Create the spline IK, besides changing attributes from the
        Settings class, the create function itself can also be
        parsed with various variables to customise the result.
        
        :param name: name that is used to prefix all nodes
        :param curve_: curve to attach the Spline IK to.
        :param numJoints: number of joints to be distributed on the curve
        :param upDirection: "x", "y" or "z", default "y"
        :param worldUpDirection: "x", "y" or "z", default "y"
        :param forwardDirection: "x", "y" or "z", default "x"
        """
        # variables
        self.name = name
        self.curve = curve_
        self.numJoints = numJoints
        self.upDirection = upDirection
        self.forwardDirection = forwardDirection

        # vector variables
        self.upVector = math.convertAxisToVector(upDirection)
        self.aimVector = math.convertAxisToVector(forwardDirection)
        self.worldUpVector = math.convertAxisToVector(worldUpDirection)
        
        # run the rest of the code in a single undo chunk
        with undo.UndoChunkContext():
            # convert curve to bezier curve
            curve.convertToBezierCurve(self.curve)
            
            # create clusters
            self.clusters = cluster.clusterCurve(self.curve, self.name)
            self.controlClusters = self.clusters[::3]
            
            # create controls
            self._rootControl, \
            self._controls, \
            self._tangentControls = self.__createControls()
            
            # get parameters
            self.cParameters, self.jParameters = self.__getParameters()
            
            # get weight mapping between clusters and locators
            self.weights = self.__getWeighting()
            
            # create up vectors
            self.blends, self.ups = self.__createUpVectors()
            
            # create point on curves
            self.pointOnCurves, \
            self.aimOnCurves = self.__createPointOnCurves()
            
            # create joints
            self._rootJoint, self._joints = self.__createJoints()
            
            # create scale readers
            self.scaleReaders = self.__createScaleReaders()
            self.scaleConstraints = self.__connectJoints()
            
            # create stretch and squash
            self.__createStretchAndSquash()
            
            # create slide
            self.__createSlide()
            
        return self.rootControl