using System;
using UnityEngine;

namespace pdinklag.CPW.Common
{
    /// <summary>
    /// Describes a curve for anim playback controllers.
    /// 
    /// Control curves consist of multiple points between which the curve is linearly interpolated.
    /// Both axes are limited to the [0, 1] range.
    /// The X axis represents the relative time of the current animation playback, 0 being the first frame and 1 being the last.
    /// The Y axis represents the control weight, 0 meaning no effect and 1 meaning 100% effect.
    /// </summary>
    public readonly struct AnimControlCurve
    {
        public static AnimControlCurve operator *(AnimControlCurve curve, float p) => new(curve, p);
        public static AnimControlCurve operator *(float p, AnimControlCurve curve) => new(curve, p);

        /// <summary>
        /// Creates a curve with a constant weight of one.
        /// </summary>
        /// <returns></returns>
        public static AnimControlCurve One => new(new Point(0, 1));

        /// <summary>
        /// Creates a rising curve.
        /// 
        /// It remains at zero up to the start time, then rises to one by the end time.
        /// </summary>
        /// <param name="start">the rise start time</param>
        /// <param name="end">the rise end time</param>
        /// <returns></returns>
        public static AnimControlCurve Rise(float start, float end) => new(new Point(start, 0), new Point(end, 1));

        /// <summary>
        /// Creates a falling curve.
        /// 
        /// It remains at one up to the start time, then falls to zero by the end time.
        /// </summary>
        /// <param name="start">the fall start time</param>
        /// <param name="end">the fall end time</param>
        /// <returns></returns>
        public static AnimControlCurve Fall(float start, float end) => new(new Point(start, 1), new Point(end, 0));

        /// <summary>
        /// Creates a peak curve.
        /// 
        /// It remains at zero up to the start time, then rises to one by the peak time and falls back to zero by the end time.
        /// </summary>
        /// <param name="start">the start time</param>
        /// <param name="peak">the peak time</param>
        /// <param name="end">the end time</param>
        /// <returns></returns>
        public static AnimControlCurve Peak(float start, float peak, float end) => new(new Point(start, 0), new Point(peak, 1), new Point(end, 0));

        /// <summary>
        /// Creates a trough curve.
        /// 
        /// It remains at one up to the start time, then falls to zero by the trough time and rises back to one by the end time.
        /// </summary>
        /// <param name="start">the start time</param>
        /// <param name="trough">the trough time</param>
        /// <param name="end">the end time</param>
        /// <returns></returns>
        public static AnimControlCurve Trough(float start, float trough, float end) => new(new Point(start, 1), new Point(trough, 0), new Point(end, 1));

        /// <summary>
        /// Creates an impulse curve.
        /// 
        /// It remains at zero up to the start time, then rises to one by the peak start time, holds it until the peak end time, and then falls back to zero by the end time.
        /// </summary>
        /// <param name="start">the start time</param>
        /// <param name="peakStart">the peak start time</param>
        /// <param name="peakEnd">the peak end time</param>
        /// <param name="end">the end time</param>
        /// <returns></returns>
        public static AnimControlCurve Impulse(float start, float peakStart, float peakEnd, float end) => new(new Point(start, 0), new Point(peakStart, 1), new Point(peakEnd, 1), new Point(end, 0));

        /// <summary>
        /// Creates an inverse impulse curve.
        /// 
        /// It remains at one up to the start time, then falls to zero by the trough start time, holds it until the trough end time, and then rises back to one by the end time.
        /// </summary>
        /// <param name="start">the start time</param>
        /// <param name="troughStart">the trough start time</param>
        /// <param name="troughEnd">the trough end time</param>
        /// <param name="end">the end time</param>
        /// <returns></returns>
        public static AnimControlCurve InverseImpulse(float start, float troughStart, float troughEnd, float end) => new(new  Point(start, 1), new  Point(troughStart, 0), new  Point(troughEnd, 0), new  Point(end, 1));

        /// <summary>
        /// Represents a point in a control curve.
        /// </summary>
        public readonly struct Point
        {
            public readonly float Time;
            public readonly float Weight;

            public Point(float time, float weight)
            {
                Time = time;
                Weight = weight;
            }
        }

        /// <summary>
        /// The points of the control curve.
        /// </summary>
        public readonly InlineArray<Point> Points;

        public AnimControlCurve(Point p1) : this(new InlineArray<Point>(p1))
        {
        }

        public AnimControlCurve(Point p1, Point p2) : this(new InlineArray<Point>(p1, p2))
        {
        }

        public AnimControlCurve(Point p1, Point p2, Point p3) : this(new InlineArray<Point>(p1, p2, p3))
        {
        }

        public AnimControlCurve(Point p1, Point p2, Point p3, Point p4) : this(new InlineArray<Point>(p1, p2, p3, p4))
        {
        }

        private AnimControlCurve(InlineArray<Point> points)
        {
            Points = points;

            if (points.Length == 0)
            {
                throw new ArgumentException("curve must contain at least one point");
            }

            for (var i = 0; i < points.Length - 1; i++)
            {
                if (points[i].Time >= points[i + 1].Time)
                {
                    throw new ArgumentException("points must be sorted by time and have unique time values");
                }
            }
        }

        private AnimControlCurve(AnimControlCurve curve, float scale)
        {
            Points = new InlineArray<Point>(curve.Points.Length);
            for (var i = 0; i < Points.Length; i++)
            {
                var p = curve.Points[i];
                Points[i] = new Point(p.Time, scale * p.Weight);
            }
        }

        /// <summary>
        /// Evaluates the curve at the given time.
        /// </summary>
        /// <param name="t">the time to evaluate at</param>
        /// <returns>the value at the given time</returns>
        public float Evaluate(float t)
        {
            if (t <= Points[0].Time)
            {
                // before first interval
                return Points[0].Weight;
            }
            else if (t >= Points[Points.Length - 1].Time)
            {
                // beyond last interval
                return Points[Points.Length - 1].Weight;
            }
            else
            {
                // find current interval's left boundary
                var i = 0;
                while (i + 1 < Points.Length && Points[i + 1].Time <= t) ++i;

                // transform t to interval and interpolate
                float tl = Points[i].Time;
                float tr = Points[i + 1].Time;
                return Mathf.Lerp(Points[i].Weight, Points[i + 1].Weight, (t - tl) / (tr - tl));
            }
        }
    }
}
