﻿// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

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

namespace HoloToolkit.Unity.UX
{
    public enum InterpolationEnum
    {
        Bezeir,
        CatmullRom,
        Hermite,
    }

    /// <summary>
    /// Default options for getting a rotation along a line
    /// </summary>
    public enum RotationTypeEnum
    {
        None,                           // Don't rotate
        Velocity,                       // Use velocity
        RelativeToOrigin,               // Rotate relative to direction from origin point
    }

    /// <summary>
    /// Default options for getting an interpolated point along a line
    /// </summary>
    public enum PointDistributionTypeEnum
    {
        None,                       // Don't adjust placement
        Auto,                       // Adjust placement automatically (default)
        DistanceSingleValue,        // Place based on distance
        DistanceCurveValue,         // Place based on curve
    }

    /// <summary>
    /// Default options for how to generate points in a line renderer
    /// </summary>
    public enum StepModeEnum
    {
        Interpolated,   // Draw points based on NumLineSteps
        FromSource,     // Draw only the points available in the source - use this for hard edges
    }

    /// <summary>
    /// Default options for how to distribute interpolated points in a line renderer
    /// </summary>
    public enum InterpolationModeEnum
    {
        FromNumSteps,   // Specify the number of interpolation steps manually
        FromLength,     // Create steps based on total length of line + manually specified length
        FromCurve       // Create steps based on total length of line + animation curve
    }

    public enum DistortionTypeEnum
    {
        NormalizedLength,   // Use the normalized length of the line plus its distortion strength curve to determine distortion strength
        Uniform,            // Use a single value to determine distortion strength
    }

    public static class LineUtils
    {
        #region offset and rotation functions

        public static readonly Vector3 DefaultUpVector = Vector3.up;

        /// <summary>
        /// Returns normalized length for OffsetModeEnum.Manual
        /// </summary>
        /// <param name="step"></param>
        /// <param name="offsetValue"></param>
        /// <param name="repeat"></param>
        /// <returns></returns>
        public static float GetDistanceSingleValue(int step, float offsetValue, bool repeat)
        {
            float value = step * offsetValue;
            return repeat ? Mathf.Repeat(value, 1f) : Mathf.Clamp01(value);
        }

        /// <summary>
        /// Returns normalized length for OffsetModeEnum.CurveNormalized
        /// </summary>
        /// <param name="step"></param>
        /// <param name="numSteps"></param>
        /// <param name="offsetCurve"></param>
        /// <param name="repeat"></param>
        /// <returns></returns>
        public static float GetDistanceCurveValue(int step, int numSteps, AnimationCurve offsetCurve, bool repeat)
        {
            float value = offsetCurve.Evaluate((float)step / numSteps);
            return repeat ? Mathf.Repeat(value, 1f) : Mathf.Clamp01(value);
        }

        /// <summary>
        /// Returns a blended value from a collection of vectors
        /// </summary>
        /// <param name="source"></param>
        /// <param name="normalizedLength"></param>
        /// <returns></returns>
        public static Vector3 GetVectorCollectionBlend(Vector3[] vectorCollection, float normalizedLength, bool repeat)
        {
            if (vectorCollection.Length == 0)
            {
                return Vector3.zero;
            }
            else if (vectorCollection.Length == 1)
            {
                return vectorCollection[0];
            }

            float arrayValueLength = 1f / vectorCollection.Length;
            int indexA = Mathf.FloorToInt(normalizedLength * vectorCollection.Length);
            if (indexA >= vectorCollection.Length)
            {
                indexA = repeat ? 0 : vectorCollection.Length - 1;
            }

            int indexB = indexA + 1;
            if (indexB >= vectorCollection.Length)
            {
                indexB = repeat ? 0 : vectorCollection.Length - 1;
            }

            float blendAmount = (normalizedLength - (arrayValueLength * indexA)) / arrayValueLength;
            Vector3 blendedVector = Vector3.Lerp(vectorCollection[indexA], vectorCollection[indexB], blendAmount);
            return blendedVector;
        }

        public static float ClosestTo(this IEnumerable<float> collection, float target)
        {
            // NB Method will return float.MaxValue for a sequence containing no elements.
            // Apply any defensive coding here as necessary. 
            var closest = float.MaxValue;
            var minDifference = float.MaxValue;
            foreach (var element in collection)
            {
                var difference = Mathf.Abs(element - target);
                if (minDifference > difference)
                {
                    minDifference = difference;
                    closest = element;
                }
            }

            return closest;
        }

        #endregion

        #region line drawing functions

        public static Vector3 GetPointAlongParabola(Vector3 start, Vector3 end, Vector3 up, float height, float normalizedLength)
        {
            float parabolaTime = normalizedLength * 2 - 1;
            Vector3 direction = end - start;
            Vector3 pos = start + normalizedLength * direction;
            pos += ((-parabolaTime * parabolaTime + 1) * height) * up.normalized;
            return pos;
        }

        public static Vector3 GetPointAlongSpline(SplinePoint[] points, float normalizedLength, InterpolationEnum interpolation = InterpolationEnum.Bezeir)
        {
            int pointIndex = (Mathf.RoundToInt(normalizedLength * points.Length));
            float subnormalizedLength = normalizedLength - ((float)pointIndex / points.Length);

            if (pointIndex + 3 >= points.Length)
            {
                return points[points.Length - 1].Point;
            }
            if (pointIndex < 0)
            {
                return points[0].Point;
            }

            Vector3 point1 = points[pointIndex].Point;
            Vector3 point2 = points[pointIndex + 1].Point;
            Vector3 point3 = points[pointIndex + 2].Point;
            Vector3 point4 = points[pointIndex + 3].Point;

            switch (interpolation)
            {
                case InterpolationEnum.Bezeir:
                default:
                    return InterpolateBezeirPoints(point1, point2, point3, point4, subnormalizedLength);

                case InterpolationEnum.CatmullRom:
                    return InterpolateCatmullRomPoints(point1, point2, point3, point4, subnormalizedLength);
            }
        }

        public static Vector3 InterpolateVectorArray(Vector3[] points, float normalizedLength)
        {
            float arrayValueLength = 1f / points.Length;
            int indexA = Mathf.FloorToInt(normalizedLength * points.Length);
            if (indexA >= points.Length)
            {
                indexA = 0;
            }

            int indexB = indexA + 1;
            if (indexB >= points.Length)
            {
                indexB = 0;
            }

            float blendAmount = (normalizedLength - (arrayValueLength * indexA)) / arrayValueLength;

            return Vector3.Lerp(points[indexA], points[indexB], blendAmount);
        }

        public static Vector3 InterpolateCatmullRomPoints(Vector3 point1, Vector3 point2, Vector3 point3, Vector3 point4, float normalizedLength)
        {
            Vector3 p1 = 2f * point2;
            Vector3 p2 = point3 - point1;
            Vector3 p3 = 2f * point1 - 5f * point2 + 4f * point3 - point4;
            Vector3 p4 = -point1 + 3f * point2 - 3f * point3 + point4;

            Vector3 point = 0.5f * (p1 + (p2 * normalizedLength) + (p3 * normalizedLength * normalizedLength) + (p4 * normalizedLength * normalizedLength * normalizedLength));

            return point;
        }

        public static Vector3 InterpolateBezeirPoints(Vector3 point1, Vector3 point2, Vector3 point3, Vector3 point4, float normalizedLength)
        {
            float invertedDistance = 1f - normalizedLength;
            return
                invertedDistance * invertedDistance * invertedDistance * point1 +
                3f * invertedDistance * invertedDistance * normalizedLength * point2 +
                3f * invertedDistance * normalizedLength * normalizedLength * point3 +
                normalizedLength * normalizedLength * normalizedLength * point4;
        }

        public static Vector3 InterpolateHermitePoints(Vector3 point1, Vector3 point2, Vector3 point3, Vector3 point4, float normalizedLength)
        {
            float invertedDistance = 1f - normalizedLength;
            return
                invertedDistance * invertedDistance * invertedDistance * point1 +
                3f * invertedDistance * invertedDistance * normalizedLength * point2 +
                3f * invertedDistance * normalizedLength * normalizedLength * point3 +
                normalizedLength * normalizedLength * normalizedLength * point4;
        }

        public static Vector3 GetEllipsePoint(float radiusX, float radiusY, float angle)
        {
            return new Vector3(radiusX * Mathf.Cos(angle), radiusY * Mathf.Sin(angle), 0.0f);
        }

        #endregion
    }
}