/*****************************************************************************
 * drivers/virt/qemu_pci_test.c
 *
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.  The
 * ASF licenses this file to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance with the
 * License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
 * License for the specific language governing permissions and limitations
 * under the License.
 *
 *****************************************************************************/

/*****************************************************************************
 * Included Files
 *****************************************************************************/

#include <nuttx/config.h>
#include <nuttx/arch.h>

#include <debug.h>
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <math.h>
#include <unistd.h>
#include <errno.h>
#include <sched.h>

#include <nuttx/pci/pci.h>
#include <nuttx/virt/qemu_pci.h>

/*****************************************************************************
 * Pre-processor Definitions
 *****************************************************************************/

/*****************************************************************************
 * Private Functions Definitions
 *****************************************************************************/

static uint32_t mem_read(FAR const volatile void *addr, int width);

static void mem_write(FAR const volatile void *addr, uint32_t val, int width);

static int qemu_pci_test_probe(FAR struct pci_bus_s *bus,
                               FAR const struct pci_dev_type_s *type,
                               uint16_t bdf);

/*****************************************************************************
 * Public Data
 *****************************************************************************/

const struct pci_dev_type_s g_pci_type_qemu_pci_test =
{
  .vendor    = 0x1b36,
  .device    = 0x0005,
  .class_rev = PCI_ID_ANY,
  .name      = "Qemu PCI test device",
  .probe     = qemu_pci_test_probe
};

/*****************************************************************************
 * Private Types
 *****************************************************************************/

struct pci_test_dev_hdr_s
{
    uint8_t  test;      /* write-only, starts a given test number */
    uint8_t  width;     /* read-only, type and width of access for a test */
    uint8_t  pad0[2];
    uint32_t offset;    /* read-only, offset in this BAR for a given test */
    uint32_t data;      /* read-only, data to use for a given test */
    uint32_t count;     /* for debugging. number of writes detected. */
    uint8_t  name[];    /* for debugging. 0-terminated ASCII string. */
};

/* Structure the read and write helpers */

struct pci_test_dev_ops_s
{
    uint32_t (*read)(FAR const volatile void *addr, int width);
    void (*write)(FAR const volatile void *addr, uint32_t val, int width);
};

/*****************************************************************************
 * Private Data
 *****************************************************************************/

static struct pci_test_dev_ops_s g_mem_ops =
{
    .read = mem_read,
    .write = mem_write
};

/*****************************************************************************
 * Private Functions
 *****************************************************************************/

static uint32_t mem_read(FAR const volatile void *addr, int unused)
{
  return *(volatile uint32_t *)addr;
}

static void mem_write(FAR const volatile void *addr, uint32_t val, int unused)
{
  *(volatile uint32_t *)addr = val;
}

static bool qemu_pci_test_bar(FAR struct pci_test_dev_ops_s *test_ops,
                             FAR struct pci_test_dev_hdr_s *test_hdr,
                             uint16_t test_num)
{
  const int write_limit = 8;
  uint32_t  count;
  uint32_t  data;
  uint32_t  offset;
  uint8_t   width;
  int       write_cnt;
  int       i;
  char      testname[32];

  pciinfo("WRITING Test# %d %p\n", test_num, &test_hdr->test);
  test_ops->write(&test_hdr->test, test_num, 1);

  /* Reading of the string is a little ugly to handle the case where
   * we must use the port access methods.  For memory map we would
   * be able to just read directly.
   */

  testname[sizeof(testname) - 1] = 0;
  for (i = 0; i < sizeof(testname); i++)
    {
      testname[i] = (char)test_ops->read((void *)&test_hdr->name + i, 1);
      if (testname[i] == 0)
        {
          break;
        }
    }

  pciinfo("Running test: %s\n", testname);

  count = test_ops->read(&test_hdr->count, 4);
  pciinfo("COUNT: %04x\n", count);
  if (count != 0)
    {
      return false;
    }

  width = test_ops->read(&test_hdr->width, 1);
  pciinfo("Width: %d\n", width);

  if (width == 0 || width > 4)
    {
      return false;
    }

  data = test_ops->read(&test_hdr->data, 4);
  pciinfo("Data: %04x\n", data);

  offset = test_ops->read(&test_hdr->offset, 4);
  pciinfo("Offset: %04x\n", offset);

  for (write_cnt = 0; write_cnt < write_limit; write_cnt++)
    {
      pciinfo("Issuing WRITE to %p %x %d\n",
              (void *)test_hdr + offset,
              data, width);
      test_ops->write((void *)test_hdr + offset, data, width);
    }

  count = test_ops->read(&test_hdr->count, 4);
  pciinfo("COUNT: %04x\n", count);

  if (!count)
    {
      return true;
    }

  return (int)count == write_cnt;
}

/*****************************************************************************
 * Name: qemu_pci_test_probe
 *
 * Description:
 *   Initialize device
 *
 *****************************************************************************/

static int qemu_pci_test_probe(FAR struct pci_bus_s *bus,
                               FAR const struct pci_dev_type_s *type,
                               uint16_t bdf)
{
  struct pci_dev_s           dev;
  struct pci_test_dev_ops_s  io_ops;
  struct pci_test_dev_ops_s *test_ops;
  struct pci_test_dev_hdr_s *test_hdr;
  uint8_t                    bar_id;
  uint32_t                   bar;
  uint64_t                   bar_addr;
  uint16_t                   test_cnt;

  /* Get dev */

  dev.bus  = bus;
  dev.type = type;
  dev.bdf  = bdf;

  /* Get io ops */

  io_ops.read  = bus->ops->pci_io_read;
  io_ops.write = bus->ops->pci_io_write;

  pci_enable_bus_master(&dev);
  pciinfo("Enabled bus mastering\n");
  pci_enable_io(&dev, PCI_SYS_RES_MEM);
  pci_enable_io(&dev, PCI_SYS_RES_IOPORT);
  pciinfo("Enabled i/o port and memory resources\n");

  for (bar_id = 0; bar_id < PCI_BAR_CNT; bar_id++)
    {
      /* Need to query the BAR for IO vs MEM
       * Also handle if the bar is 64bit address
       */

      if (pci_bar_valid(&dev, bar_id) != OK)
        {
          continue;
        }

      bar = bus->ops->pci_cfg_read(&dev,
          PCI_HEADER_NORM_BAR0 + (bar_id * 4), 4);

      bar_addr = pci_bar_addr(&dev, bar_id);
      test_hdr = (struct pci_test_dev_hdr_s *)bar_addr;

      if ((bar & PCI_BAR_LAYOUT_MASK) == PCI_BAR_LAYOUT_MEM)
        {
          test_ops = &g_mem_ops;

          /* If the BAR is MMIO the it must be mapped */

          bus->ops->pci_map_bar(bar_addr, pci_bar_size(&dev, bar_id));
        }
      else
        {
          test_ops = &io_ops;
        }

      for (test_cnt = 0; test_cnt < 0xffff; test_cnt++)
        {
          if (!qemu_pci_test_bar(test_ops, test_hdr, test_cnt))
            {
              break;
            }

          pciinfo("Test Completed BAR [%d] TEST [%d]\n", bar_id, test_cnt);
        }

      if (pci_bar_is_64(&dev, bar_id))
        {
          bar_id++;
        }
    }

  return OK;
}