# vmd help classes and functions
#
#
from UnityEngine import Quaternion, Vector3, Vector2, Color, Rect, GameObject, Transform, Mathf
from System import String, Array, Single, Double
from vnactor import Prop, Actor, ActorNeoV2, ActorCharaStudio
from vnframe import script2string
from vngameengine import get_engine_id, GData, HSNeoOCIChar, HSNeoOCIProp, random_choice, random_randint
import struct
import math
import copy
import os
import traceback
import ConfigParser

class VmdLib():

    @staticmethod
    def loadVmd(filename):
        from os import path

        print "start load VMD file:", filename
        try:
            # check file
            if not path.isfile(filename):
                return ("File '%s' not found"%filename,)

            # open to read
            fp = open(filename, "rb")

            # header
            version = fp.read(30).strip("\0")
            print "version:", version
            if version.startswith("Vocaloid Motion Data 0002"):
                name = fp.read(20)
            else:
                name = fp.read(10)
            charName = name.strip("\x00").decode("shift_jis")
            print "name:", charName
            
            # bone key frame
            numstr = fp.read(4)
            if len(numstr) == 4:
                boneKeyFrameNum = struct.unpack('I', numstr)[0]
            else:
                boneKeyFrameNum = 0
            print "bone key frames:", boneKeyFrameNum
            boneKeyFrames = []
            if boneKeyFrameNum > 0:
                for i in range(boneKeyFrameNum):
                    bkf = VmdBoneKeyFrame()
                    bkf.name = fp.read(15).strip("\x00").decode("shift_jis")
                    if "\x00" in bkf.name:
                        bkf.name = bkf.name[:bkf.name.index("\x00")]
                    bkf.frameNo = struct.unpack('I', fp.read(4))[0]
                    pos = struct.unpack('fff', fp.read(12))
                    bkf.position = Vector3(pos[0], pos[1], pos[2])
                    rot = struct.unpack('ffff', fp.read(16))
                    bkf.rotation = Quaternion(rot[0], rot[1], rot[2], rot[3])
                    bkf.xCurve = [float(n & 0xff)/127.0 for n in struct.unpack('IIII', fp.read(16))]
                    bkf.yCurve = [float(n & 0xff)/127.0 for n in struct.unpack('IIII', fp.read(16))]
                    bkf.zCurve = [float(n & 0xff)/127.0 for n in struct.unpack('IIII', fp.read(16))]
                    bkf.rCurve = [float(n & 0xff)/127.0 for n in struct.unpack('IIII', fp.read(16))]
                    boneKeyFrames.append(bkf)
                boneKeyFrames.sort(key=VmdBoneKeyFrame.sortByFrameNo)
                
            # morph key frame
            numstr = fp.read(4)
            if len(numstr) == 4:
                morphKeyFrameNum = struct.unpack('I', numstr)[0]
            else:
                morphKeyFrameNum = 0
            print "morph key frames:", morphKeyFrameNum
            morphKeyFrames = []
            if morphKeyFrameNum > 0:
                for i in range(morphKeyFrameNum):
                    mkf = VmdMorphKeyFrame()
                    mkf.name = fp.read(15).strip("\x00").decode("shift_jis")
                    if "\x00" in mkf.name:
                        mkf.name = mkf.name[:mkf.name.index("\x00")]
                    mkf.frameNo = struct.unpack('I', fp.read(4))[0]
                    mkf.weight = struct.unpack('f', fp.read(4))[0]
                    morphKeyFrames.append(mkf)
                morphKeyFrames.sort(key=VmdMorphKeyFrame.sortByFrameNo)
                
            # camera key frame
            numstr = fp.read(4)
            if len(numstr) == 4:
                cameraKeyFrameNum = struct.unpack('I', numstr)[0]
            else:
                cameraKeyFrameNum = 0
            print "camera key frames:", cameraKeyFrameNum
            cameraKeyFrames = []
            if cameraKeyFrameNum > 0:
                for i in range(cameraKeyFrameNum):
                    ckf = VmdCameraKeyFrame()
                    ckf.frameNo = struct.unpack('I', fp.read(4))[0]
                    ckf.dist = struct.unpack('f', fp.read(4))[0]
                    ckf.posX = struct.unpack('f', fp.read(4))[0]
                    ckf.posY = struct.unpack('f', fp.read(4))[0]
                    ckf.posZ = struct.unpack('f', fp.read(4))[0]
                    ckf.rotX = struct.unpack('f', fp.read(4))[0]*180/math.pi
                    ckf.rotY = struct.unpack('f', fp.read(4))[0]*180/math.pi
                    ckf.rotZ = struct.unpack('f', fp.read(4))[0]*180/math.pi
                    curveArray = []
                    for c in range(24):
                        curveArray.append(struct.unpack('B', fp.read(1))[0] / 127.0)
                    ckf.importCurve(curveArray)
                    ckf.viewAngle = float(struct.unpack('I', fp.read(4))[0])
                    ckf.orthographic = struct.unpack('B', fp.read(1))[0]
                    cameraKeyFrames.append(ckf)
                cameraKeyFrames.sort(key=VmdCameraKeyFrame.sortByFrameNo)
                    
            # light key frame
            numstr = fp.read(4)
            if len(numstr) == 4:
                lightKeyFrameNum = struct.unpack('I', numstr)[0]
            else:
                lightKeyFrameNum = 0
            print "light key frames:", lightKeyFrameNum
            lightKeyFrames = []
            if lightKeyFrameNum > 0:
                fp.seek(lightKeyFrameNum * 28, 1)
            
            # close
            fp.close()
            
            return (charName, boneKeyFrames, morphKeyFrames, cameraKeyFrames, lightKeyFrames)

        except Exception, e:
            traceback.print_exc()
            return ("Vmd format error! " + str(e),)

    @staticmethod
    def saveVmd(filename, boneKeyFrames, morphKeyFrames, cameraKeyFrames, lightKeyFrames):
        if not boneKeyFrames: boneKeyFrames = []
        if not morphKeyFrames: morphKeyFrames = []
        if not cameraKeyFrames: cameraKeyFrames = []
        if not lightKeyFrames: lightKeyFrames = []
        # export to vmd file
        try:
            fp = open(filename, "wb")
            # header
            version = "Vocaloid Motion Data 0002\0\0\0\0\0"
            fp.write(version)
            if len(boneKeyFrames) > 0 or len(morphKeyFrames) > 0:
                # name = MMD Director export
                name = (0x4d, 0x4d, 0x44, 0x20, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74,
                        0x6f, 0x72, 0x20, 0x65, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x00)
            else:
                # name for camera
                name = (0x83, 0x4a, 0x83, 0x81, 0x83, 0x89, 0x81, 0x45, 0x8f, 0xc6,
                        0x96, 0xbe, 0x00, 0x6f, 0x6e, 0x20, 0x44, 0x61, 0x74, 0x61)
            for n in name:
                nb = struct.pack('B', n)
                fp.write(nb)

            # bone key frames
            tmpStr = struct.pack('I', len(boneKeyFrames))
            fp.write(tmpStr)
            for bkf in boneKeyFrames:
                # name
                name = bkf.name.encode("shift_jis")
                while len(name) < 15:
                    name += "\0"
                fp.write(name)
                # other info
                tmpStr = struct.pack('I', bkf.frameNo)
                tmpStr += struct.pack('f', bkf.position.x)
                tmpStr += struct.pack('f', bkf.position.y)
                tmpStr += struct.pack('f', bkf.position.z)
                tmpStr += struct.pack('f', bkf.rotation.x)
                tmpStr += struct.pack('f', bkf.rotation.y)
                tmpStr += struct.pack('f', bkf.rotation.z)
                tmpStr += struct.pack('f', bkf.rotation.w)
                for c in range(4):
                    tmpStr += struct.pack('I', int(bkf.xCurve[c] * 127.0))
                for c in range(4):
                    tmpStr += struct.pack('I', int(bkf.yCurve[c] * 127.0))
                for c in range(4):
                    tmpStr += struct.pack('I', int(bkf.zCurve[c] * 127.0))
                for c in range(4):
                    tmpStr += struct.pack('I', int(bkf.rCurve[c] * 127.0))
                fp.write(tmpStr)

            # morph key frames
            tmpStr = struct.pack('I', len(morphKeyFrames))
            fp.write(tmpStr)
            for mbf in morphKeyFrames:
                # name
                name = mbf.name.encode("shift_jis")
                while len(name) < 15:
                    name += "\0"
                fp.write(name)
                # other info
                tmpStr = struct.pack('I', mbf.frameNo)
                tmpStr += struct.pack('f', mbf.weight)
                fp.write(tmpStr)

            # camera key frames
            tmpStr = struct.pack('I', len(cameraKeyFrames))
            fp.write(tmpStr)
            for camkf in cameraKeyFrames:
                tmpStr = struct.pack('I', camkf.frameNo)
                tmpStr += struct.pack('f', camkf.dist)
                tmpStr += struct.pack('f', camkf.posX)
                tmpStr += struct.pack('f', camkf.posY)
                tmpStr += struct.pack('f', camkf.posZ)
                tmpStr += struct.pack('f', camkf.rotX*math.pi/180.0)
                tmpStr += struct.pack('f', camkf.rotY*math.pi/180.0)
                tmpStr += struct.pack('f', camkf.rotZ*math.pi/180.0)
                for c in range(24):
                    tmpStr += struct.pack('B', camkf.curve[c] * 127.0)
                tmpStr += struct.pack('I', camkf.viewAngle)
                tmpStr += struct.pack('B', camkf.orthographic)
                fp.write(tmpStr)

            # light key frames, not support
            tmpStr = struct.pack('I', 0)
            fp.write(tmpStr)

            # ext1, not support
            tmpStr = struct.pack('I', 0)
            fp.write(tmpStr)

            # ext2, not support
            tmpStr = struct.pack('I', 0)
            fp.write(tmpStr)

            fp.close()
            msg = "Export to file <%s>"%(filename)
            return (True, msg)

        except Exception, e:
            traceback.print_exc()
            return (False, str(e))

    @staticmethod
    def checkVmd(filename, getFrameName=False, getFrameLength=False):
        from os import path
        try:
            # check file
            if not path.isfile(filename):
                return ("File '%s' not found"%filename,)

            # open to read
            fp = open(filename, "rb")

            # header
            version = fp.read(30).strip("\0")
            if version.startswith("Vocaloid Motion Data 0002"):
                name = fp.read(20)
            else:
                name = fp.read(10)
            charName = name.strip("\x00").decode("shift_jis")
            
            # bone key frame
            numstr = fp.read(4)
            if len(numstr) == 4:
                boneKeyFrameNum = struct.unpack('I', numstr)[0]
            else:
                boneKeyFrameNum = 0
            boneInfo = {"keyframeCount": boneKeyFrameNum}
            if boneKeyFrameNum > 0:
                if getFrameName or getFrameLength:
                    if getFrameName:
                        boneInfo["keyframeNames"] = set()
                    if getFrameLength:
                        boneInfo["frameLength"] = 0
                    for i in range(boneKeyFrameNum):
                        if getFrameName:
                            name = fp.read(15).strip("\x00").decode("shift_jis")
                            if "\x00" in name:
                                name = name[:name.index("\x00")]
                            boneInfo["keyframeNames"].add(name)
                        else:
                            fp.seek(15, 1)
                        if getFrameLength:
                            frameNo = struct.unpack('I', fp.read(4))[0]
                            if frameNo > boneInfo["frameLength"]:
                                boneInfo["frameLength"] = frameNo
                        else:
                            fp.seek(4, 1)
                        fp.seek(92, 1)
                else:
                    fp.seek(boneKeyFrameNum * 111, 1)

            # morph key frame
            numstr = fp.read(4)
            if len(numstr) == 4:
                morphKeyFrameNum = struct.unpack('I', numstr)[0]
            else:
                morphKeyFrameNum = 0
            morphInfo = {"keyframeCount": morphKeyFrameNum}
            if morphKeyFrameNum > 0:
                if getFrameName or getFrameLength:
                    if getFrameName:
                        morphInfo["keyframeNames"] = set()
                    if getFrameLength:
                        morphInfo["frameLength"] = 0
                    for i in range(morphKeyFrameNum):
                        if getFrameName:
                            name = fp.read(15).strip("\x00").decode("shift_jis")
                            if "\x00" in name:
                                name = name[:name.index("\x00")]
                            morphInfo["keyframeNames"].add(name)
                        else:
                            fp.seek(15, 1)
                        if getFrameLength:
                            frameNo = struct.unpack('I', fp.read(4))[0]
                            if frameNo > morphInfo["frameLength"]:
                                morphInfo["frameLength"] = frameNo
                        else:
                            fp.seek(4, 1)
                        fp.seek(4, 1)
                else:
                    fp.seek(morphKeyFrameNum * 23, 1)
                
            # camera key frame
            numstr = fp.read(4)
            if len(numstr) == 4:
                cameraKeyFrameNum = struct.unpack('I', numstr)[0]
            else:
                cameraKeyFrameNum = 0
            cameraInfo = {"keyframeCount": cameraKeyFrameNum}
            if cameraKeyFrameNum > 0:
                if getFrameLength:
                    cameraInfo["frameLength"] = 0
                    for i in range(cameraKeyFrameNum):
                        frameNo = struct.unpack('I', fp.read(4))[0]
                        if frameNo > cameraInfo["frameLength"]:
                            cameraInfo["frameLength"] = frameNo
                        fp.seek(57, 1)
                else:
                    fp.seek(cameraKeyFrameNum * 61, 1)
                    
            # light key frame
            numstr = fp.read(4)
            if len(numstr) == 4:
                lightKeyFrameNum = struct.unpack('I', numstr)[0]
            else:
                lightKeyFrameNum = 0
            lightInfo = {"keyframeCount": lightKeyFrameNum}
            if lightKeyFrameNum > 0:
                fp.seek(lightKeyFrameNum * 28, 1)
            
            # close
            fp.close()
            
            return (charName, boneInfo, morphInfo, cameraInfo, lightInfo)

        except Exception, e:
            traceback.print_exc()
            return ("Vmd format error! " + str(e),)

    @staticmethod
    def cubicBezierInterpolate(p1x, p2x, p1y, p2y, x):
        def cbi_s(p1, p2, t):
            a = 1 - 3*p2 + 3*p1
            b = 3*(p2 - 2*p1)
            c = 3*(p1)
            return a*t*t*t + b*t*t + c*t

        def root3(x):
            if x < 0:
                y = math.pow(-x, 0.3333333333333333333)
                return -y
            else:
                y = math.pow(x, 0.3333333333333333333)
                return y
            
        def cbi_ts(p1, p2, y):
            a = 1 - 3*p2 + 3*p1
            b = 3*(p2 - 2*p1)
            c = 3*(p1)
            d = - y
            #y = a*t*t*t + b*t*t + c*t + d
            #print ("a=", a, "b=", b, "c=", c, "d=", d)
            if a == 0:
                if b == 0:
                    #print ("linear function")
                    return y
                else:
                    #print ("square function")
                    D = c*c - 4*b*d
                    if D < 0:
                        print ("FAIL to solve square: D < 0!? %f %f %f"%(b, c, d))
                        return 0
                    else:
                        X1 = (-c + math.sqrt(D))/(2*b)
                        X2 = (-c - math.sqrt(D))/(2*b)
                        if X1 >= 0 and X1 <= 1:
                            return X1
                        elif X2 >= 0 and X2 <= 1:
                            return X2
                        else:
                            print ("FAIL to solve square: %f %f"%(X1, X2))
                            return X2
            A = b*b - 3*a*c
            B = b*c - 9*a*d
            C = c*c - 3*b*d
            D = B*B - 4*A*C
            #print ("A=", A, "B=", B, "C=", C, "D=", D)
            if A == 0 and B == 0:
                X = -b/(3*a)
                #print ("ptn1:", X)
                return X
            if D > 0:
                Y1 = A*b + 3*a*(-B + math.sqrt(D))/2
                Y2 = A*b + 3*a*(-B - math.sqrt(D))/2
                X = (-b - (root3(Y1) + root3(Y2)))/(3*a)
                #print ("ptn2:", X)
                return X
            if D == 0:
                K = B/A
                X1 = -b/a + K
                X2 = X3 = -K/2
                #print ("ptn3:",X1,X2,X3)
                if X1 >= 0 and X1 <= 1:
                    return X1
                elif X2 >= 0 and X2 <= 1:
                    return X2
                elif X3 >= 0 and X3 <= 1:
                    return X3
                else:
                    print("FAIL to solve ptn3: (%f, %f, %f)"%(X1,X2,X3))
                    return X3
            if D < 0:
                T = (2*A*b - 3*a*B)/(2*math.sqrt(A*A*A))
                theta = math.acos(T)/3
                X1 = (-b - 2*math.sqrt(A)*math.cos(theta))/(3*a)
                X2 = (-b + math.sqrt(A)*(math.cos(theta) + 1.732*math.sin(theta)))/(3*a)
                X3 = (-b + math.sqrt(A)*(math.cos(theta) - 1.732*math.sin(theta)))/(3*a)
                #print ("ptn4:",X1,X2,X3)
                if X1 >= 0 and X1 <= 1:
                    return X1
                elif X2 >= 0 and X2 <= 1:
                    return X2
                elif X3 >= 0 and X3 <= 1:
                    return X3
                else:
                    print("FAIL to solve ptn4: (%f, %f, %f)"%(X1,X2,X3))
                    return X3
        
        t = cbi_ts(p1x, p2x, x)
        y = cbi_s(p1y, p2y, t)
        return y

    @staticmethod
    def sampleBezier(handle1, handle2, t):
        zero = Vector2.zero
        full = Vector2(1.0, 1.0)
        v1 = Vector2.Lerp(zero, handle1, t)
        v2 = Vector2.Lerp(handle1, handle2, t)
        v3 = Vector2.Lerp(handle2, full, t)
        v4 = Vector2.Lerp(v1, v2, t)
        v5 = Vector2.Lerp(v2, v3, t)
        v6 = Vector2.Lerp(v4, v5, t)
        return v6 

    @staticmethod
    def cubicBezierInterpolate_2(p1x, p2x, p1y, p2y, x):
        # another cubic bezier iterploate function
        # slower but more safe?
        handle1 = Vector2(p1x, p1y)
        handle2 = Vector2(p2x, p2y)
        xtgt = x
        x0 = 0.0
        x1 = 1.0
        c = 0
        #print "try to find point at %.3f"%x
        #print "ref result:", VmdLib.cubicBezierInterpolate(p1x, p2x, p1y, p2y, x)
        while True:
            v = VmdLib.sampleBezier(handle1, handle2, x)
            c += 1
            #print "sample #%d x=%.3f get point (%.3f, %.3f)"%(c, x, v.x, v.y)
            if c >= 10: break
            if math.fabs(xtgt - v.x) < 0.001: break
            if v.x > xtgt:
                x1 = x
                x = (x1 + x0) / 2
                #print "   over! -> set seek range to %.3f to %.3f"%(x0, x1)
            else:
                x0 = x
                x = (x1 + x0) / 2
                #print "   less! -> set seek range to %.3f to %.3f"%(x0, x1)
        return v.y

    @staticmethod
    def mmdCubicBezierInterpolate(curve, x):
        # curve should be (x1, y1, x2, y2) format
        #return VmdLib.cubicBezierInterpolate(curve[0], curve[2], curve[1], curve[3], x)
        return VmdLib.cubicBezierInterpolate_2(curve[0], curve[2], curve[1], curve[3], x)

    @staticmethod
    def rotateVectorFromTo(fromV, toV):
        fromV = fromV.normalized
        toV = toV.normalized
        #if fromV == toV:
        #    return 0.0, Vector3.up
        #else:
        angle = Mathf.Acos(Vector3.Dot(fromV, toV)) * Mathf.Rad2Deg
        if math.isnan(angle):
            return 0.0, Vector3.up
        else:
            axis = Vector3.Cross(fromV, toV).normalized
            return angle, axis

    @staticmethod
    def mmdLimitRotateX(rotQ, minLimit, maxLimit):
        def rotDeg(x, y):
            if x >= 0 and y == 0:
                return 0
            elif x < 0 and y == 0:
                return 180
            elif x == 0 and y > 0:
                return 90
            elif x == 0 and y < 0:
                return -90
            elif x > 0 and y > 0:
                return Mathf.Atan(y/x) * Mathf.Rad2Deg
            elif x < 0 and y > 0:
                return Mathf.Atan(y/x) * Mathf.Rad2Deg + 180
            elif x < 0 and y < 0:
                return Mathf.Atan(y/x) * Mathf.Rad2Deg - 180
            elif x > 0 and y < 0:
                return Mathf.Atan(y/x) * Mathf.Rad2Deg
            else:
                raise Exception("impossible (x,y) position (%f,%f)"%(x,y))

        sinr_cosp = 2 * (rotQ.w * rotQ.x + rotQ.y * rotQ.z)
        cosr_cosp = 1 - 2 * (rotQ.x * rotQ.x + rotQ.y * rotQ.y)
        rotx = math.degrees(math.atan2(sinr_cosp, cosr_cosp))

        if rotx < minLimit:
            return rotx, minLimit, True
        elif rotx > maxLimit:
            return rotx, maxLimit, True
        else:
            return rotx, rotx, False

    @staticmethod
    def mmdLimitRotateY(rotQ, minLimit, maxLimit):
        def rotDeg(x, y):
            if x >= 0 and y == 0:
                return 0
            elif x < 0 and y == 0:
                return 180
            elif x == 0 and y > 0:
                return 90
            elif x == 0 and y < 0:
                return -90
            elif x > 0 and y > 0:
                return Mathf.Atan(y/x) * Mathf.Rad2Deg
            elif x < 0 and y > 0:
                return Mathf.Atan(y/x) * Mathf.Rad2Deg + 180
            elif x < 0 and y < 0:
                return Mathf.Atan(y/x) * Mathf.Rad2Deg - 180
            elif x > 0 and y > 0:
                return Mathf.Atan(y/x) * Mathf.Rad2Deg
            else:
                raise Exception("impossible (x,y) position")
        testV = rotQ * Vector3.forward
        #lmtV = Vector3(0, testV.y, testV.z)
        roty = rotDeg(testV.z, testV.x)
        if roty < minLimit:
            return roty, minLimit, True
        elif roty > maxLimit:
            return roty, maxLimit, True
        else:
            return roty, roty, False


    @staticmethod
    def mmdIKSolver(ikBone, ikTgt, ikChain, loop, stepAngle, limits, debug="ik_foot_r"):
        minDelta = 0.001
        debugPause = False
        def pv(v):
            return "(%.5f, %.5f, %.5f)"%(v.x, v.y, v.z)
        def pq(q):
            if q:
                return pv(q.eulerAngles)
            else:
                return "None"
        def dbgMsg(msg):
            if debug == ikBone.name:
                print(msg)
        def modDeg(deg):
            if deg > 180:
                return deg - 360
            else:
                return deg
        def equalTo(x, y):
            return -minDelta < (x - y) < minDelta
        def nearEnough(vx, vy):
            return equalTo(vx.x, vy.x) and equalTo(vx.y, vy.y) and equalTo(vx.z, vy.z)
        #
        tgtPos = ikBone.transform.position
        dbgMsg("mmdIKSolver for '%s', target at %s"%(ikBone.name, pv(tgtPos)))
        for dbgBone in ikChain:
            dbgMsg("    - chain bone [%s] input rotation = %s"%(dbgBone.name, pv(dbgBone.transform.rotation)))
        for n in range(loop):
            allSkip = True
            remainQ = None
            #dbgMsg("  loop #%d: target at %s"%(n, pv(tgtPos)))
            for m in range(len(ikChain)):
                # update position
                endPos = ikTgt.transform.position
                bonePos = ikChain[m].transform.position
                if nearEnough(tgtPos, endPos):
                    dbgMsg("  IK is close enough to target, ik solver done")
                    break

                # calculate vector direction and rotation
                fromV = endPos - bonePos
                toV = tgtPos - bonePos
                angle, axis = VmdLib.rotateVectorFromTo(fromV, toV)
                #dbgMsg("    chain #%d: chain bone '%s'@%s -> '%s'@%s, angle = %f"%(m, ikChain[m].name, pv(bonePos), ikTgt.name, pv(endPos), angle))
                skip = angle == 0.0
                allSkip &= skip
                if skip:
                    #dbgMsg("              skip this bone...")
                    continue

                # calculate step rotate
                if angle > stepAngle: angle = stepAngle
                deltaQ = Quaternion.AngleAxis(angle, axis)
                resultQ = deltaQ * ikChain[m].transform.rotation   # rotate from current to target

                # append remain rotate
                if remainQ != None:
                    resultQ = resultQ * remainQ 
                    remainQ = None

                # calculate limit
                if limits[m] != None:
                    # check limit
                    localDeltaQ = Quaternion.Inverse(ikChain[m].transform.parent.rotation) * resultQ
                    orgX, limitedX, limited = VmdLib.mmdLimitRotateX(localDeltaQ, 0.5, 180)
                    # rotation limited
                    limitedLocalDetlaQ = Quaternion.Euler(limitedX, 0, 0)
                    remainLocalQ = Quaternion.Euler(orgX - limitedX, 0, 0)
                    resultQ = ikChain[m].transform.parent.rotation * limitedLocalDetlaQ
                    if limited:
                        remainQ = remainLocalQ
                    else:
                        remainQ = None
                    #dbgMsg("              ideal local delta Q = %s, rotation on x = %.3f;"%(pq(localDeltaQ), orgX))
                    #dbgMsg("              limited local delta Q = %s, remain local Q = %s, remain Q = %s"%(pq(limitedLocalDetlaQ), pq(remainLocalQ), pq(remainQ)))
                    """
                    localDeltaEr = VmdLib.mmdQuaternionToEuler(localDeltaQ)
                    limitedX = localDeltaEr.x
                    limitedY = localDeltaEr.y
                    limitedZ = localDeltaEr.z
                    limited = False
                    if limits[m][0][0] == limits[m][0][1]:
                        limited |= not equalTo(limitedX, limits[m][0][0])
                        limitedX = limits[m][0][0]
                    else:
                        if limitedX < limits[m][0][0]:
                            limitedX = limits[m][0][0]
                            limited = True
                        if limitedX > limits[m][0][1]:
                            limitedX = limits[m][0][1]
                            limited = True
                    if limits[m][1][0] == limits[m][1][1]:
                        limited |= not equalTo(limitedY, limits[m][1][0])
                        limitedY = limits[m][1][0]
                    else:
                        if limitedY < limits[m][1][0]:
                            limitedY = limits[m][1][0]
                            limited = True
                        if limitedY > limits[m][1][1]:
                            limitedY = limits[m][1][1]
                            limited = True
                    if limits[m][2][0] == limits[m][2][1]:
                        limited |= not equalTo(limitedZ, limits[m][2][0])
                        limitedZ = limits[m][2][0]
                    else:
                        if limitedZ < limits[m][2][0]:
                            limitedZ = limits[m][2][0]
                            limited = True
                        if limitedZ > limits[m][2][1]:
                            limitedZ = limits[m][2][1]
                            limited = True

                    if limited:
                        # rotation limited
                        limitedLocalDetlaQ = Quaternion.Euler(limitedX, limitedY, limitedZ)
                        remainLocalQ = localDeltaQ * Quaternion.Inverse(limitedLocalDetlaQ)
                        resultQ = ikChain[m].transform.parent.rotation * limitedLocalDetlaQ
                        #remainQ = ikChain[m].transform.parent.rotation * remainLocalQ
                        remainQ = remainLocalQ
                        dbgMsg("              ideal local delta Q = %s/%s;"%(pq(localDeltaQ), pv(localDeltaEr)))
                        dbgMsg("              limited local delta Q = %s, remain local Q = %s"%(pq(limitedLocalDetlaQ), pq(remainLocalQ)))
                    else:
                        remainQ = None
                    """
                # update rotate
                ikChain[m].transform.rotation = resultQ
                #dbgMsg("              rotate resultQ = %s"%(pq(resultQ)))
            # check done
            if allSkip:
                #dbgMsg("  all chain bone skipped, ik solver done")
                break
        # solver done
        dbgMsg("  solved after %d loop, final target at %s"%(n+1, pv(ikTgt.transform.position)))
        for dbgBone in ikChain:
            dbgMsg("    - chain bone [%s] output rotation = %s"%(dbgBone.name, pv(dbgBone.transform.rotation)))

    @staticmethod
    def mmdIKSolverBackup(ikBone, ikTgt, ikChain, loop, stepAngle, limits, debug=""):
        minDelta = 0.0001
        debugPause = False
        def pv(v):
            return "(%.3f, %.3f, %.3f)"%(v.x, v.y, v.z)
        def pq(q):
            return pv(q.eulerAngles)
        def dbgMsg(msg):
            if debug == ikBone.name:
                print(msg)
        def modDeg(deg):
            if deg > 180:
                return deg - 360
            else:
                return deg
        def equalTo(x, y):
            return -minDelta < (x - y) < minDelta
        def nearEnough(vx, vy):
            return equalTo(vx.x, vy.x) and equalTo(vx.y, vy.y) and equalTo(vx.z, vy.z)
        #
        tgtPos = ikBone.transform.position
        dbgMsg("mmdIKSolver for '%s', target at %s"%(ikBone.name, pv(tgtPos)))
        for n in range(loop):
            allSkip = True
            remainQ = None
            dbgMsg("  loop #%d: target at %s"%(n, pv(tgtPos)))
            for m in range(len(ikChain)):
                # update position
                endPos = ikTgt.transform.position
                bonePos = ikChain[m].transform.position
                if nearEnough(tgtPos, endPos):
                    dbgMsg("  IK is close enough to target, ik solver done")
                    return

                # calculate vector direction and rotation
                fromV = endPos - bonePos
                toV = tgtPos - bonePos
                angle, axis = VmdLib.rotateVectorFromTo(fromV, toV)
                dbgMsg("    chain #%d: chain bone '%s'@%s -> '%s'@%s, angle = %f"%(m, ikChain[m].name, pv(bonePos), ikTgt.name, pv(endPos), angle))
                skip = angle == 0.0
                allSkip &= skip
                if skip:
                    dbgMsg("              skip this bone...")
                    continue

                # calculate step rotate
                if angle > stepAngle: angle = stepAngle
                deltaQ = Quaternion.AngleAxis(angle, axis)
                resultQ = deltaQ * ikChain[m].transform.rotation   # rotate from current to target

                # append remain rotate
                if remainQ != None:
                    resultQ = resultQ * remainQ 
                    remainQ = None

                # calculate limit
                if limits[m] != None:
                    # check limit
                    localDeltaQ = Quaternion.Inverse(ikChain[m].transform.parent.rotation) * resultQ
                    orgX, limitedX, limited = VmdLib.mmdLimitRotateX(localDeltaQ, 0.5, 180)
                    # rotation limited
                    limitedLocalDetlaQ = Quaternion.Euler(limitedX, 0, 0)
                    remainLocalQ = Quaternion.Euler(orgX - limitedX, 0, 0)
                    resultQ = ikChain[m].transform.parent.rotation * limitedLocalDetlaQ
                    if limited:
                        remainQ = remainLocalQ
                    else:
                        remainQ = None
                    dbgMsg("              ideal local delta Q = %s, rotation on x = %.3f;"%(pq(localDeltaQ), orgX))
                    dbgMsg("              limited local delta Q = %s, remain local Q = %s"%(pq(limitedLocalDetlaQ), pq(remainLocalQ)))
                    """
                    localDeltaEr = VmdLib.mmdQuaternionToEuler(localDeltaQ)
                    limitedX = localDeltaEr.x
                    limitedY = localDeltaEr.y
                    limitedZ = localDeltaEr.z
                    limited = False
                    if limits[m][0][0] == limits[m][0][1]:
                        limited |= not equalTo(limitedX, limits[m][0][0])
                        limitedX = limits[m][0][0]
                    else:
                        if limitedX < limits[m][0][0]:
                            limitedX = limits[m][0][0]
                            limited = True
                        if limitedX > limits[m][0][1]:
                            limitedX = limits[m][0][1]
                            limited = True
                    if limits[m][1][0] == limits[m][1][1]:
                        limited |= not equalTo(limitedY, limits[m][1][0])
                        limitedY = limits[m][1][0]
                    else:
                        if limitedY < limits[m][1][0]:
                            limitedY = limits[m][1][0]
                            limited = True
                        if limitedY > limits[m][1][1]:
                            limitedY = limits[m][1][1]
                            limited = True
                    if limits[m][2][0] == limits[m][2][1]:
                        limited |= not equalTo(limitedZ, limits[m][2][0])
                        limitedZ = limits[m][2][0]
                    else:
                        if limitedZ < limits[m][2][0]:
                            limitedZ = limits[m][2][0]
                            limited = True
                        if limitedZ > limits[m][2][1]:
                            limitedZ = limits[m][2][1]
                            limited = True

                    if limited:
                        # rotation limited
                        limitedLocalDetlaQ = Quaternion.Euler(limitedX, limitedY, limitedZ)
                        remainLocalQ = localDeltaQ * Quaternion.Inverse(limitedLocalDetlaQ)
                        resultQ = ikChain[m].transform.parent.rotation * limitedLocalDetlaQ
                        #remainQ = ikChain[m].transform.parent.rotation * remainLocalQ
                        remainQ = remainLocalQ
                        dbgMsg("              ideal local delta Q = %s/%s;"%(pq(localDeltaQ), pv(localDeltaEr)))
                        dbgMsg("              limited local delta Q = %s, remain local Q = %s"%(pq(limitedLocalDetlaQ), pq(remainLocalQ)))
                    else:
                        remainQ = None
                    """
                # update rotate
                ikChain[m].transform.rotation = resultQ
                dbgMsg("              rotate resultQ = %s"%(pq(resultQ)))
            # check done
            if allSkip:
                dbgMsg("  all chain bone skipped, ik solver done")
                return

    @staticmethod
    def getScaledValue(orgData, scale):
        if isinstance(orgData, float) or isinstance(orgData, Single) or isinstance(orgData, Double) or isinstance(orgData, int):
            return orgData * scale
        elif isinstance(orgData, tuple):
            return tuple([VmdLib.getScaledValue(n, scale) for n in orgData])
        elif isinstance(orgData, list):
            return [VmdLib.getScaledValue(n, scale) for n in orgData]
        elif isinstance(orgData, dict):
            nVal = {}
            for k in orgData:
                nVal[k] = VmdLib.getScaledValue(orgData[k], scale)
        else:
            raise Exception("Type %s is not supported for scale"%str(type(orgData)))

    @staticmethod
    def interpolateRangeValue(rangeMin, rangeMax, rate):
        if isinstance(rangeMin, float) or isinstance(rangeMin, Single) or isinstance(rangeMin, Double) or isinstance(rangeMin, int):
            return (rangeMax - rangeMin) * rate + rangeMin
        elif isinstance(rangeMin, tuple):
            return tuple([VmdLib.interpolateRangeValue(rangeMin[i], rangeMax[i], rate) for i in range(len(rangeMin))])
        elif isinstance(rangeMin, list):
            return [VmdLib.interpolateRangeValue(rangeMin[i], rangeMax[i], rate) for i in range(len(rangeMin))]
        elif isinstance(rangeMin, dict):
            nVal = {}
            for k in rangeMin:
                nVal[k] = VmdLib.interpolateRangeValue(rangeMin[k], rangeMax[k], rate)
        else:
            raise Exception("Type %s is not supported for range interpolate"%str(type(rangeMin)))

class ExtMorphFuncLib():
    @staticmethod
    def set_iris_size(self, irisSize):
        # irisSize: (leftw, lefth, rightw, righth)
        pupil = self.objctrl.charInfo.fileFace.pupil
        pupil[0].pupilW, pupil[0].pupilH, pupil[1].pupilW, pupil[1].pupilH, pupil[0].blackW, pupil[0].blackH, pupil[1].blackW, pupil[1].blackH = irisSize
        self.objctrl.charInfo.ChangeEyesWH(0)
        self.objctrl.charInfo.ChangeEyesWH(1)

    @staticmethod
    def get_iris_size(self):
        # return pupil size (leftw, lefth, rightw, righth)
        pupil = self.objctrl.charInfo.fileFace.pupil
        return (pupil[0].pupilW, pupil[0].pupilH, pupil[1].pupilW, pupil[1].pupilH, pupil[0].blackW, pupil[0].blackH, pupil[1].blackW, pupil[1].blackH)

    @staticmethod
    def set_iris_size_kk(self, irisSize):
        # irisSize: (width, height)
        ff = self.objctrl.charInfo.fileFace
        ff.pupilWidth, ff.pupilHeight = irisSize
        self.objctrl.charInfo.ChangeSettingEyeScaleWidth()
        self.objctrl.charInfo.ChangeSettingEyeScaleHeight()

    @staticmethod
    def get_iris_size_kk(self):
        # return pupil size (width, height)
        ff = self.objctrl.charInfo.fileFace
        return (ff.pupilWidth, ff.pupilHeight)

    @staticmethod
    def getMethod(tgtActor, funcName):
        # just get or install object method, not check is it a valid method!
        if hasattr(tgtActor, funcName):
            # from actor
            return getattr(tgtActor, funcName)
        elif hasattr(ExtMorphFuncLib, funcName):
            # try install for actor
            import types
            func = getattr(ExtMorphFuncLib, funcName)
            method = types.MethodType(func, tgtActor, type(tgtActor))
            setattr(tgtActor, funcName, method)
            return method
        else:
            return None

class VmdBoneKeyFrame():
    curveSetName = ("Position X", "Position Y", "Position Z", "Rotation")

    def __init__(self):
        self.name = ""
        self.frameNo = 0
        self.position = Vector3.zero
        self.rotation = Quaternion.identity
        self.xCurve = [0.2, 0.2, 0.8, 0.8]
        self.yCurve = [0.2, 0.2, 0.8, 0.8]
        self.zCurve = [0.2, 0.2, 0.8, 0.8]
        self.rCurve = [0.2, 0.2, 0.8, 0.8]

    def copy(self):
        nk = VmdBoneKeyFrame()
        nk.name = self.name
        nk.frameNo = self.frameNo
        nk.position = self.position
        nk.rotation = self.rotation
        nk.xCurve = copy.copy(self.xCurve)
        nk.yCurve = copy.copy(self.yCurve)
        nk.zCurve = copy.copy(self.zCurve)
        nk.rCurve = copy.copy(self.rCurve)
        return nk

    @property
    def curveSet(self):
        return (self.xCurve, self.yCurve, self.zCurve, self.rCurve)

    def exportSetting(self):
        setting = {}
        setting["name"] = self.name
        setting["frameNo"] = self.frameNo
        setting["position"] = (self.position.x, self.position.y, self.position.z)
        setting["rotation"] = (self.rotation.x, self.rotation.y, self.rotation.z, self.rotation.w)
        setting["xCurve"] = self.xCurve
        setting["yCurve"] = self.yCurve
        setting["zCurve"] = self.zCurve
        setting["rCurve"] = self.rCurve
        return setting

    def importSetting(self, setting):
        self.name = setting["name"]
        self.frameNo = setting["frameNo"]
        if isinstance(setting["position"], Vector3):
            self.position = setting["position"]
        else:
            self.position = Vector3(setting["position"][0], setting["position"][1], setting["position"][2])
        if isinstance(setting["rotation"], Quaternion):
            self.rotation = setting["rotation"]
        else:
            self.rotation = Quaternion(setting["rotation"][0], setting["rotation"][1], setting["rotation"][2], setting["rotation"][3])
        self.xCurve = setting["xCurve"]
        self.yCurve = setting["yCurve"]
        self.zCurve = setting["zCurve"]
        self.rCurve = setting["rCurve"]

    def importCurveSet(self, curveSet):
        self.xCurve = copy.copy(curveSet[0])
        self.yCurve = copy.copy(curveSet[1])
        self.zCurve = copy.copy(curveSet[2])
        self.rCurve = copy.copy(curveSet[3])

    @staticmethod
    def parseFromSetting(setting):
        nf = VmdBoneKeyFrame()
        nf.importSetting(setting)
        return nf

    @staticmethod
    def sortByFrameNo(keyframe):
        return keyframe.frameNo

class VmdMorphKeyFrame():
    def __init__(self):
        self.name = ""
        self.frameNo = 0
        self.weight = 0.0

    def copy(self):
        nk = VmdMorphKeyFrame()
        nk.name = self.name
        nk.frameNo = self.frameNo
        nk.weight = self.weight
        return nk
        
    @staticmethod
    def sortByFrameNo(keyframe):
        return keyframe.frameNo

    def exportSetting(self):
        exData = [self.name, self.frameNo, self.weight]
        return exData

    def importSetting(self, setting):
        self.name = setting[0]
        self.frameNo = setting[1]
        self.weight = setting[2]

    @staticmethod
    def parseFromSetting(setting):
        nf = VmdMorphKeyFrame()
        nf.importSetting(setting)
        return nf

class VmdCameraKeyFrame():
    curveSetName = ("Position X", "Position Y", "Position Z", "Rotation", "Distance", "View Angle")

    def __init__(self):
        self.frameNo = 0
        self.dist = 0.0
        self.posX = 0.0
        self.posY = 0.0
        self.posZ = 0.0
        self.rotX = 0.0
        self.rotY = 0.0
        self.rotZ = 0.0
        self.xCurve = [0.2, 0.2, 0.8, 0.8]
        self.yCurve = [0.2, 0.2, 0.8, 0.8]
        self.zCurve = [0.2, 0.2, 0.8, 0.8]
        self.rCurve = [0.2, 0.2, 0.8, 0.8]
        self.dCurve = [0.2, 0.2, 0.8, 0.8]
        self.vCurve = [0.2, 0.2, 0.8, 0.8]
        self.viewAngle = 0.0
        self.orthographic = 0
    
    def copy(self):
        nf = VmdCameraKeyFrame()
        nf.frameNo = self.frameNo
        nf.dist = self.dist
        nf.posX = self.posX
        nf.posY = self.posY
        nf.posZ = self.posZ
        nf.rotX = self.rotX
        nf.rotY = self.rotY
        nf.rotZ = self.rotZ
        nf.xCurve = copy.copy(self.xCurve)
        nf.yCurve = copy.copy(self.yCurve)
        nf.zCurve = copy.copy(self.zCurve)
        nf.rCurve = copy.copy(self.rCurve)
        nf.dCurve = copy.copy(self.dCurve)
        nf.vCurve = copy.copy(self.vCurve)
        nf.viewAngle = self.viewAngle
        nf.orthographic = self.orthographic
        return nf

    @property
    def pos(self):
        return Vector3(self.posX, self.posY, self.posZ)
        
    @property
    def dst(self):
        return Vector3(0, 0, self.dist)
        
    @property
    def rot(self):
        return Vector3(self.rotX, self.rotY, self.rotZ)

    @property
    def curve(self):
        return (self.xCurve[0], self.xCurve[2], self.xCurve[1], self.xCurve[3],
                self.yCurve[0], self.yCurve[2], self.yCurve[1], self.yCurve[3],
                self.zCurve[0], self.zCurve[2], self.zCurve[1], self.zCurve[3],
                self.rCurve[0], self.rCurve[2], self.rCurve[1], self.rCurve[3],
                self.dCurve[0], self.dCurve[2], self.dCurve[1], self.dCurve[3],
                self.vCurve[0], self.vCurve[2], self.vCurve[1], self.vCurve[3],
               )

    @property
    def curveSet(self):
        return (self.xCurve, self.yCurve, self.zCurve, self.rCurve, self.dCurve, self.vCurve)

    def importCurve(self, curveArray):
        self.xCurve = [curveArray[0],  curveArray[2],  curveArray[1],  curveArray[3]]
        self.yCurve = [curveArray[4],  curveArray[6],  curveArray[5],  curveArray[7]]
        self.zCurve = [curveArray[8],  curveArray[10], curveArray[9],  curveArray[11]]
        self.rCurve = [curveArray[12], curveArray[14], curveArray[13], curveArray[15]]
        self.dCurve = [curveArray[16], curveArray[18], curveArray[17], curveArray[19]]
        self.vCurve = [curveArray[20], curveArray[22], curveArray[21], curveArray[23]]

    def exportSetting(self):
        exData = [self.frameNo, self.dist, 
                  self.posX, self.posY, self.posZ,
                  self.rotX, self.rotY, self.rotZ,
                  self.curve, self.viewAngle, self.orthographic]
        return exData

    def importSetting(self, setting):
        self.frameNo = setting[0]
        self.dist = setting[1]
        self.posX = setting[2]
        self.posY = setting[3]
        self.posZ = setting[4]
        self.rotX = setting[5]
        self.rotY = setting[6]
        self.rotZ = setting[7]
        self.importCurve(setting[8])
        self.viewAngle = setting[9]
        self.orthographic = setting[10]

    @staticmethod
    def parseFromSetting(setting):
        nf = VmdCameraKeyFrame()
        nf.importSetting(setting)
        return nf

    @staticmethod
    def sortByFrameNo(keyframe):
        return keyframe.frameNo

class BoneDefineData():
    def __init__(self):
        self.enable = True
        self.parent = None
        self.child = []
        self.vmdBone = None
        self.isRotation = False
        self.isPosition = False
        self.isIK = False
        self.ikData = None
        self.isTrack = False
        self.trackData = None
        self.isLimitAxis = False
        self.limitAxisData = None
        self.isLocalAxis = False
        self.localAxisData = None
        self.positionMode = None
        self.positionData = None
        self.weightBone = None
        self.weightSyncMode = None
        self.gizmosData = None
        self.group = None

    @property
    def isOperation(self):
        return self.isRotation or self.isPosition

class BoneAdjustment():
    def __init__(self, bonename):
        self.boneName = bonename
        self.spec = "-x,y,-z"
        self.xyz = ("x", "y", "z")
        self.sign = (-1.0, 1.0, -1.0)
        self.rotAdjAuto = True
        self.rotAdjustment = Quaternion.identity
        self.rotAdjustmentVec = Vector3.zero
        self.rotAxisAuto = True
        self.rotAxisAdjustment = False
        self.rotAxisAdjustmentVec = Vector3.zero
        self.axisX = Vector3.right
        self.axisY = Vector3.up
        self.axisZ = Vector3.forward
        self.rotationScale = 1.0
        self.basePosition = Vector3.zero
        self.positionGlobalScale = 1.0
        self.positionLocalScale = 1.0
        self.positionScale = 1.0

    def copy(self):
        newBoneAdj = BoneAdjustment(self.boneName)
        newBoneAdj.spec = self.spec
        newBoneAdj.xyz = copy.copy(self.xyz)
        newBoneAdj.sign = copy.copy(self.sign)
        newBoneAdj.rotAdjAuto = self.rotAdjAuto
        newBoneAdj.rotAdjustment = self.rotAdjustment
        newBoneAdj.rotAdjustmentVec = self.rotAdjustmentVec
        newBoneAdj.rotAxisAuto = self.rotAxisAuto
        newBoneAdj.rotAxisAdjustment = self.rotAxisAdjustment
        newBoneAdj.rotAxisAdjustmentVec = self.rotAxisAdjustmentVec
        newBoneAdj.axisX = self.axisX
        newBoneAdj.axisY = self.axisY
        newBoneAdj.axisZ = self.axisZ
        newBoneAdj.rotationScale = self.rotationScale
        newBoneAdj.basePosition = self.basePosition
        newBoneAdj.positionGlobalScale = self.positionGlobalScale
        newBoneAdj.positionLocalScale = self.positionLocalScale
        newBoneAdj.positionScale = self.positionScale
        return newBoneAdj

    def setAll(self, tgtAdj, mode = "all"):
        if mode == "all":
            self.boneName = tgtAdj.boneName
            self.spec = tgtAdj.spec
            self.xyz = tgtAdj.xyz
            self.sign = tgtAdj.sign
        if mode == "all" or mode == "rot":
            self.rotAdjAuto = tgtAdj.rotAdjAuto
            self.rotAdjustment = tgtAdj.rotAdjustment
            self.rotAdjustmentVec = tgtAdj.rotAdjustmentVec
            self.rotAxisAuto = tgtAdj.rotAxisAuto
            self.rotAxisAdjustment = tgtAdj.rotAxisAdjustment
            self.rotAxisAdjustmentVec = tgtAdj.rotAxisAdjustmentVec
            self.axisX = tgtAdj.axisX
            self.axisY = tgtAdj.axisY
            self.axisZ = tgtAdj.axisZ
            self.rotationScale = tgtAdj.rotationScale
        if mode == "all" or mode == "pos":
            self.basePosition = tgtAdj.basePosition
            self.positionGlobalScale = tgtAdj.positionGlobalScale
            self.positionLocalScale = tgtAdj.positionLocalScale
            self.positionScale = tgtAdj.positionScale

    def SetRotAdjustment(self, rotVec):
        self.rotAdjustmentVec = rotVec
        self.rotAdjustment = Quaternion.Euler(rotVec)

    def SetAxisAdjustment(self, rotVec):
        self.rotAxisAdjustmentVec = rotVec
        rotQ = Quaternion.Euler(rotVec)
        self.axisX = rotQ * Vector3.right
        self.axisY = rotQ * Vector3.up
        self.axisZ = rotQ * Vector3.forward
        self.rotAxisAdjustment = rotVec != Vector3.zero

    def SetRotScale(self, rscale):
        self.rotationScale = rscale

    def SetPosGlobalScale(self, gscale):
        self.positionGlobalScale = gscale
        self.positionScale = gscale * self.positionLocalScale

    def SetPosLocalScale(self, lscale):
        self.positionLocalScale = lscale
        self.positionScale = self.positionGlobalScale * lscale

    def adjustAxis(self, v):
        return Vector3(self.sign[0] * v.x, self.sign[1] * v.y, self.sign[2] * v.z)

    def GetAdjustedRotation(self, baseRotQ):
        rotV = baseRotQ.eulerAngles
        rotV = self.adjustAxis(rotV)
        if self.rotAxisAdjustment:
            quaternion = Quaternion.AngleAxis(rotV.y, self.axisY) * Quaternion.AngleAxis(rotV.x, self.axisX) * Quaternion.AngleAxis(rotV.z, self.axisZ) * self.rotAdjustment
        else:
            quaternion = Quaternion.Euler(rotV) * self.rotAdjustment
        if self.rotationScale != 1.0:
            quaternion = Quaternion.SlerpUnclamped(Quaternion.identity, quaternion, self.rotationScale)
        return quaternion

    def GetAdjustedPosition(self, basePosV):
        deltaPos = Vector3(self.sign[0] * self.positionScale * basePosV.x, self.sign[1] * self.positionScale * basePosV.y, self.sign[2] * self.positionScale * basePosV.z)
        return self.basePosition + deltaPos

    def __str__(self):
        msg = "BoneAdjustment [%s]: Spec=%s Rot=(%.3f, %.3f, %.3f)"%(self.boneName, self.spec, self.rotAdjustmentVec.x, self.rotAdjustmentVec.y, self.rotAdjustmentVec.z)
        if self.rotAxisAdjustment:
            msg += " Axis=(%.3f, %.3f, %.3f)"%(self.rotAxisAdjustmentVec.x, self.rotAxisAdjustmentVec.y, self.rotAxisAdjustmentVec.z)
        if self.rotationScale != 1.0:
            msg += " RotScale=%.3f"%self.rotationScale
        return msg

class MMDIKSolverWrapper():
    def __init__(self):
        self.ikBone = None
        self.ikTgt = None
        self.ikChain = None
        self.iterations = None
        self.stepAngle = None
        self.limits = None

    def Verify(self):
        # check
        if self.ikBone == None:
            return "IK bone not set"
        if self.ikTgt == None:
            return "IK target bone (endpoint of chain) not set"
        if self.ikChain == None:
            return "IK chain not set or length of chain bone is not 2"
        if self.iterations == None:
            return "Iteration (max loop count) not set"
        if self.stepAngle == None:
            return "Step angle (max turning angle for 1 step) not set"
        if self.limits == None or len(self.limits) != len(self.ikChain):
            return "Limits not set or different with bone chain"
        return None
        
    def Solve(self):
        VmdLib.mmdIKSolver(self.ikBone, self.ikTgt, self.ikChain, self.iterations, self.stepAngle, self.limits)

class MMDIKSolverNoLimit():
    def __init__(self):
        self.ikBone = None
        self.ikTgt = None
        self.ikChain = None
        self.iterations = None
        self.stepAngle = None
        
    def Verify(self):
        # check
        if self.ikBone == None:
            return "IK bone not set"
        if self.ikTgt == None:
            return "IK target bone (endpoint of chain) not set"
        if self.ikChain == None:
            return "IK chain not set or length of chain bone is not 2"
        if self.iterations == None:
            return "Iteration (max loop count) not set"
        if self.stepAngle == None:
            return "Step angle (max turning angle for 1 step) not set"
        return None

    def Solve(self):
        # MMD spec CCD IK Solver without limit support 
        tgtPos = self.ikBone.position
        for n in range(self.iterations):
            allSkip = True
            for m in range(len(self.ikChain)):
                endPos = self.ikTgt.position
                bonePos = self.ikChain[m].position

                # calculate vector direction
                fromV = endPos - bonePos
                toV = tgtPos - bonePos
                angle, axis = VmdLib.rotateVectorFromTo(fromV, toV)
                skip = angle == 0.0
                allSkip &= skip
                if skip: continue

                # calculate rotate
                if angle > self.stepAngle: angle = self.stepAngle
                deltaQ = Quaternion.AngleAxis(angle, axis)
                resultQ = deltaQ * self.ikChain[m].rotation   # rotate from current to target

                # update rotate
                self.ikChain[m].rotation = resultQ
            # check done
            if allSkip:
                break

class MMDIKSolverLeg():
    def __init__(self):
        self.ikBone = None
        self.ikTgt = None
        self.ikChain = None
        self.iterations = None
        self.stepAngle = None
        self.minKneeRot = None

    def Verify(self):
        # check
        if self.ikBone == None:
            return "IK bone not set"
        if self.ikTgt == None:
            return "IK target bone (endpoint of chain) not set"
        if self.ikChain == None or len(self.ikChain) != 2:
            return "IK chain not set or length of chain bone is not 2"
        if self.iterations == None:
            return "Iteration (max loop count) not set"
        if self.stepAngle == None:
            return "Step angle (max turning angle for 1 step) not set"
        if self.minKneeRot == None:
            return "Min knee rotation angle (knee straighten angle) not set"
        # setup
        self.minKneeQ = Quaternion.Euler(self.minKneeRot, 0, 0)     # new base-rotate = min rotate
        self.ikChain[0].localRotation = self.minKneeQ
        thigh2knee = self.ikChain[0].position - self.ikChain[1].position
        knee2foot = self.ikTgt.position - self.ikChain[0].position
        thigh2knee_len = thigh2knee.magnitude
        knee2foot_len = knee2foot.magnitude
        self.legSum = (self.ikChain[1].position - self.ikTgt.position).magnitude
        self.legMul2 = thigh2knee_len * knee2foot_len * 2
        self.legSqrSum = thigh2knee_len * thigh2knee_len + knee2foot_len * knee2foot_len
        baseQ = Quaternion.FromToRotation(thigh2knee, knee2foot)
        self.baseInvQ = Quaternion.Inverse(baseQ)
        # done 
        return None

    def dbgPtr(self, msg):
        if self.ikBone.name == "ik_foot_r":
            print msg
        pass
        
    def Solve(self):
        # MMD spec CCD IK Solver specially for leg
        tgtPos = self.ikBone.position
        #self.dbgPtr("Solve %s: "%(self.ikBone.name))

        # solve knee
        thigh2tgt_len = (tgtPos - self.ikChain[1].position).magnitude
        if thigh2tgt_len < self.legSum:
            # bend knee
            kneeAngle = Mathf.Acos((self.legSqrSum - thigh2tgt_len * thigh2tgt_len) / self.legMul2) * Mathf.Rad2Deg
            kneeAngle = 180.0 - kneeAngle
            #self.dbgPtr("  knee angle = %f"%(kneeAngle))
            self.ikChain[0].localRotation = Quaternion.Euler(kneeAngle, 0, 0) * self.baseInvQ
            #self.dbgPtr("  knee local rotation x = %f"%(self.ikChain[0].localEulerAngles.x))
        else:
            # straighten knee
            self.ikChain[0].localRotation = self.minKneeQ #Quaternion.identity
            #self.dbgPtr("  knee straight")

        # solve thigh
        for n in range(self.iterations):
            endPos = self.ikTgt.position
            bonePos = self.ikChain[1].position

            # calculate vector direction
            fromV = endPos - bonePos
            toV = tgtPos - bonePos
            angle, axis = Quaternion.FromToRotation(fromV, toV).ToAngleAxis()
            if angle == 0.0:
                break

            # calculate rotate
            if angle > self.stepAngle: angle = self.stepAngle
            #self.dbgPtr(" loop #%d: rotate angle = %f"%(n, angle))
            deltaQ = Quaternion.AngleAxis(angle, axis)
            resultQ = deltaQ * self.ikChain[1].rotation   # rotate from current to target

            # update rotate
            self.ikChain[1].rotation = resultQ
        #self.dbgPtr("Done: loop = %d"%(n))

class MotionController():
    @property
    def PoseController(self):
        try:
            return self.charaAnimePlugin.Manager.GetPoseController(self.ociChar)
        except:
            return None

    @property
    def Enable(self):
        cpc = self.PoseController
        if not cpc:
            return False
        else:
            return cpc.Enable

    #@Enable.setter     # did not work!?
    def setEnable(self, value):
        self.needActive = value
        cpc = self.PoseController
        if cpc:
            #if not cpc.Initialized:
            #    print "Something wrong for PoseController.Start(), try to force it initialize"
            #    try:
            #        cpc.Initialized = True
            #    except:
            #        print "Failed to force initialize PoseController"
            cpc.Enable = value

    @property
    def CharName(self):
        if self.ociChar:
            return self.ociChar.treeNodeObject.textName
        else:
            return ""

    @property
    def FileName(self):
        if self.vmdFilePath and len(self.vmdFilePath) > 0:
            return os.path.split(self.vmdFilePath)[1]
        else:
            return self.vmdFilePath

    @property
    def TotalFrameLength(self):
        return int(self.delay * 30.0) + self.maxFrameNo

    @property
    def KeyframesForExport(self):
        k4f = []
        for kfk in self.boneKeyFrames:
            k4f.extend(self.boneKeyFrames[kfk])
        return k4f

    @property
    def Baked(self):
        return self.bakedFrames != None

    @property
    def NeedActiveMotion(self):
        return self.PoseController and self.needActive and not self.Enable

    @staticmethod
    def DefaultDefineFile():
        return os.path.splitext(__file__)[0] + "_" + get_engine_id()+ '.ini'

    @property
    def IsDefaultBoneDefine(self):
        return self.boneDefineFilename == MotionController.DefaultDefineFile()

    @staticmethod
    def ListExtendDefineFile(tgtType):
        import ConfigParser
        bfdir = os.path.join(os.path.split(__file__)[0], "mmdd")
        allfile = os.listdir(bfdir)
        validfile = {}
        for filename in allfile:
            if not filename.endswith(".ini"):
                continue
            try:
                config = ConfigParser.SafeConfigParser()
                pathname = os.path.join(bfdir, filename)
                config.read(pathname)
                # read and check general setting
                for k, v in config.items("General"):
                    if k == "description":
                        boneDescription = v.decode("utf8")
                    if k == "type":
                        boneType = v
                if boneType == tgtType:
                    validfile[pathname] = boneDescription
            except:
                print "Fail to load file:", filename
                traceback.print_exc()
                pass
        return validfile

    def __init__(self, filename = None):
        # debug
        self.indexMode = 1
        # reset
        self.boneDefineFilename = filename
        self.reset()

    def reset(self):
        # General setting
        self.charScale = 1.0
        self.delay = 0.0
        self.interpolateConsecutive = True

        # load bone setting
        if self.boneDefineFilename:
            if os.path.isabs(self.boneDefineFilename):
                filename = self.boneDefineFilename
                filename_alt = None
            else:
                filename = os.path.join(os.path.split(__file__)[0], self.boneDefineFilename)
                filename_alt = os.path.join(os.path.split(__file__)[0], "mmdd", self.boneDefineFilename)
        else:
            filename = filename_alt = None
        if filename and len(filename) > 0 and os.path.isfile(filename):
            filename = os.path.abspath(filename)
        elif filename_alt and len(filename_alt) > 0 and os.path.isfile(filename_alt):
            filename = os.path.abspath(filename_alt)
        else:
            if filename != None: print "** WARNING** Bonedefine file '%s' not found, use default bone define."%filename
            filename = MotionController.DefaultDefineFile()
        self.loadBoneSetting(filename)

        # runtime objects
        self.ociChar = None
        self.vmdBoneRoot = None
        self.vmdBones = None
        self.vmdIKs = None
        self.boneAdjustDirtySet = set()
        self.charaAnimePlugin = None
        self.needActive = False
        # keyframes
        self.vmdFilePath = ""
        self.boneKeyFrames = {}
        self.keyframeCustomized = False
        self.bakedFrames = None
        self.maxFrameNo = 0

    def loadBoneSetting(self, filename):
        try:
            self.boneDefineFilename = None
            self.boneDefineMap = {}
            self.boneNameList = []
            self.vmd2BoneMap = {}
            self.weightBoneList = []
            self.gizmosBoneList = []
            self.ikSettingMap = {}
            self.ikBoneList = []
            self.boneAdjustMap = {}
            self.boneAdjustMapBackup = {}

            config = ConfigParser.SafeConfigParser()
            config.read(filename)
            # read and check general setting
            for k, v in config.items("General"):
                if k == "description":
                    self.boneDescription = v.decode("utf8")
                if k == "type":
                    self.boneType = v
                if k == "heightbone":
                    self.charaHeightBone = v.strip()
                if k == "autorotadj":
                    self.enableAutoRotAdj = bool(eval(v))
                if k == "defweightsyncmode":
                    self.defaultWeightSyncMode = v.strip()
                if k == "axisscale":
                    self.axisScale = float(v)
            if not hasattr(self, "axisScale"):
                raise Exception("[General] AxisScale not defined!")
            if not hasattr(self, "boneType"):
                raise Exception("[General] boneType not defined!")
            if self.boneType == "chara" and not hasattr(self, "charaHeightBone"):
                raise Exception("[General] 'Chara' type bone define must set a 'heightbone' for chara auto scale.")
            if not hasattr(self, "boneDescription"):
                self.boneDescription = os.path.split(filename)[1]
            if not hasattr(self, "enableAutoRotAdj"):
                self.enableAutoRotAdj = False
            if not hasattr(self, "defaultWeightSyncMode"):
                self.defaultWeightSyncMode = "global"

            # read bone define
            for k, v in config.items("BoneDefine"):
                bname = k.decode("utf8")
                bdata = v.decode("utf8").split(":")
                if len(bdata) < 3:
                    raise Exception("BoneDefine data error for [%s]=[%s]"%(bname,v))
                # get and check define data
                bd = BoneDefineData()
                bd.group = "ungrouped"
                bd.parent = bdata[0].strip().lower() if len(bdata[0].strip()) > 0 else None
                if bd.parent and bd.parent not in self.boneDefineMap:
                    raise Exception("Unknown parent bone for bone [%s]: %s"%(bname,bd.parent))
                bd.vmdBone = bdata[1].strip()
                if len(bd.vmdBone) == 0:
                    raise Exception("VMD bone name is not defined for [%s]"%bname)
                battrs = bdata[2].strip().lower()
                nn = 3
                if "r" in battrs:
                    bd.isRotation = True
                if "p" in battrs:
                    bd.isPosition = True
                if "i" in battrs:
                    bd.isIK = True
                    if len(bdata) <= nn:
                        raise Exception("IK setting not found for bone [%s]"%bname)
                    ikdata = bdata[nn].strip().lower().split(",")
                    if len(ikdata) < 3 or ikdata[0] != "i":
                        raise Exception("IK setting error for bone [%s]"%bname)
                    ikChain = []
                    for i in range(1, len(ikdata)):
                        ikbone = ikdata[i]
                        if ikbone not in self.boneDefineMap:
                            raise Exception("Unknown ik setting bone for bone [%s]: %s"%(bname,ikbone))
                        ikChain.append(ikbone)
                    bd.ikData = tuple(ikChain)
                    nn += 1
                if "t" in battrs:
                    bd.isTrack = True
                    if len(bdata) <= nn:
                        raise Exception("Track setting not found for bone [%s]"%bname)
                    trackdata = bdata[nn].strip().lower().split(",")
                    if len(trackdata) != 3 or trackdata[0][0] != "t":
                        raise Exception("Track setting error for bone [%s]"%bname)
                    if trackdata[2] not in self.boneDefineMap:
                        raise Exception("Unknown track source bone for bone [%s]: %s"%(bname,trackdata[2]))
                    bd.trackData = (trackdata[0][1:], float(trackdata[1]), trackdata[2])
                    nn += 1
                if "l" in battrs:
                    bd.isLimitAxis = True
                    if len(bdata) <= nn:
                        raise Exception("LimitAxis setting not found for bone [%s]"%bname)
                    limitdata = bdata[nn].strip().lower().split(",")
                    if len(limitdata) != 7 or limitdata[0] != "l":
                        raise Exception("LimitAxis setting error for bone [%s]"%bname)
                    refAx = Vector3(float(limitdata[1]), float(limitdata[2]), float(limitdata[3]))
                    lmtAx = Vector3(float(limitdata[4]), float(limitdata[5]), float(limitdata[6]))
                    bd.limitAxisData = (refAx, lmtAx)
                    nn += 1
                if "a" in battrs:
                    bd.isLocalAxis = True
                    if len(bdata) <= nn:
                        raise Exception("LocalAxis setting not found for bone [%s]"%bname)
                    axisdata = bdata[nn].strip().lower().split(",")
                    if len(axisdata) != 7 or axisdata[0] != "a":
                        raise Exception("LocalAxis setting error for bone [%s]"%bname)
                    localX = Vector3(float(axisdata[1]), float(axisdata[2]), float(axisdata[3]))
                    localZ = Vector3(float(axisdata[4]), float(axisdata[5]), float(axisdata[6]))
                    bd.localAxisData = (localX, localZ)
                    nn += 1
                # add to map
                self.boneDefineMap[bname] = bd
                self.boneNameList.append(bname)
                self.vmd2BoneMap[bd.vmdBone] = bname
            self.boneNameList = tuple(self.boneNameList)
            # build child bone map
            for bname in self.boneNameList:
                bDefine = self.boneDefineMap[bname]
                for bname2 in self.boneNameList:
                    bDefine2 = self.boneDefineMap[bname2]
                    if bDefine2.parent == bname:
                        bDefine.child.append(bname2)

            # read position setting
            for k, v in config.items("BonePosition"):
                bname = k.decode("utf8")
                pdata = v.decode("utf8").split(":")
                if bname not in self.boneDefineMap:
                    raise Exception("Unknown bone [%s] in [BonePosition] setting"%(bname))
                if len(pdata) < 2:
                    raise Exception("BonePosition data error for [%s]=[%s]"%(bname,v))
                if pdata[0] == "b" and len(pdata) == 2:
                    self.boneDefineMap[bname].positionMode = pdata[0]
                    self.boneDefineMap[bname].positionData = pdata[1]
                elif pdata[0] == "bbs" and len(pdata) == 4:
                    self.boneDefineMap[bname].positionMode = pdata[0]
                    self.boneDefineMap[bname].positionData = (pdata[1], pdata[2], float(pdata[3]))
                elif pdata[0] == "bo" and len(pdata) == 3:
                    offs = pdata[2].split(",")
                    if len(offs) != 3:
                        raise Exception("BonePosition offset setting error for [%s]"%(bname))
                    offset = Vector3(float(offs[0]), float(offs[1]), float(offs[2]))
                    self.boneDefineMap[bname].positionMode = pdata[0]
                    self.boneDefineMap[bname].positionData = (pdata[1], offset)
                elif pdata[0] == "br" and len(pdata) == 3:
                    rsts = pdata[2].split(",")
                    def parseMaybeNaN(ptxt):
                        try:
                            f = float(ptxt)
                            return f
                        except:
                            return None
                    if len(rsts) != 3:
                        raise Exception("BonePosition reset setting error for [%s]"%(bname))
                    reset = (parseMaybeNaN(rsts[0]), parseMaybeNaN(rsts[1]), parseMaybeNaN(rsts[2]))
                    self.boneDefineMap[bname].positionMode = pdata[0]
                    self.boneDefineMap[bname].positionData = (pdata[1], reset)
                else:
                    raise Exception("BonePosition format error for [%s]=[%s]"%(bname,v))
            for bname in self.boneNameList:
                if self.boneDefineMap[bname].positionMode == None or self.boneDefineMap[bname].positionData == None:
                    raise Exception("Bone [%s]'s position setting missed!"%(bname))

            # read weight setting
            for k, v in config.items("BoneWeight"):
                bname = k.decode("utf8")
                wdata = v.decode("utf8").split(":")
                wbone = wdata[0]
                if len(wdata) == 2:
                    wm = wdata[1].lower()
                    if wm in {"global", "local"}:
                        wmode = wm
                    elif wm == "l":
                        wmode = "local"
                    elif wm == "g":
                        wmode = "global"
                    else:
                        raise Exception("Bone weight sync mode setting for bone [%s] invalid"%(bname))
                else:
                    wmode = self.defaultWeightSyncMode
                if bname not in self.boneDefineMap:
                    raise Exception("Unknown bone [%s] in [BoneWeight] setting"%(bname))
                if len(wbone) == 0:
                    raise Exception("BoneWeight setting for bone [%s] in empty"%(bname))
                self.boneDefineMap[bname].weightBone = wbone
                self.boneDefineMap[bname].weightSyncMode = wmode
            for bname in self.boneNameList:
                if self.boneDefineMap[bname].weightBone:
                    self.weightBoneList.append(bname)

            # read ik setting
            iksetnumber = 0
            for k, v in config.items("BoneIKSetOverview"):
                if k == "ik_set_number":
                    iksetnumber = int(v)
            for iki in range(iksetnumber):
                catname = "BoneIKSet_%d"%(iki+1)
                iksetting = GData()
                for k, v in config.items(catname):
                    if k == "name":
                        iksetting.name = v
                        if iksetting.name in self.ikSettingMap:
                            raise Exception("%s name duplicated!"%catname)
                    elif k == "enable":
                        iksetting.enable = v == "1" or v.lower() == "true"
                    elif k == "ikbone":
                        iksetting.ikBone = v.lower().split(",")
                        for bname in iksetting.ikBone:
                            if bname not in self.boneDefineMap or not self.boneDefineMap[bname].isIK:
                                raise Exception("IKBONE setting error in [%s]: %s is not a valid ik bone!"%(catname, bname))
                            if bname in self.ikBoneList:
                                raise Exception("IKBONE setting error in [%s]: %s is already registed in another ik set"%(catname, bname))
                            self.ikBoneList.append(bname)
                    elif k == "solver":
                        if v not in {"CCDIKSolver", "MMDIKSolver", "MMDIKSolverNoLimit", "MMDIKSolverLeg"}:
                            raise Exception("Unsupported solver in %s"%catname)
                        iksetting.solver = v
                    elif k == "control_weight":
                        iksetting.controlWeight = float(v)
                    elif k == "iterations":
                        iksetting.iterations = int(v)
                    elif k == "step_angle":
                        iksetting.stepAngle = float(v)
                    elif k == "limits":
                        iksetting.limits = eval(v)
                    elif k == "min_delta":
                        iksetting.minDelta = float(v)
                    elif k == "last_frame_weight":
                        iksetting.lastFrameWeight = float(v)
                    elif k == "ik_rotation_weight":
                        iksetting.ikRotationWeight = float(v)
                    elif k == "min_knee_rot":
                        iksetting.minKneeRot = float(v)
                    elif k == "use_leg":
                        iksetting.useLeg = v == "1" or v.lower() == "true"
                    elif k == "solve_method":
                        if v not in {"SolveLeg2", "Solve"}:
                            raise Exception("Unsupported solve method in %s"%catname)
                        iksetting.solveMethod = v
                    else:
                        raise Exception("Unknown IK setting [%s] in [%s]"%(k, catname))
                if not hasattr(iksetting, "name"):
                    raise Exception("IK setting [%s] miss necessary parameter NAME"%(catname))
                if not hasattr(iksetting, "enable"):
                    raise Exception("IK setting [%s] miss necessary parameter ENABLE"%(catname))
                if not hasattr(iksetting, "ikBone"):
                    raise Exception("IK setting [%s] miss necessary parameter IKBONE"%(catname))
                if not hasattr(iksetting, "solver"):
                    raise Exception("IK setting [%s] miss necessary parameter SOLVER"%(catname))
                self.ikSettingMap[iksetting.name] = iksetting

            # read twist disperse setting
            self.twistDisperse = []
            if "BoneTwistDisperse" in config.sections():
                for k, v in config.items("BoneTwistDisperse"):
                    tdi = GData()
                    tdi.name = k.decode("utf8")
                    tdiSetting = v.decode("utf8").split(":")
                    if len(tdiSetting) != 4:
                        raise Exception("Wrong twist disperse setting format for '%s'"%tdi.name)
                    tdi.group = tdiSetting[0]
                    tdi.twistBoneName = tdiSetting[1]
                    if tdi.twistBoneName not in self.boneNameList:
                        raise Exception("Unknown twist orginal bone '%s' in twist disperse setting for '%s'"%(tdi.twistBoneName, tdi.name))
                    tdi.disperseBoneName = tdiSetting[2]
                    if tdi.disperseBoneName not in self.boneNameList:
                        raise Exception("Unknown twist disperse target bone '%s' in twist disperse setting for '%s'"%(tdi.disperseBoneName, tdi.name))
                    tdi.disperseRate = tdi.disperseRateBackup = float(tdiSetting[3])
                    if tdi.disperseRate < 0 or tdi.disperseRate > 1:
                        raise Exception("Invalid disperse rate in twist disperse setting for '%s', must between 0 to 1"%tdi.name)
                    self.twistDisperse.append(tdi)

            # read gizmos setting
            for k, v in config.items("BoneGizmos"):
                bname = k.decode("utf8")
                gdata = v.split(":")
                if bname not in self.boneDefineMap:
                    raise Exception("Unknown bone [%s] in [BoneGizmos] setting"%(bname))
                if len(gdata) != 4 or len(gdata[1]) != 6:
                    raise Exception("BoneGizmos data error for [%s]=[%s]"%(bname,v))
                r = int(gdata[1][0:2], 16) / 255.0
                g = int(gdata[1][2:4], 16) / 255.0
                b = int(gdata[1][4:6], 16) / 255.0
                bparent = gdata[3].lower().strip()
                if len(bparent) == 0:
                    bparent = None
                if bparent and bparent not in self.boneDefineMap:
                    raise Exception("BoneGizemos unknown parent bone [%s] for bone [%s]"%(bparent, bname))
                gSetting = (gdata[0], Color(r,g,b), float(gdata[2]), bparent)
                self.boneDefineMap[bname].gizmosData = gSetting
            for bname in self.boneNameList:
                if self.boneDefineMap[bname].gizmosData:
                    self.gizmosBoneList.append(bname)

            # read bone/morph group setting
            self.boneGroupList = ["unsupported", "ungrouped"]
            self.boneGroupName = {"ungrouped": "Others", "unsupported": "Not-Supported"}
            self.boneGroupSet = {}
            self.vmdMorphs = set()
            self.morphGroupList = ["unsupported", "ungrouped"]
            self.morphGroupName = {"ungrouped": "Others", "unsupported": "Not-Supported"}
            self.morphGroupSet = {}
            if "PredefineGroupName" in config.sections():
                for k, v in config.items("PredefineGroupName"):
                    bgroup = k.decode("utf8")
                    if bgroup not in self.boneGroupList:
                        raise Exception("Predefine group name must be 'UNGROUPED' or 'UNSUPPORTED', [%s] is an invalid name"%bgroup)
                    self.boneGroupName[bgroup] = v.decode("utf8")
                    self.morphGroupName[bgroup] = v.decode("utf8")
            if "BoneGroups" in config.sections() and "BoneGroupSet" in config.sections():
                for k, v in config.items("BoneGroups"):
                    bgroup = k.decode("utf8")
                    if bgroup in self.boneGroupList:
                        raise Exception("Duplicated bone group name [%s]"%bgroup)
                    self.boneGroupList.insert(2, bgroup)
                    self.boneGroupName[bgroup] = v.decode("utf8")
                for k, v in config.items("BoneGroupSet"):
                    bgroup = k.decode("utf8")
                    if bgroup not in self.boneGroupList:
                        raise Exception("Undefined bone group [%s], bone group name must align with the define in [BoneGroups]"%bgroup)
                    bnumber = v.decode("utf8").split(",")
                    for vmdbone in bnumber:
                        if self.vmd2BoneMap.has_key(vmdbone):
                            self.boneDefineMap[self.vmd2BoneMap[vmdbone]].group = bgroup
                        else:
                            raise Exception("Unknown vmd bone [%s] in BoneGroupSet [%s]"%(vmdbone, bgroup))
                    self.boneGroupSet[bgroup] = bnumber
            if len(self.boneGroupSet) + 2 != len(self.boneGroupList):
                raise Exception("Bone group in [BoneGroups] and [BoneGroupSet] not match")
            self.boneGroupList.reverse()
            for k, v in config.items("MorphDefine"):
                attrs = v.decode("utf8").split(":")
                vname = attrs[0]
                if vname in self.vmdMorphs:
                    raise Exception("Morph define duplicated for vmd morph %s"%vname)
                self.vmdMorphs.add(vname)
            if "MorphGroups" in config.sections() and "MorphGroupSet" in config.sections():
                for k, v in config.items("MorphGroups"):
                    mgroup = k.decode("utf8")
                    if mgroup in self.morphGroupList:
                        raise Exception("Duplicated morph group name [%s]"%mgroup)
                    self.morphGroupList.insert(2, mgroup)
                    self.morphGroupName[mgroup] = v.decode("utf8")
                for k, v in config.items("MorphGroupSet"):
                    mgroup = k.decode("utf8")
                    if mgroup not in self.morphGroupList:
                        raise Exception("Undefined morph group [%s], morph group name must align with the define in [MorphGroups]"%mgroup)
                    mnumber = v.decode("utf8").split(",")
                    for vmdmorph in mnumber:
                        if vmdmorph not in self.vmdMorphs:
                            raise Exception("Unknown vmd morph [%s] in MorphGroupSet [%s]"%(vmdmorph, mgroup))
                    self.morphGroupSet[mgroup] = mnumber
            if len(self.morphGroupSet) + 2 != len(self.morphGroupList):
                raise Exception("Morph group in [MorphGroups] and [MrophGroupSet] not match")
            self.morphGroupList.reverse()

            # prepare actual bone before create dummy bone setting
            self.prepareBoneEnableGet = False
            self.prepareBoneEnableSet = False
            self.prepareBoneSetListedOnly = False
            self.prepareBoneSetting = {}
            if "PrepareBoneBeforeCreateDummyBone" in config.sections():
                for k, v in config.items("PrepareBoneBeforeCreateDummyBone"):
                    if k == "setting_enable_get":
                        self.prepareBoneEnableGet = bool(eval(v))
                    elif k == "setting_enable_set":
                        self.prepareBoneEnableSet = bool(eval(v))
                    elif k == "setting_set_listed_only":
                        self.prepareBoneSetListedOnly = bool(eval(v))
                    else:
                        # load setting here
                        vs = eval(v)
                        self.prepareBoneSetting[k] = Quaternion.Euler(vs[0], vs[1], vs[2])

            # read bone adjust map and make a backup
            for bname in self.boneNameList:
                self.boneAdjustMap[bname] = BoneAdjustment(bname)
                self.boneAdjustMap[bname].rotAdjAuto = self.enableAutoRotAdj
            for k, v in config.items("BoneRotAxisAdjust"):
                bname = k.decode("utf8")
                if bname not in self.boneAdjustMap:
                    raise Exception("BoneRotAxisAdjust setting for [%s] error: Unknown bone!?"%bname)
                adjdata = v.lower().split(",")
                i = 0
                while i < len(adjdata):
                    if adjdata[i] == "r" and i + 3 < len(adjdata):
                        x = float(adjdata[i+1])
                        y = float(adjdata[i+2])
                        z = float(adjdata[i+3])
                        self.boneAdjustMap[bname].SetRotAdjustment(Vector3(x, y, z))
                        self.boneAdjustMap[bname].rotAdjAuto = False
                        #print " Bone rotate adjust for [%s]: (%.3f, %.3f, %.3f)"%(bname, x, y, z)
                        i += 4
                    elif adjdata[i] == "a" and i + 3 < len(adjdata):
                        x = float(adjdata[i+1])
                        y = float(adjdata[i+2])
                        z = float(adjdata[i+3])
                        self.boneAdjustMap[bname].SetAxisAdjustment(Vector3(x, y, z))
                        self.boneAdjustMap[bname].rotAxisAuto = False
                        #print " Bone axis adjust for [%s]: (%.3f, %.3f, %.3f)"%(bname, x, y, z)
                        i += 4
                    elif adjdata[i] == "s" and i + 1 < len(adjdata):
                        s = float(adjdata[i+1])
                        self.boneAdjustMap[bname].rotationScale = s
                        #print " Bone rotate scale adjust for [%s]: %.3f"%(bname, s)
                        i += 2
                    else:
                        raise Exception("BoneRotAxisAdjust setting for [%s] format error!"%bname)
            
            # done
            self.boneDefineFilename = filename

        except Exception, e:
            traceback.print_exc()
            print("Error in load bone setting: " + str(e))
            raise e

    def forceBoneSync(self):
        if self.PoseController:
            self.PoseController.DoBoneSync(True)

    def cleanup(self):
        try:
            #self.resetPose()
            #self.forceBoneSync()
            self.restoreWeightBone()
        except:
            pass
        try:
            self.setEnable(False)
            if self.vmdBoneRoot:
                self.vmdBoneRoot.Destroy(self.vmdBoneRoot)
            self.cleanBoneGizmos()
            self.reset()
        except:
            pass

    def exportSetting(self):
        setting = {}
        # export general setting
        setting["boneDefine"] = os.path.relpath(self.boneDefineFilename, os.path.split(__file__)[0])
        setting["filename"] = self.vmdFilePath
        setting["charScale"] = str(self.charScale)
        setting["interCons"] = str(self.interpolateConsecutive)
        setting["delay"] = str(self.delay)
        setting["enable"] = str(self.Enable)
        # export ik setting
        ikst = {}
        for iks in self.ikSettingMap.values():
            ikst[iks.name] = iks.enable
        setting["ikEnable"] = script2string(ikst)
        setting["ikSolver"] = ""
        # export customized bone adjust, replace customizedBoneRot and customizedBonePos from MMDDv2
        setting["customizedBoneAdjust"] = []
        for bname in self.boneAdjustDirtySet:
            adjinfo = {}
            adjinfo["name"] = bname
            if self.boneAdjustMap[bname].rotAdjAuto != self.boneAdjustMapBackup[bname].rotAdjAuto or self.boneAdjustMap[bname].rotAdjustmentVec != self.boneAdjustMapBackup[bname].rotAdjustmentVec:
                adjinfo["rotadj"] = self.boneAdjustMap[bname].rotAdjustmentVec
            if self.boneAdjustMap[bname].rotAxisAuto != self.boneAdjustMapBackup[bname].rotAxisAuto or self.boneAdjustMap[bname].rotAxisAdjustmentVec != self.boneAdjustMapBackup[bname].rotAxisAdjustmentVec:
                adjinfo["axisadj"] = self.boneAdjustMap[bname].rotAxisAdjustmentVec
            if self.boneAdjustMap[bname].rotationScale != self.boneAdjustMapBackup[bname].rotationScale:
                adjinfo["rotscale"] = self.boneAdjustMap[bname].rotationScale
            if self.boneAdjustMap[bname].basePosition != self.boneAdjustMapBackup[bname].basePosition:
                adjinfo["posadj"] = self.boneAdjustMap[bname].basePosition
            if self.boneAdjustMap[bname].positionLocalScale != self.boneAdjustMapBackup[bname].positionLocalScale:
                adjinfo["posscale"] = self.boneAdjustMap[bname].positionLocalScale
            setting["customizedBoneAdjust"].append(script2string(adjinfo))
        # export customized frame, is for MMDDv1 only
        # setting["customizedFrame"] = []
        # format in array:
        #   {"boneDummyName#frameNo": script2string(boneKeyframe.exportSetting())}
        #   or
        #   {"boneDummyName#frameNo": ""}
        #   when user deleted a keyframe from org vmd
        # export frames, from MMDDv2
        if self.keyframeCustomized:
            setting["keyframes"] = []
            for vname in self.boneKeyFrames:
                for bkf in self.boneKeyFrames[vname]:
                    setting["keyframes"].append(bkf.exportSetting())
        # export baked data, from MMDDv2
        if self.Baked:
            setting["bakedData"] = self.exportBakedData()
        # export bone enable
        setting["boneEnable"] = {}
        for bname in self.boneNameList:
            bDefine = self.boneDefineMap[bname]
            if not bDefine.enable:
                setting["boneEnable"][bname] = "0"
        # export twist disperse
        setting["twistDisperse"] = {}
        for tdi in self.twistDisperse:
            if not tdi.enable or tdi.disperseRate != tdi.disperseRateBackup:
                setting["twistDisperse"][tdi.name] = "(%d, %.2f)"%(1 if tdi.enable else 0, tdi.disperseRate)
        # done
        return setting

    @staticmethod
    def parseFromSetting(setting, actor, vmdFrameBuffer):
        def parseBool(txt):
            return bool(eval(txt))
        # import bonesetting
        if setting.has_key("boneDefine"):
            bsFilename = setting["boneDefine"]
        else:
            bsFilename = None
        # build controller
        mc = MotionController(bsFilename)
        # import bone enable
        if setting.has_key("boneEnable"):
            for bename in setting["boneEnable"].keys():
                mc.boneDefineMap[bename].enable = False
        # bind chara
        mc.bindChara(actor)
        # import general setting
        mc.setCharScale(float(setting["charScale"]))
        mc.delay = float(setting["delay"])
        mc.setEnable(parseBool(setting["enable"]))
        if "interCons" in setting:
            mc.interpolateConsecutive = parseBool(setting["interCons"])
        # import ik setting
        ikEnableDic = eval(setting["ikEnable"])
        for ikname in ikEnableDic.keys():
            mc.ikSettingMap[ikname].enable = ikEnableDic[ikname]
        # import customized bone rotate/axis/scale adjust, from MMDDv1
        if setting.has_key("customizedBoneRot"):
            for castr in setting["customizedBoneRot"]:
                caraw = eval(castr)
                bname = caraw["name"]
                tgtAdj = mc.boneAdjustMap[bname]
                if caraw.has_key("rotadj"):
                    tgtAdj.rotAdjAuto = False
                    tgtAdj.SetRotAdjustment(Vector3(caraw["rotadj"][0], caraw["rotadj"][1], caraw["rotadj"][2]))
                if caraw.has_key("axisadj"):
                    tgtAdj.rotAxisAuto = False
                    tgtAdj.SetAxisAdjustment(Vector3(caraw["axisadj"][0], caraw["axisadj"][1], caraw["axisadj"][2]))
                if caraw.has_key("rotscale"):
                    tgtAdj.SetRotScale(caraw["rotscale"])
                mc.boneAdjustDirtySet.add(bname)
        # import customized bone position/scale adjust, from MMDDv1
        if setting.has_key("customizedBonePos"):
            for castr in setting["customizedBonePos"]:
                caraw = eval(castr)
                bname = caraw["name"]
                tgtAdj = mc.boneAdjustMap[bname]
                if caraw.has_key("posadj"):
                    tgtAdj.basePosition = Vector3(caraw["posadj"][0], caraw["posadj"][1], caraw["posadj"][2])
                if caraw.has_key("posscale"):
                    tgtAdj.SetPosLocalScale(caraw["posscale"])
                mc.boneAdjustDirtySet.add(bname)
        # import customized bone adjust, from MMDDv2
        if setting.has_key("customizedBoneAdjust"):
            for castr in setting["customizedBoneAdjust"]:
                caraw = eval(castr)
                bname = caraw["name"]
                tgtAdj = mc.boneAdjustMap[bname]
                if caraw.has_key("rotadj"):
                    tgtAdj.rotAdjAuto = False
                    tgtAdj.SetRotAdjustment(Vector3(caraw["rotadj"][0], caraw["rotadj"][1], caraw["rotadj"][2]))
                if caraw.has_key("axisadj"):
                    tgtAdj.rotAxisAuto = False
                    tgtAdj.SetAxisAdjustment(Vector3(caraw["axisadj"][0], caraw["axisadj"][1], caraw["axisadj"][2]))
                if caraw.has_key("rotscale"):
                    tgtAdj.SetRotScale(caraw["rotscale"])
                if caraw.has_key("posadj"):
                    tgtAdj.basePosition = Vector3(caraw["posadj"][0], caraw["posadj"][1], caraw["posadj"][2])
                if caraw.has_key("posscale"):
                    tgtAdj.SetPosLocalScale(caraw["posscale"])
                mc.boneAdjustDirtySet.add(bname)
        # load keyframes
        filename = setting["filename"]
        kfCus = False
        if setting.has_key("keyframes"):
            keyframes = [VmdBoneKeyFrame.parseFromSetting(kfs) for kfs in setting["keyframes"]]
            kfCus = True
        elif filename in vmdFrameBuffer:
            vmdResult = vmdFrameBuffer[filename]
            keyframes = vmdResult[1]
        else:
            vmdResult = VmdLib.loadVmd(filename)
            if len(vmdResult) == 1:
                raise Exception(vmdResult[0])
            keyframes = vmdResult[1]
            vmdFrameBuffer[filename] = vmdResult
        mc.importBoneKeyframes(filename, keyframes)
        mc.keyframeCustomized = kfCus
        # import customized frame, optional in MMDDv2
        if setting.has_key("customizedFrame"):
            frameToUpdate = set()
            frameToDelete = set()
            for cfstr in setting["customizedFrame"]:
                cfraw = eval(cfstr)
                if len(cfraw.values()[0]) > 0:
                    fsetting = eval(cfraw.values()[0])
                    frame = VmdBoneKeyFrame.parseFromSetting(fsetting)
                    frameToUpdate.add(frame)
                else:
                    dfn = cfraw.keys()[0].split("#")
                    vbname = mc.boneDefineMap[dfn[0]].vmdBone
                    frameNo = int(dfn[1])
                    frameToDelete.add((vbname, frameNo))
            if len(frameToUpdate):
                mc.updateKeyframes(frameToUpdate)
            if len(frameToDelete):
                mc.deleteKeyframes(frameToDelete)
        # import baked data, from MMDDv2
        if setting.has_key("bakedData"):
            mc.importBakedData(setting["bakedData"])
        # import twist disperse
        if setting.has_key("twistDisperse"):
            for tdstr in setting["twistDisperse"].keys():
                tdraw = eval(setting["twistDisperse"][tdstr])
                for tdi in mc.twistDisperse:
                    if tdi.name == tdstr:
                        tdi.enable = bool(tdraw[0])
                        tdi.disperseRate = tdraw[1]
        # done
        return mc

    def bindChara(self, actor):

        def preCreateVmdBone(actor):
            # chara only
            if self.boneType != "chara":
                return None
            # save old data
            preSetData = GData()
            preSetData.kinematic = actor.get_kinematic()
            preSetData.anime = actor.get_animate()
            preSetData.neck = actor.get_look_neck()
            preSetData.hand = actor.get_hand_ptn()
            # set to t-pose
            actor.set_kinematic(0)
            actor.animate(0, 0, 0)
            actor.set_look_neck(3)
            actor.set_hand_ptn((0, 0))
            # done
            return preSetData
        def postCreateVmdBone(actor, preSetData):
            # chara only
            if self.boneType != "chara":
                return
            # restore old data
            actor.set_kinematic(preSetData.kinematic)
            actor.animate(preSetData.anime[0], preSetData.anime[1], preSetData.anime[2])
            actor.set_look_neck(3)  # no restore for neck
            actor.set_hand_ptn(preSetData.hand)

        # normalize actor and oci data type
        from Studio import OCIChar, OCIItem, OCIFolder
        if self.boneType == "chara" and isinstance(actor, HSNeoOCIChar):
            oci = actor.objctrl
            actor = actor.as_actor
        elif self.boneType == "chara" and isinstance(actor, OCIChar):
            oci = actor
            actor = HSNeoOCIChar(actor).as_actor
        elif self.boneType in {"item", "folder"} and isinstance(actor, HSNeoOCIProp):
            oci = actor.objctrl
            actor = actor.as_prop
        elif self.boneType in {"item", "folder"} and isinstance(actor, Prop):
            oci = actor.objctrl
        elif self.boneType == "item" and isinstance(actor, OCIItem):
            oci = actor
            actor = HSNeoOCIProp(actor).as_prop
        elif self.boneType == "folder" and isinstance(actor, OCIFolder):
            oci = actor
            actor = HSNeoOCIProp(actor).as_prop
        else:
            raise Exception("Motion bind chara ERROR: wrong actor type!! actor=%s, type=%s, define file=%s, target bone type=%s"%(actor, type(actor), self.boneDefineFilename, self.boneType))
        
        # get pose controller from plugin
        import extplugins
        self.charaAnimePlugin = extplugins.CharaAnime()
        if not self.charaAnimePlugin.isDetected:
            self.charaAnimePlugin = None
            raise Exception("Can not load extern plugin CharaAnime.dll")
        else:
            cpc = self.PoseController

        # start bind, create vmd dummy bone
        self.ociChar = oci
        self.autoCharScale()
        #tmpData = preCreateVmdBone(actor)
        self.createVmdBone()
        #postCreateVmdBone(actor, tmpData)

    def autoCharScale(self):
        if not self.ociChar:
            print "autoCharScale error: bind a chara first!"
            return
        if self.boneType == "chara":
            chara = self.ociChar.charInfo
            bodyBone = chara.objBodyBone
            heightScaleBone = MotionController.searchObjName(bodyBone, self.charaHeightBone)
            heightScale = heightScaleBone.transform.localScale.y
            objScale = self.ociChar.oiCharInfo.changeAmount.scale.y
            self.setCharScale(heightScale * objScale)
        else:
            self.setCharScale(1)

    def setCharScale(self, cscale):
        self.charScale = cscale
        for bname in self.boneNameList:
            self.boneAdjustMap[bname].SetPosGlobalScale(self.axisScale * self.charScale)

    def createVmdBone(self):
        # create vmd bone root, locate acturl bone root
        if self.boneType == "chara":
            chara = self.ociChar.charInfo
            self.vmdBoneRoot = MotionController.searchObjName(chara.objBody.transform.parent, "VmdDummyBone")
            if self.vmdBoneRoot:
                print 'Unexpected existing VmdDummyBone, Destroy it.'
                self.vmdBoneRoot.Destroy(self.vmdBoneRoot)
            self.vmdBoneRoot = GameObject("VmdDummyBone")
            self.vmdBoneRoot.transform.parent = chara.objBody.transform.parent
            self.vmdBoneRoot.transform.localPosition = chara.objBody.transform.localPosition
            self.vmdBoneRoot.transform.localRotation = chara.objBody.transform.localRotation
            # locate bone
            bodyBone = chara.objBodyBone
        elif self.boneType == "item" or self.boneType == "folder":
            item = self.ociChar.objectItem
            self.vmdBoneRoot = MotionController.searchObjName(item, "VmdDummyBone")
            if self.vmdBoneRoot:
                print 'Unexpected existing VmdDummyBone, Destroy it.'
                self.vmdBoneRoot.Destroy(self.vmdBoneRoot)
            self.vmdBoneRoot = GameObject("VmdDummyBone")
            self.vmdBoneRoot.transform.parent = item.transform
            self.vmdBoneRoot.transform.localPosition = Vector3.zero
            self.vmdBoneRoot.transform.localRotation = Quaternion.identity
            # locate bone
            bodyBone = item
        else:
            raise Exception("Unknown bone type: " + self.boneType)
        self.actualBoneRootGo = bodyBone

        # create every bone
        self.vmdBones = {}
        for bname in self.boneNameList:
            # create bone
            boneGo = self.createBone(self.vmdBoneRoot, bname)
            if not boneGo:
                raise Exception("!!!! fail to create bone %s !!!!"%bname)
            self.vmdBones[bname] = boneGo

        # resort weight bone
        self.resortWeightBone(bodyBone)

        # backup actual bone
        self.backupWeightBone()

        # prepare actual bone process
        if self.prepareBoneEnableGet:
            print "----- PrepareBoneBeforeCreateDummyBone: output start -----"
            for bname in self.weightBoneList:
                wname = self.boneDefineMap[bname].weightBone
                chaBoneT = MotionController.trySearchObjNameWithLimit(bodyBone, wname, bodyBone).transform
                if chaBoneT.localRotation != Quaternion.identity:
                    lre = chaBoneT.localRotation.eulerAngles
                    print "%s=(%.3f,%.3f,%.3f)"%(chaBoneT.name, lre.x, lre.y, lre.z)
            print "----- PrepareBoneBeforeCreateDummyBone: output end   -----"
        if self.prepareBoneEnableSet:
            for bname in self.weightBoneList:
                wname = self.boneDefineMap[bname].weightBone
                wname_key = wname.lower()
                if self.prepareBoneSetting.has_key(wname_key):
                    preLocalRot = self.prepareBoneSetting[wname_key]
                elif not self.prepareBoneSetListedOnly:
                    preLocalRot = Quaternion.identity
                else:
                    continue
                chaBoneObj = MotionController.trySearchObjNameWithLimit(bodyBone, wname, bodyBone)
                chaBoneObj.transform.localRotation = preLocalRot
            print "Auto adjusted bone rotation before locate dummy bone"

        # locate every bone
        for bname in self.boneNameList:
            try:
                bDefine = self.boneDefineMap[bname]
                pMode = bDefine.positionMode
                pData = bDefine.positionData
                boneGo = self.vmdBones[bname]

                if pMode == "b":
                    # bind to bone
                    chaBoneObj = MotionController.trySearchObjNameWithLimit(bodyBone, pData, bodyBone)
                    chaBoneTran = chaBoneObj.transform
                    boneGo.transform.position = chaBoneTran.position
                elif pMode == "bbs":
                    # bind to bone-to-bone-with-scale
                    chaBoneObj1 = MotionController.trySearchObjNameWithLimit(bodyBone, pData[0], bodyBone)
                    chaBoneObj2 = MotionController.trySearchObjNameWithLimit(bodyBone, pData[1], bodyBone)
                    p1 = chaBoneObj1.transform.position
                    p2 = chaBoneObj2.transform.position
                    boneGo.transform.position = Vector3((p2.x-p1.x)*pData[2]+p1.x, (p2.y-p1.y)*pData[2]+p1.y, (p2.z-p1.z)*pData[2]+p1.z)
                elif pMode == "bo":
                    # bone and offset
                    chaBoneObj = MotionController.trySearchObjNameWithLimit(bodyBone, pData[0], bodyBone)
                    chaBoneTran = chaBoneObj.transform
                    boneGo.transform.position = chaBoneTran.position + pData[1]
                elif pMode == "br":
                    # bone and reset
                    chaBoneObj = MotionController.trySearchObjNameWithLimit(bodyBone, pData[0], bodyBone)
                    chaBoneTran = chaBoneObj.transform
                    refPos = chaBoneTran.position
                    if pData[1][0] != None:
                        refPos = Vector3(pData[1][0], refPos.y, refPos.z)
                    if pData[1][1] != None:
                        refPos = Vector3(refPos.x, pData[1][1], refPos.z)
                    if pData[1][2] != None:
                        refPos = Vector3(refPos.x, refPos.y, pData[1][2])
                    boneGo.transform.position = refPos
                    print "br bone %s, set to %s"%(bname, refPos)
                else:
                    raise Exception("Unknown position mode = " + pMode)

                self.boneAdjustMap[bname].basePosition = boneGo.transform.localPosition
            except:
                traceback.print_exc()
                raise Exception("!!!! fail to locate bone %s !!!!"%bname)

        # create IK
        self.createIK()

        # complete twist disperse info
        self.createTwistDisperse()

        # bind bone weight
        self.bindWeightBone()

        # auto bone rotate/axis adjust setting
        self.autoBoneRotateAdjust()
        self.autoBoneAxisAdjust()
        # bone adjust setup and backup
        for bname in self.boneNameList:
            self.boneAdjustMap[bname].SetPosGlobalScale(self.axisScale * self.charScale)
            self.boneAdjustMapBackup[bname] = self.boneAdjustMap[bname].copy()
        
        # init pose and frame
        self.initPose()
        self.setInitFrame()

    def createBone(self, rootObj, name):
        bDefine = self.boneDefineMap[name]
        if not bDefine.parent:
            # root bone
            newBoneGo = MotionController.searchObjName(rootObj.transform, name)
            if not newBoneGo:
                newBoneGo = GameObject(name)
                newBoneGo.transform.parent = rootObj.transform
                #print "build root bone [%s] under [%s]"%(name, rootObj.transform.name)
            else:
                #print "root bone [%s] exists."%name
                pass
            newBoneGo.transform.localPosition = Vector3.zero
            newBoneGo.transform.localRotation = Quaternion.identity
            return newBoneGo
        # child bone
        parentGo = MotionController.searchObjName(rootObj.transform, bDefine.parent)
        if not parentGo:
            print "Parent bone of non-root bone %s is missing!"%name
            return None
        newBoneGo = MotionController.searchObjName(parentGo.transform, name)
        if not newBoneGo:
            newBoneGo = GameObject(name)
            newBoneGo.transform.parent = parentGo.transform
            #print "build bone [%s] under [%s]"%(name, parentGo.transform.name)
        else:
            #print "bone [%s] exists."%name
            pass
        newBoneGo.transform.localPosition = Vector3.zero
        newBoneGo.transform.localRotation = Quaternion.identity
        return newBoneGo

    def createIK(self):
        from System import Array
        from CharaAnime import CCDIKSolver

        try:
            self.vmdIKs = {}
            for bname in self.ikBoneList:
                for iksetting in self.ikSettingMap.values():
                    if bname in iksetting.ikBone:
                        break
                if iksetting.solver == "CCDIKSolver":
                    iks = CCDIKSolver()
                    iks.ikBone = self.getVmdBone(bname).transform

                    boneIkData = self.boneDefineMap[bname].ikData
                    iks.target = self.getVmdBone(boneIkData[0]).transform
                    ikChain = []
                    for i in range(1, len(boneIkData)):
                        ikChain.append(self.getVmdBone(boneIkData[i]).transform)
                    iks.chains = Array[Transform](ikChain)

                    # setup other parameter
                    if hasattr(iksetting, "controlWeight"):
                        iks.controll_weight = iksetting.controlWeight
                    if hasattr(iksetting, "iterations"):
                        iks.iterations = iksetting.iterations
                    if hasattr(iksetting, "minDelta"):
                        iks.minDelta = iksetting.minDelta
                    if hasattr(iksetting, "lastFrameWeight"):
                        iks.lastFrameWeight = iksetting.lastFrameWeight
                    if hasattr(iksetting, "ikRotationWeight"):
                        iks.ikRotationWeight = iksetting.ikRotationWeight
                    if hasattr(iksetting, "minKneeRot"):
                        iks.minKneeRot = iksetting.minKneeRot
                    if hasattr(iksetting, "useLeg"):
                        iks.useLeg = iksetting.useLeg

                    self.vmdIKs[bname] = (iksetting.name, iks)
                elif iksetting.solver == "MMDIKSolver":
                    iks = MMDIKSolverWrapper()
                    iks.ikBone = self.getVmdBone(bname)

                    boneIkData = self.boneDefineMap[bname].ikData
                    iks.ikTgt = self.getVmdBone(boneIkData[0])
                    iks.ikChain = [self.getVmdBone(bn) for bn in boneIkData[1:]]
                    iks.iterations = iksetting.iterations
                    iks.stepAngle = iksetting.stepAngle
                    iks.limits = iksetting.limits

                    serr = iks.Verify()
                    if serr:
                        raise Exception(serr)
                    self.vmdIKs[bname] = (iksetting.name, iks)

                elif iksetting.solver == "MMDIKSolverNoLimit":
                    iks = MMDIKSolverNoLimit()
                    iks.ikBone = self.getVmdBone(bname).transform

                    boneIkData = self.boneDefineMap[bname].ikData
                    iks.ikTgt = self.getVmdBone(boneIkData[0]).transform
                    iks.ikChain = [self.getVmdBone(bn).transform for bn in boneIkData[1:]]
                    iks.iterations = iksetting.iterations
                    iks.stepAngle = iksetting.stepAngle
                    
                    serr = iks.Verify()
                    if serr:
                        raise Exception(serr)
                    self.vmdIKs[bname] = (iksetting.name, iks)
                    
                elif iksetting.solver == "MMDIKSolverLeg":
                    iks = MMDIKSolverLeg()
                    iks.ikBone = self.getVmdBone(bname).transform

                    boneIkData = self.boneDefineMap[bname].ikData
                    iks.ikTgt = self.getVmdBone(boneIkData[0]).transform
                    iks.ikChain = [self.getVmdBone(boneIkData[1]).transform, self.getVmdBone(boneIkData[2]).transform]
                    iks.iterations = iksetting.iterations
                    iks.stepAngle = iksetting.stepAngle
                    iks.minKneeRot = iksetting.minKneeRot
                    
                    serr = iks.Verify()
                    if serr:
                        raise Exception(serr)
                    self.vmdIKs[bname] = (iksetting.name, iks)
        except:
            traceback.print_exc()

    def createTwistDisperse(self):
        for tdi in self.twistDisperse:
            try:
                tdi.twistBoneTransform = self.getVmdBone(tdi.twistBoneName).transform
                tdi.disperseBoneTransform = self.getVmdBone(tdi.disperseBoneName).transform
                tdi.enable = tdi.disperseRate > 0
            except:
                traceback.print_exc()
                tdi.enable = False                

    def resortWeightBone(self, actualRootGo):
        # try to fix update weight bone sequence if different from bone define sequence
        def createBoneTree(gobj, parentNode, invWeightBoneNameMap):
            boneTreeNode = GData()
            boneTreeNode.gameObject = gobj
            boneTreeNode.parent = parentNode
            boneTreeNode.children = []
            if gobj.name in invWeightBoneNameMap:
                boneTreeNode.weightBone = invWeightBoneNameMap[gobj.name]
                boneTreeNode.orgIndex = self.weightBoneList.index(boneTreeNode.weightBone)
            else:
                boneTreeNode.weightBone = None
                boneTreeNode.orgIndex = None
            for cn in range(gobj.transform.childCount):
                ctrans = gobj.transform.GetChild(cn)
                cgobj = ctrans.gameObject
                cNode = createBoneTree(cgobj, boneTreeNode, invWeightBoneNameMap)
                boneTreeNode.children.append(cNode)
            return boneTreeNode
        def resortByTree(node, output):
            if node.weightBone:
                output.append(node.weightBone)
            for cNode in node.children:
                resortByTree(cNode, output)
        
        invWeightBoneNameMap = {}
        for bname in self.weightBoneList:
            wname = self.boneDefineMap[bname].weightBone
            invWeightBoneNameMap[wname] = bname
        boneTree = createBoneTree(actualRootGo, None, invWeightBoneNameMap)
    
        """
        print "----- resort weight bone: old sequence (%d)"%len(self.weightBoneList)
        for bname in self.weightBoneList:
            print " " + bname
        """
        self.weightBoneList = []
        resortByTree(boneTree, self.weightBoneList)
        """
        print "----- resort weight bone: new sequence (%d)"%len(self.weightBoneList)
        for bname in self.weightBoneList:
            print " " + bname
        print "----- resort weight bone: Done"
        """

    def backupWeightBone(self):
        self.weightBoneBackup = {}
        for bname in self.weightBoneList:
            bdefine = self.boneDefineMap[bname]
            chaBoneObj = MotionController.trySearchObjNameWithLimit(self.actualBoneRootGo, bdefine.weightBone, self.actualBoneRootGo)
            if not chaBoneObj:
                raise Exception("Unable to find target weight bone [%s] from [%s]"%(bdefine.weightBone, self.actualBoneRootGo.name))
            bkdata = []
            if bdefine.isRotation or (bdefine.isTrack and "r" in bdefine.trackData[0]):
                bkdata = [chaBoneObj.transform.localRotation]
            else:
                bkdata = [None]
            if bdefine.isPosition or (bdefine.isTrack and "t" in bdefine.trackData[0]):
                bkdata.append(chaBoneObj.transform.localPosition)
            else:
                bkdata.append(None)
            self.weightBoneBackup[bname] = tuple(bkdata)
            
    def restoreWeightBone(self, tgtBone = None):
        def restoreBone(bname):
            backupData = self.weightBoneBackup[bname]
            chaBoneObj = MotionController.trySearchObjNameWithLimit(self.actualBoneRootGo, self.boneDefineMap[bname].weightBone, self.actualBoneRootGo)
            if backupData[0]:
                chaBoneObj.transform.localRotation = backupData[0]
            if backupData[1]:
                chaBoneObj.transform.localPosition = backupData[1]
        if tgtBone:
            if tgtBone in self.weightBoneList:
                restoreBone(tgtBone)
        else:
            for bname in self.weightBoneList:
                restoreBone(bname)

    def bindWeightBone(self):
        if not self.PoseController:
            raise Exception("PoseController for %s not found!"%self.CharName)
        bodyBone = self.actualBoneRootGo
        self.PoseController.ClearBoneSyncSetting()
        for bname in self.weightBoneList:
            bDefine = self.boneDefineMap[bname]
            if not bDefine.enable: continue
            boneGo = self.getVmdBone(bname)
            chaBoneObj = MotionController.trySearchObjNameWithLimit(bodyBone, bDefine.weightBone, bodyBone)
            flag = 0
            if bDefine.isPosition:
                if bDefine.weightSyncMode == "local":
                    flag |= 0x00000007  # local position
                else:
                    flag |= 0x00070000  # global position
            if bDefine.isRotation:
                if bDefine.weightSyncMode == "local":
                    flag |= 0x00000070  # local rotation
                else:
                    flag |= 0x00700000  # global rotation
            self.PoseController.AddBoneSyncSetting(chaBoneObj, boneGo, flag)

    def processIK(self, bname):
        iksetting = self.ikSettingMap[self.vmdIKs[bname][0]]
        ik = self.vmdIKs[bname][1]
        if iksetting.enable:
            if iksetting.solver == "CCDIKSolver":
                # call solve method
                if iksetting.solveMethod == "SolveLeg2":
                    ik.SolveLeg2()
                else:
                    ik.Solve()
            elif iksetting.solver in {"MMDIKSolverNoLimit", "MMDIKSolverLeg", "MMDIKSolver"}:
                # call MMD solve method
                ik.Solve()

    def processTrack(self, bname):
        trackdata = self.boneDefineMap[bname].trackData
        tckPtn = trackdata[0]
        tckScale = trackdata[1]
        srcName = trackdata[2]

        bTran = self.getVmdBone(bname).transform
        srcBoneTran = self.getVmdBone(srcName).transform
        if "r" in tckPtn:
            rotBase = bTran.localRotation
            rotTSrc = srcBoneTran.localRotation
            rot = Quaternion.SlerpUnclamped(Quaternion.identity, rotTSrc, tckScale) * rotBase
            bTran.localRotation = rot
        if "p" in tckPtn:
            rotBase = bTran.localPosition
            rotTSrc = srcBoneTran.localPosition
            pos = Vector3(rotBase.x + rotTSrc.x * tckScale, rotBase.y + rotTSrc.y * tckScale, rotBase.z + rotTSrc.z * tckScale)
            bTran.localPosition = pos

    def processTwistDisperse(self):
        for tdi in self.twistDisperse:
            if tdi.enable:
                orgQ = tdi.twistBoneTransform.localRotation
                remainQ = Quaternion.Lerp(Quaternion.identity, orgQ, 1.0 - tdi.disperseRate)
                disperseQ = orgQ * Quaternion.Inverse(remainQ)
                tdi.twistBoneTransform.localRotation = remainQ
                tdi.disperseBoneTransform.localRotation = tdi.disperseBoneTransform.localRotation * disperseQ

    def initPose(self):
        self.poseData = {}
        for bname in self.boneNameList:
            bdefine = self.boneDefineMap[bname]
            if bdefine.isRotation or (bdefine.isTrack and "r" in bdefine.trackData[0]):
                self.poseData[bname] = [self.boneAdjustMap[bname].GetAdjustedRotation(Quaternion.identity)]
            else:
                self.poseData[bname] = [None]
            if bdefine.isPosition or (bdefine.isTrack and "t" in bdefine.trackData[0]):
                self.poseData[bname].append(self.boneAdjustMap[bname].GetAdjustedPosition(Vector3.zero))
            else:
                self.poseData[bname].append(None)

    def resetPose(self):
        for bname in self.boneNameList:
            btran = self.getVmdBone(bname).transform
            if self.poseData[bname][0]:
                #self.poseData[bname][0] = Quaternion.identity
                self.poseData[bname][0] = self.boneAdjustMap[bname].GetAdjustedRotation(Quaternion.identity)
                btran.localRotation = self.poseData[bname][0]
            if self.poseData[bname][1]:
                #self.poseData[bname][1] = self.boneAdjustMap[bname].basePosition
                self.poseData[bname][1] = self.boneAdjustMap[bname].GetAdjustedPosition(Vector3.zero)
                btran.localPosition = self.poseData[bname][1]

    def setInitFrame(self):
        self.debakeFrame()
        self.vmdFilePath = "InitPose"
        self.boneKeyFrames = {}
        for bname in self.boneNameList:
            vname = self.boneDefineMap[bname].vmdBone
            zf = VmdBoneKeyFrame()
            self.boneKeyFrames[vname] = [zf]

    def updatePose(self):
        # pose -> bone
        for bname in self.boneNameList:
            bDefine = self.boneDefineMap[bname]
            if not bDefine.enable: continue
            bpose = self.poseData[bname]
            btran = self.getVmdBone(bname).transform
            # rotation
            if bpose[0] != None:
                if bDefine.isLimitAxis:
                    refAx, lmtAx = bDefine.limitAxisData
                    angle0, axis = bpose[0].ToAngleAxis()
                    if (axis + refAx).magnitude < 1:
                        angle1 = -angle0
                    else:
                        angle1 = angle0
                    btran.localRotation = Quaternion.AngleAxis(angle1, lmtAx)
                else:
                    btran.localRotation = bpose[0]
            # position
            if bpose[1] != None:
                btran.localPosition = bpose[1]
            # ik
            if bname in self.ikBoneList:
                self.processIK(bname)
            # track
            if bDefine.isTrack:
                self.processTrack(bname)
        # twist disperse
        self.processTwistDisperse()

    def setFrame(self, frameNo):
        if self.delay > 0:
            frameNo -= self.delay * 30.0

        if self.Baked:
            # get data from baked frames
            if frameNo < 0:
                frameNo = 0
            if frameNo > self.maxFrameNo:
                frameNo = self.maxFrameNo
            i = float(frameNo) / float(self.maxFrameNo) * self.bakedFrameMaxIndex
            i = int(i)

            # reference: self.bakedFrames = (tuple(vmdBoneTrans), tuple(vmdBoneTransFlag), tuple(vmdBoneFrames))
            vmdBoneTrans = self.bakedFrames[0]
            vmdBoneTransFlag = self.bakedFrames[1]
            vmdBoneFrames = self.bakedFrames[2][i]
            for n in range(len(vmdBoneTrans)):
                if vmdBoneTransFlag[n][0]:
                    vmdBoneTrans[n].position = vmdBoneFrames[n][0]
                if vmdBoneTransFlag[n][1]:
                    vmdBoneTrans[n].rotation = vmdBoneFrames[n][1]

        elif self.indexMode:
            # use work bone index buffer to update
            for wbi in self.workBoneIndexBuffer:
                # get frame
                if wbi.needGetFrame:
                    self.getVmdBoneFrameIndexed(frameNo, wbi)
                    # rotation
                    if wbi.needRotation:
                        boneRot = wbi.boneAdjust.GetAdjustedRotation(wbi.wRotation)
                        wbi.poseData[0].Set(boneRot.x, boneRot.y, boneRot.z, boneRot.w)
                        if wbi.needLimitAxis:
                            angle, axis = boneRot.ToAngleAxis()
                            if (axis + wbi.referAxis).magnitude < 1:
                                angle = -angle
                            wbi.transform.localRotation = Quaternion.AngleAxis(angle, wbi.limitAxis)
                        else:
                            wbi.transform.localRotation = boneRot
                    # position
                    if wbi.needPosition:
                        bonePos = wbi.boneAdjust.GetAdjustedPosition(wbi.wPosition)
                        wbi.poseData[1] = bonePos
                        #wbi.poseData[1].Set(bonePos.x, bonePos.y, bonePos.z)
                        wbi.transform.localPosition = bonePos
                else:
                    if wbi.needRotation:
                        wbi.transform.localRotation = wbi.wRotation
                    if wbi.needPosition:
                        wbi.transform.localPosition = wbi.wPosition
                # ik
                if wbi.needIK:
                    wbi.solveFunc()
                # track
                if wbi.needTrackRot:
                    rotBase = wbi.transform.localRotation
                    rotTSrc = wbi.trackSrcTransform.localRotation
                    wbi.transform.localRotation = Quaternion.SlerpUnclamped(Quaternion.identity, rotTSrc, wbi.trackScale) * rotBase
                if wbi.needTrackPos:
                    posBase = wbi.transform.localPosition
                    posTSrc = wbi.trackSrcTransform.localPosition
                    wbi.transform.localPosition = Vector3(posBase.x + posTSrc.x * wbi.trackScale, posBase.y + posTSrc.y * wbi.trackScale, posBase.z + posTSrc.z * wbi.trackScale)
            # twist disperse
            for tdi in self.twistDisperse:
                if tdi.enable and tdi.twistBoneTransform.localRotation != Quaternion.identity:
                    orgQ = tdi.twistBoneTransform.localRotation
                    remainQ = Quaternion.Lerp(Quaternion.identity, orgQ, 1.0 - tdi.disperseRate)
                    disperseQ = orgQ * Quaternion.Inverse(remainQ)
                    tdi.twistBoneTransform.localRotation = remainQ
                    tdi.disperseBoneTransform.localRotation = tdi.disperseBoneTransform.localRotation * disperseQ

        else:
            # legacy mode
            for vmdBoneName in self.boneKeyFrames.keys():
                bname = self.vmd2BoneMap[vmdBoneName]

                # get target frame
                tgtFrame = self.getVmdBoneFrame(frameNo, vmdBoneName)
                tgtPose = self.poseData[bname]
                if tgtPose[0] != None:
                    tgtPose[0] = self.boneAdjustMap[bname].GetAdjustedRotation(tgtFrame.rotation)
                if tgtPose[1] != None:
                    tgtPose[1] = self.boneAdjustMap[bname].GetAdjustedPosition(tgtFrame.position)

            # process IK and track
            self.updatePose()
            
    def autoBoneRotateAdjust(self):
        def pq(q):
            v = q.eulerAngles
            return "%f,%f,%f"%(v.x, v.y, v.z)
        if not self.enableAutoRotAdj:
            return
        print "-- Auto Bone Rotate Adjust [%s] --"%self.boneDefineFilename
        for bname in self.boneNameList:
            boneAdjust = self.boneAdjustMap[bname]
            boneDefine = self.boneDefineMap[bname]
            if boneAdjust.rotAdjAuto:
                pname = boneDefine.parent
                if pname:
                    # child one
                    if bname in self.weightBoneList and pname in self.weightBoneList:
                        pbGo = MotionController.searchObjName(self.actualBoneRootGo, self.boneDefineMap[pname].weightBone)
                        wbGo = MotionController.searchObjName(self.actualBoneRootGo, boneDefine.weightBone)
                        q0 = pbGo.transform.rotation
                        q1 = wbGo.transform.rotation
                        aa = Quaternion.Inverse(q0) * q1
                    else:
                        aa = Quaternion.identity
                    print "%s=r,%s"%(bname.upper(), pq(aa))
                else:
                    # root
                    if bname in self.weightBoneList:
                        wbGo = MotionController.searchObjName(self.actualBoneRootGo, boneDefine.weightBone)
                        q0 = self.actualBoneRootGo.transform.rotation
                        q1 = wbGo.transform.rotation
                        aa = Quaternion.Inverse(q0) * q1
                    else:
                        aa = Quaternion.identity
                    print "%s=r,%s"%(bname.upper(), pq(aa))
                boneAdjust.rotAdjustmentVec = aa.eulerAngles
                boneAdjust.rotAdjustment = aa

    def autoBoneAxisAdjust(self):
        for bname in self.boneNameList:
            if self.boneAdjustMap[bname].rotAxisAuto:
                pname = self.boneDefineMap[bname].parent
                if pname:
                    pAdj = self.boneAdjustMap[pname]
                    #autoAdj = pAdj.rotAxisAdjustmentVec - pAdj.rotAdjustmentVec
                    #self.boneAdjustMap[bname].SetAxisAdjustment(autoAdj)
                    autoAdj = Quaternion.Inverse(pAdj.rotAdjustment) * Quaternion.Euler(pAdj.rotAxisAdjustmentVec)
                    self.boneAdjustMap[bname].SetAxisAdjustment(autoAdj.eulerAngles)
                    #if self.boneAdjustMap[bname].rotAxisAdjustment:
                    #    print "bone auto axis adj for %s: %s"%(bname, str(self.boneAdjustMap[bname].rotAxisAdjustmentVec))

    def getVmdBoneFrame(self, tgtFrameNo, boneName):

        def interpolateBoneFrame(tgtFrameNo, prevFrame, nextFrame):

            def isLinear(cuv):
                return cuv[0] == cuv[1] and cuv[2] == cuv[3]

            if not self.interpolateConsecutive and prevFrame.frameNo + 1 == nextFrame.frameNo:
                return prevFrame

            #print "interplate for %s@%d from %d to %d"%(prevFrame.name, tgtFrameNo, prevFrame.frameNo, nextFrame.frameNo)
            tframe = float(tgtFrameNo - prevFrame.frameNo) / float(nextFrame.frameNo - prevFrame.frameNo)
            if isLinear(nextFrame.xCurve):
                tx = tframe
            else:
                tx = VmdLib.mmdCubicBezierInterpolate(nextFrame.xCurve, tframe)
            if isLinear(nextFrame.yCurve):
                ty = tframe
            else:
                ty = VmdLib.mmdCubicBezierInterpolate(nextFrame.yCurve, tframe)
            if isLinear(nextFrame.zCurve):
                tz = tframe
            else:
                tz = VmdLib.mmdCubicBezierInterpolate(nextFrame.zCurve, tframe)
            if isLinear(nextFrame.rCurve):
                tr = tframe
            else:
                tr = VmdLib.mmdCubicBezierInterpolate(nextFrame.rCurve, tframe)

            x = prevFrame.position.x + (nextFrame.position.x - prevFrame.position.x) * tx
            y = prevFrame.position.y + (nextFrame.position.y - prevFrame.position.y) * ty
            z = prevFrame.position.z + (nextFrame.position.z - prevFrame.position.z) * tz
            r = Quaternion.Slerp(prevFrame.rotation, nextFrame.rotation, tr)

            iFrame = VmdBoneKeyFrame()
            iFrame.name = prevFrame.name
            iFrame.frameNo = tgtFrameNo
            iFrame.position = Vector3(x, y, z)
            iFrame.rotation = r
            #print "  -> pos(%.2f, %.2f, %.2f) rot(%s)"%(x, y, z, r.eulerAngles)
            return iFrame

        if self.boneKeyFrames.has_key(boneName):
            frames = self.boneKeyFrames[boneName]
        else:
            return None

        if frames[0].frameNo >= tgtFrameNo:
            return frames[0]
        elif frames[-1].frameNo <= tgtFrameNo:
            return frames[-1]
        else:
            for i in range(len(frames)):
                if frames[i].frameNo == tgtFrameNo:
                    return frames[i]
                elif frames[i].frameNo > tgtFrameNo:
                    # interpolate
                    return interpolateBoneFrame(tgtFrameNo, frames[i-1], frames[i])
            print "??? Error, cannot get Bone frame for %s @ %d"%(frames[0].name, tgtFrameNo)
            return None

    def getVmdBoneFrameIndexed(self, tgtFrameNo, wbi):
        # in work bone index mode
        if wbi.firstFrameNo >= tgtFrameNo:
            # target frame is the first keyframe or before first keyframe
            wbi.prevIndex = 0
            prevFrame = wbi.keyframes[wbi.prevIndex]
            wbi.prevFrameNo = prevFrame.frameNo
            wbi.nextIndex = 1 if len(wbi.keyframes) > 1 else 0
            nextFrame = wbi.keyframes[wbi.nextIndex]
            wbi.nextFrameNo = nextFrame.frameNo
            wbi.wRotation.Set(prevFrame.rotation.x, prevFrame.rotation.y, prevFrame.rotation.z, prevFrame.rotation.w)
            wbi.wPosition.Set(prevFrame.position.x, prevFrame.position.y, prevFrame.position.z)
            return
        elif wbi.lastFrameNo <= tgtFrameNo:
            # target frame is the last keyframe or after last keyframe
            wbi.wRotation.Set(wbi.keyframes[-1].rotation.x, wbi.keyframes[-1].rotation.y, wbi.keyframes[-1].rotation.z, wbi.keyframes[-1].rotation.w)
            wbi.wPosition.Set(wbi.keyframes[-1].position.x, wbi.keyframes[-1].position.y, wbi.keyframes[-1].position.z)
            return
        else:
            # firstFrameNo < tgtFrameNo < lastFrameNo
            if wbi.prevFrameNo < tgtFrameNo < wbi.nextFrameNo:
                # inside current range
                prevFrame = wbi.keyframes[wbi.prevIndex]
                nextFrame = wbi.keyframes[wbi.nextIndex]
            elif tgtFrameNo >= wbi.nextFrameNo:
                # seek foreward
                while 1:
                    wbi.prevIndex += 1
                    prevFrame = wbi.keyframes[wbi.prevIndex]
                    wbi.prevFrameNo = prevFrame.frameNo
                    wbi.nextIndex += 1
                    nextFrame = wbi.keyframes[wbi.nextIndex]
                    wbi.nextFrameNo = nextFrame.frameNo
                    if wbi.prevFrameNo < tgtFrameNo < wbi.nextFrameNo:
                        break
                    if wbi.prevFrameNo == tgtFrameNo:
                        wbi.wRotation.Set(prevFrame.rotation.x, prevFrame.rotation.y, prevFrame.rotation.z, prevFrame.rotation.w)
                        wbi.wPosition.Set(prevFrame.position.x, prevFrame.position.y, prevFrame.position.z)
                        return
            elif tgtFrameNo < wbi.prevFrameNo:
                # seek backward
                for i in range(len(wbi.keyframes)):
                    if wbi.keyframes[i].frameNo > tgtFrameNo:
                        wbi.prevIndex = i - 1
                        prevFrame = wbi.keyframes[wbi.prevIndex]
                        wbi.prevFrameNo = prevFrame.frameNo
                        wbi.nextIndex = i
                        nextFrame = wbi.keyframes[wbi.nextIndex]
                        wbi.nextFrameNo = nextFrame.frameNo
                        break
                    if wbi.keyframes[i].frameNo == tgtFrameNo:
                        wbi.prevIndex = i
                        prevFrame = wbi.keyframes[wbi.prevIndex]
                        wbi.prevFrameNo = prevFrame.frameNo
                        wbi.nextIndex = i + 1
                        nextFrame = wbi.keyframes[wbi.nextIndex]
                        wbi.nextFrameNo = nextFrame.frameNo
                        wbi.wRotation.Set(prevFrame.rotation.x, prevFrame.rotation.y, prevFrame.rotation.z, prevFrame.rotation.w)
                        wbi.wPosition.Set(prevFrame.position.x, prevFrame.position.y, prevFrame.position.z)
                        return 
                #print "Seek backward for #%f: [%d]#%f - [%d]#%f"%(tgtFrameNo, wbi.prevIndex, wbi.prevFrameNo, wbi.nextIndex, wbi.nextFrameNo)
            else:
                # tgtFrameNo == wbi.prevFrameNo:
                prevFrame = wbi.keyframes[wbi.prevIndex]
                wbi.wRotation.Set(prevFrame.rotation.x, prevFrame.rotation.y, prevFrame.rotation.z, prevFrame.rotation.w)
                wbi.wPosition.Set(prevFrame.position.x, prevFrame.position.y, prevFrame.position.z)
                return

            if not self.interpolateConsecutive and wbi.prevFrameNo + 1 == wbi.nextFrameNo:
                # skip interpolate for consecutive keyframe
                wbi.wRotation.Set(prevFrame.rotation.x, prevFrame.rotation.y, prevFrame.rotation.z, prevFrame.rotation.w)
                wbi.wPosition.Set(prevFrame.position.x, prevFrame.position.y, prevFrame.position.z)
                return
            else:
                # do interpolate
                tframe = float(tgtFrameNo - wbi.prevFrameNo) / float(wbi.nextFrameNo - wbi.prevFrameNo)
                if nextFrame.xCurve[0] == nextFrame.xCurve[1] and nextFrame.xCurve[2] == nextFrame.xCurve[3]:
                    tx = tframe
                else:
                    tx = VmdLib.mmdCubicBezierInterpolate(nextFrame.xCurve, tframe)
                if nextFrame.yCurve[0] == nextFrame.yCurve[1] and nextFrame.yCurve[2] == nextFrame.yCurve[3]:
                    ty = tframe
                else:
                    ty = VmdLib.mmdCubicBezierInterpolate(nextFrame.yCurve, tframe)
                if nextFrame.zCurve[0] == nextFrame.zCurve[1] and nextFrame.zCurve[2] == nextFrame.zCurve[3]:
                    tz = tframe
                else:
                    tz = VmdLib.mmdCubicBezierInterpolate(nextFrame.zCurve, tframe)
                if nextFrame.rCurve[0] == nextFrame.rCurve[1] and nextFrame.rCurve[2] == nextFrame.rCurve[3]:
                    tr = tframe
                else:
                    tr = VmdLib.mmdCubicBezierInterpolate(nextFrame.rCurve, tframe)

                wbi.wRotation = Quaternion.Slerp(prevFrame.rotation, nextFrame.rotation, tr)
                x = prevFrame.position.x + (nextFrame.position.x - prevFrame.position.x) * tx
                y = prevFrame.position.y + (nextFrame.position.y - prevFrame.position.y) * ty
                z = prevFrame.position.z + (nextFrame.position.z - prevFrame.position.z) * tz
                wbi.wPosition.Set(x, y, z)
                return

    def getVmdBone(self, boneName):
        if not self.vmdBoneRoot or not self.vmdBones or not boneName in self.vmdBones.keys():
            print "Cannot found %s in vmd bones!"%boneName
            return None
        else:
            return self.vmdBones[boneName]

    def verify(self):
        # verify works by checking vmdBoneRoot
        if self.ociChar and hasattr(self, "workBoneIndexBuffer") and self.workBoneIndexBuffer and len(self.workBoneIndexBuffer):
            valid = self.vmdBoneRoot != None
            if not valid:
                #print "MotionController for %s becomes invalid, try to fix it"%self.CharName
                #self.createVmdBone()
                #self.buildWorkBoneIndex()
                return False
        return True

    def importBoneKeyframes(self, filename, keyframes, mode="renew", ignoreList=[]):
        # supported mode: renew, replace, append
        #   renew   : delete all old data and import new data
        #   replace : replace existed keyframes with same bone name, import non-exist ones
        #   append  : leave existed keyframes untouched, import non-exist keyframes only
        if keyframes == None or len(keyframes) == 0:
            return

        # import all bones
        self.debakeFrame()
        self.vmdFilePath = filename
        if mode == "renew":
            self.boneKeyFrames = {}
            self.keyframeCustomized = False
        bkfDupCheckTemp = {}
        for bkf in keyframes:
            if bkf.name in ignoreList:
                continue
            if self.boneKeyFrames.has_key(bkf.name):
                # check if it is an existed bone
                if bkf.name in bkfDupCheckTemp:
                    # new added bone, add it
                    if bkf.frameNo not in bkfDupCheckTemp[bkf.name]:
                        self.boneKeyFrames[bkf.name].append(bkf)
                        bkfDupCheckTemp[bkf.name].add(bkf.frameNo)
                    else:
                        print "WARNING: Duplicated keyframe for %s found at #%d, ignored"%(bkf.name, bkf.frameNo)
                else:
                    # existed bone, depend on mode
                    if mode == "replace":
                        # new a bone keyframe set to replace old one
                        self.boneKeyFrames[bkf.name] = [bkf]
                        bkfDupCheckTemp[bkf.name] = {bkf.frameNo}
            else:
                # new bone set, add it
                self.boneKeyFrames[bkf.name] = [bkf]
                bkfDupCheckTemp[bkf.name] = {bkf.frameNo}
        print "Totally %d bone imported"%len(bkfDupCheckTemp)

        # check for wildcard bonename
        wildcardBoneMatch = []
        for vname in self.vmd2BoneMap.keys():
            if vname.endswith("?") and not self.boneKeyFrames.has_key(vname):
                # try match wildcard
                wcPtn = vname[:-1]
                match = []
                for vkname in self.boneKeyFrames.keys():
                    if vkname.startswith(wcPtn):
                        match.append(vkname)
                if len(match) == 0:
                    print "INFO: Wildcard bone has no match. Skipped."
                    continue
                elif len(match) == 1:
                    print "INFO: Wildcard bone %s matchs %s"%(vname, match[0])
                else:
                    print "WARNING: Wildcard bone %s matchs multi bone. Resolve as %s"%(vname, match[0])
                # resolve it later
                wildcardBoneMatch.append((vname, match[0]))
        # do wildcard resolve
        for wcpair in wildcardBoneMatch:
            bname = self.vmd2BoneMap[wcpair[1]] = self.vmd2BoneMap.pop(wcpair[0])
            self.boneDefineMap[bname].vmdBone = wcpair[1]

        # remove unmapped bones
        mappedBoneInfo = []
        unmappedBoneInfo = []
        for vname in bkfDupCheckTemp.keys(): #self.boneKeyFrames.keys():
            if not self.vmd2BoneMap.has_key(vname):
                #if len(self.boneKeyFrames[vname]) > 1:
                #    print "  VMD Bone %s with %d frame ignored."%(vname, len(self.boneKeyFrames[vname]))
                unmappedBoneInfo.append((vname, len(self.boneKeyFrames[vname])))
            else:
                mappedBoneInfo.append((vname, len(self.boneKeyFrames[vname])))

        for binfo in unmappedBoneInfo:
            del self.boneKeyFrames[binfo[0]]
        print "Totally %d bone will be append to chara"%len(self.boneKeyFrames)

        # build work bone index
        self.buildWorkBoneIndex()

        return mappedBoneInfo, unmappedBoneInfo

    def redirectBoneKeyframes(self, filepath):
        # re-direct filename and discard user data
        self.vmdFilePath = filepath
        self.keyframeCustomized = False

    def buildWorkBoneIndex(self):
        # get max frame no
        self.maxFrameNo = 0
        for bframes in self.boneKeyFrames.values():
            if bframes[-1].frameNo > self.maxFrameNo:
                self.maxFrameNo = bframes[-1].frameNo

        # build work bone index buffer
        self.workBoneIndexBuffer = []
        for bname in self.boneNameList:
            bDefine = self.boneDefineMap[bname]
            if bDefine.enable and (self.boneKeyFrames.has_key(bDefine.vmdBone) or bDefine.isTrack):
                wbi = GData()
                wbi.bonename = bname

                # bone keyframe index
                if self.boneKeyFrames.has_key(bDefine.vmdBone):
                    wbi.needGetFrame = 1
                    wbi.wRotation = Quaternion.identity
                    wbi.wPosition = Vector3.zero
                    wbi.poseData = self.poseData[bname]
                    wbi.keyframes = self.boneKeyFrames[bDefine.vmdBone]
                    wbi.boneAdjust = self.boneAdjustMap[bname]
                    wbi.prevIndex = 0
                    wbi.prevFrameNo = wbi.keyframes[wbi.prevIndex].frameNo
                    wbi.nextIndex = 1 if len(wbi.keyframes) > 1 else 0
                    wbi.nextFrameNo = wbi.keyframes[wbi.nextIndex].frameNo
                    wbi.firstFrameNo = wbi.keyframes[0].frameNo
                    wbi.lastFrameNo = wbi.keyframes[-1].frameNo
                else:
                    wbi.needGetFrame = 0
                    wbi.wRotation = Quaternion.identity
                    wbi.wPosition = self.boneAdjustMap[bname].basePosition
                    wbi.poseData = self.poseData[bname]

                # bone update info
                wbi.transform = self.getVmdBone(bname).transform
                wbi.needPosition = bDefine.isPosition or (bDefine.isTrack and "t" in bDefine.trackData[0])
                wbi.needRotation = bDefine.isRotation or (bDefine.isTrack and "r" in bDefine.trackData[0])

                # limit axis
                wbi.needLimitAxis = bDefine.isLimitAxis
                if wbi.needLimitAxis:
                    wbi.referAxis = bDefine.limitAxisData[0]
                    wbi.limitAxis = bDefine.limitAxisData[1]

                # ik
                if bDefine.isIK:
                    iksetting = self.ikSettingMap[self.vmdIKs[bname][0]]
                    if iksetting.enable:
                        wbi.needIK = True
                        ik = self.vmdIKs[bname][1]
                        if iksetting.solver == "CCDIKSolver":
                            # set solve method
                            if iksetting.solveMethod == "SolveLeg2":
                                wbi.solveFunc = ik.SolveLeg2
                            else:
                                wbi.solveFunc = ik.Solve
                        elif iksetting.solver in {"MMDIKSolver", "MMDIKSolverLeg", "MMDIKSolverNoLimit"}:
                            # set solve method
                            wbi.solveFunc = ik.Solve
                        else:
                            print "??? IK solver setting error!"
                            wbi.needIK = False
                    else:
                        wbi.needIK = False
                else:
                    wbi.needIK = False

                # track
                if bDefine.isTrack:
                    tckPtn = bDefine.trackData[0]
                    tckScale = bDefine.trackData[1]
                    srcName = bDefine.trackData[2]
                    
                    wbi.needTrackRot = "r" in tckPtn
                    wbi.needTrackPos = "p" in tckPtn
                    wbi.trackScale = tckScale
                    wbi.trackSrcTransform = self.getVmdBone(srcName).transform
                else:
                    wbi.needTrackRot = False
                    wbi.needTrackPos = False

                self.workBoneIndexBuffer.append(wbi)

        #print "Build work bone index buffer, totally %d work bone (%d skipped for only 1 keyframe and %d skipped for only 2 same keyframes)"%(len(self.workBoneIndexBuffer), boneSkip1, boneSkip2)
        print "*** Build work bone index buffer, totally %d work bone"%len(self.workBoneIndexBuffer)

    def getWorkBoneIndex(self, boneName):
        for wbi in self.workBoneIndexBuffer:
            if wbi.bonename == boneName:
                return wbi
        return None

    def updateKeyframes(self, nkeyframes):
        # prepare
        groupedNewKeyframes = {}
        for nkf in nkeyframes:
            if nkf.name in groupedNewKeyframes:
                groupedNewKeyframes[nkf.name].add(nkf)
            else:
                groupedNewKeyframes[nkf.name] = {nkf}
            if nkf.name not in self.boneKeyFrames:
                self.boneKeyFrames[nkf.name] = []
        # do update
        rebuildWBI = False
        for tgtName in groupedNewKeyframes.keys():
            hasNew = False
            tgtBoneFrames = self.boneKeyFrames[tgtName]
            existFrames = [kf.frameNo for kf in tgtBoneFrames]
            for nkf in groupedNewKeyframes[tgtName]:
                if nkf.frameNo in existFrames:
                    idx = existFrames.index(nkf.frameNo)
                    tgtBoneFrames[idx] = nkf
                else:
                    hasNew = True
                    tgtBoneFrames.append(nkf)
            if hasNew:
                rebuildWBI = True
                tgtBoneFrames.sort(key=VmdBoneKeyFrame.sortByFrameNo)
        # rebuild wbi
        if rebuildWBI: self.buildWorkBoneIndex()
        self.keyframeCustomized = True
        return rebuildWBI

    def deleteKeyframes(self, tgtNameFramePair):
        # group info
        groupedPair = {}
        for (tgtName, tgtFrame) in tgtNameFramePair:
            if tgtName in groupedPair:
                groupedPair[tgtName].add(tgtFrame)
            else:
                groupedPair[tgtName] = {tgtFrame}
        # check delete index
        toDelIndex = {}
        for tgtName in groupedPair.keys():
            tgtBoneFrames = self.boneKeyFrames[tgtName]
            if len(tgtBoneFrames) == len(groupedPair[tgtName]):
                toDelIndex[tgtName] = "ALL"
            else:
                toDelIndex[tgtName] = []
                for i in range(len(tgtBoneFrames)):
                    if tgtBoneFrames[i].frameNo in groupedPair[tgtName]:
                        toDelIndex[tgtName].append(i)
                        groupedPair[tgtName].remove(tgtBoneFrames[i].frameNo)
                        if len(groupedPair[tgtName]) == 0:
                            break
                toDelIndex[tgtName].reverse()
        # do delete
        for tgtName in toDelIndex.keys():
            if toDelIndex[tgtName] == "ALL":
                del self.boneKeyFrames[tgtName]
            else:
                tgtBoneFrames = self.boneKeyFrames[tgtName]
                for idx in toDelIndex[tgtName]:
                    del tgtBoneFrames[idx]
        # rebuild wbi
        self.buildWorkBoneIndex()
        self.keyframeCustomized = True

    def bakeFrame(self, oversampling = 1):
        if self.maxFrameNo == 0:
            print "Nothing to bake..."
            return
        if not isinstance(oversampling, int):
            print "Wrong oversampling setting %s, must be an integer"%str(oversampling)
            return
        # prepare
        import time
        start = time.clock()
        self.debakeFrame()
        backupEnable = self.Enable
        backDelay = self.delay
        self.setEnable(False)
        self.delay = 0        
        print "Bake started"
        # do bake
        vmdBoneTrans = []
        vmdBoneTransFlag = []
        for bname in self.weightBoneList:
            boneTran = self.getVmdBone(bname).transform
            vmdBoneTrans.append(boneTran)
            boneDefine = self.boneDefineMap[bname]
            vmdBoneTransFlag.append((boneDefine.isPosition, boneDefine.isRotation))
        vmdBoneFrames = []
        oldPrg = 0
        for f in range(self.maxFrameNo * oversampling):
            # bake keyframe
            frameNo = float(f) / float(oversampling)
            self.setFrame(frameNo)
            # collect result
            frmData = []
            for n in range(len(vmdBoneTrans)):
                boneTran = vmdBoneTrans[n]
                tranFlag = vmdBoneTransFlag[n]
                bonefrm = [None, None]
                if tranFlag[0]:
                    bonefrm[0] = boneTran.position
                if tranFlag[1]:
                    bonefrm[1] = boneTran.rotation
                frmData.append(tuple(bonefrm))
            vmdBoneFrames.append(tuple(frmData))
            # print progress
            prg = int((f * 100) / (self.maxFrameNo * oversampling))
            if prg != oldPrg:
                oldPrg = prg
                if prg % 10 == 0:
                    print ("...%d%%")%prg
        # done
        self.setEnable(backupEnable)
        self.delay = backDelay
        self.bakedFrames = (tuple(vmdBoneTrans), tuple(vmdBoneTransFlag), tuple(vmdBoneFrames))
        self.bakedFrameMaxIndex = len(vmdBoneFrames) - 1
        end = time.clock()
        print "Baked %d frames, oversampling=%dx, used %.3fsec (%f sec per frame by avg)"%(len(vmdBoneFrames), oversampling, end - start, (end - start) / len(vmdBoneFrames))

    def debakeFrame(self):
        self.resetPose()
        self.bakedFrames = None

    def exportBakedData(self):
        if not self.Baked:
            return None
        # V,Q to array
        def v2a(v):
            if v:
                return (v.x, v.y, v.z)
            else:
                return None
        def q2a(q):
            if q:
                return (q.x, q.y, q.z, q.w)
            else:
                return None

        bkf = []
        for f in self.bakedFrames[2]:
            bf = []
            for b in f:
                bf.append((v2a(b[0]), q2a(b[1])))
            bkf.append(tuple(bf))

        bd = {"bones": self.weightBoneList, "maxIndex": self.bakedFrameMaxIndex, "transFlag": self.bakedFrames[1], "transData": tuple(bkf)}
        return bd

    def importBakedData(self, bakedData):
        if not bakedData:
            return "Input bakedData is None"
        try:
            bones = bakedData["bones"]
            transFlag = bakedData["transFlag"]
            transData = bakedData["transData"]
            maxIndex = int(bakedData["maxIndex"])
            if maxIndex != len(transData) - 1:
                raise Exception("Max frame index mismatch!")

            vmdBoneTrans = []
            vmdBoneTransFlag = []
            for i in range(len(bones)):
                bname = bones[i]
                if bname != self.weightBoneList[i]:
                    raise Exception("Weight bone mismatch! %s(saved) <==> %s(current)"%(bname, self.weightBoneList[i]))

                boneTran = self.getVmdBone(bname).transform
                boneDefine = self.boneDefineMap[bname]
                boneFlag = (boneDefine.isPosition, boneDefine.isRotation)
                if bool(boneFlag[0]) != bool(transFlag[i][0]) or bool(boneFlag[1]) != bool(transFlag[i][1]):
                    raise Exception("Bone transform setting flag mismatch!")

                vmdBoneTrans.append(boneTran)
                vmdBoneTransFlag.append(boneFlag)

            vmdBoneFrames = []
            for f in transData:
                frmData = []
                for n in range(len(vmdBoneTrans)):
                    tranFlag = vmdBoneTransFlag[n]
                    bonefrm = [None, None]
                    if tranFlag[0]:
                        bonefrm[0] = Vector3(f[n][0][0], f[n][0][1], f[n][0][2])
                    if tranFlag[1]:
                        bonefrm[1] = Quaternion(f[n][1][0], f[n][1][1], f[n][1][2], f[n][1][3])
                    frmData.append(tuple(bonefrm))
                vmdBoneFrames.append(tuple(frmData))

            self.debakeFrame()
            self.bakedFrames = (tuple(vmdBoneTrans), tuple(vmdBoneTransFlag), tuple(vmdBoneFrames))
            self.bakedFrameMaxIndex = len(vmdBoneFrames) - 1
            print "Imported %d baked frames"%len(vmdBoneFrames)
            return None
        except Exception, e:
            traceback.print_exc()
            return "Fail to unpack baked data: " + str(e)

    def showBoneGizmos(self, show = True):
        if not self.vmdBoneRoot or not self.charaAnimePlugin:
            return

        from CharaAnime import BoneGizmos
        gm = self.charaAnimePlugin.GizmosMgr

        if not hasattr(self, "_dbgBoneGizmos"):
            self._dbgBoneGizmos = {}
            for bname in self.gizmosBoneList:
                gSetting = self.boneDefineMap[bname].gizmosData
                gStyle = gSetting[0]
                gColor = gSetting[1]
                gSize = gSetting[2]
                gParent = gSetting[3]

                btrans = MotionController.searchObjName(self.vmdBoneRoot, bname).transform
                if gParent:
                    ttrans = MotionController.searchObjName(self.vmdBoneRoot, gParent).transform
                else:
                    ttrans = None
                if gStyle == "r":
                    edges = 4
                elif gStyle == "t":
                    edges = 3
                else:
                    edges = 36

                bg = BoneGizmos(btrans, ttrans, edges)
                bg.Color = gColor
                bg.Size = gSize
                self._dbgBoneGizmos[bname] = bg
                gm.ShowGizmos(bg)

        for bgz in self._dbgBoneGizmos.values():
            bgz.SetEnable(show)
        self._dbgBoneGizmosVisible = show

    def cleanBoneGizmos(self):
        if not hasattr(self, "_dbgBoneGizmos"):
            return
        gm = self.charaAnimePlugin.GizmosMgr
        for bgz in self._dbgBoneGizmos.values():
            gm.RemoveGizmos(bgz)

        del self._dbgBoneGizmos
        del self._dbgBoneGizmosVisible

    def boneGizmosVisible(self):
        if not hasattr(self, "_dbgBoneGizmosVisible"):
            return False
        else:
            return self._dbgBoneGizmosVisible

    @staticmethod
    def searchObjName(rootTF, name, lmtRootTF = None):
        def checkRootLimit(tgt, lmt):
            #spath = ""
            while lmt and tgt:
                #spath = tgt.gameObject.name + spath
                if lmt.gameObject.name == tgt.gameObject.name:
                    #print " check root name matched, path=%s, obj match=%s"%(spath, str(lmt==tgt))
                    return lmt == tgt
                tgt = tgt.parent
                #spath = "-" + spath
            return True
        # normalize
        if isinstance(rootTF, GameObject):
            rootTF = rootTF.transform
        if isinstance(lmtRootTF, GameObject):
            lmtRootTF = lmtRootTF.transform
        # search
        #print "search <%s> from <%s>"%(name, rootTF.gameObject.name)
        if rootTF.gameObject.name == name:
            if checkRootLimit(rootTF, lmtRootTF):
                return rootTF.gameObject
        for i in range(rootTF.childCount):
            subGO = MotionController.searchObjName(rootTF.GetChild(i), name, lmtRootTF)
            if subGO:
                return subGO
        return None

    @staticmethod
    def trySearchObjNameWithLimit(rootTF, name, lmtRootTF):
        tryRes = MotionController.searchObjName(rootTF, name, lmtRootTF)
        if not tryRes:
            tryRes = MotionController.searchObjName(rootTF, name, None)
        return tryRes

class MorphController():
    class MorphDefine:
        def __init__(self, name, mtype, en):
            self.name = name
            self.type = mtype
            self.enable = en
            self.group = "ungrouped"
            # by type
            self.boneList = None
            self.blendList = None
            self.funcList = None
            self.pattern = None
            self.range = None

        @property
        def valid(self):
            if self.name == None or self.type == None:
                return False
            if self.type == "bone":
                return self.boneList != None and len(self.boneList) > 0
            if self.type == "blend":
                return self.blendList != None and len(self.blendList) > 0
            if self.type == "func":
                return self.funcList != None and len(self.funcList) > 0
            else:
                return self.pattern != None

        def copy(self):
            md = MorphController.MorphDefine(self.name, self.type, self.enable)
            md.group = self.group
            md.boneList = copy.deepcopy(self.boneList)
            md.blendList = copy.deepcopy(self.blendList)
            md.funcList = copy.deepcopy(self.funcList)
            md.pattern = self.pattern
            md.range = copy.copy(self.range)
            # specially for bonelist, Vector3 and Quaternion can not be copy!?
            if self.boneList:
                for i in range(len(self.boneList)):
                    if self.boneList[i][1]: # copy rotation
                        my = self.boneList[i][1]
                        md.boneList[i][1] = Quaternion(my.x, my.y, my.z, my.w)
                    if self.boneList[i][2]: # copy position
                        my = self.boneList[i][2]
                        md.boneList[i][2] = Vector3(my.x, my.y, my.z)
                    if self.boneList[i][3]: # copy scale
                        my = self.boneList[i][3]
                        md.boneList[i][3] = Vector3(my.x, my.y, my.z)
            return md
        
        def getValue(self, weight):
            if self.range != None:
                return (self.range[1] - self.range[0]) * weight + self.range[0]
            elif self.type == "bone":
                bwl = []
                for b in self.boneList:
                    bw = [b[0], b[1], b[2], b[3]]
                    if b[1]:
                        bw[1] = Quaternion.Slerp(Quaternion.identity, b[1], weight)
                    if b[2]:
                        bw[2] = Vector3.Lerp(Vector3.zero, b[2], weight)
                    if b[3]:
                        bw[3] = Vector3.Lerp(Vector3(1,1,1), b[3], weight)
                    bwl.append(bw)
                return bwl
            elif self.type == "blend":
                bwl = []
                for n,m,rmin,rmax in self.blendList:
                    val = (rmax - rmin) * weight + rmin
                    bwl.append(val)
                return bwl
            elif self.type == "func":
                fwl = []
                for g,s,vtype,rmin,rmax in self.funcList:
                    if vtype == "scale":
                        val = (rmax - rmin) * weight + rmin
                    elif vtype == "range":
                        val = VmdLib.interpolateRangeValue(rmin, rmax, weight)
                    else:
                        val = (rmax - rmin) * weight + rmin # not-supported
                    fwl.append(val)
                return fwl
            else:
                return None

        def exportSetting(self):
            setting = {}
            setting["name"] = self.name
            setting["type"] = self.type
            setting["enable"] = self.enable
            setting["boneList"] = self.boneList
            setting["blendList"] = self.blendList
            setting["funcList"] = self.funcList
            setting["pattern"] = self.pattern
            setting["range"] = self.range
            return setting

        @staticmethod
        def parseFromSetting(setting):
            md = MorphController.MorphDefine(setting["name"], setting["type"], setting["enable"])
            md.boneList = copy.deepcopy(setting["boneList"])
            if setting.has_key("blendList"):    # from MMDDv2
                md.blendList = copy.deepcopy(setting["blendList"])
            else:
                md.blendList = None
            if setting.has_key("funcList"):     # from MMDDv2
                md.funcList = copy.deepcopy(setting["funcList"])
            else:
                md.funcList = None
            md.pattern = setting["pattern"]
            md.range = copy.copy(setting["range"])
            return md

    class CnvMorphKeyframe:
        def __init__(self, nIndex, frameNo, value, interpolate):
            self.nIndex = nIndex
            self.frameNo = frameNo
            self.value = value
            self.interpolate = interpolate

        def copy(self):
            nf = MorphController.CnvMorphKeyframe(self.nIndex, self.frameNo, self.value, self.interpolate)
            return nf

        def copyFrom(self, src):
            self.nIndex = src.nIndex
            self.frameNo = src.frameNo
            self.value = src.value
            self.interpolate = src.interpolate

        def exportSetting(self):
            return (self.nIndex, self.frameNo, self.value, self.interpolate)

        @staticmethod
        def parseFromSetting(setting):
            md = MorphController.CnvMorphKeyframe(setting[0], setting[1], setting[2], setting[3])
            return md

        @staticmethod
        def sortByFrameNo(keyframe):
            return keyframe.frameNo

    @property
    def CharName(self):
        if self.actor:
            return self.actor.text_name
        else:
            return ""

    @property
    def FileName(self):
        if self.vmdFilePath and len(self.vmdFilePath) > 0:
            return os.path.split(self.vmdFilePath)[1]
        else:
            return self.vmdFilePath

    @property
    def TotalFrameLength(self):
        return int(self.delay * 30.0) + self.maxFrameNo

    @property
    def KeyframesForExport(self):
        k4f = []
        for kfk in self.morphKeyFrames:
            k4f.extend(self.morphKeyFrames[kfk])
        return k4f

    @property
    def NeedActiveMotion(self):
        return self.morphKeyFrames and len(self.boneIndexBuffer) > 0 and not self.motionCtrl.Enable

    @property
    def HasLegacyData(self):
        return len(self.mouthIndexBuffer) > 0 or len(self.eyeIndexBuffer) > 0 or len(self.eyebrowIndexBuffer) > 0

    def __init__(self, param = None):
        # setting
        self.enable = True
        self.delay = 0.0
        # default
        self.defaultEyesPtn = 0
        self.defaultEyebrowPtn = 0
        self.defaultMouthPtn = 0
        # runtime
        self.actor = None
        self.hasMouthEye = False
        self.hasEyebrow = False
        # link a motion and load morph setting
        if param == None or isinstance(param, str):
            self.motionCtrl = MotionController(param)
        elif isinstance(param, MotionController) or hasattr(param, "ociChar"):
            self.motionCtrl = param
        self.loadMorphSetting()
        # MMD keyframes
        self.vmdFilePath = ""
        self.morphKeyFrames = {}
        self.keyframeCustomized = False
        self.maxFrameNo = 0
        # converted keyframes
        self.convertedFrameName = ["Mouth Pattern", "Mouth Open", "Eyes Pattern", "Eyes Open", "Eyebrow Pattern"]
        self.convertedMouthKeyframes = None # TODO delete
        self.convertedEyeKeyframes = None
        self.convertedEyebrowKeyframes = None
        self.convertedKeyframes = None
        self.convertedFrameIndex = None
        self.convertedMouthIndex = None
        self.convertedEyeIndex = None
        self.convertedEyebrowIndex = None
        self.converted = False

    def exportSetting(self):
        setting = {}
        # export general setting
        setting["boneDefine"] = os.path.relpath(self.motionCtrl.boneDefineFilename, os.path.split(__file__)[0])
        setting["filename"] = self.vmdFilePath
        setting["enable"] = str(self.enable)
        setting["delay"] = str(self.delay)
        # export default setting
        setting["defaultEyesPtn"] = str(self.defaultEyesPtn)
        setting["defaultEyebrowPtn"] = str(self.defaultEyebrowPtn)
        setting["defaultMouthPtn"] = str(self.defaultMouthPtn)
        # export customized define
        setting["customizedDefine"] = []
        for vname in self.morphDefineBackup.keys():
            setting["customizedDefine"].append(script2string(self.morphDefineMap[vname].exportSetting()))
        # export customized frame, is for MMDDv1 only
        # setting["customizedFrame"] = []
        # format in array:
        #   {"morphVmdName#frameNo": script2string(morphKeyframe.exportSetting())}
        #   or
        #   {"morphVmdName#frameNo": ""}
        #   when user deleted a keyframe from org vmd
        # export frames, from MMDDv2
        if self.keyframeCustomized or (self.vmdFilePath == self.motionCtrl.vmdFilePath and self.motionCtrl.keyframeCustomized):
            setting["keyframes"] = []
            for vname in self.morphKeyFrames:
                for mkf in self.morphKeyFrames[vname]:
                    setting["keyframes"].append(mkf.exportSetting())
        # export converted frame
        setting["converted"] = str(self.converted)
        if self.converted:
            setting["covertedKeyframes"] = []
            for vi in self.convertedKeyframes:
                for ckf in self.convertedKeyframes[vi]:
                    setting["covertedKeyframes"].append(ckf.exportSetting())
        # export changed morph sequence
        if self.morphSequenceChanged:
            setting["customizedMorphSequence"] = self.morphNameList
        return setting

    @staticmethod
    def parseFromSetting(setting, param, vmdFrameBuffer):
        def parseBool(txt):
            return bool(eval(txt))
        # check import param: a motion or a actor/prop to bind
        if isinstance(param, MotionController):
            mctrl = param
            if mctrl.ociChar == None:
                raise Exception("To build morph controller by motion controller, bind char first!")
            if mctrl.boneType == "chara":
                actor = HSNeoOCIChar(param.ociChar).as_actor
            elif mctrl.boneType == "item":
                actor = HSNeoOCIProp(param.ociChar).as_prop
            else:
                raise Exception("Unknown bone type of <%s>"%mctrl.boneType)
        elif isinstance(param, Prop) or isinstance(param, Actor):
            actor = param
            mctrl = None
        else:
            raise Exception("Unknown type of param to build morph controller: %s"%str(param))
        # import bonesetting
        if setting.has_key("boneDefine"):
            bsFilename = setting["boneDefine"]
        else:
            bsFilename = None
        # build morph controller
        if mctrl:
            mp = MorphController(mctrl)
        else:
            mp = MorphController(bsFilename)
        mp.bindChara(actor)
        # import general setting
        mp.enable = parseBool(setting["enable"])
        mp.delay = float(setting["delay"])
        # import default setting
        if setting.has_key("defaultEyesPtn"):
            mp.defaultEyesPtn = int(setting["defaultEyesPtn"])
        if setting.has_key("defaultEyebrowPtn"):
            mp.defaultEyebrowPtn = int(setting["defaultEyebrowPtn"])
        if setting.has_key("defaultMouthPtn"):
            mp.defaultMouthPtn = int(setting["defaultMouthPtn"])
        # import customized define
        if setting.has_key("customizedDefine"):
            for cdstr in setting["customizedDefine"]:
                cdraw = eval(cdstr)
                cdf = MorphController.MorphDefine.parseFromSetting(cdraw)
                vname = mp.getVmdNameByDummyName(cdf.name)
                mp.updateMorphDefine(vname, cdf)
        # import customized define sequence
        if setting.has_key("customizedMorphSequence"):
            cNameList = setting["customizedMorphSequence"]
            if len(cNameList) == len(mp.morphNameList):
                cflg = True
                for mn in cNameList:
                    if mn not in mp.morphNameList:
                        cflg = False
                        break
                if cflg:
                    mp.morphNameList = cNameList
                    mp.morphSequenceChanged = True
        # load keyframe
        filename = setting["filename"]
        kfCus = False
        if setting.has_key("keyframes"):
            keyframes = [VmdMorphKeyFrame.parseFromSetting(kfs) for kfs in setting["keyframes"]]
            kfCus = True
        elif filename in vmdFrameBuffer:
            vmdResult = vmdFrameBuffer[filename]
            keyframes = vmdResult[2]
        else:
            vmdResult = VmdLib.loadVmd(filename)
            if len(vmdResult) == 1:
                raise Exception(vmdResult[0])
            keyframes = vmdResult[2]
            vmdFrameBuffer[filename] = vmdResult
        mp.importMorphKeyframes(filename, keyframes)
        mp.keyframeCustomized = kfCus
        # import customized frame, optional in MMDDv2
        if setting.has_key("customizedFrame"):
            frameToUpdate = set()
            frameToDelete = set()
            for cfstr in setting["customizedFrame"]:
                cfraw = eval(cfstr)
                if len(cfraw.values()[0]) > 0:
                    fsetting = eval(cfraw.values()[0])
                    frame = VmdMorphKeyFrame.parseFromSetting(fsetting)
                    frameToUpdate.add(frame)
                else:
                    dfn = cfraw.keys()[0].split("#")
                    vname = dfn[0]
                    frameNo = int(dfn[1])
                    frameToDelete.add((vname, frameNo))
            if len(frameToUpdate):
                mp.updateMorphKeyframes(frameToUpdate)
            if len(frameToDelete):
                mp.deleteMorphKeyframes(frameToDelete)
        # import converted frame
        if setting.has_key("converted"):
            cnv = parseBool(setting["converted"])
        else:
            cnv = False
        if cnv and setting.has_key("covertedKeyframes"):
            mp.convertedKeyframes = {}
            for ckfs in setting["covertedKeyframes"]:
                ckf = MorphController.CnvMorphKeyframe.parseFromSetting(ckfs)
                if mp.convertedKeyframes.has_key(ckf.nIndex):
                    mp.convertedKeyframes[ckf.nIndex].append(ckf)
                else:
                    mp.convertedKeyframes[ckf.nIndex] = [ckf]
            mp.buildConvertIndex()
        else:
            cnv = False
        mp.converted = cnv
        # done
        return mp

    def bindChara(self, actor):
        from Studio import OCIChar, OCIItem
        # check linked motion controller first
        if not self.motionCtrl:
            raise Exception("No linked motion contrller!")
        # type check
        if self.motionCtrl.boneType == "chara" and isinstance(actor, Actor):
            oci = actor.objctrl
        elif self.motionCtrl.boneType == "chara" and isinstance(actor, HSNeoOCIChar):
            oci = actor.objctrl
            actor = actor.as_actor
        elif self.motionCtrl.boneType == "chara" and isinstance(actor, OCIChar):
            oci = actor
            actor = HSNeoOCIChar(actor).as_actor
        elif self.motionCtrl.boneType == "item" and isinstance(actor, Prop):
            oci = actor.objctrl
        elif self.motionCtrl.boneType == "item" and isinstance(actor, HSNeoOCIProp):
            oci = actor.objctrl
            actor = actor.as_prop
        elif self.motionCtrl.boneType == "item" and isinstance(actor, OCIItem):
            oci = actor
            actor = HSNeoOCIProp(actor).as_prop
        else:
            raise Exception("Morph bind chara ERROR: wrong actor type!! %s"%str(actor))
        
        # sync check
        if self.motionCtrl.ociChar:
            if self.motionCtrl.ociChar != oci:
                raise Exception("Morph controller cannot bind char other than linked motion controller!")
        else:
            print "Auto bind chara for linked motion controller."
            self.motionCtrl.bindChara(actor)

        # bind and get default
        self.actor = actor
        if isinstance(actor, Actor):
            if isinstance(actor, ActorNeoV2) or isinstance(actor, ActorCharaStudio):
                self.oldEyebrowPtn = self.lastEyebrowPtn = actor.get_eyebrow_ptn()
                self.hasEyebrow = True
                # force to 0
                actor.set_eyebrow_ptn(0)
            self.oldEyesPtn = self.lastEyesPtn = actor.get_eyes_ptn()
            self.oldEyesOpen = self.lastEyesOpen = actor.get_eyes_open()
            self.oldMouthPtn = self.lastMouthPtn = actor.get_mouth_ptn()
            self.oldMouthOpen = self.lastMouthOpen = actor.get_mouth_open()
            self.hasMouthEye = True
            # force to 0
            actor.set_eyes_ptn(0)
            actor.set_eyes_open(1)
            actor.set_mouth_ptn(0)
            actor.set_mouth_open(0)
            #print "Reset face: eyes ptn = %d, eyes open = %f, mouth ptn = %d, mouth open = %f"%(actor.get_eyes_ptn(), actor.get_eyes_open(), actor.get_mouth_ptn(), actor.get_mouth_open())

        # set default scale if needed
        self.oldLocalScaleInfo = {}
        boneMorph = [mdef for mdef in self.morphDefineMap.values() if mdef.type == "bone"]
        for mdefine in boneMorph:
            for bname, _, _, scale in mdefine.boneList:
                if not scale:
                    continue
                if bname in self.oldLocalScaleInfo:
                    continue
                wname = self.motionCtrl.boneDefineMap[bname].weightBone
                wobj = MotionController.trySearchObjNameWithLimit(self.motionCtrl.actualBoneRootGo, wname, self.motionCtrl.actualBoneRootGo)
                self.oldLocalScaleInfo[bname] = (wobj.transform, wobj.transform.localScale)

        # set blend function if needed
        self.blendSetWeight = None
        self.blendResetAll = None
        self.blendRendererInfo = None
        blendMorph = [mdef for mdef in self.morphDefineMap.values() if mdef.type == "blend"]
        if len(blendMorph) > 0:
            from extplugins import AIPE
            aipe = AIPE()
            if self.motionCtrl.boneType == "chara" and aipe.isDetected:
                self.aipeBlendShapeEditor = aipe.GetBlendShapesEditor(oci)
                from System.Reflection import BindingFlags
                bindflgs = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy
                try:
                    self.aipeSetBlendShapeWeight = self.aipeBlendShapeEditor.GetType().GetMethod("SetBlendShapeWeight", bindflgs)
                    self.aipeResetAllBlendShape = self.aipeBlendShapeEditor.GetType().GetMethod("ResetAll", bindflgs)
                    self.aipeBlendResetAll()
                    self.initBlendRendererInfo()
                except:
                    raise Exception("Fail to get AIPE BlendShapeEditor's 'SetBlendShapeWeight' or 'ResetAll' function")
                self.blendSetWeight = self.aipeBlendSetWeight
                self.blendResetAll = self.aipeBlendResetAll

    def loadMorphSetting(self):
        from vnactor import tuple3_2_vector3
        self.morphNameList = []
        self.morphDefineMap = {}
        self.morphSequenceChanged = False
        self.morphDefineBackup = {}

        try:
            validMorphType = {"eye", "eyebrow", "mouth", "bone", "blend", "func"}

            ini_filename = self.motionCtrl.boneDefineFilename
            self.morphGroupList = self.motionCtrl.morphGroupList
            self.morphGroupName = self.motionCtrl.morphGroupName
            self.morphGroupSet = self.motionCtrl.morphGroupSet
            
            config = ConfigParser.SafeConfigParser()
            config.read(ini_filename)

            # load morph list
            morphDetailMap = {}
            for k, v in config.items("MorphDefine"):
                mname = k.decode("utf8")
                attrs = v.decode("utf8").split(":")
                if len(attrs) not in {2, 3}:
                    raise Exception("Morph define for <%s> format error."%attrs[0])
                vname = attrs[0]
                mtype = attrs[1].lower()
                if len(attrs) == 3:
                    menable = bool(eval(attrs[2]))
                else:
                    menable = True
                if vname in self.morphNameList:
                    raise Exception("Morph define for <%s> duplicated."%vname)
                if mname in morphDetailMap:
                    raise Exception("Morph define for <%s> share same morph detail others."%vname)
                if mtype not in validMorphType:
                    raise Exception("Morph define for <%s> type error, unknown type of <%s>."%(vname, mtype))
                # build a morph define
                newMD = MorphController.MorphDefine(mname, mtype, menable)
                self.morphNameList.append(vname)
                self.morphDefineMap[vname] = morphDetailMap[mname] = newMD
                # group morph
                for gkey in self.morphGroupSet.keys():
                    if vname in self.morphGroupSet[gkey]:
                        newMD.group = gkey
                        break

            # load morph detail setting
            for mdn in morphDetailMap.keys():
                cName = "Morph_" + mdn
                md = morphDetailMap[mdn]
                if md.type == "bone":
                    md.boneList = []
                    for k, v in config.items(cName):
                        if k not in self.motionCtrl.boneNameList:
                            raise Exception("Morph detail error: undefined bone name <%s>"%k)
                        sv = [k, None, None, None]
                        ss = v.split(":")
                        n = 0
                        while n + 1 < len(ss):
                            if ss[n] == "r":
                                sv[1] = Quaternion.Euler(tuple3_2_vector3(eval(ss[n+1])))
                            if ss[n] == "p":
                                sv[2] = tuple3_2_vector3(eval(ss[n+1]))
                            if ss[n] == "s":
                                sv[3] = tuple3_2_vector3(eval(ss[n+1]))
                            n += 2
                        # boneList: [(bname, rotation, position, scale), ...]
                        md.boneList.append(sv)
                elif md.type == "blend":
                    md.blendList = []
                    for k, v in config.items(cName):
                        if k.startswith("blend"):
                            renderName, blendName, rangeMin, rangeMax = v.split(":")
                            md.blendList.append([renderName, blendName, float(rangeMin), float(rangeMax)])
                elif md.type == "func":
                    md.funcList = []
                    for k, v in config.items(cName):
                        if k.startswith("func"):
                            getFuncName, setFuncName, valueType, rangeMin, rangeMax = v.split(":")
                            md.funcList.append([getFuncName, setFuncName, valueType, eval(rangeMin), eval(rangeMax)])
                else:
                    for k, v in config.items(cName):
                        if k == "pattern":
                            md.pattern = int(v)
                        if k == "range":
                            rv = v.split(",")
                            md.range = [float(rv[0]), float(rv[1])]
                if not md.valid:
                    raise Exception("Invalid morph setting <%s>, necessary setting missed!"%mdn)

        except Exception, e:
            traceback.print_exc()
            print("Error in load morph default setting: " + str(e))
    
    def verify(self):
        # verify works by checking blendRendererInfo.
        # If charactor is replaced, renderer will becomes invalid
        if self.actor and self.motionCtrl and self.blendRendererInfo:
            valid = True
            for renderer, _, _ in self.blendRendererInfo.values():
                if renderer == None:
                    valid = False
                    break
            if not valid:
                print "BlendRendererInfo for %s becomes invalid, try to fix it"%self.CharName
                self.initBlendRendererInfo()
                self.buildMorphIndex()

    def getDummyNameByVmdName(self, vmdName):
        if vmdName in self.morphNameList:
            return self.morphDefineMap[vmdName].name
        return None

    def getVmdNameByDummyName(self, dummyName):
        for vname in self.morphNameList:
            if self.morphDefineMap[vname].name == dummyName:
                return vname
        return None

    def updateMorphDefine(self, vname, newDefine):
        if not self.isMorphDefineChanged(vname):
            self.morphDefineBackup[vname] = self.morphDefineMap[vname].copy()
        newDefine.group = self.morphDefineMap[vname].group  # do not change group
        self.morphDefineMap[vname] = newDefine
        self.buildMorphIndex()

    def restoreMorphDefine(self, vname):
        if self.isMorphDefineChanged(vname):
            self.morphDefineMap[vname] = self.morphDefineBackup[vname]
            od = self.morphDefineBackup.pop(vname)
            self.buildMorphIndex()
            return od
        return None

    def isMorphDefineChanged(self, vname):
        return vname in self.morphDefineBackup

    def importMorphKeyframes(self, filename, keyframes, mode = "renew", ignoreList = []):
        if keyframes == None or len(keyframes) == 0:
            return        

        self.vmdFilePath = filename
        if mode == "renew":
            self.morphKeyFrames = {}
            self.keyframeCustomized = False
        # import morph
        mkfDupCheckTemp = {}
        for mkf in keyframes:
            if mkf.name in ignoreList:
                continue
            if self.morphKeyFrames.has_key(mkf.name):
                # check if it is an existed morph
                if mkf.name in mkfDupCheckTemp:
                    # new added morph, add it
                    if mkf.frameNo not in mkfDupCheckTemp[mkf.name]:
                        self.morphKeyFrames[mkf.name].append(mkf)
                        mkfDupCheckTemp[mkf.name].add(mkf.frameNo)
                    else:
                        print "WARNING: Duplicated keyframe for %s found at #%d, ignored"%(mkf.name, mkf.frameNo)
                else:
                    # existed morph, depend on mode
                    if mode == "replace":
                        self.morphKeyFrames[mkf.name] = [mkf]
                        mkfDupCheckTemp[mkf.name] = {mkf.frameNo}
            else:
                # new bone set, add it
                self.morphKeyFrames[mkf.name] = [mkf]
                mkfDupCheckTemp[mkf.name] = {mkf.frameNo}
        print "Totally %d morph imported"%len(mkfDupCheckTemp)

        # remove unmapped morph
        mappedMorphInfo = []
        unmappedMorphInfo = []
        for vname in mkfDupCheckTemp.keys(): #self.morphKeyFrames.keys():
            if vname not in self.morphNameList:
                #if len(self.morphKeyFrames[vname]) > 1:
                #    print "  VMD Morph %s with %d frame ignored."%(vname, len(self.morphKeyFrames[vname]))
                unmappedMorphInfo.append((vname, len(self.morphKeyFrames[vname])))
            else:
                mappedMorphInfo.append((vname, len(self.morphKeyFrames[vname])))

        for minfo in unmappedMorphInfo:
            del self.morphKeyFrames[minfo[0]]
        print "Totally %d morph will be append to chara"%len(self.morphKeyFrames)

        # build index
        self.buildMorphIndex()

        # auto enable motion controller if needed
        if self.NeedActiveMotion:
            self.motionCtrl.setEnable(1)

        return mappedMorphInfo, unmappedMorphInfo

    def redirectMorphKeyframes(self, filepath):
        # re-direct filename and discard user data
        self.vmdFilePath = filepath
        self.keyframeCustomized = False

    def updateMorphKeyframes(self, nkeyframes):
        # prepare
        groupedNewKeyframes = {}
        for nkf in nkeyframes:
            if nkf.name in groupedNewKeyframes:
                groupedNewKeyframes[nkf.name].add(nkf)
            else:
                groupedNewKeyframes[nkf.name] = {nkf}
            if nkf.name not in self.morphKeyFrames:
                self.morphKeyFrames[nkf.name] = []
        # do update
        rebuildWBI = False
        for tgtName in groupedNewKeyframes.keys():
            hasNew = False
            tgtMorphFrames = self.morphKeyFrames[tgtName]
            existFrames = [kf.frameNo for kf in tgtMorphFrames]
            for nkf in groupedNewKeyframes[tgtName]:
                if nkf.frameNo in existFrames:
                    idx = existFrames.index(nkf.frameNo)
                    tgtMorphFrames[idx] = nkf
                else:
                    hasNew = True
                    tgtMorphFrames.append(nkf)
            if hasNew:
                rebuildWBI = True
                tgtMorphFrames.sort(key=VmdMorphKeyFrame.sortByFrameNo)
        # rebuild wbi
        if rebuildWBI: self.buildMorphIndex()
        self.keyframeCustomized = True
        return rebuildWBI

    def deleteMorphKeyframes(self, tgtNameFramePair):
        # group info
        groupedPair = {}
        for (tgtName, tgtFrame) in tgtNameFramePair:
            if tgtName in groupedPair:
                groupedPair[tgtName].add(tgtFrame)
            else:
                groupedPair[tgtName] = {tgtFrame}
        # check delete index
        toDelIndex = {}
        for tgtName in groupedPair.keys():
            tgtMorphFrames = self.morphKeyFrames[tgtName]
            if len(tgtMorphFrames) == len(groupedPair[tgtName]):
                toDelIndex[tgtName] = "ALL"
            else:
                toDelIndex[tgtName] = []
                for i in range(len(tgtMorphFrames)):
                    if tgtMorphFrames[i].frameNo in groupedPair[tgtName]:
                        toDelIndex[tgtName].append(i)
                        groupedPair[tgtName].remove(tgtMorphFrames[i].frameNo)
                        if len(groupedPair[tgtName]) == 0:
                            break
                toDelIndex[tgtName].reverse()
        # do delete
        for tgtName in toDelIndex.keys():
            if toDelIndex[tgtName] == "ALL":
                del self.morphKeyFrames[tgtName]
            else:
                tgtMorphFrames = self.morphKeyFrames[tgtName]
                for idx in toDelIndex[tgtName]:
                    del tgtMorphFrames[idx]
        # rebuild wbi
        self.buildMorphIndex()
        self.keyframeCustomized = True

    def getMorphKeyframeCountByVmdName(self, vmdname):
        if self.morphKeyFrames.has_key(vmdname):
            return len(self.morphKeyFrames[vmdname])
        else:
            return 0

    def initBlendRendererInfo(self):
        from UnityEngine import SkinnedMeshRenderer
        renderers = self.motionCtrl.actualBoneRootGo.GetComponentsInChildren[SkinnedMeshRenderer](True)
        self.blendRendererInfo = {}
        for renderer in renderers:
            bsCount = renderer.sharedMesh.blendShapeCount
            if bsCount > 0:
                bsNameList = [renderer.sharedMesh.GetBlendShapeName(i) for i in range(bsCount)]
                bsOrgValue = [renderer.GetBlendShapeWeight(i) for i in range(bsCount)]
                self.blendRendererInfo[renderer.name] = (renderer, bsNameList, bsOrgValue)
                #print " SkinnedMeshRender %s:"%(renderer.name)
                #for i in range(4):
                #    print "  - %s = %f"%(bsNameList[i], bsOrgValue[i])

    def getBlendRendererInfo(self, renderName):
        if not self.blendRendererInfo:
            print "Warning: unexpected blend render initialize, should be done when bind chara"
            self.initBlendRendererInfo()
        if renderName in self.blendRendererInfo:
            return self.blendRendererInfo[renderName]
        else:
            return (None, None, None)

    def buildMorphIndex(self):
        def getIndexByBlendName(blendName, bsNameList):
            if blendName in bsNameList:
                return bsNameList.index(blendName)
            else:
                index = None
                for i in range(len(bsNameList)):
                    if bsNameList[i].endswith(blendName):
                        if index == None:
                            index = i
                        else:
                            return None
                return index

        # get max frame no
        self.maxFrameNo = 0
        for mframes in self.morphKeyFrames.values():
            if mframes[-1].frameNo > self.maxFrameNo:
                self.maxFrameNo = mframes[-1].frameNo

        # reset all blend/func/bone morph
        self.resetAllBlendMorph()
        self.resetAllFuncMorph()
        self.resetAllBoneMorph()

        # build morphs index buffer
        self.mouthIndexBuffer = []
        self.eyeIndexBuffer = []
        self.eyebrowIndexBuffer = []
        self.boneIndexBuffer = []
        self.blendIndexBuffer = []
        self.funcIndexBuffer = []
        self.blendResultBuffer = {}
        self.morphIndexMap = {}
        for vname in self.morphNameList:
            if self.morphKeyFrames.has_key(vname):
                mi = GData()
                mi.vmdName = vname

                # setting
                mi.define = self.morphDefineMap[vname]

                # runtime
                mi.weight = 0.0
                mi.wPtn = 0
                mi.wVal = 0
                
                # keyframe
                mi.keyframes = self.morphKeyFrames[vname]
                mi.prevIndex = 0
                mi.prevFrameNo = mi.keyframes[mi.prevIndex].frameNo
                mi.nextIndex = 1 if len(mi.keyframes) > 1 else 0
                mi.nextFrameNo = mi.keyframes[mi.nextIndex].frameNo
                mi.firstFrameNo = mi.keyframes[0].frameNo
                mi.lastFrameNo = mi.keyframes[-1].frameNo

                # legacy type
                if mi.define.type == "mouth" or mi.define.type == "eye":
                    if not self.hasMouthEye:
                        continue
                if mi.define.type == "eyebrow":
                    if not self.hasEyebrow:
                        continue

                # bone type
                if mi.define.type == "bone":
                    mi.lastWeight = None
                    btrans = []
                    badjs = []
                    for bi in mi.define.boneList:
                        btrans.append(self.motionCtrl.vmdBones[bi[0]].transform)
                        badjs.append(self.motionCtrl.boneAdjustMap[bi[0]])
                    mi.transform = tuple(btrans)
                    mi.boneAdjust = tuple(badjs)
                
                # blend type
                if mi.define.type == "blend":
                    mi.blendKey = []
                    for renderName, blendName, _, _ in mi.define.blendList:
                        # solve and check SkinnedMeshRenderer
                        renderer, bsNameList, bsOrgValue = self.getBlendRendererInfo(renderName)
                        if renderer == None:
                            raise Exception("Fail to resolve blend type morph %s: fail to find SkinnedMeshRenderer '%s'"%(mi.define.name, renderName))
                        index = getIndexByBlendName(blendName, bsNameList)
                        if index == None:
                            raise Exception("Fail to resolve blend type morph %s: fail to get blend shape index for '%s', not exists or duplicated in SkinnedMeshRenderer '%s'."%(mi.define.name, blendName, renderName))
                        try:
                            orgValue = bsOrgValue[index]
                        except:
                            raise Exception("Fail to get original value of blend type morph %s: fail to find blend shape named '%s' in SkinnedMeshRenderer '%s'."%(mi.define.name, blendName, renderName))
                        # buffer of result, for every blendkey in all blend morph, save the [sum, weight-count, old-value, SkinnedMeshRenderer, blendIndex, originalValue] for process
                        smrKey = renderName + "#" + str(index)
                        mi.blendKey.append(smrKey)
                        if smrKey not in self.blendResultBuffer:
                            self.blendResultBuffer[smrKey] = [0, 0, 0, renderer, index, orgValue]
                            #print 'add blend result for %s:%s index=%d, orgvalue = %f'%(renderName, blendName, index, orgValue)

                # func
                if mi.define.type == "func":
                    mi.lastWeight = None
                    mi.solvedFuncList = []
                    for getFuncName, setFuncName, valueType, _, _ in mi.define.funcList:
                        # solve functions
                        getFunc = ExtMorphFuncLib.getMethod(self.actor, getFuncName)
                        if not getFunc:
                            raise Exception("Fail to resolve func type morph %s: fail to find get function %s in %s"%(mi.define.name, getFuncName, str(self.actor)))
                        setFunc = ExtMorphFuncLib.getMethod(self.actor, setFuncName)
                        if not setFunc:
                            raise Exception("Fail to resolve func type morph %s: fail to find set function %s in %s"%(mi.define.name, setFuncName, str(self.actor)))
                        try:
                            orginalValue = getFunc()
                            VmdLib.getScaledValue(orginalValue, 1)
                            setFunc(orginalValue)
                        except Exception, e:
                            raise Exception("Fail to resovle func type morph %s: Fail to run get/set function: %s"%(mi.define.name, str(e)))
                        mi.solvedFuncList.append((getFunc, setFunc, orginalValue, valueType))

                # seperate by type
                if mi.define.type == "mouth":
                    self.mouthIndexBuffer.append(mi)
                elif mi.define.type == "eye":
                    self.eyeIndexBuffer.append(mi)
                elif mi.define.type == "eyebrow":
                    self.eyebrowIndexBuffer.append(mi)
                elif mi.define.type == "bone":
                    self.boneIndexBuffer.append(mi)
                elif mi.define.type == "blend":
                    self.blendIndexBuffer.append(mi)
                elif mi.define.type == "func":
                    self.funcIndexBuffer.append(mi)
                else:
                    raise Exception("Unexpected morph type [%s]"%mi.define.type)
                self.morphIndexMap[vname] = mi
        
    def convertKeyframes(self):

        def optimizeConvertedKeyframes(tgtKeyframes):
            toDel = []
            for i in range(1, len(tgtKeyframes)-1):
                pf = tgtKeyframes[i-1]
                cf = tgtKeyframes[i]
                nf = tgtKeyframes[i+1]
                if pf.value == cf.value == nf.value:
                    toDel.append(cf)
            for df in toDel:
                tgtKeyframes.remove(df)
            return len(toDel)

        # deconvert and check
        self.deconvertKeyframes()
        if not len(self.morphKeyFrames):
            print "No morph keyframes yet! import keyframe first!"
            return False
        if not self.hasMouthEye:
            print "Cannot convert for none-charactor!"
            return False

        # rebuild index for ensure
        self.buildMorphIndex()
        if not self.HasLegacyData:
            print "No legacy data for convert!"
            return False

        # reset converted keyframe buffer
        name_mp = 0
        name_mo = 1
        name_ep = 2
        name_eo = 3
        name_eb = 4
        self.convertedKeyframes = {}

        # init first frame
        prevMouthIndex = self.getMouthFrameIndexed(0)
        prevMouthPtn = self.defaultMouthPtn if not prevMouthIndex else prevMouthIndex.wPtn
        prevMouthOpn = 0 if not prevMouthIndex else prevMouthIndex.wVal
        prevMouthOpenKeyframeNo = 0
        prevEyeIndex = self.getEyeFrameIndexed(0)
        prevEyePtn = self.defaultEyesPtn if not prevEyeIndex else prevEyeIndex.wPtn
        prevEyeOpn = 1 if not prevEyeIndex else prevEyeIndex.wVal
        prevEyeOpenKeyframeNo = 0
        prevEyebrowIndex = self.getEyebrowFrameIndexed(0)
        prevEyebrowPtn = self.defaultEyebrowPtn if not prevEyebrowIndex else prevEyebrowIndex.wPtn
        # build first keyframe
        self.convertedKeyframes[name_mp] = [MorphController.CnvMorphKeyframe(name_mp, 0, prevMouthPtn, False)]
        self.convertedKeyframes[name_mo] = [MorphController.CnvMorphKeyframe(name_mo, 0, prevMouthOpn, True)]
        self.convertedKeyframes[name_ep] = [MorphController.CnvMorphKeyframe(name_ep, 0, prevEyePtn, False)]
        self.convertedKeyframes[name_eo] = [MorphController.CnvMorphKeyframe(name_eo, 0, prevEyeOpn, True)]
        self.convertedKeyframes[name_eb] = [MorphController.CnvMorphKeyframe(name_eb, 0, prevEyebrowPtn, False)]
        # build rest all frames
        for frameNo in range(1, self.maxFrameNo + 1):
            # mouth
            mi = self.getMouthFrameIndexed(frameNo)
            hitKeyframe = mi != prevMouthIndex or (mi and mi.prevFrameNo == frameNo)
            mouthPtn = self.defaultMouthPtn if not mi else mi.wPtn
            mouthOpn = 0 if not mi else mi.wVal
            if mouthPtn != prevMouthPtn:
                # insert mouth pattern keyframe
                nf = MorphController.CnvMorphKeyframe(name_mp, frameNo, mouthPtn, False)
                self.convertedKeyframes[name_mp].append(nf)
            if hitKeyframe:
                # hit a keyframe, 4 possibility: none->morph, morph->none, morph1->morph1, morph1->morph2
                if prevMouthIndex == None and mi != None:
                    # none->morph, check if value start from 0
                    if mouthOpn != 0 and prevMouthOpenKeyframeNo < frameNo - 1:
                        # insert a prev keyframe to keep value = 0 before keyframe
                        nf = MorphController.CnvMorphKeyframe(name_mo, frameNo-1, 0, True)
                        self.convertedKeyframes[name_mo].append(nf)
                elif prevMouthIndex != None and mi != None and prevMouthIndex != mi:
                    # morph1->morph2, check if need insert prev keyframe
                    if prevMouthOpenKeyframeNo < frameNo - 1:
                        self.getMorphFrameIndexed(frameNo, prevMouthIndex)
                        pValue = prevMouthIndex.define.getValue(prevMouthIndex.weight)
                        needPrevKeyframe = pValue != mouthOpn
                    else:
                        needPrevKeyframe = False
                    # add a prev keyframe
                    if needPrevKeyframe:
                        nf = MorphController.CnvMorphKeyframe(name_mo, frameNo-1, prevMouthOpn, True)
                        self.convertedKeyframes[name_mo].append(nf)
                else:
                    # morph1->morph1 or morph->none, add a new keyframe only, no prev frame needed
                    pass
                # add cur keyframe
                nf = MorphController.CnvMorphKeyframe(name_mo, frameNo, mouthOpn, True)
                self.convertedKeyframes[name_mo].append(nf)
                prevMouthOpenKeyframeNo = frameNo
            prevMouthIndex = mi
            prevMouthPtn = mouthPtn
            prevMouthOpn = mouthOpn

            # eyes
            mi = self.getEyeFrameIndexed(frameNo)
            hitKeyframe = mi != prevEyeIndex or (mi and mi.prevFrameNo == frameNo)
            eyePtn = self.defaultEyesPtn if not mi else mi.wPtn
            eyeOpn = 1 if not mi else mi.wVal
            if eyePtn != prevEyePtn:
                # insert eye pattern keyframe
                nf = MorphController.CnvMorphKeyframe(name_ep, frameNo, eyePtn, False)
                self.convertedKeyframes[name_ep].append(nf)
            if hitKeyframe:
                # hit a keyframe, 4 possibility: none->morph, morph->none, morph1->morph1, morph1->morph2
                if prevEyeIndex == None and mi != None:
                    # none->morph, check if value start from 1
                    if eyeOpn != 1 and prevEyeOpenKeyframeNo < frameNo - 1:
                        # insert a prev keyframe to keep value = 1 before keyframe
                        nf = MorphController.CnvMorphKeyframe(name_eo, frameNo-1, 1, True)
                        self.convertedKeyframes[name_eo].append(nf)
                elif prevEyeIndex != None and mi != None and prevEyeIndex != mi:
                    # morph1->morph2, check if need insert prev keyframe
                    if prevEyeOpenKeyframeNo < frameNo - 1:
                        self.getMorphFrameIndexed(frameNo, prevEyeIndex)
                        pValue = prevEyeIndex.define.getValue(prevEyeIndex.weight)
                        needPrevKeyframe = pValue != eyeOpn
                    else:
                        needPrevKeyframe = False
                    # add a prev keyframe
                    if needPrevKeyframe:
                        nf = MorphController.CnvMorphKeyframe(name_eo, frameNo-1, prevEyeOpn, True)
                        self.convertedKeyframes[name_eo].append(nf)
                else:
                    # morph1->morph1 or morph->none, add a new keyframe only, no prev frame needed
                    pass
                # add cur keyframe
                nf = MorphController.CnvMorphKeyframe(name_eo, frameNo, eyeOpn, True)
                self.convertedKeyframes[name_eo].append(nf)
                prevEyeOpenKeyframeNo = frameNo
            prevEyeIndex = mi
            prevEyePtn = eyePtn
            prevEyeOpn = eyeOpn

            # eyebrow
            if not self.hasEyebrow: continue
            mi = self.getEyebrowFrameIndexed(frameNo)
            eyebrowPtn = self.defaultEyebrowPtn if not mi else mi.wPtn
            if eyebrowPtn != prevEyebrowPtn:
                # insert eye pattern keyframe
                nf = MorphController.CnvMorphKeyframe(name_eb, frameNo, eyebrowPtn, False)
                self.convertedKeyframes[name_eb].append(nf)
            prevEyebrowIndex = mi
            prevEyebrowPtn = eyebrowPtn

        # optimize converted frames
        omp = optimizeConvertedKeyframes(self.convertedKeyframes[name_mp])
        omo = optimizeConvertedKeyframes(self.convertedKeyframes[name_mo])
        oep = optimizeConvertedKeyframes(self.convertedKeyframes[name_ep])
        oeo = optimizeConvertedKeyframes(self.convertedKeyframes[name_eo])
        oeb = optimizeConvertedKeyframes(self.convertedKeyframes[name_eb])
        print "converted keyframe optimized: deleted %d mouth ptn, %d mouth open, %d eye ptn, %d eye open, %d eyebrow keyframes"%(omp, omo, oep, oeo, oeb)
        
        # build converted index
        self.buildConvertIndex()
        
        # done
        self.converted = True
        return True

    def buildConvertIndex(self):

        def buildCnvIndexByTarget(tgtKeyframes):
            ci = GData()
            # runtime
            ci.wVal = 0
            # keyframe
            ci.keyframes = tgtKeyframes
            ci.prevIndex = 0
            ci.prevFrameNo = ci.keyframes[ci.prevIndex].frameNo
            ci.nextIndex = 1 if len(ci.keyframes) > 1 else 0
            ci.nextFrameNo = ci.keyframes[ci.nextIndex].frameNo
            ci.firstFrameNo = ci.keyframes[0].frameNo
            ci.lastFrameNo = ci.keyframes[-1].frameNo
            return ci
        def buildResultIndex(name):
            ci = GData()
            ci.vmdName = name
            ci.wPtn = 0
            ci.wVal = 0
            return ci

        # get max frame no
        self.maxFrameNo = 0
        for cframes in self.convertedKeyframes.values():
            if cframes[-1].frameNo > self.maxFrameNo:
                self.maxFrameNo = cframes[-1].frameNo

        # build converted index
        self.convertedFrameIndex = {}
        for i in self.convertedKeyframes.keys():
            self.convertedFrameIndex[i] = buildCnvIndexByTarget(self.convertedKeyframes[i])

        # build reslut index only for hold result
        self.convertedMouthIndex = buildResultIndex("cnv_mouth")
        self.convertedEyeIndex = buildResultIndex("cnv_eye")
        self.convertedEyebrowIndex = buildResultIndex("cnv_eyebrow")

    def deconvertKeyframes(self):
        self.convertedKeyframes = None
        self.convertedFrameIndex = None
        self.convertedMouthIndex = None
        self.convertedEyeIndex = None
        self.convertedEyebrowIndex = None
        self.converted = False

    def updateConvertKeyframes(self, newCnvKeyframes):
        if not self.converted:
            return
        # prepare
        groupedNewKeyframes = {}
        for nkf in newCnvKeyframes:
            if nkf.nIndex in groupedNewKeyframes:
                groupedNewKeyframes[nkf.nIndex].add(nkf)
            else:
                groupedNewKeyframes[nkf.nIndex] = {nkf}
            if nkf.nIndex not in self.convertedKeyframes:
                self.convertedKeyframes[nkf.nIndex] = []
        # do update
        rebuildWBI = False
        for tgtIndex in groupedNewKeyframes.keys():
            hasNew = False
            tgtCnvFrames = self.convertedKeyframes[tgtIndex]
            existFrames = [kf.frameNo for kf in tgtCnvFrames]
            for nkf in groupedNewKeyframes[tgtIndex]:
                if nkf.frameNo in existFrames:
                    idx = existFrames.index(nkf.frameNo)
                    tgtCnvFrames[idx] = nkf
                else:
                    hasNew = True
                    tgtCnvFrames.append(nkf)
            if hasNew:
                rebuildWBI = True
                tgtCnvFrames.sort(key=MorphController.CnvMorphKeyframe.sortByFrameNo)
        # rebuild wbi
        if rebuildWBI: self.buildConvertIndex()
        return rebuildWBI

    def deleteConvertKeyframes(self, tgtNameFramePair):
        if not self.converted:
            return
        # group info
        groupedPair = {}
        for (tgtIndex, tgtFrame) in tgtNameFramePair:
            if tgtIndex in groupedPair:
                groupedPair[tgtIndex].add(tgtFrame)
            else:
                groupedPair[tgtIndex] = {tgtFrame}
        # check delete index
        toDelIndex = {}
        for tgtIndex in groupedPair.keys():
            tgtCnvFrames = self.convertedKeyframes[tgtIndex]
            if len(tgtCnvFrames) == len(groupedPair[tgtIndex]):
                toDelIndex[tgtIndex] = "ALL"
            else:
                toDelIndex[tgtIndex] = []
                for i in range(len(tgtCnvFrames)):
                    if tgtCnvFrames[i].frameNo in groupedPair[tgtIndex]:
                        toDelIndex[tgtIndex].append(i)
                        groupedPair[tgtIndex].remove(tgtCnvFrames[i].frameNo)
                        if len(groupedPair[tgtIndex]) == 0:
                            break
                toDelIndex[tgtIndex].reverse()
        # do delete
        for tgtIndex in toDelIndex.keys():
            if toDelIndex[tgtIndex] == "ALL":
                del self.convertedKeyframes[tgtIndex]
            else:
                tgtCnvFrames = self.convertedKeyframes[tgtIndex]
                for idx in toDelIndex[tgtIndex]:
                    del tgtCnvFrames[idx]
        # rebuild wbi
        self.buildConvertIndex()

    def cleanup(self):
        try:
            if self.actor:
                # eye, mouth, eyebrow
                if self.hasEyebrow:
                    self.actor.set_eyebrow_ptn(self.oldEyebrowPtn)
                if self.hasMouthEye:
                    self.actor.set_eyes_ptn(self.oldEyesPtn)
                    self.actor.set_eyes_open(self.oldEyesOpen)
                    self.actor.set_mouth_ptn(self.oldMouthPtn)
                    self.actor.set_mouth_open(self.oldMouthOpen)
                # bone
                self.resetAllBoneMorph()
                # blend
                self.resetAllBlendMorph()
                # func
                self.resetAllFuncMorph()
        except:
            pass
        self.deconvertKeyframes()
        self.actor = None
        self.hasEyebrow = False
        self.hasMouthEye = False
        self.motionCtrl = None

    def getMorphIndexInfo(self, vMorphName):
        if vMorphName in self.morphIndexMap:
            return self.morphIndexMap[vMorphName]
        else:
            return None

    def getMorphWeight(self, vMorphName):
        mi = self.getMorphIndexInfo(vMorphName)
        if mi:
            return mi.weight
        else:
            return 0

    def getConvertedIndexInfo(self, vMorphNameOrIndex):
        nIndex = self.getConvertedMorphNameIndex(vMorphNameOrIndex)
        if nIndex != None and nIndex in self.convertedFrameIndex:
            return self.convertedFrameIndex[nIndex]
        else:
            return None

    def getConvertedMorphNameIndex(self, vMorphNameOrIndex):
        if not self.converted:
            return None
        if vMorphNameOrIndex in self.convertedFrameIndex:
            return vMorphNameOrIndex
        if vMorphNameOrIndex in self.convertedFrameName:
            return self.convertedFrameName.index(vMorphNameOrIndex)
        return None

    def getMorphFrameIndexed(self, tgtFrameNo, mi):
        if mi.firstFrameNo >= tgtFrameNo:
            mi.prevIndex = 0
            mi.prevFrameNo = mi.keyframes[mi.prevIndex].frameNo
            mi.nextIndex = 1 if len(mi.keyframes) > 1 else 0
            mi.nextFrameNo = mi.keyframes[mi.nextIndex].frameNo
            mi.weight = mi.keyframes[mi.prevIndex].weight
            return
        elif mi.lastFrameNo <= tgtFrameNo:
            mi.weight = mi.keyframes[-1].weight
            return
        else:
            # firstFrameNo < tgtFrameNo < lastFrameNo
            if mi.prevFrameNo < tgtFrameNo < mi.nextFrameNo:
                prevFrame = mi.keyframes[mi.prevIndex]
                nextFrame = mi.keyframes[mi.nextIndex]
            elif tgtFrameNo >= mi.nextFrameNo:
                # seek foreward
                while 1:
                    mi.prevIndex += 1
                    prevFrame = mi.keyframes[mi.prevIndex]
                    mi.prevFrameNo = prevFrame.frameNo
                    mi.nextIndex += 1
                    nextFrame = mi.keyframes[mi.nextIndex]
                    mi.nextFrameNo = nextFrame.frameNo
                    if mi.prevFrameNo < tgtFrameNo < mi.nextFrameNo:
                        break
                    if mi.prevFrameNo == tgtFrameNo:
                        mi.weight = prevFrame.weight
                        return
            elif tgtFrameNo < mi.prevFrameNo:
                # seek backward
                for i in range(len(mi.keyframes)):
                    if mi.keyframes[i].frameNo > tgtFrameNo:
                        mi.prevIndex = i - 1
                        prevFrame = mi.keyframes[mi.prevIndex]
                        mi.prevFrameNo = prevFrame.frameNo
                        mi.nextIndex = i
                        nextFrame = mi.keyframes[mi.nextIndex]
                        mi.nextFrameNo = nextFrame.frameNo
                        break
                    if mi.keyframes[i].frameNo == tgtFrameNo:
                        mi.prevIndex = i
                        prevFrame = mi.keyframes[mi.prevIndex]
                        mi.prevFrameNo = prevFrame.frameNo
                        mi.nextIndex = i + 1
                        nextFrame = mi.keyframes[mi.nextIndex]
                        mi.nextFrameNo = nextFrame.frameNo
                        mi.weight = prevFrame.weight
                        return 
                #print "Seek backward for #%f: [%d]#%f - [%d]#%f"%(tgtFrameNo, mi.prevIndex, mi.prevFrameNo, mi.nextIndex, mi.nextFrameNo)
            else:
                # tgtFrameNo == mi.prevFrameNo:
                mi.weight = mi.keyframes[mi.prevIndex].weight
                return

            # do interploate
            t = float(tgtFrameNo - mi.prevFrameNo) / float(mi.nextFrameNo - mi.prevFrameNo)
            mi.weight = prevFrame.weight + (nextFrame.weight - prevFrame.weight) * t
            return

    def getConvertedFrameIndexed(self, tgtFrameNo, ci):
        if ci.firstFrameNo >= tgtFrameNo:
            ci.prevIndex = 0
            ci.prevFrameNo = ci.keyframes[ci.prevIndex].frameNo
            ci.nextIndex = 1 if len(ci.keyframes) > 1 else 0
            ci.nextFrameNo = ci.keyframes[ci.nextIndex].frameNo
            ci.wVal = ci.keyframes[ci.prevIndex].value
            return
        elif ci.lastFrameNo <= tgtFrameNo:
            ci.wVal = ci.keyframes[-1].value
            return
        else:
            # firstFrameNo < tgtFrameNo < lastFrameNo
            if ci.prevFrameNo < tgtFrameNo < ci.nextFrameNo:
                prevFrame = ci.keyframes[ci.prevIndex]
                nextFrame = ci.keyframes[ci.nextIndex]
            elif tgtFrameNo >= ci.nextFrameNo:
                # seek foreward
                while 1:
                    ci.prevIndex += 1
                    prevFrame = ci.keyframes[ci.prevIndex]
                    ci.prevFrameNo = prevFrame.frameNo
                    ci.nextIndex += 1
                    nextFrame = ci.keyframes[ci.nextIndex]
                    ci.nextFrameNo = nextFrame.frameNo
                    if ci.prevFrameNo < tgtFrameNo < ci.nextFrameNo:
                        break
                    if ci.prevFrameNo == tgtFrameNo:
                        ci.wVal = prevFrame.value
                        return
            elif tgtFrameNo < ci.prevFrameNo:
                # seek backward
                for i in range(len(ci.keyframes)):
                    if ci.keyframes[i].frameNo > tgtFrameNo:
                        ci.prevIndex = i - 1
                        prevFrame = ci.keyframes[ci.prevIndex]
                        ci.prevFrameNo = prevFrame.frameNo
                        ci.nextIndex = i
                        nextFrame = ci.keyframes[ci.nextIndex]
                        ci.nextFrameNo = nextFrame.frameNo
                        break
                    if ci.keyframes[i].frameNo == tgtFrameNo:
                        ci.prevIndex = i
                        prevFrame = ci.keyframes[ci.prevIndex]
                        ci.prevFrameNo = prevFrame.frameNo
                        ci.nextIndex = i + 1
                        nextFrame = ci.keyframes[ci.nextIndex]
                        ci.nextFrameNo = nextFrame.frameNo
                        ci.wVal = prevFrame.value
                        return 
            else:
                # tgtFrameNo == ci.prevFrameNo:
                ci.wVal = ci.keyframes[ci.prevIndex].value
                return

            # do interploate
            if prevFrame.interpolate:
                t = float(tgtFrameNo - ci.prevFrameNo) / float(ci.nextFrameNo - ci.prevFrameNo)
                ci.wVal = prevFrame.value + (nextFrame.value - prevFrame.value) * t
                return
            else:
                ci.wVal = prevFrame.value
                return

    def getCharMorphFrameIndexed(self, mType, frameNo):
        if self.converted:
            # convert mode
            # target by type
            if mType == "mouth":
                if 0 in self.convertedFrameIndex:
                    pti = self.convertedFrameIndex[0]
                    self.getConvertedFrameIndexed(frameNo, pti)
                    self.convertedMouthIndex.wPtn = pti.wVal
                else:
                    self.convertedMouthIndex.wPtn = self.defaultMouthPtn
                if 1 in self.convertedFrameIndex:
                    vli = self.convertedFrameIndex[1]
                    self.getConvertedFrameIndexed(frameNo, vli)
                    self.convertedMouthIndex.wVal = vli.wVal
                else:
                    self.convertedMouthIndex.wVal = 0
                return self.convertedMouthIndex
            elif mType == "eye":
                if 2 in self.convertedFrameIndex:
                    pti = self.convertedFrameIndex[2]
                    self.getConvertedFrameIndexed(frameNo, pti)
                    self.convertedEyeIndex.wPtn = pti.wVal
                else:
                    self.convertedEyeIndex.wPtn = self.defaultEyesPtn
                if 3 in self.convertedFrameIndex:
                    vli = self.convertedFrameIndex[3]
                    self.getConvertedFrameIndexed(frameNo, vli)
                    self.convertedEyeIndex.wVal = vli.wVal
                else:
                    self.convertedEyeIndex.wVal = 1
                return self.convertedEyeIndex
            elif mType == "eyebrow":
                if 4 in self.convertedFrameIndex:
                    pti = self.convertedFrameIndex[4]
                    self.getConvertedFrameIndexed(frameNo, pti)
                    self.convertedEyebrowIndex.wPtn = pti.wVal
                else:
                    self.convertedEyebrowIndex.wPtn = self.defaultEyebrowPtn
                return self.convertedEyebrowIndex
            else:
                raise Exception("Unexpected morph type for charactor: " + str(mType))
        else:
            # unconvert mode
            # target by type
            if mType == "mouth":
                indexBuffer = self.mouthIndexBuffer
            elif mType == "eye":
                indexBuffer = self.eyeIndexBuffer
            elif mType == "eyebrow":
                indexBuffer = self.eyebrowIndexBuffer
            else:
                raise Exception("Unexpected morph type for charactor: " + str(mType))
            # get weighted mi
            for mi in indexBuffer:
                if not mi.define.enable:
                    continue
                self.getMorphFrameIndexed(frameNo, mi)
                if mi.weight > 0:
                    mi.wPtn = mi.define.pattern
                    mi.wVal = mi.define.getValue(mi.weight)
                    return mi
            return None

    def getMouthFrameIndexed(self, frameNo):
        return self.getCharMorphFrameIndexed("mouth", frameNo)

    def getEyeFrameIndexed(self, frameNo):
        return self.getCharMorphFrameIndexed("eye", frameNo)

    def getEyebrowFrameIndexed(self, frameNo):
        return self.getCharMorphFrameIndexed("eyebrow", frameNo)

    def setCharMorphFrameIndexed(self, mType, frameNo, force = False):
        if mType == "mouth":
            self.setMouthFrameIndexed(frameNo, force)
        elif mType == "eye":
            self.setEyeFrameIndexed(frameNo, force)
        elif mType == "eyebrow":
            self.setEyebrowFrameIndexed(frameNo, force)
        else:
            raise Exception("Unexpected morph type: " + str(mType))

    def setMouthFrameIndexed(self, frameNo, force = False):
        mi = self.getMouthFrameIndexed(frameNo)
        if mi:
            ptn = mi.wPtn
            val = mi.wVal
        else:
            ptn = self.defaultMouthPtn
            val = 0
        if ptn != self.lastMouthPtn or force:
            self.actor.set_mouth_ptn(ptn)
            self.lastMouthPtn = ptn
        if val != self.lastMouthOpen or force:
            self.actor.set_mouth_open(val)
            self.lastMouthOpen = val

    def setEyeFrameIndexed(self, frameNo, force = False):
        mi = self.getEyeFrameIndexed(frameNo)
        if mi:
            ptn = mi.wPtn
            val = mi.wVal
        else:
            ptn = self.defaultEyesPtn
            val = 1
        if ptn != self.lastEyesPtn or force:
            self.actor.set_eyes_ptn(ptn)
            self.lastEyesPtn = ptn
        if val != self.lastEyesOpen or force:
            self.actor.set_eyes_open(val)
            self.lastEyesOpen = val

    def setEyebrowFrameIndexed(self, frameNo, force = False):
        mi = self.getEyebrowFrameIndexed(frameNo)
        if mi:
            ptn = mi.wPtn
        else:
            ptn = self.defaultEyebrowPtn
        if ptn != self.lastEyebrowPtn or force:
            self.actor.set_eyebrow_ptn(ptn)
            self.lastEyebrowPtn = ptn

    def setBoneFrameIndexed(self, mi, force = False):
        if mi.lastWeight != mi.weight or force:
            # update
            mi.lastWeight = mi.weight
            #print " bone %s: weight: %f"%(mi.vmdName, mi.weight)
            bt = mi.define.getValue(mi.weight)
            for i in range(len(bt)):
                if bt[i][1]:
                    mi.transform[i].localRotation = mi.boneAdjust[i].GetAdjustedRotation(bt[i][1])
                if bt[i][2]:
                    mi.transform[i].localPosition = mi.boneAdjust[i].GetAdjustedPosition(bt[i][2])
                if bt[i][3]:
                    scaleTrans, os = self.oldLocalScaleInfo[mi.define.boneList[i][0]]
                    ds = bt[i][3]
                    scaleTrans.localScale = Vector3(os.x * ds.x, os.y * ds.y, os.z * ds.z)

    def resetAllBoneMorph(self):
        if hasattr(self, "boneIndexBuffer") and len(self.boneIndexBuffer) > 0:
            # bone positon and rotation
            for mi in self.boneIndexBuffer:
                for i in range(len(mi.transform)):
                    mi.transform[i].localRotation = mi.boneAdjust[i].GetAdjustedRotation(Quaternion.identity)
                    mi.transform[i].localPosition = mi.boneAdjust[i].GetAdjustedPosition(Vector3.zero)
            # bone scale
            for ltrans, oldScaleValue in self.oldLocalScaleInfo.values():
                ltrans.localScale = oldScaleValue


    def setAllBlendFrameIndexed(self, force = False):
        for mi in self.blendIndexBuffer:
            if not mi.define.enable:
                continue
            if mi.weight > 0:
                bt = mi.define.getValue(mi.weight)
                for i in range(len(bt)):
                    key = mi.blendKey[i]
                    self.blendResultBuffer[key][0] += bt[i]
                    self.blendResultBuffer[key][1] += 1
        for blendBufferData in self.blendResultBuffer.values():
            if blendBufferData[1]:
                w = blendBufferData[0] / blendBufferData[1]
            else:
                w = blendBufferData[5]
            if blendBufferData[2] != w or force:
                # setup W
                blendBufferData[2] = w
                self.blendSetWeight(blendBufferData[3], blendBufferData[4], w)
            blendBufferData[0] = 0
            blendBufferData[1] = 0

    def aipeBlendSetWeight(self, renderer, index, blendWeight):
        param = (renderer, index, Single(blendWeight))
        self.aipeSetBlendShapeWeight.Invoke(self.aipeBlendShapeEditor, param)

    def aipeBlendResetAll(self):
        self.aipeResetAllBlendShape.Invoke(self.aipeBlendShapeEditor, ())

    def resetAllBlendMorph(self):
        if hasattr(self, "blendIndexBuffer") and len(self.blendIndexBuffer) > 0 and self.blendResetAll:
            self.blendResetAll()

    def setFuncFrameIndexed(self, mi, force = False):
        if mi.lastWeight != mi.weight or force:
            # update
            mi.lastWeight = mi.weight
            #print " func %s: weight: %f"%(mi.vmdName, mi.weight)
            ft = mi.define.getValue(mi.weight)
            for i in range(len(ft)):
                _, setFunc, orgValue, valueType = mi.solvedFuncList[i]
                if valueType == "scale":
                    nVal = VmdLib.getScaledValue(orgValue, ft[i])
                elif valueType == "range":
                    nVal = ft[i]
                else:
                    print "value type of '%s' for func type morph is not supported yet!"%valueType
                    continue    # not-supported
                #print "set func", setFunc,  ", weight =", mi.weight, ", val =", nVal
                setFunc(nVal)

    def resetAllFuncMorph(self):
        if hasattr(self, "funcIndexBuffer") and len(self.funcIndexBuffer) > 0:
            for mi in self.funcIndexBuffer:
                for _, setFunc, orgValue, _ in mi.solvedFuncList:
                    setFunc(orgValue)

    def setFrame(self, frameNo, force = False):
        if not self.enable or not self.actor:
            return
        if self.delay > 0:
            frameNo -= self.delay * 30.0

        # chara morph
        if self.motionCtrl.boneType == "chara" and self.HasLegacyData:
            # mouth
            self.setMouthFrameIndexed(frameNo, force)
            # eye
            self.setEyeFrameIndexed(frameNo, force)
            # eyebrow
            self.setEyebrowFrameIndexed(frameNo, force)
        # bone morph
        for mi in self.boneIndexBuffer:
            if not mi.define.enable:
                continue
            self.getMorphFrameIndexed(frameNo, mi)
            self.setBoneFrameIndexed(mi, force)
        # blend morph
        for mi in self.blendIndexBuffer:
            if not mi.define.enable:
                continue
            self.getMorphFrameIndexed(frameNo, mi)
        self.setAllBlendFrameIndexed(force)
        # func morph
        for mi in self.funcIndexBuffer:
            if not mi.define.enable:
                continue
            self.getMorphFrameIndexed(frameNo, mi)
            self.setFuncFrameIndexed(mi, force)

class CameraController():
    @property
    def TotalFrameLength(self):
        return int(self.delay * 30.0) + self.maxFrameNo

    @property
    def KeyframesForExport(self):
        return self.cameraKeyFrames

    @property
    def maxFrameNo(self):
        if len(self.cameraKeyFrames) > 0:
            return self.cameraKeyFrames[-1].frameNo
        else:
            return 0

    @property
    def FileName(self):
        if self.vmdFilePath and len(self.vmdFilePath) > 0:
            return os.path.split(self.vmdFilePath)[1]
        else:
            return self.vmdFilePath

    def __init__(self, filepath = "NewCamera", keyframes = []):
        # keyframes
        self.vmdFilePath = filepath
        self.cameraKeyFrames = keyframes
        self.keyframeCustomized = False
        self.fsIndex = 0

        # camera scale and offset
        self.enable = True
        self.posScale = 1
        self.dstScale = 1
        self.backupScale = 1
        self.syncScale = True
        self.offPos = Vector3.zero
        self.offRot = Vector3.zero
        self.delay = 0.0
        self.useQuaternion = False

        # camera multi target
        self.enableMultiTarget = False
        self.switchProbability = 50
        self.switchMinFramePeriod = 60
        self.switchMaxFramePeriod = 900
        self.multiTargets = None
        self.lastSwitchFrame = None
        self.switchChanceFrame = None
        self.currentTargetIndex = None

        # runtime
        from Studio import Studio
        self.cameraCtrl = Studio.Instance.cameraCtrl
        self.cameraData = self.cameraCtrl.cameraData

        # load default axis scale to pos/dst scale
        try:
            filename = MotionController.DefaultDefineFile()
            config = ConfigParser.SafeConfigParser()
            config.read(filename)
            # read and check general setting
            for k, v in config.items("General"):
                if k == "axisscale":
                    axisScale = float(v)
                    self.posScale = self.dstScale = self.backupScale = axisScale
        except:
            raise Exception("Fail to create camera controller, miss 'AxisScale' setting in [General] catelog in %s"%(MotionController.DefaultDefineFile()))

    def exportSetting(self):
        setting = {}
        # export general setting
        setting["filename"] = self.vmdFilePath
        setting["enable"] = str(self.enable)
        setting["posScale"] = str(self.posScale)
        setting["dstScale"] = str(self.dstScale)
        setting["syncScale"] = str(self.syncScale)
        setting["offPos"] = script2string(self.offPos)
        setting["offRot"] = script2string(self.offRot)
        setting["delay"] = str(self.delay)
        setting["useQuaternion"] = str(self.useQuaternion)
        # export customized frame, is for MMDDv1 only
        # setting["customizedFrame"] = []
        # format in array:
        #   {"#frameNo": script2string(cameraKeyframe.exportSetting())}
        #   or
        #   {"#frameNo": ""}
        #   when user deleted a keyframe from org vmd
        # export frames, from MMDDv2
        if self.keyframeCustomized:
            setting["keyframes"] = []
            for ckf in self.cameraKeyFrames:
                setting["keyframes"].append(ckf.exportSetting())
        # multi target, from MMDDv2
        setting["enableMultiTarget"] = self.enableMultiTarget
        setting["switchProbability"] = self.switchProbability
        setting["switchMinFramePeriod"] = self.switchMinFramePeriod
        setting["switchMaxFramePeriod"] = self.switchMaxFramePeriod
        # export
        return setting

    def importSetting(self, setting):
        def parseBool(txt):
            return bool(eval(txt))
        # load keyframes
        filename = setting["filename"]
        kfCus = False
        if setting.has_key("keyframes"):
            keyframes = [VmdCameraKeyFrame.parseFromSetting(kfs) for kfs in setting["keyframes"]]
            kfCus = True
        elif filename != "NewCamera":
            vmdResult = VmdLib.loadVmd(filename)
            if len(vmdResult) == 1:
                raise Exception(vmdResult[0])
            keyframes = vmdResult[3]
        else:
            keyframes = []
        self.importCameraKeyframes(filename, keyframes)
        self.keyframeCustomized = kfCus
        # import general setting
        self.enable = parseBool(setting["enable"])
        self.posScale = float(setting["posScale"])
        self.dstScale = float(setting["dstScale"])
        self.syncScale = parseBool(setting["syncScale"])
        posd = eval(setting["offPos"])
        self.offPos = Vector3(posd[0], posd[1], posd[2])
        rotd = eval(setting["offRot"])
        self.offRot = Vector3(rotd[0], rotd[1], rotd[2])
        self.delay = float(setting["delay"])
        if setting.has_key("useQuaternion"):
            self.useQuaternion = parseBool(setting["useQuaternion"])
        # import customized frame, optional in MMDDv2
        if setting.has_key("customizedFrame"):
            for cfstr in setting["customizedFrame"]:
                cfraw = eval(cfstr)
                if len(cfraw.values()[0]) > 0:
                    fsetting = eval(cfraw.values()[0])
                    frame = VmdCameraKeyFrame.parseFromSetting(fsetting)
                    self.updateKeyframe(frame)
                else:
                    dfn = int(cfraw.keys()[0][1:])
                    self.deleteKeyframe(dfn)
        # multi target, from MMDDv2
        if setting.has_key("enableMultiTarget"):
            self.enableMultiTarget = setting["enableMultiTarget"]
            self.switchProbability = setting["switchProbability"]
            self.switchMinFramePeriod = setting["switchMinFramePeriod"]
            self.switchMaxFramePeriod = setting["switchMaxFramePeriod"]

    def importCameraKeyframes(self, filepath, keyframes):
        if keyframes == None or len(keyframes) == 0:
            return
        # import
        self.vmdFilePath = filepath
        self.cameraKeyFrames = keyframes
        self.keyframeCustomized = False
        self.fsIndex = 0

    def redirectCameraKeyframes(self, filepath):
        # re-direct filename and discard user data
        self.vmdFilePath = filepath
        self.keyframeCustomized = False

    def isKeyframe(self, frameNo):
        if len(self.cameraKeyFrames) == 0:
            return False
        elif frameNo < 0 or frameNo > self.cameraKeyFrames[-1].frameNo:
            return False
        else:
            for fm in self.cameraKeyFrames:
                if frameNo == fm.frameNo:
                    return True
            return False

    def updateKeyframe(self, newKeyframe):
        self.keyframeCustomized = True
        frameNo = newKeyframe.frameNo
        for i in range(len(self.cameraKeyFrames)):
            if frameNo == self.cameraKeyFrames[i].frameNo:
                # update
                self.cameraKeyFrames[i] = newKeyframe
                return
        # not a existed keyframe, create new one
        self.cameraKeyFrames.append(newKeyframe)
        self.cameraKeyFrames.sort(key=VmdCameraKeyFrame.sortByFrameNo)

    def deleteKeyframe(self, frameNo):
        for i in range(len(self.cameraKeyFrames)):
            if frameNo == self.cameraKeyFrames[i].frameNo:
                # delete
                del self.cameraKeyFrames[i]
                self.keyframeCustomized = True
                return

    def createKeyframeFromCurrentCamera(self, frameNo, curveSet):
        cpos = self.cameraData.pos
        cdst = self.cameraData.distance
        crot = self.cameraData.rotate
        #print "Current camera: pos=(%.3f, %.3f, %.3f), rot=(%.3f, %.3f, %.3f), dis=%.3f"%(self.cameraData.pos.x, self.cameraData.pos.y, self.cameraData.pos.z, self.cameraData.rotate.x, self.cameraData.rotate.y, self.cameraData.rotate.z, self.cameraData.distance.z)

        # apply inverse offset
        if self.offPos != Vector3.zero:
            cpos -= self.offPos
        if self.offRot != Vector3.zero:
            irotQ = Quaternion.Inverse(Quaternion.Euler(self.offRot))
            cpos = irotQ * cpos
            crot = (irotQ * Quaternion.Euler(crot)).eulerAngles

        # inverse scale and setup
        vf = VmdCameraKeyFrame()
        vf.frameNo = frameNo
        vf.dist = cdst.z / self.dstScale
        vf.posX = - cpos.x / self.posScale
        vf.posY = cpos.y / self.posScale
        vf.posZ = - cpos.z / self.posScale
        vf.rotX = - crot.x
        vf.rotY = 180 - crot.y
        vf.rotZ = - crot.z
        if curveSet and len(curveSet) == len(VmdCameraKeyFrame.curveSetName):
            vf.xCurve = copy.copy(curveSet[0])
            vf.yCurve = copy.copy(curveSet[1])
            vf.zCurve = copy.copy(curveSet[2])
            vf.rCurve = copy.copy(curveSet[3])
            vf.dCurve = copy.copy(curveSet[4])
            vf.vCurve = copy.copy(curveSet[5])
        vf.viewAngle = self.cameraCtrl.fieldOfView
        vf.orthographic = 0
        #print " > frame: pos=(%.3f, %.3f, %.3f), rot=(%.3f, %.3f, %.3f), dis=%.3f, fov=%.3f"%(vf.posX, vf.posY, vf.posZ, vf.rotX, vf.rotY, vf.rotZ, vf.dist, vf.viewAngle)

        return vf

    def setupMultiTarget(self, targetActors):
        try:
            self.multiTargets = []
            for actor in targetActors:
                self.multiTargets.append((actor.pos, actor.rot))
            if not len(self.multiTargets):
                raise Exception("No actor")
            self.lastSwitchFrame = 0
            self.currentTargetIndex = 0
            self.switchChanceFrame = None
        except Exception, e:
            print "Fail to setup multi targets:", e
            self.clearMultiTarget()

    def clearMultiTarget(self):
        self.multiTargets = None
        self.lastSwitchFrame = None
        self.currentTargetIndex = None
        self.switchChanceFrame = None

    def getFrame(self, frameNo):
        if len(self.cameraKeyFrames) == 0:
            return None, None

        def interpolateCameraFrame(tgtFrameNo, prevFrame, nextFrame):
            # return camera frame at target frameNo, and camera switch chance frame if detected
            def isLinear(cuv):
                return cuv[0] == cuv[1] and cuv[2] == cuv[3]
            if prevFrame.frameNo + 1 == nextFrame.frameNo:
                return prevFrame, nextFrame.frameNo
            # cubic Bezier interpolate
            tframe = float(tgtFrameNo - prevFrame.frameNo) / float(nextFrame.frameNo - prevFrame.frameNo)
            if isLinear(nextFrame.xCurve):
                tx = tframe
            else:
                tx = VmdLib.mmdCubicBezierInterpolate(nextFrame.xCurve, tframe)
            if isLinear(nextFrame.yCurve):
                ty = tframe
            else:
                ty = VmdLib.mmdCubicBezierInterpolate(nextFrame.yCurve, tframe)
            if isLinear(nextFrame.zCurve):
                tz = tframe
            else:
                tz = VmdLib.mmdCubicBezierInterpolate(nextFrame.zCurve, tframe)
            if isLinear(nextFrame.rCurve):
                tr = tframe
            else:
                tr = VmdLib.mmdCubicBezierInterpolate(nextFrame.rCurve, tframe)
            if isLinear(nextFrame.dCurve):
                td = tframe
            else:
                td = VmdLib.mmdCubicBezierInterpolate(nextFrame.dCurve, tframe)
            if isLinear(nextFrame.vCurve):
                tv = tframe
            else:
                tv = VmdLib.mmdCubicBezierInterpolate(nextFrame.vCurve, tframe)
            # interpolate and create new frame
            nf = VmdCameraKeyFrame()
            nf.frameNo = tgtFrameNo
            nf.dist = prevFrame.dist + (nextFrame.dist - prevFrame.dist) * td
            nf.posX = prevFrame.posX + (nextFrame.posX - prevFrame.posX) * tx
            nf.posY = prevFrame.posY + (nextFrame.posY - prevFrame.posY) * ty
            nf.posZ = prevFrame.posZ + (nextFrame.posZ - prevFrame.posZ) * tz
            if self.useQuaternion:
                prevQ = Quaternion.Euler(prevFrame.rotX, prevFrame.rotY, prevFrame.rotZ)
                nextQ = Quaternion.Euler(nextFrame.rotX, nextFrame.rotY, nextFrame.rotZ)
                newQ = Quaternion.Slerp(prevQ, nextQ, tr)
                newV = newQ.eulerAngles
                nf.rotX = newV.x
                nf.rotY = newV.y
                nf.rotZ = newV.z
            else:
                nf.rotX = prevFrame.rotX + (nextFrame.rotX - prevFrame.rotX) * tr
                nf.rotY = prevFrame.rotY + (nextFrame.rotY - prevFrame.rotY) * tr
                nf.rotZ = prevFrame.rotZ + (nextFrame.rotZ - prevFrame.rotZ) * tr
            nf.viewAngle = prevFrame.viewAngle + (nextFrame.viewAngle - prevFrame.viewAngle) * tv
            #nf.importCurve(nextFrame.curve)
            #print "interpolate frame[%f]: pos=(%.2f, %.2f, %.2f), rot=(%.1f, %.1f, %.1f), dis=%.2f"%(nf.frameNo, nf.posX, nf.posY, nf.posZ, nf.rotX, nf.rotY, nf.rotZ, nf.dist)
            return nf, None

        if self.delay > 0:
            frameNo -= self.delay * 30.0

        if frameNo <= self.cameraKeyFrames[0].frameNo:
            return self.cameraKeyFrames[0], None
        elif frameNo >= self.cameraKeyFrames[-1].frameNo:
            return self.cameraKeyFrames[-1], None
        elif frameNo > self.cameraKeyFrames[self.fsIndex].frameNo and frameNo < self.cameraKeyFrames[self.fsIndex+1].frameNo:
            return interpolateCameraFrame(frameNo, self.cameraKeyFrames[self.fsIndex], self.cameraKeyFrames[self.fsIndex+1])
        else:
            for i in range(len(self.cameraKeyFrames)):
                if frameNo == self.cameraKeyFrames[i].frameNo:
                    self.fsIndex = i
                    return self.cameraKeyFrames[i], frameNo+1 if frameNo+1 == self.cameraKeyFrames[i+1].frameNo else None
                elif frameNo < self.cameraKeyFrames[i+1].frameNo:
                    self.fsIndex = i
                    return interpolateCameraFrame(frameNo, self.cameraKeyFrames[i], self.cameraKeyFrames[i+1])
            raise Exception("??? Error, cannot get camera frame @ %d"%frameNo)

    def setFrame(self, frameNo):
        if not self.enable:
            return

        # get frame
        camFrame, switchChanceFrame = self.getFrame(frameNo)
        if not camFrame:
            return

        # check switch
        if self.enableMultiTarget and self.multiTargets:
            if switchChanceFrame:
                self.switchChanceFrame = switchChanceFrame
            if self.switchChanceFrame and frameNo >= self.switchChanceFrame:
                self.switchChanceFrame = None
                if len(self.multiTargets) == 1:
                    switch = False
                elif frameNo - self.lastSwitchFrame < self.switchMinFramePeriod:
                    switch = False
                elif frameNo - self.lastSwitchFrame > self.switchMaxFramePeriod:
                    switch = True
                else:
                    switch = self.switchProbability >= random_randint(0, 100)
                if switch:
                    nc = range(len(self.multiTargets))
                    nc.remove(self.currentTargetIndex)
                    self.currentTargetIndex = random_choice(nc)
                    self.lastSwitchFrame = frameNo
            offPos, offRot = self.multiTargets[self.currentTargetIndex]
        else:
            offPos, offRot = self.offPos, self.offRot
        
        # axis and scale
        cpos = Vector3(-camFrame.posX * self.posScale, camFrame.posY * self.posScale, -camFrame.posZ * self.posScale)
        cdst = Vector3(0, 0, camFrame.dist * self.dstScale)
        crot = Vector3(-camFrame.rotX, -camFrame.rotY+180, -camFrame.rotZ)
        
        # apply offset
        if offRot != Vector3.zero:
            rotQ = Quaternion.Euler(offRot)
            cpos = rotQ * cpos
            crot = (rotQ * Quaternion.Euler(crot)).eulerAngles
        if offPos != Vector3.zero:
            cpos += offPos
        
        # setup camera
        self.cameraData.pos = cpos
        self.cameraData.distance = cdst
        self.cameraData.rotate = crot
        self.cameraCtrl.fieldOfView = camFrame.viewAngle

class SoundController():
    @property
    def TotalFrameLength(self):
        return int(self.delay * 30.0) + self.maxFrameNo

    @property
    def maxFrameNo(self):
        return int(self.Length * 30.0)

    @property
    def Length(self):
        if self.loaded:
            try:
                return self.audioSource.clip.length
            except:
                self.cleanup()
                return 0
        else:
            return 0

    @property
    def isPlaying(self):
        if self.loaded:
            return self.audioSource.isPlaying
        else:
            return False

    @property
    def FileName(self):
        if len(self.wavFilePath) > 0:
            return os.path.split(self.wavFilePath)[1]
        else:
            return self.wavFilePath
            
    def __init__(self, filepath = ""):
        # setting
        self.wavFilePath = ""
        self.enable = True
        self.delay = 0.0

        # runtime
        self.loaded = False
        self.audioGO = GameObject()
        self.audioGO.transform.position = Vector3.zero
        self.audioGO.transform.rotation = Quaternion.identity

        from Manager import Sound
        if hasattr(Sound, "Instance"):
            self.audioSource = Sound.Instance.Create(Sound.Type.ENV, True)
        else:
            self.audioSource = Sound.Create(Sound.Type.ENV, True)
        self.audioSource.gameObject.transform.parent = self.audioGO.transform
        self.audioSource.loop = False
        self.audioSource.pitch = 1.0
        self.audioSource.spatialize = False
        self.audioSource.spatialBlend = 0.0
        self.audioSource.playOnAwake = False

        # load file
        if len(filepath) and os.path.isfile(filepath):
            self.loadWavFile(filepath)

    def exportSetting(self):
        setting = {}
        # export general setting
        setting["filename"] = self.wavFilePath
        setting["enable"] = str(self.enable)
        setting["delay"] = str(self.delay)
        return setting

    def importSetting(self, setting):
        def parseBool(txt):
            return bool(eval(txt))
        # load vmd
        filename = setting["filename"]
        if len(filename) > 0:
            self.loadWavFile(filename)
        # import general setting
        self.enable = parseBool(setting["enable"])
        self.delay = float(setting["delay"])

    def loadWavFile(self, filepath):
        from UnityEngine import WWW

        if not os.path.isfile(filepath):
            return "File '%s' not found"%filepath

        try:
            from System import Uri
            #tpath = "file:///" + filepath
            tpath = Uri(os.path.abspath(filepath)).AbsoluteUri
            w = WWW(tpath)
            while not w.isDone: pass
            if w.error:
                return str(w.error)
            
            if hasattr(w, "GetAudioClip"):
                self.audioSource.clip = w.GetAudioClip()
            else:
                from UnityEngine import WWWAudioExtensions
                self.audioSource.clip = WWWAudioExtensions.GetAudioClip(w)

            # check clip
            if not self.audioSource.clip:
                return "Fail to get audio clip from '%s'."%filepath
            # load ok
            self.wavFilePath = filepath
            self.loaded = True
            return None

        except Exception, e:
            traceback.print_exc()
            return str(e)

    def setTime(self, tgtTime):
        if self.enable and self.loaded and not self.audioSource.isPlaying:
            syncTime = tgtTime - self.delay
            if syncTime < 0: syncTime = 0
            if syncTime > self.audioSource.clip.length: syncTime = self.audioSource.clip.length
            self.audioSource.time = syncTime

    def setFrame(self, frameNo):
        t = frameNo / 30.0
        self.setTime(t)

    def setSpeed(self, speed):
        if self.enable and self.loaded:
            self.audioSource.pitch = speed

    def setPlay(self, timeOffset):
        if timeOffset >= self.delay:
            self.play()
            return True
        else:
            return False

    def play(self):
        if self.enable and self.loaded and not self.audioSource.isPlaying:
            self.audioSource.Play()

    def pause(self):
        if self.enable and self.loaded and self.audioSource.isPlaying:
            self.audioSource.Pause()

    def stop(self):
        if self.enable and self.loaded and self.audioSource.isPlaying:
            self.audioSource.Stop()

    def cleanup(self):
        self.stop()
        self.loaded = False
        self.wavFilePath = ""

        from UnityEngine import Object
        try:
            Object.Destroy(self.audioSource)
            self.audioSource = None
            Object.Destroy(self.audioGO)
            self.audioGO = None
        except:
            pass    # nothing to do

class ClipController():
    @property
    def TotalFrameLength(self):
        return int(self.delay * 30.0) + self.linkFrame + self.maxFrameNo

    @property
    def maxFrameNo(self):
        if self.loopCount == -1:
            return 0
        else:
            return self.OneLoopFrameNo * (self.loopCount + 1)

    @property
    def OneLoopFrameNo(self):
        if self.clip:
            of = float(self.clip.frameLength) / float(self.clip.frameRate) * 30.0 / self.playSpeed
            return int(of)
        else:
            return 0

    @property
    def Lasts(self):
        if self.loopCount == -1:
            return "infinite"
        else:
            return "%d frames"%self.maxFrameNo

    @property
    def Id(self):
        return "%s@%d"%(self.clipName, self.linkFrame)

    def __init__(self, clipName = "", linkFrame = 0):
        # setting
        self.clipName = clipName
        self.linkFrame = linkFrame
        self.loopCount = 0
        self.playSpeed = 1.0
        self.enable = True
        self.delay = 0.0

        # runtime
        self.clip = None

    def bindClip(self, game):
        if game and hasattr(game, "gdata") and hasattr(game.gdata, "kfaManagedClips") and game.gdata.kfaManagedClips.has_key(self.clipName):
            self.clip = game.gdata.kfaManagedClips[self.clipName]
        else:
            self.clip = None

    def exportSetting(self):
        def saveto(dic, key, setvalue):
            if isinstance(setvalue, bool):
                dic[key] = str(setvalue)
            elif isinstance(setvalue, int):
                dic[key] = str(setvalue)
            elif isinstance(setvalue, float):
                dic[key] = str(setvalue)
            elif isinstance(setvalue, str):
                dic[key] = setvalue
            elif isinstance(setvalue, dict) or isinstance(setvalue, list) or isinstance(setvalue, tuple):
                dic[key] = script2string(setvalue)
            else:
                raise Exception("Unsupported data type '%s'"%type(setvalue))
        setting = {}
        # export general setting
        saveto(setting, "clipname", self.clipName)
        saveto(setting, "linkframe", self.linkFrame)
        saveto(setting, "loopcount", self.loopCount)
        saveto(setting, "playspeed", self.playSpeed)
        saveto(setting, "enable", self.enable)
        saveto(setting, "delay", self.delay)
        return setting

    def importSetting(self, setting, game = None):
        def tryload(dic, key, defvalue):
            try:
                if dic.has_key(key):
                    if isinstance(defvalue, bool):
                        return dic[key].lower() == "true" or dic[key] == "1"
                    elif isinstance(defvalue, int):
                        return int(dic[key])
                    elif isinstance(defvalue, float):
                        return float(dic[key])
                    elif isinstance(defvalue, str):
                        return dic[key]
                    elif isinstance(defvalue, dict) or isinstance(defvalue, list) or isinstance(defvalue, tuple):
                        return eval(dic[key])
                    else:
                        raise Exception("Unsupported data type '%s'"%type(defvalue))
                else:
                    raise Exception("key [%s] not found in source dict."%key)
            except Exception, e:
                print "Fail to load [%s]: %s"%(key, str(e)) 
                return defvalue
        # import general setting
        self.clipName = tryload(setting, "clipname", self.clipName)
        self.linkFrame = tryload(setting, "linkframe", self.linkFrame)
        self.loopCount = tryload(setting, "loopcount", self.loopCount)
        self.playSpeed = tryload(setting, "playspeed", self.playSpeed)
        self.enable = tryload(setting, "enable", self.enable)
        self.delay = tryload(setting, "delay", self.delay)
        # do bind
        self.bindClip(game)

    def toClipFrame(self, frameNo):
        if self.delay > 0:
            frameNo -= self.delay * 30.0
        if frameNo < self.linkFrame:
            return None
        frameNo -= self.linkFrame
        if self.loopCount != -1 and frameNo > self.maxFrameNo:
            return None
        of = frameNo % (self.OneLoopFrameNo + 1)
        cf = of * self.playSpeed * float(self.clip.frameRate) / 30.0
        if cf > self.clip.frameLength:
            cf = self.clip.frameLength
        return cf
    
    def toMmddFrame(self, mmddStartFrameNo):
        # check range
        if mmddStartFrameNo < self.linkFrame:
            return None # not start yet
        if self.loopCount != -1 and mmddStartFrameNo > self.linkFrame + self.maxFrameNo:
            return None # out of range
        # get loop count and base frame of current loop
        lpn = int((mmddStartFrameNo - self.linkFrame) / (self.OneLoopFrameNo + 1))
        baseFrame = self.linkFrame + lpn * (self.OneLoopFrameNo + 1)
        of = self.clip.curFrame * 30.0 / float(self.clip.frameRate) / self.playSpeed
        mf = of + baseFrame
        if self.delay > 0:
            mf += self.delay * 30.0
        return mf

    def setFrame(self, frameNo):
        if self.enable and self.clip:
            if self.delay > 0:
                frameNo -= self.delay * 30.0
            if frameNo < self.linkFrame:
                return
            frameNo -= self.linkFrame
            if self.loopCount != -1 and frameNo > self.maxFrameNo:
                return
            of = frameNo % (self.OneLoopFrameNo + 1)
            cf = of * self.playSpeed * float(self.clip.frameRate) / 30.0
            if cf > self.clip.frameLength:
                cf = self.clip.frameLength
            if self.clip.curFrame != cf:
                self.clip.curFrame = cf
                self.clip.applyFrames()

class BindController():
    @staticmethod
    def findIdAndPath(ctrlobj, transform):
        from Studio import Studio
        studio = Studio.Instance
        # find ID
        tgtId = None
        for oid in studio.dicObjectCtrl.Keys:
            if studio.dicObjectCtrl[oid] == ctrlobj:
                tgtId = oid
                break
        if tgtId == None:
            raise Exception("Object not found in dicObjectCtrl")
        # find path
        tgtPath = [transform.name]
        while transform != ctrlobj.guideObject.transformTarget:
            if transform.parent:
                transform = transform.parent
                tgtPath.append(transform.name)
            else:
                raise Exception("Fail to get path from transform to objctrl!?")
        tgtPath.reverse()
        return tgtId, tgtPath

    @staticmethod
    def getObjFromId(tid):
        from Studio import Studio
        studio = Studio.Instance
        if studio.dicObjectCtrl.ContainsKey(tid):
            objctrl = studio.dicObjectCtrl[tid]
            from vngameengine import HSNeoOCI
            tgtObj = HSNeoOCI.create_from(objctrl)
            return tgtObj
        else:
            print "fail to restore object id = %d"%tid
            return None

    @staticmethod
    def findTransByPath(objctrl, path):
        transform = objctrl.guideObject.transformTarget
        if transform.name != path[0]:
            print "Warning: get a wrong base object! Maybe cause by auto-renamed object? obj = %s, path base object = %s"%(transform.name, path[0])
            #return None
        for pname in path[1:]:
            np = None
            for cp in transform:
                if cp.name == pname:
                    np = cp
                    break
            if np:
                transform = np
            else:
                print "cant found %s under %s"%(pname, transform.name)
                return None
        return transform

    @property
    def Valid(self):
        if self.bindMode.startswith("bbs"):
            return self.parentTransform != None and self.parentTransform2nd != None and self.childTransform != None
        else:
            return self.parentTransform != None and self.childTransform != None

    @property
    def ParentDesp(self):
        if self.parent and self.parentTransform:
            return self.parentTransform.gameObject.name + "(" + self.parent.text_name + ")"
        else:
            return ""

    @property
    def ParentDesp2nd(self):
        if self.parent2nd and self.parentTransform2nd:
            return self.parentTransform2nd.gameObject.name + "(" + self.parent2nd.text_name + ")"
        else:
            return ""

    @property
    def ChildDesp(self):
        if self.child and self.childTransform:
            return self.childTransform.gameObject.name + "(" + self.child.text_name + ")"
        else:
            return ""

    def __init__(self):
        # setting
        self.enable = True
        self.parent = None
        self.parentTransform = None
        self.parentId = -1
        self.parentPath = []
        self.child = None
        self.childTransform = None
        self.childId = -1
        self.childPath = []
        self.positionBind = True
        self.rotationBind = True
        self.scaleBind = False
        self.positionOffset = Vector3.zero
        self.rotationOffset = Quaternion.identity
        self.scaleOffset = Vector3(1, 1, 1)
        self.orgChildPosition = Vector3.zero
        self.orgChildRotation = Quaternion.identity
        self.orgChildScale = Vector3(1, 1, 1)
        self.startFrame = 0
        self.endFrame = 0
        self.alias = ""
        # version 2
        self.bindMode = "local"
        self.parent2nd = None
        self.parentTransform2nd = None
        self.parent2ndId = -1
        self.parent2ndPath = []
        self.bbsModeScale = 0.5
        self.positionBindAxis = None
        self.positionBindFollow = 1.0
        self.rotationBindAxis = None
        self.rotationBindFollow = 1.0

    def copyFrom(self, src):
        self.enable = src.enable
        self.parent = src.parent
        self.parentTransform = src.parentTransform
        self.parentId = src.parentId
        self.parentPath = copy.deepcopy(src.parentPath)
        self.child = src.child
        self.childTransform = src.childTransform
        self.childId = src.childId
        self.childPath = copy.deepcopy(src.childPath)
        self.positionBind = src.positionBind
        self.rotationBind = src.rotationBind
        self.scaleBind = src.scaleBind
        self.positionOffset = src.positionOffset
        self.rotationOffset = src.rotationOffset
        self.scaleOffset = src.scaleOffset
        self.orgChildPosition = src.orgChildPosition
        self.orgChildRotation = src.orgChildRotation
        self.orgChildScale = src.orgChildScale
        self.startFrame = src.startFrame
        self.endFrame = src.endFrame
        self.alias = src.alias
        # version 2
        self.bindMode = src.bindMode
        self.parent2nd = src.parent2nd
        self.parentTransform2nd = src.parentTransform2nd
        self.parent2ndId = src.parent2ndId
        self.parent2ndPath = copy.deepcopy(src.parent2ndPath)
        self.bbsModeScale = src.bbsModeScale
        self.positionBindAxis = src.positionBindAxis
        self.positionBindFollow = src.positionBindFollow
        self.rotationBindAxis = src.rotationBindAxis
        self.rotationBindFollow = src.rotationBindFollow

    def exportSetting(self):
        def saveto(dic, key, setvalue):
            if setvalue == None:
                dic[key] = "None"
            elif isinstance(setvalue, bool):
                dic[key] = str(setvalue)
            elif isinstance(setvalue, int):
                dic[key] = str(setvalue)
            elif isinstance(setvalue, float):
                dic[key] = str(setvalue)
            elif isinstance(setvalue, str):
                dic[key] = setvalue
            elif isinstance(setvalue, Vector3):
                dic[key] = "(%.3f, %.3f, %.3f)"%(setvalue.x, setvalue.y, setvalue.z)
            elif isinstance(setvalue, Quaternion):
                dic[key] = "(%.3f, %.3f, %.3f, %.3f)"%(setvalue.x, setvalue.y, setvalue.z, setvalue.w)
            elif isinstance(setvalue, dict) or isinstance(setvalue, list) or isinstance(setvalue, tuple):
                dic[key] = script2string(setvalue)
            else:
                raise Exception("Unsupported data type '%s'"%type(setvalue))
        setting = {}
        # export setting
        saveto(setting, "enable", self.enable)
        saveto(setting, "parentId", self.parentId)
        saveto(setting, "parentPath", self.parentPath)
        saveto(setting, "childId", self.childId)
        saveto(setting, "childPath", self.childPath)
        saveto(setting, "positionBind", self.positionBind)
        saveto(setting, "rotationBind", self.rotationBind)
        saveto(setting, "scaleBind", self.scaleBind)
        saveto(setting, "positionOffset", self.positionOffset)
        saveto(setting, "rotationOffset", self.rotationOffset)
        saveto(setting, "scaleOffset", self.scaleOffset)
        saveto(setting, "orgChildPosition", self.orgChildPosition)
        saveto(setting, "orgChildRotation", self.orgChildRotation)
        saveto(setting, "orgChildScale", self.orgChildScale)
        saveto(setting, "startFrame", self.startFrame)
        saveto(setting, "endFrame", self.endFrame)
        saveto(setting, "alias", self.alias)
        # version 2
        saveto(setting, "bindMode", self.bindMode)
        if self.bindMode.startswith("bbs"):
            saveto(setting, "parent2ndId", self.parent2ndId)
            saveto(setting, "parent2ndPath", self.parent2ndPath)
            saveto(setting, "bbsModeScale", self.bbsModeScale)
        saveto(setting, "positionBindAxis", str(self.positionBindAxis))
        saveto(setting, "positionBindFollow", self.positionBindFollow)
        saveto(setting, "rotationBindAxis", str(self.rotationBindAxis))
        saveto(setting, "rotationBindFollow", self.rotationBindFollow)
        return setting

    def importSetting(self, setting):
        def tryload(dic, key, defvalue):
            try:
                if dic.has_key(key):
                    if isinstance(defvalue, bool):
                        return dic[key].lower() == "true" or dic[key] == "1"
                    elif isinstance(defvalue, int):
                        return int(dic[key])
                    elif isinstance(defvalue, float):
                        return float(dic[key])
                    elif isinstance(defvalue, str):
                        return dic[key]
                    elif isinstance(defvalue, Vector3):
                        v3 = eval(dic[key])
                        return Vector3(v3[0], v3[1], v3[2])
                    elif isinstance(defvalue, Quaternion):
                        q4 = eval(dic[key])
                        return Quaternion(q4[0], q4[1], q4[2], q4[3])
                    elif isinstance(defvalue, dict) or isinstance(defvalue, list) or isinstance(defvalue, tuple):
                        return eval(dic[key])
                    else:
                        raise Exception("Unsupported data type '%s'"%type(defvalue))
                else:
                    raise Exception("key [%s] not found in source dict."%key)
            except Exception, e:
                print "Fail to load [%s]: %s"%(key, str(e)) 
                return defvalue
        # import setting
        self.enable = tryload(setting, "enable", self.enable)
        self.parentId = tryload(setting, "parentId", self.parentId)
        self.parentPath = tryload(setting, "parentPath", self.parentPath)
        self.childId = tryload(setting, "childId", self.childId)
        self.childPath = tryload(setting, "childPath", self.childPath)
        self.positionBind = tryload(setting, "positionBind", self.positionBind)
        self.rotationBind = tryload(setting, "rotationBind", self.rotationBind)
        self.scaleBind = tryload(setting, "scaleBind", self.scaleBind)
        self.positionOffset = tryload(setting, "positionOffset", self.positionOffset)
        self.rotationOffset = tryload(setting, "rotationOffset", self.rotationOffset)
        self.scaleOffset = tryload(setting, "scaleOffset", self.scaleOffset)
        self.orgChildPosition = tryload(setting, "orgChildPosition", self.orgChildPosition)
        self.orgChildRotation = tryload(setting, "orgChildRotation", self.orgChildRotation)
        self.orgChildScale = tryload(setting, "orgChildScale", self.orgChildScale)
        self.startFrame = tryload(setting, "startFrame", self.startFrame)
        self.endFrame = tryload(setting, "endFrame", self.endFrame)
        self.alias = tryload(setting, "alias", self.alias)
        # find objctrl and transform
        self.parent = BindController.getObjFromId(self.parentId)
        if self.parent:
            self.parentTransform = BindController.findTransByPath(self.parent.objctrl, self.parentPath)
        else:
            self.parentTransform = None
        self.child = BindController.getObjFromId(self.childId)
        if self.child:
            self.childTransform = BindController.findTransByPath(self.child.objctrl, self.childPath)
        else:
            self.childTransform = None
        self.bind()
        # version 2
        self.bindMode = tryload(setting, "bindMode", self.bindMode)
        if self.bindMode.startswith("bbs"):
            self.parent2ndId = tryload(setting, "parent2ndId", self.parent2ndId)
            self.parent2ndPath = tryload(setting, "parent2ndPath", self.parent2ndPath)
            self.parent2nd = BindController.getObjFromId(self.parent2ndId)
            if self.parent2nd:
                self.parentTransform2nd = BindController.findTransByPath(self.parent2nd.objctrl, self.parent2ndPath)
            else:
                self.parentTransform2nd = None
            self.bbsModeScale = tryload(setting, "bbsModeScale", self.bbsModeScale)
        self.positionBindAxis = eval(tryload(setting, "positionBindAxis", "None"))
        self.positionBindFollow = tryload(setting, "positionBindFollow", self.positionBindFollow)
        self.rotationBindAxis = eval(tryload(setting, "rotationBindAxis", "None"))
        self.rotationBindFollow = tryload(setting, "rotationBindFollow", self.rotationBindFollow)

    def bind(self):
        if self.Valid:
            self.orgChildPosition = self.childTransform.localPosition
            self.orgChildRotation = self.childTransform.localRotation
            self.orgChildScale = self.childTransform.localScale

    def setParent(self, tgtObj, transform):
        self.parent = tgtObj
        self.parentTransform = transform
        self.parentId, self.parentPath = BindController.findIdAndPath(tgtObj.objctrl, transform)

    def setParent2nd(self, tgtObj, transform):
        self.parent2nd = tgtObj
        self.parentTransform2nd = transform
        self.parent2ndId, self.parent2ndPath = BindController.findIdAndPath(tgtObj.objctrl, transform)

    def setChild(self, tgtObj, transform):
        self.child = tgtObj
        self.childTransform = transform
        self.childId, self.childPath = BindController.findIdAndPath(tgtObj.objctrl, transform)

    def verify(self):
        if not self.Valid:
            print "BindController %s becomes invalid, try to fix it"%str(self)
            try:
                if self.parent:
                    self.parentTransform = BindController.findTransByPath(self.parent.objctrl, self.parentPath)
                if self.child:
                    self.childTransform = BindController.findTransByPath(self.child.objctrl, self.childPath)
                if self.parent2nd:
                    self.parentTransform2nd = BindController.findTransByPath(self.parent2nd.objctrl, self.parent2ndPath)
            except:
                traceback.print_exc()
            return self.Valid
        else:
            return True

    def cleanup(self):
        if self.Valid:
            try:
                self.childTransform.localPosition = self.orgChildPosition
                self.childTransform.localRotation = self.orgChildRotation
                self.childTransform.localScale = self.orgChildScale
            except:
                traceback.print_exc()
                pass    # no error for cleanup

    def isWorking(self, frameNo):
        return self.enable and self.startFrame <= frameNo and (frameNo < self.endFrame or self.endFrame == -1) and self.Valid

    def setFrame(self, frameNo):
        if self.isWorking(frameNo):
            if self.positionBind:
                if self.bindMode == "local":
                    tgtPosition = self.parentTransform.TransformPoint(self.positionOffset)
                elif self.bindMode == "global":
                    tgtPosition = self.parentTransform.position + self.positionOffset
                elif self.bindMode == "bbsLocal":
                    tgtPosition = Vector3.LerpUnclamped(self.parentTransform.TransformPoint(self.positionOffset), self.parentTransform2nd.TransformPoint(self.positionOffset), self.bbsModeScale)
                elif self.bindMode == "bbsGlobal":
                    tgtPosition = Vector3.LerpUnclamped(self.parentTransform.position, self.parentTransform2nd.position, self.bbsModeScale) + self.positionOffset
                else:
                    raise Exception("illegal position bind offset mode")
                if self.positionBindAxis:
                    tgtPosition.Set(tgtPosition.x if "x" in self.positionBindAxis else self.childTransform.position.x, \
                                    tgtPosition.y if "y" in self.positionBindAxis else self.childTransform.position.y, \
                                    tgtPosition.z if "z" in self.positionBindAxis else self.childTransform.position.z)
                if self.positionBindFollow != 1.0:
                    tgtPosition = Vector3.Lerp(self.childTransform.position, tgtPosition, self.positionBindFollow)
                self.childTransform.position = tgtPosition
            if self.rotationBind:
                if self.bindMode in {"local", "global"}:
                    tgtRotation = self.parentTransform.rotation * self.rotationOffset
                elif self.bindMode in {"bbsLocal", "bbsGlobal"}:
                    tgtRotation = Quaternion.SlerpUnclamped(self.parentTransform.rotation, self.parentTransform2nd.rotation, self.bbsModeScale) * self.rotationOffset
                else:
                    raise Exception("illegal position bind offset mode")
                if self.rotationBindAxis:
                    te = tgtRotation.eulerAngles
                    tgtRotation = Quaternion.Euler(te.x if "x" in self.rotationBindAxis else self.childTransform.eulerAngles.x, \
                                                   te.y if "y" in self.rotationBindAxis else self.childTransform.eulerAngles.y, \
                                                   te.z if "z" in self.rotationBindAxis else self.childTransform.eulerAngles.z)
                if self.rotationBindFollow != 1.0:
                    tgtRotation = Quaternion.Slerp(self.childTransform.rotation, tgtRotation, self.rotationBindFollow)
                self.childTransform.rotation = tgtRotation
            if self.scaleBind:
                self.childTransform.localScale = Vector3(self.parentTransform.lossyScale.x * self.scaleOffset.x / self.childTransform.parent.lossyScale.x, self.parentTransform.lossyScale.y * self.scaleOffset.y / self.childTransform.parent.lossyScale.y, self.parentTransform.lossyScale.z * self.scaleOffset.z / self.childTransform.parent.lossyScale.z)

class ParentController():
    @property
    def Valid(self):
        if self.childTransform == None:
            return False
        for i in range(len(self.keyframes)):
            if self.parentIds[i] != None and self.parentTransforms[i] == None:
                return False
        return True

    @property
    def ChildDesp(self):
        if self.Valid:
            return self.childTransform.name + "(" + self.childCharName + ")"
        else:
            return ""

    @property
    def Key(self):
        if self.Valid:
            return self.childId + "#" + self.childTransform.name
        else:
            return None

    def __init__(self):
        # setting
        self.enable = True
        self.childId = ""
        self.childBone = ""
        self.keyframes = []
        self.parentIds = []
        self.parentBones = []
        self.localPositions = []
        self.localRotations = []
        self.alias = ""
        # runtime
        self.childCharName = None
        self.childTransform = None
        self.parentCharNames = []
        self.parentTransforms = []
        self.orgChildPosition = Vector3.zero
        self.orgChildRotation = Quaternion.identity
        self.orgChildParent = None
        self.childBoneAdjust = None
        self.orgChildBasePos = Vector3.zero
        self.orgChildBaseRot = Quaternion.identity
        self.fsIndex = 0
        self.currentExtParenIndex = None

    def copyFrom(self, src):
        self.enable = src.enable
        self.childId = src.childId
        self.childBone = src.childBone
        self.keyframes = copy.copy(src.keyframes)
        self.parentIds = copy.copy(src.parentIds)
        self.parentBones = copy.copy(src.parentBones)
        self.localPositions = copy.copy(src.localPositions)
        self.localRotations = copy.copy(src.localRotations)
        self.alias = src.alias
        # runtime
        self.childCharName = src.childCharName
        self.childTransform = src.childTransform
        self.parentCharNames = copy.copy(src.parentCharNames)
        self.parentTransforms = copy.copy(src.parentTransforms)
        self.orgChildPosition = src.orgChildPosition
        self.orgChildRotation = src.orgChildRotation
        self.orgChildParent = src.orgChildParent
        self.childBoneAdjust = src.childBoneAdjust
        self.orgChildBasePos = src.orgChildBasePos
        self.orgChildBaseRot = src.orgChildBaseRot
        self.fsIndex = src.fsIndex
        self.currentExtParenIndex = src.currentExtParenIndex

    def exportSetting(self):
        def saveto(dic, key, setvalue):
            if setvalue == None:
                dic[key] = "None"
            elif isinstance(setvalue, bool):
                dic[key] = str(setvalue)
            elif isinstance(setvalue, int):
                dic[key] = str(setvalue)
            elif isinstance(setvalue, float):
                dic[key] = str(setvalue)
            elif isinstance(setvalue, str):
                dic[key] = setvalue
            elif isinstance(setvalue, Vector3):
                dic[key] = "(%.3f, %.3f, %.3f)"%(setvalue.x, setvalue.y, setvalue.z)
            elif isinstance(setvalue, Quaternion):
                dic[key] = "(%.3f, %.3f, %.3f, %.3f)"%(setvalue.x, setvalue.y, setvalue.z, setvalue.w)
            elif isinstance(setvalue, dict) or isinstance(setvalue, list) or isinstance(setvalue, tuple):
                dic[key] = script2string(setvalue)
            else:
                raise Exception("Unsupported data type '%s'"%type(setvalue))
        # export setting
        setting = {}
        saveto(setting, "enable", self.enable)
        saveto(setting, "childId", self.childId)
        saveto(setting, "childBone", self.childBone)
        kfs = []
        for idx in range(len(self.keyframes)):
            if self.localPositions[idx] and self.localRotations[idx]:
                lp = (self.localPositions[idx].x, self.localPositions[idx].y, self.localPositions[idx].z)
                lr = (self.localRotations[idx].x, self.localRotations[idx].y, self.localRotations[idx].z, self.localRotations[idx].w)
                fInfo = (self.keyframes[idx], self.parentIds[idx], self.parentBones[idx], lp, lr)
            else:
                fInfo = (self.keyframes[idx], self.parentIds[idx], self.parentBones[idx], None, None)
            kfs.append(fInfo)
        setting["keyframes"] = kfs #script2string(kfs)
        saveto(setting, "alias", self.alias)
        return setting

    def importSetting(self, setting):
        def tryload(dic, key, defvalue):
            try:
                if dic.has_key(key):
                    if isinstance(defvalue, bool):
                        return dic[key].lower() == "true" or dic[key] == "1"
                    elif isinstance(defvalue, int):
                        return int(dic[key])
                    elif isinstance(defvalue, float):
                        return float(dic[key])
                    elif isinstance(defvalue, str):
                        return dic[key]
                    elif isinstance(defvalue, Vector3):
                        v3 = eval(dic[key])
                        return Vector3(v3[0], v3[1], v3[2])
                    elif isinstance(defvalue, Quaternion):
                        q4 = eval(dic[key])
                        return Quaternion(q4[0], q4[1], q4[2], q4[3])
                    elif isinstance(defvalue, dict) or isinstance(defvalue, list) or isinstance(defvalue, tuple):
                        return eval(dic[key])
                    else:
                        raise Exception("Unsupported data type '%s'"%type(defvalue))
                else:
                    raise Exception("key [%s] not found in source dict."%key)
            except Exception, e:
                print "Fail to load [%s]: %s"%(key, str(e)) 
                return defvalue
        # import setting
        self.enable = tryload(setting, "enable", self.enable)
        self.childId = tryload(setting, "childId", self.childId)
        self.childBone = tryload(setting, "childBone", self.childBone)
        for kfstr in setting["keyframes"]:
            if isinstance(kfstr, str):
                kfsetting = eval(kfstr)
            else:
                kfsetting = kfstr
            if len(kfsetting) == 5:
                keyframe, parentId, parentBone, localPosRaw, localRotRaw = kfsetting
            else:
                keyframe, parentId, parentBone = kfsetting
                localPosRaw = None
                localRotRaw = None
            self.keyframes.append(keyframe)
            self.parentIds.append(parentId)
            self.parentBones.append(parentBone)
            if localPosRaw:
                self.localPositions.append(Vector3(localPosRaw[0], localPosRaw[1], localPosRaw[2]))
            else:
                self.localPositions.append(None)
            if localRotRaw:
                self.localRotations.append(Quaternion(localRotRaw[0], localRotRaw[1], localRotRaw[2], localRotRaw[3]))
            else:
                self.localRotations.append(None)
            self.parentCharNames.append(None)
            self.parentTransforms.append(None)
        self.alias = tryload(setting, "alias", self.alias)
        
    def solveMotionControl(self, game, motionCtrlDict):
        def getActorProp(id):
            res = game.scenef_get_actor(id)
            if res: return res
            return game.scenef_get_propf(id)
        def getTranFromPath(objctrl, bonePath):
            path = bonePath.split("/")
            if len(path) == 0:
                print "get a empty path!?", bonePath
                return None
            transform = objctrl.guideObject.transformTarget
            if transform.name != path[0]:
                print "Warning: get a wrong base object! Maybe cause by auto-renamed object? obj = %s, path base object = %s"%(transform.name, path[0])
                #return None
            for pname in path[1:]:
                np = None
                for cp in transform:
                    if cp.name == pname:
                        np = cp
                        break
                if np:
                    transform = np
                else:
                    print "cant found %s under %s"%(pname, transform.name)
                    return None
            return transform
        def getTran(tid, objctrl, bonePath):
            # try old version
            if tid in motionCtrlDict and bonePath.find("/") == -1:
                vmdBone = motionCtrlDict[tid].getVmdBone(bonePath)
                if vmdBone:
                    return vmdBone.transform
            # solve as new version
            return getTranFromPath(objctrl, bonePath)
        # solve child
        childObj = getActorProp(self.childId)
        if childObj:
            boneTran = getTran(self.childId, childObj.objctrl, self.childBone)
            if boneTran:
                motionCtrl = None if self.childId not in motionCtrlDict else motionCtrlDict[self.childId]
                self.setChild(self.childId, childObj, boneTran, motionCtrl)
            else:
                raise Exception("ParentController load error: Unable to solve child bone from id='%s', bone path='%s'"%(self.childId, self.childBone))
        else:
            raise Exception("ParentController load error: Child id='%s' not tagged"%self.childId)
        # solve keyframes
        for i in range(len(self.keyframes)):
            tgtFrm = self.keyframes[i]
            tgtId = self.parentIds[i]
            tgtBone = self.parentBones[i]
            if tgtId:
                tgtObj = getActorProp(tgtId)
                if tgtObj:
                    tgtBoneTran = getTran(tgtId, tgtObj.objctrl, tgtBone)
                    if tgtBoneTran:
                        self.parentCharNames[i] = tgtObj.text_name
                        self.parentTransforms[i] = tgtBoneTran
                    else:
                        raise Exception("ParentController load error: Unable to solve keyframe #%d's parent bone from id='%s', bone path='%s'"%(i, tgtId, tgtBone))
                else:
                    raise Exception("ParentController load error: Keyframe #%d's parent id='%s' not tagged"%(i, tgtId))
            else:
                self.parentCharNames[i] = ""
                self.parentTransforms[i] = None
        # done
        return self.Valid

    @staticmethod
    def parseFromSetting(setting, game, motionCtrlDict):
        pc = ParentController()
        pc.importSetting(setting)
        pc.solveMotionControl(game, motionCtrlDict)
        return pc

    @staticmethod
    def findPath(ctrlobj, transform):
        # find path
        tgtPath = [transform.name]
        while transform != ctrlobj.guideObject.transformTarget:
            if transform.parent:
                transform = transform.parent
                tgtPath.append(transform.name)
            else:
                raise Exception("Fail to get path from transform to objctrl!?")
        tgtPath.reverse()
        return "/".join(tgtPath)

    def setChild(self, tgtId, tgtActorProp, tgtBoneTran, tgtMotionCtrl):
        if tgtId and tgtActorProp and tgtBoneTran:
            self.childId = tgtId
            self.childBone = ParentController.findPath(tgtActorProp.objctrl, tgtBoneTran)

            self.childCharName = tgtActorProp.text_name
            self.childTransform = tgtBoneTran
            self.orgChildParent = self.childTransform.parent
            self.orgChildPosition = self.childTransform.localPosition
            self.orgChildRotation = self.childTransform.localRotation

            if tgtMotionCtrl:
                self.childBoneAdjust = tgtMotionCtrl.boneAdjustMap[tgtBoneTran.name]
                self.orgChildBasePos = self.childBoneAdjust.basePosition
                self.orgChildBaseRot = self.childBoneAdjust.rotAdjustment
            else:
                self.childBoneAdjust = None
                self.orgChildBasePos = None
                self.orgChildBaseRot = None
        else:
            self.childId = ""
            self.childBone = ""
            raise Exception("Fail to set child for parent controller!")

    def clearKeyframes(self):
        self.keyframes = []
        self.parentIds = []
        self.parentBones = []
        self.parentCharNames = []
        self.parentTransforms = []
        self.localPositions = []
        self.localRotations = []
    
    def addKeyframe(self, frameNo, tgtId, tgtActorProp, tgtBoneTran):
        if tgtId and tgtActorProp and tgtBoneTran:
            tgtBonePath = ParentController.findPath(tgtActorProp.objctrl, tgtBoneTran)
            tgtCharName = tgtActorProp.text_name
        else:
            tgtBonePath = ""
            tgtCharName = ""
        if frameNo in self.keyframes:
            idx = self.keyframes.index(frameNo)
            self.parentIds[idx] = tgtId
            self.parentBones[idx] = tgtBonePath
            self.parentCharNames[idx] = tgtCharName
            self.parentTransforms[idx] = tgtBoneTran
            self.localPositions[idx] = None
            self.localRotations[idx] = None
        else:
            idx = 0
            while idx < len(self.keyframes) and self.keyframes[idx] < frameNo:
                idx += 1
            self.keyframes.insert(idx, frameNo)
            self.parentIds.insert(idx, tgtId)
            self.parentBones.insert(idx, tgtBonePath)
            self.parentCharNames.insert(idx, tgtCharName)
            self.parentTransforms.insert(idx, tgtBoneTran)
            self.localPositions.insert(idx, None)
            self.localRotations.insert(idx, None)
        return idx

    def deleteKeyframeAt(self, index):
        if 0 <= index < len(self.keyframes):
            self.keyframes.pop(index)
            self.parentIds.pop(index)
            self.parentBones.pop(index)
            self.parentCharNames.pop(index)
            self.parentTransforms.pop(index)

    def deleteKeyframe(self, frameNo):
        if frameNo in self.keyframes:
            idx = self.keyframes.index(frameNo)
            self.deleteKeyframeAt(idx)

    def setExternalParent(self, pIndex):
        if self.currentExtParenIndex != pIndex:
            if pIndex != None:
                newParent = self.parentTransforms[pIndex]
                if newParent:
                    self.childTransform.parent = newParent
                    if self.childBoneAdjust:
                        self.childBoneAdjust.basePosition = Vector3.zero
                        self.childBoneAdjust.SetRotAdjustment(Vector3.zero)
                else:
                    self.childTransform.parent = self.orgChildParent
                    if self.childBoneAdjust:
                        self.childBoneAdjust.basePosition = self.orgChildBasePos
                        self.childBoneAdjust.rotAdjustment = self.orgChildBaseRot
                        self.childBoneAdjust.rotAdjustmentVec = self.orgChildBaseRot.eulerAngles
                if self.localPositions[pIndex]:
                    self.childTransform.localPosition = self.localPositions[pIndex]
                if self.localRotations[pIndex]:
                    self.childTransform.localRotation = self.localRotations[pIndex]
            else:
                self.childTransform.parent = self.orgChildParent
                if self.childBoneAdjust:
                    self.childBoneAdjust.basePosition = self.orgChildBasePos
                    self.childBoneAdjust.rotAdjustment = self.orgChildBaseRot
                    self.childBoneAdjust.rotAdjustmentVec = self.orgChildBaseRot.eulerAngles
                self.childTransform.localPosition = self.orgChildPosition
                self.childTransform.localRotation = self.orgChildRotation
            self.currentExtParenIndex = pIndex

    def verify(self, game, motionCtrlDict):
        if not self.Valid:
            print "ParentController %s becomes invalid, try to fix it"%str(self)
            try:
                return self.solveMotionControl(game, motionCtrlDict)
            except Exception, e:
                print e
                return False
        else:
            return True

    def cleanup(self):
        if self.Valid:
            try:
                self.setExternalParent(None)
            except:
                traceback.print_exc()

    def setFrame(self, frameNo):
        #print "pc debug: setframe #%f"%frameNo
        if not self.enable or len(self.keyframes) == 0:
            self.setExternalParent(None)  # disabled or no keyframe
            return
        if frameNo < self.keyframes[0]:
            self.setExternalParent(None)  # not valid yet
            self.fsIndex = 0
            return
        if frameNo >= self.keyframes[-1]:
            self.setExternalParent(-1)
            return
        if frameNo < self.keyframes[self.fsIndex]:
            self.fsIndex = 0
        while frameNo >= self.keyframes[self.fsIndex + 1]:
            self.fsIndex += 1
        self.setExternalParent(self.fsIndex)
        return
