// --------------------------------------------------------------------------------------------------------------------
// <copyright file="CullArea.cs" company="Exit Games GmbH">
//   Part of: Photon Unity Utilities,
// </copyright>
// <summary>
//  Represents the cull area used for network culling.
// </summary>
// <author>developer@exitgames.com</author>
// --------------------------------------------------------------------------------------------------------------------

using System.Collections.Generic;
using UnityEngine;

namespace Photon.Pun.UtilityScripts
{
    using System;


    /// <summary>
    ///     Represents the cull area used for network culling.
    /// </summary>
    public class CullArea : MonoBehaviour
    {
        private const int MAX_NUMBER_OF_ALLOWED_CELLS = 250;

        public const int MAX_NUMBER_OF_SUBDIVISIONS = 3;

        /// <summary>
        ///     This represents the first ID which is assigned to the first created cell.
        ///     If you already have some interest groups blocking this first ID, fell free to change it.
        ///     However increasing the first group ID decreases the maximum amount of allowed cells.
        ///     Allowed values are in range from 1 to 250.
        /// </summary>
        public readonly byte FIRST_GROUP_ID = 1;

        /// <summary>
        ///     This represents the order in which updates are sent. 
        ///     The number represents the subdivision of the cell hierarchy:
        ///     - 0: message is sent to all players
        ///     - 1: message is sent to players who are interested in the matching cell of the first subdivision
        ///     If there is only one subdivision we are sending one update to all players
        ///     before sending three consequent updates only to players who are in the same cell
        ///     or interested in updates of the current cell.
        /// </summary>
        public readonly int[] SUBDIVISION_FIRST_LEVEL_ORDER = new int[4] { 0, 1, 1, 1 };

        /// <summary>
        ///     This represents the order in which updates are sent.
        ///     The number represents the subdivision of the cell hierarchy:
        ///     - 0: message is sent to all players
        ///     - 1: message is sent to players who are interested in the matching cell of the first subdivision
        ///     - 2: message is sent to players who are interested in the matching cell of the second subdivision
        ///     If there are two subdivisions we are sending every second update only to players
        ///     who are in the same cell or interested in updates of the current cell.
        /// </summary>
        public readonly int[] SUBDIVISION_SECOND_LEVEL_ORDER = new int[8] { 0, 2, 1, 2, 0, 2, 1, 2 };

        /// <summary>
        ///     This represents the order in which updates are sent.
        ///     The number represents the subdivision of the cell hierarchy:
        ///     - 0: message is sent to all players
        ///     - 1: message is sent to players who are interested in the matching cell of the first subdivision
        ///     - 2: message is sent to players who are interested in the matching cell of the second subdivision
        ///     - 3: message is sent to players who are interested in the matching cell of the third subdivision
        ///     If there are two subdivisions we are sending every second update only to players
        ///     who are in the same cell or interested in updates of the current cell.
        /// </summary>
        public readonly int[] SUBDIVISION_THIRD_LEVEL_ORDER = new int[12] { 0, 3, 2, 3, 1, 3, 2, 3, 1, 3, 2, 3 };

        public Vector2 Center;
        public Vector2 Size = new Vector2(25.0f, 25.0f);

        public Vector2[] Subdivisions = new Vector2[MAX_NUMBER_OF_SUBDIVISIONS];

        public int NumberOfSubdivisions;

        public int CellCount { get; private set; }

        public CellTree CellTree { get; private set; }

        public Dictionary<int, GameObject> Map { get; private set; }

        public bool YIsUpAxis = false;
        public bool RecreateCellHierarchy = false;

        private byte idCounter;

        /// <summary>
        ///     Creates the cell hierarchy at runtime.
        /// </summary>
        private void Awake()
        {
            this.idCounter = this.FIRST_GROUP_ID;

            this.CreateCellHierarchy();
        }

        /// <summary>
        ///     Creates the cell hierarchy in editor and draws the cell view.
        /// </summary>
        public void OnDrawGizmos()
        {
            this.idCounter = this.FIRST_GROUP_ID;

            if (this.RecreateCellHierarchy)
            {
                this.CreateCellHierarchy();
            }

            this.DrawCells();
        }

        /// <summary>
        ///     Creates the cell hierarchy.
        /// </summary>
        private void CreateCellHierarchy()
        {
            if (!this.IsCellCountAllowed())
            {
                if (Debug.isDebugBuild)
                {
                    Debug.LogError("There are too many cells created by your subdivision options. Maximum allowed number of cells is " + (MAX_NUMBER_OF_ALLOWED_CELLS - this.FIRST_GROUP_ID) +
                                   ". Current number of cells is " + this.CellCount + ".");
                    return;
                }
                else
                {
                    Application.Quit();
                }
            }

            CellTreeNode rootNode = new CellTreeNode(this.idCounter++, CellTreeNode.ENodeType.Root, null);

            if (this.YIsUpAxis)
            {
                this.Center = new Vector2(transform.position.x, transform.position.y);
                this.Size = new Vector2(transform.localScale.x, transform.localScale.y);

                rootNode.Center = new Vector3(this.Center.x, this.Center.y, 0.0f);
                rootNode.Size = new Vector3(this.Size.x, this.Size.y, 0.0f);
                rootNode.TopLeft = new Vector3((this.Center.x - (this.Size.x / 2.0f)), (this.Center.y - (this.Size.y / 2.0f)), 0.0f);
                rootNode.BottomRight = new Vector3((this.Center.x + (this.Size.x / 2.0f)), (this.Center.y + (this.Size.y / 2.0f)), 0.0f);
            }
            else
            {
                this.Center = new Vector2(transform.position.x, transform.position.z);
                this.Size = new Vector2(transform.localScale.x, transform.localScale.z);

                rootNode.Center = new Vector3(this.Center.x, 0.0f, this.Center.y);
                rootNode.Size = new Vector3(this.Size.x, 0.0f, this.Size.y);
                rootNode.TopLeft = new Vector3((this.Center.x - (this.Size.x / 2.0f)), 0.0f, (this.Center.y - (this.Size.y / 2.0f)));
                rootNode.BottomRight = new Vector3((this.Center.x + (this.Size.x / 2.0f)), 0.0f, (this.Center.y + (this.Size.y / 2.0f)));
            }

            this.CreateChildCells(rootNode, 1);

            this.CellTree = new CellTree(rootNode);

            this.RecreateCellHierarchy = false;
        }

        /// <summary>
        ///     Creates all child cells.
        /// </summary>
        /// <param name="parent">The current parent node.</param>
        /// <param name="cellLevelInHierarchy">The cell level within the current hierarchy.</param>
        private void CreateChildCells(CellTreeNode parent, int cellLevelInHierarchy)
        {
            if (cellLevelInHierarchy > this.NumberOfSubdivisions)
            {
                return;
            }

            int rowCount = (int)this.Subdivisions[(cellLevelInHierarchy - 1)].x;
            int columnCount = (int)this.Subdivisions[(cellLevelInHierarchy - 1)].y;

            float startX = parent.Center.x - (parent.Size.x / 2.0f);
            float width = parent.Size.x / rowCount;

            for (int row = 0; row < rowCount; ++row)
            {
                for (int column = 0; column < columnCount; ++column)
                {
                    float xPos = startX + (row * width) + (width / 2.0f);

                    CellTreeNode node = new CellTreeNode(this.idCounter++, (this.NumberOfSubdivisions == cellLevelInHierarchy) ? CellTreeNode.ENodeType.Leaf : CellTreeNode.ENodeType.Node, parent);

                    if (this.YIsUpAxis)
                    {
                        float startY = parent.Center.y - (parent.Size.y / 2.0f);
                        float height = parent.Size.y / columnCount;
                        float yPos = startY + (column * height) + (height / 2.0f);

                        node.Center = new Vector3(xPos, yPos, 0.0f);
                        node.Size = new Vector3(width, height, 0.0f);
                        node.TopLeft = new Vector3(xPos - (width / 2.0f), yPos - (height / 2.0f), 0.0f);
                        node.BottomRight = new Vector3(xPos + (width / 2.0f), yPos + (height / 2.0f), 0.0f);
                    }
                    else
                    {
                        float startZ = parent.Center.z - (parent.Size.z / 2.0f);
                        float depth = parent.Size.z / columnCount;
                        float zPos = startZ + (column * depth) + (depth / 2.0f);

                        node.Center = new Vector3(xPos, 0.0f, zPos);
                        node.Size = new Vector3(width, 0.0f, depth);
                        node.TopLeft = new Vector3(xPos - (width / 2.0f), 0.0f, zPos - (depth / 2.0f));
                        node.BottomRight = new Vector3(xPos + (width / 2.0f), 0.0f, zPos + (depth / 2.0f));
                    }

                    parent.AddChild(node);

                    this.CreateChildCells(node, (cellLevelInHierarchy + 1));
                }
            }
        }

        /// <summary>
        ///     Draws the cells.
        /// </summary>
        private void DrawCells()
        {
            if ((this.CellTree != null) && (this.CellTree.RootNode != null))
            {
                this.CellTree.RootNode.Draw();
            }
            else
            {
                this.RecreateCellHierarchy = true;
            }
        }

        /// <summary>
        ///     Checks if the cell count is allowed.
        /// </summary>
        /// <returns>True if the cell count is allowed, false if the cell count is too large.</returns>
        private bool IsCellCountAllowed()
        {
            int horizontalCells = 1;
            int verticalCells = 1;

            foreach (Vector2 v in this.Subdivisions)
            {
                horizontalCells *= (int)v.x;
                verticalCells *= (int)v.y;
            }

            this.CellCount = horizontalCells * verticalCells;

            return (this.CellCount <= (MAX_NUMBER_OF_ALLOWED_CELLS - this.FIRST_GROUP_ID));
        }

        /// <summary>
        ///     Gets a list of all cell IDs the player is currently inside or nearby.
        /// </summary>
        /// <param name="position">The current position of the player.</param>
        /// <returns>A list containing all cell IDs the player is currently inside or nearby.</returns>
        public List<byte> GetActiveCells(Vector3 position)
        {
            List<byte> activeCells = new List<byte>(0);
            this.CellTree.RootNode.GetActiveCells(activeCells, this.YIsUpAxis, position);

            // it makes sense to sort the "nearby" cells. those are in the list in positions after the subdivisions the point is inside. 2 subdivisions result in 3 areas the point is in.
            int cellsActive = this.NumberOfSubdivisions + 1;
            int cellsNearby = activeCells.Count - cellsActive;
            if (cellsNearby > 0)
            {
                activeCells.Sort(cellsActive, cellsNearby, new ByteComparer());
            }
            return activeCells;
        }
    }

    /// <summary>
    ///     Represents the tree accessible from its root node.
    /// </summary>
    public class CellTree
    {
        /// <summary>
        ///     Represents the root node of the cell tree.
        /// </summary>
        public CellTreeNode RootNode { get; private set; }

        /// <summary>
        ///     Default constructor.
        /// </summary>
        public CellTree()
        {
        }

        /// <summary>
        ///     Constructor to define the root node.
        /// </summary>
        /// <param name="root">The root node of the tree.</param>
        public CellTree(CellTreeNode root)
        {
            this.RootNode = root;
        }
    }

    /// <summary>
    ///     Represents a single node of the tree.
    /// </summary>
    public class CellTreeNode
    {
        public enum ENodeType : byte
        {
            Root = 0,
            Node = 1,
            Leaf = 2
        }

        /// <summary>
        ///     Represents the unique ID of the cell.
        /// </summary>
        public byte Id;

        /// <summary>
        ///     Represents the center, top-left or bottom-right position of the cell
        ///     or the size of the cell.
        /// </summary>
        public Vector3 Center, Size, TopLeft, BottomRight;

        /// <summary>
        ///     Describes the current node type of the cell tree node.
        /// </summary>
        public ENodeType NodeType;

        /// <summary>
        ///     Reference to the parent node.
        /// </summary>
        public CellTreeNode Parent;

        /// <summary>
        ///     A list containing all child nodes.
        /// </summary>
        public List<CellTreeNode> Childs;

        /// <summary>
        ///     The max distance the player can have to the center of the cell for being 'nearby'.
        ///     This is calculated once at runtime.
        /// </summary>
        private float maxDistance;

        /// <summary>
        ///     Default constructor.
        /// </summary>
        public CellTreeNode()
        {
        }

        /// <summary>
        ///     Constructor to define the ID and the node type as well as setting a parent node.
        /// </summary>
        /// <param name="id">The ID of the cell is used as the interest group.</param>
        /// <param name="nodeType">The node type of the cell tree node.</param>
        /// <param name="parent">The parent node of the cell tree node.</param>
        public CellTreeNode(byte id, ENodeType nodeType, CellTreeNode parent)
        {
            this.Id = id;

            this.NodeType = nodeType;

            this.Parent = parent;
        }

        /// <summary>
        ///     Adds the given child to the node.
        /// </summary>
        /// <param name="child">The child which is added to the node.</param>
        public void AddChild(CellTreeNode child)
        {
            if (this.Childs == null)
            {
                this.Childs = new List<CellTreeNode>(1);
            }

            this.Childs.Add(child);
        }

        /// <summary>
        ///     Draws the cell in the editor.
        /// </summary>
        public void Draw()
        {
#if UNITY_EDITOR
        if (this.Childs != null)
        {
            foreach (CellTreeNode node in this.Childs)
            {
                node.Draw();
            }
        }

        Gizmos.color = new Color((this.NodeType == ENodeType.Root) ? 1 : 0, (this.NodeType == ENodeType.Node) ? 1 : 0, (this.NodeType == ENodeType.Leaf) ? 1 : 0);
        Gizmos.DrawWireCube(this.Center, this.Size);

        byte offset = (byte)this.NodeType;
        GUIStyle gs = new GUIStyle() { fontStyle = FontStyle.Bold };
        gs.normal.textColor = Gizmos.color;
        UnityEditor.Handles.Label(this.Center+(Vector3.forward*offset*1f), this.Id.ToString(), gs);
#endif
        }

        /// <summary>
        ///     Gathers all cell IDs the player is currently inside or nearby.
        /// </summary>
        /// <param name="activeCells">The list to add all cell IDs to the player is currently inside or nearby.</param>
        /// <param name="yIsUpAxis">Describes if the y-axis is used as up-axis.</param>
        /// <param name="position">The current position of the player.</param>
        public void GetActiveCells(List<byte> activeCells, bool yIsUpAxis, Vector3 position)
        {
            if (this.NodeType != ENodeType.Leaf)
            {
                foreach (CellTreeNode node in this.Childs)
                {
                    node.GetActiveCells(activeCells, yIsUpAxis, position);
                }
            }
            else
            {
                if (this.IsPointNearCell(yIsUpAxis, position))
                {
                    if (this.IsPointInsideCell(yIsUpAxis, position))
                    {
                        activeCells.Insert(0, this.Id);

                        CellTreeNode p = this.Parent;
                        while (p != null)
                        {
                            activeCells.Insert(0, p.Id);

                            p = p.Parent;
                        }
                    }
                    else
                    {
                        activeCells.Add(this.Id);
                    }
                }
            }
        }

        /// <summary>
        ///     Checks if the given point is inside the cell.
        /// </summary>
        /// <param name="yIsUpAxis">Describes if the y-axis is used as up-axis.</param>
        /// <param name="point">The point to check.</param>
        /// <returns>True if the point is inside the cell, false if the point is not inside the cell.</returns>
        public bool IsPointInsideCell(bool yIsUpAxis, Vector3 point)
        {
            if ((point.x < this.TopLeft.x) || (point.x > this.BottomRight.x))
            {
                return false;
            }

            if (yIsUpAxis)
            {
                if ((point.y >= this.TopLeft.y) && (point.y <= this.BottomRight.y))
                {
                    return true;
                }
            }
            else
            {
                if ((point.z >= this.TopLeft.z) && (point.z <= this.BottomRight.z))
                {
                    return true;
                }
            }

            return false;
        }

        /// <summary>
        ///     Checks if the given point is near the cell.
        /// </summary>
        /// <param name="yIsUpAxis">Describes if the y-axis is used as up-axis.</param>
        /// <param name="point">The point to check.</param>
        /// <returns>True if the point is near the cell, false if the point is too far away.</returns>
        public bool IsPointNearCell(bool yIsUpAxis, Vector3 point)
        {
            if (this.maxDistance == 0.0f)
            {
                this.maxDistance = (this.Size.x + this.Size.y + this.Size.z) / 2.0f;
            }

            return ((point - this.Center).sqrMagnitude <= (this.maxDistance * this.maxDistance));
        }
    }


    public class ByteComparer : IComparer<byte>
    {
        /// <inheritdoc />
        public int Compare(byte x, byte y)
        {
            return x == y ? 0 : x < y ? -1 : 1;
        }
    }
}