#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(LightOnMin)                                                             \
  MODE(LightOnMax)                                                             \
  MODE(ControlOff)                                                             \
  MODE(Preset)                                                                 \
  MODE(SetColor)                                                               \
  MODE(SetSaturation)                                                          \
  MODE(SetBrightness)

#define GENERATE_MODE_ENUM(ENUM) ENUM,

/* Generate Mode_e 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::Init;
  }
  return mode = static_cast<Mode_e>(static_cast<int>(mode) + 1);
}

#if defined(DEBUG)
/* Generate string array from Mode_e names */
#define GENERATE_MODE_STRING(STRING) #STRING,
char static const *mode_str[] = {FOREACH_MODE(GENERATE_MODE_STRING)};
#endif /* defined(DEBUG) */

/* Leds */
uint16_t static const LedCount{24};

/* Led ring color and led count state */
typedef struct {
  uint16_t ledOnMin;
  uint16_t ledOnMax;
  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};

/* Preset state */
typedef struct {
  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 = {.ledOnMin = 0,
                          .ledOnMax = LedCount / 2,
                          .hue = 0,
                          .saturation = 255,
                          .brightness = 16},
    .presetState = {.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 */
#if defined(DEBUG)
#if defined(ARDUINO_AVR_ATTINYX5)
#include <SoftwareSerial.h>
SoftwareSerial debugSerial{RxPin, TxPin};
#endif /* defined(ARDUINO_AVR_ATTINYX5) */
#if defined(ARDUINO_AVR_UNO)
#include <HardwareSerial.h>
HardwareSerial &debugSerial = Serial;
#endif /* defined(ARDUINO_AVR_UNO) */
#endif /* defined(DEBUG) */

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

/* Generates state machine handlers for each Mode_e */
#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);

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

  analog_button_calibration();

  get_encoder_shift();

#if defined(DEBUG)
  debugSerial.println(mode_str[static_cast<int>(ledRingState.currentMode)]);
#endif /* defined(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;

#if defined(DEBUG)
    debugSerial.println(mode_str[static_cast<int>(ledRingState.currentMode)]);
#endif /* defined(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::SetColor:
      SetColor_execute(shift);
      break;

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

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

    case Mode_e::LightOnMin:
      LightOnMin_execute(shift);
      break;

    case Mode_e::LightOnMax:
      LightOnMax_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 LightOnMin_execute(int shift) {
  uint16_t &ledOnMin = ledRingState.ledRingColorState.ledOnMin;
  uint16_t &ledOnMax = ledRingState.ledRingColorState.ledOnMax;

  if (shift > 0) {
    if (ledOnMin == ledOnMax) {
      ledRingState.currentMode = Mode_e::LightOnMax;
      ++ledOnMax;
    } else {
      ++ledOnMin;
      if (ledOnMin == LedCount) {
        ledOnMin = 0;
      }
    }
  } else {
    if (ledOnMin == ledOnMax) {
      ledRingState.currentMode = Mode_e::LightOnMax;
      --ledOnMax;
    } else {
      --ledOnMin;
      if (ledOnMin == UINT16_MAX) {
        ledOnMin = LedCount - 1;
      }
    }
  }

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

void LightOnMax_execute(int shift) {
  uint16_t &ledOnMin = ledRingState.ledRingColorState.ledOnMin;
  uint16_t &ledOnMax = ledRingState.ledRingColorState.ledOnMax;

  if (shift > 0) {
    if (ledOnMax == ledOnMin) {
      ledRingState.currentMode = Mode_e::LightOnMin;
      ++ledOnMin;
    } else {
      ++ledOnMax;
      if (ledOnMax == LedCount) {
        ledOnMax = 0;
      }
    }
  } else {
    if (ledOnMax == ledOnMin) {
      ledRingState.currentMode = Mode_e::LightOnMin;
      --ledOnMin;
    } else {
      --ledOnMax;
      if (ledOnMax == UINT16_MAX) {
        ledOnMax = LedCount - 1;
      }
    }
  }

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

/* Others functions */

void analog_button_calibration(void) {
  int highRef, lowRef;

#if defined(DEBUG) && false
  debugSerial.print(__func__);
#endif /* defined(DEBUG) */

  do {
    highRef = analogRead(AnalogButtonPin);
    delay(300);
  } while (highRef < 1000);

#if defined(DEBUG) && false
  debugSerial.println("highRef=");
  debugSerial.println(highRef);
#endif /* defined(DEBUG) */

  lowRef = analogRead(AnalogButtonPin);
  while (highRef - lowRef < 50) {
    lowRef = analogRead(AnalogButtonPin);
    delay(300);
  }

#if defined(DEBUG) && false
  debugSerial.println("lowRef=");
  debugSerial.println(lowRef);
#endif /* defined(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) {
    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) {
#if defined(DEBUG) && false
    debugSerial.print("Preset index,level=");
    debugSerial.print(preset.index);
    debugSerial.print(",");
    debugSerial.print(preset.level);
    debugSerial.println();
#endif /* defined(DEBUG) */
    ledRingColorState = {.ledOnMin = ledRingState.ledRingColorState.ledOnMin,
                         .ledOnMax = ledRingState.ledRingColorState.ledOnMax,
                         .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) {
  uint16_t const &ledOnMin = ledRingColorState.ledOnMin;
  uint16_t const &ledOnMax = ledRingColorState.ledOnMax;
  uint16_t LedOnCount = 0;

#if defined(DEBUG)
  debugSerial.print("Color hue,sat,bri=");
  debugSerial.print(ledRingColorState.hue);
  debugSerial.print(",");
  debugSerial.print(ledRingColorState.saturation);
  debugSerial.print(",");
  debugSerial.println(ledRingColorState.brightness);
#endif /* defined(DEBUG) */

  /* reset ring */
  ledRing.fill(0);

  /* compute number of led on */
  if (ledOnMin <= ledOnMax) {
    LedOnCount = ledOnMax - ledOnMin;
  } else {
    LedOnCount = LedCount - (ledOnMax - ledOnMax);
  }

#if defined(DEBUG)
  debugSerial.print("Led min,max,count=");
  debugSerial.print(ledOnMin);
  debugSerial.print(",");
  debugSerial.print(ledOnMax);
  debugSerial.print(",");
  debugSerial.println(LedOnCount);
#endif /* defined(DEBUG) */

  for (size_t i = ledRingColorState.ledOnMin;
       i < ledRingColorState.ledOnMin + LedOnCount; ++i) {
    unsigned int ledId = i;
    if (ledId >= LedCount) {
      ledId -= LedCount;
    }
    ledRing.setPixelColor(
        ledId, Adafruit_NeoPixel::ColorHSV(ledRingColorState.hue,
                                           ledRingColorState.saturation,
                                           ledRingColorState.brightness));
  }
  ledRing.show();
}