/*
// Copyright (c) 2020 Intel 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.
*/

#include "cpuinfo.hpp"
#include "cpuinfo_utils.hpp"
#include "speed_select.hpp"

#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/ioctl.h>

#include <boost/asio/io_service.hpp>
#include <boost/asio/steady_timer.hpp>

#include <iostream>
#include <optional>
#include <sstream>
#include <string>

extern "C"
{
#include <i2c/smbus.h>
#include <linux/i2c-dev.h>
}

#include <peci.h>

#include <phosphor-logging/log.hpp>
#include <sdbusplus/asio/object_server.hpp>

namespace phosphor
{
namespace cpu_info
{
static constexpr bool debug = false;
static constexpr const char* cpuInterfaceName =
    "xyz.openbmc_project.Inventory.Decorator.Asset";
static constexpr const char* cpuProcessName =
    "xyz.openbmc_project.Smbios.MDR_V2";

// constants for reading SSPEC or QDF string from PIROM
// Currently, they are the same for platforms with icx
static constexpr uint8_t defaultI2cBus = 13;
static constexpr uint8_t defaultI2cSlaveAddr0 = 0x50;
static constexpr uint8_t sspecRegAddr = 0xd;
static constexpr uint8_t sspecSize = 6;

using CPUInfoMap = boost::container::flat_map<size_t, std::shared_ptr<CPUInfo>>;

static CPUInfoMap cpuInfoMap = {};

static boost::container::flat_map<size_t,
                                  std::unique_ptr<sdbusplus::bus::match_t>>
    cpuUpdatedMatch = {};

static std::optional<std::string> readSSpec(uint8_t bus, uint8_t slaveAddr,
                                            uint8_t regAddr, size_t count)
{
    unsigned long funcs = 0;
    std::string devPath = "/dev/i2c-" + std::to_string(bus);

    int fd = ::open(devPath.c_str(), O_RDWR);
    if (fd < 0)
    {
        phosphor::logging::log<phosphor::logging::level::ERR>(
            "Error in open!",
            phosphor::logging::entry("PATH=%s", devPath.c_str()),
            phosphor::logging::entry("SLAVEADDR=0x%x", slaveAddr));
        return std::nullopt;
    }

    if (::ioctl(fd, I2C_FUNCS, &funcs) < 0)
    {
        phosphor::logging::log<phosphor::logging::level::ERR>(
            "Error in I2C_FUNCS!",
            phosphor::logging::entry("PATH=%s", devPath.c_str()),
            phosphor::logging::entry("SLAVEADDR=0x%x", slaveAddr));
        ::close(fd);
        return std::nullopt;
    }

    if (!(funcs & I2C_FUNC_SMBUS_READ_BYTE_DATA))
    {
        phosphor::logging::log<phosphor::logging::level::ERR>(
            "i2c bus does not support read!",
            phosphor::logging::entry("PATH=%s", devPath.c_str()),
            phosphor::logging::entry("SLAVEADDR=0x%x", slaveAddr));
        ::close(fd);
        return std::nullopt;
    }

    if (::ioctl(fd, I2C_SLAVE_FORCE, slaveAddr) < 0)
    {
        phosphor::logging::log<phosphor::logging::level::ERR>(
            "Error in I2C_SLAVE_FORCE!",
            phosphor::logging::entry("PATH=%s", devPath.c_str()),
            phosphor::logging::entry("SLAVEADDR=0x%x", slaveAddr));
        ::close(fd);
        return std::nullopt;
    }

    int value = 0;
    std::string sspec;
    sspec.reserve(count);

    for (size_t i = 0; i < count; i++)
    {
        value = ::i2c_smbus_read_byte_data(fd, regAddr + i);
        if (value < 0)
        {
            phosphor::logging::log<phosphor::logging::level::ERR>(
                "Error in i2c read!",
                phosphor::logging::entry("PATH=%s", devPath.c_str()),
                phosphor::logging::entry("SLAVEADDR=0x%x", slaveAddr));
            ::close(fd);
            return std::nullopt;
        }
        if (!std::isprint(static_cast<unsigned char>(value)))
        {
            phosphor::logging::log<phosphor::logging::level::ERR>(
                "Non printable value in sspec, ignored.");
            continue;
        }
        // sspec always starts with S,
        // if not assume it is QDF string which starts at offset 2
        if (i == 0 && static_cast<unsigned char>(value) != 'S')
        {
            i = 1;
            continue;
        }
        sspec.push_back(static_cast<unsigned char>(value));
    }
    ::close(fd);
    return sspec;
}

static void setAssetProperty(
    const std::shared_ptr<sdbusplus::asio::connection>& conn, const int& cpu,
    const std::vector<std::pair<std::string, std::string>>& propValues)
{
    // cpuId from configuration is one based as
    // dbus object path used by smbios is 0 based
    const std::string objectPath = cpuPath + std::to_string(cpu - 1);
    for (const auto& prop : propValues)
    {
        conn->async_method_call(
            [](const boost::system::error_code ec) {
                if (ec)
                {
                    phosphor::logging::log<phosphor::logging::level::ERR>(
                        "Cannot get CPU property!");
                    return;
                }
            },
            cpuProcessName, objectPath.c_str(),
            "org.freedesktop.DBus.Properties", "Set", cpuInterfaceName,
            prop.first.c_str(), std::variant<std::string>{prop.second});
    }
}

static void createCpuUpdatedMatch(
    const std::shared_ptr<sdbusplus::asio::connection>& conn, const int cpu,
    const std::vector<std::pair<std::string, std::string>>& propValues)
{
    if (cpuUpdatedMatch[cpu])
    {
        return;
    }

    const std::string objectPath = cpuPath + std::to_string(cpu - 1);

    cpuUpdatedMatch.insert_or_assign(
        cpu,
        std::make_unique<sdbusplus::bus::match::match>(
            static_cast<sdbusplus::bus::bus&>(*conn),
            sdbusplus::bus::match::rules::interfacesAdded() +
                sdbusplus::bus::match::rules::argNpath(0, objectPath.c_str()),
            [conn, cpu, propValues](sdbusplus::message::message& msg) {
                sdbusplus::message::object_path objectName;
                boost::container::flat_map<
                    std::string,
                    boost::container::flat_map<
                        std::string, std::variant<std::string, uint64_t>>>
                    msgData;

                msg.read(objectName, msgData);

                // Check for xyz.openbmc_project.Inventory.Item.Cpu
                // interface match
                const auto& intfFound = msgData.find(cpuInterfaceName);
                if (msgData.end() != intfFound)
                {
                    setAssetProperty(conn, cpu, propValues);
                }
            }));
}

static void
    getProcessorInfo(boost::asio::io_service& io,
                     const std::shared_ptr<sdbusplus::asio::connection>& conn,
                     const size_t& cpu)
{
    if (cpuInfoMap.find(cpu) == cpuInfoMap.end() || cpuInfoMap[cpu] == nullptr)
    {
        std::cerr << "No information found for cpu " << cpu << "\n";
        return;
    }

    if (cpuInfoMap[cpu]->id != cpu)
    {
        std::cerr << "Incorrect CPU id " << (unsigned)cpuInfoMap[cpu]->id
                  << " expect " << cpu << "\n";
        return;
    }

    uint8_t cpuAddr = cpuInfoMap[cpu]->peciAddr;
    uint8_t i2cBus = cpuInfoMap[cpu]->i2cBus;
    uint8_t i2cDevice = cpuInfoMap[cpu]->i2cDevice;

    uint8_t cc = 0;
    CPUModel model{};
    uint8_t stepping = 0;

    if (peci_GetCPUID(cpuAddr, &model, &stepping, &cc) != PECI_CC_SUCCESS)
    {
        // Start the PECI check loop
        auto waitTimer = std::make_shared<boost::asio::steady_timer>(io);
        waitTimer->expires_after(
            std::chrono::seconds(phosphor::cpu_info::peciCheckInterval));

        waitTimer->async_wait(
            [waitTimer, &io, conn, cpu](const boost::system::error_code& ec) {
                if (ec)
                {
                    // operation_aborted is expected if timer is canceled
                    // before completion.
                    if (ec != boost::asio::error::operation_aborted)
                    {
                        phosphor::logging::log<phosphor::logging::level::ERR>(
                            "info update timer async_wait failed ",
                            phosphor::logging::entry("EC=0x%x", ec.value()));
                    }
                    return;
                }
                getProcessorInfo(io, conn, cpu);
            });
        return;
    }

    switch (model)
    {
        case icx:
        {
            // PPIN can be read through PCS 19
            static constexpr uint8_t u8Size = 4; // default to a DWORD
            static constexpr uint8_t u8PPINPkgIndex = 19;
            static constexpr uint16_t u16PPINPkgParamHigh = 2;
            static constexpr uint16_t u16PPINPkgParamLow = 1;
            uint64_t cpuPPIN = 0;
            uint32_t u32PkgValue = 0;

            int ret =
                peci_RdPkgConfig(cpuAddr, u8PPINPkgIndex, u16PPINPkgParamLow,
                                 u8Size, (uint8_t*)&u32PkgValue, &cc);
            if (0 != ret)
            {
                phosphor::logging::log<phosphor::logging::level::ERR>(
                    "peci read package config failed at address",
                    phosphor::logging::entry("PECIADDR=0x%x",
                                             (unsigned)cpuAddr),
                    phosphor::logging::entry("CC=0x%x", cc));
                u32PkgValue = 0;
            }

            cpuPPIN = u32PkgValue;
            ret = peci_RdPkgConfig(cpuAddr, u8PPINPkgIndex, u16PPINPkgParamHigh,
                                   u8Size, (uint8_t*)&u32PkgValue, &cc);
            if (0 != ret)
            {
                phosphor::logging::log<phosphor::logging::level::ERR>(
                    "peci read package config failed at address",
                    phosphor::logging::entry("PECIADDR=0x%x",
                                             (unsigned)cpuAddr),
                    phosphor::logging::entry("CC=0x%x", cc));
                cpuPPIN = 0;
                u32PkgValue = 0;
            }

            cpuPPIN |= static_cast<uint64_t>(u32PkgValue) << 32;

            std::vector<std::pair<std::string, std::string>> values;

            // set SerialNumber if cpuPPIN is valid
            if (0 != cpuPPIN)
            {
                std::stringstream stream;
                stream << std::hex << cpuPPIN;
                std::string serialNumber(stream.str());
                // cpuInfo->serialNumber(serialNumber);
                values.emplace_back(
                    std::make_pair("SerialNumber", serialNumber));
            }

            std::optional<std::string> sspec =
                readSSpec(i2cBus, i2cDevice, sspecRegAddr, sspecSize);

            // cpuInfo->model(sspec.value_or(""));
            values.emplace_back(std::make_pair("Model", sspec.value_or("")));

            /// \todo in followup patch
            // CPUInfo is created by this service
            // update the below logic, which is needed because smbios
            // service creates the cpu object
            createCpuUpdatedMatch(conn, cpu, values);
            setAssetProperty(conn, cpu, values);
            break;
        }
        default:
            phosphor::logging::log<phosphor::logging::level::INFO>(
                "in-compatible cpu for cpu asset info");
            break;
    }
}

/**
 * Get cpu and pirom address
 */
static void
    getCpuAddress(boost::asio::io_service& io,
                  const std::shared_ptr<sdbusplus::asio::connection>& conn,
                  const std::string& service, const std::string& object,
                  const std::string& interface)
{
    conn->async_method_call(
        [&io, conn](boost::system::error_code ec,
                    const boost::container::flat_map<
                        std::string,
                        std::variant<std::string, uint64_t, uint32_t, uint16_t,
                                     std::vector<std::string>>>& properties) {
            const uint64_t* value = nullptr;
            uint8_t peciAddress = 0;
            uint8_t i2cBus = defaultI2cBus;
            uint8_t i2cDevice;
            bool i2cDeviceFound = false;
            size_t cpu = 0;

            if (ec)
            {
                std::cerr << "DBUS response error " << ec.value() << ": "
                          << ec.message() << "\n";
                return;
            }

            for (const auto& property : properties)
            {
                std::cerr << "property " << property.first << "\n";
                if (property.first == "Address")
                {
                    value = std::get_if<uint64_t>(&property.second);
                    if (value != nullptr)
                    {
                        peciAddress = static_cast<uint8_t>(*value);
                    }
                }
                if (property.first == "CpuID")
                {
                    value = std::get_if<uint64_t>(&property.second);
                    if (value != nullptr)
                    {
                        cpu = static_cast<size_t>(*value);
                    }
                }
                if (property.first == "PiromI2cAddress")
                {
                    value = std::get_if<uint64_t>(&property.second);
                    if (value != nullptr)
                    {
                        i2cDevice = static_cast<uint8_t>(*value);
                        i2cDeviceFound = true;
                    }
                }
                if (property.first == "PiromI2cBus")
                {
                    value = std::get_if<uint64_t>(&property.second);
                    if (value != nullptr)
                    {
                        i2cBus = static_cast<uint8_t>(*value);
                    }
                }
            }

            ///\todo replace this with present + power state
            if (cpu != 0 && peciAddress != 0)
            {
                if (!i2cDeviceFound)
                {
                    i2cDevice = defaultI2cSlaveAddr0 + cpu - 1;
                }
                cpuInfoMap.insert_or_assign(
                    cpu, std::make_shared<CPUInfo>(cpu, peciAddress, i2cBus,
                                                   i2cDevice));

                getProcessorInfo(io, conn, cpu);
            }
        },
        service, object, "org.freedesktop.DBus.Properties", "GetAll",
        interface);
}

/**
 * D-Bus client: to get platform specific configs
 */
static void getCpuConfiguration(
    boost::asio::io_service& io,
    const std::shared_ptr<sdbusplus::asio::connection>& conn,
    sdbusplus::asio::object_server& objServer)
{
    // Get the Cpu configuration
    // In case it's not available, set a match for it
    static std::unique_ptr<sdbusplus::bus::match::match> cpuConfigMatch =
        std::make_unique<sdbusplus::bus::match::match>(
            *conn,
            "type='signal',interface='org.freedesktop.DBus.Properties',member='"
            "PropertiesChanged',arg0='xyz.openbmc_project."
            "Configuration.XeonCPU'",
            [&io, conn, &objServer](sdbusplus::message::message& msg) {
                std::cerr << "get cpu configuration match\n";
                static boost::asio::steady_timer filterTimer(io);
                filterTimer.expires_after(
                    std::chrono::seconds(configCheckInterval));

                filterTimer.async_wait(
                    [&io, conn,
                     &objServer](const boost::system::error_code& ec) {
                        if (ec == boost::asio::error::operation_aborted)
                        {
                            return; // we're being canceled
                        }
                        else if (ec)
                        {
                            std::cerr << "Error: " << ec.message() << "\n";
                            return;
                        }
                        getCpuConfiguration(io, conn, objServer);
                    });
            });

    conn->async_method_call(
        [&io, conn](
            boost::system::error_code ec,
            const std::vector<std::pair<
                std::string,
                std::vector<std::pair<std::string, std::vector<std::string>>>>>&
                subtree) {
            if constexpr (debug)
                std::cerr << "async_method_call callback\n";

            if (ec)
            {
                std::cerr << "error with async_method_call\n";
                return;
            }
            if (subtree.empty())
            {
                // No config data yet, so wait for the match
                return;
            }

            for (const auto& object : subtree)
            {
                for (const auto& service : object.second)
                {
                    getCpuAddress(io, conn, service.first, object.first,
                                  "xyz.openbmc_project.Configuration.XeonCPU");
                }
            }
            if constexpr (debug)
                std::cerr << "getCpuConfiguration callback complete\n";

            return;
        },
        "xyz.openbmc_project.ObjectMapper",
        "/xyz/openbmc_project/object_mapper",
        "xyz.openbmc_project.ObjectMapper", "GetSubTree",
        "/xyz/openbmc_project/", 0,
        std::array<const char*, 1>{
            "xyz.openbmc_project.Configuration.XeonCPU"});
}

} // namespace cpu_info
} // namespace phosphor

int main(int argc, char* argv[])
{
    // setup connection to dbus
    boost::asio::io_service io;
    std::shared_ptr<sdbusplus::asio::connection> conn =
        std::make_shared<sdbusplus::asio::connection>(io);

    // CPUInfo Object
    conn->request_name(phosphor::cpu_info::cpuInfoObject);
    sdbusplus::asio::object_server server =
        sdbusplus::asio::object_server(conn);
    sdbusplus::bus::bus& bus = static_cast<sdbusplus::bus::bus&>(*conn);
    sdbusplus::server::manager::manager objManager(
        bus, "/xyz/openbmc_project/inventory");

    cpu_info::hostStateSetup(conn);

    cpu_info::sst::init(io, conn);

    // shared_ptr conn is global for the service
    // const reference of conn is passed to async calls
    phosphor::cpu_info::getCpuConfiguration(io, conn, server);

    io.run();

    return 0;
}