/*
 * ASPEED iBT Device
 *
 * Copyright (c) 2016-2021 Cédric Le Goater, IBM Corporation.
 *
 * This code is licensed under the GPL version 2 or later.  See
 * the COPYING file in the top-level directory.
 */

#include "qemu/osdep.h"
#include "hw/sysbus.h"
#include "sysemu/qtest.h"
#include "sysemu/sysemu.h"
#include "qemu/log.h"
#include "qapi/error.h"
#include "qemu/error-report.h"
#include "hw/irq.h"
#include "hw/qdev-properties.h"
#include "hw/qdev-properties-system.h"
#include "migration/vmstate.h"
#include "hw/misc/aspeed_ibt.h"
#include "trace.h"

#define BT_IO_REGION_SIZE 0x1C

#define TO_REG(o) (o >> 2)

#define BT_CR0                0x0   /* iBT config */
#define   BT_CR0_IO_BASE         16
#define   BT_CR0_IRQ             12
#define   BT_CR0_EN_CLR_SLV_RDP  0x8
#define   BT_CR0_EN_CLR_SLV_WRP  0x4
#define   BT_CR0_ENABLE_IBT      0x1
#define BT_CR1                0x4  /* interrupt enable */
#define   BT_CR1_IRQ_H2B         0x01
#define   BT_CR1_IRQ_HBUSY       0x40
#define BT_CR2                0x8  /* interrupt status */
#define   BT_CR2_IRQ_H2B         0x01
#define   BT_CR2_IRQ_HBUSY       0x40
#define BT_CR3                0xc /* unused */
#define BT_CTRL                  0x10
#define   BT_CTRL_B_BUSY         0x80
#define   BT_CTRL_H_BUSY         0x40
#define   BT_CTRL_OEM0           0x20
#define   BT_CTRL_SMS_ATN        0x10
#define   BT_CTRL_B2H_ATN        0x08
#define   BT_CTRL_H2B_ATN        0x04
#define   BT_CTRL_CLR_RD_PTR     0x02
#define   BT_CTRL_CLR_WR_PTR     0x01
#define BT_BMC2HOST            0x14
#define BT_INTMASK             0x18
#define   BT_INTMASK_B2H_IRQEN   0x01
#define   BT_INTMASK_B2H_IRQ     0x02
#define   BT_INTMASK_BMC_HWRST   0x80

/*
 * VM IPMI defines
 */
#define VM_MSG_CHAR        0xA0 /* Marks end of message */
#define VM_CMD_CHAR        0xA1 /* Marks end of a command */
#define VM_ESCAPE_CHAR     0xAA /* Set bit 4 from the next byte to 0 */

#define VM_PROTOCOL_VERSION        1
#define VM_CMD_VERSION             0xff /* A version number byte follows */
#define VM_CMD_NOATTN              0x00
#define VM_CMD_ATTN                0x01
#define VM_CMD_ATTN_IRQ            0x02
#define VM_CMD_POWEROFF            0x03
#define VM_CMD_RESET               0x04
#define VM_CMD_ENABLE_IRQ          0x05 /* Enable/disable the messaging irq */
#define VM_CMD_DISABLE_IRQ         0x06
#define VM_CMD_SEND_NMI            0x07
#define VM_CMD_CAPABILITIES        0x08
#define   VM_CAPABILITIES_POWER    0x01
#define   VM_CAPABILITIES_RESET    0x02
#define   VM_CAPABILITIES_IRQ      0x04
#define   VM_CAPABILITIES_NMI      0x08
#define   VM_CAPABILITIES_ATTN     0x10
#define   VM_CAPABILITIES_GRACEFUL_SHUTDOWN 0x20
#define VM_CMD_GRACEFUL_SHUTDOWN   0x09

/*
 * These routines are inspired by the 'ipmi-bmc-extern' model and by
 * the lanserv simulator of OpenIPMI. See :
 *    https://github.com/cminyard/openipmi/blob/master/lanserv/serial_ipmi.c
 */
static unsigned char ipmb_checksum(const unsigned char *data, int size,
                                   unsigned char start)
{
        unsigned char csum = start;

        for (; size > 0; size--, data++) {
                csum += *data;
        }
        return csum;
}

static void vm_add_char(unsigned char ch, unsigned char *c, unsigned int *pos)
{
    switch (ch) {
    case VM_MSG_CHAR:
    case VM_CMD_CHAR:
    case VM_ESCAPE_CHAR:
        c[(*pos)++] = VM_ESCAPE_CHAR;
        c[(*pos)++] = ch | 0x10;
        break;

    default:
        c[(*pos)++] = ch;
    }
}

static void aspeed_ibt_dump_msg(const char *func, unsigned char *msg,
                                unsigned int len)
{
    if (trace_event_get_state_backends(TRACE_ASPEED_IBT_CHR_DUMP_MSG)) {
        int size = len * 3 + 1;
        g_autofree char *tmp = g_malloc(size);
        int i, n = 0;

        for (i = 0; i < len; i++) {
            n += snprintf(tmp + n, size - n, "%02x:", msg[i]);
        }
        tmp[size - 1] = 0;

        trace_aspeed_ibt_chr_dump_msg(func, tmp, len);
    }
}

static void aspeed_ibt_chr_write(AspeedIBTState *ibt, const uint8_t *buf,
                                 int len)
{
    int i;

    if (!qemu_chr_fe_get_driver(&ibt->chr)) {
        return;
    }

    aspeed_ibt_dump_msg(__func__, ibt->recv_msg, ibt->recv_msg_len);

    for (i = 0; i < len; i++) {
        qemu_chr_fe_write(&ibt->chr, &buf[i], 1);
    }
}

static void vm_send(AspeedIBTState *ibt)
{
    unsigned int i;
    unsigned int len = 0;
    g_autofree unsigned char *c = g_malloc((ibt->send_msg_len + 7) * 2);
    uint8_t netfn;

    /*
     * The VM IPMI message format does not follow the IPMI BT
     * interface format. The sequence and the netfn bytes need to be
     * swapped.
     */
    netfn = ibt->send_msg[1];
    ibt->send_msg[1] = ibt->send_msg[2];
    ibt->send_msg[2] = netfn;

    /* No length byte in the VM IPMI message format. trim it */
    for (i = 1; i < ibt->send_msg_len; i++) {
        vm_add_char(ibt->send_msg[i], c, &len);
    }

    vm_add_char(-ipmb_checksum(&ibt->send_msg[1], ibt->send_msg_len - 1, 0),
                c, &len);
    c[len++] = VM_MSG_CHAR;

    aspeed_ibt_chr_write(ibt, c, len);
}

static void aspeed_ibt_update_irq(AspeedIBTState *ibt)
{
    bool raise = false;

    /* H2B rising */
    if ((ibt->regs[TO_REG(BT_CTRL)] & BT_CTRL_H2B_ATN) &&
        ((ibt->regs[TO_REG(BT_CR1)] & BT_CR1_IRQ_H2B) == BT_CR1_IRQ_H2B)) {
        ibt->regs[TO_REG(BT_CR2)] |= BT_CR2_IRQ_H2B;

        /*
         * Also flag the fact that we are waiting for the guest/driver
         * to read a received message
         */
        ibt->recv_waiting = true;
        raise = true;
    }

    /* H_BUSY falling (not supported) */
    if ((ibt->regs[TO_REG(BT_CTRL)] & BT_CTRL_H_BUSY) &&
        ((ibt->regs[TO_REG(BT_CR1)] & BT_CR1_IRQ_HBUSY) == BT_CR1_IRQ_HBUSY)) {
        ibt->regs[TO_REG(BT_CR2)] |= BT_CR2_IRQ_HBUSY;

        raise = true;
    }

    if (raise) {
        qemu_irq_raise(ibt->irq);
    }
}

static void vm_handle_msg(AspeedIBTState *ibt, unsigned char *msg,
                          unsigned int len)
{
    uint8_t seq;

    aspeed_ibt_dump_msg(__func__, ibt->recv_msg, ibt->recv_msg_len);

    if (len < 4) {
        qemu_log_mask(LOG_GUEST_ERROR, " %s: Message too short\n", __func__);
        return;
    }

    if (ipmb_checksum(ibt->recv_msg, ibt->recv_msg_len, 0) != 0) {
        qemu_log_mask(LOG_GUEST_ERROR, " %s: Message checksum failure\n",
                      __func__);
        return;
    }

    /* Trim the checksum byte */
    ibt->recv_msg_len--;

    /*
     * The VM IPMI message format does not follow the IPMI BT
     * interface format. The sequence and the netfn bytes need to be
     * swapped.
     */
    seq = ibt->recv_msg[0];
    ibt->recv_msg[0] = ibt->recv_msg[1];
    ibt->recv_msg[1] = seq;

    aspeed_ibt_update_irq(ibt);
}

/* TODO: handle commands */
static void vm_handle_cmd(AspeedIBTState *ibt, unsigned char *msg,
                          unsigned int len)
{
    aspeed_ibt_dump_msg(__func__, ibt->recv_msg, ibt->recv_msg_len);

    if (len < 1) {
        qemu_log_mask(LOG_GUEST_ERROR, " %s: Command too short\n", __func__);
        return;
    }

    switch (msg[0]) {
    case VM_CMD_VERSION:
        break;

    case VM_CMD_CAPABILITIES:
        if (len < 2) {
            return;
        }
        break;

    case VM_CMD_RESET:
        break;
    }
}

static void vm_handle_char(AspeedIBTState *ibt, unsigned char ch)
{
    unsigned int len = ibt->recv_msg_len;

    switch (ch) {
    case VM_MSG_CHAR:
    case VM_CMD_CHAR:
        if (ibt->in_escape) {
            qemu_log_mask(LOG_GUEST_ERROR, " %s: Message ended in escape\n",
                          __func__);
        } else if (ibt->recv_msg_too_many) {
            qemu_log_mask(LOG_GUEST_ERROR, " %s: Message too long\n", __func__);
        } else if (ibt->recv_msg_len == 0) {
            /* Nothing to do */
        } else if (ch == VM_MSG_CHAR) {
            /* Last byte of message. Signal BMC as the host would do */
            ibt->regs[TO_REG(BT_CTRL)] |= BT_CTRL_H2B_ATN;

            vm_handle_msg(ibt, ibt->recv_msg, ibt->recv_msg_len);

            /* Message is only handled when read by BMC (!B_BUSY) */
        } else if (ch == VM_CMD_CHAR) {
            vm_handle_cmd(ibt, ibt->recv_msg, ibt->recv_msg_len);

            /* Command is now handled. reset receive state */
            ibt->in_escape = 0;
            ibt->recv_msg_len = 0;
            ibt->recv_msg_too_many = 0;
        }
        break;

    case VM_ESCAPE_CHAR:
        if (!ibt->recv_msg_too_many) {
            ibt->in_escape = 1;
        }
        break;

    default:
        if (ibt->in_escape) {
            ibt->in_escape = 0;
            ch &= ~0x10;
        }

        if (!ibt->recv_msg_too_many) {
            if (len >= sizeof(ibt->recv_msg)) {
                ibt->recv_msg_too_many = 1;
                break;
            }

            ibt->recv_msg[len] = ch;
            ibt->recv_msg_len++;
        }
        break;
    }
}

static void vm_connected(AspeedIBTState *ibt)
{
    unsigned int len = 0;
    unsigned char c[5];

    vm_add_char(VM_CMD_VERSION, c, &len);
    vm_add_char(VM_PROTOCOL_VERSION, c, &len);
    c[len++] = VM_CMD_CHAR;

    aspeed_ibt_chr_write(ibt, c, len);
}

static void aspeed_ibt_chr_event(void *opaque, QEMUChrEvent event)
{
    AspeedIBTState *ibt = ASPEED_IBT(opaque);

    switch (event) {
    case CHR_EVENT_OPENED:
        vm_connected(ibt);
        ibt->connected = true;
        break;

    case CHR_EVENT_CLOSED:
        if (!ibt->connected) {
            return;
        }
        ibt->connected = false;
        break;
    case CHR_EVENT_BREAK:
    case CHR_EVENT_MUX_IN:
    case CHR_EVENT_MUX_OUT:
        /* Ignore */
        break;
   }
    trace_aspeed_ibt_chr_event(ibt->connected);
}

static int aspeed_ibt_chr_can_receive(void *opaque)
{
    AspeedIBTState *ibt = ASPEED_IBT(opaque);

    return !ibt->recv_waiting && !(ibt->regs[TO_REG(BT_CTRL)] & BT_CTRL_B_BUSY);
}

static void aspeed_ibt_chr_receive(void *opaque, const uint8_t *buf,
                                   int size)
{
    AspeedIBTState *ibt = ASPEED_IBT(opaque);
    int i;

    if (!ibt->connected) {
        qemu_log_mask(LOG_GUEST_ERROR, " %s: not connected !?\n", __func__);
        return;
    }

    for (i = 0; i < size; i++) {
        vm_handle_char(ibt, buf[i]);
    }
}

static void aspeed_ibt_write(void *opaque, hwaddr offset, uint64_t data,
                             unsigned size)
{
    AspeedIBTState *ibt = ASPEED_IBT(opaque);

    trace_aspeed_ibt_write(offset, data);

    switch (offset) {
    case BT_CTRL:
        /* CLR_WR_PTR: cleared before a message is written */
        if (data & BT_CTRL_CLR_WR_PTR) {
            memset(ibt->send_msg, 0, sizeof(ibt->send_msg));
            ibt->send_msg_len = 0;
            trace_aspeed_ibt_event("CLR_WR_PTR");
        }

        /* CLR_RD_PTR: cleared before a message is read */
        else if (data & BT_CTRL_CLR_RD_PTR) {
            ibt->recv_msg_index = -1;
            trace_aspeed_ibt_event("CLR_RD_PTR");
        }

        /*
         * H2B_ATN: raised by host to end message, cleared by BMC
         * before reading message
         */
        else if (data & BT_CTRL_H2B_ATN) {
            ibt->regs[TO_REG(BT_CTRL)] &= ~BT_CTRL_H2B_ATN;
            trace_aspeed_ibt_event("H2B_ATN");
        }

        /* B_BUSY: raised and cleared by BMC when message is read */
        else if (data & BT_CTRL_B_BUSY) {
            ibt->regs[TO_REG(BT_CTRL)] ^= BT_CTRL_B_BUSY;
            trace_aspeed_ibt_event("B_BUSY");
        }

        /*
         * B2H_ATN: raised by BMC and cleared by host
         *
         * Also simulate the host busy bit which is set while the host
         * is reading the message from the BMC
         */
        else if (data & BT_CTRL_B2H_ATN) {
            trace_aspeed_ibt_event("B2H_ATN");
            ibt->regs[TO_REG(BT_CTRL)] |= (BT_CTRL_B2H_ATN | BT_CTRL_H_BUSY);

            vm_send(ibt);

            ibt->regs[TO_REG(BT_CTRL)] &= ~(BT_CTRL_B2H_ATN | BT_CTRL_H_BUSY);

            /* signal H_BUSY falling but that's a bit useless */
            aspeed_ibt_update_irq(ibt);
        }

        /* Anything else is unexpected */
        else {
            qemu_log_mask(LOG_GUEST_ERROR, "%s: unexpected CTRL setting\n",
                          __func__);
        }

        /* Message was read by BMC. we can reset the receive state */
        if (!(ibt->regs[TO_REG(BT_CTRL)] & BT_CTRL_B_BUSY)) {
            trace_aspeed_ibt_event("B_BUSY cleared");
            ibt->recv_waiting = false;
            ibt->in_escape = 0;
            ibt->recv_msg_len = 0;
            ibt->recv_msg_too_many = 0;
        }
        break;

    case BT_BMC2HOST:
        if (ibt->send_msg_len < sizeof(ibt->send_msg)) {
            trace_aspeed_ibt_event("BMC2HOST");
            ibt->send_msg[ibt->send_msg_len++] = data & 0xff;
        }
        break;

    case BT_CR0: /* TODO: iBT config */
    case BT_CR1: /* interrupt enable */
    case BT_CR3: /* unused */
    case BT_INTMASK:
        ibt->regs[TO_REG(offset)] = (uint32_t) data;
        break;
    case BT_CR2: /* interrupt status. writing 1 clears. */
        ibt->regs[TO_REG(offset)] ^= (uint32_t) data;
        qemu_irq_lower(ibt->irq);
        break;

    default:
        qemu_log_mask(LOG_UNIMP, "%s: not implemented 0x%" HWADDR_PRIx "\n",
                      __func__, offset);
        break;
    }
}

static uint64_t aspeed_ibt_read(void *opaque, hwaddr offset, unsigned size)
{
    AspeedIBTState *ibt = ASPEED_IBT(opaque);
    uint64_t val = 0;

    switch (offset) {
    case BT_BMC2HOST:
        trace_aspeed_ibt_event("BMC2HOST");
        /*
         * The IPMI BT interface requires the first byte to be the
         * length of the message
         */
        if (ibt->recv_msg_index == -1) {
            val = ibt->recv_msg_len;
            ibt->recv_msg_index++;
        } else if (ibt->recv_msg_index < ibt->recv_msg_len) {
            val = ibt->recv_msg[ibt->recv_msg_index++];
        }
        break;

    case BT_CR0:
    case BT_CR1:
    case BT_CR2:
    case BT_CR3:
    case BT_CTRL:
    case BT_INTMASK:
        return ibt->regs[TO_REG(offset)];
    default:
        qemu_log_mask(LOG_UNIMP, "%s: not implemented 0x%" HWADDR_PRIx "\n",
                      __func__, offset);
        return 0;
    }

    trace_aspeed_ibt_read(offset, val);
    return val;
}

static const MemoryRegionOps aspeed_ibt_ops = {
    .read = aspeed_ibt_read,
    .write = aspeed_ibt_write,
    .endianness = DEVICE_LITTLE_ENDIAN,
    .valid = {
        .min_access_size = 1,
        .max_access_size = 4,
    },
};

static void aspeed_ibt_reset(DeviceState *dev)
{
    AspeedIBTState *ibt = ASPEED_IBT(dev);

    memset(ibt->regs, 0, sizeof(ibt->regs));

    memset(ibt->recv_msg, 0, sizeof(ibt->recv_msg));
    ibt->recv_msg_len = 0;
    ibt->recv_msg_index = -1;
    ibt->recv_msg_too_many = 0;
    ibt->recv_waiting = false;
    ibt->in_escape = 0;

    memset(ibt->send_msg, 0, sizeof(ibt->send_msg));
    ibt->send_msg_len = 0;
}

static void aspeed_ibt_realize(DeviceState *dev, Error **errp)
{
    SysBusDevice *sbd = SYS_BUS_DEVICE(dev);
    AspeedIBTState *ibt = ASPEED_IBT(dev);

    if (!qemu_chr_fe_get_driver(&ibt->chr) && !qtest_enabled()) {
        warn_report("Aspeed iBT has no chardev backend");
    } else {
        qemu_chr_fe_set_handlers(&ibt->chr, aspeed_ibt_chr_can_receive,
                                 aspeed_ibt_chr_receive, aspeed_ibt_chr_event,
                                 NULL, ibt, NULL, true);
    }

    sysbus_init_irq(sbd, &ibt->irq);
    memory_region_init_io(&ibt->iomem, OBJECT(ibt), &aspeed_ibt_ops, ibt,
                          TYPE_ASPEED_IBT, BT_IO_REGION_SIZE);

    sysbus_init_mmio(sbd, &ibt->iomem);
}

static Property aspeed_ibt_props[] = {
    DEFINE_PROP_CHR("chardev", AspeedIBTState, chr),
    DEFINE_PROP_END_OF_LIST(),
};

static const VMStateDescription vmstate_aspeed_ibt = {
    .name = "aspeed.bt",
    .version_id = 1,
    .minimum_version_id = 1,
    .fields = (VMStateField[]) {
        VMSTATE_UINT32_ARRAY(regs, AspeedIBTState, ASPEED_IBT_NR_REGS),
        VMSTATE_END_OF_LIST()
    }
};

static void aspeed_ibt_class_init(ObjectClass *klass, void *data)
{
    DeviceClass *dc = DEVICE_CLASS(klass);
    dc->realize = aspeed_ibt_realize;
    device_class_set_legacy_reset(dc, aspeed_ibt_reset);
    dc->desc = "ASPEED iBT Device";
    dc->vmsd = &vmstate_aspeed_ibt;
    device_class_set_props(dc, aspeed_ibt_props);
}

static const TypeInfo aspeed_ibt_info = {
    .name = TYPE_ASPEED_IBT,
    .parent = TYPE_SYS_BUS_DEVICE,
    .instance_size = sizeof(AspeedIBTState),
    .class_init = aspeed_ibt_class_init,
};

static void aspeed_ibt_register_types(void)
{
    type_register_static(&aspeed_ibt_info);
}

type_init(aspeed_ibt_register_types);