/*
 * Copyright 2020 Google Inc.
 *
 * 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 "pci.hpp"

#include "tool_errors.hpp"

extern "C"
{
#include <pciaccess.h>
} // extern "C"

#include "helper.hpp"

#include <stdplus/handle/managed.hpp>

#include <cstring>
#include <format>
#include <span>
#include <system_error>

namespace host_tool
{

namespace
{

/** @brief RAII wrapper and its destructor for creating a pci_device_iterator */
static void closeIt(struct pci_device_iterator*&& it,
                    const PciAccess* const& pci)
{
    pci->pci_iterator_destroy(it);
}
using It = stdplus::Managed<struct pci_device_iterator*,
                            const PciAccess* const>::Handle<closeIt>;

} // namespace

PciAccessBridge::PciAccessBridge(const struct pci_id_match* match, int bar,
                                 std::size_t dataOffset, std::size_t dataLength,
                                 const PciAccess* pci) :
    dataOffset(dataOffset),
    dataLength(dataLength), pci(pci)
{
    It it(pci->pci_id_match_iterator_create(match), pci);

    while ((dev = pci->pci_device_next(*it)))
    {
        int ret = pci->pci_device_probe(dev);
        if (ret)
        {
            throw std::system_error(ret, std::generic_category(),
                                    "Error probing PCI device");
        }

        /* Verify it's a memory-based bar. */
        if (!dev->regions[bar].is_IO)
            break;
    }

    if (!dev)
    {
        throw NotFoundException(std::format(
            "PCI device {:#04x}:{:#04x}", match->vendor_id, match->device_id));
    }

    std::fprintf(stderr, "Find [0x%x 0x%x] \n", match->vendor_id,
                 match->device_id);
    std::fprintf(stderr, "bar%d[0x%x] \n", bar,
                 static_cast<unsigned int>(dev->regions[bar].base_addr));

    size = dev->regions[bar].size;
    int ret = pci->pci_device_map_range(
        dev, dev->regions[bar].base_addr, dev->regions[bar].size,
        PCI_DEV_MAP_FLAG_WRITABLE, reinterpret_cast<void**>(&addr));
    if (ret)
    {
        throw std::system_error(ret, std::generic_category(),
                                "Error mapping PCI device memory");
    }
}

PciAccessBridge::~PciAccessBridge()
{
    int ret = pci->pci_device_unmap_range(dev, addr, size);

    if (ret)
    {
        std::fprintf(stderr, "Error while unmapping PCI device memory: %s\n",
                     std::strerror(ret));
    }
}

void PciAccessBridge::write(const std::span<const std::uint8_t> data)
{
    if (data.size() > dataLength)
    {
        throw ToolException(
            std::format("Write of {} bytes exceeds maximum of {}", data.size(),
                        dataLength));
    }

    memcpyAligned(addr + dataOffset, data.data(), data.size());
}

void NuvotonPciBridge::enableBridge()
{
    std::uint8_t value;
    int ret;

    /* TODO: pci_device_disable support is missing in libpciaccess. Add it
     * to the disableBridge() once it is available.
     * https://gitlab.freedesktop.org/xorg/lib/libpciaccess/-/merge_requests/17
     */

    pci->pci_device_enable(dev);

    /* We need to retain this direct write to config space even though
     * pci_device_enable() should do it. Because currently disabling is done
     * through write to config space and not done through the proper api.
     * So libpciaccess ref count does not reset on disable. The
     * pci_device_enable() above will not do anything the second time.
     */
    ret = pci->pci_device_cfg_read_u8(dev, &value, bridge);
    if (ret)
    {
        throw std::system_error(ret, std::generic_category(),
                                "Error reading bridge status");
    }

    if (value & bridgeEnabled)
    {
        std::fprintf(stderr, "Bridge already enabled\n");
        return;
    }

    value |= bridgeEnabled;

    ret = pci->pci_device_cfg_write_u8(dev, value, bridge);
    if (ret)
    {
        throw std::system_error(ret, std::generic_category(),
                                "Error enabling bridge");
    }
}

void NuvotonPciBridge::disableBridge()
{
    std::uint8_t value;
    int ret;

    ret = pci->pci_device_cfg_read_u8(dev, &value, bridge);
    if (ret)
    {
        std::fprintf(stderr, "Error reading bridge status: %s\n",
                     std::strerror(ret));
        return;
    }
    value &= ~bridgeEnabled;

    ret = pci->pci_device_cfg_write_u8(dev, value, bridge);
    if (ret)
    {
        std::fprintf(stderr, "Error disabling bridge: %s\n",
                     std::strerror(ret));
    }
}

void AspeedPciBridge::enableBridge()
{
    /* We sent the open command before this, so the window should be open and
     * the bridge enabled on the BMC.
     */
    std::uint32_t value;

    /* TODO: pci_device_disable support is missing in libpciaccess. Add it
     * to the disableBridge() once it is available.
     * https://gitlab.freedesktop.org/xorg/lib/libpciaccess/-/merge_requests/17
     */

    pci->pci_device_enable(dev);

    /* We need to retain this direct write to config space even though
     * pci_device_enable() should do it. Because currently disabling is done
     * through write to config space and not done through the proper api.
     * So libpciaccess ref count does not reset on disable. The
     * pci_device_enable() above will not do anything the second time.
     */

    std::memcpy(&value, addr + config, sizeof(value));

    if (0 == (value & bridgeEnabled))
    {
        std::fprintf(stderr, "Bridge not enabled - Enabling from host\n");

        value |= bridgeEnabled;
        std::memcpy(addr + config, &value, sizeof(value));
    }

    std::fprintf(stderr, "The bridge is enabled!\n");
}

void AspeedPciBridge::disableBridge()
{
    /* addr is valid if the constructor completed */

    /* Read current value, and just blindly unset the bit. */
    std::uint32_t value;
    std::memcpy(&value, addr + config, sizeof(value));

    value &= ~bridgeEnabled;
    std::memcpy(addr + config, &value, sizeof(value));
}

void AspeedPciBridge::configure(const ipmi_flash::PciConfigResponse& configResp)
{
    std::fprintf(stderr, "Received address: 0x%x\n", configResp.address);

    /* Configure the mmio to point there. */
    std::memcpy(addr + bridge, &configResp.address, sizeof(configResp.address));
}

} // namespace host_tool