using Theriapolis.Core.Data;
using Theriapolis.Core.Util;

namespace Theriapolis.Core.Dungeons;

/// <summary>
/// Phase 7 M1 — pure deterministic room-graph assembly. Given a list of
/// picked templates (in order: entry first, others in pick order) and a
/// branching policy, returns:
///   - Per-room placement (AABB top-left + Role)
///   - List of <see cref="RoomConnection"/> binding rooms via matched
///     door tiles
///
/// The assembler does NOT paint tiles — it only decides geometry. Tile
/// painting + corridor stamping is <see cref="RoomTilePainter"/>'s job.
///
/// Placement algorithm: rooms snap to a 16-tile grid, placed left-to-right
/// in chains; each non-entry room picks an existing-room neighbour from
/// the eligible set (linear → previous; branching → uniform-random prior;
/// loop → branching + one extra closing connection). The neighbour's
/// available-side pool determines which AABB slot the new room takes.
///
/// Returns null on failure (overlap, unreachable). Caller retries with a
/// fresh seed up to <see cref="C.DUNGEON_LAYOUT_MAX_ATTEMPTS"/> times then
/// falls back to the linear policy.
/// </summary>
internal static class RoomGraphAssembler
{
    public sealed class Plan
    {
        public Room[]            Rooms;
        public RoomConnection[]  Connections;
        public int               DungeonW;
        public int               DungeonH;
        public (int X, int Y)    EntranceTile;

        public Plan(Room[] rooms, RoomConnection[] connections, int w, int h, (int, int) entranceTile)
        {
            Rooms        = rooms;
            Connections  = connections;
            DungeonW     = w;
            DungeonH     = h;
            EntranceTile = entranceTile;
        }
    }

    /// <summary>
    /// Try to assemble. <paramref name="branching"/> is one of
    /// <c>linear</c> / <c>branching</c> / <c>loop</c>. Returns null on
    /// any geometric failure; caller retries.
    /// </summary>
    public static Plan? TryAssemble(
        IReadOnlyList<RoomTemplateDef> picks,
        IReadOnlyList<RoomRole> roles,
        string branching,
        SeededRng rng)
    {
        if (picks.Count == 0) return null;
        if (picks.Count != roles.Count) throw new ArgumentException("picks and roles length mismatch");

        // Step 1: place the entry room at the origin, padded by AABB padding.
        int pad   = C.DUNGEON_AABB_PADDING;
        int gap   = C.ROOM_INTER_ROOM_GAP_TILES;

        var rooms = new Room[picks.Count];
        // Track absolute AABBs for overlap testing.
        var bounds = new (int x, int y, int w, int h)[picks.Count];

        var entry = picks[0];
        rooms[0] = new Room
        {
            Id          = 0,
            TemplateId  = entry.Id,
            AabbX       = pad,
            AabbY       = pad,
            AabbW       = entry.FootprintWTiles,
            AabbH       = entry.FootprintHTiles,
            BuiltBy     = entry.BuiltBy,
            Role        = roles[0],
            NarrativeText = entry.NarrativeText,
        };
        bounds[0] = (pad, pad, entry.FootprintWTiles, entry.FootprintHTiles);

        var connections = new List<RoomConnection>(picks.Count);

        // Step 2: place each subsequent room next to a chosen prior room.
        for (int i = 1; i < picks.Count; i++)
        {
            int parentIdx = ChooseParent(branching, i, rng);
            var parent = bounds[parentIdx];
            var tpl = picks[i];

            // Try each cardinal direction in a deterministic order; pick the
            // first that doesn't overlap. Order rotates per `i` so different
            // seeds produce different-looking layouts.
            int[] dirOrder = RotateDirOrder(i, rng);
            (int x, int y, int dir)? placement = null;
            foreach (int d in dirOrder)
            {
                var topLeft = TryPlaceAdjacent(parent, tpl.FootprintWTiles, tpl.FootprintHTiles, d, gap);
                if (topLeft is null) continue;
                var candBounds = (topLeft.Value.x, topLeft.Value.y, tpl.FootprintWTiles, tpl.FootprintHTiles);
                if (Overlaps(candBounds, bounds, i)) continue;
                placement = (topLeft.Value.x, topLeft.Value.y, d);
                break;
            }
            if (placement is null) return null; // ran out of room sides

            rooms[i] = new Room
            {
                Id          = i,
                TemplateId  = tpl.Id,
                AabbX       = placement.Value.x,
                AabbY       = placement.Value.y,
                AabbW       = tpl.FootprintWTiles,
                AabbH       = tpl.FootprintHTiles,
                BuiltBy     = tpl.BuiltBy,
                Role        = roles[i],
                NarrativeText = tpl.NarrativeText,
            };
            bounds[i] = (placement.Value.x, placement.Value.y, tpl.FootprintWTiles, tpl.FootprintHTiles);

            // Pick the door pair: parent's near-side door, this room's
            // far-side door. If neither template has a matching door we
            // synthesize a door at the AABB midpoint of the touching edge —
            // the painter will carve it through the wall.
            var conn = MatchDoors(rooms[parentIdx], picks[parentIdx], rooms[i], tpl, placement.Value.dir);
            connections.Add(conn);
        }

        // Step 3 (loop policy only): add one extra closing connection if a
        // suitable pair exists. The closing connection must not duplicate
        // an existing edge.
        if (branching == "loop" && rooms.Length >= 4)
        {
            // Pick room i (not 0) and j > 1, j != i, with no existing edge.
            int triesLeft = 8;
            while (triesLeft-- > 0)
            {
                int i = rng.NextInt(2, rooms.Length);
                int j = rng.NextInt(2, rooms.Length);
                if (i == j) continue;
                if (HasEdge(connections, i, j)) continue;
                var dir = AdjacentDirection(bounds[i], bounds[j], gap + 2);
                if (dir is null) continue;
                connections.Add(MatchDoors(rooms[i], picks[i], rooms[j], picks[j], dir.Value));
                break;
            }
        }

        // Step 4: BFS reachability — every room must be reachable from room 0.
        if (!IsReachable(rooms.Length, connections)) return null;

        // Step 5: compute dungeon bounds.
        int minX = int.MaxValue, minY = int.MaxValue, maxX = 0, maxY = 0;
        foreach (var b in bounds)
        {
            if (b.x < minX) minX = b.x;
            if (b.y < minY) minY = b.y;
            if (b.x + b.w > maxX) maxX = b.x + b.w;
            if (b.y + b.h > maxY) maxY = b.y + b.h;
        }
        // Translate everything so origin is (pad, pad).
        int dx = pad - minX;
        int dy = pad - minY;
        if (dx != 0 || dy != 0)
        {
            for (int i = 0; i < rooms.Length; i++)
            {
                rooms[i] = new Room
                {
                    Id            = rooms[i].Id,
                    TemplateId    = rooms[i].TemplateId,
                    AabbX         = rooms[i].AabbX + dx,
                    AabbY         = rooms[i].AabbY + dy,
                    AabbW         = rooms[i].AabbW,
                    AabbH         = rooms[i].AabbH,
                    BuiltBy       = rooms[i].BuiltBy,
                    Role          = rooms[i].Role,
                    NarrativeText = rooms[i].NarrativeText,
                };
            }
            for (int k = 0; k < connections.Count; k++)
            {
                var c = connections[k];
                connections[k] = c with
                {
                    DoorAx = c.DoorAx + dx,
                    DoorAy = c.DoorAy + dy,
                    DoorBx = c.DoorBx + dx,
                    DoorBy = c.DoorBy + dy,
                };
            }
        }

        int dungeonW = maxX - minX + 2 * pad;
        int dungeonH = maxY - minY + 2 * pad;

        // Entrance tile — pick the entry room's first declared door if any,
        // otherwise the centre of its top edge. (M1 always has a door because
        // every authored entry template declares at least one.)
        var entryDoor = picks[0].Doors.Length > 0 ? picks[0].Doors[0] : null;
        (int X, int Y) entranceTile;
        if (entryDoor is not null)
            entranceTile = (rooms[0].AabbX + entryDoor.X, rooms[0].AabbY + entryDoor.Y);
        else
            entranceTile = (rooms[0].AabbX + rooms[0].AabbW / 2, rooms[0].AabbY);

        return new Plan(rooms, connections.ToArray(), dungeonW, dungeonH, entranceTile);
    }

    // ── Helpers ──────────────────────────────────────────────────────────

    private static int ChooseParent(string branching, int childIdx, SeededRng rng) => branching switch
    {
        "linear"    => childIdx - 1,
        "branching" => rng.NextInt(0, childIdx),
        "loop"      => rng.NextInt(0, childIdx),
        _           => childIdx - 1,
    };

    private static int[] RotateDirOrder(int i, SeededRng rng)
    {
        // Rotate base order by `i % 4` so adjacent rooms don't pile up on
        // the same axis. Add a small RNG-driven secondary shuffle so seeds
        // diverge.
        int[] baseOrder = new[] { 0, 1, 2, 3 }; // 0=E, 1=S, 2=W, 3=N
        int rot = i % 4;
        var rotated = new int[4];
        for (int k = 0; k < 4; k++) rotated[k] = baseOrder[(k + rot) % 4];
        // 50% chance to swap pairs (small variation)
        if ((rng.NextUInt64() & 1) == 1)
        {
            (rotated[0], rotated[2]) = (rotated[2], rotated[0]);
        }
        return rotated;
    }

    private static (int x, int y)? TryPlaceAdjacent(
        (int x, int y, int w, int h) parent,
        int childW, int childH, int direction, int gap)
    {
        // direction: 0=E, 1=S, 2=W, 3=N. Childâ€™s top-left is offset from
        // parent's outer edge by `gap` tiles so a corridor segment fits.
        return direction switch
        {
            0 => (parent.x + parent.w + gap, parent.y),                                   // east
            1 => (parent.x,                  parent.y + parent.h + gap),                  // south
            2 => (parent.x - childW - gap,   parent.y),                                   // west
            3 => (parent.x,                  parent.y - childH - gap),                    // north
            _ => null,
        };
    }

    private static bool Overlaps(
        (int x, int y, int w, int h) cand,
        (int x, int y, int w, int h)[] bounds,
        int countSoFar)
    {
        for (int k = 0; k < countSoFar; k++)
        {
            var b = bounds[k];
            // AABB overlap: not (cand right of b OR cand left of b OR cand below b OR cand above b)
            bool noOverlap = cand.x + cand.w <= b.x
                          || b.x   + b.w   <= cand.x
                          || cand.y + cand.h <= b.y
                          || b.y   + b.h   <= cand.y;
            if (!noOverlap) return true;
        }
        return false;
    }

    private static int? AdjacentDirection(
        (int x, int y, int w, int h) a,
        (int x, int y, int w, int h) b, int slack)
    {
        // Returns a direction code (0=E,1=S,2=W,3=N) for "b is to the X of
        // a within `slack` tiles" — used for loop-policy closing edges.
        if (Math.Abs((a.x + a.w) - b.x) <= slack && OverlapsRange(a.y, a.h, b.y, b.h))
            return 0;
        if (Math.Abs((a.y + a.h) - b.y) <= slack && OverlapsRange(a.x, a.w, b.x, b.w))
            return 1;
        if (Math.Abs(a.x - (b.x + b.w)) <= slack && OverlapsRange(a.y, a.h, b.y, b.h))
            return 2;
        if (Math.Abs(a.y - (b.y + b.h)) <= slack && OverlapsRange(a.x, a.w, b.x, b.w))
            return 3;
        return null;
    }

    private static bool OverlapsRange(int a0, int aLen, int b0, int bLen)
        => !(a0 + aLen <= b0 || b0 + bLen <= a0);

    private static bool HasEdge(List<RoomConnection> conns, int a, int b)
    {
        foreach (var c in conns)
            if ((c.RoomA == a && c.RoomB == b) || (c.RoomA == b && c.RoomB == a))
                return true;
        return false;
    }

    private static bool IsReachable(int roomCount, List<RoomConnection> connections)
    {
        if (roomCount == 0) return true;
        var adj = new List<int>[roomCount];
        for (int i = 0; i < roomCount; i++) adj[i] = new List<int>();
        foreach (var c in connections)
        {
            adj[c.RoomA].Add(c.RoomB);
            adj[c.RoomB].Add(c.RoomA);
        }
        var visited = new bool[roomCount];
        var queue   = new Queue<int>();
        queue.Enqueue(0);
        visited[0] = true;
        int reached = 1;
        while (queue.Count > 0)
        {
            int n = queue.Dequeue();
            foreach (int m in adj[n])
            {
                if (visited[m]) continue;
                visited[m] = true;
                reached++;
                queue.Enqueue(m);
            }
        }
        return reached == roomCount;
    }

    /// <summary>
    /// Match door tiles between two rooms placed adjacent in
    /// <paramref name="direction"/>. Returns the connection record with
    /// dungeon-local door coords on each side. Falls back to the AABB
    /// midpoint of the touching edge when neither template declares a door
    /// on the relevant side.
    /// </summary>
    private static RoomConnection MatchDoors(
        Room a, RoomTemplateDef aDef,
        Room b, RoomTemplateDef bDef,
        int direction)
    {
        // Direction is from A's perspective: 0=B east of A; 1=B south; 2=B west; 3=B north.
        // Pick A's door on the matching side; pick B's door on the opposite side.
        string aFacing = direction switch { 0 => "E", 1 => "S", 2 => "W", 3 => "N", _ => "E" };
        string bFacing = direction switch { 0 => "W", 1 => "N", 2 => "E", 3 => "S", _ => "W" };

        var aDoor = FindDoorByFacing(aDef, aFacing) ?? AabbEdgeMidpoint(aDef, aFacing);
        var bDoor = FindDoorByFacing(bDef, bFacing) ?? AabbEdgeMidpoint(bDef, bFacing);

        return new RoomConnection(
            RoomA: a.Id, DoorAx: a.AabbX + aDoor.x, DoorAy: a.AabbY + aDoor.y,
            RoomB: b.Id, DoorBx: b.AabbX + bDoor.x, DoorBy: b.AabbY + bDoor.y,
            Lock: aDoor.lockTier);
    }

    private static (int x, int y, string lockTier)? FindDoorByFacing(RoomTemplateDef def, string facing)
    {
        foreach (var d in def.Doors)
            if (string.Equals(d.Facing, facing, StringComparison.OrdinalIgnoreCase))
                return (d.X, d.Y, d.Lock);
        return null;
    }

    private static (int x, int y, string lockTier) AabbEdgeMidpoint(RoomTemplateDef def, string facing)
    {
        int w = def.FootprintWTiles, h = def.FootprintHTiles;
        return facing switch
        {
            "E" => (w - 1, h / 2, ""),
            "W" => (0,     h / 2, ""),
            "N" => (w / 2, 0,     ""),
            "S" => (w / 2, h - 1, ""),
            _   => (w / 2, h / 2, ""),
        };
    }
}
