#include "function.hpp"

#include <boost/algorithm/string/join.hpp>
#include <boost/range/adaptor/transformed.hpp>
#include <boost/range/iterator.hpp>

#include <map>

namespace
{ // anonymous

const int INDEX_RETURN{-5};
const int INDEX_INSTANCE{-1};
const int INDEX_CF{150};
const int INDEX_ERROR{100};
const std::string MOVE = "std::move";

struct FunctionGenerator : public GeneratorBase
{
  using Output = FunctionDefinition::Output;

  const ElementFunction &func;
  // duplicated from above
  const std::string &kind;
  const std::string &klass, &klasstype;
  // errors collected along the way (trigger abort)
  std::vector<std::string> errors;
  // collected dependencies
  std::set<std::string> &deps;
  // tweak debug level upon error
  bool ignored = false;
  bool introspectable = true;
  bool allow_deprecated = false;

  // also detect various function generation alternatives
  enum class opt_except {
    NOEXCEPT,
    THROW,
    EXPECTED,
    GERROR,
    DEFAULT = NOEXCEPT,
    ALT = GERROR
  };
  std::set<opt_except> do_except = {opt_except::DEFAULT};
  enum class opt_output { PARAM, TUPLE, DEFAULT = PARAM, ALT = TUPLE };
  std::set<opt_output> do_output = {opt_output::DEFAULT};
  enum class opt_nullable {
    PRESENT,
    DISCARD,
    DEFAULT = PRESENT,
    ALT = DISCARD
  };
  std::set<opt_nullable> do_nullable = {opt_nullable::DEFAULT};

  struct Options
  {
    opt_except except{};
    opt_output output{};
    opt_nullable nullable{};
    Options(opt_except _except, opt_output _output, opt_nullable _nullable)
        : except(_except), output(_output), nullable(_nullable)
    {}
    Options() {}
  };

  // global info
  // track callback's user_data
  int found_user_data = INDEX_DEFAULT;
  // const method
  bool const_method = false;
  // typical async method;
  // has GAsyncReadyCallback param with async scope
  std::string async_cb_wrapper;
  // indexed by GIR numbering (first non-instance parameter index 0)
  std::map<int, Parameter> paraminfo;
  // param numbers referenced by some other parameter
  std::set<int> referenced;
  // collects generated declaration and implementation
  std::ostringstream oss_decl, oss_impl;

  using FunctionData = FunctionDefinition;

  struct FunctionDataExtended : public FunctionDefinition
  {
    // (indexed as above)
    std::vector<decltype(cpp_decl)::mapped_type> cpp_decl_unfolded;

    FunctionDataExtended() = default;

    FunctionDataExtended(FunctionDefinition odef)
        : FunctionDefinition(std::move(odef))
    {
      auto &def = *this;
      // process decl map into plain list
      def.cpp_decl_unfolded.clear();
      for (auto &&e : def.cpp_decl)
        def.cpp_decl_unfolded.emplace_back(e.second);
    }
  };

public:
  FunctionGenerator(GeneratorContext &_ctx, const std::string _ns,
      const ElementFunction &_func, const std::string &_klass,
      const std::string &_klasstype, std::set<std::string> &_deps,
      bool _allow_deprecated = false)
      : GeneratorBase(_ctx, _ns), func(_func), kind(func.kind), klass(_klass),
        klasstype(_klasstype), deps(_deps), allow_deprecated(_allow_deprecated)
  {
    assert(func.name.size());
    assert(func.kind.size());
    assert(func.c_id.size());
    assert(func.functionexp.size());
  }

  static bool ends_with(std::string const &value, std::string const &ending)
  {
    if (ending.size() > value.size())
      return false;
    return std::equal(ending.rbegin(), ending.rend(), value.rbegin());
  };

  void handle_skip(const skip &ex)
  {
    // we tried but it went wrong ...
    if (errors.empty() && !introspectable) {
      errors.push_back("not introspectable");
      // ... so let's not complain about it
      ignored = true;
    }
    if (ex.cause == skip::INVALID) {
      auto level = ignored ? Log::DEBUG : Log::WARNING;
      if (check_suppression(ns, kind, func.c_id))
        level = Log::DEBUG;
      logger(level, "skipping {} {}; {}", kind, func.c_id, ex.what());
    } else if (ex.cause == skip::IGNORE) {
      ignored = true;
    }
    errors.push_back(ex.what());
  }

  bool check_errors()
  {
    // check errors
    if (errors.size()) {
      auto reason = ignored ? "IGNORE" : "SKIP";
      auto err = fmt::format(
          "// {}; {}", reason, boost::algorithm::join(errors, ", "));
      oss_decl << err << std::endl;
      // no impl for signals
      if (kind != EL_SIGNAL)
        oss_impl << err << std::endl;
      return true;
    }
    return false;
  }

  // all parameters should have ctype info except for signal case
  void check_ctype(const ArgInfo &info)
  {
    if (info.ctype.empty() && kind != EL_SIGNAL)
      throw skip("missing c:type info");
  }

  void collect_param(const pt::ptree::value_type &n, int &param_no)
  {
    // instance parameter not in cpp signature
    auto &el = n.first;
    bool instance = (el == EL_INSTANCE_PARAMETER);
    param_no += instance ? 0 : 1;
    // directly parse into target
    // so we always have minimal attributes for raw fallback
    auto &pinfo = paraminfo[param_no];
    pinfo.instance = instance;
    pinfo.name = unreserve(get_name(n.second), false);
    // gather a bunch of attributes
    pinfo.direction = get_attribute(n.second, AT_DIRECTION, DIR_IN);
    pinfo.transfer = get_attribute(n.second, AT_TRANSFER);
    pinfo.closure = get_attribute<int>(n.second, AT_CLOSURE, -10);
    pinfo.destroy = get_attribute<int>(n.second, AT_DESTROY, -10);
    pinfo.callerallocates =
        get_attribute<int>(n.second, AT_CALLER_ALLOCATES, 0);
    pinfo.scope = get_attribute(n.second, AT_SCOPE, "");
    bool allownone = get_attribute<bool>(n.second, AT_ALLOW_NONE, 0);
    // limited backwards compatbility for allow-none
    bool is_out = pinfo.direction.find(DIR_OUT) != pinfo.direction.npos;
    pinfo.optional =
        (is_out && allownone) || get_attribute<bool>(n.second, AT_OPTIONAL, 0);
    pinfo.nullable =
        (!is_out && allownone) || get_attribute<bool>(n.second, AT_NULLABLE, 0);
    // might fail
    parse_arginfo(n.second, &pinfo.tinfo);
    check_ctype(pinfo.tinfo);
  }

  void collect_node(const pt::ptree::value_type &entry)
  {
    auto &node = entry.second;

    // should be ignored in some cases
    try {
      auto deprecated = get_attribute<int>(node, AT_DEPRECATED, 0);
      if (ctx.match_ignore.matches(ns, kind, {func.name, func.c_id}))
        throw skip("marked ignore", skip::IGNORE);
      if (deprecated && !allow_deprecated)
        throw skip("deprecated", skip::IGNORE);
      // there are a few nice-to-have in this case (e.g. some _get_source)
      // so let's try anyway but not complain too much if it goes wrong
      auto intro = get_attribute<int>(node, AT_INTROSPECTABLE, 1);
      // except when renaming is at hand
      auto shadowed = get_attribute(node, AT_SHADOWED_BY, "");
      if (shadowed.size() && !intro)
        throw skip("not introspectable; shadowed-by " + shadowed, skip::IGNORE);
      // otherwise mark and try ...
      introspectable = intro;
    } catch (skip &ex) {
      handle_skip(ex);
    }

    try {
      auto &ret = node.get_child(EL_RETURN);
      // return value
      // always try to read minimal info (e.g. ctype)
      // as it might be needed for method fallback
      auto &pinfo = paraminfo[INDEX_RETURN];
      pinfo.direction = DIR_RETURN;
      // best effort for now, see also below
      pinfo.transfer = get_attribute(ret, AT_TRANSFER, TRANSFER_FULL);
      pinfo.nullable = get_attribute<bool>(ret, AT_NULLABLE, 0);
      parse_arginfo(ret, &pinfo.tinfo);
      // override constructor to return class instance
      if (kind == EL_CONSTRUCTOR)
        parse_typeinfo(klasstype, pinfo.tinfo);
      check_ctype(pinfo.tinfo);
      pinfo.transfer = pinfo.tinfo.cpptype == CPP_VOID
                           ? TRANSFER_FULL
                           : get_attribute(ret, AT_TRANSFER);
    } catch (skip &ex) {
      handle_skip(ex);
    }

    { // first pass to collect parameter info
      int param_no = INDEX_INSTANCE;
      auto params = node.get_child_optional(EL_PARAMETERS);
      if (params.is_initialized()) {
        for (auto &n : params.value()) {
          try {
            auto el = n.first;
            if (el == EL_PARAMETER || el == EL_INSTANCE_PARAMETER) {
              collect_param(n, param_no);
            }
          } catch (skip &ex) {
            handle_skip(ex);
          }
        }
      }
    }
  }

  // could be dropped if wrap/unwrap is made more casual about const-stuff
  // but on the other hand these checks catch some incorrect annotations
  // so some warning or discarding still has merit
  void verify_const_transfer(const ArgInfo &info, const std::string &transfer,
      const std::string &direction, bool /*wrap*/)
  {
    auto ctype = info.ctype;
    // never mind in case of signal
    if (kind == EL_SIGNAL)
      return;
    // note that const for parameter is not always picked up reliably
    // so if the ctype is const, it probably really is
    // (and so we might complain or warn)
    // but if the ctype is not const, the real one in function might
    // actually be (so we can't know for sure and then should not warn)
    // const and transfer full should not go together
    if (is_const(ctype) && transfer != TRANSFER_NOTHING) {
      auto level =
          check_suppression(ns, kind, func.c_id) ? Log::DEBUG : Log::WARNING;
      logger(level, "warning; {}; const transfer {} mismatch [{}]", func.c_id,
          transfer, direction);
    }
#if 0
    if ((flags & TYPE_BASIC) && (flags & TYPE_CLASS)) {
      // string transfer none preferably unwraps to a const
      // but as mentioned above; this check is not reliable
      if (!wrap && !is_const (ctype) && transfer == TRANSFER_NOTHING)
        throw skip ("transfer none on non-const string");
    }
#endif
  }

  bool process_param_in_callback(int param_no, const Parameter &pinfo,
      const Options & /*options*/, FunctionData &def, bool defaultable)
  {
    auto closure = pinfo.closure;
    auto scope = pinfo.scope;
    auto destroy = pinfo.destroy;
    auto &pname = pinfo.name;
    auto &&cpptype = pinfo.tinfo.cppreftype(pinfo.transfer);
    const bool callee = kind == EL_CALLBACK || kind == EL_VIRTUAL_METHOD;

    logger(Log::LOG, "param_in_callback {} {}", param_no, pname);
    // lots of sanity to check
    static const std::string SCOPE_ASYNC_DEP("async_dep");
    const bool async_method = !async_cb_wrapper.empty();
    if (closure < 0)
      throw skip("callback misses closure info");
    auto it = paraminfo.find(closure);
    if (it == paraminfo.end())
      throw skip("callback has invalid closure parameter");
    const auto &cctype = it->second.ptype;
    if (get_pointer_depth(cctype) != 1)
      throw skip("invalid callback closure parameter type " + cctype);
    // if async method, there is at least 1 async scope callback
    // if call also supports another callback, it could in principle
    // have any valid scope, but quite likely it has no "sane" scope,
    // and its user_data parameter is assumed to live within the async scope
    // so allow for this case in the sanity checks below
    // though this case is only dealt with in regular callforward code
    // (not in callback code)
    if (scope.empty()) {
      if (!async_method)
        throw skip("callback misses scope info");
      else if (callee)
        throw skip(kind + "; async_dep scope not supported");
      scope = SCOPE_ASYNC_DEP;
    }
    if ((scope == SCOPE_NOTIFIED) != (destroy >= 0)) {
      if (!async_method)
        throw skip("callback destroy info mismatch");
      scope = SCOPE_ASYNC_DEP;
    }
    if (destroy >= 0) {
      it = paraminfo.find(destroy);
      if (it == paraminfo.end()) {
        throw skip("callback has invalid destroynotify parameter");
      } else {
        auto girname = it->second.tinfo.girname;
        // mind qualification
        if (girname != GIR_GDESTROYNOTIFY)
          throw skip(
              "invalid callback destroy notify parameter type " + girname);
      }
    }
    // there may be multiple callbacks, but they should not overlap
    // so check if we already entered something for the call parameter
    if (def.c_call.find(closure) != def.c_call.end())
      throw skip("callback closure parameter already used");
    if (def.c_call.find(destroy) != def.c_call.end())
      throw skip("callback destroy parameter already used");

    { // this could be callback parameter within a callback or virtual method
      // so collect the needed parts for the argument trait
      // (which have been validated above)
      auto &ti = def.arg_traits[param_no];
      ti.args = {param_no, closure};
      if (destroy >= 0)
        ti.args.push_back(destroy);
      // determine custom arg trait type from type name
      // (with suitable suffix and in internal namespace)
      auto index = cpptype.rfind(GI_SCOPE);
      auto insert = index != cpptype.npos ? index + GI_SCOPE.size() : 0;
      ti.custom = cpptype;
      ti.custom.insert(insert, GI_NS_INTERNAL + GI_SCOPE);
      ti.custom += GI_SUFFIX_CB_TRAIT;
    }

    // declaration always the same
    def.cpp_decl[param_no] = fmt::format("{} {}", cpptype, pname);
    // could be nullable/optional
    if (pinfo.nullable && defaultable) {
      def.cpp_decl[param_no] += " = nullptr";
    } else {
      defaultable = false;
    }

    // some variation in handling
    auto cbw_pname = pname + "_wrap_";
    // pass along empty callback as NULL
    if (scope == SCOPE_CALL) {
      auto s = fmt::format("auto {} = {} ? unwrap (std::move ({}), "
                           "gi::scope_call) : nullptr",
          cbw_pname, pname, pname);
      def.pre_call.push_back(s);
      s = fmt::format("std::unique_ptr<std::remove_pointer<decltype({})>:"
                      ":type> {}_sp ({})",
          cbw_pname, cbw_pname, cbw_pname);
      def.pre_call.push_back(s);
    } else if (scope == SCOPE_NOTIFIED || scope == SCOPE_ASYNC ||
               scope == SCOPE_ASYNC_DEP) {
      // consider async_dep like notified but without a destroy;
      // ownership of the wrapper (pointer) is made part of the async callback
      auto pscope =
          scope != SCOPE_ASYNC ? "gi::scope_notified" : "gi::scope_async";
      def.pre_call.push_back(
          fmt::format("auto {} = {} ? unwrap (std::move ({}), {}) : nullptr",
              cbw_pname, pname, pname, pscope));
      // track wrapper for potential use below
      // (may have to take ownership of another callback wrapper)
      const auto &ctype = pinfo.tinfo.ctype;
      if (scope == SCOPE_ASYNC &&
          ctype.find("AsyncReadyCallback") != ctype.npos &&
          async_cb_wrapper.empty())
        async_cb_wrapper = cbw_pname;
      if (destroy >= 0) {
        def.c_call[destroy] =
            fmt::format("{} ? &{}->destroy : nullptr", cbw_pname, cbw_pname);
      } else {
        assert(scope != SCOPE_NOTIFIED);
        if (scope == SCOPE_ASYNC_DEP && !async_cb_wrapper.empty()) {
          // make async scope callback take ownership of this wrapper callback
          def.pre_call.push_back(
              fmt::format("{}->take_data ({})", async_cb_wrapper, cbw_pname));
        }
      }
    } else {
      throw skip("invalid callback scope " + scope);
    }
    // common part
    def.c_call[param_no] =
        fmt::format("{} ? &{}->wrapper : nullptr", cbw_pname, cbw_pname);
    def.c_call[closure] = cbw_pname;

    return defaultable;
  }

  // returns defaultable status
  bool process_param_in_data(int param_no, const Parameter &pinfo,
      const Options &options, FunctionData &def, bool defaultable)
  {
    auto flags = pinfo.tinfo.flags;
    auto &pname = pinfo.name;
    auto &&cpptype = pinfo.tinfo.cppreftype(pinfo.transfer);
    auto &ctype = pinfo.ptype;
    auto &transfer = pinfo.transfer;
    auto &tinfo = pinfo.tinfo;
    const bool callee = kind == EL_CALLBACK || kind == EL_VIRTUAL_METHOD;

    typedef std::vector<std::pair<int, std::string>> callexp_t;
    callexp_t callexps;
    std::string callexp, cpp_decl;

    logger(Log::LOG, "param_in_data {} {}", param_no, pname);
    if (flags & TYPE_VALUE) {
      // value types always passed simply
      // mind g(const)pointer case (const not in normalized girtype)
      cpp_decl = ((flags & TYPE_ENUM) ? cpptype : ctype) + " " + pname;
      callexp = flags & TYPE_ENUM
                    ? fmt::format(GI_NS_SCOPED + "unwrap ({})", pname)
                    : pname;
    } else if (flags & TYPE_CLASS) {
      // otherwise have to mind ownership issues
      if (pinfo.nullable && options.nullable != opt_nullable::PRESENT) {
        // discard nullable
        // NOTE: default value not possible with only forward
        // declaration
        callexp = "nullptr";
      } else {
        // preserve const signature
        // at least, if it makes sense; full transfer on const does not
        // (as we may have to move from it)
        if ((is_const(ctype) && transfer == TRANSFER_NOTHING)) {
          if (flags & TYPE_OBJECT) {
            // side-step ref/unref, in as much as such matters
            cpp_decl = fmt::format("const {} & {}", cpptype, pname);
          } else {
            cpp_decl = fmt::format("const {} {}", cpptype, pname);
          }
        } else {
          cpp_decl = fmt::format("{} {}", cpptype, pname);
        }
        auto wpname = pname;
        // may need to move from a boxed wrapper or string
        bool is_string = (flags & TYPE_BASIC);
        if ((is_string || (flags & TYPE_BOXED)) &&
            pinfo.transfer == TRANSFER_FULL)
          wpname = fmt::format("{}({})", MOVE, pname);
        callexp = fmt::format(GI_NS_SCOPED + "unwrap ({}, {})", wpname,
            get_transfer_parameter(transfer));
      }
    } else if (flags & TYPE_CONTAINER) {
      auto &&cppreftype = tinfo.first.cppreftype(pinfo.transfer);
      auto tmpvar = pname + "_w";
      auto unwrapvar = pname + "_i";
      std::string coltype;
      std::string coleltype = tinfo.first.argtype;
      if (flags & TYPE_MAP) {
        coltype = get_list_type(tinfo);
        coleltype = fmt::format(
            "std::pair<{}, {}>", tinfo.first.argtype, tinfo.second.argtype);
      } else if ((flags & TYPE_LIST) || tinfo.zeroterminated) {
        if (flags & TYPE_LIST)
          coltype = get_list_type(tinfo);
        if (tinfo.zeroterminated)
          coltype = "gi::ZTSpan";
      } else if (tinfo.fixedsize) {
        coltype = fmt::format("gi::FSpan<{}>", tinfo.fixedsize);
      } else {
        // need length parameter
        auto len = tinfo.length;
        auto it = len >= 0 ? paraminfo.find(len) : paraminfo.end();
        if (it == paraminfo.end())
          throw skip("array has invalid length parameter");
        const bool basicvalue = (tinfo.first.flags & TYPE_BASIC) &&
                                (tinfo.first.flags & TYPE_VALUE);
        // annotations are often wrong with low-level buffers
        // so stick to simple interface below in that case
        // (which also works in case of bogus in/out mixup)
        if (!basicvalue) {
          if (callee)
            def.arg_traits[param_no].args = {param_no, len};
          callexps.emplace_back(len, fmt::format("{}._size()", pname));
          coltype = "gi::DSpan";
        } else {
          // this is an input parameter, so add const
          // (as what will be passed is likely const)
          cpp_decl = fmt::format("const {} * {}", cppreftype, pname);
          // plain basic case, so essentially pass-through
          auto &linfo = it->second;
          callexps.emplace_back(param_no, pname);
          def.cpp_decl[len] = linfo.tinfo.cpptype + " " + linfo.name;
          callexps.emplace_back(len, linfo.name);
        }
      }
      if (coltype.size() && coleltype.size()) {
        // for a regular function, enable auto-list creation/conversion
        // which then manages ownership as a temporary during call expression
        // but no such otherwise (neither output neither for callee input)
        auto suffix = callee ? "" : "Parameter";
        auto cpp_list = fmt::format("gi::Collection{}<{}, {}, {}>", suffix,
            coltype, coleltype, get_transfer_parameter(pinfo.transfer, true));
        cpp_decl = fmt::format("{} {}", cpp_list, pname);
        // move needed in case of full transfer with single ownership
        // so let's move anyways in all cases
        unwrapvar = fmt::format("{}({})", MOVE, pname);
        auto s = fmt::format("auto {} = unwrap ({}, {})", tmpvar, unwrapvar,
            get_transfer_parameter(pinfo.transfer));
        def.pre_call.push_back(s);
        callexps.emplace_back(param_no, tmpvar);
      } else {
        // plain case above should have handled
        assert(callexps.size());
      }
    } else {
      throw std::logic_error("invalid flags");
    }
    // transfer to def
    if (callexps.empty()) {
      if (callexp.empty())
        throw std::logic_error("unhandled flags");
      callexps.emplace_back(std::move(param_no), std::move(callexp));
    }
    if (cpp_decl.size()) {
      def.cpp_decl.emplace(std::move(param_no), std::move(cpp_decl));
      // a declaration here has no defaults
      // so no defaults further down
      defaultable = false;
    }
    for (auto &&s : callexps) {
      // use intermediate var for clarity
      if (s.first == param_no) {
        // sigh, our input name may actually be an expression
        // FIXME ?? fall back to global data
        auto varname = paraminfo[param_no].name + "_to_c";
        def.pre_call.emplace_back(
            fmt::format("auto {} = {}", varname, s.second));
        def.c_call[s.first] = varname;
      } else {
        def.c_call[s.first] = s.second;
      }
    }
    return defaultable;
  }

  bool process_param_in(int param_no, const Parameter &pinfo,
      const Options &options, FunctionData &def, bool defaultable)
  {
    auto flags = pinfo.tinfo.flags;

    verify_const_transfer(
        pinfo.tinfo, pinfo.transfer, pinfo.direction, kind == EL_CALLBACK);
    if (pinfo.instance) {
      // check sanity
      assert(flags & TYPE_CLASS);
      // method is const if it can operate on a const C struct
      const_method = const_method || is_const(pinfo.ptype);
      def.c_call[param_no] = "gobj_()";
      // NOTE transfer != NOTHING might be a silly case (e.g. _unref)
      // or a useful one (such as some _merge cases)
      defaultable = false;
    } else if (flags & TYPE_CALLBACK) {
      defaultable =
          process_param_in_callback(param_no, pinfo, options, def, defaultable);
    } else {
      defaultable =
          process_param_in_data(param_no, pinfo, options, def, defaultable);
    }
    return defaultable;
  }

  std::string get_list_type(const GeneratorBase::ArgInfo &info)
  {
    assert(info.flags & (TYPE_LIST | TYPE_MAP));
    // should be in qualified GLib.listsuffix form
    assert(info.girname.find("GLib.") == 0);
    auto c = info.girname;
    c.erase(c.begin() + 1, c.begin() + 5);
    return c;
  }

  std::string make_out_type(const Parameter &pinfo)
  {
    const auto &info = pinfo.tinfo;
    if (!info.flags)
      throw skip(fmt::format("return type {} not supported", info.cpptype));
    else if (info.flags & (TYPE_LIST | TYPE_ARRAY)) {
      std::string listtype;
      if (info.flags & TYPE_LIST) {
        listtype = get_list_type(pinfo.tinfo);
      } else if (info.zeroterminated) {
        listtype = "gi::ZTSpan";
      } else if (info.fixedsize) {
        listtype = fmt::format("gi::FSpan<{}>", info.fixedsize);
      } else {
        listtype = "gi::DSpan";
      }
      return fmt::format("gi::Collection<{}, {}, {}>", listtype,
          info.first.argtype, get_transfer_parameter(pinfo.transfer, true));
    } else if (info.flags & TYPE_MAP) {
      return fmt::format("gi::Collection<{}, std::pair<{}, {}>, {}>",
          get_list_type(pinfo.tinfo), info.first.argtype, info.second.argtype,
          get_transfer_parameter(pinfo.transfer, true));
    } else if ((info.flags & TYPE_BOXED) && pinfo.callerallocates) {
      // these always really have transfer full semantics;
      // the caller has performed allocation and thus has ownership
      return info.cppreftype(TRANSFER_FULL);
    } else {
      auto ret = info.cppreftype(pinfo.transfer);
      return ret;
    }
  }

  void process_param_out_array(int param_no, const Parameter &pinfo,
      const Options &options, FunctionData &def)
  {
    auto pname = pinfo.name;
    auto &tinfo = pinfo.tinfo;
    auto transfer = pinfo.transfer;
    bool inout = pinfo.direction == DIR_INOUT;
    bool as_param = (options.output == opt_output::PARAM) || inout;

    std::string exp_size;
    int length = -1;
    Parameter *plinfo = nullptr;

    if (tinfo.fixedsize) {
      def.cpp_decl[param_no] = fmt::format("{} {}[{}]",
          tinfo.first.cppreftype(pinfo.transfer), pname, tinfo.fixedsize);
      exp_size = std::to_string(tinfo.fixedsize);
    } else {
      // must have this (likely checked already though ...)
      length = tinfo.length;
      if (length < 0)
        throw skip("array misses length info");
      auto it = paraminfo.find(length);
      if (it == paraminfo.end())
        throw skip("array has invalid length parameter");
      plinfo = &it->second;
      auto &linfo = it->second;
      inout |= linfo.direction == DIR_INOUT;
      // in case of length parameter it is never treated as an output
      // since the length is a required input
      // so it is always in the declaration
      def.cpp_decl[param_no] =
          tinfo.first.cppreftype(pinfo.transfer) + " * " + pname;
      // also handle length parameter here
      def.cpp_decl[length] = linfo.tinfo.cpptype + " " + linfo.name;
      exp_size = linfo.name;
    }
    // FIXME ?? no real examples here and semantics not entirely clear
    // (probably a single lvalue container argument could be used here)
    if (inout)
      throw skip("inout array not supported");
    // typically lots of different pointer types around (e.g. gpointer)
    // so establish own types all the way
    auto tname_cpp = pname + "_cpptype";
    auto tname_cpp_def = fmt::format(
        "typedef {} {}", tinfo.first.cppreftype(pinfo.transfer), tname_cpp);
    auto tname_c = pname + "_ctype";
    auto tname_c_def =
        fmt::format("typedef traits::ctype<{}>::type {}", tname_cpp, tname_c);
    auto outvar = (pname.size() ? pname : "_ret") + "_o";
    auto cppouttype = make_out_type(pinfo);
    std::string wrap_data;
    std::string cppoutvar;
    // some remaining restriction in callee case
    auto check_callee_support = [this, &pinfo]() {
      if (kind == EL_CALLBACK || kind == EL_VIRTUAL_METHOD) {
        throw skip(kind + " " + pinfo.direction + " array not supported");
      }
    };
    if (pinfo.callerallocates) {
      if ((tinfo.first.flags & TYPE_BASIC) &&
          (tinfo.first.flags & TYPE_VALUE)) {
        // optimization; avoid intermediate copy below
        // simply pass along input and done
        def.c_call[param_no] = pname;
        if (length >= 0)
          def.c_call[length] = plinfo->name;
        return;
      }
      check_callee_support();
      // pretty much expected
      if (plinfo && plinfo->direction != DIR_IN)
        throw skip("unexpected length parameter");
      // vexing parse parentheses
      def.pre_call.push_back(tname_cpp_def);
      def.pre_call.push_back(tname_c_def);
      def.pre_call.push_back(fmt::format(
          "detail::unique_ptr<{}> {} ((g_malloc_n({}, sizeof({}))))", tname_c,
          outvar, exp_size, tname_c));
      def.c_call[param_no] = fmt::format("{}.get()", outvar);
      if (length >= 0)
        def.c_call[length] = exp_size;
      wrap_data = "outvar.release()";
      // normalize transfer as we allocated above
      if (transfer != TRANSFER_FULL)
        transfer = TRANSFER_CONTAINER;
    } else {
      if (pinfo.direction != DIR_RETURN) {
        // prepare an output parameter var
        def.pre_call.push_back(tname_cpp_def);
        def.pre_call.push_back(tname_c_def);
        def.pre_call.push_back(fmt::format("{} *{}", tname_c, outvar));
        def.c_call[param_no] = fmt::format("({}) &{}", pinfo.ptype, outvar);
      } else {
        check_callee_support();
        // assign return to var
        def.ret_format = fmt::format("auto {} = {{}}", outvar);
      }
      wrap_data = outvar;
      cppoutvar = pname;
      if (length >= 0) {
        auto &linfo = *plinfo;
        // in either case, lvalue container as parameter/return
        def.cpp_decl[param_no] = fmt::format(cppouttype + " & " + cppoutvar);
        // arranged above
        assert(exp_size == linfo.name);
        if (linfo.direction != DIR_IN) {
          // no size parameter in Cpp signature, use local variable
          def.cpp_decl.erase(length);
          def.pre_call.push_back(
              fmt::format("{} {}", linfo.tinfo.cpptype, linfo.name));
          def.c_call[length] = fmt::format("&{}", linfo.name);
        } else {
          // use/keep input size parameter in Cpp signature
          def.c_call[length] = exp_size;
        }
        // add length to callee arg trait (regardless of in/out, etc)
        def.arg_traits[param_no].args = {param_no, length};
        if (linfo.direction != DIR_OUT) {
          // an additional argument is needed in Cpp signature
          // (as the input size is/can not passed as part of array collection)
          def.cpp_decl_extra[length] =
              fmt::format("{} {}", linfo.tinfo.cpptype, linfo.name);
          // if needed, use any non-void type to mark inout case
          // (no longer passthrough, Cpp signature is plain, C signature is ptr)
          if (linfo.direction == DIR_INOUT) {
            auto &ti = def.arg_traits[length];
            ti.args = {length};
            ti.inout = true;
            ti.custom = "bool";
            ti.transfer = linfo.transfer;
          }
          // TODO however, no known cases and untested at present, so skip
          // (if not already skipped above by inout check)
          check_callee_support();
        }
      }
      // in case return; always use container wrapper as return
      if (pinfo.direction == DIR_RETURN || (!as_param && length >= 0)) {
        // so no cpp parameter
        def.cpp_decl.erase(param_no);
        // output container is temp helper var instead
        // make name if return value
        if (cppoutvar.empty())
          cppoutvar = "_temp_ret";
        def.post_call.push_back(fmt::format("{} {}", cppouttype, cppoutvar));
        def.cpp_outputs.push_back({cppouttype, cppoutvar});
      }
    }
    if (cppoutvar.empty()) {
      cppoutvar = pname + "_temp_wrap_";
      def.post_call.push_back(fmt::format("{} {}", cppouttype, cppoutvar));
    }
    def.post_call.push_back(
        fmt::format("{} = gi::wrap_to<{}>({}, {}, {})", cppoutvar, cppouttype,
            wrap_data, exp_size, get_transfer_parameter(transfer)));
  }

  // returns defaultable status
  bool process_param_out(int param_no, const Parameter &pinfo,
      const Options &options, FunctionData &def, bool defaultable)
  {
    auto pname = pinfo.name;
    auto &tinfo = pinfo.tinfo;
    auto &ctype = pinfo.ptype;
    int flags = tinfo.flags;
    auto &transfer = pinfo.transfer;
    bool inout = pinfo.direction == DIR_INOUT;
    const bool callee = kind == EL_CALLBACK || kind == EL_VIRTUAL_METHOD;

    verify_const_transfer(
        tinfo, transfer, pinfo.direction, kind != EL_CALLBACK);
    if (pinfo.instance)
      throw skip("instance out");

    // zero-terminated handled below
    if ((flags & TYPE_ARRAY) && !tinfo.zeroterminated) {
      process_param_out_array(param_no, pinfo, options, def);
      return false;
    }

    if (pinfo.direction == DIR_RETURN) {
      if (tinfo.cpptype == CPP_VOID) {
        def.ret_format = "{}";
      } else {
        auto cpp_ret = make_out_type(pinfo);
        auto tmpvar = "_temp_ret";
        def.ret_format = fmt::format("auto {} = {{}}", tmpvar);
        def.cpp_outputs.push_back({cpp_ret,
            fmt::format(make_wrap_format(tinfo, transfer, cpp_ret), tmpvar)});
      }
      return false;
    }

    // inout never in output tuple
    bool as_param = (options.output == opt_output::PARAM) || inout;

    // one-to-one non-array cases
    // this also handles list/map output with suitable pointer depth
    auto paramtype = make_out_type(pinfo);
    bool optional = false;
    if (as_param) {
      optional = pinfo.optional;
      auto pass = pinfo.optional ? " * " : " & ";
      // no (nullptr) default for pointer output
      // as that leads to call ambiguity with the output tuple variant
      // if no argument given in call
      def.cpp_decl[param_no] = paramtype + pass + pname;
      defaultable = false;
    } else {
      // FIXME ?? could consider defaultable in this case ??
      defaultable = false;
    }

    if ((flags & TYPE_BOXED) && pinfo.callerallocates) {
      if (callee) {
        // parameter can actually be considered an input (pointer/wrapper)
        // (with transfer none, as caller has ownership)
        def.cpp_decl.erase(param_no);
        auto tpinfo = pinfo;
        tpinfo.direction = DIR_IN;
        def.arg_traits[param_no].transfer = tpinfo.transfer = TRANSFER_NOTHING;
        return process_param_in_data(
            param_no, tpinfo, options, def, defaultable);
      }
      // special case; use provided output plain struct directly
      // mimic typical call sequence; declare local struct and pass that
      auto cvar = pname + "_c";
      // such call is typically used for things that do not need cleanup
      // (e.g. GstMapInfo, etc), though GValue is a popular and prominent
      // exception
      // in any case, this means the struct is not opaque
      // so we have no known way to allocate or free
      // (well, we could free a boxed GType but not allocate)
      // so we will really leave it up to the caller
      // and only allocate in case of a GValue or a "plain struct" case
      // (if non-optional)
      // (which is already tricky if custom setup/free needed after all)
      std::string deref;
      if (!as_param) {
        // make pname point to a local variable
        def.pre_call.push_back(fmt::format("{} {}", paramtype, cvar));
        def.pre_call.push_back(fmt::format("auto {} = &{}", pname, cvar));
        // which is then returned
        auto output = cvar;
        def.cpp_outputs.push_back({paramtype, output});
        deref = "*";
      } else if (pinfo.optional) {
        deref = "*";
      }
      if (!pinfo.optional) {
        // this should only really do something for GValue or c-boxed
        // no deref check needed here, as it is ok in either case
        def.pre_call.push_back(
            fmt::format("detail::allocate({}{})", deref, pname));
      }
      def.pre_call.push_back(fmt::format(
          "static_assert(sizeof({}) == sizeof(*({}{}).gobj_()), \"\")",
          tinfo.dtype, deref, pname));
      auto cvalue =
          fmt::format("({}*) ({}{}).gobj_()", tinfo.dtype, deref, pname);
      def.c_call[param_no] =
          deref.size() ? fmt::format("{} ? {} : nullptr", pname, cvalue)
                       : cvalue;
      // no additional post-call
      // (already done above in case of return value)
      return defaultable;
    }

    // otherwise use a temp variable and then wrap that one
    auto outvar = pname + "_o";
    // adjust arginfo for wrapping
    auto opinfo = pinfo;
    auto &otinfo = opinfo.tinfo;
    if (ctype[ctype.size() - 1] != GI_PTR)
      throw skip(pname + "; inconsistent pointer type");
    // adjust type
    opinfo.ptype = ctype.substr(0, ctype.size() - 1);

    // optionally init the temp variable using input (if such)
    std::string init(" {}");
    if (inout && as_param) {
      // operate on a temporary definition
      // only need to re-use transformation of input to call expression
      auto tdef = def;
      tdef.pre_call.clear();
      // optionally tweak the input expression (type adjusted above)
      if (optional)
        opinfo.name.insert(0, "*");
      process_param_in_data(param_no, opinfo, options, tdef, defaultable);
      // be nice, include requested code
      for (auto &&p : tdef.pre_call)
        def.pre_call.emplace_back(p);
      // rest we handle here
      init = fmt::format(" = {}", tdef.c_call[param_no]);
    }

    // always same precall, but slight variation for pointer/ref
    def.pre_call.push_back(opinfo.ptype + " " + outvar + init);
    auto call = std::string("&") + outvar;
    def.c_call[param_no] =
        optional ? fmt::format("{} ? {} : nullptr", pname, call) : call;
    auto wrapf =
        fmt::format(make_wrap_format(otinfo, transfer, paramtype), outvar);
    // assign post call or return
    if (as_param) {
      auto guard = optional ? fmt::format("if ({}) ", pname) : EMPTY;
      auto deref = optional ? "*" : "";
      def.post_call.push_back(
          fmt::format("{}{}{} = {}", guard, deref, pname, wrapf));
    } else {
      def.cpp_outputs.push_back({paramtype, wrapf});
    }
    return defaultable;
  }

  // process info to construct function
  // returns defaultable status
  bool process_param(int param_no, const Parameter &pinfo,
      const Options &options, FunctionData &def, bool defaultable)
  {
    auto &tinfo = pinfo.tinfo;
    auto ctype = tinfo.ctype;
    int flags = tinfo.flags;

    // check type first, so we do not raise any complaints on unknown type
    if (!flags)
      throw skip(
          fmt::format("{} type {} not supported", pinfo.name, tinfo.cpptype),
          skip::OK);
    // sanity check on pointer depth to verify annotation
    // array annotation or out parameter is frequently missing
    auto argdepth = std::count(pinfo.ptype.begin(), pinfo.ptype.end(), GI_PTR);
    auto rpdepth = get_pointer_depth(pinfo.tinfo.ctype);
    if ((kind != EL_SIGNAL) && (argdepth != rpdepth)) {
      // this might happen for an input array of C-boxed structs
      // (rather than the expected array of pointers to something-boxed)
      if ((pinfo.tinfo.flags & TYPE_CONTAINER) &&
          (pinfo.tinfo.first.flags & TYPE_BOXED))
        throw skip(fmt::format("{} {} boxed array not supported (depth {})",
            pinfo.name, pinfo.direction, rpdepth));
      throw skip(fmt::format("inconsistent {} {} pointer depth ({} vs {})",
          pinfo.name, pinfo.direction, rpdepth, argdepth));
    }

    // perhaps this param is part of another's one (e.g. callback)
    // processing various checks to bail out early
    if (def.c_call.find(param_no) != def.c_call.end()) {
      logger(Log::LOG, "call fragment for parameter {} already specified",
          param_no);
      return false;
    } else if (((kind != EL_CALLBACK && kind != EL_SIGNAL) ||
                   (pinfo.closure < 0)) &&
               referenced.count(param_no)) {
      /* managing parameter might come later, so skip this one for now
       * if a call has to be emitted
       * (also; user_data closure in callback can reference itself,
       * and that has to be discovered below
       * userdata parameter also has to be processed below in callback case
       * (to insert proper callexp)
       */
      logger(Log::LOG, "parameter {} referenced elsewhere", param_no);
      return false;
    }
    // on with it now
    // standard transfer, can be overridden
    if (!pinfo.instance)
      def.arg_traits[param_no] = {
          pinfo.transfer, pinfo.direction == DIR_INOUT, {param_no}};
    if (pinfo.direction != DIR_IN) {
      // (in)out or return
      defaultable =
          process_param_out(param_no, pinfo, options, def, defaultable);
    } else {
      defaultable =
          process_param_in(param_no, pinfo, options, def, defaultable);
      // handle callback user_data
      if (kind == EL_CALLBACK) {
        if (pinfo.closure >= 0) {
          if (pinfo.closure != param_no)
            throw skip("invalid closure user_data");
          if (ctype != "gpointer")
            throw skip("invalid type user_data");
          if (found_user_data >= 0)
            throw skip("duplicate user_data");
          found_user_data = param_no;
          // user_data not included in signature (nor transfer)
          def.cpp_decl.erase(param_no);
          def.arg_traits.erase(param_no);
        }
      }
    }

    return defaultable;
  }

  std::string join_outputs(
      const std::vector<FunctionDefinition::Output> &outputs,
      std::string FunctionDefinition::Output::*m,
      std::string (*transform)(std::string) = nullptr)
  {
    std::vector<std::string> temp;
    for (auto &&o : outputs)
      temp.emplace_back(transform ? transform(o.*m) : o.*m);
    return boost::algorithm::join(temp, ", ");
  }

  void make_function(const Options &options, const FunctionDataExtended &def,
      const std::string &fname)
  {
    auto &name = func.name;
    // determine return type
    std::string cpp_ret(CPP_VOID);
    if (def.cpp_outputs.size() == 1) {
      cpp_ret = def.cpp_outputs[0].type;
    } else if (def.cpp_outputs.size() > 1) {
      cpp_ret = fmt::format(
          "std::tuple<{}>", join_outputs(def.cpp_outputs, &Output::type));
    }
    if (options.except == opt_except::EXPECTED) {
      cpp_ret = fmt::format("gi::result<{}>", cpp_ret);
    }
    const auto &cpp_decl = def.cpp_decl_unfolded;
    // generate based on type
    if (kind == EL_SIGNAL) {
      auto decl_name = name;
      // always include instance in signature
      auto cpp_decls = cpp_decl;
      cpp_decls.insert(cpp_decls.begin(), qualify(klasstype, TYPE_OBJECT));
      // the function type specified as gi::signal_proxy template parameter
      // serves 2 purposes;
      // + provide a compile-time type check
      // + select each parameter's corresponding GValue GType
      //   (according to the association defined in gi/value.hpp)
      // so we should make sure to pick the proper type, especially for the
      // latter item, as e.g. gint64 may simply be a long
      // (which would not map to a G_TYPE_INT64)
      auto normalize = [](std::string &subject, const std::string &in,
                           const std::string &sub) {
        if (subject.find(in) == 0 &&
            (subject.size() == in.size() || subject[in.size()] == ' ')) {
          subject.replace(subject.begin(), subject.begin() + in.size(), sub);
        }
      };
      for (auto &decl : cpp_decls) {
        normalize(decl, "gint64", "long long");
        normalize(decl, "guint64", "unsigned long long");
      }
      // convert signal name to valid identifier
      std::replace(decl_name.begin(), decl_name.end(), '-', '_');
      auto ret = fmt::format("gi::signal_proxy<{}({})>", cpp_ret,
          boost::algorithm::join(cpp_decls, ", "));
      oss_decl << fmt::format(
                      "{0} signal_{1}()\n{{ return {0} (*this, \"{2}\"); }}",
                      ret, decl_name, name)
               << std::endl;
    } else {
      // internal namespace for callforward helper parts to avoid clutter
      NamespaceGuard nsg_decl(oss_decl);
      NamespaceGuard nsg_impl(oss_impl);
      if (kind == EL_CALLBACK) {
        nsg_decl.push(GI_NS_INTERNAL, false);
        nsg_impl.push(GI_NS_INTERNAL, false);
      }
      const char *CB_SUFFIX = "_CF";
      auto make_sig = [&](bool impl) {
        bool vm = kind == EL_VIRTUAL_METHOD;
        auto funcsuffix = kind == EL_CALLBACK ? CB_SUFFIX : "";
        auto prefix =
            impl
                ? EMPTY
                : (vm ? "virtual "
                      : ((kind != EL_METHOD) && klass.size() ? "static " : ""));
        auto klprefix = klass.size() && impl ? klass + "::" : "";
        auto pure = (vm && !impl ? " = 0" : "");
        if (!impl)
          prefix += GI_INLINE + ' ';
        // virtual method might throw in addition to error parameter
        auto sig =
            prefix +
            fmt::format("{} {}{}{} ({}){}{}{}", cpp_ret, klprefix,
                unreserve(fname, vm), funcsuffix,
                boost::algorithm::join(cpp_decl, ", "),
                (const_method ? " const" : ""),
                (((options.except == opt_except::THROW) || (vm && func.throws))
                        ? ""
                        : " noexcept"),
                pure);
        // remove default values in definition
        static const std::regex re_defaultv(" =[^,)]*", std::regex::optimize);
        return impl ? std::regex_replace(sig, re_defaultv, "") : sig;
      };
      if (!def.cf_ctype.empty())
        oss_decl << def.cf_ctype << ';' << std::endl;
      oss_decl << make_sig(false) << ";" << std::endl;
      oss_impl << make_sig(true) << std::endl;
      // transform to list for joining
      std::vector<std::string> temp;
      for (auto &&e : def.c_call)
        temp.emplace_back(e.second);
      auto call = fmt::format(
          "{} ({})", def.c_callee, boost::algorithm::join(temp, ", "));
      call = fmt::format(def.ret_format, call);
      std::vector<std::string> pre_return;
      std::string returns;
      if (def.cpp_outputs.size() == 1) {
        // avoid -Wpessimizing-move on std::move(x)
        // so pass along as-is
        auto &ret = def.cpp_outputs[0].value;
        returns = fmt::format("return {};", ret);
      } else if (def.cpp_outputs.size() > 1) {
        // wrap in a move()
        // may be needed for move-only types in temp variables
        // however, again avoid -Wpessimizing-move
        // (in case of moving from an expression/temporary)
        // so assign to an intermediate ref and move from that
        // (and should not hurt in other cases)
        // auto moved_outputs = def.cpp_outputs;
        static const std::string tmp_prefix = "tmp_return_";
        int count = 0;
        std::vector<std::string> outputs;
        for (auto &e : def.cpp_outputs) {
          pre_return.push_back(
              fmt::format("auto &&{}{} = {}", tmp_prefix, ++count, e.value));
          outputs.push_back(fmt::format("{}({}{})", MOVE, tmp_prefix, count));
        }
        returns = fmt::format("return std::make_tuple ({});",
            boost::algorithm::join(outputs, ","));
      }
      if (options.except == opt_except::EXPECTED && returns.empty()) {
        // expected<void> is no longer void, so needs explicit return
        returns = "return {};";
      }
      oss_impl << "{" << std::endl;
      // prevent calling NULL in case of vmethod
      // (i.e. Subclass::method != Superclass::method)
      if (kind == EL_VIRTUAL_METHOD) {
        // FIXME in the new approach the class/interface struct entry
        // is only filled if really needed, so it preserves the superclass
        // (unless needed) whether that is NULL or otherwise.
        // In particular, this fallback implementation should not get called
        // (which we could enforce if the new approach becomes the only one).
        // It might still get explicitly called, but then it should first be
        // checked whether the struct entry is non-NULL (as typically done
        // by C code).  So, even in that case, we should not hit the situation
        // below that is forced to return some default value
        // (and that could also be enforced in future).
        // So, eventually, the nasty default return should not apply at runtime.
        // Until then, hoping for the best
        // (though *mm does no better here) ...
        // Log a fairly serious warning, as such should such no longer happen
        // (in the auto-detected or manual specification approach).
        auto retexp = fmt::format(
            "{{ g_critical (\"no method in class struct\"); return {}; }}",
            cpp_ret == CPP_VOID ? EMPTY : "{}");
        retexp = fmt::format("if (!{}) {}", func.functionexp, retexp);
        oss_impl << indent << retexp << std::endl;
      }
      for (const auto &p : def.pre_call)
        oss_impl << indent << p << ';' << std::endl;
      oss_impl << indent << call << ';' << std::endl;
      for (const auto &p : def.post_call)
        oss_impl << indent << p << ';' << std::endl;
      for (const auto &p : pre_return)
        oss_impl << indent << p << ';' << std::endl;
      if (returns.size())
        oss_impl << indent << returns << std::endl;
      oss_impl << "}" << std::endl;
      // generate helper trait type when used as argument in another callback
      if (kind == EL_CALLBACK) {
        auto base = unreserve(fname);
        oss_decl << fmt::format("GI_CB_ARG_CALLBACK_CUSTOM({}, {}, {});",
                        base + GI_SUFFIX_CB_TRAIT, base + GI_SUFFIX_CF_CTYPE,
                        base + CB_SUFFIX)
                 << std::endl;
      }
    }
    if (kind == EL_CALLBACK) { // callback
      // NOTE limited container support at present
      // argument trait/transfers; return type to start with
      auto transfers = make_arg_traits(def.arg_traits, def.c_sig);
      // discard expected trailing callforward parameters in regular declaration
      // (return value also specified in transfers)
      assert(def.arg_traits.size() + (2 - 1) == cpp_decl.size());
      auto cpp_decls = boost::make_iterator_range(
          cpp_decl.begin(), cpp_decl.begin() + def.arg_traits.size() - 1);
      oss_decl << fmt::format("typedef {}callback<{}({}), {}> {}",
                      GI_NS_DETAIL_SCOPED, cpp_ret,
                      boost::algorithm::join(cpp_decls, ", "), transfers,
                      unreserve(name))
               << ";" << std::endl;
    }
  }

  FunctionDefinition process()
  {
    auto &name = func.name;

    const bool callee = kind == EL_CALLBACK || kind == EL_VIRTUAL_METHOD;
    // pass over parameters to collect info to assemble signature
    struct signature
    {
      std::string c_ret;
      std::vector<std::string> c_decl;
    };
    signature sig_ctype, sig_ptype;
    // as above, but now each entry contains a comment annotation
    signature annotations;
    std::map<int, int> array_sizes;

    // collect flags into a comment to append to parameter name in declaration
    auto annotate_parameter = [](const FunctionParameter &p) -> std::string {
      if ((p.tinfo.flags & TYPE_VALUE) || p.tinfo.cpptype == CPP_VOID)
        return {};
      auto dir = (p.direction.find(DIR_OUT) != p.direction.npos)
                     ? fmt::format(",{}", p.direction)
                     : "";
      return fmt::format(" /*{}{}{}{}{}*/", p.transfer, dir,
          p.optional ? ",opt" : "", p.nullable ? ",nullable" : "",
          p.callerallocates ? ",ca" : "");
    };

    int callbacks = 0;
    Parameter *callback = nullptr;
    for (auto &p : paraminfo) {
      try {
        auto &pinfo = p.second;
        assert(pinfo.name.size() || pinfo.direction == DIR_RETURN);
        assert((pinfo.direction == DIR_RETURN) == (p.first == INDEX_RETURN));
        assert(pinfo.transfer.size());
        assert(pinfo.direction.size());
        auto annotation = annotate_parameter(pinfo);
        // c signature
        if (p.first == INDEX_RETURN) {
          sig_ctype.c_ret = pinfo.tinfo.ctype;
          annotations.c_ret = annotation;
        } else {
          sig_ctype.c_decl.push_back(pinfo.tinfo.ctype + " " + pinfo.name);
          annotations.c_decl.push_back(annotation);
        }
        auto flags = pinfo.tinfo.flags;
        // also sanity checks
        if (pinfo.direction == DIR_RETURN) {
          // NOTE no more check on none return needed
          // as return type adequately specifies this situation (e.g. cstring_v)
          // and usual caution wrt return "temporary string" then apply
          // (and floating return is handled by callback wrapping code)
          // NOTE constructor return transfer is often marked none = floating
          // so that needs a ref_sink (which wrap() will arrange)
        } else {
          // overall checks
          bool function_type = false;
          if (callee || kind == EL_SIGNAL) {
            function_type = true;
            // no defaults in signatures
            pinfo.nullable = false;
            pinfo.optional = false;
          }
          if (kind == EL_SIGNAL) {
            // a signal is handled much like a callback,
            // so more complex cases need argument trait info (like callback)
            // but that needs more changes in gi support code
            if (pinfo.direction != DIR_IN && !(flags & TYPE_BASIC))
              throw skip(
                  kind + ' ' + pinfo.direction + " parameter not supported");
            if ((flags & TYPE_ARRAY) && !pinfo.tinfo.zeroterminated)
              throw skip(kind + ' ' + pinfo.direction +
                         " array parameter not supported");
          }
          // trigger additional applicable overload generation
          if (!function_type) {
            if (pinfo.direction == DIR_OUT)
              do_output.insert(opt_output::ALT);
            if ((flags & TYPE_CLASS) && pinfo.nullable &&
                pinfo.direction == DIR_IN)
              do_nullable.insert(opt_nullable::ALT);
          }
        }
        // no known cases, but check anyway
        if ((pinfo.direction != DIR_IN) && (flags & TYPE_CALLBACK))
          throw skip(kind + " " + pinfo.direction +
                     " callback parameter not supported");
        track_dependency(deps, pinfo.tinfo);
        // in case of a callback type definition,
        // closure on userdata refers to itself (to identify callback userdata)
        // otherwise, closure attribute should only be on callback argument,
        // but is sometimes also on user_data referring back to callback
        // remove the latter circular reference
        if (!(flags & TYPE_CALLBACK) && pinfo.closure != p.first)
          pinfo.closure = INDEX_DEFAULT;
        // collect parameters that are referenced from elsewhere
        // and as such managed elsewhere
        for (auto p : {&pinfo.closure, &pinfo.destroy, &pinfo.tinfo.length})
          if (*p >= 0)
            referenced.insert(*p);
        array_sizes[pinfo.tinfo.length] = p.first;
        // check if (single) callback parameter
        if (flags & TYPE_CALLBACK) {
          ++callbacks;
          if (pinfo.closure < 0)
            callback = &pinfo;
        }
      } catch (skip &ex) {
        handle_skip(ex);
      }
    }

    // userdata parameter may not be properly annotated
    // (especially for functions considered not introspectable)
    // so, in case of only 1 callback parameter, try to guess userdata
    // (see also issue #85;
    // only limited cases so far, e.g. g_bytes_new_with_free_func)
    // as there is no way to guess the annotated scope,
    // we either have to hope that at least that one is properly annotated,
    // or alternatively let's only guess in specific circumstances
    if (callbacks == 1 && callback &&
        callback->tinfo.girname == GIR_GDESTROYNOTIFY &&
        callback->scope == SCOPE_ASYNC) {
      auto &e = *paraminfo.rbegin();
      // check last parameter
      auto &pinfo = e.second;
      if ((pinfo.name == "data" || ends_with(pinfo.name, "_data")) &&
          (pinfo.tinfo.girname == "gpointer")) {
        // assign as userdata to single callback
        callback->closure = e.first;
        // callback->
        referenced.insert(e.first);
        logger(Log::DEBUG, "{}; guessed {} userdata parameter {}", func.c_id,
            callback->name, pinfo.name);
      }
    }

    // track if func has non-void return
    bool has_return = false;
    // another pass to determine expected normalized parameter type
    // unfortunately some array length size parameters share annotation
    // with the array, so a caller-allocates out array lead to wrong ptr
    // depth (if not compensated for that)
    for (auto &p : paraminfo) {
      try {
        auto &pinfo = p.second;
        // special case; out array length is not so consistent
        // always marked as out (even when passed no-ptr)
        // even when out, then caller-allocates is reversed from a
        // regular out int
        if (pinfo.direction == DIR_OUT) {
          auto it = array_sizes.find(p.first);
          if (it != array_sizes.end()) {
            // normalize based on declaration
            pinfo.direction =
                get_pointer_depth(pinfo.tinfo.ctype) > 0 ? DIR_OUT : DIR_IN;
            // make sure we end up with ptr
            if (pinfo.direction == DIR_OUT)
              pinfo.callerallocates = false;
          }
        }
        pinfo.ptype =
            make_ctype(pinfo.tinfo, pinfo.direction, pinfo.callerallocates);
        // collect deduced signature
        if (p.first == INDEX_RETURN) {
          has_return = pinfo.tinfo.cpptype != CPP_VOID;
          sig_ptype.c_ret = pinfo.ptype;
        } else {
          sig_ptype.c_decl.push_back(pinfo.ptype + " " + pinfo.name);
        }
      } catch (skip &ex) {
        handle_skip(ex);
      }
    }

    // could affect declaration signature
    if (func.throws) {
      // fixed signature
      sig_ctype.c_decl.push_back("GError ** error");
      sig_ptype.c_decl.push_back("GError ** error");
      // stay in sync with parameters, even if empty
      annotations.c_decl.push_back({});
      do_except = {opt_except::GERROR};
      if (!callee)
        do_except.insert(
            ctx.options.expected ? opt_except::EXPECTED : opt_except::THROW);
    } else if (ctx.options.dl && func.lib_symbol) {
      do_except = {
          ctx.options.expected ? opt_except::EXPECTED : opt_except::THROW};
    }

    // we collected enough info so far to reconstruct original declaration
    auto make_declaration = [&](bool def, const std::string name,
                                const signature &sig,
                                const signature *append = nullptr) {
      auto c_sig_fmt = !def ? "{} {} ({})" : "typedef {} (*{}) ({})";
      const signature *actual = &sig;
      signature combined;
      if (append && sig.c_decl.size() == append->c_decl.size()) {
        combined.c_ret = sig.c_ret + append->c_ret;
        for (std::size_t i = 0; i < sig.c_decl.size(); ++i)
          combined.c_decl.push_back(sig.c_decl[i] + append->c_decl[i]);
        actual = &combined;
      }
      return fmt::format(c_sig_fmt, actual->c_ret, name,
          boost::algorithm::join(actual->c_decl, ", "));
    };

    { // dump original/derived declarations
      bool is_signal = kind == EL_SIGNAL;
      const char *prefix = is_signal ? "(signal) " : "";
      // dump both parsed and derived
      for (auto &sig : {sig_ctype, sig_ptype}) {
        auto c_sig =
            make_declaration(kind == EL_CALLBACK, func.c_id, sig, &annotations);
        oss_decl << "// " << prefix << c_sig << ";" << std::endl;
        if (!is_signal)
          oss_impl << "// " << c_sig << ";" << std::endl;
      }
    }

    // name for error return parameter
    const static std::string ERROR_PARAM = "_error";

    auto make_definition = [&](Options options, bool fallback = false) {
      logger(Log::LOG, "generating {} with except {}, output {}, nullable {}",
          func.c_id, (int)options.except, (int)options.output,
          (int)options.nullable);
      FunctionDefinition def;
      def.c_callee = func.functionexp;
      if (callee) {
        auto c_sig = sig_ptype;
        // instance parameter should not be included
        if (kind == EL_VIRTUAL_METHOD) {
          assert(!c_sig.c_decl.empty());
          c_sig.c_decl.erase(c_sig.c_decl.begin());
        }
        def.c_sig = make_declaration(false, "", c_sig, nullptr);
      }

      if (fallback) {
        sig_ctype = signature{};
        logger(Log::INFO, "method {} creating fallback", name);
        // generate virtual method with original C signature as-is
        // however, do qualify/scope type names to avoid mixup with ns etc
        auto ctype = [](const FunctionParameter &pinfo) {
          if (pinfo.ptype.find(GI_SCOPE) == 0)
            return GI_SCOPE + pinfo.tinfo.ctype;
          return pinfo.tinfo.ctype;
        };
        for (const auto &p : paraminfo) {
          auto &pinfo = p.second;
          track_dependency(deps, pinfo.tinfo);
          if (pinfo.instance) {
            sig_ctype.c_decl.push_back(ctype(pinfo));
            def.c_call[p.first] = "gobj_()";
          } else if (pinfo.direction == DIR_RETURN) {
            sig_ctype.c_ret = ctype(pinfo);
            if (pinfo.tinfo.cpptype == CPP_VOID) {
              def.ret_format = "{}";
            } else {
              // assign return to var
              auto outvar = "result_";
              def.ret_format = fmt::format("auto {} = {{}}", outvar);
              def.cpp_outputs.push_back({ctype(pinfo), outvar});
            }
          } else {
            sig_ctype.c_decl.push_back(ctype(pinfo));
            def.cpp_decl[p.first] = ctype(pinfo) + " " + pinfo.name;
            def.c_call[p.first] = pinfo.name;
          }
        }
        // also consider optional GError
        if (func.throws) {
          const int LAST = INDEX_ERROR;
          const std::string evar = "error_";
          auto &p = def.cpp_decl[LAST] = "::GError **" + evar;
          def.c_call[LAST] = evar;
          sig_ctype.c_decl.push_back(p);
        }
        // commit/specify that a method def was made
        def.name = name;
      }

      // arrange for dynamic load if so requested
      std::string symbol_name;
      if (ctx.options.dl && func.lib_symbol) {
        symbol_name = "_symbol_name";
        def.pre_call.push_back(
            fmt::format("const char *{} = \"{}\"", symbol_name, def.c_callee));
        def.c_callee = fmt::format(
            "detail::load_symbol(internal::_libs(), {})", symbol_name);
      }

      if (kind != EL_CALLBACK) {
        // enforce deduced function signature by cast
        // i.e. type cast to deduced function type
        static const std::string call_wrap_t = "call_wrap_t";
        static const std::string call_wrap_v = "call_wrap_v";
        def.pre_call.push_back(make_declaration(
            true, call_wrap_t, fallback ? sig_ctype : sig_ptype));
        def.pre_call.push_back(fmt::format("{} {} = ({}) {}", call_wrap_t,
            call_wrap_v, call_wrap_t, def.c_callee));
        def.c_callee = call_wrap_v;
        if (symbol_name.size()) {
          std::string check_exp;
          if (options.except == opt_except::EXPECTED) {
            check_exp =
                fmt::format("if (!{}) "
                            "return gi::detail::make_unexpected(gi::detail::"
                            "missing_symbol_error({}))",
                    call_wrap_v, symbol_name);
          } else if (options.except == opt_except::THROW) {
            check_exp = fmt::format(
                "if (!{}) "
                "gi::detail::try_throw(gi::detail::missing_symbol_error({}))",
                call_wrap_v, symbol_name);
          } else if (options.except == opt_except::GERROR) {
            // this could potentially cover up quite some error
            // (in case of null error parameter, but so be it ...)
            check_exp = fmt::format(
                "if (!{0}) {{"
                "if ({2}) *{2} = gi::detail::missing_symbol_error({1}); "
                "return {3}; "
                "}}",
                call_wrap_v, symbol_name, ERROR_PARAM, has_return ? "{}" : "");
          } else {
            // no other options should be possible
            // (due to options arranged for above)
            assert(false);
          }
          def.pre_call.push_back(check_exp);
        }
      }

      if (fallback)
        return def;

      // process parameters from last to first
      // that way we can track whether it is possible to specify a default
      // which is only acceptable if so for all later parameters
      // (or later ones are dropped from signature)
      // init defaultable status to start
      // trailing GError prevents default
      bool defaultable = options.except != opt_except::GERROR;

      // in case of callback;
      // check if some parameter is specified as closure (aka userdata)
      // as it may sadly be missing
      bool has_closure = false;
      if (kind == EL_CALLBACK) {
        for (auto it = paraminfo.rbegin(); it != paraminfo.rend(); ++it) {
          auto &&e = *it;
          if (e.second.closure >= 0) {
            has_closure = true;
            break;
          }
        }
      }

      // if userdata/closure not specified for a closure,
      // accept last parameter as userdata based on name
      if (kind == EL_CALLBACK && !has_closure && paraminfo.size()) {
        auto &e = *paraminfo.rbegin();
        if (e.second.name == "data" || ends_with(e.second.name, "_data")) {
          e.second.closure = e.first;
          logger(Log::DEBUG, "{}; guessed userdata parameter {}", func.c_id,
              e.second.name);
        }
      }

      for (auto it = paraminfo.rbegin(); it != paraminfo.rend(); ++it) {
        auto &&e = *it;
        try {
          defaultable =
              process_param(e.first, e.second, options, def, defaultable);
        } catch (const skip &ex) {
          handle_skip(ex);
        }
      }

      // reverse some results
      std::reverse(def.cpp_outputs.begin(), def.cpp_outputs.end());

      // add potentially missing transfers from length parameters
      // that were not added previously
      // FIXME reorganize data to avoid such
      if (errors.empty() &&
          (kind == EL_CALLBACK || kind == EL_VIRTUAL_METHOD)) {
        for (auto &&p : paraminfo) {
          auto index = p.first;
          if (def.cpp_decl.count(index) && !def.arg_traits.count(index)) {
            if (array_sizes.count(index)) {
              def.arg_traits[p.first].transfer = p.second.transfer;
              def.arg_traits[p.first].args = {p.first};
            } else {
              handle_skip(skip("missing callback transfer info"));
            }
          }
        }
      }

      // enforce deduced function signature by cast
      for (auto &&p : def.c_call) {
        auto it = paraminfo.find(p.first);
        if (it != paraminfo.end()) {
          auto &pinfo = it->second;
          p.second = fmt::format("({}) ({})", pinfo.ptype, p.second);
        }
      }

      // callback/callforward; add additional call/userdata parameters
      if (kind == EL_CALLBACK && found_user_data >= 0 &&
          paraminfo.count(found_user_data)) {
        // need a type for the C function to call
        auto cfc = unreserve(func.name) + GI_SUFFIX_CF_CTYPE;
        def.cf_ctype =
            make_declaration(true, cfc, fallback ? sig_ctype : sig_ptype);
        // which is specified in an extra paramater
        static const std::string param_func = "_call";
        def.cpp_decl[INDEX_CF] = fmt::format("{} {}", cfc, param_func);
        // userdata also given in parameter; re-use original parameter name
        // (which should have been used in regular in parameter processing)
        def.cpp_decl[INDEX_CF + 1] =
            fmt::format("gpointer {}", paraminfo.at(found_user_data).name);
        // call supplied function
        def.c_callee = param_func;
        // def.c_call userdata part should have been arranged as usual
      }

      // if marked throws, GError argument is not mentioned in argument list
      // deal with it here
      switch (options.except) {
        case opt_except::THROW:
        case opt_except::EXPECTED:
          // could be here due to dl only
          if (!func.throws)
            break;
          def.pre_call.push_back("GError *error = NULL");
          def.c_call[def.c_call.size() + 10] = "&error";
          if (options.except == opt_except::THROW) {
            def.post_call.push_back("gi::check_error (error)");
          } else {
            // terminating ; added later
            def.post_call.push_back(
                "if (error) return gi::detail::make_unexpected (error)");
          }
          break;
        case opt_except::GERROR: {
          // simulate optional output error parameter
          options.output = opt_output::PARAM;
          Parameter err;
          err.optional = true;
          err.direction = DIR_OUT;
          err.transfer = TRANSFER_FULL;
          err.name = ERROR_PARAM;
          parse_typeinfo("GLib.Error", err.tinfo);
          err.ptype = err.tinfo.ctype = "GError**";
          // process non-nullable to make sure there is no signature
          // ambiguity
          try {
            process_param_out(INDEX_ERROR, err, options, def, false);
            auto &ti = def.arg_traits[INDEX_ERROR];
            ti.transfer = err.transfer;
            auto lparam =
                paraminfo.empty() ? 0 : paraminfo.crbegin()->first + 1;
            ti.args = {lparam};
          } catch (const skip &ex) {
            handle_skip(ex);
          }
        }
        default:
          break;
      }
      return def;
    };

    // mild check on overload conflict
    // TODO improve check ??
    // what about defaultable arguments and overlap that causes
    std::set<std::vector<std::string>> signatures;

    // pass over to produce cpp declarations and content
    // generate each option
    FunctionDataExtended def;
    if (!check_errors()) {
      for (auto &&output : do_output) {
        for (auto &&except : do_except) {
          for (auto &&nullable : do_nullable) {
            // context for this option run
            Options options(except, output, nullable);
            def = make_definition(options);

            // only care about callbacks with (trailing) user_data
            auto last = paraminfo.end();
            --last;
            if (kind == EL_CALLBACK && found_user_data != last->first)
              errors.push_back("not a callback since no user_data");

            if (check_errors())
              goto exit;

            // duplicate could happen for a special boxed output
            // which is never returned in a tuple
            // normalize without const
            // conflict might otherwise occur e.g. in case of
            // (string output, string input nullable)

            auto cpp_sig = def.cpp_decl_unfolded;
            static const std::regex re_const("const ", std::regex::optimize);
            for (auto &d : cpp_sig)
              d = std::regex_replace(d, re_const, EMPTY);
            if (signatures.count(cpp_sig)) {
              logger(Log::DEBUG,
                  "discarding duplicate signature for " + func.c_id);
            } else {
              auto fname =
                  !callee && !func.shadows.empty() ? func.shadows : name;
              make_function(options, def, fname);
              // mark ok
              def.name = name;
              signatures.insert(cpp_sig);
            }
          }
        }
      }
    }

  exit:
    // generate a raw fallback virtual method upon failure
    if (ctx.options.classfull && def.name.empty() &&
        kind == EL_VIRTUAL_METHOD) {
      Options options(
          opt_except::NOEXCEPT, opt_output::PARAM, opt_nullable::PRESENT);
      def = make_definition(options, true);
      if (def.name.size())
        make_function(options, def, name);
    }

    return def;
  }

  FunctionDefinition process(const pt::ptree::value_type *entry,
      const std::vector<Parameter> *params, std::ostream &out,
      std::ostream &impl)
  {
    assert(!params || !entry);

    logger(Log::LOG, "processing " + func.c_id);
    try {
      // check if we made it all the way
      // remove callback from known types if failure
      bool success = kind == EL_CALLBACK ? false : true;
      ScopeGuard g([&] {
        if (!success)
          ctx.repo.discard(func.name);
      });

      if (entry) {
        collect_node(*entry);
      } else {
        int pno = 0;
        for (auto &p : *params) {
          int index = pno;
          if (p.direction == DIR_RETURN) {
            index = INDEX_RETURN;
          } else if (p.instance) {
            index = INDEX_INSTANCE;
          } else {
            ++pno;
          }
          paraminfo[index] = p;
        }
      }

      auto def = process();
      // only trigger remove on callback as such has a top-level GIR entry
      // whereas e.g. a method does not
      // (and its name might conflict/match a top-level one)
      success = success || (def.name.size() > 0);

      out << oss_decl.str() << std::endl;
      impl << oss_impl.str() << std::endl;
      return def;
    } catch (std::runtime_error &ex) {
      auto err = fmt::format("// FAILURE on {}; {}", func.c_id, ex.what());
      out << err << std::endl;
      impl << err << std::endl;
    }

    return FunctionDefinition();
  }
};

} // namespace

std::string
make_arg_traits(const std::map<int, FunctionDefinition::ArgTrait> &traits,
    const std::string &c_sig)
{
  // check if in 1-to-1 case
  bool complex = false;
  for (auto &&t : traits) {
    if (t.second.args.size() > 1) {
      complex = true;
      break;
    }
  }
  // another pass to set up trait
  std::vector<std::string> transfers;
  std::string ret_transfer;
  for (auto &&t : traits) {
    auto &ti = t.second;
    auto tt = GeneratorBase::get_transfer_parameter(ti.transfer, true);
    // skip return
    if (t.first < 0) {
      ret_transfer = tt;
      continue;
    }
    if (complex) {
      auto transform = [](int index) { return std::to_string(index); };
      auto li = boost::algorithm::join(
          ti.args | boost::adaptors::transformed(transform), ", ");
      auto custom = ti.custom.empty() ? "void" : ti.custom;
      tt = fmt::format("detail::arg_info<{}, {}, {}, detail::args_index<{}>>",
          tt, ti.inout ? "true" : "false", custom, li);
    } else if (ti.inout) {
      tt = fmt::format("detail::arg_info<{}, true>", tt);
    }
    transfers.emplace_back(std::move(tt));
  }
  assert(!ret_transfer.empty());
  auto ret = fmt::format("{}, std::tuple<{}>", ret_transfer,
      boost::algorithm::join(transfers, ", "));
  // also add C signature if needed
  if (complex)
    ret += ", " + c_sig;
  return ret;
}

// process a function (or alike) and callback
//
// for a call;
// const on parameter is maintained, always added for string
// return value and output parameters always non-const
//
// callback; likewise (but not output params for now)
//
// klass: actual name of class being declared/defined (e.g. xxxBase)
// klasstype: intended target type of class (e.g. xxx), used in signal instance
// / constructor returns: last processed function definition (possibly only 1)
FunctionDefinition
process_element_function(GeneratorContext &_ctx, const std::string _ns,
    const pt::ptree::value_type &entry, std::ostream &out, std::ostream &impl,
    const std::string &klass, const std::string &klasstype,
    std::set<std::string> &deps, bool allow_deprecated)
{
  ElementFunction func;

  auto &kind = func.kind = entry.first;
  auto &node = entry.second;

  auto &name = func.name = get_name(node);
  auto c_name = (kind == EL_SIGNAL || kind == EL_VIRTUAL_METHOD)
                    ? name
                    : get_attribute(node,
                          kind == EL_CALLBACK ? AT_CTYPE : AT_CIDENTIFIER);
  func.c_id = (kind == EL_VIRTUAL_METHOD) ? klasstype + "::" + c_name : c_name;
  // global qualifier needed as c_name might otherwise resolve wrongly
  // e.g. if g_mkdir is macro to mkdir (and resolve to namespaced mkdir)
  std::string qualifier =
      (kind == EL_FUNCTION || kind == EL_METHOD) ? "::" : "";
  func.functionexp = kind == EL_VIRTUAL_METHOD
                         ? fmt::format("get_struct_()->{}", c_name)
                         : qualifier + c_name;

  func.throws = get_attribute<int>(node, AT_THROWS, 0);
  func.shadows = get_attribute(node, AT_SHADOWS, "");
  func.lib_symbol =
      (kind == EL_FUNCTION || kind == EL_METHOD || kind == EL_CONSTRUCTOR);

  FunctionGenerator gen(
      _ctx, _ns, func, klass, klasstype, deps, allow_deprecated);
  gen.const_method = (kind == EL_METHOD) && _ctx.options.const_method;
  return gen.process(&entry, nullptr, out, impl);
}

FunctionDefinition
process_element_function(GeneratorContext &_ctx, const std::string _ns,
    const ElementFunction &func, const std::vector<Parameter> &params,
    std::ostream &out, std::ostream &impl, const std::string &klass,
    const std::string &klasstype, std::set<std::string> &deps)
{
  FunctionGenerator gen(_ctx, _ns, func, klass, klasstype, deps);
  return gen.process(nullptr, &params, out, impl);
}
