using System;
using UnityEngine;
using BansheeGz.BGSpline.Components;
using BansheeGz.BGSpline.Curve;
using Random = UnityEngine.Random;

namespace BansheeGz.BGSpline.Example
{
    //check math's CalcPositionByClosestPoint method.  (this test can be  automated with CheckResults set to true)
    public class BGTestCurveClosestPoint : MonoBehaviour
    {
        [Tooltip("Line renderer material")] public Material LineRendererMaterial;

        [Tooltip("Object to use for point's indication")] public GameObject PointIndicator;

        [Range(1, 100)] [Tooltip("How much points to use with search")] public int NumberOfPointsToSeek = 10;

        [Range(2, 100)] [Tooltip("How much points to add to the curve")] public int NumberOfCurvePoints = 100;

        [Range(1, 30)] [Tooltip("How much sections to use for splitting each curve's segment")] public int NumberOfSplits = 30;

        [Range(1, 5)] [Tooltip("Transition period")] public int Period = 4;

        [Tooltip("Use slow check method to validate results")] public bool CheckResults = false;


        private BGCurve curve;
        private BGCcMath math;

        //this is an area, defined by camera frustum (so we could see curve and points)
        private static Vector3 min = new Vector3(-10, 0, -2);
        private static Vector3 max = new Vector3(10, 10, 2);


        //indicator objects
        private GameObject[] objects;

        //curve's points positions
        private Vector3[] oldCurvePointPos;
        private Vector3[] newCurvePointPos;

        //point's positions
        private Vector3[] oldPointPos;
        private Vector3[] newPointPos;

        //for transitions
        private float startTime = -100000;

        private int ErrorPointIndex = -1;
        private GUIStyle style;

        private bool HasError
        {
            get { return ErrorPointIndex >= 0; }
        }

        // Use this for initialization
        private void Start()
        {
            //init components
            curve = gameObject.AddComponent<BGCurve>();
            curve.Closed = true;
            math = gameObject.AddComponent<BGCcMath>();
            gameObject.AddComponent<BGCcVisualizationLineRenderer>();
            var lineRenderer = gameObject.GetComponent<LineRenderer>();
            lineRenderer.sharedMaterial = LineRendererMaterial;
            var color = new Color(.2f, .2f, .2f, 1f);
#if UNITY_5_5 || UNITY_5_6
            lineRenderer.startWidth = lineRenderer.endWidth = .03f;
            lineRenderer.startColor = lineRenderer.endColor = color;
#else
            lineRenderer.SetWidth(.03f, .03f);
            lineRenderer.SetColors(color, color);
#endif
            math.SectionParts = NumberOfSplits;

            //create curve's points
            for (var i = 0; i < NumberOfCurvePoints; i++)
            {
                var controlRandom = Random.Range(0, 3);
                var controlType = BGCurvePoint.ControlTypeEnum.Absent;
                switch (controlRandom)
                {
                    case 1:
                        controlType = BGCurvePoint.ControlTypeEnum.BezierIndependant;
                        break;
                    case 2:
                        controlType = BGCurvePoint.ControlTypeEnum.BezierSymmetrical;
                        break;
                }
                curve.AddPoint(new BGCurvePoint(curve,
                    Vector3.zero,
                    controlType,
                    RandomVector()*.3f,
                    RandomVector()*.3f));
            }

            //init arrays
            oldPointPos = new Vector3[NumberOfPointsToSeek];
            newPointPos = new Vector3[NumberOfPointsToSeek];
            oldCurvePointPos = new Vector3[NumberOfCurvePoints];
            newCurvePointPos = new Vector3[NumberOfCurvePoints];

            InitArray(newCurvePointPos, oldCurvePointPos);
            InitArray(newPointPos, oldPointPos);

            //create objects
            objects = new GameObject[NumberOfPointsToSeek];
            for (var i = 0; i < NumberOfPointsToSeek; i++)
            {
                var clone = Instantiate(PointIndicator);
                clone.transform.parent = transform;
                objects[i] = clone;
            }
            PointIndicator.SetActive(false);

            //init cycle
            InitCycle();
        }

        private void OnGUI()
        {
            if (style == null) style = new GUIStyle(GUI.skin.label) {fontSize = 20};
            GUI.Label(new Rect(0, 30, 600, 30), "Turn on Gizmos to see Debug lines", style);
        }


        private void InitCycle()
        {
            InitArray(oldCurvePointPos, newCurvePointPos);
            InitArray(oldPointPos, newPointPos);
        }

        // Update is called once per frame
        private void Update()
        {
            if (HasError)
            {
                //use it for debugging
                Process(ErrorPointIndex, true);
            }
            else
            {
                Calculate(null, null);


                var elapsed = Time.time - startTime;
                if (elapsed > Period)
                {
                    startTime = Time.time;
                    InitCycle();
                    elapsed = 0;
                }

                //lerp
                var ratio = elapsed/Period;
                for (var i = 0; i < NumberOfCurvePoints; i++)
                {
                    curve[i].PositionLocal = Vector3.Lerp(oldCurvePointPos[i], newCurvePointPos[i], ratio);
                }
                for (var i = 0; i < NumberOfPointsToSeek; i++)
                {
                    objects[i].transform.localPosition = Vector3.Lerp(oldPointPos[i], newPointPos[i], ratio);
                }
            }
        }


        private void Calculate(object sender, EventArgs e)
        {
            for (var i = 0; i < NumberOfPointsToSeek; i++)
            {
                Process(i);
                if (HasError) break;
            }
        }

        private void Process(int i, bool suppressWarning = false)
        {
            var point = objects[i].transform.position;
            float distanceUsingMath;
            var posUsingMath = math.CalcPositionByClosestPoint(point, out distanceUsingMath);

            Debug.DrawLine(point, posUsingMath, Color.yellow);

            if (!CheckResults) return;

            float distanceUsingCheckMethod;
            var posUsingCheckMethod = CalcPositionByClosestPoint(math, point, out distanceUsingCheckMethod);
            Debug.DrawLine(point, posUsingCheckMethod, Color.blue);

            var distanceCheck = Math.Abs(distanceUsingMath - distanceUsingCheckMethod) > .01f;
            var pointCheck = Vector3.Magnitude(posUsingMath - posUsingCheckMethod) > 0.001f;
            if ((distanceCheck || pointCheck) && Mathf.Abs((point - posUsingMath).magnitude - (point - posUsingCheckMethod).magnitude) > BGCurve.Epsilon)
            {
                ErrorPointIndex = i;
                if (!suppressWarning)
                {
                    Debug.Log("Error detected. Simulation stopped, but erroneous iteration's still running. Use debugger to debug the issue.");
                    Debug.Log("!!! Discrepancy detected while calculating pos by closest point: 1) [Using math] pos=" + posUsingMath + ", distance=" + distanceUsingMath
                              + "  2) [Using check method] pos=" + posUsingCheckMethod + ", distance=" + distanceUsingCheckMethod);


                    if (pointCheck)
                    {
                        Debug.Log("Reason: Result points varies more than " + BGCurve.Epsilon + ". Difference=" + Vector3.Magnitude(posUsingMath - posUsingCheckMethod));
                    }
                    if (distanceCheck)
                    {
                        Debug.Log("Reason: Distances varies more than 1cm. Difference=" + Math.Abs(distanceUsingMath - distanceUsingCheckMethod));
                    }

                    var mathPos = math.CalcByDistance(BGCurveBaseMath.Field.Position, distanceUsingMath);
                    var checkMethodPos = math.CalcByDistance(BGCurveBaseMath.Field.Position, distanceUsingCheckMethod);
                    Debug.Log("Distance check: 1) [Using math] check=" + (Vector3.SqrMagnitude(mathPos - posUsingMath) < BGCurve.Epsilon ? "passed" : "failed")
                              + "  2) [Using check method] check=" + (Vector3.SqrMagnitude(checkMethodPos - posUsingCheckMethod) < BGCurve.Epsilon ? "passed" : "failed"));


                    var actualDistUsingMath = Vector3.Distance(point, posUsingMath);
                    var actualDistanceUsingCheckMethod = Vector3.Distance(point, posUsingCheckMethod);
                    Debug.Log("Actual distance: 1) [Using math] Dist=" + actualDistUsingMath
                              + "  2) [Using check method] Dist=" + actualDistanceUsingCheckMethod
                              +
                              (Math.Abs(actualDistUsingMath - actualDistanceUsingCheckMethod) > BGCurve.Epsilon
                                  ? (". And the winner is " + (actualDistUsingMath < actualDistanceUsingCheckMethod ? "math" : "check method"))
                                  : ""));
                }
            }
        }

        //Bruteforce method for checking results only. Do not use it.
        private static Vector3 CalcPositionByClosestPoint(BGCcMath math, Vector3 targetPoint, out float distance)
        {
            var sections = math.Math.SectionInfos;
            var sectionsCount = sections.Count;
            var result = sections[0][0].Position;
            var minDistance = Vector3.SqrMagnitude(sections[0][0].Position - targetPoint);
            distance = 0;
            for (var i = 0; i < sectionsCount; i++)
            {
                var currentSection = sections[i];


                var points = currentSection.Points;
                var pointsCount = points.Count;
                for (var j = 1; j < pointsCount; j++)
                {
                    var point = points[j];
                    float ratio;

                    var closestPoint = CalcClosestPointToLine(points[j - 1].Position, point.Position, targetPoint, out ratio);

                    var sqrMagnitude = Vector3.SqrMagnitude(targetPoint - closestPoint);
                    if (!(minDistance > sqrMagnitude)) continue;


                    minDistance = sqrMagnitude;
                    result = closestPoint;

                    if (ratio == 1)
                    {
                        var sectionIndex = i;
                        var pointIndex = j;
                        if (j == pointsCount - 1 && i < sectionsCount - 1)
                        {
                            sectionIndex = i + 1;
                            pointIndex = 0;
                        }
                        distance = sections[sectionIndex].DistanceFromStartToOrigin + sections[sectionIndex][pointIndex].DistanceToSectionStart;
                    }
                    else
                    {
                        distance = sections[i].DistanceFromStartToOrigin + Mathf.Lerp(currentSection[j - 1].DistanceToSectionStart, point.DistanceToSectionStart, ratio);
                    }
                }
            }
            return result;
        }


        private static Vector3 RandomVector()
        {
            return new Vector3(Random.Range(min.x, max.x), Random.Range(min.y, max.y), Random.Range(min.z, max.z));
        }

        private static void InitArray(Vector3[] oldArray, Vector3[] newArray)
        {
            for (var i = 0; i < oldArray.Length; i++)
            {
                oldArray[i] = newArray[i];
                newArray[i] = RandomVector();
            }
        }

        private static Vector3 CalcClosestPointToLine(Vector3 a, Vector3 b, Vector3 p, out float ratio)
        {
            var ap = p - a;
            var ab = b - a;
            var sqrtMagnitude = ab.sqrMagnitude;
            if (Math.Abs(sqrtMagnitude) < BGCurve.Epsilon)
            {
                ratio = 1;
                return b;
            }

            var distance = Vector3.Dot(ap, ab)/sqrtMagnitude;
            if (distance < 0)
            {
                ratio = 0;
                return a;
            }
            if (distance > 1)
            {
                ratio = 1;
                return b;
            }
            ratio = distance;
            return a + ab*distance;
        }
    }
}