using System;
using System.Collections.Generic;
using UnityEngine;
using Range = EscapeRoomEngine.Engine.Runtime.Utilities.Range;

namespace EscapeRoomEngine.Engine.Runtime
{
    /// <summary>
    /// A placement defines the position, size and orientation of a module or space.
    /// </summary>
    public struct Placement
    {
        /// <summary>
        /// The placement position is always considered to be at <c>(0.5, 0.5)</c> relative to the placement.
        ///
        /// <example>In a placement of size <c>(1, 1)</c>, the position is at the center of the placement. In a larger placement, the position is in the center of the bottom left square.</example>
        /// </summary>
        public Vector3Int position;
        public Vector2Int size;
        public Orientation orientation;
        
        /// <summary>
        /// Return the bottom left corner position of the module.
        /// </summary>
        public Vector3Int BottomLeft {
            get
            {
                var sizeMinusOne = size - Vector2Int.one;
                return orientation switch
                {
                    Orientation.North => position,
                    Orientation.East => position - new Vector3Int(0, 0, sizeMinusOne.x),
                    Orientation.South => position - new Vector3Int(sizeMinusOne.x, 0, sizeMinusOne.y),
                    Orientation.West => position - new Vector3Int(sizeMinusOne.y, 0, 0),
                    _ => throw new ArgumentOutOfRangeException()
                };
            }
        }

        /// <summary>
        /// Get the geometric center of this placement taking into account its orientation.
        /// </summary>
        public Vector3 GeometricCenter
        {
            get
            {
                var sizeMinusOne = size - Vector2Int.one;
                var half = new Vector2(sizeMinusOne.x/2f, sizeMinusOne.y/2f);
                var horizontal = new Vector3(half.x, 0, half.y);
                var vertical = new Vector3(half.y, 0, -half.x);
                var geometricPosition = position + new Vector3(0.5f, 0, 0.5f);
#pragma warning disable CS8524
                return orientation switch
#pragma warning restore CS8524
                {
                    Orientation.North => geometricPosition + horizontal,
                    Orientation.East => geometricPosition + vertical,
                    Orientation.South => geometricPosition - horizontal,
                    Orientation.West => geometricPosition - vertical
                };
            }
        }
        
        /// <summary>
        /// Get the integer positions of the two back corners. The first one is just the position of the placement.
        /// </summary>
        public (Vector3Int, Vector3Int) BackCorners {
            get
            {
                var width = size.x - 1;
                var horizontal = new Vector3Int(width, 0, 0);
                var vertical = new Vector3Int(0, 0, width);
#pragma warning disable CS8524
                var otherCorner = orientation switch
#pragma warning restore CS8524
                {
                    Orientation.North => position + horizontal,
                    Orientation.East => position - vertical,
                    Orientation.South => position - horizontal,
                    Orientation.West => position + vertical
                };
                return (position, otherCorner);
            }
        }
        
        /// <summary>
        /// Create a set with every possible combination of position and orientation of placements that fit in a given space.
        /// The size of the placements are given by this placement.
        /// </summary>
        /// <param name="space">The generated placements will fit inside the placement of the given space.</param>
        public List<Placement> EveryPlacementInSpace(Space space)
        {
            var placements = new List<Placement>();
            var sizeMinusOne = size - Vector2Int.one;
            var spaceSize = space.rrPlacement.size;

            foreach (var o in OrientationExtensions.EveryOrientation())
            {
                Range x;
                Range z;
                
                switch (o)
                {
                    case Orientation.North:
                        x = new Range(0, spaceSize.x - sizeMinusOne.x);
                        z = new Range(0, spaceSize.y - sizeMinusOne.y);
                        break;
                    case Orientation.East:
                        x = new Range(0, spaceSize.x - sizeMinusOne.y);
                        z = new Range(sizeMinusOne.x, spaceSize.y);
                        break;
                    case Orientation.South:
                        x = new Range(sizeMinusOne.x, spaceSize.x);
                        z = new Range(sizeMinusOne.y, spaceSize.y);
                        break;
                    case Orientation.West:
                        x = new Range(sizeMinusOne.y, spaceSize.x);
                        z = new Range(0, spaceSize.y - sizeMinusOne.x);
                        break;
                    default:
                        throw new ArgumentOutOfRangeException();
                }
                
                placements.AddRange(ConstrainedPlacements(o, size, x, z));
            }

            return placements;
        }

        private static IEnumerable<Placement> ConstrainedPlacements(
            Orientation orientation,
            Vector2Int size,
            Range xRange, Range zRange)
        {
            var placements = new List<Placement>(xRange.Length * zRange.Length);
            
            for (var z = zRange.min; z < zRange.max; z++)
            {
                for (var x = xRange.min; x < xRange.max; x++)
                {
                    placements.Add(new Placement
                    {
                        position = new Vector3Int(x, 0, z),
                        size = size,
                        orientation = orientation
                    });
                }
            }

            return placements;
        }

        /// <summary>
        /// Performs the specified action for each position inside the size of this placement.
        ///
        /// <example>For the placement {(2, 0, 3), (1, 2), East} the action would be called twice with the parameters (2, 0, 3) and (3, 0, 3).</example>
        /// </summary>
        public void ForEachPosition(Action<Vector3Int> action)
        {
            var bottomLeft = BottomLeft;

            for (var z = 0; z < (orientation is Orientation.North or Orientation.South ? size.y : size.x); z++)
            {
                for (var x = 0; x < (orientation is Orientation.North or Orientation.South ? size.x : size.y); x++)
                {
                    action(bottomLeft + new Vector3Int(x, 0, z));
                }
            }
        }

        public string ToStringShort() => $"{{{position}, {orientation}}}";
        public override string ToString() => $"{{{position}, {size}, {orientation}}}";
    }
}