using System;
using System.Collections.Generic;
using System.Linq;
using EscapeRoomEngine.Engine.Runtime.Requirements;
using EscapeRoomEngine.Engine.Runtime.Utilities;
using UnityEngine;
using Logger = EscapeRoomEngine.Engine.Runtime.Utilities.Logger;
using LogType = EscapeRoomEngine.Engine.Runtime.Utilities.LogType;
using Object = UnityEngine.Object;

namespace EscapeRoomEngine.Engine.Runtime.Modules
{
    /// <summary>
    /// The main representation of a module. Contains references to the module description, state and handles its placement.
    /// </summary>
    public class Module
    {
        public readonly List<Module> relatedModules = new();

        public ModuleState State { get; private set; }
        internal Vector2Int Size => srPlacement.size;
        internal Orientation Orientation => srPlacement.orientation;
        /// <summary>
        /// Get the space relative (<i>SR</i>) position of this module.
        /// </summary>
        internal Vector3Int SrPosition => srPlacement.position;
        /// <summary>
        /// Get the room relative (<i>RR</i>) position of this module.
        /// </summary>
        internal Vector3Int RrPosition => space?.ToRoomRelative(SrPosition) ?? SrPosition;

        internal readonly ModuleDescription description;

        /// <summary>
        /// The space relative (<i>SR</i>) placement of this module.
        /// </summary>
        public Placement srPlacement;

        protected readonly Space space;

        internal Module(Space space, ModuleDescription description)
        {
            this.space = space;
            this.description = description;
            srPlacement.size = description.modulePrefab.size;
        }

        internal bool IsType(ModuleType type)
        {
            return description.types.Contains(type);
        }

        internal bool CheckRequirements()
        {
            return PreconditionRequirement.CheckPreconditions(this, space) &&
                   PlacementRequirement.TryPlacing(this, space);
        }

        /// <summary>
        /// Place this module with a position relative to the room.
        /// </summary>
        /// <param name="rrPosition">The room relative (<i>RR</i>) position of this module. Must be inside the space dimensions.</param>
        /// <param name="orientation">The orientation of this module.</param>
        /// <exception cref="EngineException">If the position is not inside the space dimensions.</exception>
        internal void PlaceRoomRelative(Vector3Int rrPosition, Orientation orientation) =>
            Place(new Placement
            {
                position = space.ToSpaceRelative(rrPosition),
                size = srPlacement.size,
                orientation = orientation
            });

        /// <summary>
        /// Place this module with a position relative to the space it is in.
        /// </summary>
        /// <param name="placement">The space relative (<i>SR</i>) placement of this module. Must be inside the space dimensions.</param>
        /// <exception cref="EngineException">If the position is not inside the space dimensions.</exception>
        internal void Place(Placement placement) {
            if (space != null && !placement.position.IsInsideRelative(space.rrPlacement))
            {
                throw new EngineException($"Trying to place {this} at {placement.position} which is outside space dimensions {space.rrPlacement}.");
            }

            srPlacement = placement;
            
            Logger.Log($"{this} has been placed at {placement.position} (SR)", LogType.ModulePlacement);
        }

        /// <summary>
        /// Convert a position relative to this module to one relative to its space.
        /// <example>The module relative position <c>(0, 0, 1)</c> should always be in front of the module, wherever it faces.</example>
        /// </summary>
        /// <param name="mrPosition">The module relative (<i>MR</i>) position that should be converted to a space relative (<i>SR</i>) position.</param>
        internal Vector3Int ToSpaceRelative(Vector3Int mrPosition)
        {
            // ReSharper disable once ArrangeRedundantParentheses
            return srPlacement.position + (Orientation switch
            {
                Orientation.North => mrPosition,
                Orientation.East => new Vector3Int(mrPosition.z, 0, -mrPosition.x),
                Orientation.South => -mrPosition,
                Orientation.West => new Vector3Int(-mrPosition.z, 0, mrPosition.x),
                _ => throw new ArgumentOutOfRangeException()
            });
        }

        internal virtual void InstantiateModule(Transform parent)
        {
            Logger.Log($"Instantiating {this}", LogType.ModuleInstantiation);
                
            State = Object.Instantiate(description.modulePrefab, parent, false);
            State.transform.localPosition = new Vector3(SrPosition.x + .5f, 0, SrPosition.z + .5f);
            State.transform.Rotate(Vector3.up, Orientation.Angle());
            State.name = ToString();
            State.SetModule(this);
        }

        /// <summary>
        /// Creates a module of the correct type given a module description.
        ///
        /// <example>If the module description shows "Puzzle" as a type, this will try to create a puzzle module.</example> 
        /// </summary>
        /// <param name="space">The space to create the module for.</param>
        /// <param name="description">The module description to create the module from.</param>
        /// <returns>A <see cref="PuzzleModule"/> or <see cref="DoorModule"/>.</returns>
        /// <exception cref="WrongTypeException">If there is no module type fitting the description.</exception>
        internal static Module CreateModuleByType(Space space, ModuleDescription description)
        {
            if (description.HasType(ModuleType.Puzzle) &&
                description is PuzzleModuleDescription puzzleModuleDescription)
            {
                return new PuzzleModule(space, puzzleModuleDescription);
            }

            if((description.HasType(ModuleType.DoorEntrance) || description.HasType(ModuleType.DoorExit)) &&
               description is DoorModuleDescription doorModuleDescription)
            {
                return new DoorModule(space, doorModuleDescription);
            }

            if (description.HasType(ModuleType.Generic))
            {
                return new Module(space, description);
            }

            throw new WrongTypeException("There is no module type fitting this description.");
        }

        public override string ToString()
        {
            return $"Module ({string.Join(", ", description.types.ToList().ConvertAll(type => type.ToString()))})";
        }
    }
}
