#include "config.h"

#include "manager.hpp"

#include <phosphor-logging/lg2.hpp>
#include <sdbusplus/exception.hpp>
#include <xyz/openbmc_project/Led/Physical/server.hpp>

#include <algorithm>
#include <iostream>
#include <string>
namespace phosphor
{
namespace led
{

// apply the led action to the map
static void applyGroupAction(std::map<LedName, Layout::LedAction>& newState,
                             Layout::LedAction action)
{
    if (!newState.contains(action.name))
    {
        newState[action.name] = action;
        return;
    }

    auto currentAction = newState[action.name];

    const bool hasPriority = currentAction.priority.has_value();

    if (hasPriority && currentAction.action == action.priority)
    {
        // if the current action is already the priority action,
        // we cannot override it
        return;
    }

    newState[action.name] = action;
}

// create the resulting new map from all currently asserted groups
static auto getNewMapWithGroupPriorities(
    std::set<const Layout::GroupLayout*, Layout::CompareGroupLayout> sorted)
    -> std::map<LedName, Layout::LedAction>
{
    std::map<LedName, Layout::LedAction> newState;

    // update the new map with the desired state
    for (const auto* it : sorted)
    {
        // apply all led actions of that group to the map
        for (const Layout::LedAction& action : it->actionSet)
        {
            newState[action.name] = action;
        }
    }
    return newState;
}

static std::map<LedName, Layout::LedAction> getNewMapWithLEDPriorities(
    std::set<const Layout::GroupLayout*> assertedGroups)
{
    std::map<LedName, Layout::LedAction> newState;
    // update the new map with the desired state
    for (const Layout::GroupLayout* it : assertedGroups)
    {
        // apply all led actions of that group to the map
        for (const Layout::LedAction& action : it->actionSet)
        {
            applyGroupAction(newState, action);
        }
    }
    return newState;
}

// create the resulting new map from all currently asserted groups
std::map<LedName, Layout::LedAction>
    Manager::getNewMap(std::set<const Layout::GroupLayout*> assertedGroups)
{
    std::map<LedName, Layout::LedAction> newState;

    std::set<const Layout::GroupLayout*, Layout::CompareGroupLayout> sorted;

    bool groupPriorities = false;

    for (const Layout::GroupLayout* it : assertedGroups)
    {
        sorted.insert(it);

        if (it->priority != 0)
        {
            groupPriorities = true;
        }
    }

    if (groupPriorities)
    {
        newState = getNewMapWithGroupPriorities(sorted);
    }
    else
    {
        newState = getNewMapWithLEDPriorities(assertedGroups);
    }

    return newState;
}

// Assert -or- De-assert
bool Manager::setGroupState(const std::string& path, bool assert,
                            ActionSet& ledsAssert, ActionSet& ledsDeAssert)
{
    if (assert)
    {
        assertedGroups.insert(&ledMap.at(path));
    }
    else
    {
        if (assertedGroups.contains(&ledMap.at(path)))
        {
            assertedGroups.erase(&ledMap.at(path));
        }
    }

    // create the new map from the asserted groups
    auto newState = getNewMap(assertedGroups);

    // the ledsAssert are those that are in the new map and change state
    // + those in the new map and not in the old map
    for (const auto& [name, action] : newState)
    {
        if (ledStateMap.contains(name))
        {
            // check if the led action has changed
            auto& currentAction = ledStateMap[name];

            if (currentAction.action == action.action)
            {
                continue;
            }
        }

        ledsAssert.insert(action);
    }

    // the ledsDeAssert are those in the old map but not in the new map
    for (const auto& [name, action] : ledStateMap)
    {
        if (!newState.contains(name))
        {
            ledsDeAssert.insert(action);
        }
    }

    ledStateMap = newState;

    // If we survive, then set the state accordingly.
    return assert;
}

void Manager::setLampTestCallBack(
    std::function<bool(ActionSet& ledsAssert, ActionSet& ledsDeAssert)>
        callBack)
{
    lampTestCallBack = callBack;
}

/** @brief Run through the map and apply action on the LEDs */
void Manager::driveLEDs(ActionSet& ledsAssert, ActionSet& ledsDeAssert)
{
#ifdef USE_LAMP_TEST
    // Use the lampTestCallBack method and trigger the callback method in the
    // lamp test(processLEDUpdates), in this way, all lamp test operations
    // are performed in the lamp test class.
    if (lampTestCallBack(ledsAssert, ledsDeAssert))
    {
        return;
    }
#endif
    ActionSet newReqChangedLeds;
    std::vector<std::pair<ActionSet&, ActionSet&>> actionsVec = {
        {reqLedsAssert, ledsAssert}, {reqLedsDeAssert, ledsDeAssert}};

    timer.setEnabled(false);
    std::set_union(ledsAssert.begin(), ledsAssert.end(), ledsDeAssert.begin(),
                   ledsDeAssert.end(),
                   std::inserter(newReqChangedLeds, newReqChangedLeds.begin()),
                   ledLess);

    // prepare reqLedsAssert & reqLedsDeAssert
    for (auto pair : actionsVec)
    {
        ActionSet tmpSet;

        // Discard current required LED actions, if these LEDs have new actions
        // in newReqChangedLeds.
        std::set_difference(pair.first.begin(), pair.first.end(),
                            newReqChangedLeds.begin(), newReqChangedLeds.end(),
                            std::inserter(tmpSet, tmpSet.begin()), ledLess);

        // Union the remaining LED actions with new LED actions.
        pair.first.clear();
        std::set_union(tmpSet.begin(), tmpSet.end(), pair.second.begin(),
                       pair.second.end(),
                       std::inserter(pair.first, pair.first.begin()), ledLess);
    }

    driveLedsHandler();
    return;
}

// Calls into driving physical LED post choosing the action
int Manager::drivePhysicalLED(const std::string& objPath, Layout::Action action,
                              uint8_t dutyOn, uint16_t period)
{
    try
    {
        // If Blink, set its property
        if (action == Layout::Action::Blink)
        {
            PropertyValue dutyOnValue{dutyOn};
            PropertyValue periodValue{period};

            phosphor::led::utils::DBusHandler::setProperty(
                objPath, phyLedIntf, "DutyOn", dutyOnValue);
            phosphor::led::utils::DBusHandler::setProperty(
                objPath, phyLedIntf, "Period", periodValue);
        }

        PropertyValue actionValue{getPhysicalAction(action)};
        phosphor::led::utils::DBusHandler::setProperty(objPath, phyLedIntf,
                                                       "State", actionValue);
    }
    catch (const sdbusplus::exception_t& e)
    {
        lg2::error(
            "Error setting property for physical LED, ERROR = {ERROR}, OBJECT_PATH = {PATH}",
            "ERROR", e, "PATH", objPath);
        return -1;
    }

    return 0;
}

/** @brief Returns action string based on enum */
std::string Manager::getPhysicalAction(Layout::Action action)
{
    namespace server = sdbusplus::xyz::openbmc_project::Led::server;

    // TODO: openbmc/phosphor-led-manager#5
    //    Somehow need to use the generated Action enum than giving one
    //    in ledlayout.
    if (action == Layout::Action::On)
    {
        return server::convertForMessage(server::Physical::Action::On);
    }
    else if (action == Layout::Action::Blink)
    {
        return server::convertForMessage(server::Physical::Action::Blink);
    }
    else
    {
        return server::convertForMessage(server::Physical::Action::Off);
    }
}

void Manager::driveLedsHandler(void)
{
    ActionSet failedLedsAssert;
    ActionSet failedLedsDeAssert;

    // This order of LED operation is important.
    for (const auto& it : reqLedsDeAssert)
    {
        std::string objPath = std::string(phyLedPath) + it.name;
        lg2::debug("De-Asserting LED, NAME = {NAME}, ACTION = {ACTION}", "NAME",
                   it.name, "ACTION", it.action);
        if (drivePhysicalLED(objPath, Layout::Action::Off, it.dutyOn,
                             it.period) != 0)
        {
            failedLedsDeAssert.insert(it);
        }
    }

    for (const auto& it : reqLedsAssert)
    {
        std::string objPath = std::string(phyLedPath) + it.name;
        lg2::debug("Asserting LED, NAME = {NAME}, ACTION = {ACTION}", "NAME",
                   it.name, "ACTION", it.action);
        if (drivePhysicalLED(objPath, it.action, it.dutyOn, it.period) != 0)
        {
            failedLedsAssert.insert(it);
        }
    }

    reqLedsAssert = failedLedsAssert;
    reqLedsDeAssert = failedLedsDeAssert;

    if (reqLedsDeAssert.empty() && reqLedsAssert.empty())
    {
        timer.setEnabled(false);
    }
    else
    {
        timer.restartOnce(std::chrono::seconds(1));
    }

    return;
}

} // namespace led
} // namespace phosphor