using System.Collections;
using System.Collections.Generic;
using UnityEngine;


namespace BNG {

    [ExecuteInEditMode]
    public class HandPoser : MonoBehaviour {
        public bool ShowGizmos = true;

        [Tooltip("Path of the directory where handposes should be stored. Tip : Keep these in a 'Resources' directory so you can use Resources.Load().")]
        public string ResourcePath = "Assets/BNG Framework/HandPoser/Poses/Resources/";

        public string PoseName = "Default";

        [Tooltip("The currently selected hand pose. Change this to automatically update the pose in Update")]
        public HandPose CurrentPose;

        [Header("Animation Properties")]
        [Tooltip("The speed at which to lerp the bones when changing hand poses")]
        public float AnimationSpeed = 15f;

        [Tooltip("If true the local rotation of each bone will be updated while changing hand poses. This should generally be true if you are adjusting a hand pose.")]
        public bool UpdateJointRotations = true;

        [Tooltip("If true the local position of each bone will be updated while changing hand poses. Typically this will be false as joints only adjust their rotations.")]
        public bool UpdateJointPositions = false;

        [Tooltip("If true the local position of the wrist will be updated. Useful if you need to move the entire hand.")]
        public bool UpdateWristPosition = true;

        // Hand Pose Transform Definitions
        public HandPoseDefinition HandPoseJoints {
            get {
                return GetHandPoseDefinition();
            }
        }

        public Transform WristJoint;
        public List<Transform> ThumbJoints;
        public List<Transform> IndexJoints;
        public List<Transform> MiddleJoints;
        public List<Transform> RingJoints;
        public List<Transform> PinkyJoints;
        public List<Transform> OtherJoints;

        HandPose previousPose;
        bool doSingleAnimation;

        // Continuously update pose state
        public bool ContinuousUpdate = false;

        void Start() {
            // Trigger a pose change to start the animation
            OnPoseChanged();
        }

        // This is also run in the editor
        void Update() {

            // Check for pose change event
            CheckForPoseChange();

            // Lerp to Hand Pose
            if (ContinuousUpdate || doSingleAnimation) {
                DoPoseUpdate();
            }
        }

        public void CheckForPoseChange() {
            if (previousPose == null || (CurrentPose != null && previousPose != null && previousPose.name != CurrentPose.name && CurrentPose != null)) {
                OnPoseChanged();
                previousPose = CurrentPose;
            }
        }

        public void OnPoseChanged() {

            // Allow pose to change animation
            editorAnimationTime = 0;
            doSingleAnimation = true;
        }

        public FingerJoint GetWristJoint() {
            return GetJointFromTransform(WristJoint);
        }

        public List<FingerJoint> GetThumbJoints() {
            return GetJointsFromTransforms(ThumbJoints);
        }

        public List<FingerJoint> GetIndexJoints() {
            return GetJointsFromTransforms(IndexJoints);
        }

        public List<FingerJoint> GetMiddleJoints() {
            return GetJointsFromTransforms(MiddleJoints);
        }

        public List<FingerJoint> GetRingJoints() {
            return GetJointsFromTransforms(RingJoints);
        }

        public List<FingerJoint> GetPinkyJoints() {
            return GetJointsFromTransforms(PinkyJoints);
        }

        public List<FingerJoint> GetOtherJoints() {
            return GetJointsFromTransforms(OtherJoints);
        }

        public int GetTotalJointsCount() {

            if(ThumbJoints == null || IndexJoints == null) {
                return 0;
            }

            return ThumbJoints.Count + IndexJoints.Count + MiddleJoints.Count + RingJoints.Count + PinkyJoints.Count + OtherJoints.Count + (WristJoint != null ? 1 : 0);
        }

        public Transform GetTip(List<Transform> transforms) {
            if(transforms != null) {
                return transforms[transforms.Count - 1];
            }

            return null;
        }

        public Transform GetThumbTip() { return GetTip(ThumbJoints); }
        public Transform GetIndexTip() { return GetTip(IndexJoints); }
        public Transform GetMiddleTip() { return GetTip(MiddleJoints); }
        public Transform GetRingTip() { return GetTip(RingJoints); }
        public Transform GetPinkyTip() { return GetTip(PinkyJoints); }

        public static Vector3 GetFingerTipPositionWithOffset(List<Transform> jointTransforms, float tipRadius) {

            if(jointTransforms == null || jointTransforms.Count == 0) {
                return Vector3.zero;
            }

            // Not available
            if(jointTransforms[jointTransforms.Count - 1] == null) {
                return Vector3.zero;
            }

            Vector3 tipPosition = jointTransforms[jointTransforms.Count - 1].position;

            if(jointTransforms.Count == 1) {
                return tipPosition;
            }

            return tipPosition + (jointTransforms[jointTransforms.Count - 2].position - tipPosition).normalized * tipRadius;
        }

        public virtual List<FingerJoint> GetJointsFromTransforms(List<Transform> jointTransforms) {
            List<FingerJoint> joints = new List<FingerJoint>();

            // Check for empty joints
            if(jointTransforms == null) {
                return joints;
            }

            // Add any joint information to the list
            for (int x = 0; x < jointTransforms.Count; x++) {
                if(jointTransforms[x] != null) {
                    joints.Add(GetJointFromTransform(jointTransforms[x]));
                }
            }

            return joints;
        }

        public virtual FingerJoint GetJointFromTransform(Transform jointTransform) {
            
            // Null check
            if(jointTransform == null) {
                return null;
            }

            return new FingerJoint() {
                TransformName = jointTransform.name,
                LocalPosition = jointTransform.localPosition,
                LocalRotation = jointTransform.localRotation
            };
        }

        public virtual void UpdateHandPose(HandPose handPose, bool lerp) {
            UpdateHandPose(handPose.Joints, lerp);
        }

        public virtual void UpdateHandPose(HandPoseDefinition pose, bool lerp) {
            UpdateJoint(pose.WristJoint, WristJoint, lerp, UpdateWristPosition, UpdateJointRotations);
            UpdateJoints(pose.ThumbJoints, ThumbJoints, lerp);
            UpdateJoints(pose.IndexJoints, IndexJoints, lerp);
            UpdateJoints(pose.MiddleJoints, MiddleJoints, lerp);
            UpdateJoints(pose.RingJoints, RingJoints, lerp);
            UpdateJoints(pose.PinkyJoints, PinkyJoints, lerp);
            UpdateJoints(pose.OtherJoints, OtherJoints, lerp);
        }

        public virtual void UpdateJoint(FingerJoint fromJoint, Transform toTransform, bool doLerp, bool updatePosition, bool updateRotation) {
            UpdateJoint(fromJoint, toTransform, doLerp ? Time.deltaTime * AnimationSpeed : 1, updatePosition, updateRotation);
        }

        public virtual void UpdateJoint(FingerJoint fromJoint, Transform toTransform, float lerpAmount, bool updatePosition, bool updateRotation) {

            // Invalid joint
            if (fromJoint == null || toTransform == null) {
                return;
            }

            // Lerp
            if (lerpAmount != 1) {
                if(updatePosition) {
                    toTransform.localPosition = Vector3.Lerp(toTransform.localPosition, fromJoint.LocalPosition, lerpAmount);
                }
                if(UpdateJointRotations) {
                    toTransform.localRotation = Quaternion.Lerp(toTransform.localRotation, fromJoint.LocalRotation, lerpAmount);
                }
            }
            // Instantaneous can directly set the local position and rotation
            else {
                if (UpdateJointPositions) {
                    toTransform.localPosition = fromJoint.LocalPosition;
                }
                if (UpdateJointRotations) {
                    toTransform.localRotation = fromJoint.LocalRotation;
                }
            }
        }

        public virtual void UpdateJoints(List<FingerJoint> joints, List<Transform> toTransforms, bool doLerp) {
            UpdateJoints(joints, toTransforms, doLerp ? Time.deltaTime * AnimationSpeed : 1);
        }

        public virtual void UpdateJoints(List<FingerJoint> joints, List<Transform> toTransforms, float lerpAmount) {

            // Sanity check
            if (joints == null || toTransforms == null) {
                return;
            }

            // Cache the count of our lists
            int jointCount = joints.Count;
            int transformsCount = toTransforms.Count;

            // If our joint counts don't add up, then make sure the names match before applying any changes
            // Otherwise there is a good chance the wwrong transforms are being updated
            bool verifyTransformName = jointCount != transformsCount;
            for (int x = 0; x < jointCount; x++) {
                // Make sure the indexes match
                if (x < toTransforms.Count) {

                    // Joint may have not been assigned or destroyed
                    if (joints[x] == null || toTransforms[x] == null) {
                        continue;
                    }

                    if (verifyTransformName && joints[x].TransformName == toTransforms[x].name) {
                        UpdateJoint(joints[x], toTransforms[x], lerpAmount, UpdateJointPositions, UpdateJointRotations);
                    }
                    else if (verifyTransformName == false) {
                        UpdateJoint(joints[x], toTransforms[x], lerpAmount, UpdateJointPositions, UpdateJointRotations);
                    }
                }
            }
        }

        public virtual HandPoseDefinition GetHandPoseDefinition() {
            return new HandPoseDefinition() {
                WristJoint = GetWristJoint(),
                ThumbJoints = GetThumbJoints(),
                IndexJoints = GetIndexJoints(),
                MiddleJoints = GetMiddleJoints(),
                RingJoints = GetRingJoints(),
                PinkyJoints = GetPinkyJoints(),
                OtherJoints = GetOtherJoints()
            };
        }

        /// <summary>
        /// Saves the current hand configuration as a Unity Scriptable object 
        /// Pose will be saved to the provided 'HandPosePath' (Typically within in a Resources folder)
        /// *Scriptable Objects can only be saved from within the UnityEditor.
        /// </summary>
        /// <param name="poseName"></param>
        /// <returns></returns>
        public virtual void SavePoseAsScriptablObject(string poseName) {
#if UNITY_EDITOR
            
            string fileName = poseName + ".asset";

            var poseObject = GetHandPoseScriptableObject();

            // Creates the file in the folder path
            //string fullPath = Application.dataPath + directory + fileName;
            string fullPath = ResourcePath + fileName;            
            bool exists = System.IO.File.Exists(fullPath);

            // Delete if asset already exists
            if (exists) {
                UnityEditor.AssetDatabase.DeleteAsset(fullPath);
            }

            UnityEditor.AssetDatabase.CreateAsset(poseObject, fullPath);

            UnityEditor.AssetDatabase.SaveAssets();

            UnityEditor.AssetDatabase.Refresh(UnityEditor.ImportAssetOptions.ForceUpdate);

            UnityEditor.EditorUtility.SetDirty(poseObject);

            if (exists) {
                Debug.Log("Updated Hand Pose : " + poseName);
            }
            else {
                Debug.Log("Created new Hand Pose : " + poseName);
            }
#else
    Debug.Log("Scriptable Objects can only be saved from within the Unity Editor. Consider storing in another format like JSON instead.");
#endif
        }

        /// <summary>
        /// Will create an original handpose with the specified PoseName. If the provided poseName is already taken, a number will be added to the name until a unique name is found.
        /// For example, if you provide the PoseName of "Default" and there are already two poses named "Default" and "Default 1", then a new pose named "Default 2" will be created.
        /// </summary>
        /// <param name="poseName"></param>
        public virtual void CreateUniquePose(string poseName) {

            // Don't allow empty pose names
            if(string.IsNullOrEmpty(poseName)) {
                poseName = "Pose";
            }

            string formattedPoseName = poseName;
            string fullPath = ResourcePath + formattedPoseName + ".asset";
            bool exists = System.IO.File.Exists(fullPath);
            int checkCount = 0;
            // Find a path that doesn't exist
            while(exists) {
                // Ex : "path/Pose 5.asset"
                formattedPoseName = poseName + " " + checkCount;
                exists = System.IO.File.Exists(ResourcePath + formattedPoseName + ".asset");
                
                checkCount++;
            }

            // Save the new pose
            SavePoseAsScriptablObject(formattedPoseName);
        }

        public virtual HandPose GetHandPoseScriptableObject() {
#if UNITY_EDITOR
            var poseObject = UnityEditor.Editor.CreateInstance<HandPose>();
            poseObject.PoseName = PoseName;

            poseObject.Joints = new HandPoseDefinition();
            poseObject.Joints.WristJoint = GetWristJoint();
            poseObject.Joints.ThumbJoints = GetThumbJoints();
            poseObject.Joints.IndexJoints = GetIndexJoints();
            poseObject.Joints.MiddleJoints = GetMiddleJoints();
            poseObject.Joints.RingJoints = GetRingJoints();
            poseObject.Joints.PinkyJoints = GetPinkyJoints();
            poseObject.Joints.OtherJoints = GetOtherJoints();

            return poseObject;
#else
    return null;
#endif
        }


        // How long to check for animations while in the editor mode
        float editorAnimationTime = 0f;
        float maxEditorAnimationTime = 2f;

        public virtual void DoPoseUpdate() {

            if (CurrentPose != null) {
                UpdateHandPose(CurrentPose.Joints, true);
            }

            // Are we done requesting a single animation?
            if (doSingleAnimation) {
                editorAnimationTime += Time.deltaTime;

                // Reset
                if (editorAnimationTime >= maxEditorAnimationTime) {
                    editorAnimationTime = 0;
                    doSingleAnimation = false;
                }
            }
        }

        public virtual void ResetEditorHandles() {
            EditorHandle[] handles = GetComponentsInChildren<EditorHandle>();
            for (int x = 0; x < handles.Length; x++) {
                if(handles[x] != null && handles[x].gameObject != null) {
                    GameObject.DestroyImmediate((handles[x]));
                }
            }
        }

        void OnDrawGizmos() {
#if UNITY_EDITOR
            // Update every frame even while in editor
            if (!Application.isPlaying) {
                UnityEditor.EditorApplication.QueuePlayerLoopUpdate();
                UnityEditor.SceneView.RepaintAll();
            }
#endif
        }
    }
}