#include <algorithm>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <iostream>
#include <optional>
#include <vector>

#include <raylib.h>
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunused-result"
#define RAYGUI_IMPLEMENTATION
#include <raygui.h>
#pragma clang diagnostic pop

#include "bounding_box.h"
#include "frame.h"
#include "pack.h"
#include "png.h"

namespace fs = std::filesystem;

constexpr int resize_handle = 20;

Rectangle rect_from_bbox(const freling::BoundingBox& box,
                         const Vector2& offset) {
    const Rectangle rect = {offset.x + box.x, offset.y + box.y,
                            static_cast<float>(box.width),
                            static_cast<float>(box.height)};
    return rect;
}

void draw(const freling::BoundingBox& box,
          const Vector2& offset,
          const Color& color) {
    const Rectangle rect = rect_from_bbox(box, offset);
    DrawRectangleRec(rect, ColorAlpha(color, 0.3));
    DrawRectangleLinesEx(rect, 3, color);
    DrawTriangle(
        (Vector2){rect.x + rect.width - resize_handle, rect.y + rect.height},
        (Vector2){rect.x + rect.width, rect.y + rect.height},
        (Vector2){rect.x + rect.width, rect.y + rect.height - resize_handle},
        color);
}

Image image_from_frame(const Frame& frame) {
    Image image = {0};

    const int format = PIXELFORMAT_UNCOMPRESSED_R8G8B8;
    const unsigned int size =
        GetPixelDataSize(frame.width, frame.height, format);

    image.data = RL_MALLOC(size);
    memcpy(image.data, frame.data, size);
    image.width = frame.width;
    image.height = frame.height;
    image.mipmaps = 1;
    image.format = format;

    return image;
}

int main(int argc, const char* argv[]) {
    if (argc < 2 or (argc - 2) % 4 != 0) {
        std::cerr
            << "Usage: " << argv[0] << " path/to/image"
            << " [x y width height ...]\n"
            << "x/y/w/h points must be grouped by 4 to define bounding boxes\n";
        return 1;
    }

    const fs::path input(argv[1]);
    if (!fs::exists(input)) {
        std::cerr << "Input file " << input << " does not exist.\n";
        return 1;
    }

    const std::optional<Frame> in_frame = load_png(input);
    if (!in_frame) {
        std::cerr << "Cannot load file " << input << "\n";
        return 1;
    }

    std::vector<freling::BoundingBox> bboxes;
    bboxes.reserve((argc - 2) / 4);
    int i = 2;
    while (i < argc) {
        const int32_t x = atoi(argv[i]);
        const int32_t y = atoi(argv[i + 1]);
        const uint32_t w = atoi(argv[i + 2]);
        const uint32_t h = atoi(argv[i + 3]);
        bboxes.push_back(freling::BoundingBox({x, y, w, h}));
        i += 4;
    }
    const std::vector<Color> bbox_colors = {RED,    GREEN,  BLUE,
                                            YELLOW, ORANGE, PURPLE};
    std::vector<freling::BoundingBox> packed_bboxes;

    Vector2 win_size = {800, 450};
    const int padding = 5;
    const Vector2 in_offset = {padding, 40};

    InitWindow(win_size.x, win_size.y, "netatmo - rect packing");
    SetTargetFPS(60);

    Image in_img = image_from_frame(*in_frame);
    Texture2D in_texture = LoadTextureFromImage(in_img);

    Image pack_img;
    Texture2D pack_texture;
    bool packed = false;

    win_size.x = std::max(win_size.x, in_offset.x + in_texture.width + padding);
    win_size.y =
        std::max(win_size.y, in_offset.y + in_texture.height + padding);

    SetWindowSize(win_size.x, win_size.y);

    const Vector2 b_size = {150, 30};
    Vector2 mouse_pos = {0};
    Vector2 mouse_delta = {0};
    constexpr int idx_unset = -1;
    int b_moving_idx = idx_unset;
    int b_resize_idx = idx_unset;
    int b_delete_idx = idx_unset;

    while (!WindowShouldClose()) {
        //
        // Update
        //
        mouse_pos = GetMousePosition();
        mouse_delta = GetMouseDelta();
        for (int b = 0, size = bboxes.size(); b < size; ++b) {
            const auto& box = bboxes[b];
            const Rectangle rect = rect_from_bbox(box, in_offset);
            if (not CheckCollisionPointRec(mouse_pos, rect)) {
                continue;
            }
            if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) {
                const Rectangle handle_area = {
                    rect.x + rect.width - resize_handle,
                    rect.y + rect.height - resize_handle, resize_handle,
                    resize_handle};
                if (CheckCollisionPointRec(mouse_pos, handle_area)) {
                    b_resize_idx = b;
                } else {
                    b_moving_idx = b;
                }
            }
            if (IsMouseButtonPressed(MOUSE_BUTTON_RIGHT)) {
                b_delete_idx = b;
            }
        }
        if (IsMouseButtonReleased(MOUSE_BUTTON_LEFT)) {
            b_moving_idx = idx_unset;
            b_resize_idx = idx_unset;
        }
        if (b_moving_idx != idx_unset) {
            auto& box = bboxes[b_moving_idx];
            box.x += mouse_delta.x;
            box.y += mouse_delta.y;
        }
        if (b_resize_idx != idx_unset) {
            auto& box = bboxes[b_resize_idx];
            box.width = std::max(static_cast<int>(box.width + mouse_delta.x),
                                 resize_handle);
            box.height = std::max(static_cast<int>(box.height + mouse_delta.y),
                                  resize_handle);
        }
        if (b_delete_idx != idx_unset) {
            bboxes.erase(bboxes.begin() + b_delete_idx);
            b_delete_idx = idx_unset;
        }

        //
        // Draw
        //
        BeginDrawing();
        ClearBackground(RAYWHITE);

        //
        // Widgets: buttons, ...
        //
        if (GuiButton((Rectangle){padding, padding, b_size.x, b_size.y},
                      "Add Box")) {
            bboxes.push_back(freling::BoundingBox({0, 0, 64, 64}));
        }

        if (GuiButton((Rectangle){padding * 2 + b_size.x, padding, b_size.x,
                                  b_size.y},
                      "Pack Rects")) {
            std::optional<Frame> f_packed =
                pack(*in_frame, bboxes, packed_bboxes);

            if (packed) {
                UnloadTexture(pack_texture);
                UnloadImage(pack_img);
            }

            pack_img = image_from_frame(*f_packed);
            pack_texture = LoadTextureFromImage(pack_img);
            packed = true;

            win_size.x =
                std::max(win_size.x, in_offset.x + in_texture.width +
                                         padding * 2 + pack_texture.width);
            win_size.y = std::max(
                win_size.y,
                in_offset.y + std::max(in_texture.height, pack_texture.height) +
                    padding);

            SetWindowSize(win_size.x, win_size.y);
        }

        //
        // Input image
        //
        DrawTexture(in_texture, in_offset.x, in_offset.y, WHITE);
        DrawRectangleLines(in_offset.x, in_offset.y, in_texture.width,
                           in_texture.height, BLACK);

        int c = 0;
        for (const auto& b : bboxes) {
            const auto& color = bbox_colors[c % bbox_colors.size()];
            draw(b, in_offset, color);
            ++c;
        };

        //
        // Packed image
        //
        if (packed) {
            Vector2 pack_offset = {in_offset.x + in_texture.width + padding,
                                   in_offset.y};
            DrawTexture(pack_texture, pack_offset.x, pack_offset.y, WHITE);
            DrawRectangleLines(pack_offset.x, pack_offset.y, pack_texture.width,
                               pack_texture.height, BLACK);

            int c = 0;
            for (const auto& b : packed_bboxes) {
                const auto& color = bbox_colors[c % bbox_colors.size()];
                draw(b, pack_offset, color);
                ++c;
            };
        }

        EndDrawing();
    }

    // Cleanup
    UnloadTexture(in_texture);
    UnloadImage(in_img);
    if (packed) {
        UnloadTexture(pack_texture);
        UnloadImage(pack_img);
    }
    CloseWindow();

    return 0;
}
