using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;

[Serializable]
public struct Int2
{
    public int q;
    public int r;
    public Int2(int q, int r) { this.q = q;  this.r = r; }
    public Vector2 vector2 { get { return new Vector2(q, r); } }
    public override string ToString()
    {
        return q + " " + r;
    }
    static public Int2 operator+ (Int2 a, Int2 b)
    {
        return new Int2(a.q + b.q, a.r + b.r);
    }
    static public Int2 operator* (Int2 a, int c)
    {
        return new Int2(a.q * c, a.r * c);
    }
    static public Int2 operator* (int c, Int2 a)
    {
        return new Int2(a.q * c, a.r * c);
    }
    static public Int2 operator- (Int2 a, Int2 b)
    {
        return new Int2(a.q - b.q, a.r - b.r);
    }
}

[Serializable]
public struct Int3
{
    public int x;
    public int y;
    public int z;
    public Int3(int x, int y, int z) { this.x = x;  this.y = y;  this.z = z; }
    public Int3(Int3 other) { x = other.x;  y = other.y;  z = other.z; }
    public Vector3 vector3 { get { return new Vector3(x, y, z); } }
    public int this[int index]
    {
        get
        {
            switch (index)
            {
                case 0: return x;
                case 1: return y;
                case 2: return z;
                default: return 0;
            }
        }
        set
        {
            switch (index)
            {
                case 0: x = value; return;
                case 1: y = value; return;
                case 2: z = value; return;
                default: return;
            }
        }
    }
}

[Serializable]
public class HexGrid
{

    #region static index operations

    public static float tileHeight = 1f;
    public static float vertDist = tileHeight * 0.75f;
    public static float tileWidth = tileHeight * Mathf.Sqrt(3)/2f;
    public static float horizDist = tileWidth;

    public static Int3[] directions3 = {
        new Int3(1, -1, 0),
        new Int3(1, 0, -1),
        new Int3(0, 1, -1),
        new Int3(-1, 1, 0),
        new Int3(-1, 0, 1),
        new Int3(0, -1, 1)
    };

    public static Int2[] directions2 = {
        new Int2(1, 0),
        new Int2(1, -1),
        new Int2(0, -1),
        new Int2(-1, 0),
        new Int2(-1, 1),
        new Int2(0, 1)
    };

    #region coordinate conversion
    static Int2 CubeToAxial(Int3 cube)
    {
        return new Int2(cube.x, cube.z);
    }
    static Int3 AxialToCube(Int2 axial)
    {
        return new Int3(axial.q, -axial.q - axial.r, axial.r);
    }

    static Int2 OffsetToAxial(Int2 offset)
    {
        var x = offset.q;
        var y = offset.r;
        return new Int2(
            q: x - (y - (y & 1)) / 2,
            r: y
        );
    }

    static Int2 AxialToOffset(Int2 axial)
    {
        var q = axial.q;
        var r = axial.r;
        return new Int2(
            q + (r - (r & 1)) / 2,
            r
        );
    }
    #endregion

    public static Int2[] Neighbors(Int2 coords)
    {
        Int2[] neighbors = new Int2[6];
        for (int i=0; i<6; i++)
        {
            var dir = directions2[i];
            neighbors[i] = new Int2(coords.q + dir.q, coords.r + dir.r);
        }
        return neighbors;
    }

    public static Vector2 Position(Int2 coords)
    {
        Int2 offset = AxialToOffset(coords);
        return new Vector2(
            horizDist * (offset.r % 2 == 0 ? offset.q : offset.q + 0.5f),
            vertDist  * -offset.r
        );
    }

    public static Int2 Coordinates(Vector2 position)
    {
        position.x /= horizDist;
        position.y /= vertDist;
        int y = -Mathf.RoundToInt(position.y);
        int x = y % 2 == 0 ?
            Mathf.RoundToInt(position.x) :
            Mathf.RoundToInt(position.x - 0.5f);
        var offset = new Int2(x, y);
        return OffsetToAxial(offset);
    }

    public static Int2 Coordinates(Tile tile)
    {

        var pos = tile.transform.localPosition;
        var pos2 = new Vector2(pos.x, pos.z);
        return Coordinates(pos2);
    }

    public static Int3 Rotate(Int3 cube, int degrees)
    {
        degrees %= 360;
        int turns = Mathf.RoundToInt(degrees / 60.0f);

        for (int i=0; i<turns; i++)
        {
            cube = new Int3(
                x: -cube.z,
                y: -cube.x,
                z: -cube.y
            );
        }
        return cube;
    }

    public static Int2 Rotate(Int2 coords, int degrees)
    {
        var cube = AxialToCube(coords);
        cube = Rotate(cube, degrees);
        return CubeToAxial(cube);
    }

    /// <summary>
    /// cube distance
    /// </summary>
    public static int Distance(Int3 from, Int3 to)
    {
        return Mathf.Max(Mathf.Abs(to.x - from.x), Mathf.Abs(to.y - from.y), Mathf.Abs(to.z - from.z));
    }

    public static int Distance(Int2 from, Int2 to)
    {
        var fromCube = AxialToCube(from);
        var toCube = AxialToCube(to);
        return Distance(fromCube, toCube);
    }

    public static int Distance(Int2[] coords)
    {
        return Distance(coords[0], coords[1]);
    }

    /// <summary>
    /// returns a set of either 1 or 2 directions depending on axis alignment
    /// </summary>
    /// <param name="from"></param>
    /// <param name="to"></param>
    /// <returns></returns>
    public static Int2[] Direction(Int2 from, Int2 to)
    {
        var differenceCube = AxialToCube(from - to);
        if (differenceCube.x == 0 && differenceCube.y == 0 && differenceCube.z == 0)
            return new Int2[0];
        if (differenceCube.x == 0)
        {
            if (differenceCube.y > 0)
                return new Int2[] { new Int2(0, -1) }; // +y -z
            return new Int2[] { new global::Int2(0, 1) }; // -y +z
        }
        if (differenceCube.y == 0)
        {
            if (differenceCube.z > 0)
                return new Int2[] { new Int2(-1, 1) }; // -x +z
            return new Int2[] { new global::Int2(1, -1) }; // +x -z
        }
        if (differenceCube.z == 0)
        {
            if (differenceCube.x > 0)
                return new Int2[] { new Int2(1, 0) }; // +x -y
            return new Int2[] { new global::Int2(-1, 0) }; // -x +y
        }
        if (differenceCube.x < 0 && differenceCube.y > 0 && differenceCube.z > 0)
            return new Int2[] { new Int2(-1, 0), new Int2(-1, 1) }; // -x
        if (differenceCube.x > 0 && differenceCube.y < 0 && differenceCube.z > 0)
            return new Int2[] { new Int2(0, 1), new Int2(1, 0) }; // -y
        if (differenceCube.x > 0 && differenceCube.y > 0 && differenceCube.z < 0)
            return new Int2[] { new Int2(0, -1), new Int2(1, -1) }; // -z

        if (differenceCube.x > 0 && differenceCube.y < 0 && differenceCube.z < 0)
            return new Int2[] { new Int2(1, 0), new Int2(1, -1) }; // +x
        if (differenceCube.x < 0 && differenceCube.y > 0 && differenceCube.z < 0)
            return new Int2[] { new Int2(0, -1), new Int2(-1, 0) }; // +y
        if (differenceCube.x < 0 && differenceCube.y < 0 && differenceCube.z > 0)
            return new Int2[] { new Int2(0, 1), new Int2(-1, 1) }; // +z
        return new Int2[0];
    }

    #region set operations
    public static bool Overlap(Int2[] set1, Int2[] set2)
    {
        return set1.Intersect(set2).Any();
    }

    /// <summary>
    /// finds the tuple from the cartesian product of the sets with the min distance
    /// </summary>
    /// <returns>array of length 2. Index 0 is from set1, Index 1 is from set2.</returns>
    public static Int2[] minDistancePair(Int2[] set1, Int2[] set2)
    {
        var product = from a in set1 from b in set2 select new[] { a, b };
        int minDist = int.MaxValue;
        Int2[] minPair = new Int2[2];
        foreach (var pair in product)
        {
            var dist = Distance(pair[0], pair[1]);
            if (dist < minDist)
            {
                minDist = dist;
                minPair = pair;
            }
        }
        return minPair;
    }
    #endregion
    #endregion

    [SerializeField]
    public Dictionary<Int2, Tile> grid; // stored in axial coordinates
    
    public HexGrid()
    {
        this.grid = new Dictionary<Int2, Tile>();
    }

    public IEnumerable<Tile> Neighbors(Tile tile)
    {
        var coords = Coordinates(tile);

        var tiles = new List<Tile>();
        var neighborIndices = Neighbors(coords);
        for (int i=0; i<6; i++)
        {
            if (grid.ContainsKey(neighborIndices[i]))
                tiles.Add(grid[neighborIndices[i]]);
        }
        return tiles;
    }

    public void Add(Tile tile)
    {
        var coords = Coordinates(tile);
        grid[coords] = tile;
    }

    public void Remove(Tile tile)
    {
        var coords = Coordinates(tile);
        grid.Remove(coords);
    }

    public void Remove(Int2 coords)
    {
        grid.Remove(coords);
    }

    public bool IsOccupied(Int2 coords)
    {
        return grid.ContainsKey(coords);
    }

    public Int2 nearestEmptyCoords(Vector2 pos)
    {
        // BFS
        var openQueue = new Queue<Int2>();
        var closedSet = new HashSet<Int2>();
        openQueue.Enqueue(Coordinates(pos));

        while (openQueue.Any())
        {
            var coords = openQueue.Dequeue();
            closedSet.Add(coords);
            var neighbors = Neighbors(coords);
            if (!grid.ContainsKey(coords) && neighbors.Any(n=>grid.ContainsKey(n)))
                return coords;
            foreach (var neighbor in neighbors)
                openQueue.Enqueue(neighbor);
        }
        return new Int2(0, 0);
    }

    public Vector2 nearestEmptyPos(Vector2 pos)
    {
        var coords = nearestEmptyCoords(pos);
        Debug.Log(coords + " pos: " + Position(coords));
        return Position(coords);
    }

}
