using Proyecto26;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.AI;

namespace XenseAR
{
    [SelectionBase]
    public class NavigationTargetEditor : MonoBehaviour
    {
        /// <summary>
        /// The block this target belong to
        /// </summary>
        [HideInInspector] public ARWorldBlockInfo blockRoot;

        /// <summary>
        /// The target this editor manage
        /// </summary>
        [HideInInspector] public NavigationTarget target = new NavigationTarget();

        /// <summary>
        /// Current Location this target belong to
        /// </summary>
        [HideInInspector] public LocationInfo locationInfo = new LocationInfo();

        //A newly created target, haven't been saved to server
        [HideInInspector] public bool _isNew = true;

        //Target have unsaved change
        [HideInInspector] public bool _isEdited = false;

        //Target moved in this frame
        [HideInInspector] public bool _isMoved = false;

        //Target is not usable? default is false, user must manually press verify to check
        [HideInInspector] public bool _isError = false;

        //Target is usable? default is false, user must manually press verify to check
        [HideInInspector] public bool _isVerified = false;

        //Target being test for Navigation?
        [HideInInspector] public bool _isTesting_Navigate = false;

        //Target is deleted?
        [HideInInspector] public bool _isDeleted = false;

        //Target is updating? (On server)
        [HideInInspector] public bool _isUpdating = false;

        private NavigationTargetManager targetManager;

        private mTransform _transform = new mTransform();

        private GameObject targetPin;

        private Vector3 lastPosition = Vector3.zero;

        private int NavMeshLayerMask
        {
            get
            {
                return (1 << 6) + ( 1 << 9 );
            }
        }

        public bool isNew
        {
            get { return _isNew; }
            set { _isNew = value; }
        }

        public bool isEdited
        {
            get { return _isEdited; }
            set { _isEdited = value; }
        }

        public bool isMoved
        {
            get { return _isMoved; }
            set { _isMoved = value; }
        }

        public bool isError
        {
            get { return _isError; }
            set { _isError = value; }
        }

        public bool isVerified
        {
            get { return _isVerified; }
            set { _isVerified = value; }
        }

        public bool isTesting_Navigate
        {
            get { return _isTesting_Navigate; }
            set { _isTesting_Navigate = value; }
        }

        public bool isDeleted
        {
            get { return _isDeleted; }
            set { _isDeleted = value; }
        }

        public bool isUpdating
        {
            get { return _isUpdating; }
            set { _isUpdating = value; }
        }

        private void Start()
        {
            targetManager = FindObjectOfType<NavigationTargetManager>();
            if (targetPin == null)
            {
                targetPin = GameObject.Instantiate(Resources.Load<GameObject>("DestinationPin"), transform);
                targetPin.SetActive(true);
            }
            NavigationSystemManager.NavigationStatusChange += OnNavigationStatusChange;
        }

        private void OnDestroy()
        {
            NavigationSystemManager.NavigationStatusChange -= OnNavigationStatusChange;
        }

        private void Update()
        {
            string name = gameObject.name;
            if (target.name != name)
            {
                target.name = name;
                isEdited = true;
            }

            _transform.FromUnity(gameObject.transform);
            if (!Utility.Compare(_transform, target.transform))
            {
                isEdited = true;
                //isVerified = false;
            }

            isMoved = lastPosition != transform.position;

            if (isMoved && isTesting_Navigate) StartNavigationTesting();

            lastPosition = transform.position;
        }

        public void UpdateLocationInfo()
        {
            List<LocationInfo> locationInfos = Container.Resolve<NavigationTargetManager>().GetAvailableLocationInfos().Where(location => location.id == target.locationID).ToList();
            if (locationInfos != null && locationInfos.Count > 0)
                locationInfo = locationInfos[0].Clone();
            else
            {
                if (!string.IsNullOrEmpty(target.locationID))
                {
                    isError = true;
                    Debug.LogError("A location is missing, check if it is in the correct block\n" +
                        $"Target: {gameObject.name}\n" +
                        $"Block: {blockRoot.GetInfo().Name}\n" +
                        "Missing location: {" +
                        $"'name': '{target.name}'," +
                        $"'id': '{target.id}'" +
                        "}", gameObject);
                }
                locationInfo = new LocationInfo();
            }
        }

        public void Save()
        {
            if (isUpdating || isDeleted)
            {
                return;
            }
            if (!Verify()) return;

            isUpdating = true;

            if (blockRoot == null)
            {
                blockRoot = FindObjectsOfType<ARWorldBlockInfo>(true)[0];
            }

            target.name = gameObject.name;
            target.transform.FromUnity(gameObject.transform);

            if (isNew)
            {
                Create();
                return;
            }

            string jsonStr = JsonUtility.ToJson(target);
            BackendServices.Instance.Post(new RequestHelper
            {
                SimpleForm = new Dictionary<string, string>()
        {
            {"type","UpdateNavigationTarget" },
            {"NavigationTargetID", target.id},
            {"NavigationTargetData", jsonStr},
        },
                //            Uri = "",
            }).Then(res => OnFinishSave(res)).Catch((ex) =>
            {
                Debug.LogError($"Unable to save target, error: {ex.Message}", gameObject);
                isUpdating = false;
            });
        }

        private void Create()
        {
            string newID = System.Guid.NewGuid().ToString();
            if (string.IsNullOrEmpty(newID))
            {
                isUpdating = false;
                if (isDeleted) isNew = false;
                Debug.LogError("Unable to verify new ID, please try again", gameObject);
                return;
            }

            target.id = newID;

            string jsonStr = JsonUtility.ToJson(target);
            BackendServices.Instance.Post(new RequestHelper
            {
                SimpleForm = new Dictionary<string, string>()
            {
                {"type","AddNavigationTarget" },
                {"blockID", blockRoot.GetInfo().ID },
                {"data",jsonStr},
            },
            }).Then(res => OnFinishSave(res)).Catch((ex) =>
            {
                Debug.LogError($"Unable to create target, error: {ex.Message}");
                isUpdating = false;
                if (isDeleted) isNew = false;
            });
        }

        public bool Verify()
        {
            isError = false;
            isUpdating = true;

            if (string.IsNullOrEmpty(target.locationID))
            {
                isError = true;
                isUpdating = false;
                Debug.LogError("Location ID empty, please add location ID before saving");
                return false;
            }

            List<LocationInfo> locationInfos = Container.Resolve<NavigationTargetManager>().GetAvailableLocationInfos().Where(location => location.id == target.locationID).ToList();
            if (locationInfos == null || locationInfos.Count == 0)
            {
                isError = true;
                isUpdating = false;
                Debug.LogError("A location is missing, check if it is in the correct block\n" +
                    $"Target: {gameObject.name}\n" +
                    $"Block: {blockRoot.GetInfo().Name}\n" +
                    "Missing location: {" +
                    $"'name': '{target.name}'," +
                    $"'id': '{target.id}'" +
                    "}", gameObject);
                return false;
            }

            NavMeshTriangulation triangulation = NavMesh.CalculateTriangulation();

            if (triangulation.indices.Length == 0)
            {
                isError = true;
                isUpdating = false;
                Debug.LogError("No NavMesh found, please load a navmesh before verify", gameObject);
                return false;
            }

            for (var i = 0; i < triangulation.areas.Length; i++)
            {
                Vector3 v1 = triangulation.vertices[triangulation.indices[i * 3 + 0]];
                Vector3 v2 = triangulation.vertices[triangulation.indices[i * 3 + 1]];
                Vector3 v3 = triangulation.vertices[triangulation.indices[i * 3 + 2]];
                var distance = DistancePointToPlane(transform.position, v1, v2, v3);
                var pointInTriangle = IsPointInTriangle(v1, v2, v3, transform.position - distance * Vector3.up);
                if (pointInTriangle && 0 < distance && distance < 0.5f)
                {
                    isUpdating = false;
                    return true;
                }
            }

            isError = true;
            isUpdating = false;
            Debug.LogError("Navigation Target is not place near a navmesh (~0.5m), please re-place the target and retry", gameObject);
            return false;
        }

        public void StartNavigationTesting()
        {
            NavigationSystemManager navigationSystemManager = Container.Resolve<NavigationSystemManager>();
            if (navigationSystemManager == null)
            {
                Debug.LogError("No active NavigationSystemManager found, unable to start testing", gameObject);
                return;
            }
            LocationInfo temp = locationInfo.Clone();
            temp.navigationTargets.Add(target);
            navigationSystemManager.NavigateTo(temp);
            isTesting_Navigate = true;
        }

        public void EndNavigationTesting()
        {
            isTesting_Navigate = false;
            NavigationSystemManager navigationSystemManager = Container.Resolve<NavigationSystemManager>();
            if (navigationSystemManager == null)
            {
                Debug.LogError("No active NavigationSystemManager found, unable to start testing", gameObject);
                return;
            }

            navigationSystemManager.EndNavigation();
        }

        private void OnNavigationStatusChange((NavigationEvent state, Vector3? destination) data)
        {
            if (data.state == NavigationEvent.StopNavigate)
                isTesting_Navigate = false;
        }

        public void Reload()
        {
            if (isNew)
            {
                Debug.Log("Target don't have data to reload");
                return;
            }

            if (isUpdating)
            {
                return;
            }

            isUpdating = true;

            BackendServices.Instance.Post(new RequestHelper
            {
                SimpleForm = new Dictionary<string, string>()
            {
                {"type","GetBlockNavigationTargetData" },
                {"blockID", blockRoot.GetInfo().ID},
                {"navigationTargetID", target.id},
            },
            }).Then(res => OnFinishReload(res)).Catch((ex) =>
            {
                Debug.LogError($"Unable to reload target, error: {ex.Message}", gameObject);
                isUpdating = false;
            });
        }

        public void Delete()
        {
            if (isDeleted) return;

            if (isUpdating)
            {
                return;
            }

            if (isNew)
            {
                Destroy(gameObject);
                return;
            }

            isUpdating = true;

            BackendServices.Instance.Post(new RequestHelper
            {
                SimpleForm = new Dictionary<string, string>()
            {
                {"type","DeleteNavigationTarget" },
                {"NavigationTargetID", target.id},
            },
                //            Uri = "",
            }).Then(res => OnFinishDelete(res)).Catch((ex) =>
            {
                isUpdating = false;
            });
        }

        private void OnFinishSave(ResponseHelper helper)
        {
            Debug.Log(helper.Text);
            isNew = false;
            isEdited = false;
            isDeleted = false;
            isUpdating = false;
        }

        private void OnFinishReload(ResponseHelper helper)
        {
            isUpdating = false;
            isEdited = false;
            Debug.Log(helper.Text);
            var webresp = JsonUtility.FromJson<BlockNavigationTargetResponse>(helper.Text);
            if (webresp.status != Defines.successCode)
            {
                Debug.LogError($"Unable to get NavigationTargetData!\n" +
                    $"blockID: {blockRoot.GetInfo().ID}\n" +
                    $"error code: {helper.StatusCode}", gameObject);
                return;
            }

            target = webresp.data;
            gameObject.name = target.name;
            gameObject.transform.localPosition = target.transform.position.ToUnity();
            gameObject.transform.localRotation = target.transform.orientation.ToUnity();
            gameObject.transform.localScale = target.transform.scale.ToUnity();
        }

        private void OnFinishDelete(ResponseHelper helper)
        {
            Debug.Log(helper.Text);
            isUpdating = false;
            isDeleted = true;
        }

        /// <summary>
        /// Compute barycentric coordinates (u, v, w) of P with respect to triangle ABC,
        /// without consider distance from P to ABC Plane. Meaning it return the same value
        /// if P in on the plane or 100km away from the Plane (Pendicularly).
        /// </summary>
        static Vector3 GetBarycentric(Vector3 A, Vector3 B, Vector3 C, Vector3 P)
        {
            Vector3 v0 = B - A;
            Vector3 v1 = C - A;
            Vector3 v2 = P - A;

            float d00 = Vector3.Dot(v0, v0);
            float d01 = Vector3.Dot(v0, v1);
            float d11 = Vector3.Dot(v1, v1);
            float d20 = Vector3.Dot(v2, v0);
            float d21 = Vector3.Dot(v2, v1);

            float denom = d00 * d11 - d01 * d01;
            if (Mathf.Abs(denom) < 1e-8f)
                return new Vector3(-1, -1, -1); // Degenerate triangle

            float v = (d11 * d20 - d01 * d21) / denom;
            float w = (d00 * d21 - d01 * d20) / denom;
            float u = 1.0f - v - w;

            return new Vector3(u, v, w);
        }

        /// <summary>
        /// Check if P is inside of triangle ABC (using barycentrics)    
        /// without consider distance from P to ABC Plane. Meaning it return the same value
        /// if P in on the plane or 100km away from the Plane (Pendicularly).
        /// </summary>
        static bool IsPointInTriangle(Vector3 A, Vector3 B, Vector3 C, Vector3 P)
        {
            Vector3 bary = GetBarycentric(A, B, C, P);
            return bary.x >= -1e-3f && bary.y >= -1e-3f && bary.z >= -1e-3f &&
                   bary.x <= 1 + 1e-3f && bary.y <= 1 + 1e-3f && bary.z <= 1 + 1e-3f;
        }

        /// <summary>
        /// Compute the perpendicular distance from point to plane defined by triangle ABC
        /// </summary>
        /// <returns>Positive value if point is on the same side as the normal direction (y-up if possible), negative otherwise.</returns>
        public static float DistancePointToPlane(Vector3 point, Vector3 a, Vector3 b, Vector3 c)
        {
            Vector3 normal = Vector3.Cross(b - a, c - a).normalized;
            if (normal.y < 0)
                normal = -normal;
            return Vector3.Dot(point - a, normal);
        }
    }
}
