/**
 * Copyright © 2020 IBM Corporation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
#pragma once

#include "config_base.hpp"
#include "dbus_zone.hpp"
#include "fan.hpp"

#include <nlohmann/json.hpp>
#include <sdeventplus/event.hpp>
#include <sdeventplus/utility/timer.hpp>

#include <any>
#include <chrono>
#include <functional>
#include <map>
#include <memory>
#include <tuple>

namespace phosphor::fan::control::json
{

class Manager;

using json = nlohmann::json;

/* Dbus event timer */
using Timer = sdeventplus::utility::Timer<sdeventplus::ClockId::Monotonic>;

/**
 * @class Zone - Represents a configured fan control zone
 *
 * A zone object contains the configured attributes for a zone that groups
 * a number of fans together to be under the same target control. These
 * configuration attributes include, but are not limited to, the default ceiling
 * of the fans within the zone, a default floor, the delay between increases, a
 * decrease interval, and any profiles(OPTIONAL) the zone should be included in.
 *
 * (When no profile for a zone is given, the zone defaults to always exist)
 *
 */
class Zone : public ConfigBase
{
  public:
    /* JSON file name for zones */
    static constexpr auto confFileName = "zones.json";

    Zone() = delete;
    Zone(const Zone&) = delete;
    Zone(Zone&&) = delete;
    Zone& operator=(const Zone&) = delete;
    Zone& operator=(Zone&&) = delete;
    ~Zone() = default;

    /**
     * Constructor
     * Parses and populates a zone from JSON object data
     *
     * @param[in] jsonObj - JSON object
     * @param[in] event - sdeventplus event loop
     * @param[in] mgr - Manager of this zone
     */
    Zone(const json& jsonObj, const sdeventplus::Event& event, Manager* mgr);

    /**
     * @brief Get the poweron target
     *
     * Poweron target is the target the fans within this zone should be set to
     * when the system is powered on.
     *
     * @return Poweron target of this zone
     */
    inline const auto& getPoweronTarget() const
    {
        return _poweronTarget;
    }

    /**
     * @brief Get the default ceiling
     *
     * Default ceiling is the highest target the fans within this zone is
     * allowed to increase to. The zone's ceiling defaults to this unless
     * changed by some configured event.
     *
     * @return Default ceiling of this zone
     */
    inline const auto& getDefaultCeiling() const
    {
        return _defaultCeiling;
    }

    /**
     * @brief Get the default floor
     *
     * The default floor is the lowest target the fans within this zone
     * are allowed to decrease to. The zone's floor defaults to this
     * unless changed by some configured event.
     *
     * @return Default floor
     */
    inline const auto& getDefaultFloor() const
    {
        return _defaultFloor;
    }

    /**
     * @brief Get the increase delay(OPTIONAL)
     *
     * The increase delay is the amount of time(in seconds) increases
     * to a target are delayed before being made. The default is 0, which
     * results in immediate increase requests when any events result in
     * a change to the target.
     *
     * It is recommend a value other than 0 is configured, but that inherently
     * depends on the fan controller and configured increases.
     *
     * @return Increase delay(in seconds)
     */
    inline const auto& getIncDelay() const
    {
        return _incDelay;
    }

    /**
     * @brief Get the decrease interval
     *
     * Decreases happen on a set interval when no requests for an increase
     * in fan targets exists. This is the interval(in seconds) at which the fans
     * within the zone are decreased if events exist that result in a target
     * decrease.
     *
     * @return Decrease interval(in seconds)
     */
    inline const auto& getDecInterval() const
    {
        return _decInterval;
    }

    /**
     * @brief Get the current target of the zone
     *
     * @return - The current target of the zone
     */
    inline const auto& getTarget() const
    {
        return _target;
    }

    /**
     * @brief Get the target increase delta
     *
     * @return - The current target increase delta
     */
    inline auto& getIncDelta() const
    {
        return _incDelta;
    };

    /**
     * @brief Get the target decrease delta
     *
     * @return - The current target decrease delta
     */
    inline auto& getDecDelta() const
    {
        return _decDelta;
    };

    /**
     * @brief Get the manager of the zone
     *
     * @return - The manager of the zone
     */
    inline auto* getManager() const
    {
        return _manager;
    }

    /**
     * @brief Enable the zone
     *
     * Performs the necessary tasks to enable the zone such as restoring any
     * dbus property states(if persisted), starting the decrement timer, etc...
     */
    void enable();

    /**
     * @brief Add a fan object to the zone
     *
     * @param[in] fan - Unique pointer to a fan object that will be moved into
     * the zone
     *
     * Adds a fan object to the list of fans that make up the zone by moving the
     * fan object into the list.
     */
    void addFan(std::unique_ptr<Fan> fan);

    /**
     * Sets all fans in the zone to the target given when the zone is active
     *
     * @param[in] target - Target for fans
     */
    void setTarget(uint64_t target);

    /**
     * Sets and holds all fans in the zone to the target given or releases a
     * target hold resulting in the fans being held at the highest remaining
     * hold target if other hold targets had been requested. When no hold
     * targets exist, the zone returns to being active.
     *
     * @param[in] ident - Unique identifier for a target hold
     * @param[in] target - Target to hold fans at
     * @param[in] hold - Whether to hold(true) or release(false) a target hold
     */
    void setTargetHold(const std::string& ident, uint64_t target, bool hold);

    /**
     * @brief Sets the automatic fan control allowed active state
     *
     * @param[in] ident - An identifier that affects the active state
     * @param[in] isActiveAllow - Active state according to group
     */
    void setActiveAllow(const std::string& ident, bool isActiveAllow);

    /**
     * @brief Set the floor to the given target and increase target to the floor
     * when the target is below the floor value when floor changes are allowed.
     *
     * @param[in] target - Target to set the floor to
     */
    void setFloor(uint64_t target);

    /**
     * @brief Set the default floor to the given value
     *
     * @param[in] value - Value to set the default floor to
     */
    inline void setDefaultFloor(uint64_t value)
    {
        _defaultFloor = value;
    }

    /**
     * @brief Sets the floor change allowed state
     *
     * @param[in] ident - An identifier that affects floor changes
     * @param[in] isAllow - Allow state according to the identifier
     */
    inline void setFloorChangeAllow(const std::string& ident, bool isAllow)
    {
        _floorChange[ident] = isAllow;
    }

    /**
     * @brief Sets the decrease allowed state of a group
     *
     * @param[in] ident - An identifier that affects speed decreases
     * @param[in] isAllow - Allow state according to the identifier
     */
    inline void setDecreaseAllow(const std::string& ident, bool isAllow)
    {
        _decAllowed[ident] = isAllow;
    }

    /**
     * @brief Calculate the requested target from the given delta and increases
     * the fans, not going above the ceiling.
     *
     * @param[in] targetDelta - The delta to increase the target by
     */
    void requestIncrease(uint64_t targetDelta);

    /**
     * @brief Callback function for the increase timer that delays
     * processing of requested target increases while fans are increasing
     */
    void incTimerExpired();

    /**
     * @brief Calculate the lowest requested decrease target from the given
     * delta within a decrease interval.
     *
     * @param[in] targetDelta - The delta to decrease the target by
     */
    void requestDecrease(uint64_t targetDelta);

    /**
     * @brief Callback function for the decrease timer that processes any
     * requested target decreases if allowed
     */
    void decTimerExpired();

    /**
     * @brief Set the requested target base to be used as the target to base a
     * new requested target from
     *
     * @param[in] targetBase - Base target value to use
     */
    inline void setRequestTargetBase(uint64_t targetBase)
    {
        _requestTargetBase = targetBase;
    };

    /**
     * @brief Set a property to be persisted
     *
     * @param[in] intf - Interface containing property
     * @param[in] prop - Property to be persisted
     */
    void setPersisted(const std::string& intf, const std::string& prop);

    /**
     * @brief Is the property persisted
     *
     * @param[in] intf - Interface containing property
     * @param[in] prop - Property to check if persisted
     *
     * @return - True if property is to be persisted, false otherwise
     */
    bool isPersisted(const std::string& intf, const std::string& prop) const;

    /**
     * @brief A handler function to set/update a property on a zone
     * @details Sets or updates a zone's dbus property to the given value using
     * the provided base dbus object's set property function
     *
     * @param[in] intf - Interface on zone object
     * @param[in] prop - Property on interface
     * @param[in] func - Zone dbus object's set property function pointer
     * @param[in] value - Value to set property to
     * @param[in] persist - Persist property value or not
     *
     * @return Lambda function
     *     A lambda function to set/update the zone dbus object's property
     */
    template <typename T>
    static auto setProperty(const char* intf, const char* prop,
                            T (DBusZone::*func)(T), T&& value, bool persist)
    {
        return [=, value = std::forward<T>(value)](DBusZone& dbusZone,
                                                   Zone& zone) {
            (dbusZone.*func)(value);
            if (persist)
            {
                zone.setPersisted(intf, prop);
            }
        };
    }

    /**
     * @brief A handler function to set/update a zone's dbus property's persist
     * state
     * @details Sets or updates a zone's dbus property's persist state where the
     * value of the property is to be left unchanged
     *
     * @param[in] intf - Interface on zone object
     * @param[in] prop - Property on interface
     * @param[in] persist - Persist property value or not
     *
     * @return Lambda function
     *     A lambda function to set/update the zone's dbus object's property's
     * persist state
     */
    static auto setPropertyPersist(const char* intf, const char* prop,
                                   bool persist)
    {
        return [=](DBusZone&, Zone& zone) {
            if (persist)
            {
                zone.setPersisted(intf, prop);
            }
        };
    }

  private:
    /* The zone's associated dbus object */
    std::unique_ptr<DBusZone> _dbusZone;

    /* The zone's manager */
    Manager* _manager;

    /* The zone's poweron target value for fans */
    uint64_t _poweronTarget;

    /* The zone's default ceiling value for fans */
    uint64_t _defaultCeiling;

    /* The zone's default floor value for fans */
    uint64_t _defaultFloor;

    /* Zone's increase delay(in seconds) (OPTIONAL) */
    std::chrono::seconds _incDelay;

    /* Zone's decrease interval(in seconds) (OPTIONAL) */
    std::chrono::seconds _decInterval;

    /* The floor target to not go below */
    uint64_t _floor;

    /* Target for this zone */
    uint64_t _target;

    /* Zone increase delta */
    uint64_t _incDelta;

    /* Zone decrease delta */
    uint64_t _decDelta;

    /* The ceiling target to not go above */
    uint64_t _ceiling;

    /* Requested target base */
    uint64_t _requestTargetBase;

    /* Map of whether floor changes are allowed by a string identifier */
    std::map<std::string, bool> _floorChange;

    /* Map of controlling decreases allowed by a string identifer */
    std::map<std::string, bool> _decAllowed;

    /* Map of interfaces to persisted properties the zone hosts*/
    std::map<std::string, std::vector<std::string>> _propsPersisted;

    /* Automatic fan control active state */
    bool _isActive;

    /* The target increase timer object */
    Timer _incTimer;

    /* The target decrease timer object */
    Timer _decTimer;

    /* Map of active fan control allowed by a string identifier */
    std::map<std::string, bool> _active;

    /* Map of target holds by a string identifier */
    std::unordered_map<std::string, uint64_t> _holds;

    /* Interface to property mapping of their associated set property handler
     * function */
    static const std::map<
        std::string,
        std::map<std::string, std::function<std::function<void(
                                  DBusZone&, Zone&)>(const json&, bool)>>>
        _intfPropHandlers;

    /* List of fans included in this zone */
    std::vector<std::unique_ptr<Fan>> _fans;

    /* List of configured interface set property functions */
    std::vector<std::function<void(DBusZone&, Zone&)>> _propInitFunctions;

    /**
     * @brief Parse and set the zone's poweron target value
     *
     * @param[in] jsonObj - JSON object for the zone
     *
     * Sets the poweron target value for the zone from the JSON configuration
     * object
     */
    void setPowerOnTarget(const json& jsonObj);

    /**
     * @brief Parse and set the interfaces served by the zone(OPTIONAL)
     *
     * @param[in] jsonObj - JSON object for the zone
     *
     * Constructs any zone interface handler functions for interfaces that the
     * zone serves which contains the interface's property's value and
     * persistency state (OPTIONAL). A property's "persist" state is defaulted
     * to not be persisted when not given.
     */
    void setInterfaces(const json& jsonObj);

    /**
     * @brief Get the request target base if defined, otherwise the the current
     * target is returned
     *
     * @return - The request target base or current target
     */
    inline auto getRequestTargetBase() const
    {
        return (_requestTargetBase != 0) ? _requestTargetBase : _target;
    };
};

/**
 * Properties of interfaces supported by the zone configuration
 */
namespace zone::property
{

/**
 * @brief "Supported" property on the "xyz.openbmc_project.Control.ThermalMode"
 * interface parser. Also creates the handler function for the Zone dbus object
 * to initialize the property according to what's parsed from the configuration.
 *
 * @param[in] jsonObj - JSON object for the "Supported" property
 * @param[in] persist - Whether to persist the value or not
 *
 * @return Zone dbus object's set property function for the "Supported" property
 */
std::function<void(DBusZone&, Zone&)> supported(const json& jsonObj,
                                                bool persist);

/**
 * @brief "Current" property on the "xyz.openbmc_project.Control.ThermalMode"
 * interface parser. Also creates the handler function for the Zone dbus object
 * to initialize the property according to what's parsed from the configuration.
 *
 * @param[in] jsonObj - JSON object for the "Current" property
 * @param[in] persist - Whether to persist the value or not
 *
 * @return Zone dbus object's set property function for the "Current" property
 */
std::function<void(DBusZone&, Zone&)> current(const json& jsonObj,
                                              bool persist);

} // namespace zone::property

} // namespace phosphor::fan::control::json