﻿using UnityEngine;
using System.Collections.Generic;

namespace Com.LuisPedroFonseca.ProCamera2D
{
    public enum FollowMode
    {
        BothAxis,
        HorizontalAxis,
        VerticalAxis
    }

    #if UNITY_5_3_OR_NEWER
    [HelpURLAttribute("http://www.procamera2d.com/user-guide/extension-rails/")]
    #endif
    public class ProCamera2DRails : BasePC2D, IPreMover
    {
        public static string ExtensionName = "Rails";

        [HideInInspector]
        public List<Vector3> RailNodes = new List<Vector3>();

        public FollowMode FollowMode;

        public List<CameraTarget> CameraTargets = new List<CameraTarget>();

        Dictionary<CameraTarget, Transform> _cameraTargetsOnRails = new Dictionary<CameraTarget, Transform>();

        List<CameraTarget> _tempCameraTargets = new List<CameraTarget>();

        KDTree _kdTree;

        override protected void Awake()
        {
            base.Awake();

            _kdTree = KDTree.MakeFromPoints(RailNodes.ToArray());

            for (int i = 0; i < CameraTargets.Count; i++)
            {
                var railTransform = new GameObject(CameraTargets[i].TargetTransform.name + "_OnRails").transform;
                _cameraTargetsOnRails.Add(
                    CameraTargets[i],
                    railTransform
                    );

                var cameraTarget = ProCamera2D.AddCameraTarget(railTransform);
                cameraTarget.TargetOffset = CameraTargets[i].TargetOffset;
            }

            if (CameraTargets.Count == 0)
                enabled = false;

            ProCamera2D.AddPreMover(this);

            Step();
        }

        protected override void OnDestroy()
        {
            base.OnDestroy();

            if(ProCamera2D)
                ProCamera2D.RemovePreMover(this);
        }

        #region IPreMover implementation

        public void PreMove(float deltaTime)
        {
            if (enabled)
                Step();
        }

        public int PrMOrder { get { return _prmOrder; } set { _prmOrder = value; } }

        int _prmOrder = 1000;

        #endregion

        void Step()
        {
            var pos = Vector3.zero;
            for (int i = 0; i < CameraTargets.Count; i++)
            {
                switch (FollowMode)
                {
                    case FollowMode.HorizontalAxis:
                        pos = VectorHVD(
                            Vector3H(CameraTargets[i].TargetPosition) * CameraTargets[i].TargetInfluenceH, 
                            Vector3V(ProCamera2D.LocalPosition), 
                            0);
                        break;

                    case FollowMode.VerticalAxis:
                        pos = VectorHVD(
                            Vector3H(ProCamera2D.LocalPosition), 
                            Vector3V(CameraTargets[i].TargetPosition) * CameraTargets[i].TargetInfluenceV, 
                            0);
                        break;

                    case FollowMode.BothAxis:
                        pos = VectorHVD(
                            Vector3H(CameraTargets[i].TargetPosition) * CameraTargets[i].TargetInfluenceH, 
                            Vector3V(CameraTargets[i].TargetPosition) * CameraTargets[i].TargetInfluenceV,
                            0);
                        break;
                }
                _cameraTargetsOnRails[CameraTargets[i]].position = GetPositionOnRail(pos);
            }
        }

        /// <summary>Add a target for the camera to follow on rails.</summary>
        /// <param name="targetTransform">The Transform of the target</param>
        /// <param name="targetInfluenceH">The influence this target horizontal position should have when calculating the average position of all the targets</param>
        /// <param name="targetInfluenceV">The influence this target vertical position should have when calculating the average position of all the targets</param>
        /// <param name="targetOffset">A vector that offsets the target position that the camera will follow</param>
        public void AddRailsTarget(Transform targetTransform, float targetInfluenceH = 1f, float targetInfluenceV = 1f, Vector2 targetOffset = default(Vector2))
        {
            if (GetRailsTarget(targetTransform) != null)
                return;
            
            var newCameraTarget = new CameraTarget()
            {
                    TargetTransform = targetTransform,
                    TargetInfluenceH = targetInfluenceH,
                    TargetInfluenceV = targetInfluenceV,
                    TargetOffset = targetOffset
            };

            CameraTargets.Add(newCameraTarget);

            var railTransform = new GameObject(targetTransform.name + "_OnRails").transform;

            _cameraTargetsOnRails.Add(
                newCameraTarget,
                railTransform
            );

            ProCamera2D.AddCameraTarget(railTransform);

            enabled = true;
        }

        /// <summary>Removes a target from being followed on the rails</summary>
        /// <param name="targetTransform">The Transform of the target</param>
        public void RemoveRailsTarget(Transform targetTransform)
        {
            var cameraTarget = GetRailsTarget(targetTransform);

            if (cameraTarget != null)
            {
                CameraTargets.Remove(cameraTarget);
                ProCamera2D.RemoveCameraTarget(_cameraTargetsOnRails[cameraTarget]);
            }
        }

        /// <summary>Gets the corresponding CameraTarget from an object's transform.</summary>
        /// <param name="targetTransform">The Transform of the target</param>
        public CameraTarget GetRailsTarget(Transform targetTransform)
        {
            for (int i = 0; i < CameraTargets.Count; i++)
            {
                if (CameraTargets[i].TargetTransform.GetInstanceID() == targetTransform.GetInstanceID())
                {
                    return CameraTargets[i];
                }
            }
            return null;
        }

        /// <summary>Disables the rails targets. This method is used by the Rails trigger</summary>
        /// <param name="transitionDuration">The time it takes to disable the targets</param>
        public void DisableTargets(float transitionDuration = 0f)
        {
            if(_tempCameraTargets.Count != 0)
                return;
                
            for (int i = 0; i < _cameraTargetsOnRails.Count; i++)
            {
                ProCamera2D.RemoveCameraTarget(_cameraTargetsOnRails[CameraTargets[i]], transitionDuration);
                _tempCameraTargets.Add(ProCamera2D.AddCameraTarget(CameraTargets[i].TargetTransform, CameraTargets[i].TargetInfluenceH, CameraTargets[i].TargetInfluenceV, transitionDuration, CameraTargets[i].TargetOffset));
            }
        }

        /// <summary>Enables the rails targets. This method is used by the Rails trigger</summary>
        /// <param name="transitionDuration">The time it takes to enable the targets</param>
        public void EnableTargets(float transitionDuration = 0f)
        {
            for (int i = 0; i < _tempCameraTargets.Count; i++)
            {
                ProCamera2D.RemoveCameraTarget(_tempCameraTargets[i].TargetTransform, transitionDuration);
                ProCamera2D.AddCameraTarget(_cameraTargetsOnRails[CameraTargets[i]], 1, 1, transitionDuration);
            }
            
            _tempCameraTargets.Clear();
        }

        Vector3 GetPositionOnRail(Vector3 pos)
        {
            var closestNode = _kdTree.FindNearest(pos);

            if (closestNode == 0)
            {
                return GetPositionOnRailSegment(RailNodes[0], RailNodes[1], pos);
            }
            else if (closestNode == RailNodes.Count - 1)
            {
                return GetPositionOnRailSegment(RailNodes[RailNodes.Count - 1], RailNodes[RailNodes.Count - 2], pos);
            }
            else
            {
                var leftSeg = GetPositionOnRailSegment(RailNodes[closestNode - 1], RailNodes[closestNode], pos);
                var rightSeg = GetPositionOnRailSegment(RailNodes[closestNode + 1], RailNodes[closestNode], pos);

                if ((pos - leftSeg).sqrMagnitude <= (pos - rightSeg).sqrMagnitude)
                    return leftSeg;
                else
                    return rightSeg;
            }
        }

        Vector3 GetPositionOnRailSegment(Vector3 node1, Vector3 node2, Vector3 pos)
        {
            var node1ToPos = pos - node1;
            var segmentDirection = (node2 - node1).normalized;

            float node1Dot = Vector3.Dot(segmentDirection, node1ToPos);

            if (node1Dot < 0f)
            {
                return node1;
            }
            else if (node1Dot * node1Dot > (node2 - node1).sqrMagnitude)
            {
                return node2;
            }
            else
            {
                var fromNode1 = segmentDirection * node1Dot;
                return node1 + fromNode1;
            }
        }

        #if UNITY_EDITOR
        override protected void DrawGizmos()
        {
            base.DrawGizmos();

            var cameraDimensions = Utils.GetScreenSizeInWorldCoords(ProCamera2D.GameCamera, Mathf.Abs(Vector3D(transform.localPosition)));

            Gizmos.color = EditorPrefsX.GetColor(PrefsData.RailsColorKey, PrefsData.RailsColorValue);
            
            if(_tempCameraTargets.Count > 0)
                Gizmos.color = Gizmos.color * .7f;

            // Rails nodes
            for (int i = 0; i < RailNodes.Count; i++)
            {
                Gizmos.DrawWireCube(RailNodes[i], cameraDimensions * .01f);
            }

            // Rails path
            if (RailNodes.Count >= 2)
            {
                for (int i = 0; i < RailNodes.Count - 1; i++)
                {
                    Gizmos.DrawLine(RailNodes[i], RailNodes[i + 1]);
                }
            }
        }
        #endif
    }
}