using System;
using System.Collections.Generic;
using UnityEngine.Scripting.APIUpdating;
using UnityEngine.Serialization;
using UnityEngine.U2D.Common;

namespace UnityEngine.U2D.IK
{
    /// <summary>
    /// Abstract class for implementing a 2D IK Solver.
    /// </summary>
    [MovedFrom("UnityEngine.Experimental.U2D.IK")]
    public abstract class Solver2D : MonoBehaviour, IPreviewable
    {
        [SerializeField]
        bool m_ConstrainRotation = true;

        [FormerlySerializedAs("m_RestoreDefaultPose")]
        [SerializeField]
        bool m_SolveFromDefaultPose = true;

        [SerializeField]
        [Range(0f, 1f)]
        float m_Weight = 1f;

        Plane m_Plane;
        List<Vector3> m_TargetPositions = new List<Vector3>();

        /// <summary>
        /// Used to evaluate if Solver2D needs to be updated.
        /// </summary>
        float m_LastFinalWeight;

        /// <summary>
        /// Returns the number of IKChain2D in the solver.
        /// </summary>
        public int chainCount => GetChainCount();

        /// <summary>
        /// Gets and sets the rotation constrain property.
        /// </summary>
        public bool constrainRotation
        {
            get => m_ConstrainRotation;
            set => m_ConstrainRotation = value;
        }

        /// <summary>
        /// Get and set restoring default pose before the update.
        /// </summary>
        public bool solveFromDefaultPose
        {
            get => m_SolveFromDefaultPose;
            set => m_SolveFromDefaultPose = value;
        }

        /// <summary>
        /// Returns true if the Solver2D is in a valid state.
        /// </summary>
        public bool isValid => Validate();

        /// <summary>
        /// Returns true if all chains in the Solver have a target.
        /// </summary>
        public bool allChainsHaveTargets => HasTargets();

        /// <summary>
        /// Get and Set Solver weights.
        /// </summary>
        public float weight
        {
            get => m_Weight;
            set => m_Weight = Mathf.Clamp01(value);
        }

        /// <summary>
        /// Validate and initialize the Solver.
        /// </summary>
        protected virtual void OnValidate()
        {
            m_Weight = Mathf.Clamp01(m_Weight);

            if (!isValid)
                Initialize();
        }

        bool Validate()
        {
            for (var i = 0; i < GetChainCount(); ++i)
            {
                var chain = GetChain(i);
                if (!chain.isValid)
                    return false;
            }

            return DoValidate();
        }

        bool HasTargets()
        {
            for (var i = 0; i < GetChainCount(); ++i)
            {
                var chain = GetChain(i);
                if (chain.target == null)
                    return false;
            }

            return true;
        }

        /// <summary>
        /// Initializes the solver.
        /// </summary>
        public void Initialize()
        {
            DoInitialize();

            for (var i = 0; i < GetChainCount(); ++i)
            {
                var chain = GetChain(i);
                chain.Initialize();
            }
        }

        void Prepare()
        {
            var rootTransform = GetPlaneRootTransform();
            if (rootTransform != null)
            {
                m_Plane.normal = rootTransform.forward;
                m_Plane.distance = -Vector3.Dot(m_Plane.normal, rootTransform.position);
            }

            for (var i = 0; i < GetChainCount(); ++i)
            {
                var chain = GetChain(i);
                var constrainTargetRotation = constrainRotation && chain.target != null;

                if (m_SolveFromDefaultPose)
                    chain.RestoreDefaultPose(constrainTargetRotation);
            }

            DoPrepare();
        }

        void PrepareEffectorPositions()
        {
            m_TargetPositions.Clear();

            for (var i = 0; i < GetChainCount(); ++i)
            {
                var chain = GetChain(i);

                if (chain.target)
                    m_TargetPositions.Add(chain.target.position);
            }
        }

        /// <summary>
        /// Perform the Solver IK update.
        /// </summary>
        /// <param name="globalWeight">Weight for position solving.</param>
        public void UpdateIK(float globalWeight)
        {
            if (allChainsHaveTargets)
            {
                PrepareEffectorPositions();
                UpdateIK(m_TargetPositions, globalWeight);
            }
        }

        /// <summary>
        /// Perform the Solver IK update with specified target positions.
        /// </summary>
        /// <param name="targetPositions">Target positions.</param>
        /// <param name="globalWeight">Weight for position solving.</param>
        public void UpdateIK(List<Vector3> targetPositions, float globalWeight)
        {
            if (targetPositions.Count != chainCount)
                return;

            var finalWeight = globalWeight * weight;
            var weightValueChanged = Math.Abs(finalWeight - m_LastFinalWeight) > 0.0001f;
            m_LastFinalWeight = finalWeight;

            if (finalWeight == 0f && !weightValueChanged)
                return;

            if (!isValid)
                return;

            if (finalWeight < 1f)
                StoreLocalRotations();

            Prepare();

            DoUpdateIK(targetPositions);

            if (constrainRotation)
            {
                for (var i = 0; i < GetChainCount(); ++i)
                {
                    var chain = GetChain(i);

                    if (chain.target)
                        chain.effector.rotation = chain.target.rotation;
                }
            }

            if (finalWeight < 1f)
                BlendFkToIk(finalWeight);
        }

        void StoreLocalRotations()
        {
            for (var i = 0; i < GetChainCount(); ++i)
            {
                var chain = GetChain(i);
                chain.StoreLocalRotations();
            }
        }

        void BlendFkToIk(float finalWeight)
        {
            for (var i = 0; i < GetChainCount(); ++i)
            {
                var chain = GetChain(i);
                var constrainTargetRotation = constrainRotation && chain.target != null;
                chain.BlendFkToIk(finalWeight, constrainTargetRotation);
            }
        }

        /// <summary>
        /// Override to return the IKChain2D at the given index.
        /// </summary>
        /// <param name="index">Index of the IKChain2D.</param>
        /// <returns></returns>
        public abstract IKChain2D GetChain(int index);

        /// <summary>
        /// Override to return the number of chains in the Solver.
        /// </summary>
        /// <returns>Number of chains in the solver.</returns>
        protected abstract int GetChainCount();

        /// <summary>
        /// Override to perform Solver IK update.
        /// </summary>
        /// <param name="targetPositions">Target position for the chain.</param>
        protected abstract void DoUpdateIK(List<Vector3> targetPositions);

        /// <summary>
        /// Override to perform custom validation.
        /// </summary>
        /// <returns>Returns true if the Solver is in a valid state. False otherwise.</returns>
        protected virtual bool DoValidate() => true;

        /// <summary>
        /// Override to initialize the solver.
        /// </summary>
        protected virtual void DoInitialize() { }

        /// <summary>
        /// Override to prepare the solver for update.
        /// </summary>
        protected virtual void DoPrepare() { }

        /// <summary>
        /// Override to return the root transform of the Solver. The default implementation returns the root transform of the first chain.
        /// </summary>
        /// <returns>Transform representing the root.</returns>
        protected virtual Transform GetPlaneRootTransform()
        {
            return chainCount > 0 ? GetChain(0).rootTransform : null;
        }

        /// <summary>
        /// Convert a world position coordinate to the solver's plane space.
        /// </summary>
        /// <param name="worldPosition">Vector3 representing world position</param>
        /// <returns>Converted position in solver's plane</returns>
        protected Vector3 GetPointOnSolverPlane(Vector3 worldPosition)
        {
            return GetPlaneRootTransform().InverseTransformPoint(m_Plane.ClosestPointOnPlane(worldPosition));
        }

        /// <summary>
        /// Convert a position from solver's plane to world coordinates.
        /// </summary>
        /// <param name="planePoint">Vector3 representing a position in the Solver's plane.</param>
        /// <returns>Converted position to world coordinates.</returns>
        protected Vector3 GetWorldPositionFromSolverPlanePoint(Vector2 planePoint)
        {
            return GetPlaneRootTransform().TransformPoint(planePoint);
        }

        /// <summary>
        /// Empty method. Implemented for the IPreviewable interface.
        /// </summary>
        public void OnPreviewUpdate() { }
    }
}