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

[Serializable]
public class Spline {

    public List<ControlPoint> points;
    public string name;
    [SerializeField]
    private SplineSettings settings;
    public bool[] assetIsActive;        //Bools to set assets in the settings on or of for this spline

    private float[] arcLengthTable;     //Contains a list of lengths between parametric values of the bezier curves. It is used to get a point at a distance along the spline.
    private static int tableSize = 100; //Precision value for the table

    //Constructor
    public Spline() {
        points = new List<ControlPoint> {
            new ControlPoint(Vector3.forward, Vector3.forward),
            new ControlPoint(Vector3.forward * 2, Vector3.forward)
        };
        ResetArcLengthTable();
        name = string.Concat("Spline");
        settings = null;
        assetIsActive = null;
    }

    //Constructor using position and index in component
    public Spline(Vector3 position, int index) {
        points = new List<ControlPoint> {
            new ControlPoint(position, Vector3.forward),
            new ControlPoint(position + Vector3.forward, Vector3.forward)
        };
        ResetArcLengthTable();
        name = string.Concat("Spline_", index.ToString("D2"));
        settings = null;
        assetIsActive = null;
    }

    //Add a new controlpoint in front of the spline with the same direction as the last point
    public void AddControlPoint () {
        points.Add(new ControlPoint(
            points[points.Count - 1].GetAnchorPosition() + points[points.Count - 1].GetRelativeHandlePosition(1).normalized, //Position
            .5f * points[points.Count - 1].GetRelativeHandlePosition(1).normalized));                                        //Direction
        ResetArcLengthTable();
    }

    public void RemoveControlPoint (ControlPoint point) {
        points.Remove(point);
        ResetArcLengthTable();
    }

    //Insert a point between two other points, or before the first point
    public void InsertControlPoint (int index) {
        Vector3 newAnchor = new Vector3();
        Vector3 newDirection = new Vector3();
        if (index == 0) {
            newAnchor = points[index].GetAnchorPosition() + points[index].GetRelativeHandlePosition(0).normalized;
            newDirection = 5f * points[index].GetRelativeHandlePosition(1).normalized;
        } else {
            newAnchor = GetPoint(index - 1, .5f);
            newDirection = GetDirection(index - 1, .5f) * points[index].GetRelativeHandlePosition(0).magnitude * .5f;
            points[index - 1].SetMode(BezierControlPointMode.Aligned);
            points[index - 1].SetRelativeHandlePosition(1, points[index - 1].GetRelativeHandlePosition(1) * .5f);
            points[index].SetMode(BezierControlPointMode.Aligned);
            points[index].SetRelativeHandlePosition(0, points[index].GetRelativeHandlePosition(0) * .5f);
        }
        points.Insert(index, new ControlPoint(newAnchor, newDirection));
        ResetArcLengthTable();
    }

    //Get position on spline at the arcdistance of the entire spline
    public Vector3 GetPoint(float t) {
        t = GetArcPos(t);

        int curve = (int)t;
        t = t % 1;
        if (curve == points.Count - 1) {
            curve = points.Count - 2;
            t = 1;
        }

        return GetPoint(curve, t);
    }

    //Get direction on spline at the arcdistance of the entire spline
    public Vector3 GetDirection(float t) {
        t = GetArcPos(t);

        int curve = (int)t;
        t = t % 1;
        if (curve == points.Count - 1) {
            curve = points.Count - 2;
            t = 1;
        }
        return GetDirection(curve, t);
    }

    //Get up vector on spline at the arcdistance of the entire spline
    public Vector3 GetUp(float t) {
        t = GetArcPos(t);

        int curve = (int)t;
        t = t % 1;
        if (curve == points.Count - 1) {
            curve = points.Count - 2;
            t = 1;
        }
        Vector3 direction = GetDirection(curve, t);
        Quaternion rotation = Quaternion.Lerp(points[curve].GetRotation(), points[curve + 1].GetRotation(), t);
        return Vector3.ProjectOnPlane(rotation * Vector3.up, direction);
    }

    //Returns the parametric value for the position at arcdistance t
    private float GetArcPos (float t) {
        for (int i = 0; i < arcLengthTable.Length; i++) {
            if (arcLengthTable[i] > t) {
                float T1 = (float)(i - 1) / (float)tableSize;
                float T2 = (float)(i) / (float)tableSize;
                float dT = (t - arcLengthTable[i - 1]) / (arcLengthTable[i] - arcLengthTable[i - 1]);
                return Mathf.Lerp(T1, T2, dT);
            }
        }
        return points.Count - 1;
    }

    //Get the position for a bezier curve at position t
    private Vector3 GetPoint(int curve, float t) {
        return Bezier.GetPoint(points[curve].GetAnchorPosition(), points[curve].GetHandlePosition(1),
            points[curve + 1].GetHandlePosition(0), points[curve + 1].GetAnchorPosition(), t);
    }

    //Get the direction for a bezier curve at position t
    private Vector3 GetDirection(int curve, float t) {
        return Bezier.GetFirstDerivative(points[curve].GetAnchorPosition(), points[curve].GetHandlePosition(1),
            points[curve + 1].GetHandlePosition(0), points[curve + 1].GetAnchorPosition(), t);
    }

    //Returns the entire length of this spline
    public float GetArcLength() {
        return arcLengthTable[arcLengthTable.Length - 1];
    }

    //Recaclculates the arcLengthTable
    public void ResetArcLengthTable () {
        arcLengthTable = new float[(points.Count - 1) * tableSize + 1];
        arcLengthTable[0] = 0f;
        Vector3 lastPos = points[0].GetAnchorPosition();
        for (int i = 0; i < points.Count - 1; i++) {
            for (int j = 0; j < tableSize; j++) {
                if (i + j != 0) {
                    Vector3 nextPos = Bezier.GetPoint(points[i].GetAnchorPosition(), points[i].GetHandlePosition(1), points[i + 1].GetHandlePosition(0), points[i + 1].GetAnchorPosition(), (float)j / (float)tableSize);
                    arcLengthTable[i * tableSize + j] = arcLengthTable[i * tableSize + j - 1] + (nextPos - lastPos).magnitude;
                    lastPos = nextPos;
                }
            }
        }
        arcLengthTable[arcLengthTable.Length - 1] = arcLengthTable[arcLengthTable.Length - 2] + (points[points.Count - 1].GetAnchorPosition() - lastPos).magnitude;
    }

    public void SetSettings(SplineSettings _settings) {
        settings = _settings;
        assetIsActive = new bool[settings.generated.Count + settings.placers.Count];
        for (int i = 0; i < assetIsActive.Length; i++) {
            assetIsActive[i] = true;
        }
    }

    public SplineSettings GetSettings() {
        return settings;
    }

    /* Static function to calculate the euler angles for a point given its up and forward vector.
     * This is similar to Quaternion.LookRotation. It calculates x, y and z in a different order that makes more sense for a spline*/
    public static Vector3 GetEulerAngles(Vector3 up, Vector3 forward) {
        Vector3 euler = new Vector3();
        euler.y = Mathf.Rad2Deg * Mathf.Atan2(forward.x, forward.z);

        Vector3 xzDirection = forward;
        xzDirection.y = 0;
        euler.x = -Mathf.Rad2Deg * Mathf.Atan2(forward.y, xzDirection.magnitude);

        Vector3 perpendicular = Vector3.Cross(Vector3.up, xzDirection);
        Vector3 normal = Vector3.Cross(forward, perpendicular);
        if (Vector3.Angle(perpendicular, up) < 90)
            euler.z = 360 - Vector3.Angle(normal, up);
        else
            euler.z = Vector3.Angle(normal, up);

        return euler;
    }
}