#pragma once

#include "block.h"
#include "chunk.h"
#include "noise.h"
#include "planet.h"

#include <algorithm>
#include <cmath>
#include <cstdint>

// ---- ChunkGenerator --------------------------------------------------------
class ChunkGenerator {
public:
    inline static uint64_t seed = 12345ULL; // kept for API compat

    static void generate(Chunk& chunk) {
        const ChunkPos& cp = chunk.position();
        constexpr int N = Chunk::SIZE;

        const float minX = static_cast<float>(cp.x * N);
        const float minY = static_cast<float>(cp.y * N);
        const float minZ = static_cast<float>(cp.z * N);
        const float maxX = minX + N;
        const float maxY = minY + N;
        const float maxZ = minZ + N;

        const std::vector<Planet> nearPlanets =
            PlanetManager::planetsNear(minX, minY, minZ, maxX, maxY, maxZ);

        if (nearPlanets.empty()) return; // deep space — all air

        // ── Find best planet once at chunk centre ────────────────────────────
        const float chunkCX = minX + N * 0.5f;
        const float chunkCY = minY + N * 0.5f;
        const float chunkCZ = minZ + N * 0.5f;

        float bestDepth = -1e30f;
        int   bestIdx   = -1;
        const int nNear = static_cast<int>(nearPlanets.size());
        for (int i = 0; i < nNear; ++i) {
            const Planet& pl = nearPlanets[i];
            const float ddx = chunkCX - pl.cx;
            const float ddy = chunkCY - pl.cy;
            const float ddz = chunkCZ - pl.cz;
            const float aax = std::abs(ddx), aay = std::abs(ddy), aaz = std::abs(ddz);
            const float cheby = (aax > aay && aax > aaz) ? aax : (aay > aaz) ? aay : aaz;
            const float depth = static_cast<float>(pl.baseRadius) - cheby;
            if (depth > bestDepth) { bestDepth = depth; bestIdx = i; }
        }

        if (bestIdx < 0) return;

        const Planet& pl  = nearPlanets[bestIdx];
        const float   sr  = static_cast<float>(pl.baseRadius);

        // Build noise objects for the one planet we'll use
        const Noise noiseTerr { pl.seed ^ 0x4F3A1B2CULL };
        const Noise noiseCave { pl.seed ^ 0xCA0EA0BULL  };
        const Noise noiseTemp { pl.seed ^ 0xBEEF1234ULL };
        const Noise noiseHum  { pl.seed ^ 0xDEAD5678ULL };
        const Noise noiseDecor{ pl.seed ^ 0x3C71A9F2ULL }; // ponds, rivers
        const Noise noiseCont { pl.seed ^ 0x7E58A3C1ULL }; // continentalness (plains/hills/mountains)

        static constexpr float SEA_LEVEL = 16.0f;

        // ── Climate: sample ONCE at chunk centre ─────────────────────────────
        float t, h, cont;
        {
            const float dx = chunkCX - pl.cx;
            const float dy = chunkCY - pl.cy;
            const float dz = chunkCZ - pl.cz;
            float len = std::sqrt(dx*dx + dy*dy + dz*dz);
            if (len < 1.0f) len = 1.0f;
            const float su = dx/len * sr;
            const float sv = dy/len * sr;
            const float sw = dz/len * sr;

            const float rawT = noiseTemp.octaves3(su*0.00022f, sv*0.00022f, sw*0.00022f, 3, 0.5f);
            const float rawH = noiseHum.octaves3(
                su*0.00022f + 317.3f, sv*0.00022f + 193.7f, sw*0.00022f + 451.2f, 3, 0.5f);
            // Continentalness: larger scale → determines plains / hills / mountains regions
            const float rawC = noiseCont.octaves3(
                su*0.00016f + 89.1f, sv*0.00016f + 213.5f, sw*0.00016f + 47.9f, 2, 0.5f);
            t    = std::clamp((rawT + 1.0f) * 0.5f, 0.0f, 1.0f);
            h    = std::clamp((rawH + 1.0f) * 0.5f, 0.0f, 1.0f);
            cont = std::clamp((rawC + 1.0f) * 0.5f, 0.0f, 1.0f);
        }

        const float oceanFactor = std::clamp((h - 0.60f) / 0.40f, 0.0f, 1.0f);

        // Terrain amplitude and base height driven by continentalness:
        //   cont 0.00–0.38 → plains  (gently rolling, near sea level)
        //   cont 0.38–0.70 → hills   (moderate relief)
        //   cont 0.70–1.00 → mountains (large amplitude, high base)
        const float landAmp =
            cont < 0.38f ? std::lerp( 7.0f, 16.0f, cont / 0.38f) :
            cont < 0.70f ? std::lerp(16.0f, 34.0f, (cont - 0.38f) / 0.32f) :
                           std::lerp(34.0f, 68.0f, (cont - 0.70f) / 0.30f);
        const float reliefAmp   = std::lerp(landAmp, 8.0f, oceanFactor);

        const float landBias =
            cont < 0.38f ? SEA_LEVEL + std::lerp( 3.0f,  9.0f, cont / 0.38f) :
            cont < 0.70f ? SEA_LEVEL + std::lerp( 9.0f, 18.0f, (cont - 0.38f) / 0.32f) :
                           SEA_LEVEL + std::lerp(18.0f, 28.0f, (cont - 0.70f) / 0.30f);
        const float terrainBias = std::lerp(landBias, -8.0f, oceanFactor);

        // Surface blocks (constant per chunk)
        BlockType surfTop, surfSub;
        if (t < 0.22f) {
            surfTop = BlockType::Snow;  surfSub = BlockType::Stone;
        } else if (t > 0.72f && h < 0.38f) {
            surfTop = BlockType::Sand;  surfSub = BlockType::Sandstone;
        } else if (oceanFactor > 0.15f) {
            surfTop = BlockType::Sand;  surfSub = BlockType::Gravel;
        } else {
            surfTop = BlockType::Grass; surfSub = BlockType::Dirt;
        }

        const bool isLand   = (oceanFactor < 0.15f);
        const bool isGrass  = (surfTop == BlockType::Grass);
        const bool isDesert = (surfTop == BlockType::Sand && t > 0.72f && h < 0.38f);
        const bool isSnow   = (surfTop == BlockType::Snow);

        // ── Determine planet face (which axis is "up") ───────────────────────
        const float fdx = chunkCX - pl.cx, fdy = chunkCY - pl.cy, fdz = chunkCZ - pl.cz;
        const float fax = std::abs(fdx), fay = std::abs(fdy), faz = std::abs(fdz);
        int upAx; float upSgn;
        if (fax >= fay && fax >= faz) { upAx = 0; upSgn = fdx >= 0.f ? 1.f : -1.f; }
        else if (fay >= faz)           { upAx = 1; upSgn = fdy >= 0.f ? 1.f : -1.f; }
        else                           { upAx = 2; upSgn = fdz >= 0.f ? 1.f : -1.f; }
        // Two horizontal axes
        const int ha = (upAx == 0) ? 1 : 0;
        const int hb = (upAx == 2) ? 1 : 2;

        const float cMin[3] = { minX, minY, minZ };
        const float planetC[3] = { pl.cx, pl.cy, pl.cz };

        // ── Terrain height: precompute per face-horizontal column ─────────────
        // Always iterate the two axes that are HORIZONTAL for this planet face
        // (ha, hb) so the height map is correct on all 6 faces.
        // terrH indexed [lhb * N + lha] where lha = local coord along ha, lhb along hb.
        float terrH[N * N];
        for (int lhb = 0; lhb < N; ++lhb)
        for (int lha = 0; lha < N; ++lha) {
            float world[3];
            world[ha]   = cMin[ha]   + lha + 0.5f;
            world[hb]   = cMin[hb]   + lhb + 0.5f;
            world[upAx] = cMin[upAx] + N * 0.5f;   // chunk centre along the vertical axis
            const float dx = world[0] - pl.cx, dy = world[1] - pl.cy, dz = world[2] - pl.cz;
            float len = std::sqrt(dx*dx + dy*dy + dz*dz);
            if (len < 1.0f) len = 1.0f;
            const float su = dx/len * sr;
            const float sv = dy/len * sr;
            const float sw = dz/len * sr;
            float th = noiseTerr.octaves3(su*0.006f, sv*0.006f, sw*0.006f, 5, 0.5f) * reliefAmp
                     + terrainBias;

            // Ponds: small rounded depressions that fill with water
            if (isGrass && oceanFactor < 0.1f) {
                const float sC[3] = { su, sv, sw };
                const float pn = noiseDecor.octaves2(
                    sC[ha]*0.05f + 500.f, sC[hb]*0.05f + 500.f, 2, 0.5f);
                if (pn > 0.82f)
                    th -= (pn - 0.82f) * 8.f * SEA_LEVEL;

                // Rivers: narrow winding channels (zero-crossings of smooth noise)
                const float rn = noiseDecor.octaves2(
                    sC[ha]*0.003f + 777.f, sC[hb]*0.003f + 333.f, 3, 0.5f);
                if (std::abs(rn) < 0.040f) {
                    const float riverDepth = (0.040f - std::abs(rn)) / 0.040f;
                    th -= riverDepth * 11.0f;
                }
            }

            terrH[lhb * N + lha] = th;
        }

        // ── Block placement ──────────────────────────────────────────────────
        for (int lz = 0; lz < N; ++lz)
        for (int ly = 0; ly < N; ++ly)
        for (int lx = 0; lx < N; ++lx) {
            const float wx = minX + lx + 0.5f;
            const float wy = minY + ly + 0.5f;
            const float wz = minZ + lz + 0.5f;

            const float dx = wx - pl.cx, dy = wy - pl.cy, dz = wz - pl.cz;
            const float ax = std::abs(dx), ay = std::abs(dy), az = std::abs(dz);
            const float cheby = (ax > ay && ax > az) ? ax : (ay > az) ? ay : az;

            // Look up height using the face-correct horizontal indices.
            const int lcoords3[3] = { lx, ly, lz };
            const float th = terrH[lcoords3[hb] * N + lcoords3[ha]];
            const float localDepth = (sr + th) - cheby;
            const float waterDepth = (sr + SEA_LEVEL) - cheby;

            if (localDepth > 0.0f) {
                // Cave carving
                if (localDepth > 10.0f) {
                    const float cv = noiseCave.octaves3(
                        wx * 0.025f, wy * 0.025f, wz * 0.025f, 2, 0.5f);
                    if (cv > 0.52f) {
                        if (oceanFactor > 0.0f && waterDepth > 0.0f)
                            chunk.set(lx, ly, lz, BlockType::Water);
                        continue; // air cave
                    }
                }

                // Stone peaks: high above sea level on mountains/hills
                const float heightAboveSea = th - SEA_LEVEL;
                const bool stonyPeak = (heightAboveSea > reliefAmp * 0.55f) && isGrass;
                const BlockType top = stonyPeak ? BlockType::Stone : surfTop;
                const BlockType sub = stonyPeak ? BlockType::Stone : surfSub;

                if (localDepth <= 1.5f) {
                    chunk.set(lx, ly, lz, top);
                } else if (localDepth <= 6.0f) {
                    chunk.set(lx, ly, lz, sub);
                } else {
                    chunk.set(lx, ly, lz, BlockType::Stone);
                }

            } else if (waterDepth > 0.0f) {
                chunk.set(lx, ly, lz, BlockType::Water);
            }
        }

        // ── Decoration: trees and cacti ──────────────────────────────────────
        // Skip decoration for ocean chunks and deep underground/sky chunks.
        if (!isLand) return;

        // Determine tree density for this climate.
        float treeDensity = 0.0f;
        if (isGrass) {
            // More trees on hills/mountains (cont), fewer on flat plains
            treeDensity = std::lerp(0.28f, 0.55f, cont);
            if (pl.biome == Biome::Forest) treeDensity = std::min(treeDensity * 1.5f, 0.85f);
            if (pl.biome == Biome::Swamp)  treeDensity = 0.20f;
        } else if (isSnow) {
            treeDensity = std::lerp(0.18f, 0.35f, cont);
        } else if (isDesert) {
            treeDensity = 0.12f; // cacti
        }
        if (treeDensity == 0.0f) return;

        // Recompute terrain height at any horizontal position (for cross-chunk trees).
        // IMPORTANT: world[upAx] must be chunk-independent so that different chunks
        // compute identical th for the same (wha,whb). Using the nominal planet
        // surface (planetC[upAx] + upSgn*sr) instead of chunkCY achieves this.
        auto treeTerrH = [&](float wha, float whb) -> float {
            float world[3];
            world[ha]   = wha;
            world[hb]   = whb;
            world[upAx] = planetC[upAx] + upSgn * sr; // consistent across all chunks
            const float dx = world[0] - pl.cx, dy = world[1] - pl.cy, dz = world[2] - pl.cz;
            float len = std::sqrt(dx*dx + dy*dy + dz*dz);
            if (len < 1.f) len = 1.f;
            const float su = dx/len*sr, sv = dy/len*sr, sw = dz/len*sr;
            float th = noiseTerr.octaves3(su*0.006f, sv*0.006f, sw*0.006f, 5, 0.5f)
                       * reliefAmp + terrainBias;
            // Apply same pond/river depressions so tree base matches actual terrain.
            if (isGrass && oceanFactor < 0.1f) {
                const float sC[3] = { su, sv, sw };
                const float pn = noiseDecor.octaves2(
                    sC[ha]*0.05f + 500.f, sC[hb]*0.05f + 500.f, 2, 0.5f);
                if (pn > 0.82f)
                    th -= (pn - 0.82f) * 8.f * SEA_LEVEL;
                const float rn = noiseDecor.octaves2(
                    sC[ha]*0.003f + 777.f, sC[hb]*0.003f + 333.f, 3, 0.5f);
                if (std::abs(rn) < 0.055f)
                    th -= (0.055f - std::abs(rn)) / 0.055f * SEA_LEVEL * 2.8f;
            }
            return th;
        };

        // Hash function for deterministic tree placement per grid cell.
        auto treeRng = [&](int ga, int gb) -> uint32_t {
            uint64_t v = static_cast<uint64_t>(ga) * 0x9E3779B97F4A7C15ULL
                       ^ static_cast<uint64_t>(gb) * 0x6C62272E07BB0142ULL
                       ^ pl.seed;
            v ^= v >> 33; v *= 0xFF51AFD7ED558CCDULL; v ^= v >> 33;
            return static_cast<uint32_t>(v);
        };

        static constexpr int TREE_GRID     = 7;  // one cell per 7×7 horizontal area
        static constexpr int LEAF_REACH_MAX = 2;  // max leaf radius for coarse cull

        const float minHa = cMin[ha], minHb = cMin[hb];

        // Cell range: include cells whose trees could have leaves in this chunk.
        const int ga0 = static_cast<int>(std::floor((minHa - LEAF_REACH_MAX - TREE_GRID) / TREE_GRID));
        const int ga1 = static_cast<int>(std::ceil ((minHa + N + LEAF_REACH_MAX)          / TREE_GRID));
        const int gb0 = static_cast<int>(std::floor((minHb - LEAF_REACH_MAX - TREE_GRID) / TREE_GRID));
        const int gb1 = static_cast<int>(std::ceil ((minHb + N + LEAF_REACH_MAX)          / TREE_GRID));

        for (int ga = ga0; ga <= ga1; ++ga)
        for (int gb = gb0; gb <= gb1; ++gb) {

            uint32_t rng = treeRng(ga, gb);

            // Decide if a tree/cactus is placed in this cell.
            if ((rng & 0xFFFF) >= static_cast<uint32_t>(treeDensity * 65536.f)) continue;
            rng = rng * 1664525u + 1013904223u;

            // Trunk horizontal position (random offset within cell).
            const int wta = ga * TREE_GRID + static_cast<int>(rng % static_cast<uint32_t>(TREE_GRID));
            rng = rng * 1664525u + 1013904223u;
            const int wtb = gb * TREE_GRID + static_cast<int>(rng % static_cast<uint32_t>(TREE_GRID));
            rng = rng * 1664525u + 1013904223u;

            const float wha = wta + 0.5f, whb = wtb + 0.5f;

            // Coarse horizontal overlap: does this tree's crown reach this chunk?
            if (wha + LEAF_REACH_MAX < minHa      || wha - LEAF_REACH_MAX >= minHa + N) continue;
            if (whb + LEAF_REACH_MAX < minHb      || whb - LEAF_REACH_MAX >= minHb + N) continue;

            // Surface height at trunk position.
            const float th = treeTerrH(wha, whb);
            if (th < SEA_LEVEL + 2.0f) continue; // surface at or below sea level — no trees

            const float surfW = planetC[upAx] + upSgn * (sr + th);

            // Integer block index of the first block above the surface.
            // surfW is the Chebyshev boundary (fractional). The topmost solid block
            // is at k_top = ceil(surfW - 0.5) - 1 for +face, so tree base = ceil(surfW - 0.5).
            // For -face the logic mirrors: floor(surfW + 0.5).
            const int upStep = (upSgn > 0.f) ? 1 : -1;
            const int k_base = (upSgn > 0.f)
                ? static_cast<int>(std::ceil (surfW - 0.5f))
                : static_cast<int>(std::floor(surfW + 0.5f)) - 1;
            const int kChunkMin = static_cast<int>(cMin[upAx]);

            // Extra guard: the block BELOW k_base (the actual ground) must be solid
            // within this chunk to prevent trees floating above caves or mismatched terrain.
            {
                const int k_ground = k_base - upStep;
                const int lg = k_ground - kChunkMin;
                if (lg >= 0 && lg < N) {
                    const int lha_trunk = static_cast<int>(std::floor(wha - minHa));
                    const int lhb_trunk = static_cast<int>(std::floor(whb - minHb));
                    if (lha_trunk >= 0 && lha_trunk < N && lhb_trunk >= 0 && lhb_trunk < N) {
                        int gc[3]; gc[upAx] = lg; gc[ha] = lha_trunk; gc[hb] = lhb_trunk;
                        const BlockType groundBlock = chunk.get(gc[0], gc[1], gc[2]);
                        if (!isSolid(groundBlock) || groundBlock == BlockType::Water) continue;
                    }
                }
            }

            // Tree / cactus parameters.
            int  trunkH, leafR, leafH0, leafH1;
            bool isCactus;
            if (isDesert) {
                isCactus = true;
                trunkH = 2 + static_cast<int>(rng & 1); // 2–3
                leafR = 0; leafH0 = 0; leafH1 = -1;
            } else if (isSnow) {
                isCactus = false;
                trunkH = 5 + static_cast<int>(rng % 3); // 5–7
                leafR = 1; leafH0 = 1; leafH1 = trunkH + 1;
            } else {
                // Oak
                isCactus = false;
                trunkH = 4 + static_cast<int>(rng & 1); // 4–5
                leafR = 2; leafH0 = trunkH - 2; leafH1 = trunkH + 1;
            }
            rng = rng * 1664525u + 1013904223u;

            const int maxI = isCactus ? (trunkH - 1) : leafH1;

            // Vertical overlap check using integer block indices.
            const int k_tree_lo = std::min(k_base, k_base + upStep * maxI);
            const int k_tree_hi = std::max(k_base, k_base + upStep * maxI);
            if (k_tree_hi < kChunkMin || k_tree_lo >= kChunkMin + N) continue;

            // Place tree blocks.
            for (int i = 0; i <= maxI; ++i) {
                const bool inTrunkRow  = (i < trunkH);
                const bool inLeavesRow = (!isCactus && i >= leafH0 && i <= leafH1);
                if (!inTrunkRow && !inLeavesRow) continue;

                const int lup = k_base + upStep * i - kChunkMin;
                if (lup < 0 || lup >= N) continue;

                // Horizontal spread: trunk=0, leaves=leafR (or 1 at crown tip).
                const int rowR = inLeavesRow ? (i >= trunkH ? 1 : leafR) : 0;

                for (int da = -rowR; da <= rowR; ++da)
                for (int db = -rowR; db <= rowR; ++db) {
                    // Remove corner blocks from the main leaf crown.
                    if (inLeavesRow && rowR == 2 && std::abs(da) == 2 && std::abs(db) == 2) continue;

                    const int lha = static_cast<int>(std::floor(wha + da - minHa));
                    const int lhb = static_cast<int>(std::floor(whb + db - minHb));
                    if (lha < 0 || lha >= N || lhb < 0 || lhb >= N) continue;

                    BlockType bt;
                    if (da == 0 && db == 0 && inTrunkRow) {
                        bt = BlockType::Wood;
                    } else if (inLeavesRow) {
                        bt = BlockType::Leaves;
                    } else continue;

                    int lcoords[3];
                    lcoords[upAx] = lup;
                    lcoords[ha]   = lha;
                    lcoords[hb]   = lhb;
                    if (chunk.get(lcoords[0], lcoords[1], lcoords[2]) != BlockType::Air) continue;
                    chunk.set(lcoords[0], lcoords[1], lcoords[2], bt);
                }
            }
        }
    }
};
