#include "wled.h"

/*
 * Physical IO
 */

#define WLED_DEBOUNCE_THRESHOLD      50 // only consider button input of at least 50ms as valid (debouncing)
#define WLED_LONG_PRESS             600 // long press if button is released after held for at least 600ms
#define WLED_DOUBLE_PRESS           350 // double press if another press within 350ms after a short press
#define WLED_LONG_REPEATED_ACTION   400 // how often a repeated action (e.g. dimming) is fired on long press on button IDs >0
#define WLED_LONG_AP               5000 // how long button 0 needs to be held to activate WLED-AP
#define WLED_LONG_FACTORY_RESET   10000 // how long button 0 needs to be held to trigger a factory reset
#define WLED_LONG_BRI_STEPS          16 // how much to increase/decrease the brightness with each long press repetition

static const char _mqtt_topic_button[] PROGMEM = "%s/button/%d";  // optimize flash usage
static bool buttonBriDirection = false; // true: increase brightness, false: decrease brightness

void shortPressAction(uint8_t b)
{
  if (!buttons[b].macroButton) {
    switch (b) {
      case 0: toggleOnOff(); stateUpdated(CALL_MODE_BUTTON); break;
      case 1: ++effectCurrent %= strip.getModeCount(); stateChanged = true; colorUpdated(CALL_MODE_BUTTON); break;
    }
  } else {
    applyPreset(buttons[b].macroButton, CALL_MODE_BUTTON_PRESET);
  }

#ifndef WLED_DISABLE_MQTT
  // publish MQTT message
  if (buttonPublishMqtt && WLED_MQTT_CONNECTED) {
    char subuf[MQTT_MAX_TOPIC_LEN + 32];
    sprintf_P(subuf, _mqtt_topic_button, mqttDeviceTopic, (int)b);
    mqtt->publish(subuf, 0, false, "short");
  }
#endif
}

void longPressAction(uint8_t b)
{
  if (!buttons[b].macroLongPress) {
    switch (b) {
      case 0: setRandomColor(colPri); colorUpdated(CALL_MODE_BUTTON); break;
      case 1: 
        if(buttonBriDirection) {
          if (bri == 255) break; // avoid unnecessary updates to brightness
          if (bri >= 255 - WLED_LONG_BRI_STEPS) bri = 255;
          else bri += WLED_LONG_BRI_STEPS;
        } else {
          if (bri == 1) break; // avoid unnecessary updates to brightness
          if (bri <= WLED_LONG_BRI_STEPS) bri = 1;
          else bri -= WLED_LONG_BRI_STEPS;
        }
        stateUpdated(CALL_MODE_BUTTON); 
        buttons[b].pressedTime = millis();         
        break; // repeatable action
    }
  } else {
    applyPreset(buttons[b].macroLongPress, CALL_MODE_BUTTON_PRESET);
  }

#ifndef WLED_DISABLE_MQTT
  // publish MQTT message
  if (buttonPublishMqtt && WLED_MQTT_CONNECTED) {
    char subuf[MQTT_MAX_TOPIC_LEN + 32];
    sprintf_P(subuf, _mqtt_topic_button, mqttDeviceTopic, (int)b);
    mqtt->publish(subuf, 0, false, "long");
  }
#endif
}

void doublePressAction(uint8_t b)
{
  if (!buttons[b].macroDoublePress) {
    switch (b) {
      //case 0: toggleOnOff(); colorUpdated(CALL_MODE_BUTTON); break; //instant short press on button 0 if no macro set
      case 1: ++effectPalette %= getPaletteCount(); colorUpdated(CALL_MODE_BUTTON); break;
    }
  } else {
    applyPreset(buttons[b].macroDoublePress, CALL_MODE_BUTTON_PRESET);
  }

#ifndef WLED_DISABLE_MQTT
  // publish MQTT message
  if (buttonPublishMqtt && WLED_MQTT_CONNECTED) {
    char subuf[MQTT_MAX_TOPIC_LEN + 32];
    sprintf_P(subuf, _mqtt_topic_button, mqttDeviceTopic, (int)b);
    mqtt->publish(subuf, 0, false, "double");
  }
#endif
}

bool isButtonPressed(uint8_t b)
{
  if (buttons[b].pin < 0) return false;
  unsigned pin = buttons[b].pin;

  switch (buttons[b].type) {
    case BTN_TYPE_NONE:
    case BTN_TYPE_RESERVED:
      break;
    case BTN_TYPE_PUSH:
    case BTN_TYPE_SWITCH:
      if (digitalRead(pin) == LOW) return true;
      break;
    case BTN_TYPE_PUSH_ACT_HIGH:
    case BTN_TYPE_PIR_SENSOR:
      if (digitalRead(pin) == HIGH) return true;
      break;
    case BTN_TYPE_TOUCH:
    case BTN_TYPE_TOUCH_SWITCH:
      #if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32C3)
        #ifdef SOC_TOUCH_VERSION_2 //ESP32 S2 and S3 provide a function to check touch state (state is updated in interrupt)
        if (touchInterruptGetLastStatus(pin)) return true;
        #else
        if (digitalPinToTouchChannel(pin) >= 0 && touchRead(pin) <= touchThreshold) return true;
        #endif
      #endif
     break;
  }
  return false;
}

void handleSwitch(uint8_t b)
{
  // isButtonPressed() handles inverted/noninverted logic
  if (buttons[b].pressedBefore != isButtonPressed(b)) {
    DEBUG_PRINTF_P(PSTR("Switch: State changed %u\n"), b);
    buttons[b].pressedTime = millis();
    buttons[b].pressedBefore = !buttons[b].pressedBefore; // toggle pressed state
  }

  if (buttons[b].longPressed == buttons[b].pressedBefore) return;

  if (millis() - buttons[b].pressedTime > WLED_DEBOUNCE_THRESHOLD) { //fire edge event only after 50ms without change (debounce)
    DEBUG_PRINTF_P(PSTR("Switch: Activating  %u\n"), b);
    if (!buttons[b].pressedBefore) { // on -> off
      DEBUG_PRINTF_P(PSTR("Switch: On -> Off (%u)\n"), b);
      if (buttons[b].macroButton) applyPreset(buttons[b].macroButton, CALL_MODE_BUTTON_PRESET);
      else { //turn on
        if (!bri) {toggleOnOff(); stateUpdated(CALL_MODE_BUTTON);}
      }
    } else {  // off -> on
      DEBUG_PRINTF_P(PSTR("Switch: Off -> On (%u)\n"), b);
      if (buttons[b].macroLongPress) applyPreset(buttons[b].macroLongPress, CALL_MODE_BUTTON_PRESET);
      else { //turn off
        if (bri) {toggleOnOff(); stateUpdated(CALL_MODE_BUTTON);}
      }
    }

#ifndef WLED_DISABLE_MQTT
    // publish MQTT message
    if (buttonPublishMqtt && WLED_MQTT_CONNECTED) {
      char subuf[MQTT_MAX_TOPIC_LEN + 32];
      if (buttons[b].type == BTN_TYPE_PIR_SENSOR) sprintf_P(subuf, PSTR("%s/motion/%d"), mqttDeviceTopic, (int)b);
      else sprintf_P(subuf, _mqtt_topic_button, mqttDeviceTopic, (int)b);
      mqtt->publish(subuf, 0, false, !buttons[b].pressedBefore ? "off" : "on");
    }
#endif

    buttons[b].longPressed = buttons[b].pressedBefore; //save the last "long term" switch state
  }
}

#define ANALOG_BTN_READ_CYCLE 250   // min time between two analog reading cycles
#define STRIP_WAIT_TIME 6           // max wait time in case of strip.isUpdating()
#define POT_SMOOTHING 0.25f         // smoothing factor for raw potentiometer readings
#define POT_SENSITIVITY 4           // changes below this amount are noise (POT scratching, or ADC noise)

void handleAnalog(uint8_t b)
{
  static uint8_t oldRead[WLED_MAX_BUTTONS] = {0};
  static float filteredReading[WLED_MAX_BUTTONS] = {0.0f};
  unsigned rawReading;    // raw value from analogRead, scaled to 12bit

  DEBUG_PRINTF_P(PSTR("Analog: Reading button %u\n"), b);

  #ifdef ESP8266
  rawReading = analogRead(A0) << 2;   // convert 10bit read to 12bit
  #else
  if ((buttons[b].pin < 0) /*|| (digitalPinToAnalogChannel(buttons[b].pin) < 0)*/) return; // pin must support analog ADC - newer esp32 frameworks throw lots of warnings otherwise
  rawReading = analogRead(buttons[b].pin); // collect at full 12bit resolution
  #endif
  yield();                            // keep WiFi task running - analog read may take several millis on ESP8266

  filteredReading[b] += POT_SMOOTHING * ((float(rawReading) / 16.0f) - filteredReading[b]); // filter raw input, and scale to [0..255]
  unsigned aRead = max(min(int(filteredReading[b]), 255), 0);                               // squash into 8bit
  if (aRead <= POT_SENSITIVITY) aRead = 0;                                                   // make sure that 0 and 255 are used
  if (aRead >= 255-POT_SENSITIVITY) aRead = 255;

  if (buttons[b].type == BTN_TYPE_ANALOG_INVERTED) aRead = 255 - aRead;

  // remove noise & reduce frequency of UI updates
  if (abs(int(aRead) - int(oldRead[b])) <= POT_SENSITIVITY) return;  // no significant change in reading

  DEBUG_PRINTF_P(PSTR("Analog: Raw = %u\n"), rawReading);
  DEBUG_PRINTF_P(PSTR(" Filtered = %u\n"), aRead);

  // Unomment the next lines if you still see flickering related to potentiometer
  // This waits until strip finishes updating (why: strip was not updating at the start of handleButton() but may have started during analogRead()?)
  //unsigned long wait_started = millis();
  //while(strip.isUpdating() && (millis() - wait_started < STRIP_WAIT_TIME)) {
  //  delay(1);
  //}

  oldRead[b] = aRead;

  // if no macro for "short press" and "long press" is defined use brightness control
  if (!buttons[b].macroButton && !buttons[b].macroLongPress) {
    DEBUG_PRINTF_P(PSTR("Analog: Action = %u\n"), buttons[b].macroDoublePress);
    // if "double press" macro defines which option to change
    if (buttons[b].macroDoublePress >= 250) {
      // global brightness
      if (aRead == 0) {
        briLast = bri;
        bri = 0;
      } else {
        if (bri == 0) strip.restartRuntime();
        bri = aRead;
      }
    } else if (buttons[b].macroDoublePress == 249) {
      // effect speed
      effectSpeed = aRead;
    } else if (buttons[b].macroDoublePress == 248) {
      // effect intensity
      effectIntensity = aRead;
    } else if (buttons[b].macroDoublePress == 247) {
      // selected palette
      effectPalette = map(aRead, 0, 252, 0, getPaletteCount()-1);
      effectPalette = constrain(effectPalette, 0, getPaletteCount()-1);  // map is allowed to "overshoot", so we need to contrain the result
    } else if (buttons[b].macroDoublePress == 200) {
      // primary color, hue, full saturation
      colorHStoRGB(aRead*256, 255, colPri);
    } else {
      // otherwise use "double press" for segment selection
      Segment& seg = strip.getSegment(buttons[b].macroDoublePress);
      if (aRead == 0) {
        seg.on = false; // do not use transition
        //seg.setOption(SEG_OPTION_ON, false); // off (use transition)
      } else {
        seg.opacity = aRead; // set brightness (opacity) of segment
        seg.on = true;
        //seg.setOpacity(aRead);
        //seg.setOption(SEG_OPTION_ON, true); // on (use transition)
      }
      // this will notify clients of update (websockets,mqtt,etc)
      updateInterfaces(CALL_MODE_BUTTON);
    }
  } else {
    DEBUG_PRINTLN(F("Analog: No action"));
    //TODO:
    // we can either trigger a preset depending on the level (between short and long entries)
    // or use it for RGBW direct control
  }
  colorUpdated(CALL_MODE_BUTTON);
}

void handleButton()
{
  static unsigned long lastAnalogRead = 0UL;
  static unsigned long lastRun = 0UL;
  unsigned long now = millis();

  if (strip.isUpdating() && (now - lastRun < ANALOG_BTN_READ_CYCLE+1)) return; // don't interfere with strip update (unless strip is updating continuously, e.g. very long strips)
  lastRun = now;

  for (unsigned b = 0; b < buttons.size(); b++) {
    #ifdef ESP8266
    if ((buttons[b].pin < 0 && !(buttons[b].type == BTN_TYPE_ANALOG || buttons[b].type == BTN_TYPE_ANALOG_INVERTED)) || buttons[b].type == BTN_TYPE_NONE) continue;
    #else
    if (buttons[b].pin < 0 || buttons[b].type == BTN_TYPE_NONE) continue;
    #endif

    if (UsermodManager::handleButton(b)) continue; // did usermod handle buttons

    if (buttons[b].type == BTN_TYPE_ANALOG || buttons[b].type == BTN_TYPE_ANALOG_INVERTED) { // button is not a button but a potentiometer
      if (now - lastAnalogRead > ANALOG_BTN_READ_CYCLE) {
        handleAnalog(b);
      }
      continue;
    }

    // button is not momentary, but switch. This is only suitable on pins whose on-boot state does not matter (NOT gpio0)
    if (buttons[b].type == BTN_TYPE_SWITCH || buttons[b].type == BTN_TYPE_TOUCH_SWITCH || buttons[b].type == BTN_TYPE_PIR_SENSOR) {
      handleSwitch(b);
      continue;
    }

    // momentary button logic
    if (isButtonPressed(b)) { // pressed

      // if all macros are the same, fire action immediately on rising edge
      if (buttons[b].macroButton && buttons[b].macroButton == buttons[b].macroLongPress && buttons[b].macroButton == buttons[b].macroDoublePress) {
        if (!buttons[b].pressedBefore) shortPressAction(b);
        buttons[b].pressedBefore = true;
        buttons[b].pressedTime = now; // continually update (for debouncing to work in release handler)
        continue;
      }

      if (!buttons[b].pressedBefore) buttons[b].pressedTime = now;
      buttons[b].pressedBefore = true;

      if (now - buttons[b].pressedTime > WLED_LONG_PRESS) { //long press
        if (!buttons[b].longPressed) {
          buttonBriDirection = !buttonBriDirection; //toggle brightness direction on long press
          longPressAction(b);
        } else if (b) { //repeatable action (~5 times per s) on button > 0
          longPressAction(b);
          buttons[b].pressedTime = now - WLED_LONG_REPEATED_ACTION; //200ms
        }
        buttons[b].longPressed = true;
      }

    } else if (buttons[b].pressedBefore) { //released
      long dur = now - buttons[b].pressedTime;

      // released after rising-edge short press action
      if (buttons[b].macroButton && buttons[b].macroButton == buttons[b].macroLongPress && buttons[b].macroButton == buttons[b].macroDoublePress) {
        if (dur > WLED_DEBOUNCE_THRESHOLD) buttons[b].pressedBefore = false; // debounce, blocks button for 50 ms once it has been released
        continue;
      }

      if (dur < WLED_DEBOUNCE_THRESHOLD) {buttons[b].pressedBefore = false; continue;} // too short "press", debounce
      bool doublePress = buttons[b].waitTime; //did we have a short press before?
      buttons[b].waitTime = 0;

      if (b == 0 && dur > WLED_LONG_AP) { // long press on button 0 (when released)
        if (dur > WLED_LONG_FACTORY_RESET) { // factory reset if pressed > 10 seconds
          WLED_FS.format();
          doReboot = true;
        } else {
          WLED::instance().initAP(true);
        }
      } else if (!buttons[b].longPressed) { //short press
        //NOTE: this interferes with double click handling in usermods so usermod needs to implement full button handling
        if (b != 1 && !buttons[b].macroDoublePress) { //don't wait for double press on buttons without a default action if no double press macro set
          shortPressAction(b);
        } else { //double press if less than 350 ms between current press and previous short press release (buttonWaitTime!=0)
          if (doublePress) {
            doublePressAction(b);
          } else {
            buttons[b].waitTime = now;
          }
        }
      }
      buttons[b].pressedBefore = false;
      buttons[b].longPressed = false;
    }

    //if 350ms elapsed since last short press release it is a short press
    if (buttons[b].waitTime && now - buttons[b].waitTime > WLED_DOUBLE_PRESS && !buttons[b].pressedBefore) {
      buttons[b].waitTime = 0;
      shortPressAction(b);
    }
  }
  if (now - lastAnalogRead > ANALOG_BTN_READ_CYCLE) {
    lastAnalogRead = now;
  }
}

// handleIO() happens *after* handleTransitions() (see wled.cpp) which may change bri/briT but *before* strip.service()
// where actual LED painting occurrs
// this is important for relay control and in the event of turning off on-board LED
void handleIO()
{
  handleButton();

  // if we want to control on-board LED (ESP8266) or relay we have to do it here as the final show() may not happen until
  // next loop() cycle
  handleOnOff();
}

void handleOnOff(bool forceOff)
{
  if (strip.getBrightness() && !forceOff) {
    lastOnTime = millis();
    if (offMode) {
      BusManager::on();
      if (rlyPin>=0) {
        // note: pinMode is set in first call to handleOnOff(true) in beginStrip()
        digitalWrite(rlyPin, rlyMde); // set to on state
        delay(RELAY_DELAY); // let power stabilize before sending LED data (#346 #812 #3581 #3955)
      }
      offMode = false;
    }
  } else if ((millis() - lastOnTime > 600 && !strip.needsUpdate()) || forceOff) {
    // for turning LED or relay off we need to wait until strip no longer needs updates (strip.trigger())
    if (!offMode) {
      BusManager::off();
      if (rlyPin>=0) {
        digitalWrite(rlyPin, !rlyMde); // set output before disabling high-z state to avoid output glitches
        pinMode(rlyPin, rlyOpenDrain ? OUTPUT_OPEN_DRAIN : OUTPUT);
      }
      offMode = true;
    }
  }
}

void IRAM_ATTR touchButtonISR()
{
  // used for ESP32 S2 and S3: nothing to do, ISR is just used to update registers of HAL driver
}
