/******************************************************************************
* Copyright (C) 2023 Advanced Micro Devices, Inc. All Rights Reserved.
* SPDX-License-Identifier: MIT
******************************************************************************/
/*
 * helloworld.c: simple test application
 *
 * This application configures UART 16550 to baud rate 9600.
 * PS7 UART (Zynq) is not initialized by this application, since
 * bootrom/bsp configures it to baud rate 115200
 *
 * ------------------------------------------------
 * | UART TYPE   BAUD RATE                        |
 * ------------------------------------------------
 *   uartns550   9600
 *   uartlite    Configurable only in HW design
 *   ps7_uart    115200 (configured by bootrom/bsp)
 */

#include <stdio.h>
#include "platform.h"
#include "xil_printf.h"
#include <stdint.h>
#include <stdbool.h>
#include "registers.h"
#include "spi.h"
#include "buffered_peripheral.h"
#include "spi_matrix.h"
#include "xscugic.h"
#include "xparameters.h"
#include "xtmrctr.h"
#include "xuartps.h"

#define TIM0_INT_ID XPAR_FABRIC_TMRCTR_0_VEC_ID

#define TMRCTR_DEVICE_ID XPAR_AXI_TIMER_0_DEVICE_ID
#define TMRCTR_BASEADDRESS XPAR_AXI_TIMER_0_BASEADDR

#define COMMAND_SEPARATOR '\n'

static XScuGic IntcInstance;

// Display
matrix_t matrix;
XTmrCtr timer;

// SPI
spi_t matrix_spi;
buffered_transceiver_t matrix_tx;

#define MAX_CMD_LENGTH 65

#define MAX_IMAGES 10

#define COLS 8
#define ROWS 8

uint32_t auto_toggle_cycles = 1000000;
uint8_t images_count = 4;
uint8_t current_image = 0;
uint8_t replacing_image_index = 4;
uint8_t images[MAX_IMAGES][8] = {
    {0b11111111, 0b10000001, 0b10000001, 0b10000001, 0b10000001, 0b10000001, 0b10000001, 0b11111111},
    { 0b00000100, 0b00001110, 0b11110001, 0b10000101, 0b10000001, 0b10110001, 0b10110001, 0b11111111 },
    { 0b11111000, 0b11100100, 0b11100010, 0b10000001, 0b10000001, 0b01000010, 0b00100100, 0b00011000 },
    { 0b11000000, 0b10011000, 0b11010000, 0b10011010, 0b10010010, 0b00011010, 0b00000010, 0b00000011 },
};

uint32_t cycle = 0;
bool auto_toggle = false;
bool toggle_next = false;
bool toggle_prev = false;

bool animation = false;

void tim_handler(void* callback_ref, uint8_t timer_ctr_number);
void spi_handler(void);

void num_to_string(uint16_t num, char* res, uint16_t digits) {
  for (uint16_t i = 0; i < digits; i++) {
    res[digits - 1 - i] = num % 10 + '0';
    num /= 10;
  }
}

void handle_command(unsigned char* cmd, uint16_t len) {
  if (len == 0) {
    return;
  }

  bool handled = false;

  switch (cmd[0]) {
  case 'n': // next
    if (len == 1) {
      handled = true;
      toggle_next = true;
      printf("Switching to next image.\r\n");
    }
    break;
  case 'p': // next
    if (len == 1) {
      handled = true;
      toggle_prev = true;
      printf("Switching to previous image.\r\n");
    }
    break;
  case 'N': // auto toggle
    if (len == 1) {
      handled = true;
      auto_toggle = !auto_toggle;

      if (auto_toggle) {
        printf("Going to toggle automatically.\r\n");
      } else {
        printf("Manual switch mode.\r\n");
      }
    }
    break;
  case 'a': // animate
    if (len == 1) {
      handled = true;
      animation = !animation;

      if (animation) {
        printf("Animation enabled.\r\n");
      } else {
        printf("Animation disabled.\r\n");
      }
    }
    break;
  case 'u': // upload
    if (len != ROWS * COLS + 1) {
      break;
    }

    // null row
    for (uint8_t row = 0; row < ROWS; row++) {
      images[replacing_image_index][row] = 0;
    }

    // write data
    for (uint16_t i = 0; i < ROWS * COLS; i++) {
      uint8_t row = i >> 3;
      uint8_t col = i & 7;
      images[replacing_image_index][row] |= (cmd[1 + i] == 'X' ? 1 : 0) << col;
    }

    if (images_count < MAX_IMAGES) {
      images_count++;
      printf("Added image.\r\n");
    } else {
      printf("Replaced image.\r\n");
    }
    replacing_image_index++;
    replacing_image_index %= MAX_IMAGES;
    handled = true;

    break;
  case 's': // speed decrease
    if (len == 1) {
      handled = true;
      auto_toggle_cycles += 100000;
      printf("Auto toggle speed decreased.\r\n");
    }
    break;
  case 'S': // speed increase
    if (len == 1) {
      handled = true;
      if (auto_toggle_cycles > 100000) {
        auto_toggle_cycles -= 100000;
      }
      printf("Auto toggle speed increased.\r\n");
    }
    break;
  case 'l': // number of slots is...
    if (len == 1) {
      handled = true;

      printf("Used slots: ");

      char str[5] = { '\0', '\0', '\r', '\n', '\0' };
      num_to_string(images_count, str, 2);
      printf(str);
    }
    break;
  case 'I': // increase intensity
    if (len == 1) {
      matrix_increase_intensity(&matrix);
      printf("Increasing intensity.\r\n");
      handled = true;
    }
    break;
  case 'i': // decrease intensity
    if (len == 1) {
      matrix_decrease_intensity(&matrix);
      printf("Decreasing intensity.\r\n");
      handled = true;
    }
    break;
  case 'h':
    printf("Help:\r\n");
    printf(" h - show this help messages\r\n");
    printf(" l - print used slots\r\n");
    printf(" a - toggle animating\r\n");
    printf(" n, p - next, prev image\r\n");
    printf(" s, S - decrease, increase toggle speed\r\n");
    printf(" N - automatically switch images\r\n");
    printf(" I, i - increase, decrease intensity\r\n");
    printf(" uIMG - upload new image, IMG has \"X\" for 1s and \"-\" for 0s. It should be 64 chars long\r\n");
    handled = true;
    break;
  default:
    break;
  }

  if (!handled) {
    printf("Unknown command!\r\n");
  }
}

unsigned char* receive_command(uint16_t* length)
{
  static unsigned char cmd[MAX_CMD_LENGTH];
  static uint16_t index;

  unsigned char* current_char = (cmd + index);

  while (XUartPs_Recv(XPAR_XUARTPS_0_DEVICE_ID, current_char, 1) == 1) {
    // echo
    printf("%c", *current_char);

    if (*current_char == COMMAND_SEPARATOR) {
      printf("\r");
      *current_char = '\0';
      *length = index;
      index = 0;
      return cmd;
    }

    index++;
  }

  // Commands are separated either through max length, or by new line
  if (index == MAX_CMD_LENGTH + 1) {
    *length = MAX_CMD_LENGTH;
    index = 0;
    return cmd;
  }

  return NULL;
}

void next_image(int8_t offset)
{
  if (matrix.state != MATRIX_STABLE) {
    return;
  }

  current_image += offset;
  if (current_image > images_count) {
    current_image = images_count - 1;
  }

  current_image %= images_count;

  matrix_set_buffer(&matrix, MATRIX_SLOT_OTHER, &images[current_image][0]);

  if (animation) {
    matrix_animate_swap(&matrix);
  } else {
    matrix_swap(&matrix);
  }
}

void app_loop()
{
  while (1) {
    if (auto_toggle && cycle >= auto_toggle_cycles) {
      next_image(1);
      cycle = 0;
    } else if (toggle_next) {
      next_image(1);
      toggle_next = false;
    } else if (toggle_prev) {
      next_image(-1);
      toggle_prev = false;
    }

    uint16_t command_len;
    unsigned char* cmd = receive_command(&command_len);
    if (cmd != NULL) {
      handle_command(cmd, command_len);
    }

    cycle++;
  }
}

int init_interrupts()
{
    XScuGic_Config *IntcConfig;
    int status;

    /* Initialize the exception table */
    Xil_ExceptionInit();

    /* Look up the interrupt configuration */
    IntcConfig = XScuGic_LookupConfig(XPAR_SCUGIC_SINGLE_DEVICE_ID);
    if (IntcConfig == NULL) {
        xil_printf("Failed to lookup SCUGIC config.\r\n");
        return XST_FAILURE;
    }

    /* Initialize the interrupt controller driver so that it is ready to use */
    status = XScuGic_CfgInitialize(&IntcInstance, IntcConfig, IntcConfig->CpuBaseAddress);
    if (status != XST_SUCCESS) {
        xil_printf("Failed to initialize SCUGIC.\r\n");
        return XST_FAILURE;
    }

    /*
     * Connect the interrupt controller interrupt handler to the hardware interrupt
     * handling logic in the processor.
     */
    Xil_ExceptionRegisterHandler(
        XIL_EXCEPTION_ID_INT,
        (Xil_ExceptionHandler) XScuGic_InterruptHandler,
        &IntcInstance
    );

    /* Register (connect) your custom ISR to this interrupt */
    status = XScuGic_Connect(
        &IntcInstance,
        SPI0_INT_ID,
        (Xil_ExceptionHandler) spi_handler,
        NULL
    );
    if (status != XST_SUCCESS) {
        xil_printf("Failed to connect ISR.\r\n");
        return XST_FAILURE;
    }
    /* Register (connect) your custom ISR to this interrupt */
    status = XScuGic_Connect(
        &IntcInstance,
        TIM0_INT_ID,
        (XInterruptHandler)XTmrCtr_InterruptHandler,
        (void*)&timer
    );
    if (status != XST_SUCCESS) {
        xil_printf("Failed to connect ISR for timer.\r\n");
        return XST_FAILURE;
    }

    /* Enable the interrupt in the GIC */
    XScuGic_Enable(&IntcInstance, XIL_EXCEPTION_ID_INT);

    /* Enable interrupts in the Processor */
    Xil_ExceptionEnable();

    return XST_SUCCESS;
}

int main()
{
  init_platform();

  print("Hello World\n\r");
  print("Successfully ran Hello World application");

  init_interrupts();

  { // SPI init
    spi_init(&matrix_spi, SPI0);
    spi_master_configure(&matrix_spi, false, false, SPI_MSB_FIRST, SPI_FRAME_16_BIT, true);
    spi_master_configure_speed(&matrix_spi, 4);
    spi_enable_interrupt(&matrix_spi, true, false);
    spi_master_enable(&matrix_spi, true);
  }

  { // Matrix, timer init
    matrix_init(&matrix, &matrix_tx, 5);
    matrix_setup(&matrix);
    matrix_enable(&matrix, true);

    matrix_set_buffer(&matrix, MATRIX_SLOT0, images[current_image]);

    int status;
    status = XTmrCtr_Initialize(&timer, TMRCTR_DEVICE_ID);
    if (status != XST_SUCCESS) {
      print("Could not initialize timer.\r\n");
    }

    status = XTmrCtr_SelfTest(&timer, TMRCTR_DEVICE_ID);
    if (status != XST_SUCCESS) {
      print("Timer self-test failed.\r\n");
    }

    XTmrCtr_SetHandler(&timer, tim_handler, &timer);
    XTmrCtr_SetOptions(&timer, TMRCTR_DEVICE_ID, XTC_INT_MODE_OPTION | XTC_AUTO_RELOAD_OPTION);
    XTmrCtr_SetResetValue(&timer, TMRCTR_DEVICE_ID, 100000);
    XTmrCtr_Start(&timer, TMRCTR_DEVICE_ID);
  }

  // Application
  app_loop();

  cleanup_platform();
  return 0;
}

void tim_handler(void* callback_ref, uint8_t timer_ctr_number) {
	/* XTmrCtr *instance = (XTmrCtr *)callback_ref; */
  matrix_update(&matrix, &matrix_tx);
}

void spi_handler(void) {
  // using can receive as not busy interrupt
  if (spi_can_receive(&matrix_spi)) {
    uint16_t tmp;
    spi_receive(&matrix_spi, &tmp, 1);
  }
  if (spi_can_transmit(&matrix_spi)) {
    buffered_transceiver_trigger_transmit(&matrix_tx, 1);
  }
}

bool spi_generic_can_receive(void *peripheral) {
  return spi_can_receive((spi_t*)peripheral);
}
bool spi_generic_can_transmit(void *peripheral) {
  return spi_can_transmit((spi_t*)peripheral);
}

uint16_t spi_generic_receive(void *peripheral, void *buffer,
                             uint16_t max_size) {
  return spi_receive((spi_t*)peripheral, buffer, max_size);
}
uint16_t spi_generic_transmit(void *peripheral, void *data, uint16_t size) {
  // enable the interrupt so that we can send the rest
  uint16_t ret = spi_transmit((spi_t*)peripheral, data, size);
  return ret;
}
