#include "InputBroker.h"
#include "PowerFSM.h" // needed for event trigger
#include "configuration.h"
#include "graphics/Screen.h"
#include "input/HapticFeedback.h"
#include "modules/ExternalNotificationModule.h"
#ifdef MESHTASTIC_LOCKDOWN
#include "security/LockdownDisplay.h"
#endif

#if ARCH_PORTDUINO
#include "input/LinuxInputImpl.h"
#include "input/SeesawRotary.h"
#include "platform/portduino/PortduinoGlue.h"
#endif

#if !MESHTASTIC_EXCLUDE_INPUTBROKER
#include "input/ExpressLRSFiveWay.h"
#include "input/RotaryEncoderImpl.h"
#include "input/RotaryEncoderInterruptImpl1.h"
#include "input/SerialKeyboardImpl.h"
#include "input/UpDownInterruptImpl1.h"
#include "input/i2cButton.h"
#if HAS_TRACKBALL
#include "input/TrackballInterruptImpl1.h"
#endif

#include "modules/StatusLEDModule.h"

#if !MESHTASTIC_EXCLUDE_I2C
#include "input/cardKbI2cImpl.h"
#endif
#include "input/kbMatrixImpl.h"
#endif

#if HAS_BUTTON || defined(ARCH_PORTDUINO)
#include "input/ButtonThread.h"

#if defined(BUTTON_PIN_TOUCH)
ButtonThread *TouchButtonThread = nullptr;
#if defined(PIN_EINK_EN)
static bool touchBacklightWasOn = false;
static bool touchBacklightActive = false;
#endif
#endif

#if defined(BUTTON_PIN) || defined(ARCH_PORTDUINO)
ButtonThread *UserButtonThread = nullptr;
#endif

#if defined(ALT_BUTTON_PIN)
ButtonThread *BackButtonThread = nullptr;
#endif

#if defined(CANCEL_BUTTON_PIN)
ButtonThread *CancelButtonThread = nullptr;
#endif

#endif

InputBroker *inputBroker = nullptr;

InputBroker::InputBroker()
{
#if defined(HAS_FREE_RTOS) && !defined(ARCH_RP2040)
    inputEventQueue = xQueueCreate(5, sizeof(InputEvent));
    pollSoonQueue = xQueueCreate(5, sizeof(InputPollable *));
    xTaskCreate(pollSoonWorker, "input-pollSoon", 2 * 1024, this, 10, &pollSoonTask);
#endif
}

void InputBroker::registerSource(Observable<const InputEvent *> *source)
{
    this->inputEventObserver.observe(source);
}

#if defined(HAS_FREE_RTOS) && !defined(ARCH_RP2040)
void InputBroker::requestPollSoon(InputPollable *pollable)
{
    if (xPortInIsrContext() == pdTRUE) {
        xQueueSendFromISR(pollSoonQueue, &pollable, NULL);
    } else {
        xQueueSend(pollSoonQueue, &pollable, 0);
    }
}

void InputBroker::queueInputEvent(const InputEvent *event)
{
    if (xPortInIsrContext() == pdTRUE) {
        xQueueSendFromISR(inputEventQueue, event, NULL);
    } else {
        xQueueSend(inputEventQueue, event, portMAX_DELAY);
    }
}

void InputBroker::processInputEventQueue()
{
    InputEvent event;
    while (xQueueReceive(inputEventQueue, &event, 0)) {
        handleInputEvent(&event);
    }
}
#endif

int InputBroker::handleInputEvent(const InputEvent *event)
{
#if HAS_SCREEN
    bool screenWasOff = false;
    if (screen) {
        screenWasOff = !screen->isScreenOn();
    }
#endif
    powerFSM.trigger(EVENT_INPUT);

    if (event && event->inputEvent != INPUT_BROKER_NONE && externalNotificationModule &&
        moduleConfig.external_notification.enabled && externalNotificationModule->nagging()) {
        externalNotificationModule->stopNow();
        // If this turns off a notification, don't further process the event
        return 0;
    }

#if HAS_SCREEN
    if (screen && screenWasOff) {
        // If the screen was off, it is in the process of turning on, and we just drop the event
        return 0;
    }
#endif

#ifdef MESHTASTIC_LOCKDOWN
    // Lockdown: when the display is redacted (storage locked, or screen-lock
    // latch set after idle) the screen content is hidden, but local input
    // would otherwise still flow into UI handlers — letting an operator
    // drive menus, fire canned messages, change settings etc. blind. Eat
    // the event here so input is no-op until the redaction clears.
    // The latch is cleared only by unlockScreen() on a successful
    // passphrase auth (see PhoneAPI::handleLockdownAuthInline) — local
    // input does not clear it, even if storage happens to be unlocked.
    // PowerFSM was already triggered above, so the backlight still wakes
    // to show the LOCKED frame — the input just doesn't act on anything.
    if (meshtastic_security::shouldRedactDisplay()) {
        return 0;
    }
#endif

    this->notifyObservers(event);
    return 0;
}

#if defined(HAS_FREE_RTOS) && !defined(ARCH_RP2040)
void InputBroker::pollSoonWorker(void *p)
{
    InputBroker *instance = (InputBroker *)p;
    while (true) {
        InputPollable *pollable = NULL;
        xQueueReceive(instance->pollSoonQueue, &pollable, portMAX_DELAY);
        if (pollable) {
            pollable->pollOnce();
        }
    }
    vTaskDelete(NULL);
}
#endif

void InputBroker::Init()
{

#ifdef BUTTON_PIN
#ifdef ARCH_ESP32

#if ESP_ARDUINO_VERSION_MAJOR >= 3
#ifdef BUTTON_NEED_PULLUP
    pinMode(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, INPUT_PULLUP);
#else
    pinMode(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, INPUT); // default to BUTTON_PIN
#endif
#else
    pinMode(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, INPUT); // default to BUTTON_PIN
#ifdef BUTTON_NEED_PULLUP
    gpio_pullup_en((gpio_num_t)(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN));
    delay(10);
#endif
#ifdef BUTTON_NEED_PULLUP2
    gpio_pullup_en((gpio_num_t)BUTTON_NEED_PULLUP2);
    delay(10);
#endif
#endif
#endif
#endif

// buttons are now inputBroker, so have to come after setupModules
#if HAS_BUTTON
    int pullup_sense = 0;
#ifdef INPUT_PULLUP_SENSE
    // Some platforms (nrf52) have a SENSE variant which allows wake from sleep - override what OneButton did
#ifdef BUTTON_SENSE_TYPE
    pullup_sense = BUTTON_SENSE_TYPE;
#else
    pullup_sense = INPUT_PULLUP_SENSE;
#endif
#endif
#if defined(ARCH_PORTDUINO)

    if (portduino_config.userButtonPin.enabled) {

        LOG_DEBUG("Use GPIO%02d for button", portduino_config.userButtonPin.pin);
        UserButtonThread = new ButtonThread("UserButton");
        if (screen) {
            ButtonConfig config;
            config.pinNumber = (uint8_t)portduino_config.userButtonPin.pin;
            config.activeLow = true;
            config.activePullup = true;
            config.pullupSense = INPUT_PULLUP;
            config.intRoutine = []() {
                UserButtonThread->userButton.tick();
                UserButtonThread->setIntervalFromNow(0);
                runASAP = true;
                BaseType_t higherWake = 0;
                concurrency::mainDelay.interruptFromISR(&higherWake);
            };
            config.singlePress = INPUT_BROKER_USER_PRESS;
            config.longPress = INPUT_BROKER_SELECT;
            UserButtonThread->initButton(config);
        }
    }
#endif

#ifdef BUTTON_PIN_TOUCH
    TouchButtonThread = new ButtonThread("BackButton");
    ButtonConfig touchConfig;
    touchConfig.pinNumber = BUTTON_PIN_TOUCH;
    touchConfig.activeLow = true;
    touchConfig.activePullup = true;
    touchConfig.pullupSense = pullup_sense;
    touchConfig.intRoutine = []() {
        TouchButtonThread->userButton.tick();
        TouchButtonThread->setIntervalFromNow(0);
        runASAP = true;
        BaseType_t higherWake = 0;
        concurrency::mainDelay.interruptFromISR(&higherWake);
    };
    touchConfig.singlePress = INPUT_BROKER_NONE;
    touchConfig.longPress = INPUT_BROKER_BACK;
#if defined(PIN_EINK_EN)
    // Touch pad drives the backlight on devices with e-ink backlight pin
    touchConfig.longPress = INPUT_BROKER_NONE;
    touchConfig.suppressLeadUpSound = true;
    touchConfig.onPress = []() {
        touchBacklightWasOn = uiconfig.screen_brightness == 1;
        if (!touchBacklightWasOn) {
            digitalWrite(PIN_EINK_EN, HIGH);
        }
        touchBacklightActive = true;
    };
    touchConfig.onRelease = []() {
        if (touchBacklightActive && !touchBacklightWasOn) {
            digitalWrite(PIN_EINK_EN, LOW);
        }
        touchBacklightActive = false;
    };
#endif
#if defined(HAPTIC_FEEDBACK_PIN)
    // Blip on touch, second blip when long-press fires (500 ms = touchConfig.longPressTime default).
    touchConfig.suppressLeadUpSound = true;
    initHapticFeedback();
    touchConfig.onPress = []() {
        hapticFeedback->pulse(80);
        hapticFeedback->armDelayedPulse(500, 80);
    };
    touchConfig.onRelease = []() { hapticFeedback->cancelDelayedPulse(); };
#endif
    TouchButtonThread->initButton(touchConfig);
#endif

#if defined(CANCEL_BUTTON_PIN)
    // Buttons. Moved here cause we need NodeDB to be initialized
    CancelButtonThread = new ButtonThread("CancelButton");
    ButtonConfig cancelConfig;
    cancelConfig.pinNumber = CANCEL_BUTTON_PIN;
    cancelConfig.activeLow = CANCEL_BUTTON_ACTIVE_LOW;
    cancelConfig.activePullup = CANCEL_BUTTON_ACTIVE_PULLUP;
    cancelConfig.pullupSense = pullup_sense;
    cancelConfig.intRoutine = []() {
        CancelButtonThread->userButton.tick();
        CancelButtonThread->setIntervalFromNow(0);
        runASAP = true;
        BaseType_t higherWake = 0;
        concurrency::mainDelay.interruptFromISR(&higherWake);
    };
    cancelConfig.singlePress = INPUT_BROKER_CANCEL;
    cancelConfig.longPress = INPUT_BROKER_SHUTDOWN;
    cancelConfig.longPressTime = 4000;
    CancelButtonThread->initButton(cancelConfig);
#endif

#if defined(ALT_BUTTON_PIN)
    // Buttons. Moved here cause we need NodeDB to be initialized
    BackButtonThread = new ButtonThread("BackButton");
    ButtonConfig backConfig;
    backConfig.pinNumber = ALT_BUTTON_PIN;
    backConfig.activeLow = ALT_BUTTON_ACTIVE_LOW;
    backConfig.activePullup = ALT_BUTTON_ACTIVE_PULLUP;
    backConfig.pullupSense = pullup_sense;
    backConfig.intRoutine = []() {
        BackButtonThread->userButton.tick();
        BackButtonThread->setIntervalFromNow(0);
        runASAP = true;
        BaseType_t higherWake = 0;
        concurrency::mainDelay.interruptFromISR(&higherWake);
    };
    backConfig.singlePress = INPUT_BROKER_ALT_PRESS;
    backConfig.longPress = INPUT_BROKER_ALT_LONG;
    backConfig.longPressTime = 500;
    BackButtonThread->initButton(backConfig);
#endif

#if defined(BUTTON_PIN)
#if defined(USERPREFS_BUTTON_PIN)
    int _pinNum = config.device.button_gpio ? config.device.button_gpio : USERPREFS_BUTTON_PIN;
#else
    int _pinNum = config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN;
#endif
#ifndef BUTTON_ACTIVE_LOW
#define BUTTON_ACTIVE_LOW true
#endif
#ifndef BUTTON_ACTIVE_PULLUP
#define BUTTON_ACTIVE_PULLUP true
#endif

    // Buttons. Moved here cause we need NodeDB to be initialized
    // If your variant.h has a BUTTON_PIN defined, go ahead and define BUTTON_ACTIVE_LOW and BUTTON_ACTIVE_PULLUP
    UserButtonThread = new ButtonThread("UserButton");
#if !MESHTASTIC_EXCLUDE_SCREEN
    if (screen) {
        ButtonConfig userConfig;
        userConfig.pinNumber = (uint8_t)_pinNum;
        userConfig.activeLow = BUTTON_ACTIVE_LOW;
        userConfig.activePullup = BUTTON_ACTIVE_PULLUP;
        userConfig.pullupSense = pullup_sense;
        userConfig.intRoutine = []() {
            UserButtonThread->userButton.tick();
            UserButtonThread->setIntervalFromNow(0);
            runASAP = true;
            BaseType_t higherWake = 0;
            concurrency::mainDelay.interruptFromISR(&higherWake);
        };
        userConfig.singlePress = INPUT_BROKER_USER_PRESS;
        userConfig.longPress = INPUT_BROKER_SELECT;
        userConfig.longPressTime = 500;
        userConfig.longLongPress = INPUT_BROKER_SHUTDOWN;
        UserButtonThread->initButton(userConfig);
    } else
#endif
    {
        ButtonConfig userConfigNoScreen;
        userConfigNoScreen.pinNumber = (uint8_t)_pinNum;
        userConfigNoScreen.activeLow = BUTTON_ACTIVE_LOW;
        userConfigNoScreen.activePullup = BUTTON_ACTIVE_PULLUP;
        userConfigNoScreen.pullupSense = pullup_sense;
        userConfigNoScreen.intRoutine = []() {
            UserButtonThread->userButton.tick();
            UserButtonThread->setIntervalFromNow(0);
            runASAP = true;
            BaseType_t higherWake = 0;
            concurrency::mainDelay.interruptFromISR(&higherWake);
        };
#if defined(ELECROW_ThinkNode_M7)
        userConfigNoScreen.longLongPressTime = 15 * 1000;
        userConfigNoScreen.longLongPress = INPUT_BROKER_FACTORY_RST;
#else
        userConfigNoScreen.longLongPress = INPUT_BROKER_SHUTDOWN;
#endif
        userConfigNoScreen.singlePress = INPUT_BROKER_USER_PRESS;
        userConfigNoScreen.longPress = INPUT_BROKER_NONE;
        userConfigNoScreen.longPressTime = 500;
        userConfigNoScreen.longLongPress = INPUT_BROKER_SHUTDOWN;
        userConfigNoScreen.doublePress = INPUT_BROKER_SEND_PING;
        userConfigNoScreen.triplePress = INPUT_BROKER_GPS_TOGGLE;
        UserButtonThread->initButton(userConfigNoScreen);
    }
#endif
#endif

#if (HAS_BUTTON || ARCH_PORTDUINO) && !MESHTASTIC_EXCLUDE_INPUTBROKER
    if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
#if defined(T_LORA_PAGER)
        // use a special FSM based rotary encoder version for T-LoRa Pager
        rotaryEncoderImpl = new RotaryEncoderImpl();
        if (!rotaryEncoderImpl->init()) {
            delete rotaryEncoderImpl;
            rotaryEncoderImpl = nullptr;
        }
#elif defined(INPUTDRIVER_ENCODER_TYPE) && (INPUTDRIVER_ENCODER_TYPE == 2)
        upDownInterruptImpl1 = new UpDownInterruptImpl1();
        if (!upDownInterruptImpl1->init()) {
            delete upDownInterruptImpl1;
            upDownInterruptImpl1 = nullptr;
        }
#else
        rotaryEncoderInterruptImpl1 = new RotaryEncoderInterruptImpl1();
        if (!rotaryEncoderInterruptImpl1->init()) {
            delete rotaryEncoderInterruptImpl1;
            rotaryEncoderInterruptImpl1 = nullptr;
        }
#endif
        cardKbI2cImpl = new CardKbI2cImpl();
        cardKbI2cImpl->init();
#if defined(M5STACK_UNITC6L)
        i2cButton = new i2cButtonThread("i2cButtonThread");
#endif
#ifdef INPUTBROKER_MATRIX_TYPE
        kbMatrixImpl = new KbMatrixImpl();
        kbMatrixImpl->init();
#endif // INPUTBROKER_MATRIX_TYPE
#ifdef INPUTBROKER_SERIAL_TYPE
        aSerialKeyboardImpl = new SerialKeyboardImpl();
        aSerialKeyboardImpl->init();
#endif // INPUTBROKER_MATRIX_TYPE
    }
#endif // HAS_BUTTON
#if ARCH_PORTDUINO
    if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
        if (portduino_config.i2cdev != "") {
            seesawRotary = new SeesawRotary("SeesawRotary");
            if (!seesawRotary->init()) {
                delete seesawRotary;
                seesawRotary = nullptr;
            }
        }
#ifdef __linux__
        // Linux evdev keyboard input only — macOS has no <linux/input.h>.
        aLinuxInputImpl = new LinuxInputImpl();
        aLinuxInputImpl->init();
#endif
    }
#endif
#if !MESHTASTIC_EXCLUDE_INPUTBROKER && HAS_TRACKBALL
    if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
        trackballInterruptImpl1 = new TrackballInterruptImpl1();
        trackballInterruptImpl1->init(TB_DOWN, TB_UP, TB_LEFT, TB_RIGHT, TB_PRESS);
    }
#endif
#ifdef INPUTBROKER_EXPRESSLRSFIVEWAY_TYPE
    expressLRSFiveWayInput = new ExpressLRSFiveWay();
#endif
}
