#include "morph.h"

#define export extern "C" [[maybe_unused]]

static bool set_morph_frame(am::morph* morph, size_t frame, uint16_t width, const std::vector<uint8_t>& image) {
    auto sz = image.size();
    unsigned char r = 0, g = 0, b = 0, a = 0;
    size_t pixel = 0;

    if (width > UINT16_MAX)
        return false;

    for (size_t j = 0; j < sz; ++j) {
        switch (j % 4) {
            case 0: r = image.at(j); break;
            case 1: g = image.at(j); break;
            case 2: b = image.at(j); break;
            default: {
                a = image.at(j);

                if (a == 0) {
                    pixel++;
                    continue;
                }
                morph->add_pixel(frame, am::create_pixel(pixel % width, pixel / width, r, g, b, a));
                pixel++;
                break;
            }
        }
    }
    return true;
}

export am::morph* create_morph(uint16_t width, uint16_t height, uint8_t img_a[],
                               uint8_t img_b[]) {
    auto* morph = new am::morph();

    morph->set_blob_delimiter(am::HSP);
    morph->set_blob_max_size(SIZE_MAX);
    morph->set_blob_min_size(1);
    morph->set_blob_box_grip(UINT16_MAX);
    morph->set_blob_box_samples(100);
    morph->set_blob_threshold(1.0);
    morph->set_blob_number(1);
    morph->set_seed(0);
    morph->set_blob_rgba_weight(1);
    morph->set_blob_size_weight(0);
    morph->set_blob_xy_weight(0);
    morph->set_degeneration(10000);
    morph->set_density(2); // Higher than 1 sets fluid to 0.
    morph->set_motion(am::SPLINE);
    morph->set_fading(am::PERLIN);
    morph->set_threads(16);
    morph->set_cycle_length(100000);
    morph->set_feather(0);
    morph->set_keep_background(0);
    morph->set_finite(1);
    morph->set_show_blobs(am::TEXTURE);
    morph->set_fluid(0); // Higher than 0 sets density to 1.

    morph->set_resolution(width, height);

    auto i1 = std::vector<uint8_t>(img_a, img_a + width * height * 4);
    auto i2 = std::vector<uint8_t>(img_b, img_b + width * height * 4);
    auto success = set_morph_frame(morph, 0, width, i1) && set_morph_frame(morph, 1, width, i2);
    if (!success) {
        return nullptr;
    }

    morph->compute();
    while (true) {
        morph->suspend();
        morph->synchronize();
        morph->compute();
        auto state = morph->get_state();
        if (state > am::STATE_BLOB_UNIFICATION) {
            break;
        }
    }
    return morph;
}

export void run_blob_matching(am::morph* morph, size_t ms) {
    std::chrono::steady_clock::time_point start, now;
    start = std::chrono::steady_clock::now();
    while (true) {
        morph->suspend();
        morph->synchronize();
        morph->compute();
        auto state = morph->get_state();
        if (state < am::STATE_BLOB_MATCHING) {
            continue;
        }
        now = std::chrono::steady_clock::now();
        if ((size_t)std::chrono::duration_cast<std::chrono::milliseconds>(now - start).count() >= ms) {
            morph->next_state();
            break;
        }
    }
    auto state = morph->get_state();
    while (state < am::STATE_ATOM_MORPHING) {
        morph->suspend();
        morph->synchronize();
        morph->compute();
        state = morph->get_state();
    }
}

export void finish_atom_matching(am::morph* morph) {
    morph->suspend();
    auto state = morph->get_state();
    while (state < am::STATE_ATOM_MORPHING) {
        morph->suspend();
        morph->synchronize();
        morph->compute();
        state = morph->get_state();
    }

    morph->next_state();
}

export void get_frame(am::morph* morph, size_t frame, size_t total_frames, uint32_t width, uint32_t height,
                      uint8_t out[]) {
    double t = morph->get_time(frame, total_frames);
    std::vector<am::pixel> pixels;
    morph->get_pixels(t, &pixels);
    auto size = width * height * 4;
    while (!pixels.empty()) {
        am::pixel px = pixels.back();
        pixels.pop_back();

        size_t pos = ((size_t)px.y * width + (size_t)px.x) * 4;
        if (pos >= size)
            continue;

        out[pos + 0] = px.c.r;
        out[pos + 1] = px.c.g;
        out[pos + 2] = px.c.b;
        out[pos + 3] = px.c.a;
    }
}

export void delete_morph(am::morph* morph) { delete morph; }