/* _NVRM_COPYRIGHT_BEGIN_
 *
 * Copyright 2014-2015 by NVIDIA Corporation.  All rights reserved.  All
 * information contained herein is proprietary and confidential to NVIDIA
 * Corporation.  Any use, reproduction, or disclosure without the written
 * permission of NVIDIA Corporation is prohibited.
 *
 * _NVRM_COPYRIGHT_END_
 */

#include <linux/module.h>
#include <linux/interrupt.h>
#include "conftest.h"

#include "nvlink_pci.h"
#include "nvlink_proto.h"
#include "nvlink_common.h"
#include "nvlink_errors.h"

#if defined(NVCPU_PPC64LE) && defined(NV_LINUX_OF_H_PRESENT) && \
  defined(NV_OF_PARSE_PHANDLE_PRESENT)
#include <linux/of.h>

#define IBMNPU_DEV_NAME              "ibmnpu"

#define NV_PCI_DEVICE_ID_IBMNPU      0x04EA

#define IBMNPU_MAX_BARS              2

#ifndef IRQ_RETVAL
typedef void irqreturn_t;
#define IRQ_RETVAL(a)
#endif

#if !defined(IRQF_SHARED)
#define IRQF_SHARED SA_SHIRQ
#endif

static int           ibmnpu_probe    (struct pci_dev *, const struct pci_device_id *);
static void          ibmnpu_remove   (struct pci_dev *);

static struct pci_device_id ibmnpu_pci_table[] = {
    {
        .vendor      = PCI_VENDOR_ID_IBM,
        .device      = NV_PCI_DEVICE_ID_IBMNPU,
        .subvendor   = PCI_ANY_ID,
        .subdevice   = PCI_ANY_ID,
        .class       = (PCI_CLASS_BRIDGE_OTHER << 8),
        .class_mask  = ~1
    },
    { }
};

static struct pci_driver ibmnpu_pci_driver = {
    .name     = IBMNPU_DEV_NAME,
    .id_table = ibmnpu_pci_table,
    .probe    = ibmnpu_probe,
    .remove   = ibmnpu_remove,
};

static void
ibmnpu_iounmap(
    nvlink_dev_linux_state_t *nvls
)
{
    unsigned int bar_id;

    if (nvls == NULL)
    {
        WARN_ON(nvls == NULL);
        return;
    }

    for (bar_id = 0; bar_id < IBMNPU_MAX_BARS; bar_id++)
    {
        if (nvls->pci_info.bars[bar_id].pBar != NULL)
        {
            pci_iounmap(nvls->dev, nvls->pci_info.bars[bar_id].pBar);
        }
    }
}

static irqreturn_t
ibmnpu_isr(
    int   irq,
    void *arg
#if !defined(NV_IRQ_HANDLER_T_PRESENT) || (NV_IRQ_HANDLER_T_ARGUMENT_COUNT == 3)
    ,struct pt_regs *regs
#endif
)
{
    NvBool rm_handled = NV_FALSE;

    // TODO: Route the interrupt to NPU Core.

    return IRQ_RETVAL(rm_handled);
}

static int
ibmnpu_probe
(
    struct pci_dev *dev,
    const struct pci_device_id *id_table
)
{
    nvlink_dev_linux_state_t *nvls;
    unsigned int i, j;
    int rc;
    struct pci_dev *gpu = NULL;
    struct device_node *pci_dev_node;

    nvl_print(NVL_DBG_SETUP, "IBMNPU: Probing Emulated device %04x:%02x:%02x.%x, "
              "Vendor Id = 0x%x, Device Id = 0x%x, Class = 0x%x \n",
              NV_PCI_DOMAIN_NUMBER(dev), NV_PCI_BUS_NUMBER(dev),
              NV_PCI_SLOT_NUMBER(dev), PCI_FUNC(dev->devfn),
              dev->vendor, dev->device, dev->class);

    //
    // Get the associated real PCI device (GPU) connected to NPU.
    //
    pci_dev_node = of_parse_phandle(dev->dev.of_node, "ibm,gpu", 0);
    if (!pci_dev_node)
    {
        nvl_print(NVL_DBG_SETUP, "IBMNPU: No GPU is associated to this brick.\n");
    }
    else
    {
        gpu = PCI_DN(pci_dev_node)->pcidev;

        nvl_print(NVL_DBG_SETUP, "IBMNPU: Associated GPU %04x:%02x:%02x.%x, "
                  "Vendor Id = 0x%x, Device Id = 0x%x, Class = 0x%x \n",
                  NV_PCI_DOMAIN_NUMBER(gpu), NV_PCI_BUS_NUMBER(gpu),
                  NV_PCI_SLOT_NUMBER(gpu), PCI_FUNC(gpu->devfn),
                  gpu->vendor, gpu->device, gpu->class);
    }

    if (pci_enable_device(dev) != 0)
    {
        nvl_print(NVL_DBG_ERRORS,
            "IBMNPU: pci_enable_device failed, aborting\n");
        goto failed;
    }

    if (dev->irq == 0)
    {
        nvl_print(NVL_DBG_ERRORS, "IBMNPU: Can't find an IRQ!\n");
    }

    rc = pci_request_regions(dev, IBMNPU_DEV_NAME);
    if (rc != 0)
    {
        nvl_print(NVL_DBG_ERRORS,
            "IBMNPU: pci_request_regions failed with error %x, aborting\n",
            rc);
        goto failed;
    }

    nvls = (nvlink_dev_linux_state_t *) nvl_malloc(sizeof(nvlink_dev_linux_state_t));
    if (nvls == NULL)
    {
        nvl_print(NVL_DBG_ERRORS,
            "IBMNPU: failed to allocate %d bytes for nvlink_dev_linux_state_t\n",
            sizeof(nvlink_dev_linux_state_t));
        goto err_not_supported;
    }

    nvl_memset(nvls, 0, sizeof(nvlink_dev_linux_state_t));

    pci_set_drvdata(dev, (void *)nvls);

    nvls->dev                  = dev;
    nvls->pci_info.pciDeviceId = dev->device;
    nvls->pci_info.domain      = NV_PCI_DOMAIN_NUMBER(dev);
    nvls->pci_info.bus         = NV_PCI_BUS_NUMBER(dev);
    nvls->pci_info.device      = NV_PCI_SLOT_NUMBER(dev);
    nvls->pci_info.function    = PCI_FUNC(dev->devfn);

    for (i = 0, j = 0; i < NVRM_PCICFG_NUM_BARS && j < IBMNPU_MAX_BARS; i++)
    {
        if ((NV_PCI_RESOURCE_VALID(dev, i)) &&
            (NV_PCI_RESOURCE_FLAGS(dev, i) & PCI_BASE_ADDRESS_SPACE)
                == PCI_BASE_ADDRESS_SPACE_MEMORY)
        {
            NvU32 bar = 0;
            nvls->pci_info.bars[j].offset = NVRM_PCICFG_BAR_OFFSET(i);
            pci_read_config_dword(dev, nvls->pci_info.bars[j].offset, &bar);
            nvls->pci_info.bars[j].busAddress = (bar & PCI_BASE_ADDRESS_MEM_MASK);
            if (NV_PCI_RESOURCE_FLAGS(dev, i) & PCI_BASE_ADDRESS_MEM_TYPE_64)
            {
                pci_read_config_dword(dev, nvls->pci_info.bars[j].offset + 4, &bar);
                nvls->pci_info.bars[j].busAddress |= (((NvU64)bar) << 32);
            }
            nvls->pci_info.bars[j].baseAddr = NV_PCI_RESOURCE_START(dev, i);
            nvls->pci_info.bars[j].barSize = NV_PCI_RESOURCE_SIZE(dev, i);
            nvl_print(NVL_DBG_INFO, "IBMNPU: Bar%d @ 0x%llx [size=%dK].\n",
                i, nvls->pci_info.bars[j].baseAddr, (nvls->pci_info.bars[j].barSize >> 10));

            // Map registers to kernel address space.
            nvls->pci_info.bars[j].pBar = pci_iomap(dev, i, 0);
            if (nvls->pci_info.bars[j].pBar == NULL)
            {
                nvl_print(NVL_DBG_ERRORS,
                          "IBMNPU: Unable to map BAR%d registers\n", i);
                goto err_iounmap;
            }
            j++;
        }
    }


    pci_set_master(dev);

    return 0;

err_iounmap:
    ibmnpu_iounmap(nvls);

err_not_supported:
    pci_release_regions(dev);
    nv_pci_disable_device(dev);
    pci_set_drvdata(dev, NULL);
    if (nvls != NULL)
    {
        nvl_free(nvls);
    }
failed:
    return -1;
}

void ibmnpu_remove(struct pci_dev *dev)
{
    nvlink_dev_linux_state_t *nvls = NULL;

    nvl_print(NVL_DBG_SETUP, "IBMNPU: removing device %04x:%02x:%02x.%x\n",
              NV_PCI_DOMAIN_NUMBER(dev), NV_PCI_BUS_NUMBER(dev),
              NV_PCI_SLOT_NUMBER(dev), PCI_FUNC(dev->devfn));

    nvls = pci_get_drvdata(dev);
    if (!nvls || (nvls->dev != dev))
    {
        WARN_ON(!nvls || (nvls->dev != dev));
        goto done;
    }

    // Unmap registers from kernel address space
    ibmnpu_iounmap(nvls);

done:
    pci_release_regions(dev);
    nv_pci_disable_device(dev);
    pci_set_drvdata(dev, NULL);

    if (nvls != NULL)
    {
        nvl_free(nvls);
    }
    return;
}


int ibmnpu_init(void)
{
    int rc = 0;

    rc = pci_register_driver(&ibmnpu_pci_driver);
    if (rc < 0)
    {
        nvl_print(NVL_DBG_ERRORS, "IBMNPU: pci_register_driver failed with error %x\n", rc);
        goto out;
    }

out:
    return rc;
}

void ibmnpu_exit(void)
{
    pci_unregister_driver(&ibmnpu_pci_driver);
}


NvlStatus ibmnpu_lib_initialize_device_pci_bus(nvlink_pci_info *info)
{
    return 0;
}

NvlStatus ibmnpu_lib_initialize_device_bar_info(nvlink_pci_info *info)
{
    return 0;
}

NvlStatus ibmnpu_lib_shutdown_device_bar_info(nvlink_pci_info *info)
{
    return 0;
}

#else

int ibmnpu_init(void)
{
    return 0;
}

void ibmnpu_exit(void)
{
    return;
}

NvlStatus ibmnpu_lib_initialize_device_pci_bus(nvlink_pci_info *info)
{
    return 0;
}

NvlStatus ibmnpu_lib_initialize_device_bar_info(nvlink_pci_info *info)
{
    return 0;
}

NvlStatus ibmnpu_lib_shutdown_device_bar_info(nvlink_pci_info *info)
{
    return 0;
}

#endif
