// Copyright 2021 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later

#include "qp_internal.h"
#include "qp_draw.h"
#include "qp_comms.h"
#include "qgf.h"
#include "deferred_exec.h"

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// QGF image handles

typedef struct qgf_image_handle_t {
    painter_image_desc_t base;
    bool                 validate_ok;
    union {
        qp_stream_t        stream;
        qp_memory_stream_t mem_stream;
#ifdef QP_STREAM_HAS_FILE_IO
        qp_file_stream_t file_stream;
#endif // QP_STREAM_HAS_FILE_IO
    };
} qgf_image_handle_t;

static qgf_image_handle_t image_descriptors[QUANTUM_PAINTER_NUM_IMAGES] = {0};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Helper: load image from stream

static painter_image_handle_t qp_load_image_internal(bool (*stream_factory)(qgf_image_handle_t *image, void *arg), void *arg) {
    qp_dprintf("qp_load_image: entry\n");
    qgf_image_handle_t *image = NULL;

    // Find a free slot
    for (int i = 0; i < QUANTUM_PAINTER_NUM_IMAGES; ++i) {
        if (!image_descriptors[i].validate_ok) {
            image = &image_descriptors[i];
            break;
        }
    }

    // Drop out if not found
    if (!image) {
        qp_dprintf("qp_load_image: fail (no free slot)\n");
        return NULL;
    }

    if (!stream_factory(image, arg)) {
        qp_dprintf("qp_load_image: fail (could not create stream)\n");
        return NULL;
    }

    // Now that we know the length, validate the input data
    if (!qgf_validate_stream(&image->stream)) {
        qp_dprintf("qp_load_image: fail (failed validation)\n");
        return NULL;
    }

    // Fill out the QP image descriptor
    qgf_read_graphics_descriptor(&image->stream, &image->base.width, &image->base.height, &image->base.frame_count, NULL);

    // Validation success, we can return the handle
    image->validate_ok = true;
    qp_dprintf("qp_load_image: ok\n");
    return (painter_image_handle_t)image;
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter External API: qp_load_image_mem

static inline bool image_mem_stream_factory(qgf_image_handle_t *image, void *arg) {
    void *buffer = arg;

    // Assume we can read the graphics descriptor
    image->mem_stream = qp_make_memory_stream((void *)buffer, sizeof(qgf_graphics_descriptor_v1_t));

    // Update the length of the stream to match, and rewind to the start
    image->mem_stream.length   = qgf_get_total_size(&image->stream);
    image->mem_stream.position = 0;

    return true;
}

painter_image_handle_t qp_load_image_mem(const void *buffer) {
    return qp_load_image_internal(image_mem_stream_factory, (void *)buffer);
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter External API: qp_close_image

bool qp_close_image(painter_image_handle_t image) {
    qgf_image_handle_t *qgf_image = (qgf_image_handle_t *)image;
    if (!qgf_image->validate_ok) {
        qp_dprintf("qp_close_image: fail (invalid image)\n");
        return false;
    }

    // Free up this image for use elsewhere.
    qgf_image->validate_ok = false;
    qp_stream_close(&qgf_image->stream);
    return true;
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter External API: qp_drawimage

bool qp_drawimage(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image) {
    return qp_drawimage_recolor(device, x, y, image, 0, 0, 255, 0, 0, 0);
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter External API: qp_drawimage_recolor

typedef struct qgf_frame_info_t {
    painter_compression_t compression_scheme;
    uint8_t               bpp;
    bool                  has_palette;
    bool                  is_delta;
    uint16_t              left;
    uint16_t              top;
    uint16_t              right;
    uint16_t              bottom;
    uint16_t              delay;
} qgf_frame_info_t;

static bool qp_drawimage_prepare_frame_for_stream_read(painter_device_t device, qgf_image_handle_t *qgf_image, uint16_t frame_number, qp_pixel_t fg_hsv888, qp_pixel_t bg_hsv888, qgf_frame_info_t *info) {
    struct painter_driver_t *driver = (struct painter_driver_t *)device;

    // Drop out if we can't actually place the data we read out anywhere
    if (!info) {
        qp_dprintf("Failed to prepare stream for read, output info buffer unavailable\n");
        return false;
    }

    // Seek to the frame
    qgf_seek_to_frame_descriptor(&qgf_image->stream, frame_number);

    // Read the frame descriptor
    qgf_frame_v1_t frame_descriptor;
    if (qp_stream_read(&frame_descriptor, sizeof(qgf_frame_v1_t), 1, &qgf_image->stream) != 1) {
        qp_dprintf("Failed to read frame_descriptor, expected length was not %d\n", (int)sizeof(qgf_frame_v1_t));
        return false;
    }

    // Parse out the frame info
    if (!qgf_parse_frame_descriptor(&frame_descriptor, &info->bpp, &info->has_palette, &info->is_delta, &info->compression_scheme, &info->delay)) {
        return false;
    }

    // Ensure we aren't reusing any palette
    qp_internal_invalidate_palette();

    if (!qp_internal_bpp_capable(info->bpp)) {
        qp_dprintf("qp_drawimage_recolor: fail (image bpp too high (%d), check QUANTUM_PAINTER_SUPPORTS_256_PALETTE or QUANTUM_PAINTER_SUPPORTS_NATIVE_COLORS)\n", (int)info->bpp);
        qp_comms_stop(device);
        return false;
    }

    // Handle palette if needed
    const uint16_t palette_entries  = 1u << info->bpp;
    bool           needs_pixconvert = false;
    if (info->has_palette) {
        // Load the palette from the stream
        if (!qp_internal_load_qgf_palette((qp_stream_t *)&qgf_image->stream, info->bpp)) {
            return false;
        }

        needs_pixconvert = true;
    } else {
        if (info->bpp <= 8) {
            // Interpolate from fg/bg
            needs_pixconvert = qp_internal_interpolate_palette(fg_hsv888, bg_hsv888, palette_entries);
        }
    }

    if (needs_pixconvert) {
        // Convert the palette to native format
        if (!driver->driver_vtable->palette_convert(device, palette_entries, qp_internal_global_pixel_lookup_table)) {
            qp_dprintf("qp_drawimage_recolor: fail (could not convert pixels to native)\n");
            qp_comms_stop(device);
            return false;
        }
    }

    // Handle delta if needed
    if (info->is_delta) {
        qgf_delta_v1_t delta_descriptor;
        if (qp_stream_read(&delta_descriptor, sizeof(qgf_delta_v1_t), 1, &qgf_image->stream) != 1) {
            qp_dprintf("Failed to read delta_descriptor, expected length was not %d\n", (int)sizeof(qgf_delta_v1_t));
            return false;
        }

        info->left   = delta_descriptor.left;
        info->top    = delta_descriptor.top;
        info->right  = delta_descriptor.right;
        info->bottom = delta_descriptor.bottom;
    }

    // Read the data block
    qgf_data_v1_t data_descriptor;
    if (qp_stream_read(&data_descriptor, sizeof(qgf_data_v1_t), 1, &qgf_image->stream) != 1) {
        qp_dprintf("Failed to read data_descriptor, expected length was not %d\n", (int)sizeof(qgf_data_v1_t));
        return false;
    }

    // Stream is now at the point of being able to read pixdata
    return true;
}

static bool qp_drawimage_recolor_impl(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image, int frame_number, qgf_frame_info_t *frame_info, qp_pixel_t fg_hsv888, qp_pixel_t bg_hsv888) {
    qp_dprintf("qp_drawimage_recolor: entry\n");
    struct painter_driver_t *driver = (struct painter_driver_t *)device;
    if (!driver->validate_ok) {
        qp_dprintf("qp_drawimage_recolor: fail (validation_ok == false)\n");
        return false;
    }

    qgf_image_handle_t *qgf_image = (qgf_image_handle_t *)image;
    if (!qgf_image->validate_ok) {
        qp_dprintf("qp_drawimage_recolor: fail (invalid image)\n");
        return false;
    }

    // Read the frame info
    if (!qp_drawimage_prepare_frame_for_stream_read(device, qgf_image, frame_number, fg_hsv888, bg_hsv888, frame_info)) {
        qp_dprintf("qp_drawimage_recolor: fail (could not read frame %d)\n", frame_number);
        return false;
    }

    if (!qp_comms_start(device)) {
        qp_dprintf("qp_drawimage_recolor: fail (could not start comms)\n");
        return false;
    }

    uint16_t l, t, r, b;
    if (frame_info->is_delta) {
        l = x + frame_info->left;
        t = y + frame_info->top;
        r = x + frame_info->right - 1;
        b = y + frame_info->bottom - 1;
    } else {
        l = x;
        t = y;
        r = x + image->width - 1;
        b = y + image->height - 1;
    }
    uint32_t pixel_count = ((uint32_t)(r - l + 1)) * (b - t + 1);

    // Configure where we're going to be rendering to
    if (!driver->driver_vtable->viewport(device, l, t, r, b)) {
        qp_dprintf("qp_drawimage_recolor: fail (could not set viewport)\n");
        qp_comms_stop(device);
        return false;
    }

    // Set up the input state
    struct qp_internal_byte_input_state input_state    = {.device = device, .src_stream = &qgf_image->stream};
    qp_internal_byte_input_callback     input_callback = qp_internal_prepare_input_state(&input_state, frame_info->compression_scheme);
    if (input_callback == NULL) {
        qp_dprintf("qp_drawimage_recolor: fail (invalid image compression scheme)\n");
        qp_comms_stop(device);
        return false;
    }

    bool ret = false;
    if (frame_info->bpp <= 8) {
        // Set up the output state
        struct qp_internal_pixel_output_state output_state = {.device = device, .pixel_write_pos = 0, .max_pixels = qp_internal_num_pixels_in_buffer(device)};

        // Decode the pixel data and stream to the display
        ret = qp_internal_decode_palette(device, pixel_count, frame_info->bpp, input_callback, &input_state, qp_internal_global_pixel_lookup_table, qp_internal_pixel_appender, &output_state);
        // Any leftovers need transmission as well.
        if (ret && output_state.pixel_write_pos > 0) {
            ret &= driver->driver_vtable->pixdata(device, qp_internal_global_pixdata_buffer, output_state.pixel_write_pos);
        }
    } else {
        // Set up the output state
        struct qp_internal_byte_output_state output_state = {.device = device, .byte_write_pos = 0, .max_bytes = qp_internal_num_pixels_in_buffer(device) * driver->native_bits_per_pixel / 8};

        // Stream the raw pixel data to the display
        uint32_t byte_count = pixel_count * frame_info->bpp / 8;
        ret                 = qp_internal_send_bytes(device, byte_count, input_callback, &input_state, qp_internal_byte_appender, &output_state);
        // Any leftovers need transmission as well.
        if (ret && output_state.byte_write_pos > 0) {
            ret &= driver->driver_vtable->pixdata(device, qp_internal_global_pixdata_buffer, output_state.byte_write_pos * 8 / driver->native_bits_per_pixel);
        }
    }

    qp_dprintf("qp_drawimage_recolor: %s\n", ret ? "ok" : "fail");
    qp_comms_stop(device);
    return ret;
}

bool qp_drawimage_recolor(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image, uint8_t hue_fg, uint8_t sat_fg, uint8_t val_fg, uint8_t hue_bg, uint8_t sat_bg, uint8_t val_bg) {
    qgf_frame_info_t frame_info = {0};
    qp_pixel_t       fg_hsv888  = {.hsv888 = {.h = hue_fg, .s = sat_fg, .v = val_fg}};
    qp_pixel_t       bg_hsv888  = {.hsv888 = {.h = hue_bg, .s = sat_bg, .v = val_bg}};
    return qp_drawimage_recolor_impl(device, x, y, image, 0, &frame_info, fg_hsv888, bg_hsv888);
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter External API: qp_animate

deferred_token qp_animate(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image) {
    return qp_animate_recolor(device, x, y, image, 0, 0, 255, 0, 0, 0);
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter External API: qp_animate_recolor

typedef struct animation_state_t {
    painter_device_t       device;
    uint16_t               x;
    uint16_t               y;
    painter_image_handle_t image;
    qp_pixel_t             fg_hsv888;
    qp_pixel_t             bg_hsv888;
    uint16_t               frame_number;
    deferred_token         defer_token;
} animation_state_t;

static deferred_executor_t animation_executors[QUANTUM_PAINTER_CONCURRENT_ANIMATIONS] = {0};
static animation_state_t   animation_states[QUANTUM_PAINTER_CONCURRENT_ANIMATIONS]    = {0};

static deferred_token qp_render_animation_state(animation_state_t *state, uint16_t *delay_ms) {
    qgf_frame_info_t frame_info = {0};
    qp_dprintf("qp_render_animation_state: entry (frame #%d)\n", (int)state->frame_number);
    bool ret = qp_drawimage_recolor_impl(state->device, state->x, state->y, state->image, state->frame_number, &frame_info, state->fg_hsv888, state->bg_hsv888);
    if (ret) {
        ++state->frame_number;
        if (state->frame_number >= state->image->frame_count) {
            state->frame_number = 0;
        }
        *delay_ms = frame_info.delay;
    }
    qp_dprintf("qp_render_animation_state: %s (delay %dms)\n", ret ? "ok" : "fail", (int)(*delay_ms));
    return ret;
}

static uint32_t animation_callback(uint32_t trigger_time, void *cb_arg) {
    animation_state_t *state = (animation_state_t *)cb_arg;
    uint16_t           delay_ms;
    bool               ret = qp_render_animation_state(state, &delay_ms);
    if (!ret) {
        // Setting the device to NULL clears the animation slot
        state->device = NULL;
    }
    // If we're successful, keep animating -- returning 0 cancels the deferred execution
    return ret ? delay_ms : 0;
}

deferred_token qp_animate_recolor(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image, uint8_t hue_fg, uint8_t sat_fg, uint8_t val_fg, uint8_t hue_bg, uint8_t sat_bg, uint8_t val_bg) {
    qp_dprintf("qp_animate_recolor: entry\n");

    animation_state_t *anim_state = NULL;
    for (int i = 0; i < QUANTUM_PAINTER_CONCURRENT_ANIMATIONS; ++i) {
        if (animation_states[i].device == NULL) {
            anim_state = &animation_states[i];
            break;
        }
    }

    if (!anim_state) {
        qp_dprintf("qp_animate_recolor: fail (could not find free animation slot)\n");
        return INVALID_DEFERRED_TOKEN;
    }

    // Prepare the animation state
    anim_state->device       = device;
    anim_state->x            = x;
    anim_state->y            = y;
    anim_state->image        = image;
    anim_state->fg_hsv888    = (qp_pixel_t){.hsv888 = {.h = hue_fg, .s = sat_fg, .v = val_fg}};
    anim_state->bg_hsv888    = (qp_pixel_t){.hsv888 = {.h = hue_bg, .s = sat_bg, .v = val_bg}};
    anim_state->frame_number = 0;

    // Draw the first frame
    uint16_t delay_ms;
    if (!qp_render_animation_state(anim_state, &delay_ms)) {
        anim_state->device = NULL; // disregard the allocated animation slot
        qp_dprintf("qp_animate_recolor: fail (could not render first frame)\n");
        return INVALID_DEFERRED_TOKEN;
    }

    // Set up the timer
    anim_state->defer_token = defer_exec_advanced(animation_executors, QUANTUM_PAINTER_CONCURRENT_ANIMATIONS, delay_ms, animation_callback, anim_state);
    if (anim_state->defer_token == INVALID_DEFERRED_TOKEN) {
        anim_state->device = NULL; // disregard the allocated animation slot
        qp_dprintf("qp_animate_recolor: fail (could not set up animation executor)\n");
        return INVALID_DEFERRED_TOKEN;
    }

    qp_dprintf("qp_animate_recolor: ok (deferred token = %d)\n", (int)anim_state->defer_token);
    return anim_state->defer_token;
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter External API: qp_stop_animation

void qp_stop_animation(deferred_token anim_token) {
    for (int i = 0; i < QUANTUM_PAINTER_CONCURRENT_ANIMATIONS; ++i) {
        if (animation_states[i].defer_token == anim_token) {
            cancel_deferred_exec_advanced(animation_executors, QUANTUM_PAINTER_CONCURRENT_ANIMATIONS, anim_token);
            animation_states[i].device = NULL;
            return;
        }
    }
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter Core API: qp_internal_animation_tick

void qp_internal_animation_tick(void) {
    static uint32_t last_anim_exec = 0;
    deferred_exec_advanced_task(animation_executors, QUANTUM_PAINTER_CONCURRENT_ANIMATIONS, &last_anim_exec);
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter Core API: qp_internal_task

void qp_internal_task(void) {
    qp_internal_animation_tick();
#ifdef QUANTUM_PAINTER_LVGL_INTEGRATION_ENABLE
    // Run LVGL ticks
    void qp_lvgl_internal_tick(void);
    qp_lvgl_internal_tick();
#endif
}
