using System.Collections.Generic;
using System.Linq;
using Godot;
using Theriapolis.Core;
using Theriapolis.Core.Tactical;
using Theriapolis.Core.Util;
using Theriapolis.Core.World;
using Theriapolis.Core.World.Polylines;

namespace Theriapolis.GodotHost.Rendering;

/// <summary>
/// Renders a generated <see cref="WorldState"/> across the seamless zoom
/// range — biome backdrop, polylines (rivers / roads / rails), bridges,
/// settlement dots, and the tactical-chunk layer that streams in close-up.
/// Owns its own <see cref="PanZoomCamera"/> so callers can read zoom and
/// drive position uniformly.
///
/// Per M7 plan §6.2: extracted from the M2+M4 <see cref="WorldView"/> demo
/// so PlayScreen and the standalone demo both mount the same renderer.
/// The chunk streamer itself is owned by the *caller* — PlayScreen needs
/// the streamer for NPC lifecycle separately from the visual layer — so
/// the caller subscribes to <c>OnChunkLoaded</c>/<c>OnChunkEvicting</c>
/// and forwards into <see cref="AddChunkNode"/>/<see cref="RemoveChunkNode"/>.
///
/// Per-frame: hides/shows the tactical and settlement layers based on
/// camera zoom, and counter-scales every Line2D width so polyline widths
/// stay visually consistent regardless of zoom.
/// </summary>
public partial class WorldRenderNode : Node2D
{
    // Zoom thresholds, in Camera2D zoom units (1.0 = 1 world px per screen px,
    // 32.0 = sprite-native tactical view, ~0.07 = world fits 1080p).
    public const float TacticalRenderZoomMin = 4.0f;
    public const float SettlementHideZoom    = 2.0f;

    // Polyline base widths in *screen* pixels (counter-scaled to world space
    // per frame). Mirrors the differentiation in LineFeatureRenderer.cs.
    private const float HighwayScreenPx    = 4f;
    private const float PostRoadScreenPx   = 3f;
    private const float DirtRoadScreenPx   = 2f;
    private const float RiverMajorScreenPx = 4.5f;
    private const float RiverScreenPx      = 3f;
    private const float StreamScreenPx     = 2f;
    private const float RailTieScreenPx    = 4f;
    private const float RailLineScreenPx   = 2f;
    private const float BridgeScreenPx     = 6f;

    // Polyline colours mirror LineFeatureRenderer.cs / WorldgenDump.cs.
    private static readonly Color RiverMajorColour = ColorByte(40, 100, 200);
    private static readonly Color RiverColour      = ColorByte(60, 120, 200);
    private static readonly Color StreamColour     = ColorByte(100, 150, 220);
    private static readonly Color HighwayColour    = ColorByte(210, 180, 80);
    private static readonly Color PostRoadColour   = ColorByte(180, 155, 70);
    private static readonly Color DirtRoadColour   = ColorByte(150, 130, 90);
    private static readonly Color RailTieColour    = ColorByte(120, 100, 80);
    private static readonly Color RailColour       = ColorByte(80, 65, 50);
    private static readonly Color BridgeColour     = ColorByte(160, 140, 100);

    private Node2D? _tacticalLayer;
    private Node2D? _polylineLayer;
    private Node2D? _bridgeLayer;
    private Node2D? _settlementLayer;
    private PanZoomCamera? _camera;
    private readonly Dictionary<ChunkCoord, TacticalChunkNode> _chunkNodes = new();
    private readonly List<(Line2D line, float baseScreenWidth)> _scaledLines = new();
    private bool _initialised;

    /// <summary>The camera owned by this node. Caller reads <c>Zoom</c> to
    /// pick world-map vs. tactical UI behaviour, and sets <c>Position</c>
    /// to follow the player.</summary>
    public PanZoomCamera Camera => _camera!;

    /// <summary>Initialise from a completed <see cref="WorldGenContext"/>.
    /// Idempotent on repeat — second call is a no-op. <paramref name="initialZoom"/>
    /// of 0 means "compute fit-to-viewport so the whole world is visible".</summary>
    public void Initialize(WorldState world, float initialZoom = 0f)
    {
        if (_initialised) return;
        _initialised = true;

        TacticalAtlas.EnsureLoaded();

        BuildBiomeSprite(world);
        _tacticalLayer = AddNamedLayer("TacticalChunks");
        BuildPolylines(world);
        BuildBridges(world);
        BuildSettlements(world);
        AddCamera(initialZoom);
    }

    public override void _Process(double delta)
    {
        if (!_initialised) return;
        UpdateLayerVisibility();
        UpdateZoomScaledNodes();
    }

    /// <summary>Mount the visual for a freshly-streamed chunk. Caller
    /// invokes from a <c>ChunkStreamer.OnChunkLoaded</c> subscription.</summary>
    public void AddChunkNode(TacticalChunk chunk)
    {
        if (_tacticalLayer is null) return;
        if (_chunkNodes.ContainsKey(chunk.Coord)) return;

        var node = new TacticalChunkNode { Name = $"Chunk{chunk.Coord.X}_{chunk.Coord.Y}" };
        _tacticalLayer.AddChild(node);
        node.Bind(chunk);
        _chunkNodes[chunk.Coord] = node;
    }

    /// <summary>Tear down a chunk visual on eviction. Caller invokes from
    /// <c>ChunkStreamer.OnChunkEvicting</c>.</summary>
    public void RemoveChunkNode(TacticalChunk chunk)
    {
        if (!_chunkNodes.TryGetValue(chunk.Coord, out var node)) return;
        node.QueueFree();
        _chunkNodes.Remove(chunk.Coord);
    }

    // ──────────────────────────────────────────────────────────────────────
    // Layer construction

    private void BuildBiomeSprite(WorldState world)
    {
        int W = C.WORLD_WIDTH_TILES;
        int H = C.WORLD_HEIGHT_TILES;

        var palette = new Color[(int)BiomeId.Mangrove + 1];
        foreach (var def in world.BiomeDefs!)
        {
            var (r, g, b) = def.ParsedColor();
            int id = (int)ParseBiomeId(def.Id);
            if (id >= 0 && id < palette.Length) palette[id] = ColorByte(r, g, b);
        }

        var image = Image.CreateEmpty(W, H, false, Image.Format.Rgb8);
        for (int y = 0; y < H; y++)
        for (int x = 0; x < W; x++)
        {
            int id = (int)world.Tiles[x, y].Biome;
            Color c = (id >= 0 && id < palette.Length && palette[id].A > 0f)
                ? palette[id]
                : ColorByte(255, 0, 255);
            image.SetPixel(x, y, c);
        }

        var sprite = new Sprite2D
        {
            Texture = ImageTexture.CreateFromImage(image),
            Centered = false,
            Scale = new Vector2(C.WORLD_TILE_PIXELS, C.WORLD_TILE_PIXELS),
            TextureFilter = TextureFilterEnum.Nearest,
            Name = "Biome",
        };
        AddChild(sprite);
    }

    private Node2D AddNamedLayer(string name)
    {
        var n = new Node2D { Name = name };
        AddChild(n);
        return n;
    }

    private void BuildPolylines(WorldState world)
    {
        _polylineLayer = AddNamedLayer("Polylines");

        foreach (var road in world.Roads.OrderBy(RoadDrawRank))
        {
            var (color, screenPx) = road.RoadClassification switch
            {
                RoadType.Highway  => (HighwayColour, HighwayScreenPx),
                RoadType.PostRoad => (PostRoadColour, PostRoadScreenPx),
                _                 => (DirtRoadColour, DirtRoadScreenPx),
            };
            AddScaledLine(_polylineLayer, road.Points, color, screenPx);
        }

        foreach (var river in world.Rivers)
        {
            var (color, screenPx) = river.RiverClassification switch
            {
                RiverClass.MajorRiver => (RiverMajorColour, RiverMajorScreenPx),
                RiverClass.River      => (RiverColour, RiverScreenPx),
                _                     => (StreamColour, StreamScreenPx),
            };
            float flowScale = 1f + (river.FlowAccumulation / (float)C.RIVER_MAJOR_THRESHOLD) * 0.3f;
            AddScaledLine(_polylineLayer, river.Points, color,
                Mathf.Min(screenPx * flowScale, RiverMajorScreenPx * 1.5f));
        }

        foreach (var rail in world.Rails)
        {
            AddScaledLine(_polylineLayer, rail.Points, RailTieColour, RailTieScreenPx);
            AddScaledLine(_polylineLayer, rail.Points, RailColour, RailLineScreenPx);
        }
    }

    private void BuildBridges(WorldState world)
    {
        if (world.Bridges.Count == 0) return;
        _bridgeLayer = AddNamedLayer("Bridges");

        foreach (var bridge in world.Bridges)
        {
            var line = new Line2D
            {
                DefaultColor = BridgeColour,
                JointMode = Line2D.LineJointMode.Round,
            };
            line.AddPoint(new Vector2(bridge.Start.X, bridge.Start.Y));
            line.AddPoint(new Vector2(bridge.End.X, bridge.End.Y));
            _bridgeLayer.AddChild(line);
            _scaledLines.Add((line, BridgeScreenPx));
        }
    }

    private void BuildSettlements(WorldState world)
    {
        if (world.Settlements.Count == 0) return;
        _settlementLayer = AddNamedLayer("Settlements");

        foreach (var s in world.Settlements)
        {
            var (colour, tileRadius) = s.Tier switch
            {
                1 => (ColorByte(255, 215, 0), 2.5f),
                2 => (ColorByte(230, 230, 230), 1.8f),
                3 => (ColorByte(150, 200, 255), 1.3f),
                4 => (ColorByte(200, 200, 200), 0.8f),
                _ => (ColorByte(200, 60, 60), 0.7f),
            };
            float radius = tileRadius * C.WORLD_TILE_PIXELS;
            var dot = new SettlementDot
            {
                Position = new Vector2(
                    s.TileX * C.WORLD_TILE_PIXELS + C.WORLD_TILE_PIXELS * 0.5f,
                    s.TileY * C.WORLD_TILE_PIXELS + C.WORLD_TILE_PIXELS * 0.5f),
                Radius = radius,
                FillColor = colour,
            };
            _settlementLayer.AddChild(dot);
        }
    }

    private void AddScaledLine(Node2D parent, IReadOnlyList<Vec2> pts, Color colour, float screenPx)
    {
        var line = new Line2D
        {
            DefaultColor = colour,
            JointMode = Line2D.LineJointMode.Round,
            BeginCapMode = Line2D.LineCapMode.Round,
            EndCapMode = Line2D.LineCapMode.Round,
            Antialiased = false,
        };
        for (int i = 0; i < pts.Count; i++)
            line.AddPoint(new Vector2(pts[i].X, pts[i].Y));
        parent.AddChild(line);
        _scaledLines.Add((line, screenPx));
    }

    private void AddCamera(float initialZoom)
    {
        Vector2 viewport = GetViewport().GetVisibleRect().Size;
        Vector2 worldSize = new(
            C.WORLD_WIDTH_TILES * C.WORLD_TILE_PIXELS,
            C.WORLD_HEIGHT_TILES * C.WORLD_TILE_PIXELS);
        float fitZoom = Mathf.Min(viewport.X / worldSize.X, viewport.Y / worldSize.Y) * 0.95f;
        float startZoom = initialZoom > 0f ? initialZoom : fitZoom;

        _camera = new PanZoomCamera
        {
            Position = worldSize * 0.5f,  // caller can reposition immediately after Initialize
            Zoom = new Vector2(startZoom, startZoom),
            MinZoom = fitZoom * 0.5f,
            MaxZoom = 64f,
        };
        AddChild(_camera);
        _camera.MakeCurrent();
    }

    // ──────────────────────────────────────────────────────────────────────
    // Per-frame updates

    private void UpdateLayerVisibility()
    {
        if (_camera is null) return;
        float zoom = _camera.Zoom.X;
        bool tactical = zoom >= TacticalRenderZoomMin;

        if (_tacticalLayer is not null)
            _tacticalLayer.Visible = tactical;
        if (_settlementLayer is not null)
            _settlementLayer.Visible = zoom < SettlementHideZoom;

        // Polylines and bridges are baked into the tactical chunk surface
        // tiles by TacticalChunkGen.Pass2_Polylines, so re-stroking the
        // Line2D overlay at tactical zoom double-draws the road and shows
        // as a brown line over top of the rasterised one. Hide the line
        // overlay when tactical is active.
        if (_polylineLayer is not null)
            _polylineLayer.Visible = !tactical;
        if (_bridgeLayer is not null)
            _bridgeLayer.Visible = !tactical;
    }

    private void UpdateZoomScaledNodes()
    {
        if (_camera is null) return;
        float zoom = _camera.Zoom.X;
        if (zoom <= 0f) return;
        float invZoom = 1f / zoom;
        foreach (var (line, baseScreenPx) in _scaledLines)
            line.Width = baseScreenPx * invZoom;
    }

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

    private static int RoadDrawRank(Polyline r) => r.RoadClassification switch
    {
        RoadType.Footpath => 0,
        RoadType.DirtRoad => 1,
        RoadType.PostRoad => 2,
        RoadType.Highway  => 3,
        _                 => 1,
    };

    private static Color ColorByte(byte r, byte g, byte b) =>
        new(r / 255f, g / 255f, b / 255f);

    private static BiomeId ParseBiomeId(string id) => id.ToLowerInvariant() switch
    {
        "ocean"               => BiomeId.Ocean,
        "tundra"              => BiomeId.Tundra,
        "boreal"              => BiomeId.Boreal,
        "temperate_deciduous" => BiomeId.TemperateDeciduous,
        "temperate_grassland" => BiomeId.TemperateGrassland,
        "mountain_alpine"     => BiomeId.MountainAlpine,
        "mountain_forested"   => BiomeId.MountainForested,
        "subtropical_forest"  => BiomeId.SubtropicalForest,
        "wetland"             => BiomeId.Wetland,
        "coastal"             => BiomeId.Coastal,
        "river_valley"        => BiomeId.RiverValley,
        "scrubland"           => BiomeId.Scrubland,
        "desert_cold"         => BiomeId.DesertCold,
        "forest_edge"         => BiomeId.ForestEdge,
        "foothills"           => BiomeId.Foothills,
        "marsh_edge"          => BiomeId.MarshEdge,
        "beach"               => BiomeId.Beach,
        "cliff"               => BiomeId.Cliff,
        "tidal_flat"          => BiomeId.TidalFlat,
        "mangrove"            => BiomeId.Mangrove,
        _                     => BiomeId.TemperateGrassland,
    };
}
