/* _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 "nv-misc.h"
#include "os-interface.h"
#include "nvlink-linux.h"
#include "nv-instance.h"

static int           ebridge_probe    (struct pci_dev *, const struct pci_device_id *);
static void          ebridge_remove   (struct pci_dev *);
#if !defined(NV_IRQ_HANDLER_T_PRESENT) || (NV_IRQ_HANDLER_T_ARGUMENT_COUNT == 3)
static irqreturn_t   ebridge_isr    (int, void *, struct pt_regs *);
#else
static irqreturn_t   ebridge_isr    (int, void *);
#endif

static struct pci_device_id ebridge_pci_table[] = {
    {
        .vendor      = PCI_VENDOR_ID_NVIDIA,
        .device      = NV_PCI_DEVICE_ID_EBRIDGE_1,
        .subvendor   = PCI_ANY_ID,
        .subdevice   = PCI_ANY_ID,
        .class       = (PCI_CLASS_BRIDGE_OTHER << 8),
        .class_mask  = ~0
    },
    {
        .vendor      = PCI_VENDOR_ID_NVIDIA,
        .device      = NV_PCI_DEVICE_ID_EBRIDGE_2,
        .subvendor   = PCI_ANY_ID,
        .subdevice   = PCI_ANY_ID,
        .class       = (PCI_CLASS_BRIDGE_OTHER << 8),
        .class_mask  = ~0
    },
    { }
};

static struct pci_driver ebridge_pci_driver = {
    .name     = NVLINK_DEV_NAME,
    .id_table = ebridge_pci_table,
    .probe    = ebridge_probe,
    .remove   = ebridge_remove,
};

static int
ebridge_probe
(
    struct pci_dev *dev,
    const struct pci_device_id *id_table
)
{
    ebridge_linux_state_t *nvls;
    unsigned int i, j;
    int rc;

    nv_printf(NV_DBG_SETUP, "EBRIDGE: probing 0x%x 0x%x, class 0x%x\n",
        dev->vendor, dev->device, dev->class);

    if (pci_enable_device(dev) != 0)
    {
        nv_printf(NV_DBG_ERRORS,
            "EBRIDGE: pci_enable_device failed, aborting\n");
        goto failed;
    }

    if (dev->irq == 0)
    {
        nv_printf(NV_DBG_ERRORS, "EBRIDGE: Can't find an IRQ!\n");
        goto failed;
    }

    if (!request_mem_region(NV_PCI_RESOURCE_START(dev, EBRIDGE_BAR_INDEX_REGS1),
                            NV_PCI_RESOURCE_SIZE(dev, EBRIDGE_BAR_INDEX_REGS1),
                            NVLINK_DEV_NAME))
    {
        nv_printf(NV_DBG_ERRORS,
            "EBRIDGE: request_mem_region failed for %dM @ 0x%llx.\n",
            (NV_PCI_RESOURCE_SIZE(dev, EBRIDGE_BAR_INDEX_REGS1) >> 20),
            (NvU64)NV_PCI_RESOURCE_START(dev, EBRIDGE_BAR_INDEX_REGS1));
        goto failed;
    }

    NV_KMALLOC(nvls, sizeof(ebridge_linux_state_t));
    if (nvls == NULL)
    {
        nv_printf(NV_DBG_ERRORS, "EBRIDGE: failed to allocate memory\n");
        goto err_not_supported;
    }

    os_mem_set(nvls, 0, sizeof(ebridge_linux_state_t));

    pci_set_drvdata(dev, (void *)nvls);

    nvls->dev               = dev;
    nvls->pci_info.vendor_id = dev->vendor;
    nvls->pci_info.device_id = dev->device;
    nvls->pci_info.domain    = NV_PCI_DOMAIN_NUMBER(dev);
    nvls->pci_info.bus       = NV_PCI_BUS_NUMBER(dev);
    nvls->pci_info.slot      = NV_PCI_SLOT_NUMBER(dev);

    for (i = 0, j = 0; i < NVRM_PCICFG_NUM_BARS && j < EBRIDGE_NUM_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->bars[j].offset = NVRM_PCICFG_BAR_OFFSET(i);
            pci_read_config_dword(dev, nvls->bars[j].offset, &bar);
            nvls->bars[j].bus_address = (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->bars[j].offset + 4, &bar);
                nvls->bars[j].bus_address |= (((NvU64)bar) << 32);
            }
            nvls->bars[j].cpu_address = NV_PCI_RESOURCE_START(dev, i);
            nvls->bars[j].strapped_size = NV_PCI_RESOURCE_SIZE(dev, i);
            nvls->bars[j].size = nvls->bars[j].strapped_size;
            nv_printf(NV_DBG_INFO, "EBRIDGE: Bar%d @ 0x%llx [size=%dK].\n",
                j, nvls->bars[j].cpu_address, (nvls->bars[j].size >> 10));
            j++;
                
        }
    }

    nvls->interrupt_line = dev->irq;

    pci_set_master(dev);

    rc = request_irq(nvls->interrupt_line, ebridge_isr,
                     IRQF_SHARED, NVLINK_DEV_NAME, (void *)nvls);
    if (rc != 0)
    {
        if ((nvls->interrupt_line != 0) && (rc == -EBUSY))
        {
            nv_printf(NV_DBG_ERRORS,
                "EBRIDGE: Tried to get IRQ %d, but another driver\n",
                (unsigned int) nvls->interrupt_line);
            nv_printf(NV_DBG_ERRORS, "EBRIDGE: has it and is not sharing it.\n");
        }
        nv_printf(NV_DBG_ERRORS, "EBRIDGE: request_irq() failed (%d)\n", rc);
        goto err_not_supported;
    }

    //TODO: Map registers to kernel address space.

    return 0;

err_not_supported:
    //TODO: Unmap registers from kernel address space.
    release_mem_region(NV_PCI_RESOURCE_START(dev, EBRIDGE_BAR_INDEX_REGS1),
                       NV_PCI_RESOURCE_SIZE(dev, EBRIDGE_BAR_INDEX_REGS1));
    NV_PCI_DISABLE_DEVICE(dev);
    pci_set_drvdata(dev, NULL);
    if (nvls != NULL)
    {
        NV_KFREE(nvls, sizeof(ebridge_linux_state_t));
    }
failed:
    return -1;
}

void ebridge_remove(struct pci_dev *dev)
{
    ebridge_linux_state_t *nvls = NULL;

    nv_printf(NV_DBG_SETUP, "EBRIDGE: 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))
    {
        goto done;
    }

    //TODO: Unmap registers from kernel address space

    free_irq(nvls->interrupt_line, nvls);

done:
    release_mem_region(NV_PCI_RESOURCE_START(dev, EBRIDGE_BAR_INDEX_REGS1),
                       NV_PCI_RESOURCE_SIZE(dev, EBRIDGE_BAR_INDEX_REGS1));
    NV_PCI_DISABLE_DEVICE(dev);
    pci_set_drvdata(dev, NULL);

    if (nvls != NULL)
    {
        NV_KFREE(nvls, sizeof(ebridge_linux_state_t));
    }
    return;
}

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

    //TODO: Route the interrupt to RM.

    return IRQ_RETVAL(rm_handled);
}

int ebridge_init(void)
{
    int rc;

    rc = nv_pci_register_driver(&ebridge_pci_driver);
    if (rc < 0)
    {
        nv_printf(NV_DBG_INFO, "EBRIDGE: No device found!\n");
        goto out;
    }

out:
    return rc;
}

void ebridge_exit(void)
{
    pci_unregister_driver(&ebridge_pci_driver);
}
