#include "tile_raster_map.h"

#include <windows.h>
#include <winhttp.h>

#include <atomic>
#include <condition_variable>
#include <cmath>
#include <deque>
#include <filesystem>
#include <fstream>
#include <functional>
#include <iomanip>
#include <memory>
#include <mutex>
#include <sstream>
#include <thread>
#include <unordered_map>
#include <vector>

#define STB_IMAGE_IMPLEMENTATION
#include "../external/stb/stb_image.h"

#include "../gui/dxhook/dxhook.h"
#include "../module/minimap.h"
#include "logger.h"

#pragma comment(lib, "winhttp.lib")

namespace tile_raster_map
{
    namespace
    {
        std::mutex g_http_mutex;

        enum class stitch_job_state : int
        {
            queued = 0,
            running = 1,
            ready = 2,
            failed = 3,
        };

        struct stitch_job
        {
            std::string key;
            uint64_t signature = 0;
            tile_map_params params{};
            std::atomic<int> state{static_cast<int>(stitch_job_state::queued)};
            std::vector<uint8_t> rgba;
            uint32_t width = 0;
            uint32_t height = 0;
        };

        std::mutex g_jobs_mutex;
        std::unordered_map<std::string, std::shared_ptr<stitch_job>> g_jobs;

        std::mutex g_queue_mutex;
        std::condition_variable g_queue_cv;
        std::deque<std::shared_ptr<stitch_job>> g_job_queue;
        std::atomic<bool> g_worker_started{false};

        struct stitched_cache_header
        {
            char magic[4]{'T', 'A', 'S', 'T'};
            uint32_t version = 1;
            uint64_t signature = 0;
            uint32_t width = 0;
            uint32_t height = 0;
            float bounds_min_x = 0.f;
            float bounds_max_x = 0.f;
            float bounds_min_z = 0.f;
            float bounds_max_z = 0.f;
        };

        void rotate_world(const float x, const float z, const int rotation_deg, float& out_x, float& out_z)
        {
            if (rotation_deg == 0)
            {
                out_x = x;
                out_z = z;
                return;
            }

            const float rad = static_cast<float>(rotation_deg) * 3.14159265f / 180.f;
            const float c = cosf(rad);
            const float s = sinf(rad);
            out_x = x * c - z * s;
            out_z = x * s + z * c;
        }

        void world_to_pixel(const float x, const float z, const int zoom, const tile_map_params& params,
                            float& out_px, float& out_py)
        {
            float rx = 0.f;
            float rz = 0.f;
            rotate_world(x, z, params.coordinate_rotation, rx, rz);

            const float scale = powf(2.f, static_cast<float>(zoom));
            const float scale_y = params.transform[2] * -1.f;
            out_px = (params.transform[0] * rx + params.transform[1]) * scale;
            out_py = (scale_y * rz + params.transform[3]) * scale;
        }

        std::string replace_token(std::string value, const std::string& token, const std::string& replacement)
        {
            size_t pos = 0;
            while ((pos = value.find(token, pos)) != std::string::npos)
            {
                value.replace(pos, token.size(), replacement);
                pos += replacement.size();
            }
            return value;
        }

        std::wstring utf8_to_wide(const std::string& text)
        {
            if (text.empty())
                return {};

            const int len = MultiByteToWideChar(CP_UTF8, 0, text.c_str(), -1, nullptr, 0);
            if (len <= 0)
                return {};

            std::wstring wide(static_cast<size_t>(len), L'\0');
            MultiByteToWideChar(CP_UTF8, 0, text.c_str(), -1, wide.data(), len);
            if (!wide.empty() && wide.back() == L'\0')
                wide.pop_back();
            return wide;
        }

        bool download_url(const std::string& url, std::vector<uint8_t>& out_bytes)
        {
            std::lock_guard<std::mutex> lock(g_http_mutex);
            out_bytes.clear();

            const size_t scheme_end = url.find("://");
            if (scheme_end == std::string::npos)
                return false;

            const size_t host_start = scheme_end + 3;
            const size_t path_start = url.find('/', host_start);
            if (path_start == std::string::npos)
                return false;

            const std::string host = url.substr(host_start, path_start - host_start);
            const std::string path = url.substr(path_start);

            HINTERNET session = WinHttpOpen(L"TarkovAddons/1.0", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
                WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
            if (!session)
                return false;

            const std::wstring host_wide = utf8_to_wide(host);
            HINTERNET connection = WinHttpConnect(session, host_wide.c_str(), INTERNET_DEFAULT_HTTPS_PORT, 0);
            if (!connection)
            {
                WinHttpCloseHandle(session);
                return false;
            }

            const std::wstring path_wide = utf8_to_wide(path);
            HINTERNET request = WinHttpOpenRequest(connection, L"GET", path_wide.c_str(), nullptr,
                WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, WINHTTP_FLAG_SECURE);
            if (!request)
            {
                WinHttpCloseHandle(connection);
                WinHttpCloseHandle(session);
                return false;
            }

            const bool sent = WinHttpSendRequest(request, WINHTTP_NO_ADDITIONAL_HEADERS, 0,
                WINHTTP_NO_REQUEST_DATA, 0, 0, 0) != FALSE;
            if (!sent || WinHttpReceiveResponse(request, nullptr) == FALSE)
            {
                WinHttpCloseHandle(request);
                WinHttpCloseHandle(connection);
                WinHttpCloseHandle(session);
                return false;
            }

            for (;;)
            {
                DWORD available = 0;
                if (!WinHttpQueryDataAvailable(request, &available) || available == 0)
                    break;

                const size_t offset = out_bytes.size();
                out_bytes.resize(offset + available);
                DWORD downloaded = 0;
                if (!WinHttpReadData(request, out_bytes.data() + offset, available, &downloaded))
                {
                    out_bytes.resize(offset);
                    break;
                }
                out_bytes.resize(offset + downloaded);
            }

            WinHttpCloseHandle(request);
            WinHttpCloseHandle(connection);
            WinHttpCloseHandle(session);
            return !out_bytes.empty();
        }

        bool read_file_bytes(const std::filesystem::path& path, std::vector<uint8_t>& out_bytes)
        {
            std::ifstream input(path, std::ios::binary);
            if (!input.is_open())
                return false;

            input.seekg(0, std::ios::end);
            const std::streamoff size = input.tellg();
            if (size <= 0)
                return false;

            out_bytes.resize(static_cast<size_t>(size));
            input.seekg(0, std::ios::beg);
            input.read(reinterpret_cast<char*>(out_bytes.data()), size);
            return input.good() || input.eof();
        }

        bool write_file_bytes(const std::filesystem::path& path, const std::vector<uint8_t>& bytes)
        {
            std::error_code ec;
            std::filesystem::create_directories(path.parent_path(), ec);

            std::ofstream output(path, std::ios::binary | std::ios::trunc);
            if (!output.is_open())
                return false;

            output.write(reinterpret_cast<const char*>(bytes.data()),
                static_cast<std::streamsize>(bytes.size()));
            return output.good();
        }

        std::string sanitize_cache_segment(const std::string& value, const char* fallback)
        {
            if (value.empty())
                return fallback;

            std::string out = value;
            for (char& ch : out)
            {
                if (ch == '/' || ch == '\\' || ch == ':' || ch == '*' || ch == '?'
                    || ch == '"' || ch == '<' || ch == '>' || ch == '|')
                    ch = '_';
            }
            return out;
        }

        std::filesystem::path tile_cache_root(const tile_map_params& params)
        {
            const std::string map_folder = sanitize_cache_segment(params.map_id, "unknown");
            return std::filesystem::path(minimap::get_maps_directory()) / "tile_cache" / map_folder;
        }

        std::filesystem::path tile_cache_path(const tile_map_params& params, const int zoom,
                                              const int tile_x, const int tile_y)
        {
            const std::string layer = sanitize_cache_segment(params.layer_slug, "base");

            std::ostringstream name;
            name << layer << "_z" << zoom << "_x" << tile_x << "_y" << tile_y << ".png";
            return tile_cache_root(params) / name.str();
        }

        bool load_png_rgba(const std::vector<uint8_t>& bytes, std::vector<uint8_t>& rgba,
                           int& out_w, int& out_h)
        {
            if (bytes.size() < 512u)
                return false;

            int comp = 0;
            unsigned char* pixels = stbi_load_from_memory(bytes.data(), static_cast<int>(bytes.size()),
                &out_w, &out_h, &comp, 4);
            if (!pixels || out_w <= 0 || out_h <= 0)
            {
                if (pixels)
                    stbi_image_free(pixels);
                return false;
            }

            const size_t count = static_cast<size_t>(out_w) * static_cast<size_t>(out_h) * 4u;
            rgba.assign(pixels, pixels + count);
            stbi_image_free(pixels);
            return true;
        }

        void remove_placeholder_cache_file(const std::filesystem::path& cache_path)
        {
            std::error_code ec;
            std::filesystem::remove(cache_path, ec);
        }

        bool fetch_tile_png(const tile_map_params& params, const int zoom, const int tile_x, const int tile_y,
                            std::vector<uint8_t>& out_rgba, int& out_w, int& out_h)
        {
            const std::filesystem::path cache_path = tile_cache_path(params, zoom, tile_x, tile_y);
            std::vector<uint8_t> encoded;
            if (std::filesystem::exists(cache_path) && read_file_bytes(cache_path, encoded))
            {
                if (load_png_rgba(encoded, out_rgba, out_w, out_h))
                    return true;

                remove_placeholder_cache_file(cache_path);
            }

            std::string url = params.tile_path_template;
            url = replace_token(url, "{z}", std::to_string(zoom));
            url = replace_token(url, "{x}", std::to_string(tile_x));
            url = replace_token(url, "{y}", std::to_string(tile_y));

            if (!download_url(url, encoded))
                return false;

            if (!load_png_rgba(encoded, out_rgba, out_w, out_h))
                return false;

            write_file_bytes(cache_path, encoded);
            return true;
        }

        bool compute_tile_range(const tile_map_params& params, const int zoom,
                                int& out_x0, int& out_x1, int& out_y0, int& out_y1)
        {
            float corners[4][2] = {
                {params.bounds.min_x, params.bounds.min_z},
                {params.bounds.max_x, params.bounds.min_z},
                {params.bounds.min_x, params.bounds.max_z},
                {params.bounds.max_x, params.bounds.max_z},
            };

            float min_px = 0.f;
            float min_py = 0.f;
            float max_px = 0.f;
            float max_py = 0.f;
            bool first = true;
            for (const auto& corner : corners)
            {
                float px = 0.f;
                float py = 0.f;
                world_to_pixel(corner[0], corner[1], zoom, params, px, py);
                if (first)
                {
                    min_px = max_px = px;
                    min_py = max_py = py;
                    first = false;
                }
                else
                {
                    min_px = (std::min)(min_px, px);
                    max_px = (std::max)(max_px, px);
                    min_py = (std::min)(min_py, py);
                    max_py = (std::max)(max_py, py);
                }
            }

            const int tile_size = (std::max)(1, params.tile_size);
            out_x0 = static_cast<int>(floorf(min_px / static_cast<float>(tile_size)));
            out_x1 = static_cast<int>(floorf(max_px / static_cast<float>(tile_size)));
            out_y0 = static_cast<int>(floorf(min_py / static_cast<float>(tile_size)));
            out_y1 = static_cast<int>(floorf(max_py / static_cast<float>(tile_size)));
            return out_x1 >= out_x0 && out_y1 >= out_y0;
        }

        int estimate_tile_count(const tile_map_params& params, const int zoom)
        {
            int x0 = 0;
            int x1 = 0;
            int y0 = 0;
            int y1 = 0;
            if (!compute_tile_range(params, zoom, x0, x1, y0, y1))
                return 0;
            return (x1 - x0 + 1) * (y1 - y0 + 1);
        }

        bool tile_cache_file_usable(const tile_map_params& params, const int zoom,
                                    const int tile_x, const int tile_y)
        {
            const std::filesystem::path cache_path = tile_cache_path(params, zoom, tile_x, tile_y);
            std::error_code ec;
            if (!std::filesystem::exists(cache_path, ec))
                return false;

            const auto file_size = std::filesystem::file_size(cache_path, ec);
            return !ec && file_size > 512u;
        }

        int count_usable_cached_tiles_in_range(const tile_map_params& params, const int zoom)
        {
            int x0 = 0;
            int x1 = 0;
            int y0 = 0;
            int y1 = 0;
            if (!compute_tile_range(params, zoom, x0, x1, y0, y1))
                return 0;

            int count = 0;
            for (int ty = y0; ty <= y1; ++ty)
            {
                for (int tx = x0; tx <= x1; ++tx)
                {
                    if (tile_cache_file_usable(params, zoom, tx, ty))
                        ++count;
                }
            }
            return count;
        }

        bool probe_zoom_tiles(const tile_map_params& params, const int zoom)
        {
            int x0 = 0;
            int x1 = 0;
            int y0 = 0;
            int y1 = 0;
            if (!compute_tile_range(params, zoom, x0, x1, y0, y1))
                return false;

            const int probe_x = (x0 + x1) / 2;
            const int probe_y = (y0 + y1) / 2;
            if (tile_cache_file_usable(params, zoom, probe_x, probe_y))
                return true;

            std::vector<uint8_t> rgba;
            int tile_w = 0;
            int tile_h = 0;
            if (!fetch_tile_png(params, zoom, probe_x, probe_y, rgba, tile_w, tile_h))
                return false;

            const int tile_size = (std::max)(1, params.tile_size);
            return tile_w >= tile_size / 2 && tile_h >= tile_size / 2;
        }

        int choose_zoom(const tile_map_params& params)
        {
            constexpr int k_max_tiles = 400;
            for (int zoom = params.max_zoom; zoom >= params.min_zoom; --zoom)
            {
                if (estimate_tile_count(params, zoom) > k_max_tiles)
                    continue;

                if (count_usable_cached_tiles_in_range(params, zoom) > 0)
                    return zoom;

                if (probe_zoom_tiles(params, zoom))
                    return zoom;
            }
            return params.min_zoom;
        }

        bool crop_rgba(const std::vector<uint8_t>& src, const int src_w, const int src_h,
                       const int x0, const int y0, const int crop_w, const int crop_h,
                       std::vector<uint8_t>& out)
        {
            if (src_w <= 0 || src_h <= 0 || crop_w <= 0 || crop_h <= 0)
                return false;
            if (x0 < 0 || y0 < 0 || x0 + crop_w > src_w || y0 + crop_h > src_h)
                return false;

            out.resize(static_cast<size_t>(crop_w) * static_cast<size_t>(crop_h) * 4u);
            for (int row = 0; row < crop_h; ++row)
            {
                const size_t src_offset = (static_cast<size_t>(y0 + row) * static_cast<size_t>(src_w)
                    + static_cast<size_t>(x0)) * 4u;
                const size_t dst_offset = static_cast<size_t>(row) * static_cast<size_t>(crop_w) * 4u;
                memcpy(out.data() + dst_offset, src.data() + src_offset,
                    static_cast<size_t>(crop_w) * 4u);
            }
            return true;
        }

        bool upload_rgba_texture(const std::vector<uint8_t>& rgba, const uint32_t width, const uint32_t height,
                                 raster_layer& layer)
        {
            if (!dxhook::is_ready())
                return false;

            ImTextureID tex_id = ImTextureID_Invalid;
            ID3D12Resource* resource = nullptr;
            if (!dxhook::create_texture_rgba(rgba.data(), width, height, tex_id, &resource))
                return false;

            layer.tex_id = tex_id;
            layer.dx12_resource = resource;
            layer.raster_width = width;
            layer.raster_height = height;
            layer.width = static_cast<float>(width);
            layer.height = static_cast<float>(height);
            return true;
        }

        std::filesystem::path stitched_cache_path(const tile_map_params& params, const uint64_t signature)
        {
            std::ostringstream name;
            name << std::hex << std::nouppercase << signature << ".bin";
            return tile_cache_root(params) / "stitched" / name.str();
        }

        bool save_stitched_cache(const uint64_t signature, const tile_map_params& params,
                                 const std::vector<uint8_t>& rgba, const uint32_t width, const uint32_t height)
        {
            if (rgba.empty() || width == 0 || height == 0)
                return false;

            stitched_cache_header header{};
            header.signature = signature;
            header.width = width;
            header.height = height;
            header.bounds_min_x = params.bounds.min_x;
            header.bounds_max_x = params.bounds.max_x;
            header.bounds_min_z = params.bounds.min_z;
            header.bounds_max_z = params.bounds.max_z;

            std::vector<uint8_t> bytes(sizeof(header) + rgba.size());
            memcpy(bytes.data(), &header, sizeof(header));
            memcpy(bytes.data() + sizeof(header), rgba.data(), rgba.size());
            return write_file_bytes(stitched_cache_path(params, signature), bytes);
        }

        bool load_stitched_cache(const tile_map_params& params, const uint64_t signature,
                                 std::vector<uint8_t>& out_rgba, uint32_t& out_w, uint32_t& out_h)
        {
            std::vector<uint8_t> bytes;
            if (!read_file_bytes(stitched_cache_path(params, signature), bytes))
                return false;
            if (bytes.size() <= sizeof(stitched_cache_header))
                return false;

            stitched_cache_header header{};
            memcpy(&header, bytes.data(), sizeof(header));
            if (memcmp(header.magic, "TAST", 4) != 0 || header.version != 1 || header.signature != signature)
                return false;
            if (header.width == 0 || header.height == 0)
                return false;

            const size_t expected = sizeof(header)
                + static_cast<size_t>(header.width) * static_cast<size_t>(header.height) * 4u;
            if (bytes.size() != expected)
                return false;

            out_w = header.width;
            out_h = header.height;
            out_rgba.resize(expected - sizeof(header));
            memcpy(out_rgba.data(), bytes.data() + sizeof(header), out_rgba.size());
            return true;
        }

        struct tile_stitch_projection
        {
            int zoom = 0;
            float min_px = 0.f;
            float max_px = 0.f;
            float min_py = 0.f;
            float max_py = 0.f;
        };

        bool compute_stitch_projection(const tile_map_params& params, tile_stitch_projection& out)
        {
            if (params.bounds.max_x <= params.bounds.min_x || params.bounds.max_z <= params.bounds.min_z)
                return false;

            const int zoom = choose_zoom(params);
            const float corners[4][2] = {
                {params.bounds.min_x, params.bounds.min_z},
                {params.bounds.max_x, params.bounds.min_z},
                {params.bounds.min_x, params.bounds.max_z},
                {params.bounds.max_x, params.bounds.max_z},
            };

            float min_px = 0.f;
            float min_py = 0.f;
            float max_px = 0.f;
            float max_py = 0.f;
            bool first = true;
            for (const auto& corner : corners)
            {
                float px = 0.f;
                float py = 0.f;
                world_to_pixel(corner[0], corner[1], zoom, params, px, py);
                if (first)
                {
                    min_px = max_px = px;
                    min_py = max_py = py;
                    first = false;
                }
                else
                {
                    min_px = (std::min)(min_px, px);
                    max_px = (std::max)(max_px, px);
                    min_py = (std::min)(min_py, py);
                    max_py = (std::max)(max_py, py);
                }
            }

            if (max_px <= min_px || max_py <= min_py)
                return false;

            out.zoom = zoom;
            out.min_px = min_px;
            out.max_px = max_px;
            out.min_py = min_py;
            out.max_py = max_py;
            return true;
        }

        void apply_tile_projection_to_layer(raster_layer& layer, const tile_map_params& params,
                                            const tile_stitch_projection& projection)
        {
            layer.has_tile_projection = true;
            layer.tile_zoom = projection.zoom;
            layer.tile_pixel_min_x = projection.min_px;
            layer.tile_pixel_max_x = projection.max_px;
            layer.tile_pixel_min_y = projection.min_py;
            layer.tile_pixel_max_y = projection.max_py;
            layer.tile_transform[0] = params.transform[0];
            layer.tile_transform[1] = params.transform[1];
            layer.tile_transform[2] = params.transform[2];
            layer.tile_transform[3] = params.transform[3];
            layer.tile_coordinate_rotation = params.coordinate_rotation;
        }

        bool world_to_tile_uv(const float world_x, const float world_z, const raster_layer& layer, ImVec2& out)
        {
            if (!layer.has_tile_projection)
                return false;

            tile_map_params params{};
            params.transform[0] = layer.tile_transform[0];
            params.transform[1] = layer.tile_transform[1];
            params.transform[2] = layer.tile_transform[2];
            params.transform[3] = layer.tile_transform[3];
            params.coordinate_rotation = layer.tile_coordinate_rotation;

            float px = 0.f;
            float py = 0.f;
            world_to_pixel(world_x, world_z, layer.tile_zoom, params, px, py);

            const float span_x = layer.tile_pixel_max_x - layer.tile_pixel_min_x;
            const float span_y = layer.tile_pixel_max_y - layer.tile_pixel_min_y;
            if (span_x <= 0.f || span_y <= 0.f)
                return false;

            out.x = (px - layer.tile_pixel_min_x) / span_x;
            out.y = (py - layer.tile_pixel_min_y) / span_y;
            return true;
        }

        bool stitch_tiles_to_rgba(const tile_map_params& params, std::vector<uint8_t>& out_rgba,
                                  uint32_t& out_w, uint32_t& out_h)
        {
            tile_stitch_projection projection{};
            if (!compute_stitch_projection(params, projection))
                return false;

            const int zoom = projection.zoom;
            const float min_px = projection.min_px;
            const float min_py = projection.min_py;
            const float max_px = projection.max_px;
            const float max_py = projection.max_py;

            const int tile_size = (std::max)(1, params.tile_size);
            const int tile_x0 = static_cast<int>(floorf(min_px / static_cast<float>(tile_size)));
            const int tile_x1 = static_cast<int>(floorf(max_px / static_cast<float>(tile_size)));
            const int tile_y0 = static_cast<int>(floorf(min_py / static_cast<float>(tile_size)));
            const int tile_y1 = static_cast<int>(floorf(max_py / static_cast<float>(tile_size)));

            const int stitched_w = (tile_x1 - tile_x0 + 1) * tile_size;
            const int stitched_h = (tile_y1 - tile_y0 + 1) * tile_size;
            if (stitched_w <= 0 || stitched_h <= 0)
                return false;

            std::vector<uint8_t> stitched(static_cast<size_t>(stitched_w) * static_cast<size_t>(stitched_h) * 4u, 0);
            int loaded_tiles = 0;

            for (int ty = tile_y0; ty <= tile_y1; ++ty)
            {
                for (int tx = tile_x0; tx <= tile_x1; ++tx)
                {
                    std::vector<uint8_t> tile_rgba;
                    int tile_w = 0;
                    int tile_h = 0;
                    if (!fetch_tile_png(params, zoom, tx, ty, tile_rgba, tile_w, tile_h))
                        continue;

                    if (tile_w != tile_size || tile_h != tile_size)
                        continue;

                    const int dst_x = (tx - tile_x0) * tile_size;
                    const int dst_y = (ty - tile_y0) * tile_size;
                    for (int row = 0; row < tile_size; ++row)
                    {
                        const size_t src_offset = static_cast<size_t>(row) * static_cast<size_t>(tile_size) * 4u;
                        const size_t dst_offset = (static_cast<size_t>(dst_y + row) * static_cast<size_t>(stitched_w)
                            + static_cast<size_t>(dst_x)) * 4u;
                        memcpy(stitched.data() + dst_offset, tile_rgba.data() + src_offset,
                            static_cast<size_t>(tile_size) * 4u);
                    }
                    ++loaded_tiles;
                }
            }

            if (loaded_tiles == 0)
                return false;

            const int crop_x0 = static_cast<int>(floorf(min_px)) - tile_x0 * tile_size;
            const int crop_y0 = static_cast<int>(floorf(min_py)) - tile_y0 * tile_size;
            const int crop_w = static_cast<int>(ceilf(max_px)) - static_cast<int>(floorf(min_px));
            const int crop_h = static_cast<int>(ceilf(max_py)) - static_cast<int>(floorf(min_py));

            out_w = static_cast<uint32_t>(stitched_w);
            out_h = static_cast<uint32_t>(stitched_h);
            out_rgba = std::move(stitched);

            if (crop_w > 0 && crop_h > 0
                && crop_x0 >= 0 && crop_y0 >= 0
                && crop_x0 + crop_w <= stitched_w && crop_y0 + crop_h <= stitched_h)
            {
                std::vector<uint8_t> cropped;
                if (crop_rgba(out_rgba, stitched_w, stitched_h, crop_x0, crop_y0, crop_w, crop_h, cropped))
                {
                    out_rgba = std::move(cropped);
                    out_w = static_cast<uint32_t>(crop_w);
                    out_h = static_cast<uint32_t>(crop_h);
                }
            }

            return true;
        }

        void stitch_worker_loop()
        {
            for (;;)
            {
                std::shared_ptr<stitch_job> job;
                {
                    std::unique_lock<std::mutex> lock(g_queue_mutex);
                    g_queue_cv.wait(lock, []
                    {
                        return !g_job_queue.empty();
                    });

                    job = g_job_queue.front();
                    g_job_queue.pop_front();
                }

                job->state.store(static_cast<int>(stitch_job_state::running));
                uint32_t width = 0;
                uint32_t height = 0;
                std::vector<uint8_t> rgba;
                if (stitch_tiles_to_rgba(job->params, rgba, width, height))
                {
                    job->rgba = std::move(rgba);
                    job->width = width;
                    job->height = height;
                    save_stitched_cache(job->signature, job->params, job->rgba, job->width, job->height);
                    job->state.store(static_cast<int>(stitch_job_state::ready));
                }
                else
                {
                    job->state.store(static_cast<int>(stitch_job_state::failed));
                }
            }
        }

        void ensure_stitch_worker()
        {
            bool expected = false;
            if (!g_worker_started.compare_exchange_strong(expected, true))
                return;

            std::thread(stitch_worker_loop).detach();
        }

        void enqueue_stitch_job(const std::shared_ptr<stitch_job>& job)
        {
            ensure_stitch_worker();
            {
                std::lock_guard<std::mutex> lock(g_queue_mutex);
                g_job_queue.push_back(job);
            }
            g_queue_cv.notify_one();
        }

        bool populate_layer_from_rgba(layer_cache& cache, const std::string& key, const uint64_t signature,
                                      const tile_map_params& params, const std::vector<uint8_t>& rgba,
                                      const uint32_t width, const uint32_t height)
        {
            raster_layer layer;
            if (!upload_rgba_texture(rgba, width, height, layer))
                return false;

            layer.has_tile_bounds = true;
            layer.tile_bounds_min_x = params.bounds.min_x;
            layer.tile_bounds_max_x = params.bounds.max_x;
            layer.tile_bounds_min_z = params.bounds.min_z;
            layer.tile_bounds_max_z = params.bounds.max_z;
            layer.cache_signature = signature;

            tile_stitch_projection projection{};
            if (compute_stitch_projection(params, projection))
                apply_tile_projection_to_layer(layer, params, projection);

            cache[key] = std::move(layer);
            return true;
        }

        bool try_finalize_ready_job(layer_cache& cache, const std::string& key, const uint64_t signature)
        {
            std::shared_ptr<stitch_job> job;
            {
                std::lock_guard<std::mutex> lock(g_jobs_mutex);
                const auto it = g_jobs.find(key);
                if (it == g_jobs.end())
                    return false;
                job = it->second;
            }

            if (job->signature != signature)
                return false;

            const int state = job->state.load();
            if (state == static_cast<int>(stitch_job_state::ready))
            {
                if (!populate_layer_from_rgba(cache, key, signature, job->params, job->rgba, job->width, job->height))
                    return false;

                std::lock_guard<std::mutex> lock(g_jobs_mutex);
                g_jobs.erase(key);
                return true;
            }

            return false;
        }

        void queue_async_stitch(const std::string& key, const uint64_t signature, const tile_map_params& params)
        {
            {
                std::lock_guard<std::mutex> lock(g_jobs_mutex);
                const auto it = g_jobs.find(key);
                if (it != g_jobs.end())
                {
                    const auto& existing = it->second;
                    if (existing->signature == signature)
                    {
                        const int state = existing->state.load();
                        if (state == static_cast<int>(stitch_job_state::queued)
                            || state == static_cast<int>(stitch_job_state::running)
                            || state == static_cast<int>(stitch_job_state::ready))
                            return;
                    }
                    g_jobs.erase(it);
                }
            }

            auto job = std::make_shared<stitch_job>();
            job->key = key;
            job->signature = signature;
            job->params = params;
            {
                std::lock_guard<std::mutex> lock(g_jobs_mutex);
                g_jobs[key] = job;
            }
            enqueue_stitch_job(job);
        }
    }

    void release_layer_cache(layer_cache& layers)
    {
        for (auto& entry : layers)
        {
            if (entry.second.tex_id != ImTextureID_Invalid)
                dxhook::release_texture_id(entry.second.tex_id);
            entry.second = {};
        }
        layers.clear();
    }

    void cleanup_placeholder_tiles()
    {
        static std::atomic<bool> cleaned{false};
        if (cleaned.exchange(true))
            return;

        const std::filesystem::path cache_dir =
            std::filesystem::path(minimap::get_maps_directory()) / "tile_cache";
        std::error_code ec;
        if (!std::filesystem::exists(cache_dir, ec))
            return;

        size_t removed = 0;
        const auto cleanup_file = [&](const std::filesystem::path& path)
        {
            if (path.extension() != ".png")
                return;

            const auto size = std::filesystem::file_size(path, ec);
            if (!ec && size > 0 && size <= 512)
            {
                std::filesystem::remove(path, ec);
                ++removed;
            }
        };

        const auto cleanup_dir = [&](const std::filesystem::path& dir, auto& self) -> void
        {
            for (const auto& entry : std::filesystem::directory_iterator(dir, ec))
            {
                if (entry.is_directory(ec))
                {
                    self(entry.path(), self);
                    continue;
                }
                if (entry.is_regular_file(ec))
                    cleanup_file(entry.path());
            }
        };

        for (const auto& entry : std::filesystem::directory_iterator(cache_dir, ec))
        {
            if (entry.is_directory(ec))
                cleanup_dir(entry.path(), cleanup_dir);
            else if (entry.is_regular_file(ec))
                cleanup_file(entry.path());
        }

        if (removed > 0)
            logger::info("Tile cache: removed " + std::to_string(removed) + " placeholder tiles");
    }

    static uint64_t tile_params_signature(const tile_map_params& params)
    {
        auto mix = [](const uint64_t h, const uint64_t v)
        {
            return h ^ (v + 0x9e3779b97f4a7c15ULL + (h << 6) + (h >> 2));
        };

        uint64_t hash = std::hash<std::string>{}(params.tile_path_template);
        hash = mix(hash, static_cast<uint64_t>(params.tile_size));
        hash = mix(hash, static_cast<uint64_t>(params.min_zoom));
        hash = mix(hash, static_cast<uint64_t>(params.max_zoom));
        hash = mix(hash, static_cast<uint64_t>(params.coordinate_rotation));
        for (int i = 0; i < 4; ++i)
            hash = mix(hash, static_cast<uint64_t>(static_cast<int32_t>(params.transform[i] * 1000000.f)));

        hash = mix(hash, static_cast<uint64_t>(static_cast<int32_t>(params.bounds.min_x * 100.f)));
        hash = mix(hash, static_cast<uint64_t>(static_cast<int32_t>(params.bounds.max_x * 100.f)));
        hash = mix(hash, static_cast<uint64_t>(static_cast<int32_t>(params.bounds.min_z * 100.f)));
        hash = mix(hash, static_cast<uint64_t>(static_cast<int32_t>(params.bounds.max_z * 100.f)));
        return hash;
    }

    bool ensure_stitched_layer(layer_cache& cache, const std::string& key, const tile_map_params& params)
    {
        if (params.tile_path_template.empty())
            return false;

        if (!dxhook::is_ready())
            return false;

        const uint64_t signature = tile_params_signature(params);
        const auto existing = cache.find(key);
        if (existing != cache.end())
        {
            if (existing->second.tex_id != ImTextureID_Invalid
                && existing->second.cache_signature == signature)
                return true;

            if (existing->second.tex_id != ImTextureID_Invalid)
                dxhook::release_texture_id(existing->second.tex_id);
            cache.erase(existing);
        }

        if (params.bounds.max_x <= params.bounds.min_x || params.bounds.max_z <= params.bounds.min_z)
            return false;

        if (try_finalize_ready_job(cache, key, signature))
            return true;

        std::vector<uint8_t> cached_rgba;
        uint32_t cached_w = 0;
        uint32_t cached_h = 0;
        if (load_stitched_cache(params, signature, cached_rgba, cached_w, cached_h))
        {
            if (populate_layer_from_rgba(cache, key, signature, params, cached_rgba, cached_w, cached_h))
                return true;
        }

        queue_async_stitch(key, signature, params);
        return false;
    }

    namespace
    {
        struct map_vertex
        {
            ImVec2 screen{};
            ImVec2 map_uv{};
        };

        ImVec2 lerp_vec2(const ImVec2& a, const ImVec2& b, const float t)
        {
            return {a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t};
        }

        float vertex_axis(const map_vertex& v, const int axis)
        {
            return axis == 0 ? v.map_uv.x : v.map_uv.y;
        }

        map_vertex lerp_vertex(const map_vertex& a, const map_vertex& b, const float t)
        {
            map_vertex out{};
            out.screen = lerp_vec2(a.screen, b.screen, t);
            out.map_uv = lerp_vec2(a.map_uv, b.map_uv, t);
            return out;
        }

        std::vector<map_vertex> clip_half_plane(const std::vector<map_vertex>& input,
                                                const int axis, const float limit, const bool keep_at_or_above)
        {
            std::vector<map_vertex> output;
            if (input.empty())
                return output;

            output.reserve(input.size() + 2);
            const size_t count = input.size();
            for (size_t i = 0; i < count; ++i)
            {
                const map_vertex& current = input[i];
                const map_vertex& next = input[(i + 1) % count];
                const float current_axis = vertex_axis(current, axis);
                const float next_axis = vertex_axis(next, axis);
                const bool current_inside = keep_at_or_above ? current_axis >= limit : current_axis <= limit;
                const bool next_inside = keep_at_or_above ? next_axis >= limit : next_axis <= limit;

                if (current_inside && next_inside)
                {
                    output.push_back(next);
                }
                else if (current_inside && !next_inside)
                {
                    const float denom = next_axis - current_axis;
                    if (fabsf(denom) > 1e-6f)
                        output.push_back(lerp_vertex(current, next, (limit - current_axis) / denom));
                }
                else if (!current_inside && next_inside)
                {
                    const float denom = next_axis - current_axis;
                    if (fabsf(denom) > 1e-6f)
                        output.push_back(lerp_vertex(current, next, (limit - current_axis) / denom));
                    output.push_back(next);
                }
            }
            return output;
        }

        std::vector<map_vertex> clip_quad_to_uv(const map_vertex in[4])
        {
            std::vector<map_vertex> poly(in, in + 4);
            poly = clip_half_plane(poly, 0, 0.f, true);
            if (poly.empty()) return {};
            poly = clip_half_plane(poly, 0, 1.f, false);
            if (poly.empty()) return {};
            poly = clip_half_plane(poly, 1, 0.f, true);
            if (poly.empty()) return {};
            poly = clip_half_plane(poly, 1, 1.f, false);
            return poly;
        }

        ImVec2 map_uv_to_tile_uv(const ImVec2 map_uv, const minimap::map_bounds& floor,
                                 const raster_layer& layer)
        {
            const float span_x = floor.max_x - floor.min_x;
            const float span_z = floor.max_z - floor.min_z;
            if (span_x <= 0.f || span_z <= 0.f)
                return map_uv;

            const float wx = floor.min_x + map_uv.x * span_x;
            const float wz = floor.min_z + (1.f - map_uv.y) * span_z;

            ImVec2 tile_uv{};
            if (world_to_tile_uv(wx, wz, layer, tile_uv))
                return tile_uv;

            if (!layer.has_tile_bounds)
                return map_uv;

            const float tile_span_x = layer.tile_bounds_max_x - layer.tile_bounds_min_x;
            const float tile_span_z = layer.tile_bounds_max_z - layer.tile_bounds_min_z;
            if (tile_span_x <= 0.f || tile_span_z <= 0.f)
                return map_uv;

            return {
                (wx - layer.tile_bounds_min_x) / tile_span_x,
                1.f - (wz - layer.tile_bounds_min_z) / tile_span_z
            };
        }

        void draw_textured_triangle(ImDrawList* draw_list, const ImTextureID tex_id,
                                    const map_vertex& v0, const map_vertex& v1, const map_vertex& v2,
                                    const int coordinate_rotation, const ImU32 tint)
        {
            const ImVec2 uv0 = minimap::map_tex_uv(coordinate_rotation, v0.map_uv.x, v0.map_uv.y);
            const ImVec2 uv1 = minimap::map_tex_uv(coordinate_rotation, v1.map_uv.x, v1.map_uv.y);
            const ImVec2 uv2 = minimap::map_tex_uv(coordinate_rotation, v2.map_uv.x, v2.map_uv.y);
            draw_list->AddImageQuad(tex_id,
                v0.screen, v1.screen, v2.screen, v2.screen,
                uv0, uv1, uv2, uv2, tint);
        }

        void draw_clipped_polygon(ImDrawList* draw_list, const ImTextureID tex_id,
                                  const std::vector<map_vertex>& poly, const int coordinate_rotation,
                                  const ImU32 tint)
        {
            if (poly.size() < 3)
                return;

            if (poly.size() == 3)
            {
                draw_textured_triangle(draw_list, tex_id, poly[0], poly[1], poly[2], coordinate_rotation, tint);
                return;
            }

            if (poly.size() == 4)
            {
                const ImVec2 uv0 = minimap::map_tex_uv(coordinate_rotation, poly[0].map_uv.x, poly[0].map_uv.y);
                const ImVec2 uv1 = minimap::map_tex_uv(coordinate_rotation, poly[1].map_uv.x, poly[1].map_uv.y);
                const ImVec2 uv2 = minimap::map_tex_uv(coordinate_rotation, poly[2].map_uv.x, poly[2].map_uv.y);
                const ImVec2 uv3 = minimap::map_tex_uv(coordinate_rotation, poly[3].map_uv.x, poly[3].map_uv.y);
                draw_list->AddImageQuad(tex_id,
                    poly[0].screen, poly[1].screen, poly[2].screen, poly[3].screen,
                    uv0, uv1, uv2, uv3, tint);
                return;
            }

            for (size_t i = 1; i + 1 < poly.size(); ++i)
                draw_textured_triangle(draw_list, tex_id, poly[0], poly[i], poly[i + 1], coordinate_rotation, tint);
        }
    }

    void draw_layer(ImDrawList* draw_list, const raster_layer& layer, const minimap::map_view_transform& view,
                    const int coordinate_rotation, const float opacity_mul,
                    const minimap::map_bounds& floor_bounds)
    {
        if (!draw_list || layer.tex_id == ImTextureID_Invalid || layer.width <= 0.f || layer.height <= 0.f)
            return;

        const int grid_cells = view.rotate ? 16 : 10;
        const int alpha = static_cast<int>((std::min)(255.f, (std::max)(0.f, 255.f * opacity_mul)));
        const ImU32 tint = IM_COL32(255, 255, 255, alpha);

        for (int gy = 0; gy < grid_cells; ++gy)
        {
            const float sy0 = static_cast<float>(gy) / static_cast<float>(grid_cells);
            const float sy1 = static_cast<float>(gy + 1) / static_cast<float>(grid_cells);

            for (int gx = 0; gx < grid_cells; ++gx)
            {
                const float sx0 = static_cast<float>(gx) / static_cast<float>(grid_cells);
                const float sx1 = static_cast<float>(gx + 1) / static_cast<float>(grid_cells);

                map_vertex corners[4] = {
                    {{view.rx + sx0 * view.size, view.ry + sy0 * view.size}, {}},
                    {{view.rx + sx1 * view.size, view.ry + sy0 * view.size}, {}},
                    {{view.rx + sx1 * view.size, view.ry + sy1 * view.size}, {}},
                    {{view.rx + sx0 * view.size, view.ry + sy1 * view.size}, {}},
                };

                for (int i = 0; i < 4; ++i)
                {
                    const ImVec2 map_uv = minimap::screen_to_map_uv(view, corners[i].screen);
                    corners[i].map_uv = map_uv_to_tile_uv(map_uv, floor_bounds, layer);
                }

                const std::vector<map_vertex> clipped = clip_quad_to_uv(corners);
                const int tex_rotation = layer.has_tile_projection ? 0 : coordinate_rotation;
                draw_clipped_polygon(draw_list, layer.tex_id, clipped, tex_rotation, tint);
            }
        }
    }
}