/* ahci.c
 *
 * Copyright (C) 2023 wolfSSL Inc.
 *
 * This file is part of wolfBoot.
 *
 * wolfBoot is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * wolfBoot is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
 *
 */

#include <stdint.h>

#include <x86/common.h>
#include <printf.h>
#include <pci.h>
#include <x86/ahci.h>
#include <x86/ata.h>
#include <string.h>

#define AHCI_ABAR_OFFSET     0x24
#ifdef TARGET_x86_fsp_qemu
#define SATA_BASE 0x02200000
#elif TARGET_kontron_vx3060_s2
#define SATA_BASE 0x02200000
#endif /* TARGET_qemu_fsp */

#define HBA_FIS_BASE (SATA_BASE + 0x100)
#define HBA_CLB_BASE (SATA_BASE + 0x1000)
#define HBA_TBL_BASE (SATA_BASE + 0x200000)

#define HBA_FIS_SIZE 0x100
#define HBA_CLB_SIZE 0x400
#define HBA_TBL_SIZE 0x800

#define HBA_FIS_PORT_SIZE 0x80

#define PCI_REG_PCS 0x92
#define PCI_REG_CLK 0x94
#define PCI_REG_PCS_PORT_ENABLE_MASK 0x3f
#define PCI_REG_PCS_OOB 1 << 15
#define PCI_REG_MAP 0x90
#define PCI_REG_MAP_AHCI_MODE (0x1 << 6)
#define PCI_REG_MAP_ALL_PORTS (0x1 << 5)

#ifdef DEBUG_AHCI
#define AHCI_DEBUG_PRINTF(...) wolfBoot_printf(__VA_ARGS__)
#else
#define AHCI_DEBUG_PRINTF(...) do {} while(0)
#endif /* DEBUG_AHCI */


static inline void ahci_set_bar(uint32_t bus, uint32_t dev,
                                uint32_t func, uint32_t addr)
{
    pci_config_write32(bus, dev, func, AHCI_ABAR_OFFSET, addr);
}

int init_sata_controller(uint32_t bus, uint32_t dev, uint32_t fun)
{
    uint16_t reg16;
    uint32_t reg;

    reg16 = pci_config_read16(bus, dev, fun, PCI_REG_PCS);
    /* enable all ports */
    reg16 |= 0x3f;
    reg16 |= PCI_REG_PCS_OOB;
    pci_config_write16(bus, dev, fun,
                       PCI_REG_PCS, reg16);

    reg = pci_config_read32(bus, dev, fun, PCI_REG_CLK);
    reg |= 0x193;
    pci_config_write32(bus, dev, fun, PCI_REG_CLK, reg);

    wolfBoot_printf("Device detected: %x\r\n",
                    reg16 & ~PCI_REG_PCS_PORT_ENABLE_MASK);

    return 0;
}

uint32_t ahci_enable(uint32_t bus, uint32_t dev, uint32_t fun)
{
    uint16_t reg16;
    uint32_t iobar;
    uint32_t reg;
    uint32_t bar;

    AHCI_DEBUG_PRINTF("ahci: enabling %x:%x.%x\r\n", bus, dev, fun);
    reg = pci_config_read32(bus, dev, fun, PCI_COMMAND_OFFSET);

    bar = pci_config_read32(bus, dev, fun, AHCI_ABAR_OFFSET);
    AHCI_DEBUG_PRINTF("PCI BAR: %08x\r\n", bar);
    iobar = pci_config_read32(bus, dev, fun, AHCI_AIDPBA_OFFSET);
    AHCI_DEBUG_PRINTF("PCI I/O space: %08x\r\n", iobar);

    reg |= PCI_COMMAND_BUS_MASTER;
    reg |= PCI_COMMAND_MEM_SPACE;
    pci_config_write32(bus, dev, fun, PCI_COMMAND_OFFSET, reg);

    reg = pci_config_read32(bus, dev, fun, PCI_INTR_OFFSET);
    AHCI_DEBUG_PRINTF("Interrupt pin for AHCI controller: %02x\r\n",
                    (reg >> 8) & 0xFF);
    pci_config_write32(bus, dev, fun, PCI_INTR_OFFSET,
                       (reg & 0xFFFFFF00 | 0x0a));
    AHCI_DEBUG_PRINTF("Setting interrupt line: 0x0A\r\n");

    return bar;
}

void ahci_dump_port(uint32_t base, int i)
{
    uint32_t cmd, ci, is, tfd, serr, ssst;

    cmd = mmio_read32(AHCI_PxCMD(base, i));
    ci = mmio_read32(AHCI_PxCI(base, i));
    is = mmio_read32(AHCI_PxIS(base, i));
    tfd = mmio_read32(AHCI_PxTFD(base, i));
    serr = mmio_read32(AHCI_PxSERR(base, i));
    ssst = mmio_read32(AHCI_PxSSTS(base, i));
    AHCI_DEBUG_PRINTF("%d: cmd:0x%x ci:0x%x is: 0x%x tfd: 0x%x serr: 0x%x ssst: 0x%x\r\n",
                    i, cmd, ci, is, tfd, serr, ssst);
}

void sata_enable(uint32_t base) {
    volatile uint32_t count;
    uint32_t cap, ports_impl;
    uint8_t sata_only;
    uint8_t cap_sud;
    uint32_t n_ports;
    uint32_t i, j;
    uint64_t data64;
    uint32_t data;
    uint32_t reg;

    mmio_or32(AHCI_HBA_GHC(base), HBA_GHC_AE);

    /* Wait until enabled. */
    while ((mmio_read32(AHCI_HBA_GHC(base)) & HBA_GHC_AE) == 0)
        ;

    AHCI_DEBUG_PRINTF("AHCI memory mapped at %08x\r\n", base);

    /* Resetting the controller */
    mmio_or32(AHCI_HBA_GHC(base), HBA_GHC_HR | HBA_GHC_IE);

    /* Wait until reset is complete */
    while ((mmio_read32(AHCI_HBA_GHC(base)) & HBA_GHC_HR) != 0)
        ;

    /* Wait until enabled. */
    if ((mmio_read32(AHCI_HBA_GHC(base)) & HBA_GHC_AE) == 0)
          mmio_or32(AHCI_HBA_GHC(base), HBA_GHC_AE);;

    AHCI_DEBUG_PRINTF("AHCI reset complete.\r\n");

    cap = mmio_read32(AHCI_HBA_CAP(base));
    n_ports = (cap & 0x1F) + 1;
    sata_only = (cap & AHCI_CAP_SAM);
    cap_sud = (cap & AHCI_CAP_SSS);

    if (!sata_only)
        mmio_or32(AHCI_HBA_GHC(base), HBA_GHC_AE);

    ports_impl = mmio_read32(AHCI_HBA_PI(base));

    /* Clear global HBA IS */
    reg = mmio_read32(AHCI_HBA_IS(base));
    mmio_write32(AHCI_HBA_IS(base), reg);
    AHCI_DEBUG_PRINTF("AHCI HBA: Cleared IS\r\n");

    AHCI_DEBUG_PRINTF("AHCI: %d ports\r\n", n_ports);
    for (i = 0; i < AHCI_MAX_PORTS; i++) {
        if ((ports_impl & (1 << i)) != 0) {
            uint32_t reg;
            uint32_t ssts = mmio_read32(AHCI_PxSSTS(base, i));
            uint8_t ipm = (ssts >> 8) & 0xFF;
            uint8_t det = ssts & 0x0F;
            volatile struct hba_cmd_header *hdr;

            data = mmio_read32(AHCI_PxCMD(base, i));
            /* Detect POD */
            if ((data & AHCI_PORT_CMD_CPD) != 0) {
                AHCI_DEBUG_PRINTF("AHCI port %d: POD\r\n", i);
                mmio_or32(AHCI_PxCMD(base, i), AHCI_PORT_CMD_POD);
            }

            /* Detect pre-spinning */
            if (cap_sud != 0) {
                AHCI_DEBUG_PRINTF("AHCI port %d: Spinning\r\n", i);
                mmio_or32(AHCI_PxCMD(base, i), AHCI_PORT_CMD_SUD);
            }

            /* Disable aggressive powersaving */
            mmio_or32(AHCI_PxSCTL(base, i), (0x03 << 8));

            /* Disable interrupt reporting to SW */
            //mmio_write32(AHCI_PxIE(base, i), 0);

            count = 0;
            while (1) {
                ssts = mmio_read32(AHCI_PxSSTS(base, i));
                ipm = (ssts >> 8) & 0x0F;
                ssts &= AHCI_SSTS_DET_MASK;
                if (ssts == AHCI_PORT_SSTS_DET_PCE)
                    break;
                if (count++ > 5) {
                    AHCI_DEBUG_PRINTF("AHCI port %d: Timeout occurred.\r\n", i);
                    break;
                }
                delay(1000);
            };

            if (ssts == 0) {
                wolfBoot_printf("AHCI port %d: No disk detected\r\n", i);
            } else {
                wolfBoot_printf("AHCI port %d: Disk detected (det: %02x ipm: %02x)\r\n",
                                i, det, ipm);

                /* Clear port SERR */
                reg = mmio_read32(AHCI_PxSERR(base, i));
                mmio_write32(AHCI_PxSERR(base,i), reg);
                AHCI_DEBUG_PRINTF("AHCI port: Cleared SERR\r\n");

                /* Clear port IS */
                reg = mmio_read32(AHCI_PxIS(base, i));
                mmio_write32(AHCI_PxIS(base,i), reg);
                AHCI_DEBUG_PRINTF("AHCI port: Cleared IS\r\n");

                /* Send STOP command */
                reg = mmio_read32(AHCI_PxCMD(base, i));
                if ((reg & (AHCI_PORT_CMD_START | AHCI_PORT_CMD_CR)) != 0) {
                    if (reg & AHCI_PORT_CMD_START)
                        mmio_write32(AHCI_PxCMD(base, i),
                                (reg & (~AHCI_PORT_CMD_START)));
                }
                AHCI_DEBUG_PRINTF("AHCI port: Sending STOP ...\r\n");

                /* Wait for CR to be cleared */
                count = 0;
                do {
                    reg = mmio_read32(AHCI_PxCMD(base, i));
                    if (count++ > 5) {
                        AHCI_DEBUG_PRINTF("AHCI Error: Port did not clear CR!\r\n");
                        break;
                    }
                    delay(1000);
                } while ((reg & AHCI_PORT_CMD_CR) != 0);
                AHCI_DEBUG_PRINTF("AHCI port: Sent STOP.\r\n");

                AHCI_DEBUG_PRINTF("AHCI port: Disabling FIS ...\r\n");
                /* Disable FIS RX */
                reg = mmio_read32(AHCI_PxCMD(base, i));
                if (reg & (AHCI_PORT_CMD_CR | AHCI_PORT_CMD_START)) {
                    wolfBoot_printf("AHCI Error: Could not disable FIS while DMA is running\r");
                } else if ((reg & AHCI_PORT_CMD_FR) != 0) {
                    mmio_write32(AHCI_PxCMD(base, i),
                            reg & (~AHCI_PORT_CMD_FRE));
                }

                /* Wait for FR to be cleared */
                count = 0;
                do {
                    reg = mmio_read32(AHCI_PxCMD(base, i));
                    if (count++ > 5) {
                        wolfBoot_printf("AHCI Error: Port did not clear FR!\r\n");
                        break;
                    }
                    delay(1000);
                } while ((reg & AHCI_PORT_CMD_FR) != 0);
                AHCI_DEBUG_PRINTF("AHCI port: FIS disabled.\r\n");

                /* Initialize FIS and CLB address */
                mmio_write32(AHCI_PxCLB(base, i),
                        HBA_CLB_BASE + i * HBA_CLB_SIZE);
                mmio_write32(AHCI_PxCLBH(base, i), 0);

                mmio_write32(AHCI_PxFB(base, i),
                        HBA_FIS_BASE + i * HBA_FIS_SIZE);
                mmio_write32(AHCI_PxFBH(base, i), 0);

                memset((void*)(uintptr_t)(HBA_CLB_BASE + i * HBA_CLB_SIZE),
                       0, HBA_CLB_SIZE);
                memset((void*)(uintptr_t)(HBA_FIS_BASE + i * HBA_FIS_SIZE),
                       0, HBA_FIS_SIZE);

                /* Wait until CR is cleared */
                do {
                    reg = mmio_read32(AHCI_PxCMD(base, i));
                } while(reg & AHCI_PORT_CMD_CR);

                reg |= AHCI_PORT_CMD_FRE | AHCI_PORT_CMD_START;
                mmio_write32(AHCI_PxCMD(base, i), reg);

                AHCI_DEBUG_PRINTF("AHCI port %d command engine started\r\n", i);

                /* Put port into active state */
                reg = mmio_read32(AHCI_PxCMD(base, i));
                mmio_write32(AHCI_PxCMD(base, i), reg | AHCI_PORT_CMD_ICC_ACTIVE);

                /* Check device type by signature */
                reg = mmio_read32(AHCI_PxSIG(base, i));
                AHCI_DEBUG_PRINTF("SATA disk drive detected on AHCI. Sign: %x\r\n",
                            reg);
                if (reg == AHCI_PORT_SIG_SATA) {
                    int drv;
                    wolfBoot_printf("SATA disk drive detected on AHCI port %d\r\n",
                            i);
                    drv = ata_drive_new(base, i, HBA_CLB_BASE + i * HBA_CLB_SIZE,
                          HBA_TBL_BASE + i * HBA_TBL_SIZE, HBA_FIS_BASE + i * HBA_FIS_SIZE);
                    if (drv < 0) {
                        wolfBoot_printf("Failed to associate ATA drive to disk\r\n");
                    } else {
                        char buf[512] ="";
                        int r;
                        AHCI_DEBUG_PRINTF("ATA%d associated to AHCI port %d\r\n",
                                drv, i);
                        r = ata_identify_device(drv);
                        AHCI_DEBUG_PRINTF("ATA identify: returned %d\r\n", r);
                    }
                } else {
                    AHCI_DEBUG_PRINTF("AHCI port %d: device with signature %08x is not supported\r\n",
                            i, reg);
                }
            }
        }
    }
}

void sata_disable(uint32_t base)
{
    uint32_t i, reg;
    volatile uint32_t count;

    for (i = 0; i < AHCI_MAX_PORTS; i++) {
        /* Clear port SERR */
        reg = mmio_read32(AHCI_PxSERR(base, i));
        mmio_write32(AHCI_PxSERR(base,i), reg);

        /* Clear port IS */
        reg = mmio_read32(AHCI_PxIS(base, i));
        mmio_write32(AHCI_PxIS(base,i), reg);

        /* Send STOP command */
        reg = mmio_read32(AHCI_PxCMD(base, i));
        if ((reg & (AHCI_PORT_CMD_START | AHCI_PORT_CMD_CR)) != 0) {
            if (reg & AHCI_PORT_CMD_START)
                mmio_write32(AHCI_PxCMD(base, i),
                        (reg & (~AHCI_PORT_CMD_START)));
        }

        /* Wait for CR to be cleared */
        count = 0;
        do {
            reg = mmio_read32(AHCI_PxCMD(base, i));
            if (count++ > 5) {
                break;
            }
            delay(1000);
        } while ((reg & AHCI_PORT_CMD_CR) != 0);

        /* Disable FIS RX */
        reg = mmio_read32(AHCI_PxCMD(base, i));
        if (reg & (AHCI_PORT_CMD_CR | AHCI_PORT_CMD_START)) {
            wolfBoot_printf("AHCI Error: Could not disable FIS while DMA is running\r\n");
        } else if ((reg & AHCI_PORT_CMD_FR) != 0) {
            mmio_write32(AHCI_PxCMD(base, i),
                    reg & (~AHCI_PORT_CMD_FRE));
        }

        /* Wait for FR to be cleared */
        count = 0;
        do {
            reg = mmio_read32(AHCI_PxCMD(base, i));
            if (count++ > 5) {
                wolfBoot_printf("AHCI Error: Port did not clear FR!\r\n"); 
                break;
            }
            delay(1000);
        } while ((reg & AHCI_PORT_CMD_FR) != 0);
        reg = mmio_read32(AHCI_PxCMD(base, i));
        mmio_write32(AHCI_PxCMD(base, i),
                reg & (~AHCI_PORT_CMD_ICC_ACTIVE));

    }
    /* reg = mmio_read32(AHCI_HBA_GHC(base)); */
    /* mmio_write32(AHCI_HBA_GHC(base), reg & (~HBA_GHC_AE)); */
    /* mmio_or32(AHCI_HBA_GHC(base), HBA_GHC_HR | HBA_GHC_IE); */
    /* memset((void *)SATA_BASE, 0, 0x1000000); */
}
