/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */
/*
 * This file is part of the Collabora Office project.
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

#include <sal/config.h>

#include <cassert>
#include <cstddef>
#include <cstdlib>
#include <fstream>
#include <ios>
#include <iostream>
#include <list>
#include <map>
#include <memory>
#include <ostream>
#include <set>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

#include <codemaker/commoncpp.hxx>
#include <codemaker/global.hxx>
#include <codemaker/typemanager.hxx>
#include <codemaker/unotype.hxx>
#include <osl/file.hxx>
#include <osl/process.h>
#include <osl/thread.h>
#include <rtl/process.h>
#include <rtl/ref.hxx>
#include <rtl/string.hxx>
#include <rtl/textcvt.h>
#include <rtl/ustrbuf.hxx>
#include <rtl/ustring.hxx>
#include <sal/main.h>
#include <sal/types.h>
#include <unoidl/unoidl.hxx>

namespace
{
[[noreturn]] void badUsage()
{
    std::cerr
        << "Usage:\n\n"
           "  embindmaker <name> <cpp-output> <hpp-output> <js-output> <registries>\n\n"
           "where each <registry> is '+' (primary) or ':' (secondary), followed by: either a\n"
           "new- or legacy-format .rdb file, a single .idl file, or a root directory of an\n"
           ".idl file tree.  For all primary registries, Embind code is written to\n"
           "<cpp-output>/<hpp-output> and corresponding JavaScript scaffolding code is\n"
           "written to <js-output>.  The <name> is used as part of some of the identifiers\n"
           "in those generated files.\n";
    std::exit(EXIT_FAILURE);
}

std::string getPathnameArgument(sal_uInt32 argument)
{
    OUString arg;
    rtl_getAppCommandArg(argument, &arg.pData);
    OString path;
    auto const enc = osl_getThreadTextEncoding();
    if (!arg.convertToString(&path, enc,
                             RTL_UNICODETOTEXT_FLAGS_UNDEFINED_ERROR
                                 | RTL_UNICODETOTEXT_FLAGS_INVALID_ERROR))
    {
        std::cerr << "Cannot convert \"" << arg << "\" to system encoding " << enc << "\n";
        std::exit(EXIT_FAILURE);
    }
    return std::string(path);
}

std::pair<OUString, bool> parseRegistryArgument(sal_uInt32 argument)
{
    OUString arg;
    rtl_getAppCommandArg(argument, &arg.pData);
    bool primary;
    if (arg.startsWith(u"+", &arg))
    {
        primary = true;
    }
    else if (arg.startsWith(u":", &arg))
    {
        primary = false;
    }
    else
    {
        std::cerr << "Bad registry argument \"" << arg << "\"\n";
        std::exit(EXIT_FAILURE);
    }
    OUString url;
    auto const e1 = osl::FileBase::getFileURLFromSystemPath(arg, url);
    if (e1 != osl::FileBase::E_None)
    {
        std::cerr << "Cannot convert \"" << arg << "\" to file URL, error code " << +e1 << "\n";
        std::exit(EXIT_FAILURE);
    }
    OUString cwd;
    auto const e2 = osl_getProcessWorkingDir(&cwd.pData);
    if (e2 != osl_Process_E_None)
    {
        std::cerr << "Cannot obtain working directory, error code " << +e2 << "\n";
        std::exit(EXIT_FAILURE);
    }
    OUString abs;
    auto const e3 = osl::FileBase::getAbsoluteFileURL(cwd, url, abs);
    if (e3 != osl::FileBase::E_None)
    {
        std::cerr << "Cannot make \"" << url << "\" into an absolute file URL, error code " << +e3
                  << "\n";
        std::exit(EXIT_FAILURE);
    }
    return { abs, primary };
}

struct Module
{
    std::map<OUString, std::shared_ptr<Module>> modules;
    std::vector<std::pair<OUString, OUString>> mappings;
};

OUString
getServiceConstructorName(unoidl::SingleInterfaceBasedServiceEntity::Constructor const& constructor)
{
    return constructor.defaultConstructor ? u"create"_ustr : constructor.name;
}

OUString jsName(OUString const& name) { return name.replace(' ', '_').replace('.', '$'); }

OUString
jsServiceConstructor(OUString const& service,
                     unoidl::SingleInterfaceBasedServiceEntity::Constructor const& constructor)
{
    return "uno_Function_" + jsName(service) + "$$" + getServiceConstructorName(constructor);
}

OUString jsSingleton(OUString const& singleton) { return "uno_Function_" + jsName(singleton); }

void scan(rtl::Reference<unoidl::MapCursor> const& cursor, std::u16string_view prefix,
          Module* module, std::vector<OUString>& enums, std::vector<OUString>& structs,
          std::vector<OUString>& exceptions, std::vector<OUString>& interfaces,
          std::vector<OUString>& services, std::vector<OUString>& singletons)
{
    assert(cursor.is());
    assert(module != nullptr);
    for (;;)
    {
        OUString id;
        auto const ent = cursor->getNext(&id);
        if (!ent.is())
        {
            break;
        }
        OUString name(prefix + id);
        switch (ent->getSort())
        {
            case unoidl::Entity::SORT_MODULE:
            {
                auto& sub = module->modules[id];
                if (!sub)
                {
                    sub = std::make_shared<Module>();
                }
                scan(static_cast<unoidl::ModuleEntity*>(ent.get())->createCursor(),
                     Concat2View(name + "."), sub.get(), enums, structs, exceptions, interfaces,
                     services, singletons);
                break;
            }
            case unoidl::Entity::SORT_ENUM_TYPE:
                module->mappings.emplace_back(id, "instance.uno_Type_" + jsName(name));
                enums.emplace_back(name);
                break;
            case unoidl::Entity::SORT_PLAIN_STRUCT_TYPE:
                module->mappings.emplace_back(id, "instance.uno_Type_" + jsName(name));
                structs.emplace_back(name);
                break;
            case unoidl::Entity::SORT_EXCEPTION_TYPE:
                module->mappings.emplace_back(id, "instance.uno_Type_" + jsName(name));
                exceptions.emplace_back(name);
                break;
            case unoidl::Entity::SORT_INTERFACE_TYPE:
                module->mappings.emplace_back(id, "instance.uno_Type_" + jsName(name));
                interfaces.emplace_back(name);
                break;
            case unoidl::Entity::SORT_CONSTANT_GROUP:
            {
                auto const& members
                    = static_cast<unoidl::ConstantGroupEntity*>(ent.get())->getMembers();
                if (!members.empty())
                {
                    auto sub = std::make_shared<Module>();
                    for (auto const& member : members)
                    {
                        OUString value;
                        switch (member.value.type)
                        {
                            case unoidl::ConstantValue::TYPE_BOOLEAN:
                                value = member.value.booleanValue ? u"true"_ustr : u"false"_ustr;
                                break;
                            case unoidl::ConstantValue::TYPE_BYTE:
                                value = OUString::number(member.value.byteValue);
                                break;
                            case unoidl::ConstantValue::TYPE_SHORT:
                                value = OUString::number(member.value.shortValue);
                                break;
                            case unoidl::ConstantValue::TYPE_UNSIGNED_SHORT:
                                value = OUString::number(member.value.unsignedShortValue);
                                break;
                            case unoidl::ConstantValue::TYPE_LONG:
                                value = OUString::number(member.value.longValue);
                                break;
                            case unoidl::ConstantValue::TYPE_UNSIGNED_LONG:
                                value = OUString::number(member.value.unsignedLongValue);
                                break;
                            case unoidl::ConstantValue::TYPE_HYPER:
                                value = OUString::number(member.value.hyperValue) + "n";
                                break;
                            case unoidl::ConstantValue::TYPE_UNSIGNED_HYPER:
                                value = OUString::number(member.value.unsignedHyperValue) + "n";
                                break;
                            case unoidl::ConstantValue::TYPE_FLOAT:
                                value = OUString::number(member.value.floatValue);
                                break;
                            case unoidl::ConstantValue::TYPE_DOUBLE:
                                value = OUString::number(member.value.doubleValue);
                                break;
                        }
                        sub->mappings.emplace_back(member.name, value);
                    }
                    module->modules[id] = sub;
                }
                break;
            }
            case unoidl::Entity::SORT_SINGLE_INTERFACE_BASED_SERVICE:
            {
                auto const& ctors
                    = static_cast<unoidl::SingleInterfaceBasedServiceEntity*>(ent.get())
                          ->getConstructors();
                if (!ctors.empty())
                {
                    auto sub = std::make_shared<Module>();
                    for (auto const& ctor : ctors)
                    {
                        sub->mappings.emplace_back(getServiceConstructorName(ctor),
                                                   "instance." + jsServiceConstructor(name, ctor));
                    }
                    module->modules[id] = sub;
                    services.emplace_back(name);
                }
            }
            break;
            case unoidl::Entity::SORT_INTERFACE_BASED_SINGLETON:
                module->mappings.emplace_back(id, "instance." + jsSingleton(name));
                singletons.emplace_back(name);
                break;
            default:
                break;
        }
    }
}

OUString cppName(OUString const& name)
{
    sal_Int32 k;
    std::vector<OString> args;
    OUString n(b2u(codemaker::UnoType::decompose(u2b(name), &k, &args)));
    OUStringBuffer buf;
    for (sal_Int32 i = 0; i != k; ++i)
    {
        buf.append("::cpo::uno::Sequence<");
    }
    if (n == "boolean")
    {
        buf.append("bool");
    }
    else if (n == "byte")
    {
        buf.append("::sal_Int8");
    }
    else if (n == "short")
    {
        buf.append("::sal_Int16");
    }
    else if (n == "unsigned short")
    {
        buf.append("::sal_uInt16");
    }
    else if (n == "long")
    {
        buf.append("::sal_Int32");
    }
    else if (n == "unsigned long")
    {
        buf.append("::sal_uInt32");
    }
    else if (n == "hyper")
    {
        buf.append("::sal_Int64");
    }
    else if (n == "unsigned hyper")
    {
        buf.append("::sal_uInt64");
    }
    else if (n == "float")
    {
        buf.append("float");
    }
    else if (n == "double")
    {
        buf.append("double");
    }
    else if (n == "char")
    {
        buf.append("::sal_Unicode");
    }
    else if (n == "string")
    {
        buf.append("::rtl::OUString");
    }
    else if (n == "type")
    {
        buf.append("::cpo::uno::Type");
    }
    else if (n == "any")
    {
        buf.append("::cpo::uno::Any");
    }
    else
    {
        buf.append("::" + n.replaceAll(u".", u"::"));
    }
    if (!args.empty())
    {
        buf.append('<');
        bool first = true;
        for (auto const& i : args)
        {
            if (first)
            {
                first = false;
            }
            else
            {
                buf.append(", ");
            }
            buf.append(cppName(b2u(i)));
        }
        buf.append('>');
    }
    for (sal_Int32 i = 0; i != k; ++i)
    {
        buf.append('>');
    }
    return buf.makeStringAndClear();
}

OUString resolveOuterTypedefs(rtl::Reference<TypeManager> const& manager, OUString const& name)
{
    for (OUString n(name);;)
    {
        rtl::Reference<unoidl::Entity> ent;
        if (manager->getSort(n, &ent) != codemaker::UnoType::Sort::Typedef)
        {
            return n;
        }
        n = dynamic_cast<unoidl::TypedefEntity&>(*ent).getType();
    }
}

OUString resolveAllTypedefs(rtl::Reference<TypeManager> const& manager, std::u16string_view name)
{
    sal_Int32 k1;
    OUString n(b2u(codemaker::UnoType::decompose(u2b(name), &k1)));
    for (;;)
    {
        rtl::Reference<unoidl::Entity> ent;
        if (manager->getSort(n, &ent) != codemaker::UnoType::Sort::Typedef)
        {
            break;
        }
        sal_Int32 k2;
        n = b2u(codemaker::UnoType::decompose(
            u2b(static_cast<unoidl::TypedefEntity*>(ent.get())->getType()), &k2));
        k1 += k2; //TODO: overflow
    }
    OUStringBuffer b;
    for (sal_Int32 i = 0; i != k1; ++i)
    {
        b.append("[]");
    }
    b.append(n);
    return b.makeStringAndClear();
}

bool passByReference(rtl::Reference<TypeManager> const& manager, OUString const& name)
{
    switch (manager->getSort(resolveOuterTypedefs(manager, name)))
    {
        case codemaker::UnoType::Sort::Boolean:
        case codemaker::UnoType::Sort::Byte:
        case codemaker::UnoType::Sort::Short:
        case codemaker::UnoType::Sort::UnsignedShort:
        case codemaker::UnoType::Sort::Long:
        case codemaker::UnoType::Sort::UnsignedLong:
        case codemaker::UnoType::Sort::Hyper:
        case codemaker::UnoType::Sort::UnsignedHyper:
        case codemaker::UnoType::Sort::Float:
        case codemaker::UnoType::Sort::Double:
        case codemaker::UnoType::Sort::Char:
        case codemaker::UnoType::Sort::Enum:
            return false;
        case codemaker::UnoType::Sort::String:
        case codemaker::UnoType::Sort::Type:
        case codemaker::UnoType::Sort::Any:
        case codemaker::UnoType::Sort::Sequence:
        case codemaker::UnoType::Sort::PlainStruct:
        case codemaker::UnoType::Sort::InstantiatedPolymorphicStruct:
        case codemaker::UnoType::Sort::Interface:
            return true;
        default:
            throw CannotDumpException("unexpected entity \"" + name
                                      + "\" in call to passByReference");
    }
}

void dumpType(std::ostream& out, rtl::Reference<TypeManager> const& manager,
              std::u16string_view name)
{
    sal_Int32 k;
    std::vector<OString> args;
    OUString n(
        b2u(codemaker::UnoType::decompose(u2b(resolveAllTypedefs(manager, name)), &k, &args)));
    for (sal_Int32 i = 0; i != k; ++i)
    {
        out << "::cpo::uno::Sequence<";
    }
    switch (manager->getSort(n))
    {
        case codemaker::UnoType::Sort::Void:
            out << "void";
            break;
        case codemaker::UnoType::Sort::Boolean:
            out << "bool";
            break;
        case codemaker::UnoType::Sort::Byte:
            out << "::sal_Int8";
            break;
        case codemaker::UnoType::Sort::Short:
            out << "::sal_Int16";
            break;
        case codemaker::UnoType::Sort::UnsignedShort:
            out << "::sal_uInt16";
            break;
        case codemaker::UnoType::Sort::Long:
            out << "::sal_Int32";
            break;
        case codemaker::UnoType::Sort::UnsignedLong:
            out << "::sal_uInt32";
            break;
        case codemaker::UnoType::Sort::Hyper:
            out << "::sal_Int64";
            break;
        case codemaker::UnoType::Sort::UnsignedHyper:
            out << "::sal_uInt64";
            break;
        case codemaker::UnoType::Sort::Float:
            out << "float";
            break;
        case codemaker::UnoType::Sort::Double:
            out << "double";
            break;
        case codemaker::UnoType::Sort::Char:
            out << "::sal_Unicode";
            break;
        case codemaker::UnoType::Sort::String:
            out << "::rtl::OUString";
            break;
        case codemaker::UnoType::Sort::Type:
            out << "::cpo::uno::Type";
            break;
        case codemaker::UnoType::Sort::Any:
            out << "::cpo::uno::Any";
            break;
        case codemaker::UnoType::Sort::Enum:
        case codemaker::UnoType::Sort::PlainStruct:
        case codemaker::UnoType::Sort::Exception:
            out << cppName(n);
            break;
        case codemaker::UnoType::Sort::PolymorphicStructTemplate:
            out << cppName(n);
            if (!args.empty())
            {
                out << "<";
                bool first = true;
                for (auto const& arg : args)
                {
                    if (first)
                    {
                        first = false;
                    }
                    else
                    {
                        out << ", ";
                    }
                    dumpType(out, manager, b2u(arg));
                }
                out << ">";
            }
            break;
        case codemaker::UnoType::Sort::Interface:
            out << "::com::sun::star::uno::Reference<";
            out << cppName(n);
            out << ">";
            break;
        default:
            throw CannotDumpException(OUString::Concat("unexpected entity \"") + name
                                      + "\" in call to dumpType");
    }
    for (sal_Int32 i = 0; i != k; ++i)
    {
        out << ">";
    }
}

void dumpStructMembers(std::ostream& out, rtl::Reference<TypeManager> const& manager,
                       OUString const& name, rtl::Reference<unoidl::PlainStructTypeEntity> struc)
{
    auto const& base = struc->getDirectBase();
    if (!base.isEmpty())
    {
        auto const ent = manager->getManager()->findEntity(base);
        if (!ent.is() || ent->getSort() != unoidl::Entity::SORT_PLAIN_STRUCT_TYPE)
        {
            throw CannotDumpException("bad struct base \"" + base + "\"");
        }
        dumpStructMembers(out, manager, name,
                          static_cast<unoidl::PlainStructTypeEntity*>(ent.get()));
    }
    for (auto const& mem : struc->getDirectMembers())
    {
        out << "\n        .field(\"" << mem.name << "\", &" << cppName(name) << "::" << mem.name
            << ")";
    }
}

void dumpInstantiationMembers(std::ostream& out, OUString const& name,
                              rtl::Reference<unoidl::PolymorphicStructTypeTemplateEntity> poly)
{
    for (auto const& mem : poly->getMembers())
    {
        out << "\n        .field(\"" << mem.name << "\", &" << cppName(name) << "::" << mem.name
            << ")";
    }
}

void dumpExceptionMembers(std::ostream& out, rtl::Reference<TypeManager> const& manager,
                          OUString const& name,
                          rtl::Reference<unoidl::ExceptionTypeEntity> exception)
{
    auto const& base = exception->getDirectBase();
    if (!base.isEmpty())
    {
        auto const ent = manager->getManager()->findEntity(base);
        if (!ent.is() || ent->getSort() != unoidl::Entity::SORT_EXCEPTION_TYPE)
        {
            throw CannotDumpException("bad exception base \"" + base + "\"");
        }
        dumpExceptionMembers(out, manager, name,
                             static_cast<unoidl::ExceptionTypeEntity*>(ent.get()));
    }
    for (auto const& mem : exception->getDirectMembers())
    {
        out << "\n        .field(\"" << mem.name << "\", &" << cppName(name) << "::" << mem.name
            << ")";
    }
}

void dumpAttributes(std::ostream& out, rtl::Reference<TypeManager> const& manager,
                    OUString const& name, rtl::Reference<unoidl::InterfaceTypeEntity> const& entity,
                    std::list<OUString> const& baseTrail)
{
    for (auto const& attr : entity->getDirectAttributes())
    {
        out << "        .property<";
        dumpType(out, manager, attr.type);
        out << ">(\"" << attr.name << "\", +[](" << cppName(name) << " const & the_self) { return ";
        for (auto const& base : baseTrail)
        {
            out << "static_cast<" << cppName(base) << " &>(";
        }
        out << "const_cast<" << cppName(name) << " &>(the_self)";
        for (std::size_t i = 0; i != baseTrail.size(); ++i)
        {
            out << ")";
        }
        out << ".get" << attr.name << "(); }";
        if (!attr.readOnly)
        {
            out << ", +[](" << cppName(name) << " & the_self, ";
            dumpType(out, manager, attr.type);
            if (passByReference(manager, attr.type))
            {
                out << " const &";
            }
            out << " the_value) { ";
            for (auto const& base : baseTrail)
            {
                out << "static_cast<" << cppName(base) << " &>(";
            }
            out << "the_self";
            for (std::size_t i = 0; i != baseTrail.size(); ++i)
            {
                out << ")";
            }
            out << ".set" << attr.name << "(the_value); }";
        }
        out << "/*only supported since "
               "<https://github.com/emscripten-core/emscripten/commit/"
               "09b765f76e052e6bfcf741ed6d2bae1788200734> \"[embind] Return value policy support "
               "for properties. (#21935)\" towards emsdk 3.1.62: , "
               "::emscripten::pure_virtual()*/)\n";
    }
}

bool hasInOutParameters(unoidl::InterfaceTypeEntity::Method const& method)
{
    return std::any_of(method.parameters.begin(), method.parameters.end(),
                       [](auto const& parameter) {
                           return parameter.direction
                                  != unoidl::InterfaceTypeEntity::Method::Parameter::DIRECTION_IN;
                       });
}

void dumpParameters(std::ostream& out, rtl::Reference<TypeManager> const& manager,
                    unoidl::InterfaceTypeEntity::Method const& method, bool declarations)
{
    bool first = true;
    for (auto const& param : method.parameters)
    {
        if (first)
        {
            first = false;
        }
        else
        {
            out << ", ";
        }
        if (declarations)
        {
            if (param.direction != unoidl::InterfaceTypeEntity::Method::Parameter::DIRECTION_IN)
            {
                out << "::unoembindhelpers::UnoInOutParam<";
            }
            dumpType(out, manager, param.type);
            if (param.direction == unoidl::InterfaceTypeEntity::Method::Parameter::DIRECTION_IN)
            {
                if (passByReference(manager, param.type))
                {
                    out << " const &";
                }
            }
            else
            {
                out << "> *";
            }
            out << " ";
        }
        out << param.name;
        if (!declarations
            && param.direction != unoidl::InterfaceTypeEntity::Method::Parameter::DIRECTION_IN)
        {
            out << "->value";
        }
    }
}

void dumpWrapper(std::ostream& out, rtl::Reference<TypeManager> const& manager,
                 OUString const& interfaceName, unoidl::InterfaceTypeEntity::Method const& method,
                 std::list<OUString> const& baseTrail)
{
    out << "        .function(\"" << method.name << "\", +[](::com::sun::star::uno::Reference<"
        << cppName(interfaceName);
    out << "> const & the_self";
    if (!method.parameters.empty())
    {
        out << ", ";
    }
    dumpParameters(out, manager, method, true);
    out << ") { return ";
    for (auto const& base : baseTrail)
    {
        out << "static_cast<" << cppName(base) << " *>(";
    }
    out << "the_self";
    if (!baseTrail.empty())
    {
        out << ".get()";
    }
    for (std::size_t i = 0; i != baseTrail.size(); ++i)
    {
        out << ")";
    }
    out << "->" << method.name << "(";
    dumpParameters(out, manager, method, false);
    out << "); }";
    if (hasInOutParameters(method))
    {
        out << ", ::emscripten::allow_raw_pointers()";
    }
    out << ", ::emscripten::pure_virtual())\n";
}

void dumpMethods(std::ostream& out, rtl::Reference<TypeManager> const& manager,
                 OUString const& name, rtl::Reference<unoidl::InterfaceTypeEntity> const& entity,
                 std::list<OUString> const& baseTrail)
{
    if (name != "com.sun.star.uno.XInterface")
    {
        for (auto const& meth : entity->getDirectMethods())
        {
            if (!baseTrail.empty() || hasInOutParameters(meth))
            {
                dumpWrapper(out, manager, name, meth, baseTrail);
            }
            else
            {
                out << "        .function(\"" << meth.name << "\", &" << cppName(name)
                    << "::" << meth.name << ", ::emscripten::pure_virtual())\n";
            }
        }
    }
}

rtl::Reference<unoidl::InterfaceTypeEntity>
resolveInterface(rtl::Reference<TypeManager> const& manager, OUString const& name)
{
    auto const ent = manager->getManager()->findEntity(name);
    if (!ent.is() || ent->getSort() != unoidl::Entity::SORT_INTERFACE_TYPE)
    {
        throw CannotDumpException("bad interface \"" + name + "\"");
    }
    return static_cast<unoidl::InterfaceTypeEntity*>(ent.get());
}

void recordVisitedBases(rtl::Reference<TypeManager> const& manager, OUString const& name,
                        std::set<OUString>& visitedBases)
{
    auto const ent = resolveInterface(manager, name);
    for (auto const& base : ent->getDirectMandatoryBases())
    {
        if (visitedBases.insert(base.name).second)
        {
            recordVisitedBases(manager, base.name, visitedBases);
        }
    }
}

void dumpBase(std::ostream& out, rtl::Reference<TypeManager> const& manager,
              OUString const& interface, OUString const& name, std::set<OUString>& visitedBases,
              std::list<OUString> const& baseTrail)
{
    auto const ent = resolveInterface(manager, name);
    for (auto const& base : ent->getDirectMandatoryBases())
    {
        if (visitedBases.insert(base.name).second)
        {
            auto trail = baseTrail;
            trail.push_front(base.name);
            dumpBase(out, manager, interface, base.name, visitedBases, trail);
        }
    }
    dumpAttributes(out, manager, interface, ent, baseTrail);
    dumpMethods(out, manager, interface, ent, baseTrail);
}

void dumpWrapperClassMembers(std::ostream& out, rtl::Reference<TypeManager> const& manager,
                             OUString const& interface, OUString const& name,
                             std::set<OUString>& visitedBases)
{
    auto const ent = resolveInterface(manager, name);
    for (auto const& base : ent->getDirectMandatoryBases())
    {
        if (visitedBases.insert(base.name).second)
        {
            dumpWrapperClassMembers(out, manager, interface, base.name, visitedBases);
        }
    }
    for (auto const& attr : ent->getDirectAttributes())
    {
        out << "    ";
        dumpType(out, manager, attr.type);
        out << " get" << attr.name << "() override {";
        if (attr.type == "any" || attr.type.startsWith("[]"))
        {
            out << " return *call<std::unique_ptr<";
            dumpType(out, manager, attr.type);
            out << ">>(\"get" << attr.name << "\"); }\n";
        }
        else
        {
            out << " return call<";
            dumpType(out, manager, attr.type);
            out << ">(\"get" << attr.name << "\"); }\n";
        }
        if (!attr.readOnly)
        {
            out << "    void set" << attr.name << "(";
            dumpType(out, manager, attr.type);
            switch (manager->getSort(resolveOuterTypedefs(manager, attr.type)))
            {
                case codemaker::UnoType::Sort::Boolean:
                case codemaker::UnoType::Sort::Byte:
                case codemaker::UnoType::Sort::Short:
                case codemaker::UnoType::Sort::UnsignedShort:
                case codemaker::UnoType::Sort::Long:
                case codemaker::UnoType::Sort::UnsignedLong:
                case codemaker::UnoType::Sort::Hyper:
                case codemaker::UnoType::Sort::UnsignedHyper:
                case codemaker::UnoType::Sort::Float:
                case codemaker::UnoType::Sort::Double:
                case codemaker::UnoType::Sort::Char:
                case codemaker::UnoType::Sort::Enum:
                    break;
                case codemaker::UnoType::Sort::String:
                case codemaker::UnoType::Sort::Type:
                case codemaker::UnoType::Sort::Any:
                case codemaker::UnoType::Sort::Sequence:
                case codemaker::UnoType::Sort::PlainStruct:
                case codemaker::UnoType::Sort::InstantiatedPolymorphicStruct:
                case codemaker::UnoType::Sort::Interface:
                    out << " const &";
                    break;
                default:
                    throw CannotDumpException("unexpected entity \"" + attr.type
                                              + "\" as attribute type");
            }
            out << " the_value) override { return call<void>(\"set" << attr.name
                << "\", the_value); }\n";
        }
    }
    for (auto const& meth : ent->getDirectMethods())
    {
        out << "    ";
        dumpType(out, manager, meth.returnType);
        out << " " << meth.name << "(";
        bool first = true;
        for (auto const& param : meth.parameters)
        {
            if (first)
            {
                first = false;
            }
            else
            {
                out << ", ";
            }
            dumpType(out, manager, param.type);
            if (param.direction == unoidl::InterfaceTypeEntity::Method::Parameter::DIRECTION_IN)
            {
                switch (manager->getSort(resolveOuterTypedefs(manager, param.type)))
                {
                    case codemaker::UnoType::Sort::Boolean:
                    case codemaker::UnoType::Sort::Byte:
                    case codemaker::UnoType::Sort::Short:
                    case codemaker::UnoType::Sort::UnsignedShort:
                    case codemaker::UnoType::Sort::Long:
                    case codemaker::UnoType::Sort::UnsignedLong:
                    case codemaker::UnoType::Sort::Hyper:
                    case codemaker::UnoType::Sort::UnsignedHyper:
                    case codemaker::UnoType::Sort::Float:
                    case codemaker::UnoType::Sort::Double:
                    case codemaker::UnoType::Sort::Char:
                    case codemaker::UnoType::Sort::Enum:
                        break;
                    case codemaker::UnoType::Sort::String:
                    case codemaker::UnoType::Sort::Type:
                    case codemaker::UnoType::Sort::Any:
                    case codemaker::UnoType::Sort::Sequence:
                    case codemaker::UnoType::Sort::PlainStruct:
                    case codemaker::UnoType::Sort::InstantiatedPolymorphicStruct:
                    case codemaker::UnoType::Sort::Interface:
                        out << " const &";
                        break;
                    default:
                        throw CannotDumpException("unexpected entity \"" + param.type
                                                  + "\" as parameter type");
                }
            }
            else
            {
                out << " &";
            }
            out << " " << param.name;
        }
        out << ") override {";
        if (meth.returnType == "any" || meth.returnType.startsWith("[]"))
        {
            out << " return *call<std::unique_ptr<";
            dumpType(out, manager, meth.returnType);
            out << ">>(\"" << meth.name << "\"";
            for (auto const& param : meth.parameters)
            {
                out << ", " << param.name;
            }
            out << "); }\n";
        }
        else
        {
            out << " return call<";
            dumpType(out, manager, meth.returnType);
            out << ">(\"" << meth.name << "\"";
            for (auto const& param : meth.parameters)
            {
                out << ", " << param.name;
            }
            out << "); }\n";
        }
    }
}

void dumpRegisterFunctionProlog(std::ostream& out, unsigned long long& counter)
{
    out << "static void __attribute__((noinline)) register" << counter << "() {\n";
}

void dumpRegisterFunctionEpilog(std::ostream& out, unsigned long long& counter)
{
    out << "}\n";
    ++counter;
    if (counter == 0)
    {
        std::cerr << "Emitting too many register functions\n";
        std::exit(EXIT_FAILURE);
    }
}

void recordGenericTypes(rtl::Reference<TypeManager> const& manager, OUString const& type,
                        std::set<OUString>& sequences, std::set<OUString>& instantiations)
{
    auto const res = resolveAllTypedefs(manager, type);
    switch (manager->getSort(res))
    {
        case codemaker::UnoType::Sort::Sequence:
            if (sequences.insert(res).second)
            {
                assert(res.startsWith("[]"));
                recordGenericTypes(manager, res.copy(2), sequences, instantiations);
            }
            break;
        case codemaker::UnoType::Sort::InstantiatedPolymorphicStruct:
            if (instantiations.insert(res).second)
            {
                std::vector<OString> args;
                codemaker::UnoType::decompose(u2b(res), nullptr, &args);
                for (auto const& i : args)
                {
                    recordGenericTypes(manager, b2u(i), sequences, instantiations);
                }
            }
            break;
        default:
            break;
    }
}

void recordInOutParameterType(rtl::Reference<TypeManager> const& manager, OUString const& type,
                              std::set<OUString>& inOutParameters)
{
    auto const res = resolveAllTypedefs(manager, type);
    inOutParameters.insert(res);
}

void writeJsMap(std::ostream& out, Module const& module, std::string const& prefix)
{
    auto comma = false;
    for (auto const & [ id, to ] : module.mappings)
    {
        if (comma)
        {
            out << ",\n";
        }
        out << prefix << "'" << id << "': " << to;
        comma = true;
    }
    for (auto const & [ id, sub ] : module.modules)
    {
        if (comma)
        {
            out << ",\n";
        }
        out << prefix << "'" << id << "': {\n";
        writeJsMap(out, *sub, prefix + "    ");
        out << prefix << "}";
        comma = true;
    }
    if (comma)
    {
        out << "\n";
    }
}
}

SAL_IMPLEMENT_MAIN()
{
    try
    {
        auto const args = rtl_getAppCommandArgCount();
        if (args < 4)
        {
            badUsage();
        }
        OUString name;
        rtl_getAppCommandArg(0, &name.pData);
        auto const cppPathname = getPathnameArgument(1);
        auto const hppPathname = getPathnameArgument(2);
        auto const jsPathname = getPathnameArgument(3);
        rtl::Reference<TypeManager> mgr(new TypeManager);
        for (sal_uInt32 i = 4; i != args; ++i)
        {
            auto const & [ uri, primary ] = parseRegistryArgument(i);
            try
            {
                mgr->loadProvider(uri, primary);
            }
            catch (unoidl::NoSuchFileException&)
            {
                std::cerr << "Input <" << uri << "> does not exist\n";
                std::exit(EXIT_FAILURE);
            }
        }
        auto const module = std::make_shared<Module>();
        std::vector<OUString> enums;
        std::vector<OUString> structs;
        std::vector<OUString> exceptions;
        std::vector<OUString> interfaces;
        std::vector<OUString> services;
        std::vector<OUString> singletons;
        for (auto const& prov : mgr->getPrimaryProviders())
        {
            scan(prov->createRootCursor(), u"", module.get(), enums, structs, exceptions,
                 interfaces, services, singletons);
        }
        std::ofstream cppOut(cppPathname, std::ios_base::out | std::ios_base::trunc);
        if (!cppOut)
        {
            std::cerr << "Cannot open \"" << cppPathname << "\" for writing\n";
            std::exit(EXIT_FAILURE);
        }
        cppOut << "#include <sal/config.h>\n"
                  "#include <memory>\n"
                  "#include <emscripten/bind.h>\n"
                  "#include <cpo/uno/Any.hxx>\n"
                  "#include <com/sun/star/uno/Reference.hxx>\n"
                  "#include <o3tl/unreachable.hxx>\n"
                  "#include <static/unoembindhelpers/PrimaryBindings.hxx>\n";
        for (auto const& enm : enums)
        {
            cppOut << "#include <" << enm.replace('.', '/') << ".hpp>\n";
        }
        for (auto const& str : structs)
        {
            cppOut << "#include <" << str.replace('.', '/') << ".hpp>\n";
        }
        for (auto const& exc : exceptions)
        {
            cppOut << "#include <" << exc.replace('.', '/') << ".hpp>\n";
        }
        for (auto const& ifc : interfaces)
        {
            cppOut << "#include <" << ifc.replace('.', '/') << ".hpp>\n";
        }
        for (auto const& srv : services)
        {
            cppOut << "#include <" << srv.replace('.', '/') << ".hpp>\n";
        }
        for (auto const& sng : singletons)
        {
            cppOut << "#include <" << sng.replace('.', '/') << ".hpp>\n";
        }
        cppOut << "\n"
                  "namespace emscripten::internal {\n";
        for (auto const& ifc : interfaces)
        {
            cppOut << "    template<> void raw_destructor<" << cppName(ifc) << ">(" << cppName(ifc)
                   << " *) { O3TL_UNREACHABLE; }\n";
        }
        cppOut << "}\n\n";
        unsigned long long n = 0;
        for (auto const& enm : enums)
        {
            auto const ent = mgr->getManager()->findEntity(enm);
            assert(ent.is());
            assert(ent->getSort() == unoidl::Entity::SORT_ENUM_TYPE);
            rtl::Reference const enmEnt(static_cast<unoidl::EnumTypeEntity*>(ent.get()));
            dumpRegisterFunctionProlog(cppOut, n);
            cppOut << "    ::emscripten::enum_<" << cppName(enm) << ">(\"uno_Type_" << jsName(enm)
                   << "\")";
            for (auto const& mem : enmEnt->getMembers())
            {
                cppOut << "\n        .value(\"" << mem.name << "\", " << cppName(enm) << "_"
                       << mem.name << ")";
            }
            cppOut << ";\n";
            cppOut << "    ::unoembindhelpers::registerUnoType<" << cppName(enm) << ">();\n";
            dumpRegisterFunctionEpilog(cppOut, n);
        }
        std::set<OUString> sequences;
        std::set<OUString> instantiations;
        for (auto const& str : structs)
        {
            auto const ent = mgr->getManager()->findEntity(str);
            assert(ent.is());
            assert(ent->getSort() == unoidl::Entity::SORT_PLAIN_STRUCT_TYPE);
            rtl::Reference const strEnt(static_cast<unoidl::PlainStructTypeEntity*>(ent.get()));
            dumpRegisterFunctionProlog(cppOut, n);
            cppOut << "    ::emscripten::value_object<" << cppName(str) << ">(\"uno_Type_"
                   << jsName(str) << "\")";
            dumpStructMembers(cppOut, mgr, str, strEnt);
            cppOut << ";\n";
            cppOut << "    ::unoembindhelpers::registerUnoType<" << cppName(str) << ">();\n";
            dumpRegisterFunctionEpilog(cppOut, n);
            for (auto const& mem : strEnt->getDirectMembers())
            {
                recordGenericTypes(mgr, mem.type, sequences, instantiations);
            }
        }
        for (auto const& exc : exceptions)
        {
            auto const ent = mgr->getManager()->findEntity(exc);
            assert(ent.is());
            assert(ent->getSort() == unoidl::Entity::SORT_EXCEPTION_TYPE);
            rtl::Reference const excEnt(static_cast<unoidl::ExceptionTypeEntity*>(ent.get()));
            dumpRegisterFunctionProlog(cppOut, n);
            cppOut << "    ::emscripten::value_object<" << cppName(exc) << ">(\"uno_Type_"
                   << jsName(exc) << "\")";
            dumpExceptionMembers(cppOut, mgr, exc, excEnt);
            cppOut << ";\n";
            cppOut << "    ::unoembindhelpers::registerUnoType<" << cppName(exc) << ">();\n";
            dumpRegisterFunctionEpilog(cppOut, n);
            for (auto const& mem : excEnt->getDirectMembers())
            {
                recordGenericTypes(mgr, mem.type, sequences, instantiations);
            }
        }
        std::set<OUString> inOutParams;
        for (auto const& ifc : interfaces)
        {
            auto const ent = mgr->getManager()->findEntity(ifc);
            assert(ent.is());
            assert(ent->getSort() == unoidl::Entity::SORT_INTERFACE_TYPE);
            rtl::Reference const ifcEnt(static_cast<unoidl::InterfaceTypeEntity*>(ent.get()));
            {
                auto i = ifc.lastIndexOf('.');
                auto j = i + 1;
                if (i == -1)
                {
                    i = 0;
                }
                cppOut << "namespace the_wrappers" << cppName(ifc.copy(0, i)) << " {\n"
                       << "struct " << ifc.copy(j) << " final: public ::emscripten::wrapper<"
                       << cppName(ifc) << "> {\n"
                       << "    EMSCRIPTEN_WRAPPER(" << ifc.copy(j) << ");\n";
                std::set<OUString> visitedBases;
                dumpWrapperClassMembers(cppOut, mgr, ifc, ifc, visitedBases);
                cppOut << "};\n}\n";
            }
            dumpRegisterFunctionProlog(cppOut, n);
            cppOut << "    ::emscripten::class_<" << cppName(ifc);
            //TODO: Embind only supports single inheritance, so use that support at least for a UNO
            // interface's first base, and explicitly spell out the attributes and methods of any
            // remaining bases:
            auto const& bases = ifcEnt->getDirectMandatoryBases();
            if (bases.size() != 0)
            {
                cppOut << ", ::emscripten::base<" << cppName(bases[0].name) << ">";
            }
            cppOut << ">(\"uno_Type_" << jsName(ifc)
                   << "\")\n"
                      "        .allow_subclass<the_wrappers"
                   << cppName(ifc) << ">(\"uno_Wrapper_" << jsName(ifc)
                   << "\")\n"
                      "        .smart_ptr<::com::sun::star::uno::Reference<"
                   << cppName(ifc) << ">>(\"uno_Reference_" << jsName(ifc)
                   << "\")\n"
                      "        .class_function(\"query\", "
                      "+[](::com::sun::star::uno::Reference<::com::sun::star::uno::XInterface> "
                      "const & the_object) { return ::com::sun::star::uno::Reference<"
                   << cppName(ifc)
                   << ">(the_object, ::com::sun::star::uno::UNO_QUERY); })\n"
                      "        .class_function(\"reference\", +[]("
                   << cppName(ifc)
                   << " * the_interface) { return ::com::sun::star::uno::Reference(the_interface); "
                      "}, ::emscripten::allow_raw_pointers())\n";
            if (bases.size() > 1)
            {
                std::set<OUString> visitedBases;
                recordVisitedBases(mgr, bases[0].name, visitedBases);
                for (std::size_t i = 1; i != bases.size(); ++i)
                {
                    dumpBase(cppOut, mgr, ifc, bases[i].name, visitedBases, { bases[i].name });
                }
            }
            dumpAttributes(cppOut, mgr, ifc, ifcEnt, {});
            dumpMethods(cppOut, mgr, ifc, ifcEnt, {});
            cppOut << "        ;\n"
                      "    ::unoembindhelpers::registerUnoType<::com::sun::star::uno::Reference<"
                   << cppName(ifc) << ">>();\n";
            dumpRegisterFunctionEpilog(cppOut, n);
            for (auto const& attr : ifcEnt->getDirectAttributes())
            {
                recordGenericTypes(mgr, attr.type, sequences, instantiations);
            }
            for (auto const& meth : ifcEnt->getDirectMethods())
            {
                for (auto const& param : meth.parameters)
                {
                    recordGenericTypes(mgr, param.type, sequences, instantiations);
                    if (param.direction
                        != unoidl::InterfaceTypeEntity::Method::Parameter::DIRECTION_IN)
                    {
                        recordInOutParameterType(mgr, param.type, inOutParams);
                    }
                }
                recordGenericTypes(mgr, meth.returnType, sequences, instantiations);
            }
        }
        for (auto const& ins : instantiations)
        {
            std::vector<OString> templArgs;
            auto const templ = b2u(codemaker::UnoType::decompose(u2b(ins), nullptr, &templArgs));
            auto const ent = mgr->getManager()->findEntity(templ);
            assert(ent.is());
            assert(ent->getSort() == unoidl::Entity::SORT_POLYMORPHIC_STRUCT_TYPE_TEMPLATE);
            rtl::Reference const polEnt(
                static_cast<unoidl::PolymorphicStructTypeTemplateEntity*>(ent.get()));
            dumpRegisterFunctionProlog(cppOut, n);
            cppOut << "    ::emscripten::value_object<" << cppName(ins) << ">(\"uno_Type_"
                   << jsName(ins) << "\")";
            dumpInstantiationMembers(cppOut, ins, polEnt);
            cppOut << ";\n";
            cppOut << "    ::unoembindhelpers::registerUnoType<" << cppName(ins) << ">();\n";
            dumpRegisterFunctionEpilog(cppOut, n);
            for (auto const& arg : templArgs)
            {
                recordGenericTypes(mgr, b2u(arg), sequences, instantiations);
            }
            for (auto const& mem : polEnt->getMembers())
            {
                if (!mem.parameterized)
                {
                    recordGenericTypes(mgr, mem.type, sequences, instantiations);
                }
            }
        }
        for (auto const& srv : services)
        {
            auto const ent = mgr->getManager()->findEntity(srv);
            assert(ent.is());
            assert(ent->getSort() == unoidl::Entity::SORT_SINGLE_INTERFACE_BASED_SERVICE);
            rtl::Reference const srvEnt(
                static_cast<unoidl::SingleInterfaceBasedServiceEntity*>(ent.get()));
            dumpRegisterFunctionProlog(cppOut, n);
            for (auto const& ctor : srvEnt->getConstructors())
            {
                cppOut << "    ::emscripten::function(\"" << jsServiceConstructor(srv, ctor)
                       << "\", &" << cppName(srv) << "::" << getServiceConstructorName(ctor)
                       << ");\n";
            }
            dumpRegisterFunctionEpilog(cppOut, n);
        }
        for (auto const& sng : singletons)
        {
            dumpRegisterFunctionProlog(cppOut, n);
            cppOut << "    ::emscripten::function(\"" << jsSingleton(sng) << "\", &" << cppName(sng)
                   << "::get);\n";
            dumpRegisterFunctionEpilog(cppOut, n);
        }
        cppOut << "void init_unoembind_" << name << "() {\n";
        for (unsigned long long i = 0; i != n; ++i)
        {
            cppOut << "    register" << i << "();\n";
        }
        for (auto const& seq : sequences)
        {
            cppOut << "    ::unoembindhelpers::registerSequence<";
            assert(seq.startsWith("[]"));
            dumpType(cppOut, mgr, seq.copy(2));
            cppOut << ">(\"uno_Sequence";
            sal_Int32 k;
            auto const nuc = b2u(codemaker::UnoType::decompose(u2b(seq), &k));
            assert(k >= 1);
            if (k > 1)
            {
                cppOut << k;
            }
            cppOut << "_" << jsName(nuc) << "\");\n";
        }
        for (auto const& par : inOutParams)
        {
            cppOut << "    ::unoembindhelpers::registerInOutParameter<";
            dumpType(cppOut, mgr, par);
            cppOut << ">(\"uno_InOutParam_";
            sal_Int32 k;
            auto const nuc = b2u(codemaker::UnoType::decompose(u2b(par), &k));
            if (k >= 1)
            {
                cppOut << "sequence";
                if (k > 1)
                {
                    cppOut << k;
                }
                cppOut << "_";
            }
            cppOut << jsName(nuc.replace(' ', '_')) << "\");\n";
        }
        cppOut << "}\n";
        cppOut.close();
        if (!cppOut)
        {
            std::cerr << "Failed to write \"" << cppPathname << "\"\n";
            std::exit(EXIT_FAILURE);
        }
        std::ofstream hppOut(hppPathname, std::ios_base::out | std::ios_base::trunc);
        if (!hppOut)
        {
            std::cerr << "Cannot open \"" << hppPathname << "\" for writing\n";
            std::exit(EXIT_FAILURE);
        }
        hppOut << "void init_unoembind_" << name << "();\n";
        hppOut.close();
        if (!hppOut)
        {
            std::cerr << "Failed to write \"" << hppPathname << "\"\n";
            std::exit(EXIT_FAILURE);
        }
        std::ofstream jsOut(jsPathname, std::ios_base::out | std::ios_base::trunc);
        if (!jsOut)
        {
            std::cerr << "Cannot open \"" << jsPathname << "\" for writing\n";
            std::exit(EXIT_FAILURE);
        }
        jsOut << "function init_unoembind_" << name << "(instance, tagSymbol) {\n";
        for (auto const& enm : enums)
        {
            auto const ent = mgr->getManager()->findEntity(enm);
            if (!ent.is() || ent->getSort() != unoidl::Entity::SORT_ENUM_TYPE)
            {
                throw CannotDumpException("bad enum type \"" + enm + "\"");
            }
            for (auto const& mem :
                 static_cast<unoidl::EnumTypeEntity const*>(ent.get())->getMembers())
            {
                jsOut << "    instance.uno_Type_" << enm.replace('.', '$') << "." << mem.name
                      << "[tagSymbol] = {kind: 'enumerator', type: '" << enm << "'};\n";
            }
        }
        jsOut << "    return {\n";
        writeJsMap(jsOut, *module, "        ");
        jsOut << "    };\n"
                 "};\n";
        jsOut.close();
        if (!jsOut)
        {
            std::cerr << "Failed to write \"" << jsPathname << "\"\n";
            std::exit(EXIT_FAILURE);
        }
        return EXIT_SUCCESS;
    }
    catch (unoidl::FileFormatException const& e)
    {
        std::cerr << "Bad input <" << e.getUri() << ">: " << e.getDetail() << "\n";
        std::exit(EXIT_FAILURE);
    }
    catch (CannotDumpException const& e)
    {
        std::cerr << "Failure: " << e.getMessage() << "\n";
        std::exit(EXIT_FAILURE);
    }
    catch (std::exception const& e)
    {
        std::cerr << "Failure: " << e.what() << "\n";
        std::exit(EXIT_FAILURE);
    }
}

/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */
