/*
 * main.cpp
 * MontaukOS 2048 - standalone MTK game
 * Copyright (c) 2026 Daniel Hammer
 */

#include <cstdint>
#include <montauk/syscall.h>
#include <gui/gui.hpp>
#include <gui/canvas.hpp>
#include <gui/dialogs.hpp>
#include <gui/mtk.hpp>
#include <gui/mtk/settings.hpp>
#include <gui/standalone.hpp>
#include <gui/truetype.hpp>

extern "C" {
#include <stdio.h>
}

using namespace gui;

static constexpr int INIT_W = 420;
static constexpr int INIT_H = 540;
static constexpr int BOARD_N = 4;
static constexpr int HEADER_PAD = 16;
static constexpr int SCORE_CARD_H = 64;
static constexpr int SCORE_CARD_W = 112;
static constexpr int SCORE_LABEL_SIZE = 16;
static constexpr int SCORE_VALUE_SIZE = 32;
static constexpr int BUTTON_W = 94;
static constexpr int BUTTON_H = 30;
static constexpr int FOOTER_H = 34;

enum MoveDir : uint8_t {
    DIR_LEFT = 0,
    DIR_RIGHT,
    DIR_UP,
    DIR_DOWN,
};

struct GameState {
    uint32_t cells[BOARD_N][BOARD_N];
    unsigned long score;
    unsigned long best;
    bool game_over;
    bool game_over_dialog_shown;
    bool won;
};

struct AppLayout {
    Rect title;
    Rect score_card;
    Rect best_card;
    Rect new_button;
    Rect board;
    int tile;
    int gap;
};

static WsWindow g_win;
static GameState g_game = {};
static uint32_t g_rng = 0x20482048u;
static int g_mouse_x = -1;
static int g_mouse_y = -1;
static char g_status[96] = {};
static uint64_t g_status_time = 0;
static Color g_accent = colors::ACCENT;

static Color shade(Color c, int delta) {
    return Color::from_rgb(
        (uint8_t)gui_clamp((int)c.r + delta, 0, 255),
        (uint8_t)gui_clamp((int)c.g + delta, 0, 255),
        (uint8_t)gui_clamp((int)c.b + delta, 0, 255));
}

static void set_status(const char* text) {
    snprintf(g_status, sizeof(g_status), "%s", text ? text : "");
    g_status_time = montauk::get_milliseconds();
}

static bool status_visible() {
    return g_status[0] && (montauk::get_milliseconds() - g_status_time < 4000);
}

static void seed_rng() {
    uint32_t seed = 0;
    if (montauk::getrandom(&seed, sizeof(seed)) != (int64_t)sizeof(seed) || seed == 0) {
        seed = (uint32_t)montauk::get_milliseconds();
        seed ^= (uint32_t)montauk::get_ticks() << 11;
        seed ^= (uint32_t)montauk::getpid() * 2654435761u;
    }
    if (seed == 0) seed = 0x20482048u;
    g_rng = seed;
}

static uint32_t next_random() {
    uint32_t x = g_rng;
    x ^= x << 13;
    x ^= x >> 17;
    x ^= x << 5;
    if (x == 0) x = 0x6D2B79F5u;
    g_rng = x;
    return x;
}

static int count_digits(uint32_t value) {
    int digits = 1;
    while (value >= 10) {
        value /= 10;
        digits++;
    }
    return digits;
}

static mtk::Theme app_theme() {
    mtk::Theme theme = mtk::make_theme(g_accent);
    theme.radius_sm = 4;
    theme.radius_md = 6;
    theme.surface = Color::from_rgb(0xF0, 0xF2, 0xF4);
    theme.surface_alt = Color::from_rgb(0xF7, 0xF8, 0xFA);
    theme.surface_hover = mtk::mix(theme.surface, theme.accent, 26);
    return theme;
}

static AppLayout compute_layout(int w, int h) {
    AppLayout lo = {};

    int pad = gui_clamp(w / 26, 12, HEADER_PAD);
    int card_w = gui_clamp((w - pad * 2 - 8) / 2, 72, SCORE_CARD_W);
    bool compact = w < 390;

    lo.title = {pad, pad + 2, 116, 40};

    if (compact) {
        lo.new_button = {gui_max(pad, w - pad - BUTTON_W), pad + 8, BUTTON_W, BUTTON_H};
        lo.score_card = {pad, pad + 56, card_w, SCORE_CARD_H};
        lo.best_card = {pad + card_w + 8, pad + 56, card_w, SCORE_CARD_H};
    } else {
        lo.best_card = {w - pad - SCORE_CARD_W, pad, SCORE_CARD_W, SCORE_CARD_H};
        lo.score_card = {lo.best_card.x - SCORE_CARD_W - 8, pad, SCORE_CARD_W, SCORE_CARD_H};
        lo.new_button = {w - pad - BUTTON_W, pad + SCORE_CARD_H + 8, BUTTON_W, BUTTON_H};
    }

    int header_h = compact
        ? (pad + 56 + SCORE_CARD_H + 14)
        : (pad + SCORE_CARD_H + BUTTON_H + 20);
    int max_board_w = gui_max(w - pad * 2, 80);
    int max_board_h = gui_max(h - header_h - FOOTER_H - pad, 80);
    int board_size = gui_min(max_board_w, max_board_h);
    int gap = gui_clamp(board_size / 42, 6, 10);
    int tile = (board_size - gap * (BOARD_N + 1)) / BOARD_N;
    if (tile < 12) tile = 12;
    board_size = tile * BOARD_N + gap * (BOARD_N + 1);

    lo.gap = gap;
    lo.tile = tile;
    lo.board = {(w - board_size) / 2, header_h, board_size, board_size};
    if (lo.board.x < pad) lo.board.x = pad;

    return lo;
}

static int text_w(const char* text, int size, bool bold = false) {
    TrueTypeFont* font = (bold && fonts::system_bold) ? fonts::system_bold : fonts::system_font;
    return gui::text_width(font, text, size);
}

static void draw_text_center(Canvas& c, const Rect& rect, const char* text,
                             Color color, int size, bool bold = false) {
    TrueTypeFont* font = (bold && fonts::system_bold) ? fonts::system_bold : fonts::system_font;
    int tw = gui::text_width(font, text, size);
    int th = gui::text_height(font, size);
    int x = rect.x + (rect.w - tw) / 2;
    int y = rect.y + (rect.h - th) / 2;
    gui::draw_text(c, font, x, y, text, color, size);
}

static void draw_text_center_fit(Canvas& c, const Rect& rect, const char* text,
                                 Color color, int max_size, int min_size, bool bold = false) {
    int size = max_size;
    while (size > min_size && text_w(text, size, bold) > rect.w)
        size -= 2;
    draw_text_center(c, rect, text, color, size, bold);
}

static void draw_text_left(Canvas& c, int x, int y, const char* text,
                           Color color, int size, bool bold = false) {
    TrueTypeFont* font = (bold && fonts::system_bold) ? fonts::system_bold : fonts::system_font;
    gui::draw_text(c, font, x, y, text, color, size);
}

static Color tile_color(uint32_t value, const mtk::Theme& theme) {
    switch (value) {
    case 0:    return mtk::mix(theme.surface, theme.accent, 10);
    case 2:    return Color::from_rgb(0xE7, 0xF0, 0xF7);
    case 4:    return Color::from_rgb(0xD8, 0xF0, 0xE6);
    case 8:    return Color::from_rgb(0xF9, 0xDE, 0x8F);
    case 16:   return Color::from_rgb(0xF5, 0xB3, 0x75);
    case 32:   return Color::from_rgb(0xF0, 0x85, 0x72);
    case 64:   return Color::from_rgb(0xE1, 0x5D, 0x5D);
    case 128:  return Color::from_rgb(0xB5, 0x7E, 0xD8);
    case 256:  return Color::from_rgb(0x6D, 0x9D, 0xE8);
    case 512:  return Color::from_rgb(0x44, 0xB7, 0xAF);
    case 1024: return Color::from_rgb(0x69, 0x83, 0xD8);
    case 2048: return Color::from_rgb(0x56, 0xB8, 0x76);
    default:   return shade(theme.accent, 18);
    }
}

static Color tile_text_color(uint32_t value) {
    if (value == 0) return colors::TEXT_COLOR;
    if (value <= 16) return Color::from_rgb(0x32, 0x3A, 0x42);
    return colors::WHITE;
}

static Rect tile_rect(const AppLayout& lo, int row, int col) {
    int x = lo.board.x + lo.gap + col * (lo.tile + lo.gap);
    int y = lo.board.y + lo.gap + row * (lo.tile + lo.gap);
    return {x, y, lo.tile, lo.tile};
}

static void draw_score_card(Canvas& c, const Rect& rect, const char* label,
                            unsigned long value, const mtk::Theme& theme) {
    char value_text[24];
    snprintf(value_text, sizeof(value_text), "%lu", value);
    draw_text_center(c, {rect.x, rect.y + 3, rect.w, 22}, label,
                     theme.text, SCORE_LABEL_SIZE, false);
    draw_text_center_fit(c, {rect.x, rect.y + 25, rect.w, 34}, value_text,
                         theme.text, SCORE_VALUE_SIZE, 20, true);
}

static void draw_board(Canvas& c, const AppLayout& lo, const mtk::Theme& theme) {
    Color board_bg = mtk::mix(theme.surface, theme.accent, 30);
    mtk::draw_rounded_frame(c, lo.board, 8, board_bg, board_bg);

    for (int row = 0; row < BOARD_N; row++) {
        for (int col = 0; col < BOARD_N; col++) {
            uint32_t value = g_game.cells[row][col];
            Rect tile = tile_rect(lo, row, col);
            Color fill = tile_color(value, theme);
            c.fill_rounded_rect(tile.x, tile.y, tile.w, tile.h, 6, fill);

            if (value != 0) {
                char text[12];
                snprintf(text, sizeof(text), "%u", (unsigned)value);

                int digits = count_digits(value);
                int size = lo.tile * 47 / 100;
                if (digits == 4) size = lo.tile * 39 / 100;
                if (digits >= 5) size = lo.tile * 33 / 100;
                size = gui_clamp(size, 16, 42);
                while (size > 12 && text_w(text, size, true) > tile.w - 10)
                    size -= 2;

                draw_text_center(c, tile, text, tile_text_color(value), size, true);
            }
        }
    }
}

static void render() {
    mtk::StandaloneHost host(&g_win);
    Canvas c = host.canvas();
    mtk::Theme theme = app_theme();
    AppLayout lo = compute_layout(g_win.width, g_win.height);

    c.fill(theme.window_bg);

    draw_text_left(c, lo.title.x, lo.title.y, "2048", theme.text, 34, true);
    draw_score_card(c, lo.score_card, "Score", g_game.score, theme);
    draw_score_card(c, lo.best_card, "Best", g_game.best, theme);

    mtk::draw_button(c, lo.new_button, "New Game", mtk::BUTTON_PRIMARY,
                     mtk::widget_state(false, lo.new_button.contains(g_mouse_x, g_mouse_y)),
                     theme);

    draw_board(c, lo, theme);

    const char* status = "";
    if (g_game.game_over) {
        status = "Game over";
    } else if (status_visible()) {
        status = g_status;
    }

    if (status[0]) {
        int sw = text_w(status, 14);
        draw_text_left(c, (g_win.width - sw) / 2,
                       gui_min(lo.board.y + lo.board.h + 12, g_win.height - FOOTER_H + 8),
                       status, theme.text_subtle, 14, false);
    }

    host.present();
}

static bool has_moves() {
    for (int row = 0; row < BOARD_N; row++) {
        for (int col = 0; col < BOARD_N; col++) {
            uint32_t v = g_game.cells[row][col];
            if (v == 0) return true;
            if (col + 1 < BOARD_N && g_game.cells[row][col + 1] == v) return true;
            if (row + 1 < BOARD_N && g_game.cells[row + 1][col] == v) return true;
        }
    }
    return false;
}

static bool spawn_tile() {
    int empty[BOARD_N * BOARD_N];
    int count = 0;

    for (int row = 0; row < BOARD_N; row++) {
        for (int col = 0; col < BOARD_N; col++) {
            if (g_game.cells[row][col] == 0)
                empty[count++] = row * BOARD_N + col;
        }
    }

    if (count <= 0) return false;

    int pick = empty[next_random() % (uint32_t)count];
    uint32_t value = (next_random() % 10u == 0u) ? 4 : 2;
    g_game.cells[pick / BOARD_N][pick % BOARD_N] = value;
    return true;
}

static void clear_board() {
    for (int row = 0; row < BOARD_N; row++) {
        for (int col = 0; col < BOARD_N; col++)
            g_game.cells[row][col] = 0;
    }
}

static void new_game() {
    clear_board();
    g_game.score = 0;
    g_game.game_over = false;
    g_game.game_over_dialog_shown = false;
    g_game.won = false;
    spawn_tile();
    spawn_tile();
    set_status("New board");
}

static void show_game_over_dialog() {
    if (!g_game.game_over || g_game.game_over_dialog_shown) return;
    g_game.game_over_dialog_shown = true;

    char message[128];
    snprintf(message, sizeof(message),
             "No moves left.\nScore: %lu\nStart a new game?",
             g_game.score);

    gui::dialogs::MessageBoxResult result =
        gui::dialogs::message_box("2048", message, gui::dialogs::MESSAGE_BOX_YES_NO);

    if (result == gui::dialogs::MESSAGE_BOX_RESULT_YES)
        new_game();
}

static unsigned long slide_values(const uint32_t in[BOARD_N], uint32_t out[BOARD_N]) {
    uint32_t compact[BOARD_N] = {};
    int compact_count = 0;
    unsigned long gained = 0;

    for (int i = 0; i < BOARD_N; i++) {
        if (in[i] != 0)
            compact[compact_count++] = in[i];
    }

    int out_count = 0;
    for (int i = 0; i < compact_count; i++) {
        if (i + 1 < compact_count && compact[i] == compact[i + 1]) {
            out[out_count] = compact[i] * 2;
            gained += out[out_count];
            out_count++;
            i++;
        } else {
            out[out_count++] = compact[i];
        }
    }

    while (out_count < BOARD_N)
        out[out_count++] = 0;

    return gained;
}

static bool apply_move(MoveDir dir) {
    uint32_t next[BOARD_N][BOARD_N] = {};
    unsigned long gained = 0;

    for (int line = 0; line < BOARD_N; line++) {
        uint32_t in[BOARD_N] = {};
        uint32_t out[BOARD_N] = {};

        for (int i = 0; i < BOARD_N; i++) {
            switch (dir) {
            case DIR_LEFT:  in[i] = g_game.cells[line][i]; break;
            case DIR_RIGHT: in[i] = g_game.cells[line][BOARD_N - 1 - i]; break;
            case DIR_UP:    in[i] = g_game.cells[i][line]; break;
            case DIR_DOWN:  in[i] = g_game.cells[BOARD_N - 1 - i][line]; break;
            }
        }

        gained += slide_values(in, out);

        for (int i = 0; i < BOARD_N; i++) {
            switch (dir) {
            case DIR_LEFT:  next[line][i] = out[i]; break;
            case DIR_RIGHT: next[line][BOARD_N - 1 - i] = out[i]; break;
            case DIR_UP:    next[i][line] = out[i]; break;
            case DIR_DOWN:  next[BOARD_N - 1 - i][line] = out[i]; break;
            }
        }
    }

    bool changed = false;
    for (int row = 0; row < BOARD_N; row++) {
        for (int col = 0; col < BOARD_N; col++) {
            if (next[row][col] != g_game.cells[row][col])
                changed = true;
        }
    }

    if (!changed) return false;

    for (int row = 0; row < BOARD_N; row++) {
        for (int col = 0; col < BOARD_N; col++) {
            g_game.cells[row][col] = next[row][col];
            if (g_game.cells[row][col] >= 2048 && !g_game.won) {
                g_game.won = true;
                set_status("2048 reached");
            }
        }
    }

    g_game.score += gained;
    if (g_game.score > g_game.best)
        g_game.best = g_game.score;

    spawn_tile();

    if (!has_moves()) {
        g_game.game_over = true;
        set_status("Game over");
        render();
        show_game_over_dialog();
    }

    return true;
}

static bool handle_key(const Montauk::KeyEvent& key) {
    if (!key.pressed) return false;

    if (key.scancode == 0x01)
        return false;

    if (key.ascii == 'r' || key.ascii == 'R') {
        new_game();
        return true;
    }

    if (g_game.game_over)
        return false;

    if (key.scancode == 0x4B || key.ascii == 'a' || key.ascii == 'A')
        return apply_move(DIR_LEFT);
    if (key.scancode == 0x4D || key.ascii == 'd' || key.ascii == 'D')
        return apply_move(DIR_RIGHT);
    if (key.scancode == 0x48 || key.ascii == 'w' || key.ascii == 'W')
        return apply_move(DIR_UP);
    if (key.scancode == 0x50 || key.ascii == 's' || key.ascii == 'S')
        return apply_move(DIR_DOWN);

    return false;
}

static bool handle_mouse(const Montauk::WinEvent& ev) {
    int old_x = g_mouse_x;
    int old_y = g_mouse_y;
    g_mouse_x = ev.mouse.x;
    g_mouse_y = ev.mouse.y;

    AppLayout lo = compute_layout(g_win.width, g_win.height);
    bool redraw = old_x != g_mouse_x || old_y != g_mouse_y;
    bool clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1);
    if (!clicked) return redraw;

    if (lo.new_button.contains(g_mouse_x, g_mouse_y)) {
        new_game();
        return true;
    }

    return redraw;
}

extern "C" void _start() {
    if (!fonts::init())
        montauk::exit(1);

    g_accent = mtk::load_system_accent();
    seed_rng();
    new_game();

    if (!g_win.create("2048", INIT_W, INIT_H))
        montauk::exit(1);

    render();

    while (true) {
        Montauk::WinEvent ev;
        int r = g_win.poll(&ev);

        if (r < 0) break;
        if (r == 0) {
            montauk::sleep_ms(16);
            continue;
        }

        bool redraw = false;

        if (ev.type == 3) break;

        if (ev.type == 2) {
            redraw = true;
        } else if (ev.type == 0) {
            if (ev.key.scancode == 0x01 && ev.key.pressed)
                break;
            redraw = handle_key(ev.key);
        } else if (ev.type == 1) {
            redraw = handle_mouse(ev);
        } else if (ev.type == 4) {
            redraw = true;
        }

        if (redraw)
            render();
    }

    g_win.destroy();
    montauk::exit(0);
}
