/** \file main.c
 *  \brief This is the main exectution program
 */

/***************************************************************************************************
 * Includes
 **************************************************************************************************/
#include <ctype.h>
#include <errno.h>
#include <pthread.h>
#include <signal.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>

#include "log.h"

#include "drivers/leddriver/rpi_leddriver.h"
#include "drivers/selector/rpi_selector.h"

#include "tasks/artnet/rpi_artnet.h"
#include "tasks/cava/rpi_cava.h"

#include "rpi_midi_controller.h"
#include "rpi_param.h"

/***************************************************************************************************
 * Preprocessor Constants and Macros
 **************************************************************************************************/

/***************************************************************************************************
 * Type and Contant Definitions
 **************************************************************************************************/

/***************************************************************************************************
 * Persistent Variables
 **************************************************************************************************/
/*  Command-line parameters */
bool IsTestMode = false;
int chanLedCount = 0;
int logLevel = 2;

int previousMode = -1;

unsigned long mainLoopCycle = 0;

pthread_mutex_t logLockMutex = PTHREAD_MUTEX_INITIALIZER;

/***************************************************************************************************
 * Internal Function Prototypes
 **************************************************************************************************/

void parseCommandLineArgs(int argc, char const *argv[]);

void terminate(int sig);

void manage_tasks(int previousMode, int currentMode);

void execute_task(int mode);

void execute_test_mode();

void execute_artnet_mode();

void execute_autonomous_mode();

void execute_manual_mode();

void adjust_loop(struct timespec const *loopStart);

void log_lock_helper(bool lock, void *udata);

/***************************************************************************************************
 * External Function Definitions
 **************************************************************************************************/

int main(int argc, char const *argv[]) {
  // setup
  parseCommandLineArgs(argc, argv);
  log_set_level(logLevel);

  signal(SIGINT, terminate);

  struct sched_param sp;
  sp.sched_priority = 32;
  if (pthread_setschedparam(pthread_self(), SCHED_FIFO, &sp)) {
    fprintf(stderr, "WARNING: Failed to set stepper thread to real-time priority: %s\n",
            strerror(errno));
  }

  log_set_lock(log_lock_helper, &logLockMutex);

  param_setup();
  midi_controller_setup();
  selector_setup();
  leddriver_setup();

  // setup_cava();

  // loop
  while (1) {
    struct timespec loopStart;
    int mode;

    clock_gettime(CLOCK_MONOTONIC, &loopStart);
    if (IsTestMode) {
      if (mainLoopCycle % (180 * 3) < 180) {
        mode = 0;
      } else if (mainLoopCycle % (180 * 3) < 180 * 2) {
        mode = 1;
      } else {
        mode = 2;
      }
    } else {
      mode = selector_get_position();
    }
    /* todo thread ? */
    midi_controller_execute();

    if (mode != -1) {
      if (mode != previousMode) {
        log_info("swtching to mode : %d", mode);
        manage_tasks(previousMode, mode);
      } else {
        execute_task(mode);
      }
      previousMode = mode;
    }
    adjust_loop(&loopStart);
  }
}

/***************************************************************************************************
 * Internal Function Definitions
 **************************************************************************************************/

void parseCommandLineArgs(int argc, char const *argv[]) {
  int args = 0;

  while (argc > ++args) // Process command-line args
  {
    if (argv[args][0] == '-') {
      switch (toupper(argv[args][1])) {
      case 'N': // -N: number of LEDs per channel
        if (args >= argc - 1) {
          log_error("no numeric value");
          exit(-EXIT_FAILURE);
        } else {
          chanLedCount = atoi(argv[++args]);
        }
        break;
      case 'D': // -D: debug level
        if (args >= argc - 1) {
          log_error("no debug level");
          exit(-EXIT_FAILURE);
        } else {
          logLevel = atoi(argv[++args]);
        }
        break;
      case 'T': // -T: test mode
        IsTestMode = true;
        break;
      default: // Otherwise error
        log_error("Unrecognised option '%c'\n", argv[args][1]);
        fprintf(stderr, "Options:\n"
                        "  -t        Test mode (flash LEDs)\n"
                        "  -n num    number of LEDs per channel\n"
                        "  -d lvl    debug level\n");
        exit(-EXIT_FAILURE);
      }
    }
  }
}

void terminate(int sig) {
  manage_tasks(previousMode, -1);
  leddriver_close();
  log_info("Goodbye !");
  exit(EXIT_SUCCESS);
}

void manage_tasks(int previousMode, int currentMode) {
  /* stop previous bg task */
  switch (previousMode) {
  case 1:
    artnet_stop();
    break;
  case 2:
    cava_stop();
    break;
  default:
    break;
  }

  /* start new bg task */
  switch (currentMode) {
  case 1:
    artnet_start();
    break;
  case 2:
    cava_start();
    break;
  default:
    break;
  }
}

void execute_task(int mode) {
  switch (mode) {
  case 0:
    // mode test
    execute_test_mode();
    break;
  case 1:
    // artnet mode
    execute_artnet_mode();
    break;
  case 2:
    execute_autonomous_mode();
    break;
  case 3:
    // manual mode
    execute_manual_mode();
    break;

  default:
    break;
  }
}

// Pointer to uncached Tx data buffer
// TXDATA_T tx_buffer[TX_BUFF_LEN(CHAN_MAXLEDS)]; // Tx buffer for assembling data

void execute_test_mode() {
  // RGB values for test mode (1 value for each of 16 channels)
  uint32_t on_rgbs[] = {0x5f0000, 0x005f00, 0x00005f, 0x5f5f00, 0x5f005f, 0x005f5f, 0x5f5f5f};
  uint32_t off_rgbs = 0x000000;

  static int i = 0, offset = 0;

  for (size_t ledIndex = 0; ledIndex < chanLedCount; ++ledIndex) {
    set_color(ledIndex <= offset % chanLedCount ? on_rgbs[i] * .5 : off_rgbs, ledIndex);
  }

  leddriver_refresh();

  if (offset < chanLedCount) {
    ++offset;
  } else {
    offset = 0;
    if (i < 7) {
      ++i;
    } else {
      i = 0;
    }
  }
}

// RGB data
int rgb_data[CHAN_MAXLEDS][LED_NCHANS];

void execute_artnet_mode() {
  uint8_t *dmxData;

  for (size_t ledBar = 0; ledBar < LED_NCHANS; ledBar++) {
    if (artnet_get_dmx_data(ledBar, &dmxData) == 0) {
      for (size_t i = 0; i < chanLedCount; ++i) {
        uint8_t *rgb = dmxData + (i * 3);
        rgb_data[i][ledBar] = (rgb[0] << 16) | (rgb[1] << 8) | rgb[2];
        rgb_txdata(rgb_data[i], i);
      }
    }
  }

  leddriver_refresh();
}

uint16_t static const minHue = 120 * UINT16_MAX / 360;
uint16_t static const maxHue = 300 * UINT16_MAX / 360;
uint16_t static const hueInterval = UINT16_MAX - (maxHue - minHue);

void execute_autonomous_mode() {
  int ret;
  uint16_t *buffer;

  if ((ret = cava_get_buffer(&buffer)) == 0) {
    for (size_t ledBarIndex = 0; ledBarIndex < LED_NCHANS; ++ledBarIndex) {
      uint16_t barMax = 0;
      for (size_t cavaBar = 0; cavaBar < CAVA_BAR_NUMBER / LED_NCHANS; ++cavaBar) {
        unsigned barIndex = ledBarIndex * CAVA_BAR_NUMBER / LED_NCHANS + cavaBar;
        if (barMax < buffer[barIndex]) {
          barMax = buffer[barIndex];
        }
      }
      barMax *= param_access->auton.sensitivity * param_access->ledbar[ledBarIndex].sensitivity;
      unsigned ledToLight = barMax * chanLedCount / UINT16_MAX;
      unsigned long hueShift = (long)barMax * (long)hueInterval / hueInterval;
      uint16_t hue = minHue - hueShift;
      uint32_t color = ColorHSV(hue, 255, 100);

      for (size_t i = 0; i < ledToLight; ++i) {
        rgb_data[i][ledBarIndex] = color;
        rgb_txdata(rgb_data[i], i);
      }
      for (size_t i = ledToLight; i < chanLedCount; ++i) {
        rgb_data[i][ledBarIndex] = 0x000000;
        rgb_txdata(rgb_data[i], i);
      }
    }

    leddriver_refresh();
  }
}

void execute_manual_mode() {
  for (size_t ledBarIndex = 0; ledBarIndex < LED_NCHANS; ++ledBarIndex) {
    for (size_t i = 0; i < chanLedCount; ++i) {
      rgb_data[i][ledBarIndex] = 0x000000;
      rgb_txdata(rgb_data[i], i);
    }
  }
  leddriver_refresh();
}

void adjust_loop(struct timespec const *loopStart) {
  struct timespec loopEnd;
  long elapsedTimeUs, remainingTimeUs;

  clock_gettime(CLOCK_MONOTONIC, &loopEnd);
  elapsedTimeUs = (loopEnd.tv_nsec - loopStart->tv_nsec) / 1E3 +
                  ((unsigned)(loopEnd.tv_sec - loopStart->tv_sec)) * 1E6;
  remainingTimeUs = 16000 - elapsedTimeUs;

  if (remainingTimeUs >= 0) {
    log_trace("cycle %lu, loop remaining time %ld", mainLoopCycle, remainingTimeUs);
    usleep(remainingTimeUs);
  } else {
    log_warn("loop overlap by %06ldus", -remainingTimeUs);
    log_info("loop start %ld.%09lds - loop end %ld.%09lds", loopStart->tv_sec, loopStart->tv_nsec,
             loopEnd.tv_sec, loopEnd.tv_nsec);
  }
  ++mainLoopCycle;
}

void log_lock_helper(bool lock, void *udata) {
  pthread_mutex_t *pLogLockMutex = (pthread_mutex_t *)(udata);
  if (lock) {
    pthread_mutex_lock(pLogLockMutex);
  } else {
    pthread_mutex_unlock(pLogLockMutex);
  }
}
