// MegaRAID SAS boot support.
//
// Copyright (C) 2012 Hannes Reinecke, SUSE Linux Products GmbH
//
// Authors:
//  Hannes Reinecke <hare@suse.de>
//
// based on virtio-scsi.c which is written by:
//  Paolo Bonzini <pbonzini@redhat.com>
//
// This file may be distributed under the terms of the GNU LGPLv3 license.

#include "biosvar.h" // GET_GLOBALFLAT
#include "block.h" // struct drive_s
#include "blockcmd.h" // scsi_drive_setup
#include "config.h" // CONFIG_*
#include "malloc.h" // free
#include "output.h" // dprintf
#include "pci.h" // pci_config_readl
#include "pcidevice.h" // foreachpci
#include "pci_ids.h" // PCI_DEVICE_ID_XXX
#include "pci_regs.h" // PCI_VENDOR_ID
#include "stacks.h" // yield
#include "std/disk.h" // DISK_RET_SUCCESS
#include "string.h" // memset
#include "util.h" // timer_calc

#define MFI_DB 0x0 // Doorbell
#define MFI_OMSG0 0x18 // Outbound message 0
#define MFI_IDB 0x20 // Inbound doorbell
#define MFI_ODB 0x2c // Outbound doorbell
#define MFI_IQP 0x40 // Inbound queue port
#define MFI_OSP0 0xb0 // Outbound scratch pad0
#define MFI_IQPL 0xc0 // Inbound queue port (low bytes)
#define MFI_IQPH 0xc4 // Inbound queue port (high bytes)

#define MFI_STATE_MASK                0xf0000000
#define MFI_STATE_WAIT_HANDSHAKE      0x60000000
#define MFI_STATE_BOOT_MESSAGE_PENDING 0x90000000
#define MFI_STATE_READY               0xb0000000
#define MFI_STATE_OPERATIONAL         0xc0000000
#define MFI_STATE_FAULT               0xf0000000

/* MFI Commands */
typedef enum {
    MFI_CMD_INIT = 0x00,
    MFI_CMD_LD_READ,
    MFI_CMD_LD_WRITE,
    MFI_CMD_LD_SCSI_IO,
    MFI_CMD_PD_SCSI_IO,
    MFI_CMD_DCMD,
    MFI_CMD_ABORT,
    MFI_CMD_SMP,
    MFI_CMD_STP
} mfi_cmd_t;

struct megasas_cmd_frame {
    u8 cmd;             /*00h */
    u8 sense_len;       /*01h */
    u8 cmd_status;      /*02h */
    u8 scsi_status;     /*03h */

    u8 target_id;       /*04h */
    u8 lun;             /*05h */
    u8 cdb_len;         /*06h */
    u8 sge_count;       /*07h */

    u32 context;        /*08h */
    u32 context_64;     /*0Ch */

    u16 flags;          /*10h */
    u16 timeout;        /*12h */
    u32 data_xfer_len;   /*14h */

    union {
        struct {
            u32 opcode;       /*18h */
            u8 mbox[12];      /*1Ch */
            u32 sgl_addr;     /*28h */
            u32 sgl_len;      /*32h */
            u32 pad;          /*34h */
        } dcmd;
        struct {
            u32 sense_buf_lo; /*18h */
            u32 sense_buf_hi; /*1Ch */
            u8 cdb[16];       /*20h */
            u32 sgl_addr;     /*30h */
            u32 sgl_len;      /*34h */
        } pthru;
        struct {
            u8 pad[22];       /*18h */
        } gen;
    };
} __attribute__ ((packed));

struct mfi_ld_list_s {
    u32     count;
    u32     reserved_0;
    struct {
        u8          target;
        u8          lun;
        u16         seq;
        u8          state;
        u8          reserved_1[3];
        u64         size;
    } lds[64];
} __attribute__ ((packed));

#define MEGASAS_POLL_TIMEOUT 60000 // 60 seconds polling timeout

struct megasas_lun_s {
    struct drive_s drive;
    struct megasas_cmd_frame *frame;
    u32 iobase;
    u16 pci_id;
    u8 target;
    u8 lun;
};

static int megasas_fire_cmd(u16 pci_id, u32 ioaddr,
                            struct megasas_cmd_frame *frame)
{
    u32 frame_addr = (u32)frame;
    int frame_count = 1;
    u8 cmd_state;

    dprintf(2, "Frame 0x%x\n", frame_addr);
    if (pci_id == PCI_DEVICE_ID_LSI_SAS2004 ||
        pci_id == PCI_DEVICE_ID_LSI_SAS2008) {
        outl(0, ioaddr + MFI_IQPH);
        outl(frame_addr | frame_count << 1 | 1, ioaddr + MFI_IQPL);
    } else if (pci_id == PCI_DEVICE_ID_DELL_PERC5 ||
               pci_id == PCI_DEVICE_ID_LSI_SAS1064R ||
               pci_id == PCI_DEVICE_ID_LSI_VERDE_ZCR) {
        outl(frame_addr >> 3 | frame_count, ioaddr + MFI_IQP);
    } else {
        outl(frame_addr | frame_count << 1 | 1, ioaddr + MFI_IQP);
    }

    u32 end = timer_calc(MEGASAS_POLL_TIMEOUT);
    do {
        for (;;) {
            cmd_state = GET_LOWFLAT(frame->cmd_status);
            if (cmd_state != 0xff)
                break;
            if (timer_check(end)) {
                warn_timeout();
                return -1;
            }
            yield();
        }
    } while (cmd_state == 0xff);

    if (cmd_state == 0 || cmd_state == 0x2d)
        return 0;
    dprintf(1, "ERROR: Frame 0x%x, status 0x%x\n", frame_addr, cmd_state);
    return -1;
}

int
megasas_process_op(struct disk_op_s *op)
{
    if (!CONFIG_MEGASAS)
        return DISK_RET_EBADTRACK;
    u8 cdb[16];
    int blocksize = scsi_fill_cmd(op, cdb, sizeof(cdb));
    if (blocksize < 0)
        return default_process_op(op);
    struct megasas_lun_s *mlun_gf =
        container_of(op->drive_fl, struct megasas_lun_s, drive);
    struct megasas_cmd_frame *frame = GET_GLOBALFLAT(mlun_gf->frame);
    u16 pci_id = GET_GLOBALFLAT(mlun_gf->pci_id);
    int i;

    memset_fl(frame, 0, sizeof(*frame));
    SET_LOWFLAT(frame->cmd, MFI_CMD_LD_SCSI_IO);
    SET_LOWFLAT(frame->cmd_status, 0xFF);
    SET_LOWFLAT(frame->target_id, GET_GLOBALFLAT(mlun_gf->target));
    SET_LOWFLAT(frame->lun, GET_GLOBALFLAT(mlun_gf->lun));
    SET_LOWFLAT(frame->flags, 0x0001);
    SET_LOWFLAT(frame->data_xfer_len, op->count * blocksize);
    SET_LOWFLAT(frame->cdb_len, 16);

    for (i = 0; i < 16; i++) {
        SET_LOWFLAT(frame->pthru.cdb[i], cdb[i]);
    }
    dprintf(2, "pthru cmd 0x%x count %d bs %d\n",
            cdb[0], op->count, blocksize);

    if (op->count) {
        SET_LOWFLAT(frame->pthru.sgl_addr, (u32)op->buf_fl);
        SET_LOWFLAT(frame->pthru.sgl_len, op->count * blocksize);
        SET_LOWFLAT(frame->sge_count, 1);
    }
    SET_LOWFLAT(frame->context, (u32)frame);

    if (megasas_fire_cmd(pci_id, GET_GLOBALFLAT(mlun_gf->iobase), frame) == 0)
        return DISK_RET_SUCCESS;

    dprintf(2, "pthru cmd 0x%x failed\n", cdb[0]);
    return DISK_RET_EBADTRACK;
}

static int
megasas_add_lun(struct pci_device *pci, u32 iobase, u8 target, u8 lun)
{
    struct megasas_lun_s *mlun = malloc_fseg(sizeof(*mlun));
    char *name;
    int prio, ret = 0;

    if (!mlun) {
        warn_noalloc();
        return -1;
    }
    memset(mlun, 0, sizeof(*mlun));
    mlun->drive.type = DTYPE_MEGASAS;
    mlun->drive.cntl_id = pci->bdf;
    mlun->pci_id = pci->device;
    mlun->target = target;
    mlun->lun = lun;
    mlun->iobase = iobase;
    mlun->frame = memalign_low(256, sizeof(struct megasas_cmd_frame));
    if (!mlun->frame) {
        warn_noalloc();
        free(mlun);
        return -1;
    }
    name = znprintf(MAXDESCSIZE, "MegaRAID SAS (PCI %pP) LD %d:%d"
                    , pci, target, lun);
    prio = bootprio_find_scsi_device(pci, target, lun);
    ret = scsi_drive_setup(&mlun->drive, name, prio);
    free(name);
    if (ret) {
        free(mlun->frame);
        free(mlun);
        ret = -1;
    }

    return ret;
}

static void megasas_scan_target(struct pci_device *pci, u32 iobase)
{
    struct mfi_ld_list_s ld_list;
    struct megasas_cmd_frame *frame = memalign_tmp(256, sizeof(*frame));
    if (!frame) {
        warn_noalloc();
        return;
    }

    memset(&ld_list, 0, sizeof(ld_list));
    memset_fl(frame, 0, sizeof(*frame));

    frame->cmd = MFI_CMD_DCMD;
    frame->cmd_status = 0xFF;
    frame->sge_count = 1;
    frame->flags = 0x0011;
    frame->data_xfer_len = sizeof(ld_list);
    frame->dcmd.opcode = 0x03010000;
    frame->dcmd.sgl_addr = (u32)MAKE_FLATPTR(GET_SEG(SS), &ld_list);
    frame->dcmd.sgl_len = sizeof(ld_list);
    frame->context = (u32)frame;

    if (megasas_fire_cmd(pci->device, iobase, frame) == 0) {
        dprintf(2, "%d LD found\n", ld_list.count);
        int i;
        for (i = 0; i < ld_list.count; i++) {
            dprintf(2, "LD %d:%d state 0x%x\n",
                    ld_list.lds[i].target, ld_list.lds[i].lun,
                    ld_list.lds[i].state);
            if (ld_list.lds[i].state != 0) {
                megasas_add_lun(pci, iobase,
                                ld_list.lds[i].target, ld_list.lds[i].lun);
            }
        }
    }
}

static int megasas_transition_to_ready(struct pci_device *pci, u32 ioaddr)
{
    u32 fw_state = 0, new_state, mfi_flags = 0;

    if (pci->device == PCI_DEVICE_ID_LSI_SAS1064R ||
        pci->device == PCI_DEVICE_ID_DELL_PERC5)
        new_state = inl(ioaddr + MFI_OMSG0) & MFI_STATE_MASK;
    else
        new_state = inl(ioaddr + MFI_OSP0) & MFI_STATE_MASK;

    while (fw_state != new_state) {
        switch (new_state) {
        case MFI_STATE_FAULT:
            dprintf(1, "ERROR: fw in fault state\n");
            return -1;
            break;
        case MFI_STATE_WAIT_HANDSHAKE:
            mfi_flags = 0x08;
            /* fallthrough */
        case MFI_STATE_BOOT_MESSAGE_PENDING:
            mfi_flags |= 0x10;
            if (pci->device == PCI_DEVICE_ID_LSI_SAS2004 ||
                pci->device == PCI_DEVICE_ID_LSI_SAS2008 ||
                pci->device == PCI_DEVICE_ID_LSI_SAS2208 ||
                pci->device == PCI_DEVICE_ID_LSI_SAS3108) {
                outl(mfi_flags, ioaddr + MFI_DB);
            } else {
                outl(mfi_flags, ioaddr + MFI_IDB);
            }
            break;
        case MFI_STATE_OPERATIONAL:
            mfi_flags = 0x07;
            if (pci->device == PCI_DEVICE_ID_LSI_SAS2004 ||
                pci->device == PCI_DEVICE_ID_LSI_SAS2008 ||
                pci->device == PCI_DEVICE_ID_LSI_SAS2208 ||
                pci->device == PCI_DEVICE_ID_LSI_SAS3108) {
                outl(mfi_flags, ioaddr + MFI_DB);
                if (pci->device == PCI_DEVICE_ID_LSI_SAS2208 ||
                    pci->device == PCI_DEVICE_ID_LSI_SAS3108) {
                    int j = 0;
                    u32 doorbell;

                    while (j < MEGASAS_POLL_TIMEOUT) {
                        doorbell = inl(ioaddr + MFI_DB) & 1;
                        if (!doorbell)
                            break;
                        msleep(20);
                        j++;
                    }
                }
            } else {
                outl(mfi_flags, ioaddr + MFI_IDB);
            }
            break;
        case MFI_STATE_READY:
            dprintf(2, "MegaRAID SAS fw ready\n");
            return 0;
        }
        // The current state should not last longer than poll timeout
        u32 end = timer_calc(MEGASAS_POLL_TIMEOUT);
        for (;;) {
            if (timer_check(end)) {
                break;
            }
            yield();
            fw_state = new_state;
            if (pci->device == PCI_DEVICE_ID_LSI_SAS1064R ||
                pci->device == PCI_DEVICE_ID_DELL_PERC5)
                new_state = inl(ioaddr + MFI_OMSG0) & MFI_STATE_MASK;
            else
                new_state = inl(ioaddr + MFI_OSP0) & MFI_STATE_MASK;
            if (new_state != fw_state) {
                break;
            }
        }
    }
    dprintf(1, "ERROR: fw in state %x\n", new_state & MFI_STATE_MASK);
    return -1;
}

static void
init_megasas(void *data)
{
    struct pci_device *pci = data;
    u32 bar = PCI_BASE_ADDRESS_2;
    if (!(pci_config_readl(pci->bdf, bar) & PCI_BASE_ADDRESS_IO_MASK))
        bar = PCI_BASE_ADDRESS_0;
    u32 iobase = pci_enable_iobar(pci, bar);
    if (!iobase)
        return;
    pci_enable_busmaster(pci);

    dprintf(1, "found MegaRAID SAS at %pP, io @ %x\n", pci, iobase);

    // reset
    if (megasas_transition_to_ready(pci, iobase) == 0)
        megasas_scan_target(pci, iobase);
}

void
megasas_setup(void)
{
    ASSERT32FLAT();
    if (!CONFIG_MEGASAS)
        return;

    dprintf(3, "init megasas\n");

    struct pci_device *pci;
    foreachpci(pci) {
        if (pci->vendor != PCI_VENDOR_ID_LSI_LOGIC &&
            pci->vendor != PCI_VENDOR_ID_DELL)
            continue;
        if (pci->device == PCI_DEVICE_ID_LSI_SAS1064R ||
            pci->device == PCI_DEVICE_ID_LSI_SAS1078 ||
            pci->device == PCI_DEVICE_ID_LSI_SAS1078DE ||
            pci->device == PCI_DEVICE_ID_LSI_SAS2108 ||
            pci->device == PCI_DEVICE_ID_LSI_SAS2108E ||
            pci->device == PCI_DEVICE_ID_LSI_SAS2004 ||
            pci->device == PCI_DEVICE_ID_LSI_SAS2008 ||
            pci->device == PCI_DEVICE_ID_LSI_VERDE_ZCR ||
            pci->device == PCI_DEVICE_ID_DELL_PERC5 ||
            pci->device == PCI_DEVICE_ID_LSI_SAS2208 ||
            pci->device == PCI_DEVICE_ID_LSI_SAS3108)
            run_thread(init_megasas, pci);
    }
}
