﻿using System.Collections.Generic;
using UnityEngine;

public class WindOnSpline : MonoBehaviour {

    BezierSpline spline;
    List<Vector3> points = new List<Vector3>();
    [SerializeField]
    float windStrength = 5;
    [SerializeField]
    float minDistanceFromSpline = 5;

    [SerializeField]
    Boat overridenBoat;

    EnvironmentSetup environment;
    [SerializeField]
    WindZone windZone;

    Boat Boat {
        get {
            if (overridenBoat != null)
                return overridenBoat;
            else return Boat.instance;
        }
    }
    bool boatIsOnSpline = true;
    public delegate void WindEvent();
    public WindEvent BoatEnteredSpline;
    public WindEvent BoatLeftSpline;
    float boatProgression = 0;
    public Vector3 PlannedBoatPosition { get { return spline.GetPoint(boatProgression + .1f); } }

    int firstClosestIndex = 0;
    int secondClosestIndex = 0;

    struct Line {
        public Vector3 pointA, pointB;
        public Line(Vector3 pA, Vector3 pb)
        {
            pointA = pA;
            pointB = pb;
        }
    }
    Line closestLine;

    public static WindOnSpline instance;

    private void Awake()
    {
        environment = GetComponentInParent<EnvironmentSetup>();
        instance = this;
    }

    void Start()
    {
        SetupSpline();
        SpawnWindZones();
    }

    private void Update()
    {
        if (Boat == null)
            return;
        // Add force to the boat if it is close.
        GetClosestIndexes(Boat.transform.position);
        GetClosestLine();
        float distanceFromSpline = GetDistanceFromLine(Boat.transform.position, closestLine);
        if (distanceFromSpline < minDistanceFromSpline)
        {
            boatProgression = GetProgressionOnSpline(Boat.transform.position);
            Vector3 wind = new Vector3(spline.GetDirection(boatProgression).x, 0, spline.GetDirection(boatProgression).z) * windStrength;
            if (Boat.Rb.velocity.magnitude < windStrength)
            {
                float facingFactor = Mathf.Abs(Vector3.Dot(Boat.transform.forward, wind.normalized));
                // Add a force depending on where the boat is looking at and how close it is from the spline.
                Boat.Rb.AddForce(wind * facingFactor);
                Boat.MoveCloth(wind);
            }
            // Set boat is on spline, call event.
            if (!boatIsOnSpline)
            {
                boatIsOnSpline = true;
                if (BoatEnteredSpline != null)
                    BoatEnteredSpline();
            }
        }
        else
        {
            Boat.MoveCloth(Vector3.zero);
            // Set boat is on spline, call event.
            if (boatIsOnSpline)
            {
                boatIsOnSpline = false;
                if (BoatLeftSpline != null)
                    BoatLeftSpline();
            }
        }
    }

    #region Setup spline -----------
    void SetupSpline()
    {
        spline = GetComponent<BezierSpline>();
        GetPoints();
        /*//debug
        foreach (Vector3 item in points)
        {
            Instantiate(visual, transform).transform.position = item;
        }*/
    }
    void GetPoints()
    {
        // Split the curve into a number of points.
        for (int i = 1; i <= 20; i++)
            points.Add(spline.GetPoint(i / (float)20));
    }
    #endregion ----------------------


    void SpawnWindZones()
    {
        if (windZone == null || environment == null)
            return;
        for (int i = 0; i < environment.IslandsGetter.Count; i++)
        {
            Transform newZone = Instantiate(windZone, transform).transform;
            newZone.position = environment.IslandsGetter[i].transform.position;
        }
    }

    #region Progression on spline, distance from spline -----------------------------------------------
    void GetClosestIndexes(Vector3 position)
    {
        // Get first closest point.
        for (int i = 1; i < points.Count; i++)
        {
            if (Vector3.Distance(points[i], position) < Vector3.Distance(points[firstClosestIndex], position))
                firstClosestIndex = i;
        }
        // Get second closest point.
        if (firstClosestIndex == 0)
            secondClosestIndex = 1;
        else if (firstClosestIndex == points.Count - 1)
            secondClosestIndex = firstClosestIndex - 1;
        else
        {
            if (Vector3.Distance(position, points[firstClosestIndex - 1]) < Vector3.Distance(position, points[firstClosestIndex + 1]))
                secondClosestIndex = firstClosestIndex - 1;
            else
                secondClosestIndex = firstClosestIndex + 1;
        }
    }

    void GetClosestLine()
    {
        if (firstClosestIndex < secondClosestIndex)
            closestLine = new Line(points[firstClosestIndex], points[secondClosestIndex]);
        else
            closestLine = new Line(points[secondClosestIndex], points[firstClosestIndex]);
    }

    float GetProgressionAlongLine(Line line, Vector3 pos)
    {
        float distance = GetDistanceFromFirstLinePoint(pos, line);
        float progression = distance / Vector3.Distance(line.pointA, line.pointB);
        return progression;
    }

    float GetDistanceFromFirstLinePoint(Vector3 point, Line line)
    {
        // https://math.stackexchange.com/questions/1905533/find-perpendicular-distance-from-point-to-line-in-3d
        Vector3 d = (line.pointB - line.pointA) / Vector3.Distance(line.pointB, line.pointA);
        Vector3 v = point - line.pointA;
        float t = Vector3.Dot(v, d);
        Vector3 P = line.pointA + t * d;
        return Vector3.Distance(P, line.pointA);
    }

    float GetDistanceFromLine(Vector3 point, Line line)
    {
        Vector3 d = (line.pointB - line.pointA) / Vector3.Distance(line.pointB, line.pointA);
        Vector3 v = point - line.pointA;
        float t = Vector3.Dot(v, d);
        Vector3 P = line.pointA + t * d;
        return Vector3.Distance(P, point);
    }


    float GetProgressionOnSpline(Vector3 position)
    {
        float progressionOnLine = GetProgressionAlongLine(closestLine, position);
        // Return progression.
        float firstPointProgress = (firstClosestIndex + 1) / (float)points.Count;
        float secondPointProgress = (secondClosestIndex + 1) / (float)points.Count;
        float resultProgression = 0;
        if (firstClosestIndex == 0)
        {
            // Return 0 if we are behind the first point.
            if (Vector3.Dot(points[secondClosestIndex] - points[firstClosestIndex], position - points[firstClosestIndex]) < 0)
                resultProgression = firstPointProgress;
        }
        else
        {
            // Return lerped progression from the closest points.
            if (firstPointProgress < secondPointProgress)
                resultProgression = Mathf.Lerp(firstPointProgress, secondPointProgress, progressionOnLine);
            else
                resultProgression = Mathf.Lerp(secondPointProgress, firstPointProgress, progressionOnLine);
        }
        return resultProgression;
    }
    #endregion ----------------------------------------------------------------------------------------------
}
