/*
 * Copyright (c) 2020 Raspberry Pi (Trading) Ltd.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 */

#include <stdio.h>
#include <stdlib.h>

#include "pico.h"
#include "hardware/uart.h"
#include "hardware/gpio.h"
#include "pico/scanvideo.h"
#include "pico/scanvideo/composable_scanline.h"
#include "pico/multicore.h"
#include "pico/sync.h"
#include "pico/stdlib.h"
#include "hardware/clocks.h"

#include "sprite.h"
#include "raspberry_128x128_bgar5515.h"
#include "raspberry_128x128_bgar5515_flip.h"

#include "hardware/structs/vreg_and_chip_reset.h"

//#define VGA_MODE vga_mode_320x240_60
#define VGA_MODE vga_mode_640x480_60
#define DUAL_CORE_RENDER
// #define TURBO_BOOST
#define N_BERRIES 45

CU_REGISTER_DEBUG_PINS(generation)
CU_SELECT_DEBUG_PINS(generation)

extern const struct scanvideo_pio_program video_24mhz_composable;

// to make sure only one core updates the state when the frame number changes
// todo note we should actually make sure here that the other core isn't still rendering (i.e. all must arrive before either can proceed - a la barrier)
static struct mutex frame_logic_mutex;

static void frame_update_logic();
static void render_scanline(struct scanvideo_scanline_buffer *dest, int core);

// "Worker thread" for each core
void __time_critical_func(render_loop)() {
    static uint32_t last_frame_num = 0;
    int core_num = get_core_num();
    printf("Rendering on core %d\n", core_num);
    while (true) {
        struct scanvideo_scanline_buffer *scanline_buffer = scanvideo_begin_scanline_generation(true);
        mutex_enter_blocking(&frame_logic_mutex);
        uint32_t frame_num = scanvideo_frame_number(scanline_buffer->scanline_id);
        // Note that with multiple cores we may have got here not for the first
        // scanline, however one of the cores will do this logic first before either
        // does the actual generation
        if (frame_num != last_frame_num) {
            last_frame_num = frame_num;
            frame_update_logic();
        }
        mutex_exit(&frame_logic_mutex);

        render_scanline(scanline_buffer, core_num);

        // Release the rendered buffer into the wild
        scanvideo_end_scanline_generation(scanline_buffer);
    }
}

struct semaphore video_setup_complete;

void core1_func() {
    sem_acquire_blocking(&video_setup_complete);
    render_loop();
}

int vga_main(void) {
    mutex_init(&frame_logic_mutex);
    sem_init(&video_setup_complete, 0, 1);

    // Core 1 will wait for us to finish video setup, and then start rendering
#ifdef DUAL_CORE_RENDER
    multicore_launch_core1(core1_func);
#endif

    hard_assert(VGA_MODE.width + 4 <= PICO_SCANVIDEO_MAX_SCANLINE_BUFFER_WORDS * 2);
    scanvideo_setup(&VGA_MODE);
    scanvideo_timing_enable(true);

    sem_release(&video_setup_complete);
    render_loop();

}

// Helper functions to:
// - Get a scanbuf into a state where a region of it can be directly rendered to,
//   and return a pointer to this region
// - After rendering, manipulate this scanbuffer into a form where PIO can
//   yeet it out on VGA

static inline uint16_t *raw_scanline_prepare(struct scanvideo_scanline_buffer *dest, uint width) {
    assert(width >= 3);
    assert(width % 2 == 0);
    // +1 for the black pixel at the end, -3 because the program outputs n+3 pixels.
    dest->data[0] = COMPOSABLE_RAW_RUN | (width + 1 - 3 << 16);
    // After user pixels, 1 black pixel then discard remaining FIFO data
    dest->data[width / 2 + 2] = 0x0000u | (COMPOSABLE_EOL_ALIGN << 16);
    dest->data_used = width / 2 + 2;
    assert(dest->data_used <= dest->data_max);
    return (uint16_t *) &dest->data[1];
}

static inline void raw_scanline_finish(struct scanvideo_scanline_buffer *dest) {
    // Need to pivot the first pixel with the count so that PIO can keep up
    // with its 1 pixel per 2 clocks
    uint32_t first = dest->data[0];
    uint32_t second = dest->data[1];
    dest->data[0] = (first & 0x0000ffffu) | ((second & 0x0000ffffu) << 16);
    dest->data[1] = (second & 0xffff0000u) | ((first & 0xffff0000u) >> 16);
    dest->status = SCANLINE_OK;
}

sprite_t berry[N_BERRIES];
int vx[N_BERRIES];
int vy[N_BERRIES];

void __time_critical_func(render_scanline)(struct scanvideo_scanline_buffer *dest, int core) {
    int l = scanvideo_scanline_number(dest->scanline_id);
    uint16_t *colour_buf = raw_scanline_prepare(dest, VGA_MODE.width);

    DEBUG_PINS_SET(generation, (core + 1));
    DEBUG_PINS_SET(generation, 4);
    const uint16_t bgcol = PICO_SCANVIDEO_PIXEL_FROM_RGB8(0x40, 0xc0, 0xff);
    sprite_fill16(colour_buf, bgcol, VGA_MODE.width);
    DEBUG_PINS_CLR(generation, 4);

    for (int i = 0; i < N_BERRIES; ++i)
        sprite_sprite16(colour_buf, &berry[i], l, VGA_MODE.width);

    DEBUG_PINS_CLR(generation, (core + 1));
    raw_scanline_finish(dest);
}

static inline int random_velocity() {
    // Never return 0 -- it makes the demo much less interesting
    return (rand() % 5 + 1) * (rand() & 0x8000 ? 1 : -1);
}

static inline int clip(int x, int min, int max) {
    return x < min ? min : x > max ? max : x;
}

static int xmin;
static int xmax;
static int ymin;
static int ymax;

void __time_critical_func(frame_update_logic)() {
    for (int i = 0; i < N_BERRIES; ++i) {
        berry[i].x += vx[i];
        berry[i].y += vy[i];
        int xclip = clip(berry[i].x, xmin, xmax);
        int yclip = clip(berry[i].y, ymin, ymax);
        if (berry[i].x != xclip || berry[i].y != yclip) {
            berry[i].x = xclip;
            berry[i].y = yclip;
            vx[i] = random_velocity();
            vy[i] = random_velocity();
            berry[i].img = vx[i] < 0 ? raspberry_128x128_flip : raspberry_128x128;
        }
    }
}

int main(void) {
#ifdef TURBO_BOOST
    hw_set_bits(&mm_vreg_and_chip_reset->vreg, VREG_AND_CHIP_RESET_VREG_VSEL_BITS);
    sleep_ms(10);
    set_sys_clock_khz(400000, true);
#else
#if PICO_SCANVIDEO_48MHZ
    set_sys_clock_khz(192000, true);
#else
    set_sys_clock_khz(200000, true);
#endif
#endif
    // Re init uart now that clk_peri has changed
    setup_default_uart();

#ifdef PICO_SMPS_MODE_PIN
    gpio_init(PICO_SMPS_MODE_PIN);
    gpio_set_dir(PICO_SMPS_MODE_PIN, GPIO_OUT);
    gpio_put(PICO_SMPS_MODE_PIN, 1);
#endif

    xmin = -100;
    xmax = VGA_MODE.width - 30;
    ymin = -100;
    ymax = VGA_MODE.height - 30;

    for (int i = 0; i < N_BERRIES; ++i) {
        berry[i].x = rand() % (xmax - xmin + 1) - xmin;
        berry[i].y = rand() % (ymax - ymin + 1) - ymin;
        berry[i].img = raspberry_128x128_flip;
        berry[i].log_size = 7;
        berry[i].has_opacity_metadata = true;
        vx[i] = random_velocity();
        vy[i] = random_velocity();
    }

    return vga_main();
}
