#include <emscripten/bind.h>
#include <libheif/heif.h>
#include <cstdint>
#include <limits>
#include <string>

using namespace emscripten;

constexpr uint64_t MAX_SOURCE_BYTES = UINT64_C(20) * 1024 * 1024;
// A real 4032x3024 decode measured about 146 MiB RSS before bounded output,
// while a real 8064x6048 decode grew by more than 575 MiB. The fallback is
// therefore limited to 12 Mi-pixels / a 48 MiB RGBA plane. Current Safari may
// still accept 48 MP through its separate native decoder path.
constexpr uint64_t MAX_IMAGE_PIXELS = UINT64_C(12) * 1024 * 1024;
constexpr uint64_t MAX_MEMORY_BLOCK_BYTES = UINT64_C(48) * 1024 * 1024;
constexpr uint64_t MAX_TOTAL_MEMORY_BYTES = UINT64_C(128) * 1024 * 1024;

std::string validate_dimensions(int width, int height, size_t* row_bytes,
                                size_t* output_bytes) {
    if (width <= 0 || height <= 0) {
        return "HEIC dimensions are invalid";
    }

    const uint64_t width_u64 = static_cast<uint64_t>(width);
    const uint64_t height_u64 = static_cast<uint64_t>(height);
    if (height_u64 > MAX_IMAGE_PIXELS / width_u64) {
        return "HEIC dimensions exceed the pixel limit";
    }
    const uint64_t pixels = width_u64 * height_u64;
    if (pixels > std::numeric_limits<uint64_t>::max() / 4) {
        return "HEIC byte size overflows";
    }
    const uint64_t output_bytes_u64 = pixels * 4;
    if (output_bytes_u64 > MAX_MEMORY_BLOCK_BYTES ||
        output_bytes_u64 > std::numeric_limits<size_t>::max()) {
        return "HEIC output exceeds the memory limit";
    }

    *row_bytes = static_cast<size_t>(width_u64 * 4);
    *output_bytes = static_cast<size_t>(output_bytes_u64);
    return "";
}

struct DecodeProgressData {
    val callback;
    int max_progress = 0;
};

class HeicDecoderWasm {
public:
    HeicDecoderWasm() {}

    ~HeicDecoderWasm() {}

    // Test/provenance surface for validating real policy fixtures without
    // allocating their full RGBA planes. Production decode repeats the same
    // handle-level validation immediately before heif_decode_image.
    val dimensions(std::string data) {
        if (data.empty() || data.size() > MAX_SOURCE_BYTES) {
            return val("HEIC source size is invalid");
        }

        heif_context* ctx = heif_context_alloc();
        if (!ctx) {
            return val("Failed to allocate heif context");
        }
        heif_security_limits* limits = heif_context_get_security_limits(ctx);
        if (!limits) {
            heif_context_free(ctx);
            return val("Failed to configure HEIC security limits");
        }
        limits->max_image_size_pixels = MAX_IMAGE_PIXELS;
        limits->max_memory_block_size = MAX_MEMORY_BLOCK_BYTES;
        limits->max_total_memory = MAX_TOTAL_MEMORY_BYTES;

        heif_error err = heif_context_read_from_memory_without_copy(
            ctx, data.data(), data.size(), nullptr);
        if (err.code != heif_error_Ok) {
            heif_context_free(ctx);
            return val("HEIC container is invalid");
        }

        heif_image_handle* handle;
        err = heif_context_get_primary_image_handle(ctx, &handle);
        if (err.code != heif_error_Ok) {
            heif_context_free(ctx);
            return val("HEIC primary image is invalid");
        }

        const int width = heif_image_handle_get_width(handle);
        const int height = heif_image_handle_get_height(handle);
        size_t row_bytes = 0;
        size_t output_bytes = 0;
        const std::string dimension_error =
            validate_dimensions(width, height, &row_bytes, &output_bytes);
        heif_image_handle_release(handle);
        heif_context_free(ctx);
        if (!dimension_error.empty()) {
            return val(dimension_error);
        }

        val result = val::object();
        result.set("width", width);
        result.set("height", height);
        return result;
    }

    val decode(std::string data, val progress_callback) {
        if (data.empty() || data.size() > MAX_SOURCE_BYTES) {
            return val("HEIC source size is invalid");
        }

        heif_context* ctx = heif_context_alloc();
        if (!ctx) {
            return val("Failed to allocate heif context");
        }

        heif_security_limits* limits = heif_context_get_security_limits(ctx);
        if (!limits) {
            heif_context_free(ctx);
            return val("Failed to configure HEIC security limits");
        }
        limits->max_image_size_pixels = MAX_IMAGE_PIXELS;
        limits->max_memory_block_size = MAX_MEMORY_BLOCK_BYTES;
        limits->max_total_memory = MAX_TOTAL_MEMORY_BYTES;

        heif_error err = heif_context_read_from_memory_without_copy(
            ctx, data.data(), data.size(), nullptr);

        if (err.code != heif_error_Ok) {
            heif_context_free(ctx);
            std::string msg = "Error code " + std::to_string(err.code) +
                              " (subcode " + std::to_string(err.subcode) + "): ";
            if (err.message) {
                msg += err.message;
            } else {
                msg += "No message";
            }
            return val(msg);
        }

        heif_image_handle* handle;
        err = heif_context_get_primary_image_handle(ctx, &handle);
        if (err.code != heif_error_Ok) {
            heif_context_free(ctx);
            std::string msg = "Error code " + std::to_string(err.code) +
                              " (subcode " + std::to_string(err.subcode) + "): ";
            if (err.message) {
                msg += err.message;
            } else {
                msg += "No message";
            }
            return val(msg);
        }

        // Container dimensions are available from the primary handle without
        // allocating a decoded plane. Reject oversized or invalid images before
        // heif_decode_image can create a full-resolution codec/RGBA allocation.
        size_t preflight_row_bytes = 0;
        size_t preflight_output_bytes = 0;
        const std::string preflight_error = validate_dimensions(
            heif_image_handle_get_width(handle),
            heif_image_handle_get_height(handle),
            &preflight_row_bytes, &preflight_output_bytes);
        if (!preflight_error.empty()) {
            heif_image_handle_release(handle);
            heif_context_free(ctx);
            return val(preflight_error);
        }

        DecodeProgressData progress_data{progress_callback, 0};
        heif_decoding_options* options = heif_decoding_options_alloc();
        if (!options) {
            heif_image_handle_release(handle);
            heif_context_free(ctx);
            return val("Failed to allocate HEIC decoding options");
        }

        if (!progress_callback.isUndefined() && !progress_callback.isNull()) {
            options->start_progress = [](enum heif_progress_step step, int max_progress, void* progress_user_data) {
                if (progress_user_data) {
                    auto* d = static_cast<DecodeProgressData*>(progress_user_data);
                    d->max_progress = max_progress;
                }
            };
            options->on_progress = [](enum heif_progress_step step, int progress, void* progress_user_data) {
                if (progress_user_data) {
                    auto* d = static_cast<DecodeProgressData*>(progress_user_data);
                    if (d->max_progress > 0) {
                        double percent = (double)progress / d->max_progress * 100.0;
                        d->callback(percent);
                    }
                }
            };
            options->progress_user_data = &progress_data;
        }

        if (!progress_callback.isUndefined() && !progress_callback.isNull()) {
            progress_callback(0.0);
        }

        heif_image* img;
        err = heif_decode_image(handle, &img, heif_colorspace_RGB, heif_chroma_interleaved_RGBA, options);
        heif_image_handle_release(handle);
        heif_decoding_options_free(options);

        if (err.code == heif_error_Ok && !progress_callback.isUndefined() && !progress_callback.isNull()) {
            progress_callback(100.0);
        }

        if (err.code != heif_error_Ok) {
            heif_context_free(ctx);
            std::string msg = "Error code " + std::to_string(err.code) +
                              " (subcode " + std::to_string(err.subcode) + "): ";
            if (err.message) {
                msg += err.message;
            } else {
                msg += "No message";
            }
            return val(msg);
        }

        int width = heif_image_get_width(img, heif_channel_interleaved);
        int height = heif_image_get_height(img, heif_channel_interleaved);

        size_t row_bytes = 0;
        size_t output_bytes = 0;
        const std::string decoded_dimension_error =
            validate_dimensions(width, height, &row_bytes, &output_bytes);
        if (!decoded_dimension_error.empty()) {
            heif_image_release(img);
            heif_context_free(ctx);
            return val(decoded_dimension_error);
        }

        const uint64_t height_u64 = static_cast<uint64_t>(height);

        int stride = 0;
        const uint8_t* p = heif_image_get_plane_readonly(img, heif_channel_interleaved, &stride);
        if (!p || stride <= 0 || static_cast<uint64_t>(stride) < row_bytes ||
            (height_u64 - 1) > (std::numeric_limits<size_t>::max() - row_bytes) /
                                   static_cast<size_t>(stride)) {
            heif_image_release(img);
            heif_context_free(ctx);
            return val("Decoded HEIC plane layout is invalid");
        }

        // Copy data to a JS Uint8Array
        val resultData = val::global("Uint8Array").new_(output_bytes);
        for (int y = 0; y < height; ++y) {
            const size_t source_offset = static_cast<size_t>(y) * static_cast<size_t>(stride);
            const size_t target_offset = static_cast<size_t>(y) * row_bytes;
            val memoryView = val(typed_memory_view(row_bytes, p + source_offset));
            resultData.call<void>("set", memoryView, val(target_offset));
        }

        heif_image_release(img);
        heif_context_free(ctx);

        val result = val::object();
        result.set("width", width);
        result.set("height", height);
        result.set("data", resultData);
        return result;
    }
};

EMSCRIPTEN_BINDINGS(my_module) {
    class_<HeicDecoderWasm>("HeicDecoder")
        .constructor<>()
        .function("dimensions", &HeicDecoderWasm::dimensions)
        .function("decode", &HeicDecoderWasm::decode);
}
