/** @file

  Extendible

  @section license License

  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.

  @section details Details

////////////////////////////////////////////
  Implements:
  * Extendible<Derived_t>
  * Schema
  * fieldAdd
  * fieldFind
 */

#pragma once
#include <cstdint>
#include <typeinfo>
#include <typeindex>
#include <cstddef>
#include <cstring>
#include <sstream>
#include <functional>
#include <iomanip>
#include <ostream>
#include <typeindex>
#include <typeinfo>
#include <type_traits>
#include <unordered_map>
#include <limits>

#include "tscore/AtomicBit.h"
#include "tscore/ink_assert.h"
#include "tscore/ink_memory.h"
#include "tscore/ink_defs.h"

//////////////////////////////////////////
/// SUPPORT MACRO
#define DEF_EXT_NEW_DEL(cls)               \
  void *operator new(size_t)               \
  {                                        \
    return ats_malloc(ext::sizeOf<cls>()); \
  }                                        \
  void *operator new(size_t, void *ptr)    \
  {                                        \
    return ptr;                            \
  }                                        \
  void operator delete(void *ptr)          \
  {                                        \
    free(ptr);                             \
  }

//////////////////////////////////////////
/// HELPER CLASSES

/////////////////////////
// Has 'const_iterator' trait class
template <typename T> class has_const_iterator
{
  template <typename U> static std::true_type  check(typename U::const_iterator *);
  template <typename U> static std::false_type check(...);

public:
  static constexpr decltype(check<T>(nullptr)) value{}; // value is 'constexpr true' if T defines 'const_iterator'
};
/////////////////////////
// Has 'super_type' trait class
template <typename T> class has_super_type
{
  template <typename U> static std::true_type  check(typename U::super_type *);
  template <typename U> static std::false_type check(...);

public:
  static constexpr decltype(check<T>(nullptr)) value{}; // value is 'constexpr true' if T defines 'super_type'
};

//////////////////////////////////////////
//////////////////////////////////////////
//////////////////////////////////////////

// C API

// context (internally FieldDesc*)
using ExtFieldContext = const void *;
using DerivedPtr      = void *;
using FieldPtr        = void *;

FieldPtr ExtFieldPtr(DerivedPtr derived, ExtFieldContext field_context, int *size = nullptr);

//////////////////////////////////////////
//////////////////////////////////////////
//////////////////////////////////////////

namespace ext
{
////////////////////////////////////////////////////
// Forward declarations
template <typename Derived_t, typename Field_t> class FieldId; // field handle (strongly type)
template <typename Derived_t> class Extendible;

namespace details // internal stuff
{
  using Offest_t = uint16_t; // used to store byte offsets to fields
  struct FieldDesc;          // keeps the field properties, and methods
  class Schema;              // container of fields and methods

  // forward declare friend functions used outside of details
  template <typename T> uintptr_t initRecurseSuper(T &, uintptr_t);
  template <typename T> FieldPtr  FieldPtrGet(Extendible<T> const &, details::FieldDesc const &);

  //////////////////////////////////////////

  bool &areFieldsFinalized();

  /////////////////////////////////////////////////////////////////////
  /// ext::details::FieldDesc - type erased field descriptor, with type specific std::functions
  struct FieldDesc {
    Offest_t        ext_loc_offset; ///< byte offset to Extendible._ext_loc
    Offest_t        field_offset;   ///< byte offset from ext_loc to field
    std::type_index field_type_idx; ///< data type index
    uint16_t        size;           ///< byte size of field
    uint8_t         align;          ///< alignment of field
    uint8_t         mask;           ///< mask for packed bit operations

    // specialize the following
    std::function<void(FieldPtr)>                     constructor;
    std::function<void(FieldPtr)>                     destructor;
    std::function<void(std::ostream &, void const *)> serializer;

    FieldDesc() : field_type_idx(typeid(std::nullptr_t)) {}
  };

  /////////////////////////////////////////////////////////////////////
  /// ext::details::Schema manages the a static layout of fields as data structures
  class Schema
  {
  public:
    std::unordered_map<std::string, FieldDesc> fields;                ///< defined elements of the blob by name
    size_t                                     alloc_size      = 0;   ///< bytes to allocate for fields
    uint8_t                                    alloc_align     = 1;   ///< alignment of block
    std::atomic<uint>                          cnt_constructed = {0}; ///< the number of Extendible<Derived> created.
    std::atomic<uint> cnt_fld_constructed = {0}; ///< the number of Extendible<Derived> that constructed fields.
    std::atomic<uint> cnt_destructed      = {0}; ///< the number of Extendible<Derived> destroyed.

  public:
    Schema() {}
    ~Schema();

    /// Testing methods
    bool no_instances() const; ///< returns true if there are no instances of Extendible<Derived_t>
    bool reset();              ///< clears all field definitions.

    size_t fullSize(size_t base_size) const;         ///< returns sizeof memory allocated
    void   updateMemOffsets();                       ///< updates memory offsets, alignment, and total allocation size
    void   callConstructor(uintptr_t ext_start_ptr); ///< calls constructor for each field
    void   callDestructor(uintptr_t ext_start_ptr);  ///< call destructor for each field

  }; // end Schema struct
} // namespace details

/// ext::Extendible allows code (and Plugins) to declare member-like variables during system init.
/*
 * This class uses a special allocator (ext::create) to extend the memory allocated to store run-time static
 * variables, which are registered by plugins during system init. The API is in a functional style to support
 * multiple inheritance of Extendible classes. This is templated so static variables are instanced per Derived
 * type, because we need to have different field schema per type.
 *
 * @tparam Derived_t - the class that you want to extend at runtime.
 *
 * @see test_Extendible.cc for examples
 *
 */
template <typename Derived_t> class Extendible
{
public:
  using short_ptr_t = uint16_t;

  // static
  static details::Schema schema; ///< one schema instance per Derived_t to define contained fields

  // return the address offset of the ext_loc member variable
  static constexpr size_t
  getLocOffset()
  {
    return offsetof(Extendible<Derived_t>, ext_loc);
  }

  // member variables
private:
  short_ptr_t ext_loc = 0; ///< byte offset to extendible storage

  // lifetime management
  /** don't allow copy construct, that doesn't allow atomicity */
public:
  Extendible(Extendible &) = delete;

protected:
  Extendible();
  // use ext::create() exclusively for allocation and initialization

  /** destruct all fields */
  ~Extendible();

private:
  /** construct all fields */
  size_t initFields(uintptr_t start_ptr); ///< tell this extendible where it's memory offset start is.

  uintptr_t
  getBegin() const
  {
    return uintptr_t(this) + ext_loc;
  }

  template <typename T> friend T          *create();
  template <typename T> friend uintptr_t   details::initRecurseSuper(T &, uintptr_t);
  template <typename T> friend FieldPtr    details::FieldPtrGet(Extendible<T> const &, details::FieldDesc const &);
  template <typename T> friend std::string viewFormat(T const &, uintptr_t, int);
};

// define the static schema per derived type
template <typename Derived_t> details::Schema Extendible<Derived_t>::schema;

// ####################################################
// ####################################################
//  UTILITY Functions

//////////////////////////////////////////////////////
/// HexToString function for serializing untyped C storage
// TODO: use swoc::bwf::As_Hex() after PR goes through
inline void
hexToStream(std::ostream &os, void const *buf, uint16_t size)
{
  static const char hexDigits[] = "0123456789abcdef";

  const uint8_t *src = static_cast<const uint8_t *>(buf);
  for (int i = 0; i < size; i++) {
    os << hexDigits[src[i]];
  }
};

template <typename Field_t>
void
serializeField(std::ostream &os, Field_t const &f)
{
  using namespace std;
  // print containers as lists
  if constexpr (has_const_iterator<Field_t>::value) {
    os << "[";
    for (auto const &a : f) {
      serializeField(os, a);
      os << ", ";
    }
    os << "]";
    return;
  } else {
    os << f;
  }
}
// ####################################################
// ####################################################
// ####################################################

/////////////////////////////////////////////////////////////////////
// FieldId
/// a strongly typed pointer to FieldDesc
template <typename Derived_t, typename Field_t> class FieldId
{
public:
  ext::details::FieldDesc const *desc = nullptr;
  bool                           isValid() const;
  FieldId(ext::details::FieldDesc const &);
  FieldId() {}
  FieldId(FieldId const &)            = default;
  FieldId &operator=(FieldId const &) = default;
};

namespace details
{
  template <typename Derived_t>
  FieldPtr
  FieldPtrGet(Extendible<Derived_t> const &d, FieldDesc const &desc)
  {
    return FieldPtr(d.Extendible<Derived_t>::getBegin() + desc.field_offset);
  }

  // overloadable function to construct and init an FieldId
  //////////////////////////////////////////////////////
  /// Type Generic Template

  template <typename Derived_t, typename Field_t>
  Field_t const &
  fieldGet(void const *fld_ptr, FieldId<Derived_t, Field_t> const & /* field ATS_UNUSED */)
  {
    return *static_cast<Field_t const *>(fld_ptr);
  }

  template <typename Derived_t, typename Field_t>
  Field_t &
  fieldSet(void *fld_ptr, FieldId<Derived_t, Field_t> const & /* field ATS_UNUSED */)
  {
    return *static_cast<Field_t *>(fld_ptr);
  }

  template <typename Derived_t, typename Field_t>
  void
  makeFieldId(FieldId<Derived_t, Field_t> &id, FieldDesc &desc)
  {
    ink_assert(!areFieldsFinalized());

    desc.field_type_idx = std::type_index(typeid(Field_t));
    desc.ext_loc_offset = Extendible<Derived_t>::getLocOffset();
    desc.field_offset   = std::numeric_limits<decltype(desc.field_offset)>::max();
    desc.size           = sizeof(Field_t);
    desc.align          = alignof(Field_t);
    desc.mask           = 0;

    id = FieldId<Derived_t, Field_t>(desc);

    //
    desc.constructor = [](FieldPtr fld_ptr) { new (fld_ptr) Field_t(); };
    desc.destructor  = [](FieldPtr fld_ptr) { static_cast<Field_t *>(fld_ptr)->~Field_t(); };
    desc.serializer  = [id](std::ostream &os, void const *fld_ptr) { serializeField(os, fieldGet(fld_ptr, id)); };
  }

  //////////////////////////////////////////////////////
  /// C API specialization
  // no type or constructor. Just a size.

  template <typename Derived_t>
  void
  makeFieldId(FieldDesc &desc, uint16_t size)
  {
    ink_assert(!areFieldsFinalized());
    desc.field_type_idx = typeid(void);
    desc.ext_loc_offset = Extendible<Derived_t>::getLocOffset();
    desc.field_offset   = std::numeric_limits<decltype(desc.field_offset)>::max();
    desc.size           = size;
    desc.align          = 1;
    desc.mask           = 0;

    //
    desc.constructor = nullptr;
    desc.destructor  = nullptr;
    desc.serializer  = [size](std::ostream &os, void const *fld_ptr) { hexToStream(os, fld_ptr, size); };
  }

  //////////////////////////////////////////////////////
  /// Bool specializations

  template <typename Derived_t>
  bool
  fieldGet(const void *fld_ptr, FieldId<Derived_t, bool> const &field)
  {
    return static_cast<bool>((*static_cast<const uint8_t *>(fld_ptr)) & field.desc->mask);
  }

  template <typename Derived_t>
  AtomicBit
  fieldSet(FieldPtr fld_ptr, FieldId<Derived_t, bool> const &field)
  {
    return AtomicBit{static_cast<uint8_t *>(fld_ptr), field.desc->mask};
  }

  template <typename Derived_t>
  void
  makeFieldId(FieldId<Derived_t, bool> &id, FieldDesc &desc)
  {
    desc.field_type_idx = std::type_index(typeid(bool));
    desc.ext_loc_offset = Extendible<Derived_t>::getLocOffset();
    desc.field_offset   = std::numeric_limits<decltype(desc.field_offset)>::max();
    desc.size           = 0;
    desc.align          = 0;
    desc.mask           = 0;

    id = FieldId<Derived_t, bool>(desc);

    //
    desc.constructor = nullptr;
    desc.destructor  = nullptr;
    desc.serializer  = [id](std::ostream &os, void const *fld_ptr) { serializeField(os, fieldGet(fld_ptr, id)); };
  }

  //////////////////////////////////////////////////////
  /// std::atomic<bool> specializations (same as bool)

  template <typename Derived_t>
  inline bool
  fieldGet(void const *fld_ptr, FieldId<Derived_t, std::atomic<bool>> const &field)
  {
    return bool(fld_ptr & field.mask);
  }

  template <typename Derived_t>
  inline AtomicBit
  fieldSet(FieldPtr fld_ptr, FieldId<Derived_t, std::atomic<bool>> const &field)
  {
    return AtomicBit{fld_ptr, field.mask};
  }

  template <typename Derived_t>
  void
  makeFieldId(FieldId<Derived_t, std::atomic<bool>> &id, FieldDesc &desc)
  {
    desc.field_type_idx = std::type_index(typeid(std::atomic<bool>));
    desc.ext_loc_offset = Extendible<Derived_t>::getLocOffset();
    desc.field_offset   = std::numeric_limits<decltype(desc.field_offset)>::max();
    desc.size           = 0;
    desc.align          = 0;
    desc.mask           = 0;

    id = FieldId<Derived_t, std::atomic<bool>>(desc);

    //
    desc.constructor = nullptr;
    desc.destructor  = nullptr;
    desc.serializer  = [id](std::ostream &os, void const *fld_ptr) { serializeField(os, fieldGet(fld_ptr, id)); };
  }
} // namespace details

//////////////////////////////////////////////////////
/// safely cast FieldDesc back to FieldId

template <typename Derived_t, typename Field_t>
FieldId<Derived_t, Field_t>::FieldId(ext::details::FieldDesc const &fld_desc) : desc(&fld_desc)
{
  const size_t loc = Extendible<Derived_t>::getLocOffset();
  ink_assert(loc == fld_desc.ext_loc_offset);
  ink_assert(std::type_index(typeid(Field_t)) == fld_desc.field_type_idx);
}

template <typename Derived_t, typename Field_t>
bool
FieldId<Derived_t, Field_t>::isValid() const
{
  return desc != nullptr;
}

// ####################################################
// ####################################################
// ####################################################
//  Functional API for Extendible Field Access
//

////////////////////////////////////////////////////
// Schema Method Definitions
//

/// Add a new Field to this record type
template <class Derived_t, class Field_t>
bool
fieldAdd(FieldId<Derived_t, Field_t> &field_id, char const *field_name)
{
  using namespace ext::details;
  Schema &schema = Extendible<Derived_t>::schema;
  ink_release_assert(schema.no_instances()); // it's too late, we already started allocating.
  ink_release_assert(!areFieldsFinalized()); // it's too late, Fields must be added during Plugin Init.

  auto field_iter = schema.fields.find(field_name);
  if (field_iter != schema.fields.end()) {
    return false;
  }

  makeFieldId(field_id, schema.fields[field_name]);
  schema.updateMemOffsets();
  return true;
}

/// Add a new Field to Derived_t, this C function uses a fat API
template <class Derived_t>
ExtFieldContext
fieldAdd(char const *field_name, int size, void (*construct_fn)(void *), void (*destruct_fn)(void *))
{
  using namespace ext::details;
  Schema &schema = Extendible<Derived_t>::schema;
  ink_release_assert(schema.no_instances()); // it's too late, we already started allocating.
  ink_release_assert(!areFieldsFinalized()); // it's too late, Fields must be added during Plugin Init.
  ink_release_assert(size >= 0);             // non-negative numbers please

  auto field_iter = schema.fields.find(field_name);
  if (field_iter != schema.fields.end()) {
    return nullptr;
  }
  if (size == 0) {
    FieldId<Derived_t, bool> id;
    makeFieldId(id, schema.fields[field_name]);
  } else {
    FieldDesc &desc = schema.fields[field_name];
    makeFieldId<Derived_t>(desc, size);
    desc.constructor = construct_fn;
    desc.destructor  = destruct_fn;
  }
  schema.updateMemOffsets();
  return &schema.fields[field_name];
}

template <class Derived_t, class Field_t>
bool
fieldFind(FieldId<Derived_t, Field_t> &field_id, char const *field_name)
{
  using namespace ext::details;
  ink_release_assert(areFieldsFinalized());
  Schema const &schema     = Extendible<Derived_t>::schema;
  auto          field_iter = schema.fields.find(field_name);
  if (field_iter == schema.fields.end()) {
    return false; // didn't find name
  }
  field_id = FieldId<Derived_t, Field_t>(field_iter->second);
  return true;
}

// each Derived_t will have a DerivedExtfieldFind
template <class Derived_t>
ExtFieldContext
fieldFind(char const *field_name)
{
  using namespace ext::details;
  ink_release_assert(areFieldsFinalized());
  Schema const &schema     = Extendible<Derived_t>::schema;
  auto          field_iter = schema.fields.find(field_name);
  if (field_iter == schema.fields.end()) {
    return nullptr; // didn't find name
  }
  return &field_iter->second;
}

////////////////////////////////////////////////////
/// ext::get & ext::set accessor functions
template <typename T, typename Derived_t, typename Field_t>
inline decltype(auto)
get(T const &d, FieldId<Derived_t, Field_t> &field)
{
  Extendible<Derived_t> const &ext     = d;
  FieldPtr                     fld_ptr = ext::details::FieldPtrGet(ext, *field.desc);
  return ext::details::fieldGet(fld_ptr, field);
}

template <typename T, typename Derived_t, typename Field_t>
inline decltype(auto)
set(T &d, FieldId<Derived_t, Field_t> &field)
{
  Extendible<Derived_t> const &ext     = d;
  FieldPtr                     fld_ptr = ext::details::FieldPtrGet(ext, *field.desc);
  return ext::details::fieldSet(fld_ptr, field);
}

/////////////////////////
// ext::sizeOf - returns the size of a class + all extensions.
//

template <typename Derived_t>
inline size_t
sizeOf(size_t size = sizeof(Derived_t))
{
  // add size of super extendibles
  if constexpr (has_super_type<Derived_t>::value) {
    size = ext::sizeOf<typename Derived_t::super_type>(size);
  } else {
    static_assert(std::is_same<decltype(Derived_t::schema), decltype(Extendible<Derived_t>::schema)>::value,
                  "ambiguous schema, Derived_t is missing super_type");
  }

  // add size of this extendible
  if constexpr (std::is_base_of<Extendible<Derived_t>, Derived_t>::value) {
    size = Extendible<Derived_t>::schema.fullSize(size);
    // assert that the schema is not ambiguous.
  }
  return size;
}

/////////////////////////
// Ext Alloc & Init
//
template <class Derived_t> Extendible<Derived_t>::Extendible()
{
  ink_assert(ext::details::areFieldsFinalized());
  // don't call callConstructor until the derived class is fully constructed.
  ++schema.cnt_constructed;
}

template <class Derived_t> Extendible<Derived_t>::~Extendible()
{
  // assert callConstructors was called.
  ink_assert(ext_loc);
  schema.callDestructor(uintptr_t(this) + ext_loc);
  ++schema.cnt_destructed;
  ink_assert(schema.cnt_destructed <= schema.cnt_fld_constructed);
}

/// tell this extendible where it's memory offset start is. Added to support inheriting from extendible classes
template <class Derived_t>
uintptr_t
Extendible<Derived_t>::initFields(uintptr_t start_ptr)
{
  ink_assert(ext_loc == 0);
  start_ptr = ROUNDUP(start_ptr, schema.alloc_align); // pad the previous struct, so that our fields are memaligned correctly
  ink_assert(start_ptr - uintptr_t(this) < UINT16_MAX);
  ext_loc = static_cast<uint16_t>(start_ptr - uintptr_t(this)); // store the offset to be used by ext::get and ext::set
  ink_assert(ext_loc > 0);
  schema.callConstructor(start_ptr);    // construct all fields
  return start_ptr + schema.alloc_size; // return the end of the extendible data
}

namespace details
{
  /// recursively init all extendible structures, and construct fields
  template <typename Derived_t>
  uintptr_t
  initRecurseSuper(Derived_t &derived, uintptr_t tail_ptr /*= 0*/)
  {
    // track a tail pointer, that starts after the class, and iterate each extendible block
    if constexpr (has_super_type<Derived_t>::value) {
      // init super type, move tail pointer
      tail_ptr = initRecurseSuper<typename Derived_t::super_type>(derived, tail_ptr);
    }
    if constexpr (std::is_base_of<Extendible<Derived_t>, Derived_t>::value) {
      // set start for this extendible block after the previous extendible, and move tail pointer to after this block
      tail_ptr = derived.Extendible<Derived_t>::initFields(tail_ptr);
    }
    return tail_ptr;
  }

} // namespace details

// allocate and initialize an extendible data structure
template <typename Derived_t, typename... Args>
Derived_t *
create(Args &&...args)
{
  // don't instantiate until all Fields are finalized.
  ink_assert(ext::details::areFieldsFinalized());

  // calculate the memory needed for the class and all Extendible blocks
  const size_t type_size = ext::sizeOf<Derived_t>();

  // alloc one block of memory
  Derived_t *ptr = static_cast<Derived_t *>(ats_memalign(alignof(Derived_t), type_size));

  // construct (recursively super-to-sub class)
  new (ptr) Derived_t(std::forward(args)...);

  // define extendible blocks start offsets (recursively super-to-sub class)
  details::initRecurseSuper(*ptr, uintptr_t(ptr) + sizeof(Derived_t));
  return ptr;
}

/////////////////////////
// ExtDebugFormat - print a ascii chart of memory layout of a class
//
//  Example layout of C -> B -> A, where C and A are extendible. All contain 1 int.
//  See test_Extendible.cc for class implementation.
//  1A | EXT  | 2b | ##________##__
//  1A | BASE | 2b | __##__________
//  1B | BASE | 2b | ____##________
//  1C | EXT  | 2b | ______##____##
//  1C | BASE | 2b | ________##____

template <typename T>
std::string
viewFormat(T const &t, uintptr_t _base_addr = 0, int _full_size = ext::sizeOf<T>())
{
  using namespace std;
  stringstream ss;
  if (_base_addr == 0) {
    _base_addr = uintptr_t(&t);
  }
  int super_size = 0;
  if constexpr (has_super_type<T>::value) {
    ss << viewFormat<typename T::super_type>(t, _base_addr, _full_size);
    super_size += sizeof(typename T::super_type);
  }

  if constexpr (is_base_of<Extendible<T>, T>::value) {
    Extendible<T> const *e         = &t;
    int                  ptr_start = uintptr_t(e) + Extendible<T>::getLocOffset() - _base_addr;
    int                  ptr_end   = ptr_start + sizeof(typename Extendible<T>::short_ptr_t);
    int                  ext_start = e->getBegin() - _base_addr;
    int                  ext_end   = Extendible<T>::schema.fullSize(ext_start);

    ink_assert(ptr_end <= ext_start);
    ink_assert(ext_end <= _full_size);

    ss << endl << setw(30) << typeid(T).name() << " | EXT  | " << setw(5) << ext_end - ext_start << "b |";
    ss << string(ptr_start, '_').c_str();
    ss << string(ptr_end - ptr_start, '#').c_str();
    ss << string(ext_start - ptr_end, '_').c_str();
    ss << string(ext_end - ext_start, '#').c_str();
    ss << string(_full_size - ext_end, '_').c_str();

    super_size += sizeof(Extendible<T>);
  }

  int super_start  = uintptr_t(&t) - _base_addr;
  int member_start = super_start + super_size;
  int member_end   = super_start + sizeof(T);

  ink_assert(member_start <= member_end);
  ink_assert(member_end <= _full_size);

  ss << endl << setw(30) << typeid(T).name() << " | BASE | " << setw(5) << sizeof(T) - super_size << "b |";
  ss << string(super_start, '_').c_str();
  ss << string(member_start - super_start, '_').c_str();
  ss << string(member_end - member_start, '#').c_str();
  ss << string(_full_size - member_end, '_').c_str();

  return ss.str();
}

namespace details
{
  inline std::string
  ltrim(std::string const &str, const std::string &chars = "\t\n\v\f\r ")
  {
    std::string r(str);
    r.erase(0, str.find_first_not_of(chars));
    return r;
  }
} // namespace details

template <typename T>
void
serialize(std::ostream &os, T const &t)
{
  using namespace std;
  size_t indent = os.width();
  os << endl << setw(indent) << "" << details::ltrim(typeid(T).name(), " 0123456789") << ": {" << endl;
  indent += 2;
  if constexpr (is_base_of<Extendible<T>, T>::value) {
    if constexpr (has_super_type<T>::value) {
      serialize<typename T::super_type>(os, t);
    }
    auto const &schema     = T::schema;
    size_t      name_width = 0;
    for (const auto &kv : schema.fields) {
      name_width = max(name_width, kv.first.length());
    }
    // TODO: clang-5 didn't like the use of a range based for here, change later
    for (auto it = schema.fields.begin(); it != schema.fields.end(); ++it) {
      auto &fname = it->first;
      auto &field = it->second;
      ink_assert(field.serializer);
      os << setw(indent) << "" << setw(name_width) << right << fname << ": ";
      field.serializer(os, details::FieldPtrGet(t, field));
      os << "," << endl;
    }
  }
  indent -= 2;
  os << setw(indent + 1) << "}";
}

template <typename T>
std::string
toString(T const &t)
{
  std::stringstream ss;
  ss.width(0);
  serialize(ss, t);
  return ss.str();
}
} // namespace ext
