#include "sensordatahandler.hpp"

#include "sensorhandler.hpp"

#include <bitset>
#include <ipmid/types.hpp>
#include <ipmid/utils.hpp>
#include <optional>
#include <phosphor-logging/elog-errors.hpp>
#include <phosphor-logging/log.hpp>
#include <sdbusplus/message/types.hpp>
#include <xyz/openbmc_project/Common/error.hpp>

#if __has_include(<filesystem>)
#include <filesystem>
#elif __has_include(<experimental/filesystem>)
#include <experimental/filesystem>
namespace std
{
// splice experimental::filesystem into std
namespace filesystem = std::experimental::filesystem;
} // namespace std
#else
#error filesystem not available
#endif

namespace ipmi
{
namespace sensor
{

namespace variant_ns = sdbusplus::message::variant_ns;

using namespace phosphor::logging;
using InternalFailure =
    sdbusplus::xyz::openbmc_project::Common::Error::InternalFailure;

static constexpr auto MAPPER_BUSNAME = "xyz.openbmc_project.ObjectMapper";
static constexpr auto MAPPER_PATH = "/xyz/openbmc_project/object_mapper";
static constexpr auto MAPPER_INTERFACE = "xyz.openbmc_project.ObjectMapper";

/** @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)
{
    auto depth = 0;
    auto mapperCall = bus.new_method_call(MAPPER_BUSNAME, MAPPER_PATH,
                                          MAPPER_INTERFACE, "GetSubTree");
    mapperCall.append("/");
    mapperCall.append(depth);
    mapperCall.append(std::vector<Interface>({interface}));

    auto mapperResponseMsg = bus.call(mapperCall);
    if (mapperResponseMsg.is_method_error())
    {
        log<level::ERR>("Mapper GetSubTree failed",
                        entry("PATH=%s", path.c_str()),
                        entry("INTERFACE=%s", interface.c_str()));
        elog<InternalFailure>();
    }

    MapperResponseType mapperResponse;
    mapperResponseMsg.read(mapperResponse);
    if (mapperResponse.empty())
    {
        log<level::ERR>("Invalid mapper response",
                        entry("PATH=%s", path.c_str()),
                        entry("INTERFACE=%s", interface.c_str()));
        elog<InternalFailure>();
    }

    if (path.empty())
    {
        // Get the first one if the path is not in list.
        return std::make_pair(mapperResponse.begin()->first,
                              mapperResponse.begin()->second.begin()->first);
    }
    const auto& iter = mapperResponse.find(path);
    if (iter == mapperResponse.end())
    {
        log<level::ERR>("Couldn't find D-Bus path",
                        entry("PATH=%s", path.c_str()),
                        entry("INTERFACE=%s", interface.c_str()));
        elog<InternalFailure>();
    }
    return std::make_pair(iter->first, iter->second.begin()->first);
}

AssertionSet getAssertionSet(const SetSensorReadingReq& cmdData)
{
    Assertion assertionStates =
        (static_cast<Assertion>(cmdData.assertOffset8_14)) << 8 |
        cmdData.assertOffset0_7;
    Deassertion deassertionStates =
        (static_cast<Deassertion>(cmdData.deassertOffset8_14)) << 8 |
        cmdData.deassertOffset0_7;
    return std::make_pair(assertionStates, deassertionStates);
}

ipmi_ret_t updateToDbus(IpmiUpdateData& msg)
{
    sdbusplus::bus::bus bus{ipmid_get_sd_bus_connection()};
    try
    {
        auto serviceResponseMsg = bus.call(msg);
        if (serviceResponseMsg.is_method_error())
        {
            log<level::ERR>("Error in D-Bus call");
            return IPMI_CC_UNSPECIFIED_ERROR;
        }
    }
    catch (InternalFailure& e)
    {
        commit<InternalFailure>();
        return IPMI_CC_UNSPECIFIED_ERROR;
    }
    return IPMI_CC_OK;
}

namespace get
{

SensorName nameParentLeaf(const Info& sensorInfo)
{
    const auto pos = sensorInfo.sensorPath.find_last_of('/');
    const auto leaf = sensorInfo.sensorPath.substr(pos + 1);

    const auto remaining = sensorInfo.sensorPath.substr(0, pos);

    const auto parentPos = remaining.find_last_of('/');
    auto parent = remaining.substr(parentPos + 1);

    parent += "_" + leaf;
    return parent;
}

GetSensorResponse mapDbusToAssertion(const Info& sensorInfo,
                                     const InstancePath& path,
                                     const DbusInterface& interface)
{
    sdbusplus::bus::bus bus{ipmid_get_sd_bus_connection()};
    GetSensorResponse response{};
    auto responseData = reinterpret_cast<GetReadingResponse*>(response.data());

    auto service = ipmi::getService(bus, interface, path);

    const auto& interfaceList = sensorInfo.propertyInterfaces;

    for (const auto& interface : interfaceList)
    {
        for (const auto& property : interface.second)
        {
            auto propValue = ipmi::getDbusProperty(
                bus, service, path, interface.first, property.first);

            for (const auto& value : std::get<OffsetValueMap>(property.second))
            {
                if (propValue == value.second.assert)
                {
                    setOffset(value.first, responseData);
                    break;
                }
            }
        }
    }

    return response;
}

GetSensorResponse assertion(const Info& sensorInfo)
{
    return mapDbusToAssertion(sensorInfo, sensorInfo.sensorPath,
                              sensorInfo.sensorInterface);
}

GetSensorResponse eventdata2(const Info& sensorInfo)
{
    sdbusplus::bus::bus bus{ipmid_get_sd_bus_connection()};
    GetSensorResponse response{};
    auto responseData = reinterpret_cast<GetReadingResponse*>(response.data());

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

    const auto& interfaceList = sensorInfo.propertyInterfaces;

    for (const auto& interface : interfaceList)
    {
        for (const auto& property : interface.second)
        {
            auto propValue =
                ipmi::getDbusProperty(bus, service, sensorInfo.sensorPath,
                                      interface.first, property.first);

            for (const auto& value : std::get<OffsetValueMap>(property.second))
            {
                if (propValue == value.second.assert)
                {
                    setReading(value.first, responseData);
                    break;
                }
            }
        }
    }

    return response;
}

} // namespace get

namespace set
{

IpmiUpdateData makeDbusMsg(const std::string& updateInterface,
                           const std::string& sensorPath,
                           const std::string& command,
                           const std::string& sensorInterface)
{
    sdbusplus::bus::bus bus{ipmid_get_sd_bus_connection()};
    using namespace std::string_literals;

    auto dbusService = getService(bus, sensorInterface, sensorPath);

    return bus.new_method_call(dbusService.c_str(), sensorPath.c_str(),
                               updateInterface.c_str(), command.c_str());
}

ipmi_ret_t eventdata(const SetSensorReadingReq& cmdData, const Info& sensorInfo,
                     uint8_t data)
{
    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);
        const auto& iter = std::get<OffsetValueMap>(property.second).find(data);
        if (iter == std::get<OffsetValueMap>(property.second).end())
        {
            log<level::ERR>("Invalid event data");
            return IPMI_CC_PARM_OUT_OF_RANGE;
        }
        msg.append(iter->second.assert);
    }
    return updateToDbus(msg);
}

ipmi_ret_t assertion(const SetSensorReadingReq& cmdData, const Info& sensorInfo)
{
    std::bitset<16> assertionSet(getAssertionSet(cmdData).first);
    std::bitset<16> deassertionSet(getAssertionSet(cmdData).second);
    auto bothSet = assertionSet ^ deassertionSet;

    const auto& interface = sensorInfo.propertyInterfaces.begin();

    for (const auto& property : interface->second)
    {
        std::optional<Value> tmp;
        for (const auto& value : std::get<OffsetValueMap>(property.second))
        {
            if (bothSet.size() <= value.first || !bothSet.test(value.first))
            {
                // A BIOS shouldn't do this but ignore if they do.
                continue;
            }

            if (assertionSet.test(value.first))
            {
                tmp = value.second.assert;
                break;
            }
            if (deassertionSet.test(value.first))
            {
                tmp = value.second.deassert;
                break;
            }
        }

        if (tmp)
        {
            auto msg = makeDbusMsg("org.freedesktop.DBus.Properties",
                                   sensorInfo.sensorPath, "Set",
                                   sensorInfo.sensorInterface);
            msg.append(interface->first);
            msg.append(property.first);
            msg.append(*tmp);

            auto rc = updateToDbus(msg);
            if (rc)
            {
                return rc;
            }
        }
    }

    return IPMI_CC_OK;
}

} // namespace set

namespace notify
{

IpmiUpdateData makeDbusMsg(const std::string& updateInterface,
                           const std::string& sensorPath,
                           const std::string& command,
                           const std::string& sensorInterface)
{
    sdbusplus::bus::bus bus{ipmid_get_sd_bus_connection()};
    using namespace std::string_literals;

    static const auto dbusPath = "/xyz/openbmc_project/inventory"s;
    std::string dbusService = ipmi::getService(bus, updateInterface, dbusPath);

    return bus.new_method_call(dbusService.c_str(), dbusPath.c_str(),
                               updateInterface.c_str(), command.c_str());
}

ipmi_ret_t assertion(const SetSensorReadingReq& cmdData, const Info& sensorInfo)
{
    auto msg = makeDbusMsg(sensorInfo.sensorInterface, sensorInfo.sensorPath,
                           "Notify", sensorInfo.sensorInterface);

    std::bitset<16> assertionSet(getAssertionSet(cmdData).first);
    std::bitset<16> deassertionSet(getAssertionSet(cmdData).second);
    ipmi::sensor::ObjectMap objects;
    ipmi::sensor::InterfaceMap interfaces;
    for (const auto& interface : sensorInfo.propertyInterfaces)
    {
        // For a property like functional state the result will be
        // calculated based on the true value of all conditions.
        for (const auto& property : interface.second)
        {
            ipmi::sensor::PropertyMap props;
            bool valid = false;
            auto result = true;
            for (const auto& value : std::get<OffsetValueMap>(property.second))
            {
                if (assertionSet.test(value.first))
                {
                    // Skip update if skipOn is ASSERT
                    if (SkipAssertion::ASSERT == value.second.skip)
                    {
                        return IPMI_CC_OK;
                    }
                    result =
                        result && variant_ns::get<bool>(value.second.assert);
                    valid = true;
                }
                else if (deassertionSet.test(value.first))
                {
                    // Skip update if skipOn is DEASSERT
                    if (SkipAssertion::DEASSERT == value.second.skip)
                    {
                        return IPMI_CC_OK;
                    }
                    result =
                        result && variant_ns::get<bool>(value.second.deassert);
                    valid = true;
                }
            }
            for (const auto& value :
                 std::get<PreReqOffsetValueMap>(property.second))
            {
                if (assertionSet.test(value.first))
                {
                    result =
                        result && variant_ns::get<bool>(value.second.assert);
                }
                else if (deassertionSet.test(value.first))
                {
                    result =
                        result && variant_ns::get<bool>(value.second.deassert);
                }
            }
            if (valid)
            {
                props.emplace(property.first, result);
                interfaces.emplace(interface.first, std::move(props));
            }
        }
    }

    objects.emplace(sensorInfo.sensorPath, std::move(interfaces));
    msg.append(std::move(objects));
    return updateToDbus(msg);
}

} // namespace notify

namespace inventory
{

namespace get
{

GetSensorResponse assertion(const Info& sensorInfo)
{
    namespace fs = std::filesystem;

    fs::path path{ipmi::sensor::inventoryRoot};
    path += sensorInfo.sensorPath;

    return ipmi::sensor::get::mapDbusToAssertion(
        sensorInfo, path.string(),
        sensorInfo.propertyInterfaces.begin()->first);
}

} // namespace get

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