#pragma once

#include "common/utils.hpp"
#include "libpldmresponder/pdr.hpp"
#include "pdr_utils.hpp"
#include "pldmd/handler.hpp"

#include <libpldm/platform.h>
#include <libpldm/states.h>
#include <math.h>
#include <stdint.h>

#include <phosphor-logging/lg2.hpp>

#include <map>
#include <optional>

PHOSPHOR_LOG2_USING;

namespace pldm
{
namespace responder
{
namespace platform_numeric_effecter
{
/** @brief Function to get the effecter value by PDR factor coefficient, etc.
 *  @param[in] pdr - The structure of pldm_numeric_effecter_value_pdr.
 *  @param[in] effecterValue - effecter value.
 *  @param[in] propertyType - type of the D-Bus property.
 *
 *  @return - std::pair<int, std::optional<PropertyValue>> - rc:Success or
 *          failure, PropertyValue: The value to be set
 */
template <typename T>
std::pair<int, std::optional<pldm::utils::PropertyValue>>
    getEffecterRawValue(const pldm_numeric_effecter_value_pdr* pdr,
                        T& effecterValue, std::string propertyType)
{
    // X = Round [ (Y - B) / m ]
    // refer to DSP0248_1.2.0 27.8
    int rc = 0;
    pldm::utils::PropertyValue value;
    switch (pdr->effecter_data_size)
    {
        case PLDM_EFFECTER_DATA_SIZE_UINT8:
        {
            auto rawValue = static_cast<uint8_t>(
                round(effecterValue - pdr->offset) / pdr->resolution);
            if (pdr->min_settable.value_u8 < pdr->max_settable.value_u8 &&
                (rawValue < pdr->min_settable.value_u8 ||
                 rawValue > pdr->max_settable.value_u8))
            {
                rc = PLDM_ERROR_INVALID_DATA;
            }
            value = rawValue;
            if (propertyType == "uint64_t")
            {
                auto tempValue = std::get<uint8_t>(value);
                value = static_cast<uint64_t>(tempValue);
            }
            else if (propertyType == "uint32_t")
            {
                auto tempValue = std::get<uint8_t>(value);
                value = static_cast<uint32_t>(tempValue);
            }
            break;
        }
        case PLDM_EFFECTER_DATA_SIZE_SINT8:
        {
            auto rawValue = static_cast<int8_t>(
                round(effecterValue - pdr->offset) / pdr->resolution);
            if (pdr->min_settable.value_s8 < pdr->max_settable.value_s8 &&
                (rawValue < pdr->min_settable.value_s8 ||
                 rawValue > pdr->max_settable.value_s8))
            {
                rc = PLDM_ERROR_INVALID_DATA;
            }
            value = rawValue;
            break;
        }
        case PLDM_EFFECTER_DATA_SIZE_UINT16:
        {
            auto rawValue = static_cast<uint16_t>(
                round(effecterValue - pdr->offset) / pdr->resolution);
            if (pdr->min_settable.value_u16 < pdr->max_settable.value_u16 &&
                (rawValue < pdr->min_settable.value_u16 ||
                 rawValue > pdr->max_settable.value_u16))
            {
                rc = PLDM_ERROR_INVALID_DATA;
            }
            value = rawValue;
            if (propertyType == "uint64_t")
            {
                auto tempValue = std::get<uint16_t>(value);
                value = static_cast<uint64_t>(tempValue);
            }
            else if (propertyType == "uint32_t")
            {
                auto tempValue = std::get<uint16_t>(value);
                value = static_cast<uint32_t>(tempValue);
            }
            break;
        }
        case PLDM_EFFECTER_DATA_SIZE_SINT16:
        {
            auto rawValue = static_cast<int16_t>(
                round(effecterValue - pdr->offset) / pdr->resolution);
            if (pdr->min_settable.value_s16 < pdr->max_settable.value_s16 &&
                (rawValue < pdr->min_settable.value_s16 ||
                 rawValue > pdr->max_settable.value_s16))
            {
                rc = PLDM_ERROR_INVALID_DATA;
            }
            value = rawValue;
            if (propertyType == "uint64_t")
            {
                auto tempValue = std::get<int16_t>(value);
                value = static_cast<uint64_t>(tempValue);
            }
            else if (propertyType == "uint32_t")
            {
                auto tempValue = std::get<int16_t>(value);
                value = static_cast<uint32_t>(tempValue);
            }
            break;
        }
        case PLDM_EFFECTER_DATA_SIZE_UINT32:
        {
            auto rawValue = static_cast<uint32_t>(
                round(effecterValue - pdr->offset) / pdr->resolution);
            if (pdr->min_settable.value_u32 < pdr->max_settable.value_u32 &&
                (rawValue < pdr->min_settable.value_u32 ||
                 rawValue > pdr->max_settable.value_u32))
            {
                rc = PLDM_ERROR_INVALID_DATA;
            }
            value = rawValue;
            if (propertyType == "uint64_t")
            {
                auto tempValue = std::get<uint32_t>(value);
                value = static_cast<uint64_t>(tempValue);
            }
            else if (propertyType == "uint32_t")
            {
                auto tempValue = std::get<uint32_t>(value);
                value = static_cast<uint32_t>(tempValue);
            }
            break;
        }
        case PLDM_EFFECTER_DATA_SIZE_SINT32:
        {
            auto rawValue = static_cast<int32_t>(
                round(effecterValue - pdr->offset) / pdr->resolution);
            if (pdr->min_settable.value_s32 < pdr->max_settable.value_s32 &&
                (rawValue < pdr->min_settable.value_s32 ||
                 rawValue > pdr->max_settable.value_s32))
            {
                rc = PLDM_ERROR_INVALID_DATA;
            }
            value = rawValue;
            if (propertyType == "uint64_t")
            {
                auto tempValue = std::get<int32_t>(value);
                value = static_cast<uint64_t>(tempValue);
            }
            else if (propertyType == "uint32_t")
            {
                auto tempValue = std::get<int32_t>(value);
                value = static_cast<uint32_t>(tempValue);
            }
            break;
        }
    }

    return {rc, std::make_optional(std::move(value))};
}

/** @brief Function to convert the D-Bus value by PDR factor and effecter value.
 *  @param[in] pdr - The structure of pldm_numeric_effecter_value_pdr.
 *  @param[in] effecterDataSize - effecter value size.
 *  @param[in,out] effecterValue - effecter value.
 *  @param[in] propertyType - type of the D-Bus property.
 *
 *  @return std::pair<int, std::optional<PropertyValue>> - rc:Success or
 *          failure, PropertyValue: The value to be set
 */
std::pair<int, std::optional<pldm::utils::PropertyValue>>
    convertToDbusValue(const pldm_numeric_effecter_value_pdr* pdr,
                       uint8_t effecterDataSize, uint8_t* effecterValue,
                       std::string propertyType)
{
    if (effecterDataSize == PLDM_EFFECTER_DATA_SIZE_UINT8)
    {
        uint8_t currentValue = *(reinterpret_cast<uint8_t*>(&effecterValue[0]));
        return getEffecterRawValue<uint8_t>(pdr, currentValue, propertyType);
    }
    else if (effecterDataSize == PLDM_EFFECTER_DATA_SIZE_SINT8)
    {
        int8_t currentValue = *(reinterpret_cast<int8_t*>(&effecterValue[0]));
        return getEffecterRawValue<int8_t>(pdr, currentValue, propertyType);
    }
    else if (effecterDataSize == PLDM_EFFECTER_DATA_SIZE_UINT16)
    {
        uint16_t currentValue =
            *(reinterpret_cast<uint16_t*>(&effecterValue[0]));
        return getEffecterRawValue<uint16_t>(pdr, currentValue, propertyType);
    }
    else if (effecterDataSize == PLDM_EFFECTER_DATA_SIZE_SINT16)
    {
        int16_t currentValue = *(reinterpret_cast<int16_t*>(&effecterValue[0]));
        return getEffecterRawValue<int16_t>(pdr, currentValue, propertyType);
    }
    else if (effecterDataSize == PLDM_EFFECTER_DATA_SIZE_UINT32)
    {
        uint32_t currentValue =
            *(reinterpret_cast<uint32_t*>(&effecterValue[0]));
        return getEffecterRawValue<uint32_t>(pdr, currentValue, propertyType);
    }
    else if (effecterDataSize == PLDM_EFFECTER_DATA_SIZE_SINT32)
    {
        int32_t currentValue = *(reinterpret_cast<int32_t*>(&effecterValue[0]));
        return getEffecterRawValue<int32_t>(pdr, currentValue, propertyType);
    }
    else
    {
        error("Unknown Effecter Size {SIZE}", "SIZE", effecterDataSize);
        return {PLDM_ERROR, {}};
    }
}

/** @brief Function to set the effecter value requested by pldm requester
 *  @tparam[in] DBusInterface - DBus interface type
 *  @tparam[in] Handler - pldm::responder::platform::Handler
 *  @param[in] dBusIntf - The interface object of DBusInterface
 *  @param[in] handler - The interface object of
 *             pldm::responder::platform::Handler
 *  @param[in] effecterId - Effecter ID sent by the requester to act on
 *  @param[in] effecterDataSize - The bit width and format of the setting
 * 				value for the effecter
 *  @param[in] effecter_value - The setting value of numeric effecter being
 * 				requested.
 *  @param[in] effecterValueLength - The setting value length of numeric
 *              effecter being requested.
 *  @return - Success or failure in setting the states. Returns failure in
 * terms of PLDM completion codes if atleast one state fails to be set
 */
template <class DBusInterface, class Handler>
int setNumericEffecterValueHandler(const DBusInterface& dBusIntf,
                                   Handler& handler, uint16_t effecterId,
                                   uint8_t effecterDataSize,
                                   uint8_t* effecterValue,
                                   size_t effecterValueLength)
{
    constexpr auto effecterValueArrayLength = 4;
    pldm_numeric_effecter_value_pdr* pdr = nullptr;

    std::unique_ptr<pldm_pdr, decltype(&pldm_pdr_destroy)>
        numericEffecterPdrRepo(pldm_pdr_init(), pldm_pdr_destroy);
    if (!numericEffecterPdrRepo)
    {
        error("Failed to instantiate numeric effecter PDR repository");
        return PLDM_ERROR;
    }
    pldm::responder::pdr_utils::Repo numericEffecterPDRs(
        numericEffecterPdrRepo.get());
    pldm::responder::pdr::getRepoByType(handler.getRepo(), numericEffecterPDRs,
                                        PLDM_NUMERIC_EFFECTER_PDR);
    if (numericEffecterPDRs.empty())
    {
        error("The Numeric Effecter PDR repo is empty.");
        return PLDM_ERROR;
    }

    // Get the pdr structure of pldm_numeric_effecter_value_pdr according
    // to the effecterId
    pldm::responder::pdr_utils::PdrEntry pdrEntry{};
    auto pdrRecord = numericEffecterPDRs.getFirstRecord(pdrEntry);
    while (pdrRecord)
    {
        pdr = reinterpret_cast<pldm_numeric_effecter_value_pdr*>(pdrEntry.data);
        if (pdr->effecter_id != effecterId)
        {
            pdr = nullptr;
            pdrRecord = numericEffecterPDRs.getNextRecord(pdrRecord, pdrEntry);
            continue;
        }

        break;
    }

    if (!pdr)
    {
        return PLDM_PLATFORM_INVALID_EFFECTER_ID;
    }

    if (effecterValueLength != effecterValueArrayLength)
    {
        error("Incorrect effecter data size {SIZE}", "SIZE",
              effecterValueLength);
        return PLDM_ERROR_INVALID_DATA;
    }

    try
    {
        const auto& [dbusMappings,
                     dbusValMaps] = handler.getDbusObjMaps(effecterId);
        pldm::utils::DBusMapping dbusMapping{
            dbusMappings[0].objectPath, dbusMappings[0].interface,
            dbusMappings[0].propertyName, dbusMappings[0].propertyType};

        // convert to dbus effectervalue according to the factor
        auto [rc, dbusValue] = convertToDbusValue(
            pdr, effecterDataSize, effecterValue, dbusMappings[0].propertyType);
        if (rc != PLDM_SUCCESS)
        {
            return rc;
        }
        try
        {
            dBusIntf.setDbusProperty(dbusMapping, dbusValue.value());
        }
        catch (const std::exception& e)
        {
            error(
                "Failed to set property '{PROPERTY}', interface '{INTERFACE}' and path '{PATH}', error - {ERROR}",
                "PROPERTY", dbusMapping.propertyName, "INTERFACE",
                dbusMapping.interface, "PATH", dbusMapping.objectPath, "ERROR",
                e);
            return PLDM_ERROR;
        }
    }
    catch (const std::out_of_range& e)
    {
        error("Unknown effecter ID '{EFFECTERID}', error - {ERROR}",
              "EFFECTERID", effecterId, "ERROR", e);
        return PLDM_ERROR;
    }

    return PLDM_SUCCESS;
}

/** @brief Function to convert the D-Bus value based on effecter data size
 *         and create the response for getNumericEffecterValue request.
 *  @param[in] PropertyValue - D-Bus Value
 *  @param[in] effecterDataSize - effecter value size.
 *  @param[in,out] responsePtr - Response of getNumericEffecterValue.
 *  @param[in] responsePayloadLength - reponse length.
 *  @param[in] instanceId - instance id for response
 *
 *  @return PLDM_SUCCESS/PLDM_ERROR
 */
template <typename T>
int getEffecterValue(T propertyValue, uint8_t effecterDataSize,
                     pldm_msg* responsePtr, size_t responsePayloadLength,
                     uint8_t instanceId)
{
    switch (effecterDataSize)
    {
        case PLDM_EFFECTER_DATA_SIZE_UINT8:
        {
            uint8_t value = static_cast<uint8_t>(propertyValue);
            return (encode_get_numeric_effecter_value_resp(
                instanceId, PLDM_SUCCESS, effecterDataSize,
                EFFECTER_OPER_STATE_ENABLED_NOUPDATEPENDING, &value, &value,
                responsePtr, responsePayloadLength));
        }
        case PLDM_EFFECTER_DATA_SIZE_SINT8:
        {
            int8_t value = static_cast<int8_t>(propertyValue);
            return (encode_get_numeric_effecter_value_resp(
                instanceId, PLDM_SUCCESS, effecterDataSize,
                EFFECTER_OPER_STATE_ENABLED_NOUPDATEPENDING,
                reinterpret_cast<uint8_t*>(&value),
                reinterpret_cast<uint8_t*>(&value), responsePtr,
                responsePayloadLength));
        }
        case PLDM_EFFECTER_DATA_SIZE_UINT16:
        {
            uint16_t value = static_cast<uint16_t>(propertyValue);
            return (encode_get_numeric_effecter_value_resp(
                instanceId, PLDM_SUCCESS, effecterDataSize,
                EFFECTER_OPER_STATE_ENABLED_NOUPDATEPENDING,
                reinterpret_cast<uint8_t*>(&value),
                reinterpret_cast<uint8_t*>(&value), responsePtr,
                responsePayloadLength));
        }
        case PLDM_EFFECTER_DATA_SIZE_SINT16:
        {
            int16_t value = static_cast<int16_t>(propertyValue);
            return (encode_get_numeric_effecter_value_resp(
                instanceId, PLDM_SUCCESS, effecterDataSize,
                EFFECTER_OPER_STATE_ENABLED_NOUPDATEPENDING,
                reinterpret_cast<uint8_t*>(&value),
                reinterpret_cast<uint8_t*>(&value), responsePtr,
                responsePayloadLength));
        }
        case PLDM_EFFECTER_DATA_SIZE_UINT32:
        {
            uint32_t value = static_cast<uint32_t>(propertyValue);
            return (encode_get_numeric_effecter_value_resp(
                instanceId, PLDM_SUCCESS, effecterDataSize,
                EFFECTER_OPER_STATE_ENABLED_NOUPDATEPENDING,
                reinterpret_cast<uint8_t*>(&value),
                reinterpret_cast<uint8_t*>(&value), responsePtr,
                responsePayloadLength));
        }
        case PLDM_EFFECTER_DATA_SIZE_SINT32:
        {
            int32_t value = static_cast<int32_t>(propertyValue);
            return (encode_get_numeric_effecter_value_resp(
                instanceId, PLDM_SUCCESS, effecterDataSize,
                EFFECTER_OPER_STATE_ENABLED_NOUPDATEPENDING,
                reinterpret_cast<uint8_t*>(&value),
                reinterpret_cast<uint8_t*>(&value), responsePtr,
                responsePayloadLength));
        }
        default:
        {
            error("Unknown Effecter Size {SIZE}", "SIZE", effecterDataSize);
            return PLDM_ERROR;
        }
    }
}

/** @brief Function to convert the D-Bus value to the effector data size value
 *  @param[in] PropertyType - String contains the dataType of the Dbus value.
 *  @param[in] PropertyValue - Variant contains the D-Bus Value
 *  @param[in] effecterDataSize - effecter value size.
 *  @param[in,out] responsePtr - Response of getNumericEffecterValue.
 *  @param[in] responsePayloadLength - reponse length.
 *  @param[in] instanceId - instance id for response
 *
 *  @return PLDM_SUCCESS/PLDM_ERROR
 */
int getNumericEffecterValueHandler(const std::string& propertyType,
                                   pldm::utils::PropertyValue propertyValue,
                                   uint8_t effecterDataSize,
                                   pldm_msg* responsePtr,
                                   size_t responsePayloadLength,
                                   uint8_t instanceId)
{
    if (propertyType == "uint8_t")
    {
        uint8_t propVal = std::get<uint8_t>(propertyValue);
        return getEffecterValue<uint8_t>(propVal, effecterDataSize, responsePtr,
                                         responsePayloadLength, instanceId);
    }
    else if (propertyType == "uint16_t")
    {
        uint16_t propVal = std::get<uint16_t>(propertyValue);
        return getEffecterValue<uint16_t>(propVal, effecterDataSize,
                                          responsePtr, responsePayloadLength,
                                          instanceId);
    }
    else if (propertyType == "uint32_t")
    {
        uint32_t propVal = std::get<uint32_t>(propertyValue);
        return getEffecterValue<uint32_t>(propVal, effecterDataSize,
                                          responsePtr, responsePayloadLength,
                                          instanceId);
    }
    else if (propertyType == "uint64_t")
    {
        uint64_t propVal = std::get<uint64_t>(propertyValue);
        return getEffecterValue<uint64_t>(propVal, effecterDataSize,
                                          responsePtr, responsePayloadLength,
                                          instanceId);
    }
    else
    {
        error("Property type '{TYPE}' not supported", "TYPE", propertyType);
    }
    return PLDM_ERROR;
}

/** @brief Function to get the effecter details as data size, D-Bus property
 *         type, D-Bus Value
 *  @tparam[in] DBusInterface - DBus interface type
 *  @tparam[in] Handler - pldm::responder::platform::Handler
 *  @param[in] dBusIntf - The interface object of DBusInterface
 *  @param[in] handler - The interface object of
 *             pldm::responder::platform::Handler
 *  @param[in] effecterId - Effecter ID sent by the requester to act on
 *  @param[in] effecterDataSize - The bit width and format of the setting
 *             value for the effecter
 *  @param[in] propertyType - The data type of the D-Bus value
 *  @param[in] propertyValue - The value of numeric effecter being
 *                             requested.
 *  @return - Success or failure in getting the D-Bus property or the
 *  effecterId not found in the PDR repo
 */
template <class DBusInterface, class Handler>
int getNumericEffecterData(const DBusInterface& dBusIntf, Handler& handler,
                           uint16_t effecterId, uint8_t& effecterDataSize,
                           std::string& propertyType,
                           pldm::utils::PropertyValue& propertyValue)
{
    pldm_numeric_effecter_value_pdr* pdr = nullptr;

    std::unique_ptr<pldm_pdr, decltype(&pldm_pdr_destroy)>
        numericEffecterPdrRepo(pldm_pdr_init(), pldm_pdr_destroy);
    pldm::responder::pdr_utils::Repo numericEffecterPDRs(
        numericEffecterPdrRepo.get());
    pldm::responder::pdr::getRepoByType(handler.getRepo(), numericEffecterPDRs,
                                        PLDM_NUMERIC_EFFECTER_PDR);
    if (numericEffecterPDRs.empty())
    {
        error("The Numeric Effecter PDR repo is empty.");
        return PLDM_ERROR;
    }

    // Get the pdr structure of pldm_numeric_effecter_value_pdr according
    // to the effecterId
    pldm::responder::pdr_utils::PdrEntry pdrEntry{};
    auto pdrRecord = numericEffecterPDRs.getFirstRecord(pdrEntry);

    while (pdrRecord)
    {
        pdr = reinterpret_cast<pldm_numeric_effecter_value_pdr*>(pdrEntry.data);
        if (pdr->effecter_id != effecterId)
        {
            pdr = nullptr;
            pdrRecord = numericEffecterPDRs.getNextRecord(pdrRecord, pdrEntry);
            continue;
        }
        effecterDataSize = pdr->effecter_data_size;
        break;
    }

    if (!pdr)
    {
        error("Failed to find numeric effecter ID {EFFECTERID}", "EFFECTERID",
              effecterId);
        return PLDM_PLATFORM_INVALID_EFFECTER_ID;
    }

    pldm::utils::DBusMapping dbusMapping{};
    try
    {
        const auto& [dbusMappings,
                     dbusValMaps] = handler.getDbusObjMaps(effecterId);
        if (dbusMappings.size() > 0)
        {
            dbusMapping = {
                dbusMappings[0].objectPath, dbusMappings[0].interface,
                dbusMappings[0].propertyName, dbusMappings[0].propertyType};

            propertyValue = dBusIntf.getDbusPropertyVariant(
                dbusMapping.objectPath.c_str(),
                dbusMapping.propertyName.c_str(),
                dbusMapping.interface.c_str());
            propertyType = dbusMappings[0].propertyType;
        }
    }
    catch (const std::exception& e)
    {
        error(
            "Failed to do dbus mapping or the dbus query for the effecter ID '{EFFECTERID}', error - {ERROR}",
            "EFFECTERID", effecterId, "ERROR", e);
        error(
            "Dbus Details path [{PATH}], interface [{INTERFACE}] and  property [{PROPERTY}]",
            "PATH", dbusMapping.objectPath, "INTERFACE", dbusMapping.interface,
            "PROPERTY", dbusMapping.propertyName);
        return PLDM_ERROR;
    }

    return PLDM_SUCCESS;
}

} // namespace platform_numeric_effecter
} // namespace responder
} // namespace pldm