/* SPDX-License-Identifier: MIT */
/*
 * Copyright (C) 2022, 2024 Kernkonzept GmbH.
 * Author(s): Christian Pötzsch <christian.poetzsch@kernkonzept.com>
 *
 */
#include <iostream>
#include <unistd.h>
#include <memory>
#include <cstring>
#include <cstdio>

#include <l4/vbus/vbus>
#include <l4/vbus/vbus_gpio>

#include <l4/re/env>
#include <l4/re/error_helper>
#include <l4/re/util/cap_alloc>
#include <l4/re/util/unique_cap>
#include <l4/re/util/object_registry>
#include <l4/re/util/br_manager>
#include <l4/re/util/debug>
#include <terminate_handler-l4>

#include <l4/sys/cxx/ipc_epiface>
#include <l4/cxx/exceptions>
#include <l4/cxx/unique_ptr>

#include "l4/drivers/uart_pl011.h"

/*
 * This example shows how to use uart4 of an Raspberry 4.
 *
 * This uart uses pin 8 (tx) and 9 (rx) when the gpio block for those pins is
 * set to alternate mode 4.
 *
 * Therefor we query the gpio device from io and reprogram the gpio block for
 * those two pins.
 *
 * Afterwards we can initialize the uart device with the given io memory
 * location. The required pl011 uart driver is in the drivers-frst package.
 *
 * To test this setup, two serial to usb converters are required. Connect pin
 * 14/15/ground for the console uart and pin 8/9/ground for the extra uart.
 *
 * This example will print on both consoles different strings in a loop.
 *
 */

//using namespace std;

// Create registry
static L4Re::Util::Registry_server<L4Re::Util::Br_manager_hooks> server;

class Serial_drv : public L4::Irqep_t<Serial_drv>
{
  L4::Cap<L4::Icu> _icu;
  L4::Cap<L4::Irq> _irq;

public:

  Serial_drv(L4::Cap<L4::Icu> icu, l4_addr_t regs) :
    _icu(icu)
  {
    _regs = std::make_shared<L4::Io_register_block_mmio>(regs);

    _uart = std::make_shared<L4::Uart_pl011>(48000001);
    _uart->startup(_regs.get());
    _uart->enable_rx_irq(true);
  }

  void init(L4::Cap<L4::Irq> irq) { _irq = irq; }

  void write(const char *s)
  {
    auto l = strlen(s);
    _uart->write(s, l);
  }

  std::string read()
  {
    std::string s;
    for (;;)
      {
        auto c =_uart->get_char(true);
        if (c == '\r')
          {
            const char *n = "\r\n";
            _uart->write(n, 2);
            break;
          }
        else
          {
            _uart->write((const char*)&c, 1);
            s += (char)c;
          }
      }

    return s;
  }


  int handle_irq()
  {
    auto c =_uart->get_char(true);
    if (c == '\r')
      {
        const char *n = "\r\n";
        _uart->write(n, 2);
      }
    else
      {
        _uart->write((const char*)&c, 1);
      }

    _irq->unmask();

    return 0;
  }

private:
  std::shared_ptr<L4::Io_register_block_mmio> _regs;
  std::shared_ptr<L4::Uart> _uart;

  L4::Cap<L4::Icu> _dev_icu;
  L4::Cap<L4::Irq_eoi> _eoi;
};

int main(void)
{
  std::cout << "Hello, this is ex_gpio_uart" << std::endl;

  try
    {
      L4::Cap<L4vbus::Vbus> vbus =
        L4Re::chkcap(L4Re::Env::env()->get_cap<L4vbus::Vbus>("vbus"),
                     "Get vbus capability.", -L4_ENOENT);

      L4vbus::Device root(vbus, L4VBUS_ROOT_BUS);
      L4vbus::Device dev;
      L4vbus::Icu icu_dev;

      // find ICU
      L4Re::chksys(root.device_by_hid(&dev, "L40009"),
                   "Request ICU device from vbus");
      icu_dev = static_cast<L4vbus::Icu &>(dev);

      l4vbus_device_t dev_info;
      bool gpio_init = false;
      l4_addr_t uart_addr = 0;
      l4_size_t uart_size = 0;
      int uart_irq = 0;

      // Scan vbus for hw devices
      while (root.next_device(&dev, L4VBUS_MAX_DEPTH, &dev_info) == L4_EOK)
        {
          // match device against 'arm,pl011' compatibility ID
          if (dev.is_compatible("arm,pl011") == 1)
            {
              // get all device resources: MMIO, IRQ, GPIO pins for TX and RX
              for (unsigned i = 0; i < dev_info.num_resources; ++i)
                {
                  l4vbus_resource_t res;
                  if (dev.get_resource(i, &res))
                    break;

                  if (res.type == L4VBUS_RESOURCE_MEM)
                    {
                      uart_addr = res.start;
                      uart_size = res.end - res.start + 1;
                    }

                  if (res.type == L4VBUS_RESOURCE_IRQ)
                      uart_irq = res.start;

                  if (res.type == L4VBUS_RESOURCE_GPIO)
                    {
                      gpio_init = true;
                      L4vbus::Gpio_module chipdev = L4vbus::Device(vbus, res.provider);
                      L4vbus::Gpio_module::Pin_slice
                        pin_slice(0, 1<<res.start | 1<<res.end);
                      chipdev.setup(pin_slice, 3, 0);
                      chipdev.pin(res.start).config_pull(0x100); // Pull_none
                      chipdev.pin(res.end).config_pull(0x200); // Pull_up
                    }
                }
            }
        }

      if (!gpio_init)
        L4Re::throw_error(-L4_EINVAL, "Gpio wasn't found\n");
      if (!uart_addr)
        L4Re::throw_error(-L4_EINVAL, "Uart has no MMIO resource.\n");
      if (!uart_irq)
        L4Re::throw_error(-L4_EINVAL, "Uart has no IRQ resource.\n");

      // map MMIO memory
      l4_addr_t offs = uart_addr - l4_trunc_page(uart_addr);
      L4::Cap<L4Re::Dataspace> iocap =
        L4::cap_reinterpret_cast<L4Re::Dataspace>(root.bus_cap());
      l4_addr_t uart_vaddr = 0;
      l4_addr_t addr_trunc = l4_trunc_page(uart_addr);

      L4Re::chksys(L4Re::Env::env()->rm()->attach(&uart_vaddr, uart_size + offs,
                                                  L4Re::Rm::F::Search_addr
                                                  | L4Re::Rm::F::Cache_uncached
                                                  | L4Re::Rm::F::RW,
                                                  L4::Ipc::make_cap_rw(iocap),
                                                  addr_trunc, L4_PAGESHIFT));

      printf("Device MMIO mapped @ %lx:%zx\n", uart_vaddr, uart_size);

      // allocate capability slot for the Icu
      L4Re::Util::Unique_cap<L4::Icu> _icu =
        L4Re::chkcap(L4Re::Util::make_unique_cap<L4::Icu>(), "Allocate icu cap.");

      // retrieve Icu capability
      L4Re::chksys(icu_dev.vicu(_icu.get()), "Request ICU cap from vbus");


      // 1. set irq mode
      L4Re::chksys(_icu->set_mode(uart_irq, L4_IRQ_F_LEVEL_HIGH));

      // 2. create and register server object
      cxx::unique_ptr<Serial_drv> drv =
        cxx::make_unique<Serial_drv>(_icu.get(), uart_vaddr + offs);
      L4::Cap<L4::Irq> _irq = server.registry()->register_irq_obj(drv.get());

      // 3. bind irq cap with irq number to Icu
      l4_msgtag_t ret;
      L4Re::chksys(ret = _icu->bind(uart_irq, _irq),
                                    "Binding Irq capability to Icu");

      drv.get()->init(_irq);

      // 4. unmask irq
      _irq->unmask();

      printf("Console UART\n");
      drv.get()->write("Secondary UART\r\n");

      drv.release();

      // Start server loop
      server.loop();
    }
  catch (L4::Runtime_error &e)
    {
      std::cerr << "Runtime error: " << e.str() << ". Reason: " << e.extra_str()
                << std::endl;
    }

  return 0;

}
