/*
 * PCIe Data Object Exchange
 *
 * Copyright (C) 2021 Avery Design Systems, Inc.
 *
 * This work is licensed under the terms of the GNU GPL, version 2 or later.
 * See the COPYING file in the top-level directory.
 */

#include "qemu/osdep.h"
#include "qemu/log.h"
#include "qemu/error-report.h"
#include "qapi/error.h"
#include "qemu/range.h"
#include "hw/pci/pci.h"
#include "hw/pci/pcie.h"
#include "hw/pci/pcie_doe.h"
#include "hw/pci/msi.h"
#include "hw/pci/msix.h"

#define DWORD_BYTE 4

typedef struct DoeDiscoveryReq {
    DOEHeader header;
    uint8_t index;
    uint8_t reserved[3];
} QEMU_PACKED DoeDiscoveryReq;

typedef struct DoeDiscoveryRsp {
    DOEHeader header;
    uint16_t vendor_id;
    uint8_t data_obj_type;
    uint8_t next_index;
} QEMU_PACKED DoeDiscoveryRsp;

static bool pcie_doe_discovery(DOECap *doe_cap)
{
    DoeDiscoveryReq *req = pcie_doe_get_write_mbox_ptr(doe_cap);
    DoeDiscoveryRsp rsp;
    uint8_t index = req->index;
    DOEProtocol *prot;

    /* Discard request if length does not match DoeDiscoveryReq */
    if (pcie_doe_get_obj_len(req) <
        DIV_ROUND_UP(sizeof(DoeDiscoveryReq), DWORD_BYTE)) {
        return false;
    }

    rsp.header = (DOEHeader) {
        .vendor_id = PCI_VENDOR_ID_PCI_SIG,
        .data_obj_type = PCI_SIG_DOE_DISCOVERY,
        .length = DIV_ROUND_UP(sizeof(DoeDiscoveryRsp), DWORD_BYTE),
    };

    /* Point to the requested protocol, index 0 must be Discovery */
    if (index == 0) {
        rsp.vendor_id = PCI_VENDOR_ID_PCI_SIG;
        rsp.data_obj_type = PCI_SIG_DOE_DISCOVERY;
    } else {
        if (index < doe_cap->protocol_num) {
            prot = &doe_cap->protocols[index - 1];
            rsp.vendor_id = prot->vendor_id;
            rsp.data_obj_type = prot->data_obj_type;
        } else {
            rsp.vendor_id = 0xFFFF;
            rsp.data_obj_type = 0xFF;
        }
    }

    if (index + 1 == doe_cap->protocol_num) {
        rsp.next_index = 0;
    } else {
        rsp.next_index = index + 1;
    }

    pcie_doe_set_rsp(doe_cap, &rsp);

    return true;
}

static void pcie_doe_reset_mbox(DOECap *st)
{
    st->read_mbox_idx = 0;
    st->read_mbox_len = 0;
    st->write_mbox_len = 0;

    memset(st->read_mbox, 0, PCI_DOE_DW_SIZE_MAX * DWORD_BYTE);
    memset(st->write_mbox, 0, PCI_DOE_DW_SIZE_MAX * DWORD_BYTE);
}

void pcie_doe_init(PCIDevice *dev, DOECap *doe_cap, uint16_t offset,
                   DOEProtocol *protocols, bool intr, uint16_t vec)
{
    pcie_add_capability(dev, PCI_EXT_CAP_ID_DOE, 0x1, offset,
                        PCI_DOE_SIZEOF);

    doe_cap->pdev = dev;
    doe_cap->offset = offset;

    if (intr && (msi_present(dev) || msix_present(dev))) {
        doe_cap->cap.intr = intr;
        doe_cap->cap.vec = vec;
    }

    doe_cap->write_mbox = g_malloc0(PCI_DOE_DW_SIZE_MAX * DWORD_BYTE);
    doe_cap->read_mbox = g_malloc0(PCI_DOE_DW_SIZE_MAX * DWORD_BYTE);

    pcie_doe_reset_mbox(doe_cap);

    doe_cap->protocols = protocols;
    for (; protocols->vendor_id; protocols++) {
        doe_cap->protocol_num++;
    }
    assert(doe_cap->protocol_num < PCI_DOE_PROTOCOL_NUM_MAX);

    /* Increment to allow for the discovery protocol */
    doe_cap->protocol_num++;
}

void pcie_doe_fini(DOECap *doe_cap)
{
    g_free(doe_cap->read_mbox);
    g_free(doe_cap->write_mbox);
    g_free(doe_cap);
}

uint32_t pcie_doe_build_protocol(DOEProtocol *p)
{
    return DATA_OBJ_BUILD_HEADER1(p->vendor_id, p->data_obj_type);
}

void *pcie_doe_get_write_mbox_ptr(DOECap *doe_cap)
{
    return doe_cap->write_mbox;
}

/*
 * Copy the response to read mailbox buffer
 * This might be called in self-defined handle_request() if a DOE response is
 * required in the corresponding protocol
 */
void pcie_doe_set_rsp(DOECap *doe_cap, void *rsp)
{
    uint32_t len = pcie_doe_get_obj_len(rsp);

    memcpy(doe_cap->read_mbox + doe_cap->read_mbox_len, rsp, len * DWORD_BYTE);
    doe_cap->read_mbox_len += len;
}

uint32_t pcie_doe_get_obj_len(void *obj)
{
    uint32_t len;

    if (!obj) {
        return 0;
    }

    /* Only lower 18 bits are valid */
    len = DATA_OBJ_LEN_MASK(((DOEHeader *)obj)->length);

    /* PCIe r6.0 Table 6.29: a value of 00000h indicates 2^18 DW */
    return (len) ? len : PCI_DOE_DW_SIZE_MAX;
}

static void pcie_doe_irq_assert(DOECap *doe_cap)
{
    PCIDevice *dev = doe_cap->pdev;

    if (doe_cap->cap.intr && doe_cap->ctrl.intr) {
        if (doe_cap->status.intr) {
            return;
        }
        doe_cap->status.intr = 1;

        if (msix_enabled(dev)) {
            msix_notify(dev, doe_cap->cap.vec);
        } else if (msi_enabled(dev)) {
            msi_notify(dev, doe_cap->cap.vec);
        }
    }
}

static void pcie_doe_set_ready(DOECap *doe_cap, bool rdy)
{
    doe_cap->status.ready = rdy;

    if (rdy) {
        pcie_doe_irq_assert(doe_cap);
    }
}

static void pcie_doe_set_error(DOECap *doe_cap, bool err)
{
    doe_cap->status.error = err;

    if (err) {
        pcie_doe_irq_assert(doe_cap);
    }
}

/*
 * Check incoming request in write_mbox for protocol format
 */
static void pcie_doe_prepare_rsp(DOECap *doe_cap)
{
    bool success = false;
    int p;
    bool (*handle_request)(DOECap *) = NULL;

    if (doe_cap->status.error) {
        return;
    }

    if (doe_cap->write_mbox[0] ==
        DATA_OBJ_BUILD_HEADER1(PCI_VENDOR_ID_PCI_SIG, PCI_SIG_DOE_DISCOVERY)) {
        handle_request = pcie_doe_discovery;
    } else {
        for (p = 0; p < doe_cap->protocol_num - 1; p++) {
            if (doe_cap->write_mbox[0] ==
                pcie_doe_build_protocol(&doe_cap->protocols[p])) {
                handle_request = doe_cap->protocols[p].handle_request;
                break;
            }
        }
    }

    /*
     * PCIe r6 DOE 6.30.1:
     * If the number of DW transferred does not match the
     * indicated Length for a data object, then the
     * data object must be silently discarded.
     */
    if (handle_request && (doe_cap->write_mbox_len ==
        pcie_doe_get_obj_len(pcie_doe_get_write_mbox_ptr(doe_cap)))) {
        success = handle_request(doe_cap);
    }

    if (success) {
        pcie_doe_set_ready(doe_cap, 1);
    } else {
        pcie_doe_reset_mbox(doe_cap);
    }
}

/*
 * Read from DOE config space.
 * Return false if the address not within DOE_CAP range.
 */
bool pcie_doe_read_config(DOECap *doe_cap, uint32_t addr, int size,
                          uint32_t *buf)
{
    uint32_t shift;
    uint16_t doe_offset = doe_cap->offset;

    if (!range_covers_byte(doe_offset + PCI_EXP_DOE_CAP,
                           PCI_DOE_SIZEOF - 4, addr)) {
        return false;
    }

    addr -= doe_offset;
    *buf = 0;

    if (range_covers_byte(PCI_EXP_DOE_CAP, DWORD_BYTE, addr)) {
        *buf = FIELD_DP32(*buf, PCI_DOE_CAP_REG, INTR_SUPP,
                          doe_cap->cap.intr);
        *buf = FIELD_DP32(*buf, PCI_DOE_CAP_REG, DOE_INTR_MSG_NUM,
                          doe_cap->cap.vec);
    } else if (range_covers_byte(PCI_EXP_DOE_CTRL, DWORD_BYTE, addr)) {
        /* Must return ABORT=0 and GO=0 */
        *buf = FIELD_DP32(*buf, PCI_DOE_CAP_CONTROL, DOE_INTR_EN,
                          doe_cap->ctrl.intr);
    } else if (range_covers_byte(PCI_EXP_DOE_STATUS, DWORD_BYTE, addr)) {
        *buf = FIELD_DP32(*buf, PCI_DOE_CAP_STATUS, DOE_BUSY,
                          doe_cap->status.busy);
        *buf = FIELD_DP32(*buf, PCI_DOE_CAP_STATUS, DOE_INTR_STATUS,
                          doe_cap->status.intr);
        *buf = FIELD_DP32(*buf, PCI_DOE_CAP_STATUS, DOE_ERROR,
                          doe_cap->status.error);
        *buf = FIELD_DP32(*buf, PCI_DOE_CAP_STATUS, DATA_OBJ_RDY,
                          doe_cap->status.ready);
    /* Mailbox should be DW accessed */
    } else if (addr == PCI_EXP_DOE_RD_DATA_MBOX && size == DWORD_BYTE) {
        if (doe_cap->status.ready && !doe_cap->status.error) {
            *buf = doe_cap->read_mbox[doe_cap->read_mbox_idx];
        }
    }

    /* Process Alignment */
    shift = addr % DWORD_BYTE;
    *buf = extract32(*buf, shift * 8, size * 8);

    return true;
}

/*
 * Write to DOE config space.
 * Return if the address not within DOE_CAP range or receives an abort
 */
void pcie_doe_write_config(DOECap *doe_cap,
                           uint32_t addr, uint32_t val, int size)
{
    uint16_t doe_offset = doe_cap->offset;
    uint32_t shift;

    if (!range_covers_byte(doe_offset + PCI_EXP_DOE_CAP,
                           PCI_DOE_SIZEOF - 4, addr)) {
        return;
    }

    /* Process Alignment */
    shift = addr % DWORD_BYTE;
    addr -= (doe_offset + shift);
    val = deposit32(val, shift * 8, size * 8, val);

    switch (addr) {
    case PCI_EXP_DOE_CTRL:
        if (FIELD_EX32(val, PCI_DOE_CAP_CONTROL, DOE_ABORT)) {
            pcie_doe_set_ready(doe_cap, 0);
            pcie_doe_set_error(doe_cap, 0);
            pcie_doe_reset_mbox(doe_cap);
            return;
        }

        if (FIELD_EX32(val, PCI_DOE_CAP_CONTROL, DOE_GO)) {
            pcie_doe_prepare_rsp(doe_cap);
        }

        if (FIELD_EX32(val, PCI_DOE_CAP_CONTROL, DOE_INTR_EN)) {
            doe_cap->ctrl.intr = 1;
        /* Clear interrupt bit located within the first byte */
        } else if (shift == 0) {
            doe_cap->ctrl.intr = 0;
        }
        break;
    case PCI_EXP_DOE_STATUS:
        if (FIELD_EX32(val, PCI_DOE_CAP_STATUS, DOE_INTR_STATUS)) {
            doe_cap->status.intr = 0;
        }
        break;
    case PCI_EXP_DOE_RD_DATA_MBOX:
        /* Mailbox should be DW accessed */
        if (size != DWORD_BYTE) {
            return;
        }
        doe_cap->read_mbox_idx++;
        if (doe_cap->read_mbox_idx == doe_cap->read_mbox_len) {
            pcie_doe_reset_mbox(doe_cap);
            pcie_doe_set_ready(doe_cap, 0);
        } else if (doe_cap->read_mbox_idx > doe_cap->read_mbox_len) {
            /* Underflow */
            pcie_doe_set_error(doe_cap, 1);
        }
        break;
    case PCI_EXP_DOE_WR_DATA_MBOX:
        /* Mailbox should be DW accessed */
        if (size != DWORD_BYTE) {
            return;
        }
        doe_cap->write_mbox[doe_cap->write_mbox_len] = val;
        doe_cap->write_mbox_len++;
        break;
    case PCI_EXP_DOE_CAP:
        /* fallthrough */
    default:
        break;
    }
}