#pragma once

#include "config.h"

#include "sensorhandler.hpp"

#include <cmath>
#include <ipmid/api.hpp>
#include <ipmid/types.hpp>
#include <ipmid/utils.hpp>
#include <phosphor-logging/elog-errors.hpp>
#include <phosphor-logging/log.hpp>
#include <sdbusplus/message/types.hpp>

#ifdef FEATURE_SENSORS_CACHE

extern ipmi::sensor::SensorCacheMap sensorCacheMap;

// The signal's message type is 0x04 from DBus spec:
// https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-messages
static constexpr auto msgTypeSignal = 0x04;

#endif

namespace ipmi
{
namespace sensor
{

using Assertion = uint16_t;
using Deassertion = uint16_t;
using AssertionSet = std::pair<Assertion, Deassertion>;
using Service = std::string;
using Path = std::string;
using Interface = std::string;
using ServicePath = std::pair<Path, Service>;
using Interfaces = std::vector<Interface>;
using MapperResponseType = std::map<Path, std::map<Service, Interfaces>>;
using PropertyMap = ipmi::PropertyMap;

using namespace phosphor::logging;

/** @brief get the D-Bus service and service path
 *  @param[in] bus - The Dbus bus object
 *  @param[in] interface - interface to the service
 *  @param[in] path - interested path in the list of objects
 *  @return pair of service path and service
 */
ServicePath getServiceAndPath(sdbusplus::bus::bus& bus,
                              const std::string& interface,
                              const std::string& path = std::string());

/** @brief Make assertion set from input data
 *  @param[in] cmdData - Input sensor data
 *  @return pair of assertion and deassertion set
 */
AssertionSet getAssertionSet(const SetSensorReadingReq& cmdData);

/** @brief send the message to DBus
 *  @param[in] msg - message to send
 *  @return failure status in IPMI error code
 */
ipmi_ret_t updateToDbus(IpmiUpdateData& msg);

namespace get
{

/** @brief Populate sensor name from the D-Bus property associated with the
 *         sensor. In the example entry from the yaml, the name of the D-bus
 *         property "AttemptsLeft" is the sensor name.
 *
 *         0x07:
 *            sensorType: 195
 *            path: /xyz/openbmc_project/state/host0
 *            sensorReadingType: 0x6F
 *            serviceInterface: org.freedesktop.DBus.Properties
 *            readingType: readingAssertion
 *            sensorNamePattern: nameProperty
 *            interfaces:
 *              xyz.openbmc_project.Control.Boot.RebootAttempts:
 *                AttemptsLeft:
 *                    Offsets:
 *                        0xFF:
 *                          type: uint32_t
 *
 *
 *  @param[in] sensorInfo - Dbus info related to sensor.
 *
 *  @return On success return the sensor name for the sensor.
 */
inline SensorName nameProperty(const Info& sensorInfo)
{
    return sensorInfo.propertyInterfaces.begin()->second.begin()->first;
}

/** @brief Populate sensor name from the D-Bus object associated with the
 *         sensor. If the object path is /system/chassis/motherboard/dimm0 then
 *         the leaf dimm0 is considered as the sensor name.
 *
 *  @param[in] sensorInfo - Dbus info related to sensor.
 *
 *  @return On success return the sensor name for the sensor.
 */
inline SensorName nameLeaf(const Info& sensorInfo)
{
    return sensorInfo.sensorPath.substr(
        sensorInfo.sensorPath.find_last_of('/') + 1,
        sensorInfo.sensorPath.length());
}

/** @brief Populate sensor name from the D-Bus object associated with the
 *         sensor and the property.
 *         If the object path is /xyz/openbmc_project/inventory/Fan0 and
 *         the property is Present, the leaf Fan0 and the Property is
 *         joined to Fan0_Present as the sensor name.
 *
 *  @param[in] sensorInfo - Dbus info related to sensor.
 *
 *  @return On success return the sensor name for the sensor.
 */
inline SensorName nameLeafProperty(const Info& sensorInfo)
{
    return nameLeaf(sensorInfo) + "_" + nameProperty(sensorInfo);
}

/** @brief Populate sensor name from the D-Bus object associated with the
 *         sensor. If the object path is /system/chassis/motherboard/cpu0/core0
 *         then the sensor name is cpu0_core0. The leaf and the parent is put
 *         together to get the sensor name.
 *
 *  @param[in] sensorInfo - Dbus info related to sensor.
 *
 *  @return On success return the sensor name for the sensor.
 */
SensorName nameParentLeaf(const Info& sensorInfo);

/**
 *  @brief Helper function to map the dbus info to sensor's assertion status
 *         for the get sensor reading command.
 *
 *  @param[in] sensorInfo - Dbus info related to sensor.
 *  @param[in] path - Dbus object path.
 *  @param[in] interface - Dbus interface.
 *
 *  @return Response for get sensor reading command.
 */
GetSensorResponse mapDbusToAssertion(const Info& sensorInfo,
                                     const InstancePath& path,
                                     const DbusInterface& interface);

#ifndef FEATURE_SENSORS_CACHE
/**
 *  @brief Map the Dbus info to sensor's assertion status in the Get sensor
 *         reading command response.
 *
 *  @param[in] sensorInfo - Dbus info related to sensor.
 *
 *  @return Response for get sensor reading command.
 */
GetSensorResponse assertion(const Info& sensorInfo);

/**
 *  @brief Maps the Dbus info to the reading field in the Get sensor reading
 *         command response.
 *
 *  @param[in] sensorInfo - Dbus info related to sensor.
 *
 *  @return Response for get sensor reading command.
 */
GetSensorResponse eventdata2(const Info& sensorInfo);

/**
 *  @brief readingAssertion is a case where the entire assertion state field
 *         serves as the sensor value.
 *
 *  @tparam T - type of the dbus property related to sensor.
 *  @param[in] sensorInfo - Dbus info related to sensor.
 *
 *  @return Response for get sensor reading command.
 */
template <typename T>
GetSensorResponse readingAssertion(const Info& sensorInfo)
{
    sdbusplus::bus::bus bus{ipmid_get_sd_bus_connection()};
    GetSensorResponse response{};

    enableScanning(&response);

    auto service = ipmi::getService(bus, sensorInfo.sensorInterface,
                                    sensorInfo.sensorPath);

    auto propValue = ipmi::getDbusProperty(
        bus, service, sensorInfo.sensorPath,
        sensorInfo.propertyInterfaces.begin()->first,
        sensorInfo.propertyInterfaces.begin()->second.begin()->first);

    setAssertionBytes(static_cast<uint16_t>(std::get<T>(propValue)), &response);

    return response;
}

/** @brief Map the Dbus info to the reading field in the Get sensor reading
 *         command response
 *
 *  @tparam T - type of the dbus property related to sensor.
 *  @param[in] sensorInfo - Dbus info related to sensor.
 *
 *  @return Response for get sensor reading command.
 */
template <typename T>
GetSensorResponse readingData(const Info& sensorInfo)
{
    sdbusplus::bus::bus bus{ipmid_get_sd_bus_connection()};

    GetSensorResponse response{};

    enableScanning(&response);

    auto service = ipmi::getService(bus, sensorInfo.sensorInterface,
                                    sensorInfo.sensorPath);

#ifdef UPDATE_FUNCTIONAL_ON_FAIL
    // Check the OperationalStatus interface for functional property
    if (sensorInfo.propertyInterfaces.begin()->first ==
        "xyz.openbmc_project.Sensor.Value")
    {
        bool functional = true;
        try
        {
            auto funcValue = ipmi::getDbusProperty(
                bus, service, sensorInfo.sensorPath,
                "xyz.openbmc_project.State.Decorator.OperationalStatus",
                "Functional");
            functional = std::get<bool>(funcValue);
        }
        catch (...)
        {
            // No-op if Functional property could not be found since this
            // check is only valid for Sensor.Value read for hwmonio
        }
        if (!functional)
        {
            throw SensorFunctionalError();
        }
    }
#endif

    auto propValue = ipmi::getDbusProperty(
        bus, service, sensorInfo.sensorPath,
        sensorInfo.propertyInterfaces.begin()->first,
        sensorInfo.propertyInterfaces.begin()->second.begin()->first);

    double value = std::get<T>(propValue) *
                   std::pow(10, sensorInfo.scale - sensorInfo.exponentR);
    int32_t rawData =
        (value - sensorInfo.scaledOffset) / sensorInfo.coefficientM;

    constexpr uint8_t sensorUnitsSignedBits = 2 << 6;
    constexpr uint8_t signedDataFormat = 0x80;
    // if sensorUnits1 [7:6] = 10b, sensor is signed
    if ((sensorInfo.sensorUnits1 & sensorUnitsSignedBits) == signedDataFormat)
    {
        if (rawData > std::numeric_limits<int8_t>::max() ||
            rawData < std::numeric_limits<int8_t>::lowest())
        {
            log<level::ERR>("Value out of range");
            throw std::out_of_range("Value out of range");
        }
        setReading(static_cast<int8_t>(rawData), &response);
    }
    else
    {
        if (rawData > std::numeric_limits<uint8_t>::max() ||
            rawData < std::numeric_limits<uint8_t>::lowest())
        {
            log<level::ERR>("Value out of range");
            throw std::out_of_range("Value out of range");
        }
        setReading(static_cast<uint8_t>(rawData), &response);
    }

    if (!std::isfinite(value))
    {
        response.readingOrStateUnavailable = 1;
    }

    bool critAlarmHigh;
    try
    {
        critAlarmHigh = std::get<bool>(ipmi::getDbusProperty(
            bus, service, sensorInfo.sensorPath,
            "xyz.openbmc_project.Sensor.Threshold.Critical",
            "CriticalAlarmHigh"));
    }
    catch (const std::exception& e)
    {
        critAlarmHigh = false;
    }
    bool critAlarmLow;
    try
    {
        critAlarmLow = std::get<bool>(ipmi::getDbusProperty(
            bus, service, sensorInfo.sensorPath,
            "xyz.openbmc_project.Sensor.Threshold.Critical",
            "CriticalAlarmLow"));
    }
    catch (const std::exception& e)
    {
        critAlarmLow = false;
    }
    bool warningAlarmHigh;
    try
    {
        warningAlarmHigh = std::get<bool>(ipmi::getDbusProperty(
            bus, service, sensorInfo.sensorPath,
            "xyz.openbmc_project.Sensor.Threshold.Warning",
            "WarningAlarmHigh"));
    }
    catch (const std::exception& e)
    {
        warningAlarmHigh = false;
    }
    bool warningAlarmLow;
    try
    {
        warningAlarmLow = std::get<bool>(ipmi::getDbusProperty(
            bus, service, sensorInfo.sensorPath,
            "xyz.openbmc_project.Sensor.Threshold.Warning", "WarningAlarmLow"));
    }
    catch (const std::exception& e)
    {
        warningAlarmLow = false;
    }
    response.thresholdLevelsStates =
        (static_cast<uint8_t>(critAlarmHigh) << 3) |
        (static_cast<uint8_t>(critAlarmLow) << 2) |
        (static_cast<uint8_t>(warningAlarmHigh) << 1) |
        (static_cast<uint8_t>(warningAlarmLow));

    return response;
}

#else

/**
 *  @brief Map the Dbus info to sensor's assertion status in the Get sensor
 *         reading command response.
 *
 *  @param[in] id - The sensor id
 *  @param[in] sensorInfo - Dbus info related to sensor.
 *  @param[in] msg - Dbus message from match callback.
 *
 *  @return Response for get sensor reading command.
 */
std::optional<GetSensorResponse> assertion(uint8_t id, const Info& sensorInfo,
                                           const PropertyMap& properties);

/**
 *  @brief Maps the Dbus info to the reading field in the Get sensor reading
 *         command response.
 *
 *  @param[in] id - The sensor id
 *  @param[in] sensorInfo - Dbus info related to sensor.
 *  @param[in] msg - Dbus message from match callback.
 *
 *  @return Response for get sensor reading command.
 */
std::optional<GetSensorResponse> eventdata2(uint8_t id, const Info& sensorInfo,
                                            const PropertyMap& properties);

/**
 *  @brief readingAssertion is a case where the entire assertion state field
 *         serves as the sensor value.
 *
 *  @tparam T - type of the dbus property related to sensor.
 *  @param[in] id - The sensor id
 *  @param[in] sensorInfo - Dbus info related to sensor.
 *  @param[in] msg - Dbus message from match callback.
 *
 *  @return Response for get sensor reading command.
 */
template <typename T>
std::optional<GetSensorResponse> readingAssertion(uint8_t id,
                                                  const Info& sensorInfo,
                                                  const PropertyMap& properties)
{
    GetSensorResponse response{};
    enableScanning(&response);

    auto iter = properties.find(
        sensorInfo.propertyInterfaces.begin()->second.begin()->first);
    if (iter == properties.end())
    {
        return {};
    }

    setAssertionBytes(static_cast<uint16_t>(std::get<T>(iter->second)),
                      &response);

    if (!sensorCacheMap[id].has_value())
    {
        sensorCacheMap[id] = SensorData{};
    }
    sensorCacheMap[id]->response = response;
    return response;
}

/** @brief Get sensor reading from the dbus message from match
 *
 *  @tparam T - type of the dbus property related to sensor.
 *  @param[in] id - The sensor id
 *  @param[in] sensorInfo - Dbus info related to sensor.
 *  @param[in] msg - Dbus message from match callback.
 *
 *  @return Response for get sensor reading command.
 */
template <typename T>
std::optional<GetSensorResponse> readingData(uint8_t id, const Info& sensorInfo,
                                             const PropertyMap& properties)
{
    auto iter = properties.find("Functional");
    if (iter != properties.end())
    {
        sensorCacheMap[id]->functional = std::get<bool>(iter->second);
    }
    iter = properties.find("Available");
    if (iter != properties.end())
    {
        sensorCacheMap[id]->available = std::get<bool>(iter->second);
    }
#ifdef UPDATE_FUNCTIONAL_ON_FAIL
    if (sensorCacheMap[id])
    {
        if (!sensorCacheMap[id]->functional)
        {
            throw SensorFunctionalError();
        }
    }
#endif

    GetSensorResponse response{};

    enableScanning(&response);

    iter = properties.find(
        sensorInfo.propertyInterfaces.begin()->second.begin()->first);
    if (iter == properties.end())
    {
        return {};
    }

    double value = std::get<T>(iter->second) *
                   std::pow(10, sensorInfo.scale - sensorInfo.exponentR);
    int32_t rawData =
        (value - sensorInfo.scaledOffset) / sensorInfo.coefficientM;

    constexpr uint8_t sensorUnitsSignedBits = 2 << 6;
    constexpr uint8_t signedDataFormat = 0x80;
    // if sensorUnits1 [7:6] = 10b, sensor is signed
    if ((sensorInfo.sensorUnits1 & sensorUnitsSignedBits) == signedDataFormat)
    {
        if (rawData > std::numeric_limits<int8_t>::max() ||
            rawData < std::numeric_limits<int8_t>::lowest())
        {
            log<level::ERR>("Value out of range");
            throw std::out_of_range("Value out of range");
        }
        setReading(static_cast<int8_t>(rawData), &response);
    }
    else
    {
        if (rawData > std::numeric_limits<uint8_t>::max() ||
            rawData < std::numeric_limits<uint8_t>::lowest())
        {
            log<level::ERR>("Value out of range");
            throw std::out_of_range("Value out of range");
        }
        setReading(static_cast<uint8_t>(rawData), &response);
    }

    if (!std::isfinite(value))
    {
        response.readingOrStateUnavailable = 1;
    }

    if (!sensorCacheMap[id].has_value())
    {
        sensorCacheMap[id] = SensorData{};
    }
    sensorCacheMap[id]->response = response;

    return response;
}

#endif // FEATURE_SENSORS_CACHE

} // namespace get

namespace set
{

/** @brief Make a DBus message for a Dbus call
 *  @param[in] updateInterface - Interface name
 *  @param[in] sensorPath - Path of the sensor
 *  @param[in] command - command to be executed
 *  @param[in] sensorInterface - DBus interface of sensor
 *  @return a dbus message
 */
IpmiUpdateData makeDbusMsg(const std::string& updateInterface,
                           const std::string& sensorPath,
                           const std::string& command,
                           const std::string& sensorInterface);

/** @brief Update d-bus based on assertion type sensor data
 *  @param[in] cmdData - input sensor data
 *  @param[in] sensorInfo - sensor d-bus info
 *  @return a IPMI error code
 */
ipmi_ret_t assertion(const SetSensorReadingReq& cmdData,
                     const Info& sensorInfo);

/** @brief Update d-bus based on a reading assertion
 *  @tparam T - type of d-bus property mapping this sensor
 *  @param[in] cmdData - input sensor data
 *  @param[in] sensorInfo - sensor d-bus info
 *  @return a IPMI error code
 */
template <typename T>
ipmi_ret_t readingAssertion(const SetSensorReadingReq& cmdData,
                            const Info& sensorInfo)
{
    auto msg =
        makeDbusMsg("org.freedesktop.DBus.Properties", sensorInfo.sensorPath,
                    "Set", sensorInfo.sensorInterface);

    const auto& interface = sensorInfo.propertyInterfaces.begin();
    msg.append(interface->first);
    for (const auto& property : interface->second)
    {
        msg.append(property.first);
        std::variant<T> value = static_cast<T>((cmdData.assertOffset8_14 << 8) |
                                               cmdData.assertOffset0_7);
        msg.append(value);
    }
    return updateToDbus(msg);
}

/** @brief Update d-bus based on a discrete reading
 *  @param[in] cmdData - input sensor data
 *  @param[in] sensorInfo - sensor d-bus info
 *  @return an IPMI error code
 */
template <typename T>
ipmi_ret_t readingData(const SetSensorReadingReq& cmdData,
                       const Info& sensorInfo)
{
    T raw_value =
        (sensorInfo.coefficientM * cmdData.reading) + sensorInfo.scaledOffset;

    raw_value *= std::pow(10, sensorInfo.exponentR - sensorInfo.scale);

    auto msg =
        makeDbusMsg("org.freedesktop.DBus.Properties", sensorInfo.sensorPath,
                    "Set", sensorInfo.sensorInterface);

    const auto& interface = sensorInfo.propertyInterfaces.begin();
    msg.append(interface->first);

    for (const auto& property : interface->second)
    {
        msg.append(property.first);
        std::variant<T> value = raw_value;
        msg.append(value);
    }
    return updateToDbus(msg);
}

/** @brief Update d-bus based on eventdata type sensor data
 *  @param[in] cmdData - input sensor data
 *  @param[in] sensorInfo - sensor d-bus info
 *  @return a IPMI error code
 */
ipmi_ret_t eventdata(const SetSensorReadingReq& cmdData, const Info& sensorInfo,
                     uint8_t data);

/** @brief Update d-bus based on eventdata1 type sensor data
 *  @param[in] cmdData - input sensor data
 *  @param[in] sensorInfo - sensor d-bus info
 *  @return a IPMI error code
 */
inline ipmi_ret_t eventdata1(const SetSensorReadingReq& cmdData,
                             const Info& sensorInfo)
{
    return eventdata(cmdData, sensorInfo, cmdData.eventData1);
}

/** @brief Update d-bus based on eventdata2 type sensor data
 *  @param[in] cmdData - input sensor data
 *  @param[in] sensorInfo - sensor d-bus info
 *  @return a IPMI error code
 */
inline ipmi_ret_t eventdata2(const SetSensorReadingReq& cmdData,
                             const Info& sensorInfo)
{
    return eventdata(cmdData, sensorInfo, cmdData.eventData2);
}

/** @brief Update d-bus based on eventdata3 type sensor data
 *  @param[in] cmdData - input sensor data
 *  @param[in] sensorInfo - sensor d-bus info
 *  @return a IPMI error code
 */
inline ipmi_ret_t eventdata3(const SetSensorReadingReq& cmdData,
                             const Info& sensorInfo)
{
    return eventdata(cmdData, sensorInfo, cmdData.eventData3);
}

} // namespace set

namespace notify
{

/** @brief Make a DBus message for a Dbus call
 *  @param[in] updateInterface - Interface name
 *  @param[in] sensorPath - Path of the sensor
 *  @param[in] command - command to be executed
 *  @param[in] sensorInterface - DBus interface of sensor
 *  @return a dbus message
 */
IpmiUpdateData makeDbusMsg(const std::string& updateInterface,
                           const std::string& sensorPath,
                           const std::string& command,
                           const std::string& sensorInterface);

/** @brief Update d-bus based on assertion type sensor data
 *  @param[in] interfaceMap - sensor interface
 *  @param[in] cmdData - input sensor data
 *  @param[in] sensorInfo - sensor d-bus info
 *  @return a IPMI error code
 */
ipmi_ret_t assertion(const SetSensorReadingReq& cmdData,
                     const Info& sensorInfo);

} // namespace notify

namespace inventory
{

namespace get
{

#ifndef FEATURE_SENSORS_CACHE

/**
 *  @brief Map the Dbus info to sensor's assertion status in the Get sensor
 *         reading command response.
 *
 *  @param[in] sensorInfo - Dbus info related to sensor.
 *
 *  @return Response for get sensor reading command.
 */
GetSensorResponse assertion(const Info& sensorInfo);

#else

/**
 *  @brief Map the Dbus info to sensor's assertion status in the Get sensor
 *         reading command response.
 *
 *  @param[in] id - The sensor id
 *  @param[in] sensorInfo - Dbus info related to sensor.
 *  @param[in] msg - Dbus message from match callback.
 *
 *  @return Response for get sensor reading command.
 */
std::optional<GetSensorResponse> assertion(uint8_t id, const Info& sensorInfo,
                                           const PropertyMap& properties);

#endif

} // namespace get

} // namespace inventory
} // namespace sensor
} // namespace ipmi