using System.Collections.Generic;
using System.Linq;
using EscapeRoomEngine.Engine.Runtime.Measurements;
using EscapeRoomEngine.Engine.Runtime.Modules;
using EscapeRoomEngine.Engine.Runtime.UI;
using EscapeRoomEngine.Engine.Runtime.Utilities;
using NaughtyAttributes;
using UnityEngine;
using Logger = EscapeRoomEngine.Engine.Runtime.Utilities.Logger;
using LogType = EscapeRoomEngine.Engine.Runtime.Utilities.LogType;

namespace EscapeRoomEngine.Engine.Runtime
{
    /// <summary>
    /// The engine controls the whole escape room. It generates rooms and manages the puzzle plan.
    /// </summary>
    public class Engine : MonoBehaviour
    {
        /// <summary>
        /// The active theme of the engine.
        /// </summary>
        public static EngineTheme Theme => Instance.theme;
        /// <summary>
        /// The active instance of the engine.
        /// </summary>
        public static Engine Instance { get; private set; }

        public delegate void UpdateUIHandler();
        public event UpdateUIHandler UpdateUIEvent;

        [InfoBox("If a space was generated without any puzzles in it, the engine will try generating another new space. To prevent infinite loops, the amount of retries is bound.")]
        public int maxSpaceGenerationTries = 1000;
        [Tooltip("The engine will try to generate a room that takes approximately this many seconds to complete.")]
        public float initialTargetTime = 10 * 60;
        [Tooltip("The offset each room will have from the previous one.")]
        public Vector3 roomOffset = new(0, 1000, 0);
        [Tooltip("The theme of the engine that decides the available puzzles, doors and more.")]
        [Required] public EngineTheme theme;

        public int NumberOfRooms => _rooms.Count;
        public IOption<Room> CurrentRoom => NumberOfRooms > 0 ? Some<Room>.Of(_rooms[^1]) : None<Room>.New();
        /// <summary>
        /// The currendly estimated time for the player to finish the experience.
        /// </summary>
        public float EstimatedTimeRemaining { get; private set; }

        private readonly List<Room> _rooms = new();
        private List<PuzzleModuleDescription> _availablePuzzles, _plannedPuzzles;
        private GameObject _playSpaceOrigin;

        private void Awake()
        {
            Instance = this;
            
            Measure.Clear();

            _availablePuzzles = new List<PuzzleModuleDescription>(theme.puzzleTypes);
            _plannedPuzzles = new List<PuzzleModuleDescription>(_availablePuzzles);
        }

        private void Start()
        {
            _playSpaceOrigin = new GameObject("Play Space Origin");
            _playSpaceOrigin.transform.SetParent(transform);
            _playSpaceOrigin.transform.localPosition = new Vector3(-theme.playSpace.x / 2f, 0, -theme.playSpace.y / 2f);
        }

        #region Generation

        public void GenerateRoom()
        {
            Logger.Log("Generating room...", LogType.RoomGeneration);
            
            // get the last entrance from the newest room or create a spawn passage with no entrance door for where the player will start
            var entrance = NumberOfRooms > 0 ? _rooms.Last().exit : new Passage(new DoorModule(null, theme.spawnDoor));
            
            var room = new Room(entrance);
            _rooms.Add(room);
            
            if (_plannedPuzzles.Count > 0)
            {
                GeneratePuzzleSpace(room, entrance);
            }
            else
            {
                GenerateEndSpace(room, entrance);
                GameControl.Instance.StopGame();
            }

            var roomId = _rooms.Count - 1;
            room.InstantiateRoom(_playSpaceOrigin.transform, roomId * roomOffset, roomId.ToString());

            if (theme.environment)
            {
                Instantiate(theme.environment, room.roomObject.transform, false);
            }

            GameControl.Instance.TimeInRoom = 0;
            UpdateUI();
        }

        private void GeneratePuzzleSpace(Room room, Passage entrance)
        {
            var puzzlesAdded = 0;
            var tries = 0;
            Space space;
            Passage exit;
            
            // choose the next puzzle
            var puzzle = ChoosePuzzle();
            GameControl.Instance.CurrentPuzzle = puzzle;

            do
            {
                tries++;
                Logger.Log($"Generating space{(tries > 1 ? $" (try {tries})" : "")}...", LogType.RoomGeneration);

                // create space
                space = new Space(room, entrance);

                // add exit
                var exitDoor = new DoorModule(space, theme.exitDoorTypes.RandomElement());
                if (!space.AddModuleWithRequirements(exitDoor))
                {
                    throw new EngineException("Could not satisfy requirements for exit door.");
                }
                exit = new Passage(exitDoor);

                    // add puzzle
                if (space.AddModuleWithRequirements(Module.CreateModuleByType(space, puzzle)))
                {
                    puzzlesAdded++;
                }
            } while (puzzlesAdded == 0 && tries < maxSpaceGenerationTries);

            if (puzzlesAdded == 0)
            {
                Logger.Log($"Unable to create space with puzzles after {tries} tries", LogType.Important);
            }
            
            room.AddSpace(space, exit);
        }

        private void GenerateEndSpace(Room room, Passage entrance)
        {
            Logger.Log($"Generating end space...", LogType.RoomGeneration);
            
            room.AddSpace(new Space(room, entrance), null);
        }

        private PuzzleModuleDescription ChoosePuzzle()
        {
            // choose a puzzle from the plan and remove it from both the plan and later available puzzles (so it is only chosen once)
            var puzzle = _plannedPuzzles.PopRandomElement();
            _availablePuzzles.Remove(puzzle);
            return puzzle;
        }

        #endregion

        /// <summary>
        /// Updates the list of puzzles planned for this run.
        /// </summary>
        public void PlanPuzzles()
        {
            var n = _availablePuzzles.Count;
            var estimates = new int[n];
            var indices = new Range(0, n).ToArray();
            var target = Mathf.RoundToInt(GameControl.Instance.TargetTime - GameControl.Instance.EstimatedTimeRoom);
            _plannedPuzzles = new List<PuzzleModuleDescription>();

            for (var i = 0; i < n; i++)
            {
                estimates[i] = Mathf.RoundToInt(Measure.EstimateTime(_availablePuzzles[i]));
            }
            var chosen = Backtrack.Closest(indices, estimates, target);
            foreach (var i in chosen)
            {
                _plannedPuzzles.Add(_availablePuzzles[i]);
            }
            
            EstimatedTimeRemaining = Measure.EstimateTime(_plannedPuzzles);
            GameControl.Instance.PlannedPuzzles = _plannedPuzzles;
        }

        /// <summary>
        /// Hide or destroy the room two rooms ago. The actual previous room is kept for the player to be able to backtrack one room.
        /// </summary>
        /// <param name="destroy"></param>
        public void HidePreviousRoom(bool destroy = true)
        {
            if (NumberOfRooms > 2)
            {
                var room = _rooms[NumberOfRooms - 3];
                
                // lock the doors that might be used to return to the old room
                room.exit.toIn.DoorState.Lock();
                room.exit.fromOut.DoorState.Lock();

                // destroy or hide the old room
                if (destroy)
                {
                    Destroy(room.roomObject);
                }
                else
                {
                    room.roomObject.SetActive(false);
                }
            }
        }

        private void UpdateUI() => UpdateUIEvent?.Invoke();
    }
}
