using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Theriapolis.Core;
using Theriapolis.Core.Tactical;

namespace Theriapolis.Game.Rendering;

/// <summary>
/// Renders the streamed tactical view: per-tile surface + decoration sprites,
/// with a soft edge-blend pass between dissimilar adjacent surfaces.
///
/// Each tactical tile occupies 1×1 world pixel (the canonical coord system).
/// Sprites authored at <see cref="C.TACTICAL_TILE_SPRITE_PX"/>² are drawn at
/// scale = 1 / TACTICAL_TILE_SPRITE_PX so the source texture fits inside that
/// 1×1 cell.
///
/// Render order, per visible chunk:
///   1. Base surface tile (its own texture).
///   2. Edge blend overlays — for each cardinal neighbour with a different
///      surface, draw a per-edge alpha-gradient mask tinted with the
///      neighbour's average color. This softens the otherwise hard seam
///      between dissimilar surfaces (the "wallpaper grid" look).
///   3. Decoration sprite, if any.
///
/// This is "Option B" autotiling per the Phase 4 plan — short-term smoothing
/// using flat color blends. The longer-term plan is "Option C": full Wang
/// corner-based autotiling driven by Pixellab's `create_topdown_tileset`,
/// where each cell picks one of 16 transition tiles based on its 4-corner
/// terrain sample. Tracked in `theriapolis-tactical-tile-art-request.md`.
///
/// Rivers/roads/rail are NOT redrawn here; the polyline burn-in already
/// embedded them in the chunk's surface tiles, and LineFeatureRenderer keeps
/// drawing the source polylines on top so the shared visual is unbroken.
/// </summary>
public sealed class TacticalRenderer : IMapView, IDisposable
{
    private readonly GraphicsDevice _gd;
    private readonly ChunkStreamer _streamer;
    private readonly TacticalAtlas _atlas;
    private bool _disposed;

    // 1/SpritePx — multiplied by sprite source size (32) to land at 1×1 world pixel.
    private static readonly Vector2 SpriteScale =
        new(1f / C.TACTICAL_TILE_SPRITE_PX, 1f / C.TACTICAL_TILE_SPRITE_PX);

    // Toggle for the Option B edge-blend pass. Currently OFF — the first
    // tuning produced washed-out tiles when many neighbouring surfaces had
    // saturated placeholder colours (snow≈white, sand=cream). Two issues to
    // fix before re-enabling:
    //   1. 4 overlapping masks compound into ~4× alpha at tile corners with
    //      4 different neighbours — needs cap or non-overlapping geometry.
    //   2. Tint alpha (0.55) and 16-px falloff are both too aggressive when
    //      the neighbour colour is nothing like our own.
    // Defer re-tuning until the per-tile art set is filled in; the placeholder
    // colour palette isn't a fair test bed.
    // static readonly (not const) so the guard evaluates at runtime — avoids
    // CS0162 unreachable-code warnings on the gated branch.
    private static readonly bool EdgeBlendEnabled = false;

    // Edge masks — 32×32 textures with white RGB and an alpha gradient that
    // fades from the named edge inward. Drawn over a tile (tinted with the
    // neighbour's avg color) to bleed neighbour colour over our own.
    private Texture2D _edgeN = null!, _edgeE = null!, _edgeS = null!, _edgeW = null!;

    public TacticalRenderer(GraphicsDevice gd, ChunkStreamer streamer, TacticalAtlas atlas)
    {
        _gd       = gd;
        _streamer = streamer;
        _atlas    = atlas;
        BuildEdgeMasks();
    }

    private void BuildEdgeMasks()
    {
        _edgeN = MakeMask((x, y, sz) => 1f - (float)y / (sz / 2));
        _edgeS = MakeMask((x, y, sz) => 1f - (float)(sz - 1 - y) / (sz / 2));
        _edgeW = MakeMask((x, y, sz) => 1f - (float)x / (sz / 2));
        _edgeE = MakeMask((x, y, sz) => 1f - (float)(sz - 1 - x) / (sz / 2));
    }

    private Texture2D MakeMask(Func<int, int, int, float> alphaAt)
    {
        int sz = C.TACTICAL_TILE_SPRITE_PX;
        var pixels = new Color[sz * sz];
        for (int y = 0; y < sz; y++)
        for (int x = 0; x < sz; x++)
        {
            float a = MathHelper.Clamp(alphaAt(x, y, sz), 0f, 1f);
            // Quadratic falloff feels softer than linear at the seam itself.
            a = a * a;
            pixels[y * sz + x] = new Color((byte)255, (byte)255, (byte)255, (byte)(a * 255));
        }
        var tex = new Texture2D(_gd, sz, sz);
        tex.SetData(pixels);
        return tex;
    }

    public void Draw(SpriteBatch sb, Camera2D camera, GameTime gameTime)
    {
        // Visible AABB in tactical-tile (world-pixel) coords.
        var tl = camera.ScreenToWorld(Vector2.Zero);
        var br = camera.ScreenToWorld(new Vector2(camera.ScreenWidth, camera.ScreenHeight));
        int x0 = (int)MathF.Floor(tl.X) - 1;
        int y0 = (int)MathF.Floor(tl.Y) - 1;
        int x1 = (int)MathF.Ceiling(br.X) + 1;
        int y1 = (int)MathF.Ceiling(br.Y) + 1;

        // Clamp to the world.
        int worldPxW = C.WORLD_WIDTH_TILES  * C.TACTICAL_PER_WORLD_TILE;
        int worldPxH = C.WORLD_HEIGHT_TILES * C.TACTICAL_PER_WORLD_TILE;
        x0 = Math.Clamp(x0, 0, worldPxW);
        y0 = Math.Clamp(y0, 0, worldPxH);
        x1 = Math.Clamp(x1, 0, worldPxW);
        y1 = Math.Clamp(y1, 0, worldPxH);
        if (x0 >= x1 || y0 >= y1) return;

        sb.Begin(
            transformMatrix: camera.TransformMatrix,
            samplerState:    SamplerState.PointClamp,
            sortMode:        SpriteSortMode.Deferred,
            blendState:      BlendState.AlphaBlend);

        // Iterate chunk-by-chunk so we touch each cached chunk array directly.
        var ccTl = ChunkCoord.ForTactical(x0, y0);
        var ccBr = ChunkCoord.ForTactical(x1 - 1, y1 - 1);

        // Pass 1: base surfaces.
        for (int cy = ccTl.Y; cy <= ccBr.Y; cy++)
        for (int cx = ccTl.X; cx <= ccBr.X; cx++)
        {
            var chunk = _streamer.Get(new ChunkCoord(cx, cy));
            DrawChunkSurfaces(sb, chunk, x0, y0, x1, y1);
        }

        // Pass 2: edge blends — gated on EdgeBlendEnabled (currently false).
        if (EdgeBlendEnabled)
        {
            for (int cy = ccTl.Y; cy <= ccBr.Y; cy++)
            for (int cx = ccTl.X; cx <= ccBr.X; cx++)
            {
                var chunk = _streamer.Get(new ChunkCoord(cx, cy));
                DrawChunkEdgeBlends(sb, chunk, x0, y0, x1, y1);
            }
        }

        // Pass 3: decorations.
        for (int cy = ccTl.Y; cy <= ccBr.Y; cy++)
        for (int cx = ccTl.X; cx <= ccBr.X; cx++)
        {
            var chunk = _streamer.Get(new ChunkCoord(cx, cy));
            DrawChunkDecos(sb, chunk, x0, y0, x1, y1);
        }

        sb.End();
    }

    private void DrawChunkSurfaces(SpriteBatch sb, TacticalChunk chunk, int vx0, int vy0, int vx1, int vy1)
    {
        int ox = chunk.OriginX;
        int oy = chunk.OriginY;
        int sx = Math.Max(0, vx0 - ox);
        int sy = Math.Max(0, vy0 - oy);
        int ex = Math.Min(C.TACTICAL_CHUNK_SIZE, vx1 - ox);
        int ey = Math.Min(C.TACTICAL_CHUNK_SIZE, vy1 - oy);

        for (int ly = sy; ly < ey; ly++)
        for (int lx = sx; lx < ex; lx++)
        {
            ref var t = ref chunk.Tiles[lx, ly];
            var tex = _atlas.GetSurface(t.Surface, t.Variant);
            sb.Draw(tex,
                position:        new Vector2(ox + lx, oy + ly),
                sourceRectangle: null,
                color:           Color.White,
                rotation:        0f,
                origin:          Vector2.Zero,
                scale:           SpriteScale,
                effects:         SpriteEffects.None,
                layerDepth:      0f);
        }
    }

    private void DrawChunkEdgeBlends(SpriteBatch sb, TacticalChunk chunk, int vx0, int vy0, int vx1, int vy1)
    {
        int ox = chunk.OriginX;
        int oy = chunk.OriginY;
        int sx = Math.Max(0, vx0 - ox);
        int sy = Math.Max(0, vy0 - oy);
        int ex = Math.Min(C.TACTICAL_CHUNK_SIZE, vx1 - ox);
        int ey = Math.Min(C.TACTICAL_CHUNK_SIZE, vy1 - oy);

        for (int ly = sy; ly < ey; ly++)
        for (int lx = sx; lx < ex; lx++)
        {
            ref var t = ref chunk.Tiles[lx, ly];
            int tx = ox + lx, ty = oy + ly;
            // Sample 4 cardinal neighbours via the streamer (handles cross-chunk).
            var nN = _streamer.SampleTile(tx,     ty - 1).Surface;
            var nS = _streamer.SampleTile(tx,     ty + 1).Surface;
            var nW = _streamer.SampleTile(tx - 1, ty    ).Surface;
            var nE = _streamer.SampleTile(tx + 1, ty    ).Surface;

            if (nN != t.Surface) BlendEdge(sb, tx, ty, _edgeN, _atlas.GetSurfaceAverageColor(nN));
            if (nS != t.Surface) BlendEdge(sb, tx, ty, _edgeS, _atlas.GetSurfaceAverageColor(nS));
            if (nW != t.Surface) BlendEdge(sb, tx, ty, _edgeW, _atlas.GetSurfaceAverageColor(nW));
            if (nE != t.Surface) BlendEdge(sb, tx, ty, _edgeE, _atlas.GetSurfaceAverageColor(nE));
        }
    }

    private void BlendEdge(SpriteBatch sb, int tx, int ty, Texture2D mask, Color tint)
    {
        if (tint.A == 0) return;
        // Cap blend strength so the seam softens but we don't drown out our own
        // surface. 0.55 is a comfortable mid-point — stronger than a hint, weaker
        // than a 50/50 blend.
        var c = new Color(tint.R, tint.G, tint.B, (byte)(0.55f * 255));
        sb.Draw(mask,
            position:        new Vector2(tx, ty),
            sourceRectangle: null,
            color:           c,
            rotation:        0f,
            origin:          Vector2.Zero,
            scale:           SpriteScale,
            effects:         SpriteEffects.None,
            layerDepth:      0f);
    }

    private void DrawChunkDecos(SpriteBatch sb, TacticalChunk chunk, int vx0, int vy0, int vx1, int vy1)
    {
        int ox = chunk.OriginX;
        int oy = chunk.OriginY;
        int sx = Math.Max(0, vx0 - ox);
        int sy = Math.Max(0, vy0 - oy);
        int ex = Math.Min(C.TACTICAL_CHUNK_SIZE, vx1 - ox);
        int ey = Math.Min(C.TACTICAL_CHUNK_SIZE, vy1 - oy);

        for (int ly = sy; ly < ey; ly++)
        for (int lx = sx; lx < ex; lx++)
        {
            ref var t = ref chunk.Tiles[lx, ly];
            var tex = _atlas.GetDeco(t.Deco, t.Variant);
            if (tex is null) continue;
            sb.Draw(tex,
                position:        new Vector2(ox + lx, oy + ly),
                sourceRectangle: null,
                color:           Color.White,
                rotation:        0f,
                origin:          Vector2.Zero,
                scale:           SpriteScale,
                effects:         SpriteEffects.None,
                layerDepth:      0f);
        }
    }

    public void Dispose()
    {
        if (_disposed) return;
        _disposed = true;
        _edgeN?.Dispose();
        _edgeE?.Dispose();
        _edgeS?.Dispose();
        _edgeW?.Dispose();
        // _atlas is owned by PlayScreen; do not dispose here.
    }
}
