#include "VoxelWorld.h"
#include "wiJobSystem.h"
#include <cmath>
#include <algorithm>

namespace voxel {

VoxelWorld::VoxelWorld() {
    setupDefaultMaterials();
}

VoxelWorld::~VoxelWorld() = default;

void VoxelWorld::setupDefaultMaterials() {
    // Material 0: Air (empty, never rendered)
    // Material 1: Grass
    materials[1].albedoTextureIndex = 0;
    materials[1].roughness = 200;
    materials[1].flags = MaterialDesc::FLAG_TRIPLANAR;
    // Material 2: Dirt
    materials[2].albedoTextureIndex = 1;
    materials[2].roughness = 220;
    materials[2].flags = MaterialDesc::FLAG_TRIPLANAR;
    // Material 3: Stone
    materials[3].albedoTextureIndex = 2;
    materials[3].roughness = 180;
    materials[3].flags = MaterialDesc::FLAG_TRIPLANAR;
    // Material 4: Sand
    materials[4].albedoTextureIndex = 3;
    materials[4].roughness = 230;
    materials[4].flags = MaterialDesc::FLAG_TRIPLANAR;
    // Material 5: Snow
    materials[5].albedoTextureIndex = 4;
    materials[5].roughness = 150;
    materials[5].flags = MaterialDesc::FLAG_TRIPLANAR;
}

// ── Permutation-based noise (no external dependency) ────────────

static constexpr int PERM_SIZE = 256;
static uint8_t perm[512];
static uint32_t permSeed = 0;
static bool permInitialized = false;

static void initPerm(uint32_t seed) {
    if (permInitialized && permSeed == seed) return;
    for (int i = 0; i < PERM_SIZE; i++) perm[i] = (uint8_t)i;
    // Fisher-Yates shuffle with seed
    uint32_t s = seed;
    for (int i = PERM_SIZE - 1; i > 0; i--) {
        s = s * 1664525u + 1013904223u; // LCG
        int j = s % (i + 1);
        uint8_t tmp = perm[i];
        perm[i] = perm[j];
        perm[j] = tmp;
    }
    for (int i = 0; i < 256; i++) perm[i + 256] = perm[i];
    permSeed = seed;
    permInitialized = true;
}

static float fade(float t) { return t * t * t * (t * (t * 6.0f - 15.0f) + 10.0f); }
static float lerp(float a, float b, float t) { return a + t * (b - a); }

static float grad(int hash, float x, float y, float z) {
    int h = hash & 15;
    float u = h < 8 ? x : y;
    float v = h < 4 ? y : (h == 12 || h == 14 ? x : z);
    return ((h & 1) ? -u : u) + ((h & 2) ? -v : v);
}

float VoxelWorld::noise3D(float x, float y, float z) const {
    initPerm(seed_);
    int X = (int)std::floor(x) & 255;
    int Y = (int)std::floor(y) & 255;
    int Z = (int)std::floor(z) & 255;
    x -= std::floor(x);
    y -= std::floor(y);
    z -= std::floor(z);
    float u = fade(x), v = fade(y), w = fade(z);

    int A  = perm[X] + Y;
    int AA = perm[A] + Z;
    int AB = perm[A + 1] + Z;
    int B  = perm[X + 1] + Y;
    int BA = perm[B] + Z;
    int BB = perm[B + 1] + Z;

    return lerp(
        lerp(lerp(grad(perm[AA], x, y, z),       grad(perm[BA], x-1, y, z), u),
             lerp(grad(perm[AB], x, y-1, z),     grad(perm[BB], x-1, y-1, z), u), v),
        lerp(lerp(grad(perm[AA+1], x, y, z-1),   grad(perm[BA+1], x-1, y, z-1), u),
             lerp(grad(perm[AB+1], x, y-1, z-1), grad(perm[BB+1], x-1, y-1, z-1), u), v),
        w);
}

float VoxelWorld::fbm(float x, float y, float z, int octaves) const {
    float value = 0.0f;
    float amplitude = 1.0f;
    float frequency = 1.0f;
    float maxVal = 0.0f;
    for (int i = 0; i < octaves; i++) {
        value += amplitude * noise3D(x * frequency, y * frequency, z * frequency);
        maxVal += amplitude;
        amplitude *= 0.5f;
        frequency *= 2.0f;
    }
    return value / maxVal;
}

void VoxelWorld::generateChunk(Chunk& chunk, float timeOffset) {
    const float scale = 0.02f;    // terrain horizontal scale
    const float heightScale = 20.0f;  // flatter terrain (was 64)
    const float baseHeight = 40.0f;
    const float caveScale = 0.05f;
    const float caveThreshold = 0.3f;

    // Animation mode: fewer octaves + skip caves (much faster for 20Hz regen)
    const bool animating = (timeOffset != 0.0f);
    const int heightOctaves = animating ? 2 : 5;

    for (int z = 0; z < CHUNK_SIZE; z++) {
        for (int x = 0; x < CHUNK_SIZE; x++) {
            // World-space coordinates
            float wx = (float)(chunk.pos.x * CHUNK_SIZE + x);
            float wz = (float)(chunk.pos.z * CHUNK_SIZE + z);

            // Heightmap using fBm — timeOffset shifts the Y coord of the noise
            // to create a rolling wave effect across the terrain
            float height = baseHeight + heightScale * fbm(wx * scale, timeOffset, wz * scale, heightOctaves);

            // ── Surface material via noise-based patches ──
            // Use 2D noise at different frequencies/seeds to create organic patches
            // of each material on the surface, instead of altitude bands.
            float matNoise1 = fbm(wx * 0.03f + 500.0f, 0.0f, wz * 0.03f + 500.0f, 3);   // large patches
            float matNoise2 = fbm(wx * 0.08f + 1000.0f, 0.0f, wz * 0.08f + 1000.0f, 2);  // medium detail
            float matNoise3 = fbm(wx * 0.05f + 2000.0f, 0.0f, wz * 0.05f + 2000.0f, 3);  // third channel
            // Combined noise for material selection (range roughly -1..1)
            float matVal = matNoise1 * 0.6f + matNoise2 * 0.4f;

            uint8_t surfaceMat;
            bool surfaceSmooth = false;
            if (matVal < -0.30f) {
                surfaceMat = 4; // Sand
            } else if (matVal < -0.15f) {
                surfaceMat = 2; // Dirt (adjacent to sand for sand↔dirt testing)
            } else if (matVal < -0.05f) {
                surfaceMat = 3; // Stone (blocky, with topings)
            } else if (matVal < 0.05f) {
                surfaceMat = 6; // SmoothStone (smooth surface)
                surfaceSmooth = true;
            } else if (matVal < 0.20f) {
                surfaceMat = 1; // Grass
            } else if (matVal < 0.30f) {
                surfaceMat = 4; // Sand (adjacent to grass for sand↔grass testing)
            } else if (matNoise3 > 0.1f) {
                surfaceMat = 5; // Snow (smooth)
                surfaceSmooth = true;
            } else {
                surfaceMat = 2; // Dirt
            }

            for (int y = 0; y < CHUNK_SIZE; y++) {
                float wy = (float)(chunk.pos.y * CHUNK_SIZE + y);
                VoxelData v;

                uint8_t smoothFlag = surfaceSmooth ? VoxelData::FLAG_SMOOTH : 0;
                if (wy > height) {
                    // Air above terrain
                    v = VoxelData();
                } else if (!animating) {
                    // Cave generation (only for initial generation, too costly for animation)
                    float cave = fbm(wx * caveScale, wy * caveScale, wz * caveScale, 3);
                    if (std::abs(cave) < caveThreshold && wy > 10.0f && wy < height - 3.0f) {
                        v = VoxelData(); // Cave
                    } else if (wy > height - 1.0f) {
                        v = VoxelData(surfaceMat, smoothFlag);
                    } else if (wy > height - 4.0f) {
                        v = VoxelData(2); // Dirt sub-surface
                    } else {
                        v = VoxelData(3); // Stone deep underground
                    }
                } else {
                    // Animation path: simplified material assignment (no caves)
                    if (wy > height - 1.0f) {
                        v = VoxelData(surfaceMat, smoothFlag);
                    } else if (wy > height - 4.0f) {
                        v = VoxelData(2);
                    } else {
                        v = VoxelData(3);
                    }
                }

                chunk.at(x, y, z) = v;
            }
        }
    }

    chunk.dirty = true;

    // Scan for smooth voxels (used by SmoothMesher for early-exit)
    chunk.containsSmooth = false;
    for (int i = 0; i < CHUNK_VOLUME && !chunk.containsSmooth; i++) {
        if (chunk.voxels[i].isSmooth())
            chunk.containsSmooth = true;
    }
}

void VoxelWorld::regenerateAnimated(float time, uint32_t* packDst, uint32_t packDstCapacity) {
    // Regenerate all existing chunks with time-shifted noise (wave effect)
    // Parallelized across all CPU cores via wi::jobsystem
    float timeOffset = time * 0.1f;

    // Collect chunk pointers for indexed access (hashmap isn't index-friendly)
    std::vector<Chunk*> chunkPtrs;
    chunkPtrs.reserve(chunks_.size());
    for (auto& [pos, chunk] : chunks_) {
        chunkPtrs.push_back(chunk.get());
    }

    const uint32_t wordsPerChunk = CHUNK_VOLUME / 2; // 16384

    wi::jobsystem::context ctx;
    wi::jobsystem::Dispatch(ctx, (uint32_t)chunkPtrs.size(), 1,
        [&chunkPtrs, timeOffset, packDst, packDstCapacity, wordsPerChunk, this](wi::jobsystem::JobArgs args) {
            generateChunk(*chunkPtrs[args.jobIndex], timeOffset);
            // Fused pack: memcpy voxel data into GPU staging cache
            if (packDst) {
                uint32_t offset = args.jobIndex * wordsPerChunk;
                if (offset + wordsPerChunk <= packDstCapacity) {
                    std::memcpy(packDst + offset,
                        chunkPtrs[args.jobIndex]->voxels,
                        wordsPerChunk * sizeof(uint32_t));
                }
            }
        });
    wi::jobsystem::Wait(ctx);
}

void VoxelWorld::generateAround(float cx, float cy, float cz, int radiusChunks) {
    int ccx = (int)std::floor(cx / CHUNK_SIZE);
    int ccy = (int)std::floor(cy / CHUNK_SIZE);
    int ccz = (int)std::floor(cz / CHUNK_SIZE);

    for (int dz = -radiusChunks; dz <= radiusChunks; dz++) {
        for (int dx = -radiusChunks; dx <= radiusChunks; dx++) {
            // Y range: only generate chunks that could contain terrain (0 to ~4 chunks high)
            for (int dy = 0; dy < 8; dy++) {
                ChunkPos pos = { ccx + dx, dy, ccz + dz };
                if (chunks_.find(pos) == chunks_.end()) {
                    auto chunk = std::make_unique<Chunk>();
                    chunk->pos = pos;
                    generateChunk(*chunk);
                    chunks_[pos] = std::move(chunk);
                }
            }
        }
    }
}

Chunk* VoxelWorld::getChunk(const ChunkPos& pos) {
    auto it = chunks_.find(pos);
    return it != chunks_.end() ? it->second.get() : nullptr;
}

const Chunk* VoxelWorld::getChunk(const ChunkPos& pos) const {
    auto it = chunks_.find(pos);
    return it != chunks_.end() ? it->second.get() : nullptr;
}

VoxelData VoxelWorld::getVoxel(int wx, int wy, int wz) const {
    // Integer floor division that works for negatives
    auto floorDiv = [](int a, int b) -> int {
        return (a >= 0) ? (a / b) : ((a - b + 1) / b);
    };
    auto floorMod = [](int a, int b) -> int {
        int r = a % b;
        return (r < 0) ? r + b : r;
    };

    ChunkPos cp = {
        floorDiv(wx, CHUNK_SIZE),
        floorDiv(wy, CHUNK_SIZE),
        floorDiv(wz, CHUNK_SIZE)
    };
    const Chunk* chunk = getChunk(cp);
    if (!chunk) return VoxelData();

    return chunk->at(
        floorMod(wx, CHUNK_SIZE),
        floorMod(wy, CHUNK_SIZE),
        floorMod(wz, CHUNK_SIZE)
    );
}

void VoxelWorld::setVoxel(int wx, int wy, int wz, VoxelData v) {
    auto floorDiv = [](int a, int b) -> int {
        return (a >= 0) ? (a / b) : ((a - b + 1) / b);
    };
    auto floorMod = [](int a, int b) -> int {
        int r = a % b;
        return (r < 0) ? r + b : r;
    };

    ChunkPos cp = {
        floorDiv(wx, CHUNK_SIZE),
        floorDiv(wy, CHUNK_SIZE),
        floorDiv(wz, CHUNK_SIZE)
    };
    Chunk* chunk = getChunk(cp);
    if (!chunk) return;

    chunk->at(
        floorMod(wx, CHUNK_SIZE),
        floorMod(wy, CHUNK_SIZE),
        floorMod(wz, CHUNK_SIZE)
    ) = v;
    chunk->dirty = true;
}

void VoxelWorld::generateDebug() {
    chunks_.clear();

    // Create a single chunk at origin
    ChunkPos cp = {0, 0, 0};
    auto chunk = std::make_unique<Chunk>();
    chunk->pos = cp;
    std::memset(chunk->voxels, 0, sizeof(chunk->voxels));

    VoxelData stone(3); // material 3 = stone

    // Block 1: single isolated block at (5, 5, 5)
    //   → should show all 6 faces
    chunk->at(5, 5, 5) = stone;

    // Block 2: 2x1x1 bar at (12, 5, 5) along X
    //   → internal faces should be culled
    chunk->at(12, 5, 5) = stone;
    chunk->at(13, 5, 5) = stone;

    // Block 3: L-shape at (5, 5, 12)
    chunk->at(5, 5, 12) = stone;
    chunk->at(6, 5, 12) = stone;
    chunk->at(5, 5, 13) = stone;

    // Block 4: 3-high column at (12, 5, 12)
    chunk->at(12, 5, 12) = stone;
    chunk->at(12, 6, 12) = stone;
    chunk->at(12, 7, 12) = stone;

    // Block 5: single block at (20, 5, 5) with material 1 (grass)
    chunk->at(20, 5, 5) = VoxelData(1);

    chunk->dirty = true;
    chunks_[cp] = std::move(chunk);
}

void VoxelWorld::generateDebugSmooth() {
    chunks_.clear();

    // Create two chunks at Y=0 to have enough space
    // Chunk (0,0,0) and (1,0,0) for 64 blocks along X
    auto makeChunk = [&](int cx, int cy, int cz) -> Chunk& {
        ChunkPos cp = {cx, cy, cz};
        auto chunk = std::make_unique<Chunk>();
        chunk->pos = cp;
        std::memset(chunk->voxels, 0, sizeof(chunk->voxels));
        chunks_[cp] = std::move(chunk);
        return *chunks_[cp];
    };

    Chunk& c00 = makeChunk(0, 0, 0);

    // Helper: place a filled platform of a given material
    auto fillBlock = [](Chunk& c, int x0, int y0, int z0, int x1, int y1, int z1,
                        uint8_t mat, uint8_t flags = 0) {
        for (int z = z0; z <= z1; z++)
            for (int y = y0; y <= y1; y++)
                for (int x = x0; x <= x1; x++)
                    if (c.isInBounds(x, y, z))
                        c.at(x, y, z) = VoxelData(mat, flags);
    };

    // ── Config 1 (X=2..6): SmoothStone (6) next to Grass (1) ──
    // SmoothStone 3x3x3 block
    fillBlock(c00, 2, 2, 2, 4, 4, 4, 6, VoxelData::FLAG_SMOOTH);
    // Grass 3x3x3 block touching on +X side
    fillBlock(c00, 5, 2, 2, 7, 4, 4, 1);

    // ── Config 2 (X=2..6, Z=8): SmoothStone (6) next to Dirt (2) ──
    fillBlock(c00, 2, 2, 8, 4, 4, 10, 6, VoxelData::FLAG_SMOOTH);
    fillBlock(c00, 5, 2, 8, 7, 4, 10, 2);

    // ── Config 3 (X=2..6, Z=14): SmoothStone (6) next to Sand (4) ──
    fillBlock(c00, 2, 2, 14, 4, 4, 16, 6, VoxelData::FLAG_SMOOTH);
    fillBlock(c00, 5, 2, 14, 7, 4, 16, 4);

    // ── Config 4 (X=2..6, Z=20): SmoothStone (6) next to Stone (3, blocky) ──
    fillBlock(c00, 2, 2, 20, 4, 4, 22, 6, VoxelData::FLAG_SMOOTH);
    fillBlock(c00, 5, 2, 20, 7, 4, 22, 3);

    // ── Config 5 (X=2..6, Z=26): Snow (5, smooth) next to Grass (1) ──
    fillBlock(c00, 2, 2, 26, 4, 4, 28, 5, VoxelData::FLAG_SMOOTH);
    fillBlock(c00, 5, 2, 26, 7, 4, 28, 1);

    // ── Config 6 (X=12..16): Sand (4) next to Dirt (2) — blocky reference ──
    fillBlock(c00, 12, 2, 2, 14, 4, 4, 4);
    fillBlock(c00, 15, 2, 2, 17, 4, 4, 2);

    // ── Config 7 (X=12..16, Z=8): Grass (1) next to Dirt (2) — blocky reference ──
    fillBlock(c00, 12, 2, 8, 14, 4, 10, 1);
    fillBlock(c00, 15, 2, 8, 17, 4, 10, 2);

    // ── Config 8 (X=12..16, Z=14): SmoothStone staircase ──
    // Step 1 (low)
    fillBlock(c00, 12, 2, 14, 14, 2, 16, 6, VoxelData::FLAG_SMOOTH);
    // Step 2 (mid)
    fillBlock(c00, 15, 2, 14, 17, 3, 16, 6, VoxelData::FLAG_SMOOTH);
    // Step 3 (high)
    fillBlock(c00, 18, 2, 14, 20, 4, 16, 6, VoxelData::FLAG_SMOOTH);

    // ── Config 9 (X=12..20, Z=20): Large smooth terrain patch ──
    // SmoothStone ground with grass neighbors on all sides
    fillBlock(c00, 14, 2, 20, 18, 3, 24, 6, VoxelData::FLAG_SMOOTH);
    // Grass borders
    fillBlock(c00, 12, 2, 20, 13, 3, 24, 1);
    fillBlock(c00, 19, 2, 20, 20, 3, 24, 1);
    fillBlock(c00, 14, 2, 18, 18, 3, 19, 1);
    fillBlock(c00, 14, 2, 25, 18, 3, 26, 1);

    // ── Config 10 (X=24..28, Z=2): Snow smooth next to Sand ──
    fillBlock(c00, 24, 2, 2, 26, 4, 4, 5, VoxelData::FLAG_SMOOTH);
    fillBlock(c00, 27, 2, 2, 29, 4, 4, 4);

    // ── Config 11 (X=24..28, Z=8): Isolated smooth block (no blending) ──
    fillBlock(c00, 25, 2, 9, 27, 4, 11, 6, VoxelData::FLAG_SMOOTH);

    // Mark all chunks dirty
    for (auto& [pos, chunk] : chunks_) {
        chunk->dirty = true;
    }
}

} // namespace voxel
