/*
 * EMS-ESP - https://github.com/emsesp/EMS-ESP
 * Copyright 2020-2025  emsesp.org
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "led.h"
#include "emsesp.h"

namespace emsesp {

uuid::log::Logger LED::logger_{F_(led), uuid::log::Facility::KERN};

// initialise the LED, fetching the settings from the WebSettingsService
// set the LED to on or off when in normal operating mode
void LED::init() {
    // copy the application settings
    EMSESP::webSettingsService.read([&](WebSettings & settings) {
        led_gpio_ = settings.led_gpio;
        led_type_ = settings.led_type;
        hide_led_ = settings.hide_led;
    });

    if (!led_gpio_) { // 0 means disabled
        LOG_INFO("LED disabled");
        return;
    }

    // for safety
    if (!led_type_) {
        pinMode(led_gpio_, OUTPUT);
    }

    reset_led(); // start with LED in default state, depending on if it's hidden or not
}

// handle LED routine
// called from the System::loop()
// returns true if the LED flash is active, i.e its a lock down state
bool LED::loop(uint8_t healthcheck, bool button_busy) {
    // if LED flashing is active it means its about to perform a factory reset, so don't do anything else and keep it flashing
    if (led_fast_flash_timer_) {
        led_fast_flash();
        return true;
    }

    // the user-requested LED blink always has preference
    if (!is_user_led_blink_) {
        // check for button press.
        // if button is pressed, show LED (yellow on RGB LED, on/off on standard LED)
        // it will turn off on the next loop cycle
        if (last_button_busy_ != button_busy) {
            last_button_busy_ = button_busy;
            set_led(button_busy ? Color::OFF : Color::YELLOW); // Yellow
            return false;
        }

        // check the system health.
        // Set the sequence accordingly, only if the healthcheck is not 0 and has changed
        if (healthcheck != previous_healthcheck_) {
            previous_healthcheck_ = healthcheck;
            color_steps_[0]       = Color::OFF;
            color_steps_[1]       = Color::OFF;
            color_steps_[2]       = Color::OFF;

            // if the healthcheck is 0, i.e. system is healthy, reset the LED
            if (healthcheck == 0) {
                reset_led();
                return false;
            }

            //  1 flash (blue) is the EMS bus is not connected
            //  2 flashes (red, red) if the network (wifi or ethernet) is not connected
            //  3 flashes (red, red, blue) is both the bus and the network are not connected
            bool no_network = (healthcheck & System::HEALTHCHECK_NO_NETWORK) == System::HEALTHCHECK_NO_NETWORK;
            bool no_bus     = (healthcheck & System::HEALTHCHECK_NO_BUS) == System::HEALTHCHECK_NO_BUS;

            // set step 1
            if (no_network) {
                color_steps_[0] = Color::RED; // red, no network
            } else if (no_bus) {
                color_steps_[0] = Color::BLUE; // blue, no bus
            }

            // set step 2
            if (no_network) {
                color_steps_[1] = Color::RED; // red, no network
            }

            // set step 3
            if (no_network && no_bus) {
                color_steps_[2] = Color::BLUE; // blue, no network and no bus
            }
        }
    }

    // show the LED status based on the healthcheck and button busy status
    sequence_led();

    return false;
}

// turn the LED back it's default state depending on if it's hidden or not
void LED::reset_led() {
    is_user_led_blink_ = false;
    set_led(hide_led_ ? Color::OFF : Color::GREEN); // Green
    color_steps_[0] = Color::OFF;
    color_steps_[1] = Color::OFF;
    color_steps_[2] = Color::OFF;
}

// LED flash every few ms and then perform a factory reset
void LED::led_fast_flash() {
    uint32_t current_time = uuid::get_uptime();

    if (current_time - last_toggle_time_ >= LED_FLASH_INTERVAL_MS) {
        led_flash_state_  = !led_flash_state_;
        last_toggle_time_ = current_time;
        set_led(led_flash_state_ ? Color::YELLOW : Color::OFF); // Yellow
    }

    // after duration, turn off the LED
    if (current_time - led_flash_start_time_ >= led_flash_duration_) {
        set_led(Color::OFF);
        led_fast_flash_timer_ = false;
#ifndef EMSESP_DEBUG
        System::command_format(nullptr, 0); // Execute format operation, unless in debug mode
#endif
    }
}

// set LED on/off or RGB color
// ignores whether the LED is hidden or not (if hide_led_ is set)
void LED::set_led(Color color) {
    if (!led_gpio_) {
        return;
    }

    // RGB lookup table indexed by Color enum (must match enum order in led.h)
    static constexpr uint8_t B              = RGB_LED_BRIGHTNESS;
    static constexpr uint8_t H              = RGB_LED_BRIGHTNESS / 2;
    static constexpr uint8_t rgb_table[][3] = {
        {0, 0, 0}, // OFF
        {B, B, B}, // ON (white)
        {B, 0, 0}, // RED
        {0, B, 0}, // GREEN
        {0, 0, B}, // BLUE
        {B, B, 0}, // YELLOW
        {B, H, 0}, // ORANGE
        {0, B, B}, // CYAN
        {H, 0, H}  // PINK
    };

    const uint8_t color_idx = static_cast<uint8_t>(color);
    const uint8_t idx       = (color_idx < sizeof(rgb_table) / sizeof(rgb_table[0])) ? color_idx : static_cast<uint8_t>(Color::OFF);
    const uint8_t red       = rgb_table[idx][0];
    const uint8_t green     = rgb_table[idx][1];
    const uint8_t blue      = rgb_table[idx][2];

    if (led_type_) {
        rgbLedWrite(led_gpio_, red, green, blue);
    } else {
        digitalWrite(led_gpio_, (red == 0 && green == 0 && blue == 0) || color == Color::OFF ? !LED_ON : LED_ON);
    }
}

// set LED custom routine
// For example: /api/system/led?data=red:blink1
// For older non-RGB models, the colour would default to just being on.
bool LED::set_custom_led_routine(std::string color, std::string pattern) {
    static constexpr struct {
        const char * name;
        Color        value;
    } color_map[] = {
        {"off", Color::OFF},
        {"on", Color::ON},
        {"white", Color::ON},
        {"red", Color::RED},
        {"green", Color::GREEN},
        {"blue", Color::BLUE},
        {"yellow", Color::YELLOW},
        {"orange", Color::ORANGE},
        {"cyan", Color::CYAN},
        {"pink", Color::PINK},
    };

    Color color_type    = Color::OFF;
    bool  color_matched = false;
    for (const auto & entry : color_map) {
        if (color == entry.name) {
            color_type    = entry.value;
            color_matched = true;
            break;
        }
    }
    if (!color_matched) {
        return false;
    }

    // reset the color steps
    color_steps_[0] = Color::OFF;
    color_steps_[1] = Color::OFF;
    color_steps_[2] = Color::OFF;

    // blink patterns
    if (pattern == "blink1") {
        color_steps_[0] = color_type;
    } else if (pattern == "blink2") {
        color_steps_[0] = color_type;
        color_steps_[1] = color_type;
    } else if (pattern == "blink3") {
        color_steps_[0] = color_type;
        color_steps_[1] = color_type;
        color_steps_[2] = color_type;

        // special patterns, ignores the user color
    } else if (pattern == "rgb") {
        color_steps_[0] = Color::RED;
        color_steps_[1] = Color::GREEN;
        color_steps_[2] = Color::BLUE;
    } else if (pattern == "cpc") {
        color_steps_[0] = Color::CYAN;
        color_steps_[1] = Color::PINK;
        color_steps_[2] = Color::CYAN;
    } else {
        return false; // pattern not recognized
    }

    is_user_led_blink_ = true; // user routine is active

    // when this is called we want the sequence_led to restart immediately and skip the long pause
    led_long_timer_ = uuid::get_uptime() + HEALTHCHECK_LED_FLASH_FAST_DURATION + 200UL;

    return true;
}

// uses LED to show system health and user-requested LED blinks
// it works in a batch of 3 configured flashes, then a long pause
// the timing is different for user-requested LED blink and for system healthcheck
void LED::sequence_led() {
    // first long pause before we start flashing
    auto current_time = uuid::get_uptime();
    if (led_long_timer_
        && (uint32_t)(current_time - led_long_timer_) >= (is_user_led_blink_ ? HEALTHCHECK_LED_LONG_FAST_DURATION : HEALTHCHECK_LED_LONG_DURATION)) {
        led_short_timer_ = current_time; // start the short timer
        led_long_timer_  = 0;            // stop long timer
        led_flash_step_  = 1;            // enable the short flash timer
    }

    // the flash timer which starts after the long pause
    if (led_flash_step_
        && (uint32_t)(current_time - led_short_timer_) >= (is_user_led_blink_ ? HEALTHCHECK_LED_FLASH_FAST_DURATION : HEALTHCHECK_LED_FLASH_DURATION)) {
        led_long_timer_  = 0; // stop the long timer
        led_short_timer_ = current_time;

        if (++led_flash_step_ == 8) {
            // finished first iteration, reset the whole sequence, turn off LED
            led_long_timer_ = uuid::get_uptime();
            led_flash_step_ = 0;
            set_led(Color::OFF); // turn off the LED

            // if we're running a user-requested LED blink, turn it off and go back to the healthcheck sequence
            if (is_user_led_blink_) {
                is_user_led_blink_    = false;
                previous_healthcheck_ = System::HEALTHCHECK_RESET; // this will force the healthcheck to be checked again
            }
            return;
        }

        if (led_flash_step_ % 2) {
            // handle the three step events (on odd numbers 3,5,7 etc). see if we need to set a LED color
            switch (led_flash_step_) {
            case 3: // first flash
                set_led(color_steps_[0]);
                break;
            case 5: // second flash
                set_led(color_steps_[1]);
                break;
            case 7: // third flash
                set_led(color_steps_[2]);
                break;
            default:
                break;
            }
        } else {
            set_led(Color::OFF); // turn off on even number count, to make it flash
        }
    }
}

// Start the LED flash timer - duration in seconds
void LED::start_led_fast_flash(uint8_t duration) {
    // Don't start if already running
    if (led_fast_flash_timer_) {
        return;
    }

    // Reset counter and state
    led_flash_start_time_ = uuid::get_uptime();        // current time
    led_flash_duration_   = (uint32_t)duration * 1000; // duration in milliseconds
    led_fast_flash_timer_ = true;                      // it's active
}

} // namespace emsesp
