#include "TextureCache.h"
#include "SimpleTextureLoader.h"
#include <cmath>
#include <cstdint>
#include <filesystem>
#include <vector>
#include <algorithm>

std::unordered_map<std::string, GLuint> TextureCache::cache_;

namespace {

// Deterministic value noise ----------------------------------------------
uint32_t hash32(uint32_t x) {
    x ^= x >> 16; x *= 0x7feb352du;
    x ^= x >> 15; x *= 0x846ca68bu;
    x ^= x >> 16;
    return x;
}
float hashf(int x, int y, uint32_t seed = 0) {
    uint32_t h = hash32((uint32_t)x * 374761393u + (uint32_t)y * 668265263u + seed);
    return (h & 0xffffff) / float(0xffffff);
}
float smooth(float t) { return t * t * (3.0f - 2.0f * t); }
float noise2D(float x, float y, uint32_t seed = 0) {
    int xi = (int)std::floor(x), yi = (int)std::floor(y);
    float xf = x - xi, yf = y - yi;
    float a = hashf(xi,     yi,     seed);
    float b = hashf(xi + 1, yi,     seed);
    float c = hashf(xi,     yi + 1, seed);
    float d = hashf(xi + 1, yi + 1, seed);
    float u = smooth(xf), v = smooth(yf);
    return (1 - u) * (1 - v) * a + u * (1 - v) * b
         + (1 - u) * v * c + u * v * d;
}
float fbm(float x, float y, int octaves, uint32_t seed = 0) {
    float total = 0, amp = 1, sum = 0;
    for (int i = 0; i < octaves; ++i) {
        total += noise2D(x, y, seed + i * 7919u) * amp;
        sum += amp; amp *= 0.5f; x *= 2; y *= 2;
    }
    return total / sum;
}

void setPixel(std::vector<unsigned char>& buf, int w, int x, int y,
              unsigned char r, unsigned char g, unsigned char b, unsigned char a = 255) {
    int i = (y * w + x) * 4;
    buf[i + 0] = r; buf[i + 1] = g; buf[i + 2] = b; buf[i + 3] = a;
}

// Linear interpolate between 2 RGB colors (0-255)
void lerpColor(float t, const float a[3], const float b[3], unsigned char out[3]) {
    t = std::clamp(t, 0.0f, 1.0f);
    out[0] = (unsigned char)std::clamp(a[0] + (b[0] - a[0]) * t, 0.0f, 255.0f);
    out[1] = (unsigned char)std::clamp(a[1] + (b[1] - a[1]) * t, 0.0f, 255.0f);
    out[2] = (unsigned char)std::clamp(a[2] + (b[2] - a[2]) * t, 0.0f, 255.0f);
}

}  // namespace

GLuint TextureCache::UploadRGBA(const unsigned char* data, int w, int h, bool srgb) {
    GLuint id;
    glGenTextures(1, &id);
    glBindTexture(GL_TEXTURE_2D, id);
    GLint internalFmt = srgb ? GL_SRGB8_ALPHA8 : GL_RGBA8;
    glTexImage2D(GL_TEXTURE_2D, 0, internalFmt, w, h, 0,
                 GL_RGBA, GL_UNSIGNED_BYTE, data);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glGenerateMipmap(GL_TEXTURE_2D);
    return id;
}

GLuint TextureCache::Procedural(const std::string& name) {
    const int W = 512, H = 512;
    std::vector<unsigned char> buf(W * H * 4, 255);

    if (name == "white") {
        std::fill(buf.begin(), buf.end(), 255);
    } else if (name == "black") {
        for (int i = 0; i < W * H; ++i) {
            buf[i * 4 + 0] = 0; buf[i * 4 + 1] = 0; buf[i * 4 + 2] = 0;
            buf[i * 4 + 3] = 255;
        }
    } else if (name == "checker") {
        int cell = 32;
        for (int y = 0; y < H; ++y) for (int x = 0; x < W; ++x) {
            bool k = ((x / cell) + (y / cell)) & 1;
            unsigned char v = k ? 230 : 50;
            setPixel(buf, W, x, y, v, v, v);
        }
    } else if (name == "grid") {
        int cell = 64;
        for (int y = 0; y < H; ++y) for (int x = 0; x < W; ++x) {
            bool line = (x % cell == 0) || (y % cell == 0)
                     || (x % cell == cell - 1) || (y % cell == cell - 1);
            unsigned char v = line ? 30 : 220;
            setPixel(buf, W, x, y, v, v, v);
        }
    } else if (name == "stripes") {
        for (int y = 0; y < H; ++y) for (int x = 0; x < W; ++x) {
            bool k = ((x / 32) & 1);
            if (k) setPixel(buf, W, x, y, 220, 220, 210);
            else   setPixel(buf, W, x, y, 80, 50, 40);
        }
    } else if (name == "brick") {
        int bw = 128, bh = 48, mortar = 3;
        for (int y = 0; y < H; ++y) for (int x = 0; x < W; ++x) {
            int row = y / bh;
            int offset = (row & 1) ? bw / 2 : 0;
            int mx = (x + offset) % bw;
            int my = y % bh;
            bool is_mortar = (mx < mortar) || (my < mortar);
            if (is_mortar) setPixel(buf, W, x, y, 180, 180, 170);
            else {
                float n = fbm(x * 0.05f, y * 0.05f, 4, 11) * 0.3f + 0.7f;
                unsigned char r = (unsigned char)(150 * n);
                unsigned char g = (unsigned char)(60  * n);
                unsigned char b = (unsigned char)(40  * n);
                setPixel(buf, W, x, y, r, g, b);
            }
        }
    } else if (name == "wood") {
        for (int y = 0; y < H; ++y) for (int x = 0; x < W; ++x) {
            // Rings + grain
            float r = std::sqrt((x - W * 0.5f) * (x - W * 0.5f)
                               + (y - H * 0.5f) * (y - H * 0.5f)) * 0.04f;
            float rings = std::sin(r + fbm(x * 0.01f, y * 0.01f, 4, 7) * 2.0f);
            float grain = fbm(x * 0.08f, y * 0.008f, 4, 13);
            float t = std::clamp(0.5f + 0.3f * rings + 0.3f * grain, 0.0f, 1.0f);
            const float dark[3] = { 85,  48,  22};
            const float light[3]= {175, 120,  70};
            unsigned char c[3];
            lerpColor(t, dark, light, c);
            setPixel(buf, W, x, y, c[0], c[1], c[2]);
        }
    } else if (name == "marble") {
        for (int y = 0; y < H; ++y) for (int x = 0; x < W; ++x) {
            float f = fbm(x * 0.012f, y * 0.012f, 5, 3);
            float v = std::sin(x * 0.02f + f * 5.0f);
            float t = std::clamp(0.7f + v * 0.25f, 0.0f, 1.0f);
            const float dark[3] = {190, 195, 200};
            const float light[3]= {245, 245, 250};
            unsigned char c[3];
            lerpColor(t, dark, light, c);
            setPixel(buf, W, x, y, c[0], c[1], c[2]);
        }
    } else if (name == "concrete") {
        for (int y = 0; y < H; ++y) for (int x = 0; x < W; ++x) {
            float n = fbm(x * 0.05f, y * 0.05f, 5, 17);
            float speck = hashf(x, y, 42);
            unsigned char v = (unsigned char)(160 + n * 60 - speck * 20);
            v = std::clamp<int>(v, 90, 230);
            setPixel(buf, W, x, y, v, v, v + 4);
        }
    } else if (name == "dirt") {
        for (int y = 0; y < H; ++y) for (int x = 0; x < W; ++x) {
            float f = fbm(x * 0.04f, y * 0.04f, 5, 23);
            const float low[3]  = { 55, 35, 20};
            const float high[3] = {130, 90, 55};
            unsigned char c[3];
            lerpColor(f, low, high, c);
            setPixel(buf, W, x, y, c[0], c[1], c[2]);
        }
    } else if (name == "rock") {
        for (int y = 0; y < H; ++y) for (int x = 0; x < W; ++x) {
            float f = fbm(x * 0.02f, y * 0.02f, 6, 31);
            float cracks = std::pow(std::abs(std::sin(x * 0.01f + f * 8.0f)), 40.0f);
            float v = 100 + f * 100 - cracks * 40;
            unsigned char c = (unsigned char)std::clamp(v, 40.0f, 230.0f);
            setPixel(buf, W, x, y, c, c - 4, c - 8);
        }
    } else if (name == "grass") {
        for (int y = 0; y < H; ++y) for (int x = 0; x < W; ++x) {
            float f = fbm(x * 0.08f, y * 0.08f, 4, 53);
            const float low[3]  = { 40, 80, 25};
            const float high[3] = {105, 160, 55};
            unsigned char c[3];
            lerpColor(f, low, high, c);
            // Thin darker blades
            float blade = std::pow(std::abs(std::sin(x * 0.2f + f * 3.0f)), 12.0f);
            c[0] = (unsigned char)std::clamp<int>(c[0] - (int)(blade * 35), 0, 255);
            c[1] = (unsigned char)std::clamp<int>(c[1] - (int)(blade * 35), 0, 255);
            c[2] = (unsigned char)std::clamp<int>(c[2] - (int)(blade * 15), 0, 255);
            setPixel(buf, W, x, y, c[0], c[1], c[2]);
        }
    } else if (name == "noise") {
        for (int y = 0; y < H; ++y) for (int x = 0; x < W; ++x) {
            float f = fbm(x * 0.03f, y * 0.03f, 5, 1);
            unsigned char v = (unsigned char)(f * 255);
            setPixel(buf, W, x, y, v, v, v);
        }
    } else {
        return 0;  // unknown
    }

    return UploadRGBA(buf.data(), W, H, /*srgb=*/true);
}

GLuint TextureCache::LoadFromFile(const std::string& path) {
    namespace fs = std::filesystem;
    if (!fs::exists(path)) return 0;
    std::string ext = fs::path(path).extension().string();
    for (auto& c : ext) c = (char)std::tolower((unsigned char)c);
    if (ext == ".bmp") return SimpleTextureLoader::LoadBMP(path);
    if (ext == ".tga") return SimpleTextureLoader::LoadTGA(path);
    return 0;
}

GLuint TextureCache::Get(const std::string& name) {
    if (name.empty()) return DefaultWhite();
    auto it = cache_.find(name);
    if (it != cache_.end()) return it->second;

    GLuint id = Procedural(name);
    if (id == 0) id = LoadFromFile(name);
    if (id == 0) {
        // Fallback to white rather than 0 so shader path stays valid.
        id = DefaultWhite();
    }
    cache_[name] = id;
    return id;
}

GLuint TextureCache::DefaultWhite() {
    static GLuint white = 0;
    if (white != 0) return white;
    unsigned char px[4] = {255, 255, 255, 255};
    glGenTextures(1, &white);
    glBindTexture(GL_TEXTURE_2D, white);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 1, 1, 0,
                 GL_RGBA, GL_UNSIGNED_BYTE, px);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    return white;
}

GLuint TextureCache::DefaultNormal() {
    static GLuint n = 0;
    if (n != 0) return n;
    // Tangent-space (0,0,1) encodes as (0.5, 0.5, 1.0).
    unsigned char px[4] = {128, 128, 255, 255};
    glGenTextures(1, &n);
    glBindTexture(GL_TEXTURE_2D, n);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 1, 1, 0,
                 GL_RGBA, GL_UNSIGNED_BYTE, px);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    return n;
}

void TextureCache::Clear() {
    for (auto& [k, v] : cache_) glDeleteTextures(1, &v);
    cache_.clear();
}
