using UnityEngine;

namespace pdinklag.CPW.Common
{
    /// <summary>
    /// Deforms a mesh by shifting affected vertices to a retargeted location
    /// and scaling them according to an influence box.
    /// 
    /// The influence on a vertex depends on its distance from the box.
    /// 
    /// The box can be resized to the target body using a lateral, vertical and longitudinal measure.
    /// Influenced vertices are then pushed or pulled towards the outer box's surface accordingly.
    /// </summary>
    public class PropDeformBox : PropDeform
    {
        [Header("Influence Box")]
        [Tooltip("The extents (half-size) of the inner box, which fully influences contained vertices.")]
        public Vector3 InnerExtents = new(0.1f, 0.1f, 0.1f);

        [Tooltip("The radius of the outer box, which influences contained vertices based on their distance from the inner box.")]
        public Vector3 OuterExtents = new(0.2f, 0.2f, 0.2f);

        [Header("Scale Vertices")]
        [Tooltip("The measure used to determine the lateral (x-axis) scaling of an influenced vertex; leave null to disable.")]
        public BodyMeasure ScaleX;

        [Tooltip("The amount of lateral (x-axis) scaling to apply if any (1 meaning full, 0 meaning none).")]
        [Range(0, 1)]
        public float ScaleXAmount = 1;

        [Header("Push Vertices")]
        [Tooltip("The measure used to determine the vertical (y-axis) scaling of an influenced vertex; leave null to disable.")]
        public BodyMeasure ScaleY;

        [Tooltip("The amount of vertical (y-axis) scaling to apply if any (1 meaning full, 0 meaning none).")]
        [Range(0, 1)]
        public float ScaleYAmount = 1;

        [Header("Push Vertices")]
        [Tooltip("The measure used to determine the longitudinal (z-axis) scaling of an influenced vertex; leave null to disable.")]
        public BodyMeasure ScaleZ;

        [Tooltip("The amount of longitudinal (z-axis) scaling to apply if any (1 meaning full, 0 meaning none).")]
        [Range(0, 1)]
        public float ScaleZAmount = 1;

        // precomputed for the current target
        private Vector3 _shift, _aspect;

        private float Influence(Vector3 v, Vector3 pos, out Vector3 vx, out Vector3 vy, out Vector3 vz)
        {
            var xAxis = transform.right;
            var yAxis = transform.up;
            var zAxis = transform.forward;

            var vOrigin = v - pos;
            vx = pos + Vector3.Project(vOrigin, xAxis);
            vy = pos + Vector3.Project(vOrigin, yAxis);
            vz = pos + Vector3.Project(vOrigin, zAxis);

            var xDist = Mathf.Abs(Vector3.Dot(vOrigin, xAxis));
            var yDist = Mathf.Abs(Vector3.Dot(vOrigin, yAxis));
            var zDist = Mathf.Abs(Vector3.Dot(vOrigin, zAxis));

            var extDiff = OuterExtents - InnerExtents;
            return 1.0f - Mathf.Max(
                Mathf.Max(
                    Mathf.Clamp01((xDist - InnerExtents.x) / extDiff.x),
                    Mathf.Clamp01((yDist - InnerExtents.y) / extDiff.y)),
                Mathf.Clamp01((zDist - InnerExtents.z) / extDiff.z));
        }

        public override float Influence(Vector3 v) => Influence(v, transform.position, out _, out _, out _);

        public override void PrepareDeform(BodyDescription target)
        {
            _shift = GetComponent<BodyLocation>().Target(target) - transform.position;
            _aspect = new(
                ScaleX ? ScaleX.Aspect(target) : 1,
                ScaleY ? ScaleY.Aspect(target) : 1,
                ScaleZ ? ScaleZ.Aspect(target) : 1
            );
        }

        protected override Vector3 Deform(Vector3 v, BodyDescription target)
        {
            var pos = transform.position;
            var influence = Influence(v, pos, out var vx, out var vy, out var vz);
            if (influence > 0)
            {
                var xScale = ScaleX ? ScaleXAmount * (_aspect.x - 1.0f) * (vx - pos) : Vector3.zero;
                var yScale = ScaleY ? ScaleYAmount * (_aspect.y - 1.0f) * (vy - pos) : Vector3.zero;
                var zScale = ScaleZ ? ScaleZAmount * (_aspect.z - 1.0f) * (vz - pos) : Vector3.zero;
                return influence * (_shift + xScale + yScale + zScale);
            }
            else
            {
                return Vector3.zero;
            }
        }

#if UNITY_EDITOR
        public virtual void OnDrawGizmosSelected()
        {
            DebugExtension.DrawLocalCube(transform, 2.0f * InnerExtents, Color.cyan);
            DebugExtension.DrawLocalCube(transform, 2.0f * OuterExtents, Color.blue);
        }
#endif
    }
}
