#include "Web/MapTileService.h"

#include <algorithm>
#include <cstring>
#include <filesystem>
#include <string>
#include <vector>

#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <Windows.h>

#include "EmbeddedMaps.h"

static std::filesystem::path ExeDir() {
    wchar_t buf[MAX_PATH] = {};
    GetModuleFileNameW(nullptr, buf, MAX_PATH);
    return std::filesystem::path(buf).parent_path();
}

// stb_image / stb_image_write are implemented in stb_impl.cpp.
// Include the headers only — no implementation macros here.
#include <stb_image.h>
#include <stb_image_write.h>

static constexpr int kTileSize = 512;
static constexpr int kChannels = 4; // RGBA

// -------------------------------------------------------------------------
// PNG write callback: appends bytes to a vector<uint8_t>*.
// -------------------------------------------------------------------------

static void PngWriteCallback(void* context, void* data, int size) {
    auto* buf = static_cast<std::vector<uint8_t>*>(context);
    const auto* bytes = static_cast<const uint8_t*>(data);
    buf->insert(buf->end(), bytes, bytes + size);
}

// -------------------------------------------------------------------------
// MapTileService::LoadOrGet
// -------------------------------------------------------------------------

const MapTileService::Image* MapTileService::LoadOrGet(const std::string& mapId) {
    // Caller must already hold m_mutex.
    auto it = m_cache.find(mapId);
    if (it != m_cache.end()) {
        return &it->second;  // cache hit
    }

    // Disk file takes priority — allows map updates without rebuilding.
    // Falls back to the PNG baked into the binary as a Windows RCDATA resource.
    std::string path = (ExeDir() / "maps" / (mapId + ".png")).string();

    int w = 0, h = 0, channels = 0;
    unsigned char* raw = nullptr;

    if (std::filesystem::exists(path)) {
        raw = stbi_load(path.c_str(), &w, &h, &channels, kChannels);
    } else {
        auto [ptr, sz] = GetEmbeddedMap(mapId);
        if (ptr)
            raw = stbi_load_from_memory(ptr, static_cast<int>(sz),
                                        &w, &h, &channels, kChannels);
    }

    Image img;
    if (raw && w > 0 && h > 0) {
        img.w = w;
        img.h = h;
        img.pixels.assign(raw, raw + static_cast<size_t>(w) * h * kChannels);
        stbi_image_free(raw);
    } else {
        if (raw) stbi_image_free(raw);
        // Don't cache the failure — the user may drop the PNG in while running.
        return nullptr;
    }

    auto [inserted_it, ok] = m_cache.emplace(mapId, std::move(img));
    (void)ok;
    return &inserted_it->second;
}

// -------------------------------------------------------------------------
// MapTileService::GetTile
// -------------------------------------------------------------------------

std::vector<uint8_t> MapTileService::GetTile(const MapInfo& map, int tileX,
                                              int tileY, int& errCode)
{
    if (tileX < 0 || tileY < 0) {
        errCode = 400;
        return {};
    }

    std::lock_guard<std::mutex> lk(m_mutex);

    // Tile PNG cache — keyed by "mapId:tx:ty".  PNG encoding is expensive
    // (zlib compression on a 512×512 RGBA block); cache the result so repeated
    // requests for the same tile (pan/zoom, reconnect, second browser tab) are
    // just a map lookup + memcpy instead of a full re-encode.
    std::string tileKey = map.id + ':' + std::to_string(tileX) + ':' + std::to_string(tileY);
    {
        auto it = m_tileCache.find(tileKey);
        if (it != m_tileCache.end()) {
            errCode = 200;
            return it->second;
        }
    }

    const Image* img = LoadOrGet(map.id);
    if (!img || img->pixels.empty() || img->w == 0 || img->h == 0) {
        errCode = 404;
        return {};
    }

    const int pixW = img->w;
    const int pixH = img->h;
    const float ws = static_cast<float>(map.worldSize);

    const float scaleX = static_cast<float>(pixW) / ws;
    const float scaleY = static_cast<float>(pixH) / ws;

    int srcX = static_cast<int>(static_cast<float>(tileX) * kTileSize * scaleX);
    int srcY = static_cast<int>(static_cast<float>(tileY) * kTileSize * scaleY);
    int srcW = static_cast<int>(kTileSize * scaleX);
    int srcH = static_cast<int>(kTileSize * scaleY);

    // Clamp to image extents.
    if (srcX >= pixW || srcY >= pixH) {
        errCode = 404;
        return {};
    }
    srcW = std::max(1, std::min(srcW, pixW - srcX));
    srcH = std::max(1, std::min(srcH, pixH - srcY));

    // Build 512×512 RGBA output via nearest-neighbour scaling.
    std::vector<uint8_t> tile(static_cast<size_t>(kTileSize) * kTileSize * kChannels, 0);

    for (int ty = 0; ty < kTileSize; ++ty) {
        // Map output pixel row → source row.
        int sy = srcY + static_cast<int>((static_cast<float>(ty) / kTileSize) * srcH);
        sy = std::clamp(sy, 0, pixH - 1);

        for (int tx = 0; tx < kTileSize; ++tx) {
            int sx = srcX + static_cast<int>((static_cast<float>(tx) / kTileSize) * srcW);
            sx = std::clamp(sx, 0, pixW - 1);

            const size_t srcOff  = (static_cast<size_t>(sy) * pixW + sx) * kChannels;
            const size_t dstOff  = (static_cast<size_t>(ty) * kTileSize + tx) * kChannels;

            std::memcpy(tile.data() + dstOff, img->pixels.data() + srcOff, kChannels);
        }
    }

    // Encode to PNG.
    std::vector<uint8_t> png;
    png.reserve(kTileSize * kTileSize); // rough estimate

    int ok = stbi_write_png_to_func(PngWriteCallback, &png,
                                    kTileSize, kTileSize, kChannels,
                                    tile.data(),
                                    kTileSize * kChannels);
    if (!ok || png.empty()) {
        errCode = 500;
        return {};
    }

    m_tileCache.emplace(std::move(tileKey), png);

    errCode = 200;
    return png;
}
