using System.Collections.Generic;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif

namespace Streamingle.Utils.RoughConstraints
{
    public enum RoughConstraintUpdateMode
    {
        Update,
        LateUpdate,
        FixedUpdate,
        Manual
    }

    [ExecuteAlways]
    public abstract class RoughConstraintBase : MonoBehaviour
    {
        [Header("Constraint")]
        public bool constraintActive = true;
        public bool locked = true;

        [Range(0f, 1f)]
        public float weight = 1f;

        public List<RoughConstraintSource> sources = new List<RoughConstraintSource>();

        [Header("Rough Follow")]
        [Min(0f)]
        public float damping = 12f;

        public RoughConstraintUpdateMode updateMode = RoughConstraintUpdateMode.LateUpdate;
        public bool useUnscaledTime;
        public bool snapOnEnable = true;

#if UNITY_EDITOR
        private bool editorUpdateRegistered;
#endif

        protected float CurrentDeltaTime
        {
            get
            {
                if (!Application.isPlaying)
                {
                    return 1f / 60f;
                }

                if (updateMode == RoughConstraintUpdateMode.FixedUpdate)
                {
                    return useUnscaledTime ? Time.fixedUnscaledDeltaTime : Time.fixedDeltaTime;
                }

                return useUnscaledTime ? Time.unscaledDeltaTime : Time.deltaTime;
            }
        }

        protected float FollowT
        {
            get
            {
                if (damping <= 0f)
                {
                    return 1f;
                }

                return 1f - Mathf.Exp(-damping * Mathf.Max(0f, CurrentDeltaTime));
            }
        }

        protected virtual void OnEnable()
        {
#if UNITY_EDITOR
            RegisterEditorUpdate();
#endif

            if (!snapOnEnable)
            {
                return;
            }

            SnapToSources();
        }

        protected virtual void OnDisable()
        {
#if UNITY_EDITOR
            UnregisterEditorUpdate();
#endif
        }

        private void Update()
        {
            if (!Application.isPlaying)
            {
                if (updateMode != RoughConstraintUpdateMode.Manual)
                {
                    Evaluate();
                }

                return;
            }

            if (updateMode == RoughConstraintUpdateMode.Update)
            {
                Evaluate();
            }
        }

        private void LateUpdate()
        {
            if (!Application.isPlaying)
            {
                return;
            }

            if (updateMode == RoughConstraintUpdateMode.LateUpdate)
            {
                Evaluate();
            }
        }

        private void FixedUpdate()
        {
            if (!Application.isPlaying)
            {
                return;
            }

            if (updateMode == RoughConstraintUpdateMode.FixedUpdate)
            {
                Evaluate();
            }
        }

#if UNITY_EDITOR
        private void RegisterEditorUpdate()
        {
            if (editorUpdateRegistered)
            {
                return;
            }

            EditorApplication.update += EditorUpdate;
            editorUpdateRegistered = true;
        }

        private void UnregisterEditorUpdate()
        {
            if (!editorUpdateRegistered)
            {
                return;
            }

            EditorApplication.update -= EditorUpdate;
            editorUpdateRegistered = false;
        }

        private void EditorUpdate()
        {
            if (this == null)
            {
                UnregisterEditorUpdate();
                return;
            }

            if (Application.isPlaying || updateMode == RoughConstraintUpdateMode.Manual)
            {
                return;
            }

            Evaluate();
        }
#endif

        public void Evaluate()
        {
            if (!CanEvaluate())
            {
                return;
            }

            ExecuteConstraint(FollowT);
        }

        public void SnapToSources()
        {
            if (!CanEvaluate())
            {
                return;
            }

            ExecuteConstraint(1f);
        }

        public void SetLocked(bool value)
        {
            if (value && !locked)
            {
                CaptureOffsetsFromCurrentPose();
            }

            locked = value;
        }

        public void ActivateConstraint()
        {
            constraintActive = true;
            locked = true;
            SnapToSources();
        }

        protected abstract void ExecuteConstraint(float followT);

        protected virtual void CaptureOffsetsFromCurrentPose()
        {
        }

        private bool CanEvaluate()
        {
            return constraintActive && locked && weight > 0f;
        }

        protected bool TryGetWeightedPosition(out Vector3 position)
        {
            position = Vector3.zero;
            var totalWeight = 0f;

            for (var i = 0; i < sources.Count; i++)
            {
                var source = sources[i];
                if (source.sourceTransform == null || source.weight <= 0f)
                {
                    continue;
                }

                position += source.sourceTransform.position * source.weight;
                totalWeight += source.weight;
            }

            if (totalWeight <= 0f)
            {
                return false;
            }

            position /= totalWeight;
            return true;
        }

        protected bool TryGetWeightedRotation(out Quaternion rotation)
        {
            rotation = Quaternion.identity;
            var hasRotation = false;
            var accumulatedWeight = 0f;

            for (var i = 0; i < sources.Count; i++)
            {
                var source = sources[i];
                if (source.sourceTransform == null || source.weight <= 0f)
                {
                    continue;
                }

                var sourceRotation = source.sourceTransform.rotation;
                if (!hasRotation)
                {
                    rotation = sourceRotation;
                    accumulatedWeight = source.weight;
                    hasRotation = true;
                    continue;
                }

                var lerpWeight = source.weight / (accumulatedWeight + source.weight);
                rotation = Quaternion.Slerp(rotation, sourceRotation, lerpWeight);
                accumulatedWeight += source.weight;
            }

            return hasRotation;
        }

        protected bool TryGetWeightedScale(out Vector3 scale)
        {
            scale = Vector3.zero;
            var totalWeight = 0f;

            for (var i = 0; i < sources.Count; i++)
            {
                var source = sources[i];
                if (source.sourceTransform == null || source.weight <= 0f)
                {
                    continue;
                }

                scale += source.sourceTransform.localScale * source.weight;
                totalWeight += source.weight;
            }

            if (totalWeight <= 0f)
            {
                return false;
            }

            scale /= totalWeight;
            return true;
        }

        protected static Vector3 ApplyAxisMask(Vector3 current, Vector3 target, Vector3Bool constrainedAxis)
        {
            if (!constrainedAxis.x) target.x = current.x;
            if (!constrainedAxis.y) target.y = current.y;
            if (!constrainedAxis.z) target.z = current.z;
            return target;
        }

        protected static Vector3 ApplyEulerAxisMask(Quaternion current, Quaternion target, Vector3Bool constrainedAxis)
        {
            var currentEuler = current.eulerAngles;
            var targetEuler = target.eulerAngles;

            if (!constrainedAxis.x) targetEuler.x = currentEuler.x;
            if (!constrainedAxis.y) targetEuler.y = currentEuler.y;
            if (!constrainedAxis.z) targetEuler.z = currentEuler.z;

            return targetEuler;
        }

        protected static Quaternion LookRotationForAxis(Vector3 direction, Vector3 worldUp, Vector3 localForward, Vector3 localUp)
        {
            var safeDirection = SafeNormal(direction, Vector3.forward);
            var safeWorldUp = SafeUp(safeDirection, worldUp);
            var safeLocalForward = SafeNormal(localForward, Vector3.forward);
            var safeLocalUp = SafeUp(safeLocalForward, localUp);

            var targetWorldRotation = Quaternion.LookRotation(safeDirection, safeWorldUp);
            var localAxisRotation = Quaternion.LookRotation(safeLocalForward, safeLocalUp);
            return targetWorldRotation * Quaternion.Inverse(localAxisRotation);
        }

        private static Vector3 SafeNormal(Vector3 value, Vector3 fallback)
        {
            return value.sqrMagnitude > 0.000001f ? value.normalized : fallback;
        }

        private static Vector3 SafeUp(Vector3 forward, Vector3 up)
        {
            var safeUp = SafeNormal(up, Vector3.up);
            if (Mathf.Abs(Vector3.Dot(forward, safeUp)) < 0.999f)
            {
                return safeUp;
            }

            return Mathf.Abs(Vector3.Dot(forward, Vector3.up)) < 0.999f ? Vector3.up : Vector3.right;
        }
    }
}
