#include <Arduino.h>

// TODO : use core built-in lib tinyNeoPixel
#include <Adafruit_NeoPixel.h>
#include <Encoder.h>

#include "button.hpp"
#include "pin_map.hpp"

/* Types and Constants */
/*----------------------------------------------------------------------------------------------*/

/* State machine */
#define FOREACH_MODE(MODE)                                                     \
  MODE(Init)                                                                   \
  MODE(Preset)                                                                 \
  MODE(SetColor)                                                               \
  MODE(SetSaturation)                                                          \
  MODE(SetBrightness)                                                          \
  MODE(LightOn)                                                                \
  MODE(Shift)                                                                  \
  MODE(ControlOff)

#define GENERATE_MODE_ENUM(ENUM) ENUM,

enum class Mode_e { FOREACH_MODE(GENERATE_MODE_ENUM) };

/* cyclic increment operator for mode_e */
Mode_e &operator++(Mode_e &mode) {
  if (mode == Mode_e::ControlOff) {
    return mode = Mode_e::Preset;
  }
  return mode = static_cast<Mode_e>(static_cast<int>(mode) + 1);
}

#ifdef DEBUG
#define GENERATE_MODE_STRING(STRING) #STRING,
char static const *mode_str[] = {FOREACH_MODE(GENERATE_MODE_STRING)};
#endif /*DEBUG*/

/* Leds */
uint16_t static const LedCount{24};
uint8_t static const ColorSaturation{128};
uint8_t static const ColorArray[3][3] = {
    {ColorSaturation, 0, 0}, {0, ColorSaturation, 0}, {0, 0, ColorSaturation}};

/* Led ring color and led count state */
typedef struct {
  uint16_t ledOnCounter;
  uint16_t ledOnShift;
  uint16_t hue;
  uint8_t saturation;
  uint8_t brightness;
} LedRingColorState_t;

uint8_t const PresetMax{8};

// https://hslpicker.com/          = {0, 40, 50, 110, 240, 280, 310, 360 for white}
uint16_t const PresetHue[PresetMax] = {0, 7282, 9102, 20025, 43691, 50972, 56434, 65535};

uint8_t const PresetLevelMax{3};

uint8_t const PresetBrightness[PresetLevelMax] = {16, 64, 127};

// LedRingColorState_t const Preset[PresetMax] = {{
//     .ledOnCounter = LedCount,
//     .ledOnShift = 0,
//     .hue        = 0,
//     .saturation = 255,
//     .brightness = 255
//     }, {
//     .ledOnCounter = LedCount,
//     .ledOnShift = 0,
//     .hue        = 28,
//     .saturation = 255,
//     .brightness = 255
//     }, {
//     .ledOnCounter = LedCount,
//     .ledOnShift = 0,
//     .hue        = 36,
//     .saturation = 255,
//     .brightness = 255
//     }, {
//     .ledOnCounter = LedCount,
//     .ledOnShift = 0,
//     .hue        = 78,
//     .saturation = 255,
//     .brightness = 255
//     }, {
//     .ledOnCounter = LedCount,
//     .ledOnShift = 0,
//     .hue        = 171,
//     .saturation = 255,
//     .brightness = 16,
//     }, {
//     .ledOnCounter = LedCount,
//     .ledOnShift = 0,
//     .hue        = 199,
//     .saturation = 255,
//     .brightness = 16,
//     }, {
//     .ledOnCounter = LedCount,
//     .ledOnShift = 0,
//     .hue        = 220,
//     .saturation = 255,
//     .brightness = 16,
//     }
// };

/* Preset state */
typedef struct {
  bool isActive;
  uint8_t index;
  uint8_t level;
} PresetState_t;

/* Led ring system state */
typedef struct {
  LedRingColorState_t ledRingColorState;
  PresetState_t presetState;
  Mode_e currentMode;
} LedRingState_t;

/* Button */
int const AnalogButtonVRef{970};

/* Global variables */
/*----------------------------------------------------------------------------------------------*/

/* Led ring state */
LedRingState_t ledRingState {
    .ledRingColorState = {
      .ledOnCounter = LedCount / 2,
      .ledOnShift = 0,
      .hue = 0,
      .saturation = 255,
      .brightness = 16
      },
    .presetState = {
      .isActive = false,
      .index = 0,
      .level = 0
    },
    .currentMode = Mode_e::Init
};

/*  IO Objects */
Adafruit_NeoPixel ledRing{LedCount, LedPin, NEO_GRB + NEO_KHZ800};
Encoder encoder{EncoderPinA, EncoderPinB};
AnalogButton button{AnalogButtonPin, AnalogButtonVRef};

/* debug Serial definition */
#ifdef DEBUG
#ifdef ARDUINO_AVR_ATTINYX5
#include <SoftwareSerial.h>
SoftwareSerial debugSerial{RxPin, TxPin};
#endif /*ARDUINO_AVR_ATTINYX5*/
#ifdef ARDUINO_AVR_UNO
#include <HardwareSerial.h>
HardwareSerial &debugSerial = Serial;
#endif /*ARDUINO_AVR_UNO*/
#endif /*DEBUG*/

/* Private function declarations */
/*----------------------------------------------------------------------------------------------*/

/* State machine */
#define GENERATE_MODE_EXEC_DECLARTION(MODE) void MODE##_execute(int shift);
FOREACH_MODE(GENERATE_MODE_EXEC_DECLARTION)

/* Encoder */
void analog_button_calibration(void);

int get_encoder_shift(void);

void display_mode(Mode_e mode);

void display_led_ring(void);

void refresh_led_ring(LedRingColorState_t const &ledRingColorState = ledRingState.ledRingColorState);

/* Function definitions */
/*----------------------------------------------------------------------------------------------*/
void setup() {

  ledRing.begin();
  ledRing.fill(Adafruit_NeoPixel::Color(45, 0, 0));
  delay(300);
  ledRing.fill(Adafruit_NeoPixel::Color(0, 45, 0));
  delay(300);
  ledRing.fill(Adafruit_NeoPixel::Color(0, 0, 45));
  delay(300);

#ifdef DEBUG
  debugSerial.begin(9600);
  while (!debugSerial.available())
    ;
  debugSerial.println("Init ... ");
#endif /*DEBUG*/

  analog_button_calibration();

  get_encoder_shift();

#ifdef DEBUG
  debugSerial.println(mode_str[static_cast<int>(ledRingState.currentMode)]);
#endif /*DEBUG*/
}

void loop() {
  bool hasModeChanged{false};
  int shift{get_encoder_shift()};

  if (button.is_button_pressed()) {
    while (button.is_button_pressed()) {
      delay(150);
    }
    ++ledRingState.currentMode;

#ifdef DEBUG
    debugSerial.println(mode_str[static_cast<int>(ledRingState.currentMode)]);
#endif /*DEBUG*/

    display_mode(ledRingState.currentMode);
    hasModeChanged = true;
  }

  if (shift != 0) {
    switch (ledRingState.currentMode) {
    case Mode_e::ControlOff:
      ControlOff_execute(shift);
      break;

    case Mode_e::Preset:
      Preset_execute(shift);
      break;

    case Mode_e::LightOn:
      LightOn_execute(shift);
      break;

    case Mode_e::SetColor:
      SetColor_execute(shift);
      break;

    case Mode_e::SetBrightness:
      SetBrightness_execute(shift);
      break;

    case Mode_e::SetSaturation:
      SetSaturation_execute(shift);
      break;

    case Mode_e::Shift:
      Shift_execute(shift);
      break;

    default:
      break;
    }
  }

  if (ledRingState.currentMode != Mode_e::ControlOff && (shift != 0 || hasModeChanged)) {
    display_led_ring();
  }
}

/* State Machine functions */

void ControlOff_execute(int shift) {}

void Preset_execute(int shift) {
  PresetState_t &preset = ledRingState.presetState;

  if (shift > 0) {
    ++preset.index;
    if (preset.index >= PresetMax) {
      preset.index = 0;
      ++preset.level;
      if (preset.level >= PresetLevelMax) {
        preset.level = 0;
      }
    }
  } else {
    --preset.index;
    if (preset.index == UINT8_MAX) {
      preset.index = PresetMax - 1;
      --preset.level;
      if (preset.level == UINT8_MAX) {
        preset.level = PresetLevelMax - 1;
      }
    }
  }
} /* Preset_execute */

void SetColor_execute(int shift) {
  uint16_t &hue = ledRingState.ledRingColorState.hue;

  hue += (shift << 8);

#if defined(DEBUG) && defined(ARDUINO_AVR_UNO)
  if (shift != 0) {
    debugSerial.print(__func__);
    debugSerial.print(" : shift=");
    debugSerial.print(shift, DEC);
    debugSerial.print(", hue=");
    debugSerial.println(hue, DEC);
  }
#endif /*defined(DEBUG) && defined(ARDUINO_AVR_UNO)*/
} /* SetColor_execute */

void SetBrightness_execute(int shift) {
  uint8_t &brightness = ledRingState.ledRingColorState.brightness;

  brightness += (shift << 2);

#if defined(DEBUG) && defined(ARDUINO_AVR_UNO)
  if (shift != 0) {
    debugSerial.print(__func__);
    debugSerial.print(" : shift=");
    debugSerial.print(shift, DEC);
    debugSerial.print(", brightness=");
    debugSerial.println(brightness, DEC);
  }
#endif /*defined(DEBUG) && defined(ARDUINO_AVR_UNO)*/
} /* SetBrightness_execute */

void SetSaturation_execute(int shift) {
  uint8_t &saturation = ledRingState.ledRingColorState.saturation;

  saturation += (shift << 2);

#if defined(DEBUG) && defined(ARDUINO_AVR_UNO)
  if (shift != 0) {
    debugSerial.print(__func__);
    debugSerial.print(" : shift=");
    debugSerial.print(shift, DEC);
    debugSerial.print(", saturation=");
    debugSerial.println(saturation, DEC);
  }
#endif /*defined(DEBUG) && defined(ARDUINO_AVR_UNO)*/
} /* SetSaturation_execute */

void LightOn_execute(int shift) {
  uint16_t &ledOnCounter = ledRingState.ledRingColorState.ledOnCounter;
  static bool clockwise = true;

  if ((shift > 0) != !clockwise) {
    ++ledOnCounter;
    if (ledOnCounter > LedCount) {
      ledOnCounter = LedCount;
      clockwise = !clockwise;
    }
  } else {
    --ledOnCounter;
    if (ledOnCounter == UINT16_MAX) {
      ledOnCounter = 0;
      clockwise = !clockwise;
    }
  }

#if defined(DEBUG) && defined(ARDUINO_AVR_UNO)
  if (shift != 0) {
    debugSerial.print(__func__);
    debugSerial.print(" : shift=");
    debugSerial.print(shift, DEC);
    debugSerial.print(", ledOnCounter=");
    debugSerial.println(ledOnCounter, DEC);
  }
#endif /*defined(DEBUG) && defined(ARDUINO_AVR_UNO)*/
} /* LightOn_execute */

void Shift_execute(int shift) {
  uint16_t &ledOnShift = ledRingState.ledRingColorState.ledOnShift;

  if (shift > 0) {
    ++ledOnShift;
    if (ledOnShift == LedCount) {
      ledOnShift = 0;
    }
  } else {
    --ledOnShift;
    if (ledOnShift == UINT16_MAX) {
      ledOnShift = LedCount - 1;
    }
  }

#if defined(DEBUG) && defined(ARDUINO_AVR_UNO)
  if (shift != 0) {
    debugSerial.print(__func__);
    debugSerial.print(" : shift=");
    debugSerial.print(shift, DEC);
    debugSerial.print(", ledOnShift=");
    debugSerial.println(ledOnShift, DEC);
  }
#endif /*defined(DEBUG) && defined(ARDUINO_AVR_UNO)*/
} /* Shift_execute */

/* Others functions */

void analog_button_calibration(void) {
  int highRef, lowRef;

  do {
    highRef = analogRead(AnalogButtonPin);
    delay(300);

#ifdef DEBUG
    debugSerial.println(highRef);
#endif /*DEBUG*/
  } while (highRef < 1000);

  lowRef = analogRead(AnalogButtonPin);
  while (highRef - lowRef < 50) {
#ifdef DEBUG
    debugSerial.println(lowRef);
#endif /*DEBUG*/

    lowRef = analogRead(AnalogButtonPin);
    delay(300);
  }

#ifdef DEBUG
  debugSerial.println(lowRef);
#endif /*DEBUG*/

  button = AnalogButton{AnalogButtonPin, lowRef + 10};
}

int get_encoder_shift(void) {
  int32_t static _previousValue{encoder.read()};

  int32_t currentValue{encoder.read()};
  int32_t diff{currentValue - _previousValue};

  if (diff <= -4) {
    _previousValue = currentValue;
    return diff >> 2;

  } else if (diff >= 4) {
    _previousValue = currentValue;
    return diff >> 2;

  } else {
    return 0;
  }
}

void display_mode(Mode_e mode) {
  if (ledRingState.currentMode != Mode_e::ControlOff) {
#ifdef DEBUG
    debugSerial.println("Display mode");
#endif /*DEBUG*/

    for (size_t i = 0; i < 3; ++i) {
      ledRing.fill(0);
      ledRing.show();
      delay(300);
      for (Mode_e i = Mode_e::Init; i < ledRingState.currentMode; ++i) {
        ledRing.setPixelColor(int(i), Adafruit_NeoPixel::ColorHSV(
                                        0, 255, 16));
      }
      ledRing.show();
      delay(300);
    }
  }
}

void display_led_ring(void) {
  PresetState_t &preset = ledRingState.presetState;
  LedRingColorState_t ledRingColorState;

  if (ledRingState.currentMode == Mode_e::Preset) {
#ifdef DEBUG
    debugSerial.print("Preset : ");
    debugSerial.print(preset.index);
    debugSerial.print(", ");
    // debugSerial.print((preset.index == (PresetMax - 1)) ? 0 : UINT8_MAX);
    // debugSerial.print(", ");
    debugSerial.println(preset.level);
#endif
    ledRingColorState = {
      .ledOnCounter = LedCount / 4,
      .ledOnShift = 0,
      .hue = PresetHue[preset.index],
      .saturation = (preset.index == (PresetMax - 1)) ? 0 : UINT8_MAX,
      .brightness = PresetBrightness[preset.level]
    };
    refresh_led_ring(ledRingColorState);
  } else {
    refresh_led_ring();
  }

}

void refresh_led_ring(LedRingColorState_t const &ledRingColorState) {
#ifdef DEBUG
    debugSerial.print("Color : ");
    debugSerial.print(ledRingColorState.hue);
    debugSerial.print(", ");
    debugSerial.print(ledRingColorState.saturation);
    debugSerial.print(", ");
    debugSerial.println(ledRingColorState.brightness);
#endif
  ledRing.fill(0);
  for (size_t i = 0; i < ledRingColorState.ledOnCounter; ++i) {
    unsigned int ledId = i + ledRingColorState.ledOnShift;
    if (ledId >= LedCount) {
      ledId -= LedCount;
    }
    ledRing.setPixelColor(ledId, Adafruit_NeoPixel::ColorHSV(
                                     ledRingColorState.hue, ledRingColorState.saturation,
                                     ledRingColorState.brightness));
  }
  ledRing.show();
}