// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.MixedReality.Toolkit.UI.BoundsControlTypes;
using System.Collections.Generic;
using UnityEngine;

namespace Microsoft.MixedReality.Toolkit.UI.BoundsControl
{
    /// <summary>
    /// Links that are rendered in between the corners of <see cref="BoundsControl"/>
    /// </summary>
    public class Links
    {
        /// <summary>
        /// defines a bounds control link - the line between 2 corners of a box
        /// it keeps track of the transform and the axis the link is representing
        /// </summary>
        private class Link
        {
            public Transform transform;
            public CardinalAxisType axisType;
            public Link(Transform linkTransform, CardinalAxisType linkAxis)
            {
                transform = linkTransform;
                axisType = linkAxis;
            }
        }

        private List<Link> links = new List<Link>();

        private LinksConfiguration config;
        private Vector3 cachedExtents;
        private FlattenModeType cachedFlattenAxis;

        internal Links(LinksConfiguration configuration)
        {
            Debug.Assert(configuration != null, "Can't create BoundsControlLinks without valid configuration");
            config = configuration;
            config.wireFrameChanged.AddListener(UpdateWireframe);
        }

        ~Links()
        {
            config.wireFrameChanged.RemoveListener(UpdateWireframe);
        }

        private void UpdateWireframe(LinksConfiguration.WireframeChangedEventType changedType)
        {
            switch (changedType)
            {
                case LinksConfiguration.WireframeChangedEventType.Visibility:
                    Reset(config.ShowWireFrame, cachedFlattenAxis);
                    break;
                case LinksConfiguration.WireframeChangedEventType.Radius:
                    UpdateLinkScales(cachedExtents);
                    break;
                case LinksConfiguration.WireframeChangedEventType.Shape:
                    UpdateLinkPrimitive();
                    break;
                case LinksConfiguration.WireframeChangedEventType.Material:
                    UpdateMaterial();
                    break;
            }
        }

        internal void Clear()
        {
            if (links != null)
            {
                foreach (Link link in links)
                {
                    Object.Destroy(link.transform.gameObject);
                }
                links.Clear();
            }
        }

        internal void UpdateMaterial()
        {
            if (links != null && config.WireframeMaterial != null)
            {
                for (int i = 0; i < links.Count; ++i)
                {
                    Renderer linkRenderer = links[i].transform.gameObject.GetComponent<Renderer>();
                    if (linkRenderer != null)
                    {
                        linkRenderer.material = config.WireframeMaterial;
                    }
                }
            }
        }

        internal void UpdateVisibilityInInspector(HideFlags flags)
        {
            if (links != null)
            {
                foreach (var link in links)
                {
                    link.transform.hideFlags = flags;
                }
            }
        }

        internal void Reset(bool isVisible, FlattenModeType flattenAxis)
        {
            cachedFlattenAxis = flattenAxis;
            if (links != null)
            {
                for (int i = 0; i < links.Count; ++i)
                {
                    links[i].transform.gameObject.SetActive(isVisible && config.ShowWireFrame);
                }

                List<int> flattenedHandles = VisualUtils.GetFlattenedIndices(cachedFlattenAxis, VisualUtils.EdgeAxisType);
                if (flattenedHandles != null && VisualUtils.EdgeAxisType.Length == links.Count)
                {
                    for (int i = 0; i < flattenedHandles.Count; ++i)
                    {
                        links[flattenedHandles[i]].transform.gameObject.SetActive(false);
                    }
                }
            }
        }

        private Vector3 GetLinkDimensions(Vector3 currentBoundsExtents)
        {
            float wireframeEdgeRadius = config.WireframeEdgeRadius;
            float linkLengthAdjustor = config.WireframeShape == WireframeType.Cubic ? 2.0f : 1.0f - (6.0f * wireframeEdgeRadius);
            return (currentBoundsExtents * linkLengthAdjustor) + new Vector3(wireframeEdgeRadius, wireframeEdgeRadius, wireframeEdgeRadius);
        }

        internal void UpdateLinkPositions(ref Vector3[] boundsCorners)
        {
            if (boundsCorners != null)
            {
                for (int i = 0; i < links.Count; ++i)
                {
                    links[i].transform.position = VisualUtils.GetLinkPosition(i, ref boundsCorners);
                }
            }
        }

        internal void UpdateLinkScales(Vector3 currentBoundsExtents)
        {
            if (links != null)
            {
                for (int i = 0; i < links.Count; ++i)
                {
                    Transform parent = links[i].transform.parent;
                    Vector3 rootScale = parent.lossyScale;
                    Vector3 invRootScale = new Vector3(1.0f / rootScale[0], 1.0f / rootScale[1], 1.0f / rootScale[2]);
                    // Compute the local scale that produces the desired world space dimensions
                    Vector3 linkDimensions = Vector3.Scale(GetLinkDimensions(currentBoundsExtents), invRootScale);

                    float wireframeEdgeRadius = config.WireframeEdgeRadius;
                    if (links[i].axisType == CardinalAxisType.X)
                    {
                        links[i].transform.localScale = new Vector3(wireframeEdgeRadius, linkDimensions.x, wireframeEdgeRadius);
                    }
                    else if (links[i].axisType == CardinalAxisType.Y)
                    {
                        links[i].transform.localScale = new Vector3(wireframeEdgeRadius, linkDimensions.y, wireframeEdgeRadius);
                    }
                    else//Z
                    {
                        links[i].transform.localScale = new Vector3(wireframeEdgeRadius, linkDimensions.z, wireframeEdgeRadius);
                    }
                }
                cachedExtents = currentBoundsExtents;
            }
        }

        private void UpdateLinkPrimitive()
        {
            GameObject sharedMeshPrimitive;
            if (config.WireframeShape == WireframeType.Cubic)
            {
                sharedMeshPrimitive = GameObject.CreatePrimitive(PrimitiveType.Cube);
            }
            else
            {
                sharedMeshPrimitive = GameObject.CreatePrimitive(PrimitiveType.Cylinder);
            }
            var sharedMeshFilter = sharedMeshPrimitive.GetComponent<MeshFilter>();
            if (sharedMeshFilter)
            {
                foreach (var link in links)
                {
                    GameObject linkObj = link.transform.gameObject;
                    // replace shared mesh - note that we don't have a collider to replace in case of wireframe
                    linkObj.GetComponent<MeshFilter>().sharedMesh = sharedMeshFilter.sharedMesh;
                }
            }

            Object.Destroy(sharedMeshPrimitive);
            UpdateLinkScales(cachedExtents);
        }

        internal void CreateLinks(ref Vector3[] boundsCorners, Transform parent, Vector3 currentBoundsExtents)
        {
            // create links
            if (links != null)
            {
                GameObject link;
                Vector3 linkDimensions = GetLinkDimensions(currentBoundsExtents);
                for (int i = 0; i < VisualUtils.EdgeAxisType.Length; ++i)
                {
                    if (config.WireframeShape == WireframeType.Cubic)
                    {
                        link = GameObject.CreatePrimitive(PrimitiveType.Cube);
                        Object.Destroy(link.GetComponent<BoxCollider>());
                    }
                    else
                    {
                        link = GameObject.CreatePrimitive(PrimitiveType.Cylinder);
                        Object.Destroy(link.GetComponent<CapsuleCollider>());
                    }
                    link.name = "link_" + i.ToString();

                    CardinalAxisType axisType = VisualUtils.EdgeAxisType[i];
                    float wireframeEdgeRadius = config.WireframeEdgeRadius;
                    if (axisType == CardinalAxisType.Y)
                    {
                        link.transform.localScale = new Vector3(wireframeEdgeRadius, linkDimensions.y, wireframeEdgeRadius);
                        link.transform.Rotate(new Vector3(0.0f, 90.0f, 0.0f));
                    }
                    else if (axisType == CardinalAxisType.Z)
                    {
                        link.transform.localScale = new Vector3(wireframeEdgeRadius, linkDimensions.z, wireframeEdgeRadius);
                        link.transform.Rotate(new Vector3(90.0f, 0.0f, 0.0f));
                    }
                    else//X
                    {
                        link.transform.localScale = new Vector3(wireframeEdgeRadius, linkDimensions.x, wireframeEdgeRadius);
                        link.transform.Rotate(new Vector3(0.0f, 0.0f, 90.0f));
                    }

                    link.transform.position = VisualUtils.GetLinkPosition(i, ref boundsCorners);
                    link.transform.parent = parent;
                    Renderer linkRenderer = link.GetComponent<Renderer>();

                    if (config.WireframeMaterial != null)
                    {
                        linkRenderer.material = config.WireframeMaterial;
                    }

                    link.SetActive(config.ShowWireFrame);
                    links.Add(new Link(link.transform, axisType));
                }
            }
        }
    }
}
