// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

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

namespace HoloToolkit.Unity.Collections
{
    /// <summary>
    /// An Object Collection is simply a set of child objects organized with some
    /// layout parameters.  The object collection can be used to quickly create 
    /// control panels or sets of prefab/objects.
    /// </summary>
    public class ObjectCollection : MonoBehaviour
    {
        #region public members
        /// <summary>
        /// Action called when collection is updated
        /// </summary>
        public Action<ObjectCollection> OnCollectionUpdated;

        /// <summary>
        /// List of objects with generated data on the object.
        /// </summary>
        [SerializeField]
        public List<CollectionNode> NodeList = new List<CollectionNode>();

        /// <summary>
        /// Type of surface to map the collection to.
        /// </summary>
        [Tooltip("Type of surface to map the collection to")]
        public SurfaceTypeEnum SurfaceType = SurfaceTypeEnum.Plane;

        /// <summary>
        /// Type of sorting to use.
        /// </summary>
        [Tooltip("Type of sorting to use")]
        public SortTypeEnum SortType = SortTypeEnum.None;

        /// <summary>
        /// Should the objects in the collection face the origin of the collection
        /// </summary>
        [Tooltip("Should the objects in the collection be rotated / how should they be rotated")]
        public OrientTypeEnum OrientType = OrientTypeEnum.FaceOrigin;

        /// <summary>
        /// Whether to sort objects by row first or by column first
        /// </summary>
        [Tooltip("Whether to sort objects by row first or by column first")]
        public LayoutTypeEnum LayoutType = LayoutTypeEnum.ColumnThenRow;

        /// <summary>
        /// Whether to treat inactive transforms as 'invisible'
        /// </summary>
        public bool IgnoreInactiveTransforms = true;

        /// <summary>
        /// This is the radius of either the Cylinder or Sphere mapping and is ignored when using the plane mapping.
        /// </summary>
        [Range(0.05f, 5.0f)]
        [Tooltip("Radius for the sphere or cylinder")]
        public float Radius = 2f;

        /// <summary>
        /// Number of rows per column, column number is automatically determined
        /// </summary>
        [Tooltip("Number of rows per column")]
        public int Rows = 3;

        /// <summary>
        /// Width of the cell per object in the collection.
        /// </summary>
        [Tooltip("Width of cell per object")]
        public float CellWidth = 0.5f;

        /// <summary>
        /// Height of the cell per object in the collection.
        /// </summary>
        [Tooltip("Height of cell per object")]
        public float CellHeight = 0.5f;

        /// <summary>
        /// Reference mesh to use for rendering the sphere layout
        /// </summary>
        [HideInInspector]
        public Mesh SphereMesh;

        /// <summary>
        /// Reference mesh to use for rendering the cylinder layout
        /// </summary>
        [HideInInspector]
        public Mesh CylinderMesh;
        #endregion


        #region private variables
        private int _columns;
        private float _width;
        private float _height;
        private float _circumference;
        private Vector2 _halfCell;
        #endregion

        public float Width
        {
            get { return _width; }
        }

        public float Height
        {
            get { return _height; }
        }

        /// <summary>
        /// Update collection is called from the editor button on the inspector.
        /// This function rebuilds / updates the layout.
        /// </summary>
        public void UpdateCollection()
        {
            // Check for empty nodes and remove them
            List<CollectionNode> emptyNodes = new List<CollectionNode>();

            for (int i = 0; i < NodeList.Count; i++)
            {
                if (NodeList[i].transform == null || (IgnoreInactiveTransforms && !NodeList[i].transform.gameObject.activeSelf) || NodeList[i].transform.parent==null || !(NodeList[i].transform.parent.gameObject==this.gameObject))
                {
                    emptyNodes.Add(NodeList[i]);
                }
            }

            // Now delete the empty nodes
            for (int i = 0; i < emptyNodes.Count; i++)
            {
                NodeList.Remove(emptyNodes[i]);
            }

            emptyNodes.Clear();

            // Check when children change and adjust
            for (int i = 0; i < this.transform.childCount; i++)
            {
                Transform child = this.transform.GetChild(i);

                if (!ContainsNode(child) && (child.gameObject.activeSelf || !IgnoreInactiveTransforms))
                {
                    CollectionNode node = new CollectionNode();

                    node.Name = child.name;
                    node.transform = child;
                    NodeList.Add(node);
                }
            }

            switch (SortType)
            {
                case SortTypeEnum.None:
                    break;

                case SortTypeEnum.Transform:
                    NodeList.Sort(delegate (CollectionNode c1, CollectionNode c2) { return c1.transform.GetSiblingIndex().CompareTo(c2.transform.GetSiblingIndex()); });
                    break;

                case SortTypeEnum.Alphabetical:
                    NodeList.Sort(delegate (CollectionNode c1, CollectionNode c2) { return c1.Name.CompareTo(c2.Name); });
                    break;

                case SortTypeEnum.AlphabeticalReversed:
                    NodeList.Sort(delegate (CollectionNode c1, CollectionNode c2) { return c1.Name.CompareTo(c2.Name); });
                    NodeList.Reverse();
                    break;

                case SortTypeEnum.TransformReversed:
                    NodeList.Sort(delegate (CollectionNode c1, CollectionNode c2) { return c1.transform.GetSiblingIndex().CompareTo(c2.transform.GetSiblingIndex()); });
                    NodeList.Reverse();
                    break;
            }

            _columns = Mathf.CeilToInt((float)NodeList.Count / Rows);
            _width = _columns * CellWidth;
            _height = Rows * CellHeight;
            _halfCell = new Vector2(CellWidth / 2f, CellHeight / 2f);
            _circumference = 2f * Mathf.PI * Radius;

            LayoutChildren();

            if (OnCollectionUpdated != null)
            {
                OnCollectionUpdated.Invoke(this);
            }
        }

        /// <summary>
        /// Internal function for laying out all the children when UpdateCollection is called.
        /// </summary>
        private void LayoutChildren() {
        
            int cellCounter = 0;
            float startOffsetX;
            float startOffsetY;

            Vector3[] nodeGrid = new Vector3[NodeList.Count];
            Vector3 newPos = Vector3.zero;
            Vector3 newRot = Vector3.zero;

            // Now lets lay out the grid
            startOffsetX = (_columns * 0.5f) * CellWidth;
            startOffsetY = (Rows * 0.5f) * CellHeight;

            cellCounter = 0;
            
            // First start with a grid then project onto surface
            switch (LayoutType)
            {
                case LayoutTypeEnum.ColumnThenRow:
                default:
                    for (int c = 0; c < _columns; c++)
                    {
                        for (int r = 0; r < Rows; r++)
                        {
                            if (cellCounter < NodeList.Count)
                            {
                                nodeGrid[cellCounter] = new Vector3((c * CellWidth) - startOffsetX + _halfCell.x, -(r * CellHeight) + startOffsetY - _halfCell.y, 0f) + (Vector3)((NodeList[cellCounter])).Offset;
                            }
                            cellCounter++;
                        }
                    }
                    break;

                case LayoutTypeEnum.RowThenColumn:
                    for (int r = 0; r < Rows; r++)
                    {
                        for (int c = 0; c < _columns; c++)
                        {
                            if (cellCounter < NodeList.Count)
                            {
                                nodeGrid[cellCounter] = new Vector3((c * CellWidth) - startOffsetX + _halfCell.x, -(r * CellHeight) + startOffsetY - _halfCell.y, 0f) + (Vector3)((NodeList[cellCounter])).Offset;
                            }
                            cellCounter++;
                        }
                    }
                    break;

            }

            switch (SurfaceType) {
                case SurfaceTypeEnum.Plane:
                    for (int i = 0; i < NodeList.Count; i++)
                    {
                        newPos = nodeGrid[i];
                        NodeList[i].transform.localPosition = newPos;
                        switch (OrientType)
                        {
                            case OrientTypeEnum.FaceOrigin:
                            case OrientTypeEnum.FaceFoward:
                                NodeList[i].transform.forward = transform.forward;
                                break;

                            case OrientTypeEnum.FaceOriginReversed:
                            case OrientTypeEnum.FaceForwardReversed:
                                newRot = Vector3.zero;
                                NodeList[i].transform.forward = transform.forward;
                                NodeList[i].transform.Rotate(0f, 180f, 0f);
                                break;

                            case OrientTypeEnum.None:
                                break;

                            default:
                                throw new ArgumentOutOfRangeException();
                        }
                    }
                    break;
                case SurfaceTypeEnum.Cylinder:
                    for (int i = 0; i < NodeList.Count; i++)
                    {
                        newPos = CylindricalMapping(nodeGrid[i], Radius);
                        switch (OrientType)
                        {
                            case OrientTypeEnum.FaceOrigin:
                                newRot = new Vector3(newPos.x, 0.0f, newPos.z);
                                NodeList[i].transform.rotation = Quaternion.LookRotation(newRot);
                                break;

                            case OrientTypeEnum.FaceOriginReversed:
                                newRot = new Vector3(newPos.x, 0f, newPos.z);
                                NodeList[i].transform.rotation = Quaternion.LookRotation(newRot);
                                NodeList[i].transform.Rotate(0f, 180f, 0f);
                                break;

                            case OrientTypeEnum.FaceFoward:
                                NodeList[i].transform.forward = transform.forward;
                                break;

                            case OrientTypeEnum.FaceForwardReversed:
                                NodeList[i].transform.forward = transform.forward;
                                NodeList[i].transform.Rotate(0f, 180f, 0f);
                                break;

                            case OrientTypeEnum.None:
                                break;

                            default:
                                throw new ArgumentOutOfRangeException();
                        }
                        NodeList[i].transform.localPosition = newPos;
                    }
                    break;
                case SurfaceTypeEnum.Sphere:

                    for (int i = 0; i < NodeList.Count; i++)
                    {
                        newPos = SphericalMapping(nodeGrid[i], Radius);
                        switch (OrientType)
                        {
                            case OrientTypeEnum.FaceOrigin:
                                newRot = newPos;
                                NodeList[i].transform.rotation = Quaternion.LookRotation(newRot);
                                break;

                            case OrientTypeEnum.FaceOriginReversed:
                                newRot = newPos;
                                NodeList[i].transform.rotation = Quaternion.LookRotation(newRot);
                                NodeList[i].transform.Rotate(0f, 180f, 0f);
                                break;

                            case OrientTypeEnum.FaceFoward:
                                NodeList[i].transform.forward = transform.forward;
                                break;

                            case OrientTypeEnum.FaceForwardReversed:
                                NodeList[i].transform.forward = transform.forward;
                                NodeList[i].transform.Rotate(0f, 180f, 0f);
                                break;

                            case OrientTypeEnum.None:
                                break;

                            default:
                                throw new ArgumentOutOfRangeException();
                        }
                        NodeList[i].transform.localPosition = newPos;
                    }
                    break;

                case SurfaceTypeEnum.Scatter:
                    // Get randomized planar mapping
                    // Calculate radius of each node while we're here
                    // Then use the packer function to shift them into place
                    for (int i = 0; i < NodeList.Count; i++)
                    {
                        newPos = ScatterMapping (nodeGrid[i], Radius);
                        Collider nodeCollider = NodeList[i].transform.GetComponentInChildren<Collider>();
                        if (nodeCollider != null)
                        {
                            // Make the radius the largest of the object's dimensions to avoid overlap
                            Bounds bounds = nodeCollider.bounds;
                            NodeList[i].Radius = Mathf.Max (Mathf.Max(bounds.size.x, bounds.size.y), bounds.size.z) / 2;
                        }
                        else
                        {
                            // Make the radius a default value
                            // TODO move this into a public field ?
                            NodeList[i].Radius = 1f;
                        }
                        NodeList[i].transform.localPosition = newPos;
                        switch (OrientType)
                        {
                            case OrientTypeEnum.FaceOrigin:
                            case OrientTypeEnum.FaceFoward:
                                NodeList[i].transform.rotation = Quaternion.LookRotation(newRot);
                                break;

                            case OrientTypeEnum.FaceOriginReversed:
                            case OrientTypeEnum.FaceForwardReversed:
                                NodeList[i].transform.rotation = Quaternion.LookRotation(newRot);
                                NodeList[i].transform.Rotate(0f, 180f, 0f);
                                break;

                            case OrientTypeEnum.None:
                                break;

                            default:
                                throw new ArgumentOutOfRangeException();
                        }
                    }

                    // Iterate [x] times
                    // TODO move center, iterations and padding into a public field
                    for (int i = 0; i < 100; i++)
                    {
                        IterateScatterPacking (NodeList, Radius);
                    }
                    break;
            }
        }

        /// <summary>
        /// Internal function for getting the relative mapping based on a source Vec3 and a radius for spherical mapping.
        /// </summary>
        /// <param name="source">The source <see cref="Vector3"/> to be mapped to sphere</param>
        /// <param name="radius">This is a <see cref="float"/> for the radius of the sphere</param>
        /// <returns></returns>
        private Vector3 SphericalMapping(Vector3 source, float radius)
        {
            Radius = radius >= 0 ? Radius : radius;
            Vector3 newPos = new Vector3(0f, 0f, Radius);

            float xAngle = (source.x / _circumference) * 360f;
            float yAngle = -(source.y / _circumference) * 360f;

            Quaternion rot = Quaternion.Euler(yAngle, xAngle, 0.0f);
            newPos = rot * newPos;

            return newPos;
        }

        /// <summary>
        /// Internal function for getting the relative mapping based on a source Vec3 and a radius for cylinder mapping.
        /// </summary>
        /// <param name="source">The source <see cref="Vector3"/> to be mapped to cylinder</param>
        /// <param name="radius">This is a <see cref="float"/> for the radius of the cylinder</param>
        /// <returns></returns>
        private Vector3 CylindricalMapping(Vector3 source, float radius)
        {
            Radius = radius >= 0 ? Radius : radius;
            Vector3 newPos = new Vector3(0f, source.y, Radius);

            float xAngle = (source.x / _circumference) * 360f;

            Quaternion rot = Quaternion.Euler(0.0f, xAngle, 0.0f);
            newPos = rot * newPos;

            return newPos;
        }

        /// <summary>
        /// Internal function to check if a node exists in the NodeList.
        /// </summary>
        /// <param name="node">A <see cref="Transform"/> of the node to see if it's in the NodeList</param>
        /// <returns></returns>
        private bool ContainsNode(Transform node)
        {
            for (int i = 0; i < NodeList.Count; i++)
            {
                if (NodeList[i] != null)
                {
                    if (NodeList[i].transform == node)
                    {
                        return true;
                    }
                }
            }
            return false;
        }

        /// <summary>
        /// Internal function for randomized mapping based on a source Vec3 and a radius for randomization distance.
        /// </summary>
        /// <param name="source">The source <see cref="Vector3"/> to be mapped to cylinder</param>
        /// <param name="radius">This is a <see cref="float"/> for the radius of the cylinder</param>
        /// <returns></returns>
        private Vector3 ScatterMapping(Vector3 source, float radius)
        {
            source.x = UnityEngine.Random.Range(-radius, radius);
            source.y = UnityEngine.Random.Range(-radius, radius);
            return source;
        }

    
        /// <summary>
        /// Internal function to pack randomly spaced nodes so they don't overlap
        /// Usually requires about 25 iterations for decent packing
        /// </summary>
        /// <returns></returns>
        private void IterateScatterPacking(List<CollectionNode> nodes, float radiusPadding)
        {
            // Sort by closest to center (don't worry about z axis)
            // Use the position of the collection as the packing center
            nodes.Sort(delegate (CollectionNode circle1, CollectionNode circle2) {
                float distance1 = (circle1.transform.localPosition).sqrMagnitude;
                float distance2 = (circle2.transform.localPosition).sqrMagnitude;
                return distance1.CompareTo(distance2);
            });
            
            Vector3 difference;
            Vector2 difference2D;

           // Move them closer together
           float radiusPaddingSquared = Mathf.Pow(radiusPadding, 2f);

           for (int i = 0; i < nodes.Count - 1; i++)
           {
                for (int j = i + 1; j < nodes.Count; j++)
                {
                    if (i != j)
                    {
                        difference = nodes[j].transform.localPosition - nodes[i].transform.localPosition;
                        // Ignore Z axis
                        difference2D.x = difference.x;
                        difference2D.y = difference.y;
                        float combinedRadius = nodes[i].Radius + nodes[j].Radius;
                        float distance = difference2D.SqrMagnitude() - radiusPaddingSquared;
                        float minSeparation = Mathf.Min(distance, radiusPaddingSquared);
                        distance -= minSeparation;

                        if (distance < (Mathf.Pow(combinedRadius, 2)))
                        {
                            difference2D.Normalize();
                            difference *= ((combinedRadius - Mathf.Sqrt(distance)) * 0.5f);
                            nodes[j].transform.localPosition += difference;
                            nodes[i].transform.localPosition -= difference;
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Gizmos to draw when the Collection is selected.
        /// </summary>
        protected virtual void OnDrawGizmosSelected()
        {
            Vector3 scale = (2f * Radius) * Vector3.one;
            switch (SurfaceType)
            {
                case SurfaceTypeEnum.Plane:
                    break;
                case SurfaceTypeEnum.Cylinder:
                    Gizmos.color = Color.green;
                    Gizmos.DrawWireMesh(CylinderMesh, transform.position, transform.rotation, scale);
                    break;
                case SurfaceTypeEnum.Sphere:
                    Gizmos.color = Color.green;
                    Gizmos.DrawWireMesh(SphereMesh, transform.position, transform.rotation, scale);
                    break;
            }
        }
    }
}
