using System.Collections.Generic;
using EscapeRoomEngine.Engine.Runtime.Modules;
using EscapeRoomEngine.Engine.Runtime.Modules.Description;
using EscapeRoomEngine.Engine.Runtime.UI;
using EscapeRoomEngine.Engine.Runtime.Utilities;
using UnityEngine;
using Object = UnityEngine.Object;

namespace EscapeRoomEngine.Engine.Runtime
{
    /// <summary>
    /// The space is responsible for placing and instantiating any modules in it.
    /// </summary>
    public class Space
    {
        /// <summary>
        /// The room relative (<i>RR</i>) dimensions of this space.
        /// </summary>
        internal readonly Placement rrPlacement;
        /// <summary>
        /// The room this space is in.
        /// </summary>
        internal readonly Room room;
        
        /// <summary>
        /// A list of all modules.
        /// </summary>
        internal List<Module> Modules { get; } = new(2);
        /// <summary>
        /// A list of all staged and added modules.
        /// </summary>
        internal List<Module> AllModules
        {
            get
            {
                var modules = new List<Module>(Modules);
                if (_stagedModules.Count > 0)
                {
                    modules.AddRange(_stagedModules);
                }
                return modules;
            }
        }

        protected GameObject spaceObject;
        
        private GameObject _spaceTiles;
        private List<Module> _stagedModules = new();

        internal Space(Room room, Passage entrance)
        {
            this.room = room;
            rrPlacement = new Placement
            {
                position = Vector3Int.zero,
                size = new Vector2Int(GameControl.Instance.RoomSize.x, GameControl.Instance.RoomSize.y)
            };
            
            // connect the space to its entrance passage
            entrance.PlaceEntrance(new DoorModule(this,
                ((DoorModuleDescription)entrance.fromOut.description).connectedDoorDescription));
            Modules.Add(entrance.toIn);
        }

        internal bool AddModuleWithRequirements(Module module)
        {
            var requirementsFulfilled = module.CheckRequirements();
            if (requirementsFulfilled)
            {
                // add all previously staged modules first ...
                for (var i = _stagedModules.Count - 1; i >= 0; i--)
                {
                    Modules.Add(_stagedModules[i]);
                }
                
                // ... and then add the module itself
                Modules.Add(module);
            }
            
            // always clear the staged modules. if a module isn't added, its corresponding staged modules should be discarded
            _stagedModules = new List<Module>();

            return requirementsFulfilled;
        }

        /// <summary>
        /// Stage a module if its requirements are currently met. It will be added as soon as the next module is added by <see cref="AddModuleWithRequirements"/>.
        /// </summary>
        /// <param name="module">The module to stage</param>
        /// <returns>Whether the requirements were met and the module was staged</returns>
        internal bool StageModuleWithRequirements(Module module)
        {
            var requirementsFulfilled = module.CheckRequirements();
            if (requirementsFulfilled)
            {
                _stagedModules.Add(module);
            }

            return requirementsFulfilled;
        }

        internal virtual void InstantiateSpace(Transform parent, string name)
        {
            spaceObject = new GameObject($"Space {name}");
            spaceObject.transform.SetParent(parent, false);
            spaceObject.transform.localPosition = new Vector3(rrPlacement.position.x, 0, rrPlacement.position.z);
            
            // build the space floor out of tiles
            _spaceTiles = new GameObject($"Space Geometry");
            _spaceTiles.transform.SetParent(spaceObject.transform, false);
            _spaceTiles.isStatic = true;
            for (var z = 0; z < rrPlacement.size.y; z++)
            {
                for (var x = 0; x < rrPlacement.size.x; x++)
                {
                    var left = x == 0;
                    var right = x == rrPlacement.size.x - 1;
                    var bottom = z == 0;
                    var top = z == rrPlacement.size.y - 1;
                    
                    TileLocation location;
                    if (bottom)
                        location = left ? TileLocation.SW : right ? TileLocation.SE : TileLocation.S;
                    else if (top)
                        location = left ? TileLocation.NW : right ? TileLocation.NE : TileLocation.N;
                    else
                        location = left ? TileLocation.W : right ? TileLocation.E : TileLocation.C;
                    
                    var tileObject = Object.Instantiate(Engine.Theme.spaceTile, _spaceTiles.transform, false);
                    tileObject.transform.localPosition = new Vector3(x, 0, z);
                    tileObject.showTile = location;
                }
            }

            // instantiate all modules inside this space
            Modules.ForEach(module => module.InstantiateModule(spaceObject.transform));
        }

        /// <summary>
        /// Convert a position relative to this space to one relative to the room.
        /// </summary>
        /// <param name="srPosition">The space relative (<i>SR</i>) position that should be converted to a room relative (<i>RR</i>) position.</param>
        internal Vector3Int ToRoomRelative(Vector3Int srPosition) => srPosition + rrPlacement.position;
        /// <summary>
        /// Convert a position relative to the room to one relative to this space.
        /// </summary>
        /// <param name="rrPosition">The room relative (<i>RR</i>) position that should be converted to a space relative (<i>SR</i>) position.</param>
        internal Vector3Int ToSpaceRelative(Vector3Int rrPosition) => rrPosition - rrPlacement.position;
    }
}
