/* SPDX-FileCopyrightText: 2024 Google LLC */
/* SPDX-License-Identifier: Apache-2.0 */

#include "drivers/display.h"

#include <stdbool.h>

#include "board/board.h"
#include "drivers/dbgserial.h"
#include "drivers/display/bootloader_fpga_bitstream.auto.h"
#include "drivers/display/ice40lp.h"
#include "drivers/display/ice40lp_definitions.h"
#include "system/passert.h"
#include "util/delay.h"
#include "util/misc.h"
#include "util/sle.h"

#define CMD_NULL (0)
#define CMD_SET_PARAMETER (1)
#define CMD_DISPLAY_OFF (2)
#define CMD_DISPLAY_ON (3)
#define CMD_DRAW_SCENE (4)
#define CMD_RESET_RELEASE (8)
#define CMD_RESET_ASSERT (9)

#define SCENE_BLACK (0)
#define SCENE_SPLASH (1)
#define SCENE_UPDATE (2)
#define SCENE_ERROR (3)

#define UPDATE_PROGRESS_MAX (47)

static uint8_t s_decoded_fpga_image[35000];  // the FPGA image is currently ~30k

static bool prv_reset_fpga(void) {
  const uint32_t length = sle_decode(s_fpga_bitstream, sizeof(s_fpga_bitstream),
                                     s_decoded_fpga_image, sizeof(s_decoded_fpga_image));
  return display_program(s_decoded_fpga_image, length);
}

static bool prv_wait_busy(void) {
  // The display should come out of busy within 35 milliseconds;
  // it is a waste of time to wait more than twice that.
  int timeout = 50 * 10;
  while (display_busy()) {
    if (timeout-- == 0) {
      dbgserial_putstr("Display busy-wait timeout expired!");
      return false;
    }
    delay_us(100);
  }
  return true;
}

static void prv_screen_on(void) {
  display_write_cmd(CMD_DISPLAY_ON, NULL, 0);
}

static void prv_screen_off(void) {
  display_write_cmd(CMD_DISPLAY_OFF, NULL, 0);
}

static void prv_draw_scene(uint8_t scene) {
  display_write_cmd(CMD_DRAW_SCENE, &scene, sizeof(scene));
}

static void prv_set_parameter(uint32_t param) {
  display_write_cmd(CMD_SET_PARAMETER, (uint8_t *)&param, sizeof(param));
}

#ifdef DISPLAY_DEMO_LOOP
static void prv_play_demo_loop(void) {
  while (1) {
    for (int i = 0; i <= UPDATE_PROGRESS_MAX; ++i) {
      display_firmware_update_progress(i, UPDATE_PROGRESS_MAX);
      delay_ms(80);
    }

    for (uint32_t i = 0; i <= 0xf; ++i) {
      display_error_code(i * 0x11111111);
      delay_ms(200);
    }
    for (uint32_t i = 0; i < 8; ++i) {
      for (uint32_t j = 1; j <= 0xf; ++j) {
        display_error_code(j << (i * 4));
        delay_ms(200);
      }
    }
    display_error_code(0x01234567);
    delay_ms(200);
    display_error_code(0x89abcdef);
    delay_ms(200);
    display_error_code(0xcafebabe);
    delay_ms(200);
    display_error_code(0xfeedface);
    delay_ms(200);
    display_error_code(0x8badf00d);
    delay_ms(200);
    display_error_code(0xbad1ce40);
    delay_ms(200);
    display_error_code(0xbeefcace);
    delay_ms(200);
    display_error_code(0x0defaced);
    delay_ms(200);
    display_error_code(0xd15ea5e5);
    delay_ms(200);
    display_error_code(0xdeadbeef);
    delay_ms(200);
    display_boot_splash();
    delay_ms(1000);
  }
}
#endif

void display_init(void) {
  display_start();
  if (!prv_reset_fpga()) {
    dbgserial_putstr("FPGA configuration failed.");
    return;
  }

  // enable the power rails
  display_power_enable();

  // start with the screen off
  prv_screen_off();

  // Work around an issue which some boards exhibit where the FPGA ring
  // oscillator can start up with higher harmonics, massively overclocking the
  // design and causing malfunction. When this occurrs, the draw-scene command
  // will not work, asserting BUSY indefinitely but never updating the display.
  // Other commands such as display-on and display-off are less affected by the
  // overclocking, so the display can be turned on while the FPGA is in this
  // state, showing only garbage.
  // FPGA malfunction can be detected in software. In an attempt to restore
  // proper functioning, the FPGA can be reset and reconfigured in the hopes
  // that the ring oscillator will start up and oscillate without any higher
  // harmonics. Bootloader release 03 attempts to mitigate this problem by
  // delaying oscillator startup until after configuration completes. Time will
  // tell whether this actually fixes things.
  for (int retries = 0; retries <= 10; ++retries) {
    prv_draw_scene(SCENE_SPLASH);
    if (prv_wait_busy()) {
      prv_screen_on();
      dbgserial_print("Display initialized after ");
      dbgserial_print_hex(retries);
      dbgserial_putstr(" retries.");
#ifdef DISPLAY_DEMO_LOOP
      prv_play_demo_loop();
#endif
      return;
    }

    if (!prv_reset_fpga()) {
      dbgserial_putstr("FPGA configuration failed.");
      return;
    }
  }

  // It's taken too many attempts and the FPGA still isn't behaving. Give up on
  // showing the splash screen and keep the screen off so that the user doesn't
  // see a broken-looking staticky screen on boot.
  dbgserial_putstr("Display initialization failed.");
  prv_screen_off();
}

void display_boot_splash(void) {
  prv_wait_busy();
  prv_draw_scene(SCENE_SPLASH);
  // Don't turn the screen on until the boot-splash is fully drawn.
  prv_wait_busy();
  prv_screen_on();
}

void display_firmware_update_progress(
    uint32_t numerator, uint32_t denominator) {
  static uint8_t last_bar_fill = UINT8_MAX;
  // Scale progress to the number of pixels in the progress bar,
  // rounding half upwards.
  uint8_t bar_fill =
      ((numerator * UPDATE_PROGRESS_MAX) + ((denominator+1)/2)) / denominator;
  // Don't waste time and power redrawing the same screen repeatedly.
  if (bar_fill != last_bar_fill) {
    last_bar_fill = bar_fill;
    prv_set_parameter(bar_fill);
    prv_draw_scene(SCENE_UPDATE);
  }
}

void display_error_code(uint32_t error_code) {
  prv_set_parameter(error_code);
  prv_draw_scene(SCENE_ERROR);
}

void display_prepare_for_reset(void) {
  prv_screen_off();
}
