// vi:set ft=cpp: -*- Mode: C++ -*-
/*
 * Copyright (C) 2016-2020, 2022-2023 Kernkonzept GmbH.
 * Author(s): Sarah Hoffmann <sarah.hoffmann@kernkonzept.com>
 *
 * This file is distributed under the terms of the GNU General Public
 * License, version 2.  Please see the COPYING-GPL-2 file for details.
 */
/**
 * \file
 * Convenience functions for creating %L4Re objects.
 */
#pragma once

#include <l4/sys/factory>
#include <l4/sys/ipc_gate>
#include <l4/sys/task>

#include <l4/re/env>
#include <l4/re/dataspace>
#include <l4/re/error_helper>
#include <l4/re/rm>
#include <l4/re/util/unique_cap>
#include <l4/re/util/shared_cap>
#include <l4/cxx/ref_ptr>

/**
 * L4Re test framework
 */
namespace Atkins {

/**
 * Convenience functions for creating L4Re objects.
 *
 * Provides factory functions for a number of frequently used L4Re resources.
 * All returned objects are properly RAII-managed. They will throw an
 * exception if creation fails and clean up after themselves when they
 * go out of scope.
 */
namespace Factory {

enum : unsigned long { Default_factory_limit = 1UL << 16 };

/// Create a capability slot.
template <typename T>
L4Re::Util::Unique_cap<T>
cap(char const *err_msg = "")
{ return L4Re::chkcap(L4Re::Util::make_unique_cap<T>(), err_msg); }

/// Create a capability slot with automatically deleted capability.
template <typename T>
typename L4Re::Util::Unique_del_cap<T>
del_cap(char const *err_msg = "")
{ return L4Re::chkcap(L4Re::Util::make_unique_del_cap<T>(), err_msg); }

/// Create a reference-counted capability slot.
template <typename T>
typename L4Re::Util::Shared_cap<T>
shared_cap(char const *err_msg = "")
{ return L4Re::chkcap(L4Re::Util::make_shared_cap<T>(), err_msg); }

/**
 * Create a reference-counted capability slot with an automatically deleted
 * capability.
 */
template <typename T>
typename L4Re::Util::Shared_del_cap<T>
shared_del_cap(char const *err_msg = "")
{ return L4Re::chkcap(L4Re::Util::make_shared_del_cap<T>(), err_msg); }

/// Create an unattached dataspace.
inline
L4Re::Util::Unique_del_cap<L4Re::Dataspace>
dataspace(unsigned long size = L4_PAGESIZE, unsigned long flags = 0,
          L4::Cap<L4Re::Mem_alloc> fab = L4Re::Env::env()->mem_alloc())
{
  auto ds = del_cap<L4Re::Dataspace>();
  L4Re::chksys(fab->alloc(size, ds.get(), flags));
  return ds;
}

/**
 * A region for which a dataspace is newly allocated and attached.
 *
 * The region is automatically cleaned when the object goes out of scope.
 */
struct Test_region : public cxx::Ref_obj
{
  /**
   * Create a new allocated region.
   *
   * \param size   Size of the region in byte.
   * \param flags  Extra flags to use when allocating the dataspace.
   * \param fab    Factory to create the dataspace from.
   *
   * The region will be attached to the default region manager.
   */
  Test_region(unsigned long size, unsigned long flags,
              L4::Cap<L4Re::Mem_alloc> fab)
  : dataspace(del_cap<L4Re::Dataspace>())
  {
    L4Re::chksys(fab->alloc(size, dataspace.get(), flags));
    auto *e = L4Re::Env::env();
    L4Re::chksys(e->rm()->attach(&region, size,
                                 L4Re::Rm::F::Search_addr | L4Re::Rm::F::RWX,
                                 L4::Ipc::make_cap_rw(dataspace.get()), 0,
                                 L4_PAGESHIFT));
  }

  /**
   * Create a new allocated region.
   *
   * \param size      Size of the region in byte.
   * \param ds_flags  Extra flags to use when allocating the dataspace.
   * \param rm_flags  Extra flags to use when attaching the dataspace
   *                  (L4Re::Rm::Search_addr is always enabled).
   * \param align     Alignment of the virtual region, log2-size, default:
   *                  a page (#L4_PAGESHIFT).
   * \param fab       Factory to create the dataspace from.
   *
   * The region will be attached to the default region manager.
   */
  Test_region(unsigned long size, unsigned long ds_flags = 0,
              L4Re::Rm::Flags rm_flags = L4Re::Rm::F::RWX,
              unsigned char align = L4_PAGESHIFT,
              L4::Cap<L4Re::Mem_alloc> fab = L4Re::Env::env()->mem_alloc())
  : dataspace(del_cap<L4Re::Dataspace>())
  {
    L4Re::chksys(fab->alloc(size, dataspace.get(), ds_flags, align));
    auto *e = L4Re::Env::env();
    L4Re::chksys(e->rm()->attach(&region, size, L4Re::Rm::F::Search_addr | rm_flags,
                                 L4::Ipc::make_cap_rw(dataspace.get()), 0,
                                 align));
  }

  /// Get a pointer to the beginning of the region.
  template <typename T>
  T get() const
  { return reinterpret_cast<T>(region.get()); }

  /**
   * Create a flexpage from the beginning of the region.
   *
   * \param order   Log2(size) of the flexpage.
   * \param rights  Flexpage rights.
   * \return Created flexpage.
   *
   * This function verifies if the beginning of the region is aligned according
   * to the passed order of the intended flexpage.
   */
  l4_fpage_t fpage(unsigned char order, unsigned char rights = L4_FPAGE_RWX)
  {
    if (   (sizeof(l4_addr_t) == 8 && order > 63)
        || (sizeof(l4_addr_t) == 4 && order > 31))
      throw L4::Runtime_error(L4_EINVAL, "flexpage size overflow");
    if (get<l4_addr_t>() & ((1UL << order) - 1))
      throw L4::Runtime_error(L4_EINVAL, "region not aligned to size of flexpage");
    return l4_fpage(get<l4_addr_t>(), order, rights);
  }

  L4Re::Util::Unique_del_cap<L4Re::Dataspace> dataspace;
  L4Re::Rm::Unique_region<void *> region;
};

/** Create an attached dataspace.
 *
 * \note Use this version when a reference to the region needs to be shared.
 *       Otherwise use Test_region directly.
 */
inline
cxx::Ref_ptr<Test_region>
ds_region(unsigned long size = L4_PAGESIZE, unsigned long flags = 0,
          L4::Cap<L4Re::Mem_alloc> fab = L4Re::Env::env()->mem_alloc())
{ return cxx::make_ref_obj<Test_region>(size, flags, fab); }

/// Create a kernel object with a previously allocated capability.
template <typename T>
inline void
create_kobj(L4::Cap<T> cap, char const *err_msg = "")
{
  L4Re::chksys(L4Re::Env::env()->factory()->create(cap), err_msg);
}

/// Create a task with a previously allocated capability.
inline void
create_kobj(L4::Cap<L4::Task> cap, char const *err_msg = "",
            l4_fpage_t fpage = l4_fpage_invalid())
{
  L4Re::chksys(L4Re::Env::env()->factory()->create(cap) << fpage, err_msg);
}

/// Create a factory with a previously allocated capability.
inline void
create_kobj(L4::Cap<L4::Factory> cap, char const *err_msg = "",
            unsigned long limit = Default_factory_limit)
{
  L4Re::chksys(L4Re::Env::env()->factory()->create(cap) << limit, err_msg);
}

/// Create an unbound IPC gate with a previously allocated capability.
inline void
create_kobj(L4::Cap<L4::Ipc_gate> cap, char const *err_msg = "")
{
  L4Re::chksys(
    L4Re::Env::env()->factory()->create_gate(cap, L4::Cap_base::Invalid, 0),
    err_msg);
}

/// Allocate a capability slot and create a kernel object.
template <typename T>
L4Re::Util::Unique_cap<T>
kobj(char const *err_msg = "")
{
  auto ret = cap<T>(err_msg);
  create_kobj(ret.get(), err_msg);
  if (0 == L4Re::chksys(ret.validate(), err_msg))
    throw L4::Runtime_error(L4_EINVAL, err_msg);

  return ret;
}

/**
 * Allocate a capability slot and create a kernel object. The object will be
 * automatically destroyed when the capability goes out of scope.
 */
template <typename T>
L4Re::Util::Unique_del_cap<T>
del_kobj(char const *err_msg = "")
{
  auto ret = del_cap<T>(err_msg);
  create_kobj(ret.get(), err_msg);
  if (0 == L4Re::chksys(ret.validate(), err_msg))
    throw L4::Runtime_error(L4_EINVAL, err_msg);

  return ret;
}

/**
 * Create a new capability with `rights` referencing the same kernel object.
 *
 * \tparam T   Capability interface type, i.e. Factory, Irq, Task.
 *
 * \param src      Source capability to create the new capability from.
 * \param rights   Rights the new capability should have.
 *
 * \return A new capability mapping.
 *
 * \pre Caller possesses at least the specified rights on `src`, otherwise the
 *      intersection of the caller's rights and the specified rights will be
 *      the resulting capability's rights.
 *
 * \post A new capability slot is allocated with the default allocator. The
 *       ownership is passed to the caller.
 */
template <typename T>
L4Re::Util::Unique_cap<T>
copy_cap(L4::Cap<T> const &src, unsigned rights)
{
  auto mappee = cap<T>("Allocate new capability slot.");

  auto me = L4Re::Env::env()->task();
  L4Re::chksys(me->map(me, l4_obj_fpage(src.cap(), 0, rights),
                       mappee.snd_base()),
               "Create a mapping in the new capability slot.");

  long ret = l4_error(me->cap_valid(mappee.get()));
  if (ret < 0)
    L4Re::chksys(ret,
                 "Validate the presence of the kernel object (IPC error).");

  if (ret == 0)
    L4Re::chksys(-L4_ENOENT, "Validate the presence of the kernel object.");

  return mappee;
}

/// \copydoc copy_cap
template <typename T>
L4Re::Util::Unique_cap<T>
copy_cap(L4Re::Util::Unique_cap<T> const &src, unsigned rights)
{
  return copy_cap<T>(src.get(), rights);
}

/// \copydoc copy_cap
template <typename T>
L4Re::Util::Unique_cap<T>
copy_cap(L4Re::Util::Unique_del_cap<T> const &src, unsigned rights)
{
  return copy_cap<T>(src.get(), rights);
}

/// \copydoc copy_cap
template <typename T>
L4Re::Util::Unique_cap<T>
copy_cap(L4Re::Util::Shared_cap<T> const &src, unsigned rights)
{
  return copy_cap<T>(src.get(), rights);
}

/// \copydoc copy_cap
template <typename T>
L4Re::Util::Unique_cap<T>
copy_cap(L4Re::Util::Shared_del_cap<T> const &src, unsigned rights)
{
  return copy_cap<T>(src.get(), rights);
}
} } //namespace Factory, namespace Atkins
