#
# VNGE based Physics module
#
# v0.2 from Countd360:
# - initial version
# 0.2.1 (Keitaro)
# - added check for run Python code from scene data (it's not good from security to allow it by default)
# v1.0 (Countd360)
# - support vnanime
# v1.0.1 (Countd360)
# - fix Input trigger's bug when timing is 'Up' or 'Hold'
# - update setting UI

import traceback
import os
import copy
import codecs
from UnityEngine import Vector3, Quaternion, Color, ForceMode, Input, KeyCode
from UnityEngine import GameObject, Collider, BoxCollider, SphereCollider, CapsuleCollider, MeshCollider, PhysicMaterialCombine, Rigidbody
from vnactor import value_2_enum, str_2_enum
from System import Single

# ====================================  Manager classes ====================================
class PhysicsManager(object):
    SAVE_ID = "vnge_physics"
    CUR_SAVE_VERSION = "1"
    VALID_SAVE_VERSION = ["1"]
    FLASH_LENGTH = 0.1

    @property
    def hasContents(self):
        return len(self.colliders) > 0 or len(self.rigidBodys) > 0 or len(self.triggers) > 0 or len(self.actions) > 0

    @property
    def hasScenePython(self):
        return len([act for act in self.actions if act.type == "PYTHON"]) > 0

    def __init__(self, game):
        # settings
        self.colliders = []
        self.rigidBodys = []
        self.triggers = []
        self.actions = []

        # dicts
        self.colliderIdDict = {}
        self.colliderObjDict = {}
        self.rigidBodyIdDict = {}
        self.rigidBodyObjDict = {}
        self.triggerIdDict = {}
        self.helperIdDict = {}
        self.actionIdDict = {}

        # runtimes
        self.game = game
        self.wndId = None

        self.appendCount = 0
        self.keycodeDetectMode = False
        self.lastDetectedKeycode = None
        self.lastMouseHitInfo = None
        self.lastMouseHitted = False
        self.draggingGuideObj = False
        self.linkedPyFilepath = None
        self.flashingObjects = {}

    def cleanup(self):
        try:
            # clear collider
            for cld in self.colliders:
                cld.removeCollider()
            # clear rigidbody
            for rgb in self.rigidBodys:
                rgb.removeRigidBody()
            # clear helper
            for tgr in self.triggers:
                self.deleteHelper(tgr)

            # clear setting
            self.colliders = []
            self.rigidBodys = []
            self.triggers = []
            self.actions = []

            # clear dicts
            self.colliderIdDict = {}
            self.colliderObjDict = {}
            self.rigidBodyIdDict = {}
            self.rigidBodyObjDict = {}
            self.triggerIdDict = {}
            self.helperIdDict = {}
            self.actionIdDict = {}

            # clear runtime
            self.appendCount = 0
            self.lastMouseHitInfo = None
            self.lastMouseHitted = False
            self.draggingGuideObj = False
            self.flashingObjects = {}
        except:
            traceback.print_exc()

    def exportSetting(self):
        setting = {}
        # basic data
        setting["version"] = "1"
        # colliders
        setting["colliders"] = []
        for cld in self.colliders:
            setting["colliders"].append(cld.exportSetting())
        # rigidBodys
        setting["rigidBodys"] = []
        for rgd in self.rigidBodys:
            setting["rigidBodys"].append(rgd.exportSetting())
        # triggers
        setting["triggers"] = []
        for trg in self.triggers:
            setting["triggers"].append(trg.exportSetting())
        # actions
        setting["actions"] = []
        for act in self.actions:
            setting["actions"].append(act.exportSetting())
        return setting

    def importSetting(self, setting, append = False):
        if not append:
            self.cleanup()
        loadCount = [0, 0, 0, 0]
        # colliders
        for cldSetting in setting["colliders"]:
            cld = PhyCollider.CreateFromSetting(cldSetting)
            if cld:
                self.registerCollider(cld)
                loadCount[0] += 1
        # rigidBodys
        for rgbSetting in setting["rigidBodys"]:
            rgb = PhyRigidBody.CreateFromSetting(rgbSetting)
            if rgb:
                self.registerRigidBody(rgb)
                loadCount[1] += 1
        # triggers
        for trgSetting in setting["triggers"]:
            trg = PhyTrigger.CreateFromSetting(trgSetting)
            if trg:
                self.registerTrigger(trg)
                loadCount[2] += 1
        # actions
        for actSetting in setting["actions"]:
            act = VNAction.CreateFromSetting(actSetting)
            if act:
                self.registerAction(act)
                loadCount[3] += 1
        # Done
        return loadCount

    def saveData(self, filename = None, forUser = False):
        from vnframe import dumpToJsonString
        print("VNPHY: scene saving...")
        strData = dumpToJsonString(self.exportSetting(), forUser)
        if filename == None:
            self.game.dataPNG_save(PhysicsManager.SAVE_ID, strData, PhysicsManager.CUR_SAVE_VERSION)
            print("VNPHY: data saved [%d, %d, %d, %d]"%(len(self.colliders), len(self.rigidBodys), len(self.triggers), len(self.actions)))
        else:
            try:
                fp = codecs.open(filename, "w", 'utf-8')
                fp.write(strData)
                fp.close()
                print("VNPHY: export to file %s"%str(filename))
            except:
                print("VNPHY: Fail to save to file %s"%str(filename))
                return False
        return True

    def loadData(self, filename = None, append = False, dicKeyOldToNew = None):
        # renewId
        def append_AutoIdModify(setting):
            # judge rule: autoId->update, manual id but conflicted->update, manual id no conflicted->keep
            def judgeIdUpdate(oldId, typeIndex, append):
                prefix = ("vcd", "vrb", "vtg", "vpa")[typeIndex]
                idDict = (self.colliderIdDict, self.rigidBodyIdDict, self.triggerIdDict, self.actionIdDict)[typeIndex]
                if oldId.startswith(prefix) and oldId[len(prefix):].isdecimal():
                    return oldId + append
                elif oldId in idDict:
                    return oldId + append
                else:
                    return oldId

            # append no
            appNo = "_%d"%self.appendCount
            # colliders
            for cldSetting in setting["colliders"]:
                cldSetting["id"] = judgeIdUpdate(cldSetting["id"], 0, appNo)
            # rigidBodys
            for rgbSetting in setting["rigidBodys"]:
                rgbSetting["id"] = judgeIdUpdate(rgbSetting["id"], 1, appNo)
            # triggers
            for trgSetting in setting["triggers"]:
                trgSetting["id"] = judgeIdUpdate(trgSetting["id"], 2, appNo)
                # update options
                if "colliders" in trgSetting["options"]:
                    trgSetting["options"]["colliders"] = list([judgeIdUpdate(cldId, 0, appNo) for cldId in trgSetting["options"]["colliders"]])
                trgSetting["actions"] = list([judgeIdUpdate(actId, 3, appNo) for actId in trgSetting["actions"]])
            # actions
            for actSetting in setting["actions"]:
                actSetting["id"] = judgeIdUpdate(actSetting["id"], 3, appNo)

        def append_DicKeyUpdate(setting, dicKeyOldToNew):
            if dicKeyOldToNew == None:
                return
            # colliders
            for cldSetting in setting["colliders"]:
                cldSetting["targetDicKey"] = dicKeyOldToNew[cldSetting["targetDicKey"]]
            # rigidBodys
            for rgbSetting in setting["rigidBodys"]:
                rgbSetting["targetDicKey"] = dicKeyOldToNew[rgbSetting["targetDicKey"]]

        # get data
        print("VNPHY: scene loading...")
        if filename == None:
            saveData, version = self.game.dataPNG_load_checkversion(PhysicsManager.SAVE_ID, PhysicsManager.VALID_SAVE_VERSION)
        else:
            try:
                fp = codecs.open(filename, "r", 'utf-8')
                saveData = fp.read()
                fp.close()
            except:
                print("VNPHY: Fail to load from file %s"%str(filename))
                return False
        # import data
        if saveData != None and len(saveData) != 0:
            try:
                from vnframe import loadFromJsonString
                setting = loadFromJsonString(saveData)
                # if append is used, update id and dicKey
                if append:
                    self.appendCount += 1
                    append_AutoIdModify(setting)
                    append_DicKeyUpdate(setting, dicKeyOldToNew)
                else:
                    self.appendCount = 0
                # import
                loadCount = self.importSetting(setting, append)
                # report
                if append:
                    print("VNPHY: data appended %s as #%d"%(str(loadCount), self.appendCount))
                else:
                    print("VNPHY: data loaded %s"%str(loadCount))
                return any(loadCount)
            except:
                print("VNPHY: Fail to import from setting")
        else:
            print("VNPHY: No data found...")
        return False

    def registerCollider(self, tgtCollider):
        if tgtCollider.id not in self.colliderIdDict:
            self.colliders.append(tgtCollider)
            self.colliderIdDict[tgtCollider.id] = tgtCollider
            self.colliderObjDict[tgtCollider.collider] = tgtCollider
        else:
            raise Exception("Collider ID existed")

    def deleteCollider(self, tgtCollider):
        if tgtCollider.id in self.colliderIdDict:
            oldCollider = self.colliderIdDict.pop(tgtCollider.id)
            self.colliderObjDict.pop(oldCollider.collider)
            self.colliders.remove(oldCollider)
            oldCollider.removeCollider()

    def getColliderById(self, tgtId):
        if tgtId in self.colliderIdDict:
            return self.colliderIdDict[tgtId]
        else:
            return None

    def getAttachedRigidBodyByColliderId(self, tgtCldId):
        cld = self.getColliderById(tgtCldId)
        if cld and cld.collider.attachedRigidbody:
            if cld.collider.attachedRigidbody in self.rigidBodyObjDict:
                return self.rigidBodyObjDict[cld.collider.attachedRigidbody]
        return None

    def registerRigidBody(self, tgtRigidBody):
        if tgtRigidBody.id not in self.rigidBodyIdDict:
            self.rigidBodys.append(tgtRigidBody)
            self.rigidBodyIdDict[tgtRigidBody.id] = tgtRigidBody
            self.rigidBodyObjDict[tgtRigidBody.rigidBody] = tgtRigidBody
        else:
            raise Exception("RigidBody ID existed")

    def deleteRigidBody(self, tgtRigidBody):
        if tgtRigidBody.id in self.rigidBodyIdDict:
            oldRigidBody = self.rigidBodyIdDict.pop(tgtRigidBody.id)
            self.rigidBodyObjDict.pop(oldRigidBody.rigidBody)
            self.rigidBodys.remove(oldRigidBody)
            oldRigidBody.removeRigidBody()

    def getRigidBodyById(self, tgtId):
        if tgtId in self.rigidBodyIdDict:
            return self.rigidBodyIdDict[tgtId]
        else:
            return None

    def registerTrigger(self, tgtTrigger):
        if tgtTrigger.id not in self.triggerIdDict:
            self.triggers.append(tgtTrigger)
            self.triggerIdDict[tgtTrigger.id] = tgtTrigger
        else:
            oldTrigger = self.triggerIdDict[tgtTrigger.id]
            oldTrigger.update(tgtTrigger)
        self.registerHelper(tgtTrigger)

    def deleteTrigger(self, tgtTrigger):
        if tgtTrigger.id in self.triggerIdDict:
            oldTrigger = self.triggerIdDict.pop(tgtTrigger.id)
            self.triggers.remove(oldTrigger)
        self.deleteHelper(tgtTrigger)

    def fireTrigger(self, tgtTriggerId, callerInfo = {}, inputParam = {}, force = False):
        if tgtTriggerId in self.triggerIdDict:
            tgtTrg = self.triggerIdDict[tgtTriggerId]
            if force or tgtTrg.checkPreCondition():
                tgtTrg.fireTrigger()
                # flash trigger
                self.flashObject(tgtTrg)
                # add trgger to callerInfo
                callerInfo["trigger"] = tgtTrg
                # run all actions
                for actId in tgtTrg.actions:
                    self.runAction(actId, callerInfo, inputParam)

    def registerHelper(self, tgtTrigger):
        # if helper exist, delete it
        self.deleteHelper(tgtTrigger)
        
        # create new helper
        if tgtTrigger.type == "Collision":
            try:
                # create helper
                import VNHelper
                cldId = tgtTrigger.options["colliders"][0]
                phyCld = self.colliderIdDict[cldId]
                helper = phyCld.colliderGO.AddComponent(VNHelper.VNHelperComponent)
                self.helperIdDict[tgtTrigger.id] = helper

                # update helper
                helper.owner = self
                helper.id = tgtTrigger.id
                helper.ResetAllEvent()
                if tgtTrigger.options["event"] == "Collision":
                    if tgtTrigger.options["timing"] == "Enter":
                        helper.collisionEnterEvent = PhysicsManager.CollisionHelperFunc
                    elif tgtTrigger.options["timing"] == "Stay":
                        helper.collisionStayEvent = PhysicsManager.CollisionHelperFunc
                    elif tgtTrigger.options["timing"] == "Exit":
                        helper.collisionExitEvent = PhysicsManager.CollisionHelperFunc
                    else:
                        raise Exception("Unknown collision timing '%s'"%tgtTrigger.options["timing"])
                elif tgtTrigger.options["event"] == "Trigger":
                    if tgtTrigger.options["timing"] == "Enter":
                        helper.triggerEnterEvent = PhysicsManager.TriggerHelperFunc
                    elif tgtTrigger.options["timing"] == "Stay":
                        helper.triggerStayEvent = PhysicsManager.TriggerHelperFunc
                    elif tgtTrigger.options["timing"] == "Exit":
                        helper.triggerExitEvent = PhysicsManager.TriggerHelperFunc
                    else:
                        raise Exception("Unknown Collision trigger event '%s'"%tgtTrigger.options["event"])
                else:
                    raise Exception("Unknown trigger timing '%s'"%tgtTrigger.options["timing"])
            except Exception as e:
                self.game.logger_log_message("Fail to add VNHelper component: " + str(e), "VNPHY: ")
                traceback.print_exc()
        else:
            # this trigger do not need helper
            pass
    
    def deleteHelper(self, tgtTrigger):
        if tgtTrigger.id in self.helperIdDict:
            helper = self.helperIdDict.pop(tgtTrigger.id)
            GameObject.Destroy(helper)
                    
    def registerAction(self, tgtAction):
        if tgtAction.id not in self.actionIdDict:
            self.actions.append(tgtAction)
            self.actionIdDict[tgtAction.id] = (tgtAction)
        else:
            oldAction = self.actionIdDict[tgtAction.id]
            oldAction.update(tgtAction)

    def deleteAction(self, tgtAction):
        if tgtAction.id in self.actionIdDict:
            oldAction = self.actionIdDict.pop(tgtAction.id)
            self.actions.remove(oldAction)

    def runAction(self, tgtActionId, callerInfo = {}, inputParam = {}):
        if tgtActionId in self.actionIdDict:
            vnAct = self.actionIdDict[tgtActionId]
            res = vnAct.run(self.game, callerInfo, inputParam)
            # flash action
            self.flashObject(vnAct)
            return res
        return "Action <%s> not registered"%tgtActionId

    # manager functions
    def flashObject(self, tgtObj):
        self.flashingObjects[tgtObj] = PhysicsManager.FLASH_LENGTH

    def houseKeeping(self, delta):
        # collider/rigidbody delete detect
        for phyCld in self.colliders:
            if not phyCld.valid:
                print("[VNPHY HOUSEKEEPING] delete non-valid collider %s"%phyCld.id)
                phyCld.removeGizmos()
                self.deleteCollider(phyCld)
                break   # delete once one every update
            if phyCld.gizmosObj != None and phyCld.gizmosObj.objectItem == None:
                print("[VNPHY HOUSEKEEPING] delete non-valid gizmos of %s"%phyCld.id)
                phyCld.removeGizmos()
        for phyRgb in self.rigidBodys:
            if not phyRgb.valid:
                print("[VNPHY HOUSEKEEPING] delete non-valid rigidbody %s"%phyRgb.id)
                self.deleteRigidBody(phyRgb)
                break   # delete once one every update

        # collider job
        for phyCld in self.colliders:
            phyCld.syncEnableStatus()
            phyCld.updateGizmos(self.game, self.draggingGuideObj)

        # flash object
        ended = set()
        for fobj in self.flashingObjects.keys():
            self.flashingObjects[fobj] -= delta
            if self.flashingObjects[fobj] <= 0:
                ended.add(fobj)
        for eobj in ended:
            del self.flashingObjects[eobj]

        # trigger cooling down
        for trg in self.triggers:
            trg.coolingDown(delta)

    def checkClickTrigger(self, mouseButton, mousePosition, isColliderHitted, colliderHitInfo):
        clickTriggers = [t for t in self.triggers if t.type == "Click"]
        if not len(clickTriggers):
            return
        param = {}
        param["mouseButton"] = mouseButton
        param["mousePosition"] = mousePosition
        param["colliderHitted"] = isColliderHitted
        if isColliderHitted:
            param["colliderId"] = self.colliderObjDict[colliderHitInfo.collider].id
            param["hitPoint"] = colliderHitInfo.point
        for trg in clickTriggers:
            if trg.checkTrigger(param):
                # flash trigger
                self.flashObject(trg)
                # run all actions
                callerInfo = {"caller": "vnphy_trigger", "trigger": trg}
                if isColliderHitted:
                    callerInfo["collider"] = self.colliderObjDict[colliderHitInfo.collider]
                    callerInfo["rigidBody"] = self.getAttachedRigidBodyByColliderId(param["colliderId"])
                for actId in trg.actions:
                    self.runAction(actId, callerInfo, param)

    def checkInputTrigger(self):
        inputTriggers = [t for t in self.triggers if t.type == "Input"]
        if not len(inputTriggers):
            return
        param = {"keycode": None}
        for trg in inputTriggers:
            if trg.checkTrigger(param):
                # flash trigger
                self.flashObject(trg)
                # run all actions
                callerInfo = {"caller": "vnphy_trigger", "trigger": trg}
                for actId in trg.actions:
                    self.runAction(actId, callerInfo, param)

    def checkSceneEventTrigger(self, eventType, filename):
        eventTriggers = [t for t in self.triggers if t.type == "SceneEvent" and t.options["event"] == eventType]
        if not len(eventTriggers):
            return
        param = {}
        if filename:
            from os import path
            param["filename"] = path.realpath(filename)
        for trg in eventTriggers:
            if trg.checkTrigger(param):
                # flash trigger
                self.flashObject(trg)
                # run all actions
                callerInfo = {"caller": "vnphy_trigger", "trigger": trg}
                for actId in trg.actions:
                    self.runAction(actId, callerInfo, param)

    @staticmethod
    def CollisionHelperFunc(self, id, collisionInfo):
        #print("CollisionHelperFunc from %s: %s"%(id, str(collisionInfo)))
        if id in self.triggerIdDict:
            trg = self.triggerIdDict[id]
        else:
            return
        # fire related trigger
        try:
            callerInfo = {"caller": "vnphy_trigger", "trigger": trg}
            callerInfo["collider"] = self.colliderIdDict[trg.options["colliders"][0]]
            
            param = {"collision": collisionInfo}
            param["otherCollider"] = self.colliderObjDict[collisionInfo.collider] if collisionInfo.collider in self.colliderObjDict else None

            self.fireTrigger(id, callerInfo, param)
        except:
            print("CollisionHelperFunc failed")
            traceback.print_exc()

    @staticmethod
    def TriggerHelperFunc(self, id, other):
        #print("TriggerHelperFunc from %s: %s"%(id, str(other)))
        if id in self.triggerIdDict:
            trg = self.triggerIdDict[id]
        else:
            return
        # fire related trigger
        try:
            callerInfo = {"caller": "vnphy_trigger", "trigger": trg}
            callerInfo["collider"] = self.colliderIdDict[trg.options["colliders"][0]]
            
            param = {"other": other}
            param["otherCollider"] = self.colliderObjDict[other] if other in self.colliderObjDict else None

            self.fireTrigger(id, callerInfo, param)
        except:
            print("TriggerHelperFunc failed")
            traceback.print_exc()

# ====================================  Collider classes ====================================
class PhyCollider(object):
    COLLIDER_TYPES = ("Box", "Sphere", "Capsule")
    COLLIDER_GIZMOS_STUDIONEO = ((1,), (0,), (4, 0, 0))
    COLLIDER_GIZMOS_PLAYHOMESTUDIO = ((1,), (0,), (4, 0, 0))
    COLLIDER_GIZMOS_CHARASTUDIO = ((0, 0, 1), (0, 0, 0), (0, 0, 4, 0, 0, 1173, 0, 0, 1173))
    COLLIDER_GIZMOS_STUDIONEOV2 = ((0, 18, 591), (0, 18, 590), (0, 18, 594, 0, 18, 655, 0, 18, 655))    # capsule (0, 18, 638)

    @property
    def enable(self):
        if self.valid:
            return self.collider.enabled
        return False

    @enable.setter
    def enable(self, value):
        if self.valid and value != self.collider.enabled:
            self.collider.enabled = value
            if not value:
                self.removeGizmos()

    @property
    def valid(self):
        return self.vnObject != None and \
               self.vnObject.objctrl != None and \
               self.colliderGO != None and \
               self.collider != None

    @property
    def title(self):
        return self.id + ": " + self.desp

    def __init__(self):
        self.id = ""
        self.desp = ""
        self.type = ""
        self.autoEnable = False
        self.targetDicKey = None
        self.targetBonePath = ""
        # runtime
        from vngameengine import vnge_game as game
        if game.isNEOV2:
            self.colliderScale = Single(1)
        else:
            self.colliderScale = Single(0.1)
        self.vnObject = None
        self.colliderGO = None
        self.collider = None
        self.gizmosObj = None
        self.gizmosEditMode = False

    def __str__(self):
        return "PhyCollider %s (%s)"%(self.title, self.type)

    def exportSetting(self):
        setting = {}
        # basic data
        setting["version"] = "1"
        setting["id"] = self.id
        setting["desp"] = self.desp
        setting["type"] = self.type
        setting["autoEnable"] = self.autoEnable
        setting["targetDicKey"] = self.targetDicKey
        setting["targetBonePath"] = self.targetBonePath
        # dimension
        if self.type == "Box":
            setting["dimension"] = {"center": self.collider.center, "size": self.collider.size}
        elif self.type == "Sphere":
            setting["dimension"] = {"center": self.collider.center, "radius": self.collider.radius}
        elif self.type == "Capsule":
            setting["dimension"] = {"center": self.collider.center, "radius": self.collider.radius, "height": self.collider.height, "direction": self.collider.direction}
        else:
            raise Exception("Fail to export collider of unknown type")
        # trigger
        setting["isTrigger"] = self.collider.isTrigger
        # physics material
        setting["material"] = {}
        setting["material"]["staticFriction"] = float(self.collider.material.staticFriction)
        setting["material"]["dynamicFriction"] = float(self.collider.material.dynamicFriction)
        setting["material"]["frictionCombine"] = int(self.collider.material.frictionCombine)
        setting["material"]["bounciness"] = float(self.collider.material.bounciness)
        setting["material"]["bounceCombine"] = int(self.collider.material.staticFriction)
        return setting

    def createCollider(self):
        if self.valid:
            return
        if self.type == "Box":
            self.collider = self.colliderGO.AddComponent(BoxCollider)
            self.collider.size = Vector3.one * self.colliderScale
        elif self.type == "Sphere":
            self.collider = self.colliderGO.AddComponent(SphereCollider)
            self.collider.radius = 0.5 * self.colliderScale
        elif self.type == "Capsule":
            self.collider = self.colliderGO.AddComponent(CapsuleCollider)
            self.collider.radius = 0.5 * self.colliderScale
            self.collider.height = 2 * self.colliderScale
        else:
            self.collider = None
        if self.collider == None:
            raise Exception("Fail to create collider component")

    def removeCollider(self):
        if not self.valid:
            return
        GameObject.Destroy(self.collider)
        self.vnObject = None
        self.colliderGO = None
        self.collider = None
        self.removeGizmos()

    def createGizmos(self):
        from vngameengine import vnge_game as game
        def searchTreeNode(target):
            treeNodeCtrl = game.studio.treeNodeCtrl
            for tno in treeNodeCtrl.m_TreeNodeObject:
                if tno.textName == target:
                    return game.studio.dicInfo[tno]
            return None

        def createGizmosItem():
            from Studio import AddObjectItem
            tidx = PhyCollider.COLLIDER_TYPES.index(self.type)
            
            if game.isNEOV2:
                iInfo = PhyCollider.COLLIDER_GIZMOS_STUDIONEOV2[tidx]
                ociObj = AddObjectItem.Add(iInfo[0], iInfo[1], iInfo[2])
                ociObj.treeNodeObject.textName = "[" + self.id + "]"
                setupGizmosItem(ociObj)
                if self.type == "Capsule":
                    ociObj1 = AddObjectItem.Add(iInfo[3], iInfo[4], iInfo[5])
                    game.studio.treeNodeCtrl.SetParent(ociObj1.treeNodeObject, ociObj.treeNodeObject)
                    ociObj1.objectInfo.changeAmount.pos = Vector3(0, 0.5, 0)
                    ociObj1.objectInfo.changeAmount.rot = Vector3.zero
                    ociObj1.objectInfo.changeAmount.scale = Vector3.one
                    setupGizmosItem(ociObj1)
                    ociObj2 = AddObjectItem.Add(iInfo[6], iInfo[7], iInfo[8])
                    game.studio.treeNodeCtrl.SetParent(ociObj2.treeNodeObject, ociObj.treeNodeObject)
                    ociObj2.objectInfo.changeAmount.pos = Vector3(0, -0.5, 0)
                    ociObj2.objectInfo.changeAmount.rot = Vector3(180, 0, 0)
                    ociObj2.objectInfo.changeAmount.scale = Vector3.one
                    setupGizmosItem(ociObj2)
            elif game.isCharaStudio:
                iInfo = PhyCollider.COLLIDER_GIZMOS_CHARASTUDIO[tidx]
                ociObj = AddObjectItem.Add(iInfo[0], iInfo[1], iInfo[2])
                ociObj.treeNodeObject.textName = "[" + self.id + "]"
                setupGizmosItem(ociObj)
                if self.type == "Capsule":
                    ociObj1 = AddObjectItem.Add(iInfo[3], iInfo[4], iInfo[5])
                    game.studio.treeNodeCtrl.SetParent(ociObj1.treeNodeObject, ociObj.treeNodeObject)
                    ociObj1.objectInfo.changeAmount.pos = Vector3(0, 0.05, 0)
                    ociObj1.objectInfo.changeAmount.rot = Vector3.zero
                    ociObj1.objectInfo.changeAmount.scale = Vector3.one
                    setupGizmosItem(ociObj1)
                    ociObj2 = AddObjectItem.Add(iInfo[6], iInfo[7], iInfo[8])
                    game.studio.treeNodeCtrl.SetParent(ociObj2.treeNodeObject, ociObj.treeNodeObject)
                    ociObj2.objectInfo.changeAmount.pos = Vector3(0, -0.05, 0)
                    ociObj2.objectInfo.changeAmount.rot = Vector3(180, 0, 0)
                    ociObj2.objectInfo.changeAmount.scale = Vector3.one
                    setupGizmosItem(ociObj2)
            elif game.isPlayHomeStudio:
                iInfo = PhyCollider.COLLIDER_GIZMOS_PLAYHOMESTUDIO[tidx]
                ociObj = AddObjectItem.Add(iInfo[0])
                ociObj.treeNodeObject.textName = "[" + self.id + "]"
                setupGizmosItem_HS1PH(ociObj)
                if self.type == "Capsule":
                    ociObj1 = AddObjectItem.Add(iInfo[1])
                    game.studio.treeNodeCtrl.SetParent(ociObj1.treeNodeObject, ociObj.treeNodeObject)
                    ociObj1.objectInfo.changeAmount.pos = Vector3(0, 0.05, 0)
                    ociObj1.objectInfo.changeAmount.rot = Vector3.zero
                    ociObj1.objectInfo.changeAmount.scale = Vector3.one
                    setupGizmosItem_HS1PH(ociObj1)
                    ociObj2 = AddObjectItem.Add(iInfo[2])
                    game.studio.treeNodeCtrl.SetParent(ociObj2.treeNodeObject, ociObj.treeNodeObject)
                    ociObj2.objectInfo.changeAmount.pos = Vector3(0, -0.05, 0)
                    ociObj2.objectInfo.changeAmount.rot = Vector3(180, 0, 0)
                    ociObj2.objectInfo.changeAmount.scale = Vector3.one
                    setupGizmosItem_HS1PH(ociObj2)
            elif game.isStudioNEO:
                iInfo = PhyCollider.COLLIDER_GIZMOS_STUDIONEO[tidx]
                ociObj = AddObjectItem.Add(iInfo[0])
                ociObj.treeNodeObject.textName = "[" + self.id + "]"
                setupGizmosItem_HS1PH(ociObj)
                if self.type == "Capsule":
                    ociObj1 = AddObjectItem.Add(iInfo[1])
                    game.studio.treeNodeCtrl.SetParent(ociObj1.treeNodeObject, ociObj.treeNodeObject)
                    ociObj1.objectInfo.changeAmount.pos = Vector3(0, 0.05, 0)
                    ociObj1.objectInfo.changeAmount.rot = Vector3.zero
                    ociObj1.objectInfo.changeAmount.scale = Vector3.one
                    setupGizmosItem_HS1PH(ociObj1)
                    ociObj2 = AddObjectItem.Add(iInfo[2])
                    game.studio.treeNodeCtrl.SetParent(ociObj2.treeNodeObject, ociObj.treeNodeObject)
                    ociObj2.objectInfo.changeAmount.pos = Vector3(0, -0.05, 0)
                    ociObj2.objectInfo.changeAmount.rot = Vector3(180, 0, 0)
                    ociObj2.objectInfo.changeAmount.scale = Vector3.one
                    setupGizmosItem_HS1PH(ociObj2)
            else:
                print("Not supported engine")
                return None
            return ociObj

        def setupGizmosItem_HS1PH(ociObj):
            # change status
            from vngameengine import HSNeoOCI
            vnoci = HSNeoOCI.create_from(ociObj).as_prop
            vnoci.set_color(((Color(0, 1, 0, 0.5), Color.white, 0, 0),))

        def setupGizmosItem(ociObj):
            # change status
            from vngameengine import HSNeoOCI
            vnoci = HSNeoOCI.create_from(ociObj).as_prop
            vnoci.set_alpha(0.5)
            vnoci.set_color((Color.green, None, None, None))

        if self.gizmosObj != None and self.gizmosObj.objectItem != None:
            print("Gizmos of %s existed!"%self.id)
            return self.gizmosObj

        ociObj = createGizmosItem()
        self.gizmosObj = ociObj
        self.gizmosEditMode = False
        return ociObj

    def removeGizmos(self):
        if self.gizmosObj != None and self.gizmosObj.objectItem != None:
            from vngameengine import vnge_game as game
            if self.gizmosObj.treeNodeObject in game.studio.treeNodeCtrl.selectNodes:
                game.studio.treeNodeCtrl.selectNode = None
            game.studio.DeleteNode(self.gizmosObj.treeNodeObject)
        self.gizmosObj = None
        self.gizmosEditMode = False

    def updateGizmos(self, game, draggingGuideObject):
        def globalScale(trans):
            scl = trans.localScale
            while trans.parent != None:
                trans = trans.parent
                scl = Vector3.Scale(scl, trans.localScale)
            return scl
        
        if self.valid and self.gizmosObj != None and self.gizmosObj.objectItem != None:
            #self.gizmosEditMode = draggingGuideObject and self.gizmosObj.treeNodeObject == game.studio.treeNodeCtrl.selectNode
            self.gizmosEditMode = draggingGuideObject and self.gizmosObj.treeNodeObject in game.studio.treeNodeCtrl.selectNodes

            if self.gizmosEditMode:
                # gizmos to collider
                pos = self.gizmosObj.objectInfo.changeAmount.pos
                self.collider.center = self.colliderGO.transform.InverseTransformPoint(pos)

                if self.type == "Box":
                    gScl = self.gizmosObj.objectInfo.changeAmount.scale
                    lScl = globalScale(self.colliderGO.transform)
                    scl = Vector3(gScl.x / lScl.x, gScl.y / lScl.y, gScl.z / lScl.z)
                    self.collider.size = scl * self.colliderScale
                elif self.type == "Sphere":
                    gScl = self.gizmosObj.objectInfo.changeAmount.scale
                    lScl = globalScale(self.colliderGO.transform)
                    if self.collider.radius != gScl.x / lScl.x / 2:
                        radius = gScl.x / lScl.x / 2
                    elif self.collider.radius != gScl.y / lScl.y / 2:
                        radius = gScl.y / lScl.y / 2
                    else:
                        radius = gScl.z / lScl.z / 2
                    if radius <= 0:
                        radius = 0.001
                    self.collider.radius = radius * self.colliderScale
                    self.gizmosObj.objectInfo.changeAmount.scale = Vector3.Scale(Vector3.one * radius * 2, lScl)
                    #gScl = Vector3.one * radius * 2
                    
                elif self.type == "Capsule":
                    gScl = self.gizmosObj.objectInfo.changeAmount.scale
                    lScl = globalScale(self.colliderGO.transform)
                    if self.collider.radius != gScl.x / lScl.x / 2:
                        radius = gScl.x / lScl.x / 2
                    else:
                        radius = gScl.z / lScl.z / 2
                    height = gScl.y / lScl.y + radius * 2
                    self.collider.height = height * self.colliderScale
                    self.collider.radius = radius * self.colliderScale
                    scl = Vector3(radius * 2, gScl.y / lScl.y if gScl.y / lScl.y > 0 else 0.00001, radius * 2)
                    self.gizmosObj.objectInfo.changeAmount.scale = Vector3.Scale(scl, lScl)
                    # update sub-object
                    tObj1 = self.gizmosObj.treeNodeObject.child[0]
                    tObj2 = self.gizmosObj.treeNodeObject.child[1]
                    if tObj1 in game.studio.dicInfo and tObj2 in game.studio.dicInfo:
                        gObj1 = game.studio.dicInfo[tObj1]
                        gObj1.objectInfo.changeAmount.scale = Vector3.Scale(Vector3.one * radius * 2, lScl)
                        gObj2 = game.studio.dicInfo[tObj2]
                        gObj2.objectInfo.changeAmount.scale = Vector3.Scale(Vector3.one * radius * 2, lScl)
            else:   
                # collider to gizmos
                pos = self.colliderGO.transform.TransformPoint(self.collider.center)
                self.gizmosObj.objectInfo.changeAmount.pos = pos

                if self.type == "Box":
                    scl = globalScale(self.colliderGO.transform)
                    scl = Vector3.Scale(scl, self.collider.size) / self.colliderScale
                elif self.type == "Sphere":
                    scl = globalScale(self.colliderGO.transform)
                    radius = max(scl.x, scl.y, scl.z) * self.collider.radius / self.colliderScale
                    scl = Vector3.one * radius * 2
                elif self.type == "Capsule":
                    scl = globalScale(self.colliderGO.transform)
                    radius = max(scl.x, scl.z) * self.collider.radius / self.colliderScale
                    height = scl.y * self.collider.height / self.colliderScale
                    height = 0.00001 if height <= radius * 2 else height - radius * 2
                    scl = Vector3(radius * 2, height, radius * 2)
                    # update sub-object
                    tObj1 = self.gizmosObj.treeNodeObject.child[0]
                    tObj2 = self.gizmosObj.treeNodeObject.child[1]
                    if tObj1 in game.studio.dicInfo and tObj2 in game.studio.dicInfo:
                        gObj1 = game.studio.dicInfo[tObj1]
                        gObj1.objectInfo.changeAmount.scale = Vector3.one * radius * 2
                        gObj2 = game.studio.dicInfo[tObj2]
                        gObj2.objectInfo.changeAmount.scale = Vector3.one * radius * 2
                self.gizmosObj.objectInfo.changeAmount.scale = scl

            # rotation
            rot = self.colliderGO.transform.eulerAngles
            if self.type == "Capsule":
                if self.collider.direction == 0:
                    # rotation to x
                    zVec = self.colliderGO.transform.TransformDirection(Vector3.forward)
                    zRot = Quaternion.AngleAxis(270, zVec)
                    rotQ = zRot * self.colliderGO.transform.rotation
                    rot = rotQ.eulerAngles
                elif self.collider.direction == 2:
                    # rotation no z
                    xVec = self.colliderGO.transform.TransformDirection(Vector3.right)
                    xRot = Quaternion.AngleAxis(90, xVec)
                    rotQ = xRot * self.colliderGO.transform.rotation
                    rot = rotQ.eulerAngles
            self.gizmosObj.objectInfo.changeAmount.rot = rot

    def syncEnableStatus(self):
        if self.autoEnable and self.valid:
            # check if vnObject visible
            tno = self.vnObject.treeNodeObject
            pVisible = tno.visible
            while pVisible and tno.parent:
                tno = tno.parent
                pVisible &= tno.visible
            # sync enable with visible
            if not pVisible and self.enable:
                self.enable = False
                self.removeGizmos()
            if pVisible and not self.enable:
                self.enable = True

    @staticmethod
    def CreateFromOciObject(id, type, ociObj, boneGO):
        pc = PhyCollider()
        pc.id = id
        pc.type = type
        pc.targetDicKey = ociObj.objectInfo.dicKey
        pc.vnObject, rootGO = PhyCollider.GetVnObjAndRootGoFromOciObj(ociObj)
        pc.desp = pc.vnObject.text_name
        pc.colliderGO = boneGO
        pc.targetBonePath = PhyCollider.GetPathFromRootToGO(rootGO, boneGO)
        # check path
        if pc.targetBonePath == None:
            raise Exception("Bone gameobject not found in ociObject's childern")
        # create collider
        pc.createCollider()
        # done
        return pc

    @staticmethod
    def CreateFromSetting(setting):
        def loadData(settingDict, key, defaultValue = None):
            return settingDict[key] if key in settingDict else defaultValue
        # basic
        pc = PhyCollider()
        pc.id = loadData(setting, "id", pc.id)
        pc.type = loadData(setting, "type", pc.type)
        pc.desp = loadData(setting, "desp", pc.desp)
        pc.autoEnable = loadData(setting, "autoEnable", pc.autoEnable)
        pc.targetDicKey = loadData(setting, "targetDicKey", pc.targetDicKey)
        pc.targetBonePath = loadData(setting, "targetBonePath", pc.targetBonePath)
        # runtime
        try:
            pc.vnObject, rootGO = PhyCollider.GetVnObjAndRootGoFromDicKey(pc.targetDicKey)
            pc.colliderGO = PhyCollider.GetTgtGoFromRootByPath(rootGO, pc.targetBonePath)
            # create collider
            pc.createCollider()
            # restore dimension
            dimSetting = loadData(setting, "dimension", {})
            if pc.type == "Box":
                pc.collider.center = loadData(dimSetting, "center", Vector3.zero)
                pc.collider.size = loadData(dimSetting, "size", Vector3.one)
            elif pc.type == "Sphere":
                pc.collider.center = loadData(dimSetting, "center", Vector3.zero)
                pc.collider.radius = loadData(dimSetting, "radius", 0.5)
            elif pc.type == "Capsule":
                pc.collider.center = loadData(dimSetting, "center", Vector3.zero)
                pc.collider.radius = loadData(dimSetting, "radius", 0.5)
                pc.collider.height = loadData(dimSetting, "height", 2.0)
                pc.collider.direction = loadData(dimSetting, "direction", 1)
            else:
                raise Exception("Unkown type <%s>"%pc.type)
            # restore trigger
            pc.collider.isTrigger = loadData(setting, "isTrigger", False)
            # restore material
            matSetting = loadData(setting, "material", {})
            pc.collider.material.staticFriction = loadData(matSetting, "staticFriction", 0.6)
            pc.collider.material.dynamicFriction = loadData(matSetting, "dynamicFriction", 0.6)
            pc.collider.material.frictionCombine = value_2_enum(PhysicMaterialCombine, loadData(matSetting, "frictionCombine", 0))
            pc.collider.material.bounciness = loadData(matSetting, "bounciness", 0.0)
            pc.collider.material.bounceCombine = value_2_enum(PhysicMaterialCombine, loadData(matSetting, "bounceCombine", 0))
            # default desp
            if len(pc.desp) == 0:
                pc.desp = pc.vnObject.text_name
            # done
            return pc
        except Exception as e:
            traceback.print_exc()
            print("Failed in creating collider: " + str(e))
            return None

    @staticmethod
    def GetVnObjAndRootGoFromOciObj(ociObj):
        from vngameengine import HSNeoOCI, HSNeoOCIChar
        hsOci = HSNeoOCI.create_from(ociObj)
        if isinstance(hsOci, HSNeoOCIChar):
            vnObj = hsOci.as_actor
            rootGO = ociObj.charInfo.objRoot
        else:
            vnObj = hsOci.as_prop
            rootGO = ociObj.objectItem
        return vnObj, rootGO

    @staticmethod
    def GetVnObjAndRootGoFromDicKey(dicKey):
        from vngameengine import vnge_game
        ociObj = vnge_game.studio.dicObjectCtrl[dicKey]
        return PhyCollider.GetVnObjAndRootGoFromOciObj(ociObj)

    @staticmethod
    def GetPathFromRootToGO(rootGO, tgtGO):
        paths = []
        found = True
        while tgtGO != rootGO:
            paths.append(tgtGO.name)
            if tgtGO.transform.parent == None:
                found = False
                break
            tgtGO = tgtGO.transform.parent.gameObject
        paths.reverse()
        if found:
            return "/".join(paths)
        else:
            return None

    @staticmethod
    def GetTgtGoFromRootByPath(rootGO, path):
        def searchSubInChild(pGO, tgtName):
            for i in range(pGO.transform.childCount):
                st = pGO.transform.GetChild(i)
                if st.name == tgtName:
                    return st.gameObject
            return None
        tgtGO = rootGO
        if len(path) == 0:
            return tgtGO
        paths = path.split("/")
        for pName in paths:
            tgtGO = searchSubInChild(tgtGO, pName)
            if tgtGO == None:
                return None
        return tgtGO


# ====================================  Collider classes ====================================
class PhyRigidBody(object):
    @property
    def valid(self):
        return self.vnObject != None and \
               self.vnObject.objctrl != None and \
               self.rigidBodyGO != None and \
               self.rigidBody != None

    @property
    def title(self):
        return self.id + ": " + self.desp

    def __init__(self):
        self.id = ""
        self.desp = ""
        self.targetDicKey = None
        self.targetBonePath = ""
        # runtime
        self.vnObject = None
        self.rigidBodyGO = None
        self.rigidBody = None
        self.orgPosition = None
        self.orgRotation = None

    def __str__(self):
        return "PhyRigidBody %s"%(self.title)

    def exportSetting(self):
        setting = {}
        # basic data
        setting["version"] = "1"
        setting["id"] = self.id
        setting["desp"] = self.desp
        setting["targetDicKey"] = self.targetDicKey
        setting["targetBonePath"] = self.targetBonePath
        # rigidbody setting
        setting["mass"] = self.rigidBody.mass
        setting["drag"] = self.rigidBody.drag
        setting["angularDrag"] = self.rigidBody.angularDrag
        setting["useGravity"] = self.rigidBody.useGravity
        setting["isKinematic"] = self.rigidBody.isKinematic
        setting["constraints"] = int(self.rigidBody.constraints)
        return setting

    def createRigidBody(self):
        if self.valid:
            return
        self.rigidBody = self.rigidBodyGO.AddComponent(Rigidbody)
        if self.rigidBody == None:
            raise Exception("Fail to create collider component")
        self.rigidBody.useGravity = False
        self.rigidBody.isKinematic = True
        self.orgPosition = self.rigidBodyGO.transform.position
        self.orgRotation = self.rigidBodyGO.transform.rotation

    def removeRigidBody(self):
        if not self.valid:
            return
        self.resetRigidBody()
        GameObject.Destroy(self.rigidBody)
        self.vnObject = None
        self.rigidBodyGO = None
        self.rigidBody = None
        self.orgPosition = None
        self.orgRotation = None

    def resetRigidBody(self):
        if self.valid:
            self.rigidBody.position = self.orgPosition
            self.rigidBody.rotation = self.orgRotation
            self.rigidBody.velocity = Vector3.zero
            self.rigidBody.angularVelocity = Vector3.zero

    @staticmethod
    def CreateFromOciObject(id, ociObj, boneGO):
        prb = PhyRigidBody()
        prb.id = id
        prb.targetDicKey = ociObj.objectInfo.dicKey
        prb.vnObject, rootGO = PhyCollider.GetVnObjAndRootGoFromOciObj(ociObj)
        prb.desp = prb.vnObject.text_name
        prb.rigidBodyGO = boneGO
        prb.targetBonePath = PhyCollider.GetPathFromRootToGO(rootGO, boneGO)
        # check path
        if prb.targetBonePath == None:
            raise Exception("Bone gameobject not found in ociObject's childern")
        # create collider
        prb.createRigidBody()
        # done
        return prb

    @staticmethod
    def CreateFromSetting(setting):
        def loadData(settingDict, key, defaultValue = None):
            return settingDict[key] if key in settingDict else defaultValue
        # basic
        prb = PhyRigidBody()
        prb.id = loadData(setting, "id", prb.id)
        prb.desp = loadData(setting, "desp", prb.desp)
        prb.targetDicKey = loadData(setting, "targetDicKey", prb.targetDicKey)
        prb.targetBonePath = loadData(setting, "targetBonePath", prb.targetBonePath)
        # runtime
        try:
            prb.vnObject, rootGO = PhyCollider.GetVnObjAndRootGoFromDicKey(prb.targetDicKey)
            prb.rigidBodyGO = PhyCollider.GetTgtGoFromRootByPath(rootGO, prb.targetBonePath)
            # create rigidBody
            prb.createRigidBody()
            # restore rigidbody setting
            prb.rigidBody.mass = loadData(setting, "mass", prb.rigidBody.mass)
            prb.rigidBody.drag = loadData(setting, "drag", prb.rigidBody.drag)
            prb.rigidBody.angularDrag = loadData(setting, "angularDrag", prb.rigidBody.angularDrag)
            prb.rigidBody.useGravity = loadData(setting, "useGravity", False)
            prb.rigidBody.isKinematic = loadData(setting, "isKinematic", prb.rigidBody.isKinematic)
            #prb.rigidBody.constraints = loadData(setting, "constraints", 0)
            #prb.rigidBody.height = loadData(setting, "height", 2.0)
            #prb.rigidBody.direction = loadData(setting, "direction", 1)
            if len(prb.desp) == 0:
                prb.desp = prb.vnObject.text_name
            return prb
        except Exception as e:
            traceback.print_exc()
            print("Failed in creating rigidbody: " + str(e))
            return None


# ====================================  Trigger classes ====================================
class PhyTrigger(object):
    TRIGGER_TYPES = ("Click", "Input", "Collision", "SceneEvent", "Manual")
    TRIGGER_CLICK_TYPES = ("any", "sel", "exd")
    TRIGGER_EVENT_TYPES = ("Collision", "Trigger")
    TRIGGER_TIMING_TYPES = ("Enter", "Stay", "Exit")
    TRIGGER_INPUT_TYPES = ("Down", "Hold", "Up")
    TRIGGER_SCENEEVENT_TYPES = ("Load", "Import", "Save", "Update")

    @property
    def title(self):
        return self.id + ": " + self.desp

    def __init__(self):
        self.id = ""
        self.desp = ""
        self.type = ""
        self.enable = True
        self.interval = 0
        self.options = {}
        self.actions = []
        # runtime
        self.cdTimer = 0
        self.keycodeValues = None

    def __str__(self):
        return "PhyTrigger %s (%s)"%(self.title, self.type)

    def copy(self):
        nt = PhyTrigger()
        nt.id = self.id
        nt.desp = self.desp
        nt.type = self.type
        nt.enable = self.enable
        nt.interval = self.interval
        nt.options = copy.deepcopy(self.options)
        nt.actions = copy.deepcopy(self.actions)
        return nt

    def update(self, newTrigger):
        self.desp = newTrigger.desp
        self.type = newTrigger.type
        self.enable = newTrigger.enable
        self.interval = newTrigger.interval
        self.options = copy.deepcopy(newTrigger.options)
        self.actions = copy.deepcopy(newTrigger.actions)        
        # runtime
        self.cdTimer = 0
        self.keycodeValues = copy.copy(newTrigger.keycodeValues)

    def exportSetting(self):
        setting = {}
        # basic data
        setting["version"] = "1"
        setting["id"] = self.id
        setting["desp"] = self.desp
        setting["type"] = self.type
        setting["enable"] = self.enable
        setting["interval"] = self.interval
        setting["options"] = self.options
        setting["actions"] = self.actions
        return setting

    def importSetting(self, setting):
        def loadData(settingDict, key, defaultValue = None):
            return settingDict[key] if key in settingDict else defaultValue
        self.id = loadData(setting, "id", self.id)
        self.desp = loadData(setting, "desp", self.desp)
        self.type = loadData(setting, "type", self.type)
        self.enable = loadData(setting, "enable", self.enable)
        self.interval = loadData(setting, "interval", self.interval)
        self.options = loadData(setting, "options", self.options)
        self.actions = loadData(setting, "actions", self.actions)
        # check after import
        err = self.checkSetting()
        if err:
            raise Exception(err)

    @staticmethod
    def CreateFromSetting(setting):
        try:
            trg = PhyTrigger()
            trg.importSetting(setting)
            return trg
        except Exception as e:
            print("Error in creating physics trigger setting: " + str(e))
            return None

    def checkSetting(self):
        # return error message if failed
        # type
        if self.type not in PhyTrigger.TRIGGER_TYPES:
            return "Unknown type of trigger"
        # interval
        if self.interval < 0:
            self.interval = -1
        # options
        if self.type == "Click":
            # click options
            if "colliders" not in self.options:
                return "colliders setting missed in options"
            if "clickTarget" not in self.options:
                return "clickTarget setting missed in options"
            if "mouseButton" not in self.options:
                return "mouseButton setting missed in options"
            if len(self.options["mouseButton"]) == 0:
                return "No mouse button assigned"
            if self.options["clickTarget"] not in PhyTrigger.TRIGGER_CLICK_TYPES:
                return "Unknown type of click target"
            if self.options["clickTarget"] == "sel" and len(self.options["colliders"]) == 0:
                return "No collider selected"
        elif self.type == "Collision":
            # click options
            if "colliders" not in self.options:
                return "colliders setting missed in options"
            if "event" not in self.options:
                return "event setting missed in options"
            if "timing" not in self.options:
                return "timing setting missed in options"
            if len(self.options["colliders"]) == 0:
                return "No collider selected"
            if self.options["event"] not in PhyTrigger.TRIGGER_EVENT_TYPES:
                return "No mouse button assigned"
            if self.options["timing"] not in PhyTrigger.TRIGGER_TIMING_TYPES:
                return "Unknown type of click target"
        elif self.type == "Input":
            # key/mouse/button input options
            if "keycodes" not in self.options:
                return "keycodes setting missed in options"
            if "timing" not in self.options:
                return "timing setting missed in options"
            if len(self.options["keycodes"]) == 0:
                return "No keycode assigned"
            self.keycodeValues = []
            for kc in self.options["keycodes"]:
                try:
                    self.keycodeValues.append(str_2_enum(KeyCode, kc))
                except:
                    self.keycodeValues = None
                    return "Unknown keycode '%s'"%kc
            if self.options["timing"] not in PhyTrigger.TRIGGER_INPUT_TYPES:
                return "Unknown input timing"
        elif self.type == "SceneEvent":
            # Scene event options
            if "event" not in self.options:
                return "event setting missed in options"
            if self.options["event"] not in PhyTrigger.TRIGGER_SCENEEVENT_TYPES:
                return "Unknown scene event"
        elif self.type == "Manual":
            pass
        else:
            return "[%s] type trigger not implemented yet"%self.type
        # actions
        if len(self.actions) == 0:
            return "No action assigned"
        return None

    def checkPreCondition(self):
        if not self.enable:
            return False
        if self.cdTimer != 0:
            return False
        return True

    def fireTrigger(self):
        self.cdTimer =  self.interval
        return True

    def coolingDown(self, delta):
        if self.cdTimer > 0:
            self.cdTimer -= delta
            if self.cdTimer < 0:
                self.cdTimer = 0

    def checkTrigger(self, param):
        if not self.checkPreCondition():
            return False
        if self.type == "Click":
            chk = self.checkTriggerClick(param)
            if not chk: return False
        elif self.type == "Input":
            chk = self.checkTriggerInput(param)
            if not chk: return False
        else:
            # no check for other types of trigger
            pass
        # fire
        return self.fireTrigger()

    def checkTriggerClick(self, param):
        try:
            # mouse button
            if param["mouseButton"] not in self.options["mouseButton"]:
                return False
            # target and collider
            clickTarget = self.options["clickTarget"]
            if clickTarget == "any":
                return True
            elif clickTarget == "sel":
                return param["colliderHitted"] and param["colliderId"] in self.options["colliders"]
            elif clickTarget == "exd":
                if len(self.options["colliders"]) == 0:
                    return not param["colliderHitted"]
                else:
                    return not param["colliderHitted"] or param["colliderId"] not in self.options["colliders"]
            else:
                raise Exception("Unknown click target type")
        except:
            traceback.print_exc()
            return False

    def checkTriggerInput(self, param):
        try:
            for kcv in self.keycodeValues:
                if self.options["timing"] == "Down":
                    chk = Input.GetKeyDown(kcv)
                elif self.options["timing"] == "Hold":
                    chk = Input.GetKey(kcv)
                elif self.options["timing"] == "Up":
                    chk = Input.GetKeyUp(kcv)
                else:
                    raise Exception("Unknown input timing type")
                if chk:
                    param["keycode"] = kcv
                    return True
            return False
        except:
            traceback.print_exc()
            return False

    def export_full_status(self):
        # for clip support
        return vnphy_export_full_status()


# ====================================  Action classes ====================================
class VNAction(object):
    ACTION_TYPES = ("PYTHON", "VNSS")
    SCENE_PYTHON_ALLOWED = False
    VNSS_EXT_MODULES = None
    DEBUG_PRINT_RUN_LOG = False
    DEBUG_PRINT_CALLER_INFO = False
    DEBUG_PRINT_INPUT_PARAM = False

    @property
    def title(self):
        return self.id + ": " + self.desp

    def __init__(self):
        self.id = ""
        self.desp = ""
        self.type = ""
        self.script = ""
        self.enable = True
        # runtime
        self.vnssActs = None
        self.pythonDisableWarning = False

    def __str__(self):
        return "VNAction %s (%s)"%(self.title, self.type)

    def copy(self):
        na = VNAction()
        na.id = self.id
        na.desp = self.desp
        na.type = self.type
        na.script = self.script
        na.enable = self.enable
        return na

    def update(self, newAction):
        self.desp = newAction.desp
        self.type = newAction.type
        self.script = newAction.script
        self.enable = newAction.enable
        # runtime
        self.vnssActs = None
        self.pythonDisableWarning = False

    def exportSetting(self):
        setting = {}
        # basic data
        setting["version"] = "1"
        setting["id"] = self.id
        setting["desp"] = self.desp
        setting["type"] = self.type
        setting["script"] = self.script
        setting["enable"] = self.enable
        return setting

    def importSetting(self, setting):
        def loadData(settingDict, key, defaultValue = None):
            return settingDict[key] if key in settingDict else defaultValue
        self.id = loadData(setting, "id", self.id)
        self.desp = loadData(setting, "desp", self.desp)
        self.type = loadData(setting, "type", self.type)
        self.script = loadData(setting, "script", self.script)
        self.enable = loadData(setting, "enable", self.enable)

    @staticmethod
    def CreateFromSetting(setting):
        try:
            vpa = VNAction()
            vpa.importSetting(setting)
            return vpa
        except Exception as e:
            print("Error in creating VNAction from setting: " + str(e))
            return None

    def checkAction(self, checkId):
        from vnframe import verifyTagName
        ide = verifyTagName(self.id)
        if checkId and ide != None:
            return ide
        if self.type not in VNAction.ACTION_TYPES:
            return "Unknown type of action"
        self.script = self.script.strip()
        #if len(self.script) == 0:
        #    return "No script"
        if self.type == "VNSS":
            verr = self.parseVNSS()
            if verr: return verr
        return None
    
    def parseVNSS(self):
        from scenesavestate_vnssext import analysis_vnss_cmd
        errs = []
        acts = []
        
        acmds = analysis_vnss_cmd(self.script)
        for vncmd in acmds:
            # check error
            if vncmd["error"]:
                errs.append(vncmd["desp"])
            # parse act
            ar = vncmd["org"].split(":", 3)
            act = {}
            act["origintext"] = vncmd["org"]
            act["action"] = ar[0]
            if len(ar) > 1:
                act["actionparam"] = ar[1]
                if len(ar) > 2:
                    act["actionparam2"] = ar[2]
                    if len(ar) > 3:
                        act["actionparam3"] = ar[3]
            acts.append(act)
        
        self.vnssActs = acts
        if len(errs):
            return "\n".join(errs)
        else:
            return None

    def run(self, game, callerInfo = {}, inputParam = {}):
        if not self.enable:
            return "Action disabled"
        if VNAction.DEBUG_PRINT_RUN_LOG:
            print("Action %s run"%self.id)
        if VNAction.DEBUG_PRINT_CALLER_INFO:
            print("  [CALLER INFO]")
            for k in sorted(callerInfo.keys()):
                print("    %s: %s"%(k, str(callerInfo[k])))
        if VNAction.DEBUG_PRINT_INPUT_PARAM:
            print("  [INPUT PARAM]")
            for k in sorted(inputParam.keys()):
                print("    %s: %s"%(k, str(inputParam[k])))
        try:
            if self.type == "PYTHON":
                if VNAction.SCENE_PYTHON_ALLOWED:
                    localParam = copy.copy(callerInfo)
                    localParam.update(inputParam)
                    exec(self.script, {"game": game}, localParam)
                elif self.pythonDisableWarning == False:
                    game.logger_log_message("Python type action [%s] disabled due to security reasons."%self.title)
                    self.pythonDisableWarning = True
            elif self.type == "VNSS":
                from vnscenescript import run_action
                if self.vnssActs == None:
                    self.parseVNSS()
                #if hasattr(game.scenedata, "scScriptExts"):
                #    sebackup = game.scenedata.scScriptExts
                #else:
                #    sebackup = []
                game.scenedata.scScriptExts = self.ListAllVnssExtModule()
                for vact in self.vnssActs:
                    run_action(game, vact)
                #game.scenedata.scScriptExts = sebackup
            return None
        except Exception as e:
            print("\nAction <%s> running into exception!"%self.title)
            traceback.print_exc()
            return str(e)

    def export_full_status(self):
        # for clip support
        return vnphy_export_full_status()

    @staticmethod
    def ListAllVnssExtModule(force = False):
        if VNAction.VNSS_EXT_MODULES == None or force:
            print("Collect all VNSS Ext module:")
            from vngameengine import import_or_reload
            pydirname = os.path.dirname(__file__)
            VNAction.VNSS_EXT_MODULES = []
            extMdlfiles = [f for f in os.listdir(pydirname) if f.lower().startswith("vnscenescriptext_") and f.lower().endswith(".py")]
            for emf in extMdlfiles:
                mdlName = os.path.splitext(emf)[0]
                try:
                    mod = import_or_reload(mdlName)
                    VNAction.VNSS_EXT_MODULES.append(mod)
                    print("  - " + mdlName + " ... Loaded")
                except:
                    print("  - " + mdlName + " ... Failed")
            print("")
        return VNAction.VNSS_EXT_MODULES


# ==================================== Dummy Action function ====================================
def vnphy_export_full_status():
    # for clip support, get keyframe info. FrameNo maybe not correct
    from vngameengine import vnge_game as game
    if hasattr(game.scenedata, "kfamOnModifyClip") and game.scenedata.kfamOnModifyClip != None:
        cid = game.scenedata.kfamOnModifyClip.name + "#" + str(int(game.scenedata.kfamOnModifyClip.curFrame))
    else:
        cid = ""
    return {"fire": cid}


def vnphy_fire(target, param):
    # target is the PhyTrigger/VNAction object to run
    # param is send to trigger or action without check its value
    from vngameengine import vnge_game as game
    if not hasattr(game.gdata, "vnphy"):
        print("Unexpected vnphy_fire call! vnphy is not")
        return

    if isinstance(target, PhyTrigger):
        callerInfo = {"caller": "vnanime_trigger", "keyframe": param}
        game.gdata.vnphy.fireTrigger(target.id, callerInfo, {}, False)
    elif isinstance(target, VNAction):
        callerInfo = {"caller": "vnanime_action", "keyframe": param}
        game.gdata.vnphy.runAction(target.id, callerInfo, {})
    else:
        print("vnphy_fire called with wrong param (%s, %s)"%(str(target), str(param)))

vnphy_act_funcs = {
    'fire': (vnphy_fire, False, "force", "T"),
}

vnphy_act_update_sequence = (
    'fire',
)