// Copyright (c) ZeroC, Inc.

#include "Gen.h"
#include "../Ice/Endian.h"
#include "../Ice/FileUtil.h"
#include "../Slice/FileTracker.h"
#include "../Slice/Util.h"
#include "Ice/StringUtil.h"
#include "Ice/UUID.h"

#include <algorithm>
#include <cassert>
#include <iterator>

using namespace std;
using namespace Slice;
using namespace Slice::JavaScript;
using namespace Ice;
using namespace IceInternal;

namespace
{
    /// Converts absolute paths to relative and ensures "./" prefix for relative paths.
    /// @param path The file path to normalize
    /// @param relativeTo The file to make absolute paths relative to
    /// @return The normalized path
    string toRelativePath(const string& path, const string& relativeTo)
    {
        string f = path;
        if (IceInternal::isAbsolutePath(f))
        {
            f = relativePath(f, Slice::dirName(relativeTo));
        }
        else if (!f.empty() && f[0] != '.')
        {
            f = "./" + f;
        }
        return f;
    }

    string sliceModeToIceMode(Operation::Mode opMode)
    {
        switch (opMode)
        {
            case Operation::Normal:
                return "0";
            case Operation::Idempotent:
                return "2";
            default:
                assert(false);
        }

        return "???";
    }

    string opFormatTypeToString(FormatType opFormat)
    {
        switch (opFormat)
        {
            case CompactFormat:
                return "0";
            case SlicedFormat:
                return "1";
            default:
                assert(false);
                return "???";
        }
    }

    void printHeader(IceInternal::Output& out)
    {
        out << "// Copyright (c) ZeroC, Inc.";
        out << sp;
        out << nl << "// slice2js version " << ICE_STRING_VERSION;
    }

    string escapeParam(const ParameterList& params, const string& name)
    {
        for (const auto& param : params)
        {
            if (param->mappedName() == name)
            {
                return name + "_";
            }
        }
        return name;
    }

    void writeDocLines(Output& out, const StringList& lines, bool commentFirst, const string& space = " ")
    {
        if (lines.size() > 0)
        {
            StringList l = lines;
            if (!commentFirst)
            {
                out << l.front();
                l.pop_front();
            }

            for (const auto& i : l)
            {
                out << nl << " *";
                if (!i.empty())
                {
                    out << space << i;
                }
            }
        }
    }

    void writeDeprecated(Output& out, const optional<DocComment>& comment, const ContainedPtr& contained)
    {
        // JavaScript doesn't provide a way to deprecate elements other than by using a comment, so we map both the
        // Slice @deprecated tag and the deprecated metadata argument to a `@deprecated` JSDoc tag.
        if (comment && comment->isDeprecated())
        {
            // If a reason was supplied, append it after the `@deprecated` tag.
            const StringList& deprecatedDoc = comment->deprecated();
            if (!deprecatedDoc.empty())
            {
                out << nl << " * @deprecated ";
                writeDocLines(out, deprecatedDoc, false);
                return;
            }
        }
        if ((comment && comment->isDeprecated()) || contained->isDeprecated())
        {
            // If no reason was supplied, fallback to the 'deprecated' metadata argument.
            out << nl << " * @deprecated";
            if (auto deprecated = contained->getDeprecationReason())
            {
                out << " " << *deprecated;
            }
        }
    }

    void writeRemarks(Output& out, const StringList& remarks)
    {
        if (remarks.empty())
        {
            return;
        }

        out << nl << " *";
        out << nl << " * @remarks";
        writeDocLines(out, remarks, true);
    }

    enum OpDocParamType
    {
        OpDocInParams,
        OpDocOutParams,
        OpDocAllParams
    };

    void writeOpDocExceptions(Output& out, const OperationPtr& op, const DocComment& doc)
    {
        for (const auto& [name, lines] : doc.exceptions())
        {
            // Try to locate the exception's definition using the name given in the comment.
            string mappedName = name;
            if (ExceptionPtr ex = op->container()->lookupException(name, false))
            {
                mappedName = ex->mappedScoped(".");
            }
            out << nl << " * @throws {@link " << mappedName << "} ";
            writeDocLines(out, lines, false);
        }
    }
}

bool
Slice::IncludeAggregationVisitor::visitUnitStart(const UnitPtr& unit)
{
    _topLevelFile = unit->topLevelFile();
    _topLevelModule = getJavaScriptModule(unit->findDefinitionContext(_topLevelFile));
    _unit = unit;

    // A map with the unit's direct includes for lookup.
    // resolved path -> filename (as seen in #include)
    map<string, string> directIncludeMap;

    // The set of direct includes for which we need to aggregate transitive imported types.
    // For JavaScript, we aggregate same-module includes; we skip external modules which represent separate packages.
    set<string> directIncludes;
    for (const auto& directInclude : unit->includeFiles())
    {
        auto dc = unit->findDefinitionContext(directInclude);
        assert(dc);

        string resolvedPath = dc->resolvedFilename();
        directIncludeMap[resolvedPath] = directInclude;

        // Only aggregate includes that belong to the same js:module as the top-level file.
        if (getJavaScriptModule(dc) == _topLevelModule)
        {
            directIncludes.insert(resolvedPath);
        }
    }

    // This logic relies on the ordering of unit->allFiles(): a direct include is followed by its transitives.
    // For each file in allFiles (skipping the top-level file):
    // - If it's in directIncludeMap, this is a direct include, and we check the directIncludes set to see if we need
    //   to track its transitives.
    // - If it's not a direct include (so it is treated as transitive), assign it to the current owner.

    // Tracks the last seen direct include for which we need to aggregate transitives. Is set to empty when we don't
    // need to aggregate transitives (e.g., an external module).
    string currentDirectInclude;

    for (const auto& file : unit->allFiles())
    {
        if (file == _topLevelFile)
        {
            continue;
        }

        auto it = directIncludeMap.find(file);
        if (it != directIncludeMap.end())
        {
            // This is a direct include file. Check if we want to aggregate its transitives.
            currentDirectInclude = directIncludes.find(file) != directIncludes.end()
                                       ? toRelativePath(it->second, _topLevelFile)
                                       : string();
        }
        else if (!currentDirectInclude.empty())
        {
            // This is a transitive include, assign it to the current direct include owner.
            _transitiveToDirectInclude[file] = currentDirectInclude;
        }
    }

    return true;
}

void
Slice::IncludeAggregationVisitor::addImportedType(const ContainedPtr& definition)
{
    auto dc = definition->definitionContext();
    int includeLevel = dc->includeLevel();
    if (includeLevel == 0)
    {
        // Not from an included file, skip.
        return;
    }

    // Skip files with js:module metadata - they don't need TypeScript import aggregation.
    if (dc->hasMetadata("js:module"))
    {
        return;
    }

    string typeScope = definition->mappedScoped(".");
    if (includeLevel == 1)
    {
        string includedFile = toRelativePath(dc->filename(), _topLevelFile);
        _importedTypesByTopLevel[_topLevelFile][includedFile].insert(typeScope);
    }
    else if (auto includeKey = resolveDirectInclude(dc))
    {
        // Transitive include - use the owning direct include's key.
        _importedTypesByTopLevel[_topLevelFile][*includeKey].insert(typeScope);
    }
}

std::optional<std::string>
Slice::IncludeAggregationVisitor::resolveDirectInclude(const DefinitionContextPtr& dc) const
{
    auto it = _transitiveToDirectInclude.find(dc->resolvedFilename());
    if (it != _transitiveToDirectInclude.end())
    {
        return it->second;
    }
    return std::nullopt;
}

void
Slice::IncludeAggregationVisitor::visitModuleEnd(const ModulePtr& module)
{
    auto dc = module->definitionContext();
    int includeLevel = dc->includeLevel();
    if (includeLevel == 0)
    {
        // Not from an included file, skip.
        return;
    }

    // Skip files that don't belong to the same js:module - they're imported directly.
    if (getJavaScriptModule(dc) != _topLevelModule)
    {
        return;
    }

    // Only track nested modules, not top-level modules.
    if (module->isTopLevel())
    {
        return;
    }

    if (includeLevel == 1)
    {
        string includedFile = toRelativePath(dc->filename(), _topLevelFile);
        _nestedModulesByTopLevel[_topLevelFile][includedFile].insert(module->mappedScoped("."));
    }
    else if (auto includeKey = resolveDirectInclude(dc))
    {
        _nestedModulesByTopLevel[_topLevelFile][*includeKey].insert(module->mappedScoped("."));
    }
}

bool
Slice::IncludeAggregationVisitor::visitClassDefStart(const ClassDefPtr& p)
{
    addImportedType(p);
    return true;
}

bool
Slice::IncludeAggregationVisitor::visitInterfaceDefStart(const InterfaceDefPtr& p)
{
    addImportedType(p);
    return true;
}

bool
Slice::IncludeAggregationVisitor::visitStructStart(const StructPtr& p)
{
    addImportedType(p);
    return true;
}

bool
Slice::IncludeAggregationVisitor::visitExceptionStart(const ExceptionPtr& p)
{
    addImportedType(p);
    return true;
}

void
Slice::IncludeAggregationVisitor::visitSequence(const SequencePtr& p)
{
    addImportedType(p);
}

void
Slice::IncludeAggregationVisitor::visitDictionary(const DictionaryPtr& p)
{
    addImportedType(p);
}

void
Slice::IncludeAggregationVisitor::visitEnum(const EnumPtr& p)
{
    addImportedType(p);
}

const std::map<std::string, std::map<std::string, std::set<std::string>>>&
Slice::IncludeAggregationVisitor::nestedModulesByTopLevel() const
{
    return _nestedModulesByTopLevel;
}

std::map<std::string, std::set<std::string>>
Slice::IncludeAggregationVisitor::importedTypesByTopLevel(const std::string& topLevelFile) const
{
    auto it = _importedTypesByTopLevel.find(topLevelFile);
    return it != _importedTypesByTopLevel.end() ? it->second : std::map<std::string, std::set<std::string>>{};
}

bool
Slice::IncludeAggregationVisitor::shouldVisitIncludedDefinitions() const
{
    return true;
}

Slice::JsVisitor::JsVisitor(Output& out) : _out(out) {}

Slice::JsVisitor::~JsVisitor() = default;

void
Slice::JsVisitor::writeMarshalDataMembers(const DataMemberList& dataMembers, const DataMemberList& optionalMembers)
{
    for (const auto& dataMember : dataMembers)
    {
        if (!dataMember->optional())
        {
            writeMarshalUnmarshalCode(_out, dataMember->type(), "this." + dataMember->mappedName(), true, _jsModule);
        }
    }

    for (const auto& optionalMember : optionalMembers)
    {
        writeOptionalMarshalUnmarshalCode(
            _out,
            optionalMember->type(),
            "this." + optionalMember->mappedName(),
            optionalMember->tag(),
            true,
            _jsModule);
    }
}

void
Slice::JsVisitor::writeUnmarshalDataMembers(const DataMemberList& dataMembers, const DataMemberList& optionalMembers)
{
    for (const auto& dataMember : dataMembers)
    {
        if (!dataMember->optional())
        {
            writeMarshalUnmarshalCode(_out, dataMember->type(), "this." + dataMember->mappedName(), false, _jsModule);
        }
    }

    for (const auto& optionalMember : optionalMembers)
    {
        writeOptionalMarshalUnmarshalCode(
            _out,
            optionalMember->type(),
            "this." + optionalMember->mappedName(),
            optionalMember->tag(),
            false,
            _jsModule);
    }
}

void
Slice::JsVisitor::writeOneShotConstructorArguments(const DataMemberList& members)
{
    for (const auto& member : members)
    {
        string value;
        if (member->defaultValue())
        {
            value = writeConstantValue(member->type(), member->defaultValueType(), *member->defaultValue());
        }
        else if (member->optional())
        {
            value = "undefined";
        }
        else
        {
            value = getValue(member->type());
        }
        _out << (member->mappedName() + (value.empty() ? value : (" = " + value)));
    }
}

string
Slice::JsVisitor::getValue(const TypePtr& type)
{
    assert(type);

    BuiltinPtr builtin = dynamic_pointer_cast<Builtin>(type);
    if (builtin)
    {
        switch (builtin->kind())
        {
            case Builtin::KindBool:
            {
                return "false";
                break;
            }
            case Builtin::KindByte:
            case Builtin::KindShort:
            case Builtin::KindInt:
            {
                return "0";
                break;
            }
            case Builtin::KindLong:
            {
                return "0n";
                break;
            }
            case Builtin::KindFloat:
            case Builtin::KindDouble:
            {
                return "0.0";
                break;
            }
            case Builtin::KindString:
            {
                return "\"\"";
            }
            default:
            {
                return "null";
                break;
            }
        }
    }

    EnumPtr en = dynamic_pointer_cast<Enum>(type);
    if (en)
    {
        return resolveJsScope(*en->enumerators().begin(), _jsModule);
    }

    StructPtr st = dynamic_pointer_cast<Struct>(type);
    if (st)
    {
        return "new " + resolveJsType(type, _jsModule) + "()";
    }

    return "null";
}

string
Slice::JsVisitor::writeConstantValue(const TypePtr& type, const SyntaxTreeBasePtr& valueType, const string& value)
{
    ostringstream os;
    ConstPtr constant = dynamic_pointer_cast<Const>(valueType);
    if (constant)
    {
        os << resolveJsScope(constant, _jsModule);
    }
    else
    {
        BuiltinPtr bp = dynamic_pointer_cast<Builtin>(type);
        EnumPtr ep;
        if (bp && bp->kind() == Builtin::KindString)
        {
            //
            // For now, we generate strings in ECMAScript 5 format, with two \unnnn for astral characters
            //
            os << "\"" << toStringLiteral(value, "\b\f\n\r\t\v", "", ShortUCN, 0) << "\"";
        }
        else if (bp && bp->kind() == Builtin::KindLong)
        {
            os << value << "n";
        }
        else if ((ep = dynamic_pointer_cast<Enum>(type)))
        {
            EnumeratorPtr lte = dynamic_pointer_cast<Enumerator>(valueType);
            assert(lte);
            os << resolveJsScope(lte, _jsModule);
        }
        else
        {
            os << value;
        }
    }
    return os.str();
}

void
Slice::JsVisitor::writeDocSummary(const ContainedPtr& p, const DocSummaryOptions& options)
{
    assert(!dynamic_pointer_cast<Operation>(p));
    const optional<DocComment>& comment = p->docComment();
    if (!comment && (!options.generateDeprecated || !p->isDeprecated()) && !options.generatedType.has_value())
    {
        // There's nothing to write for this doc-comment.
        return;
    }

    _out << nl << "/**";

    StringList remarks;
    if (comment)
    {
        const StringList& overview = comment->overview();
        if (!overview.empty())
        {
            writeDocLines(_out, overview, true);
        }

        for (const string& line : comment->seeAlso())
        {
            _out << nl << " * " << line;
        }
        remarks = comment->remarks();
    }

    if (options.generateDeprecated)
    {
        writeDeprecated(_out, comment, p);
    }

    if (options.includeRemarks)
    {
        if (options.generatedType)
        {
            // If there's user-provided remarks, and a generated-type message, we insert an empty line between them.
            if (!remarks.empty())
            {
                remarks.emplace_back("");
            }

            remarks.push_back(
                "The Slice compiler generated this " + *options.generatedType + " from Slice " + p->kindOf() + " `" +
                p->scoped() + "`.");
        }

        writeRemarks(_out, remarks);
    }

    _out << nl << " */";
}

Slice::Gen::Gen(const string& base, const string& dir, bool typeScript)
    : _javaScriptOutput(false, true), // No break before opening block in JS + short empty blocks
      _typeScriptOutput(false, true), // No break before opening block in TS + short empty blocks
      _useStdout(false),
      _typeScript(typeScript)
{
    _fileBase = base;

    string::size_type pos = base.find_last_of("/\\");
    if (pos != string::npos)
    {
        _fileBase = base.substr(pos + 1);
    }

    string file = _fileBase + ".js";

    if (!dir.empty())
    {
        file = dir + '/' + file;
    }

    _javaScriptOutput.open(file.c_str());
    if (!_javaScriptOutput)
    {
        ostringstream os;
        os << "cannot open '" << file << "': " << IceInternal::errorToString(errno);
        throw FileException(os.str());
    }
    FileTracker::instance()->addFile(file);

    if (typeScript)
    {
        file = _fileBase + ".d.ts";

        if (!dir.empty())
        {
            file = dir + '/' + file;
        }

        _typeScriptOutput.open(file.c_str());
        if (!_typeScriptOutput)
        {
            ostringstream os;
            os << "cannot open '" << file << "': " << IceInternal::errorToString(errno);
            throw FileException(os.str());
        }
        FileTracker::instance()->addFile(file);
    }
}

Slice::Gen::Gen(const string& base, const string& /*dir*/, bool typeScript, ostream& out)
    : _javaScriptOutput(out, false, true),
      _typeScriptOutput(out, false, true),
      _useStdout(true),
      _typeScript(typeScript)
{
    _fileBase = base;
    string::size_type pos = base.find_last_of("/\\");
    if (pos != string::npos)
    {
        _fileBase = base.substr(pos + 1);
    }
}

Slice::Gen::~Gen()
{
    if (_javaScriptOutput.isOpen() || _useStdout)
    {
        _javaScriptOutput << '\n';
        _javaScriptOutput.close();
    }

    if (_typeScriptOutput.isOpen() || _useStdout)
    {
        _typeScriptOutput << '\n';
        _typeScriptOutput.close();
    }
}

void
Slice::Gen::generate(const UnitPtr& p)
{
    validateJsMetadata(p);

    string module = getJavaScriptModule(p->findDefinitionContext(p->topLevelFile()));

    if (_useStdout)
    {
        _javaScriptOutput << "\n";
        _javaScriptOutput << "/** slice2js: " << _fileBase << ".js generated begin module:\"" << module << "\" **/";
        _javaScriptOutput << "\n";
    }
    printHeader(_javaScriptOutput);
    printGeneratedHeader(_javaScriptOutput, _fileBase + ".ice");

    _javaScriptOutput << sp;
    _javaScriptOutput << nl << "/* eslint-disable */";
    _javaScriptOutput << nl << "/* jshint ignore: start */";
    _javaScriptOutput << nl;

    {
        IncludeAggregationVisitor moduleVisitor;
        p->visit(&moduleVisitor);

        ImportVisitor importVisitor(_javaScriptOutput);
        p->visit(&importVisitor);
        set<string> importedModules = importVisitor.writeImports(p, moduleVisitor.nestedModulesByTopLevel());

        ExportsVisitor exportsVisitor(_javaScriptOutput, importedModules);
        p->visit(&exportsVisitor);
        set<string> exportedModules = exportsVisitor.exportedModules();

        set<string> seenModules = importedModules;
        seenModules.merge(exportedModules);

        TypesVisitor typesVisitor(_javaScriptOutput, module);
        p->visit(&typesVisitor);
    }

    if (_useStdout)
    {
        _javaScriptOutput << "\n";
        _javaScriptOutput << "/** slice2js: generated end **/";
        _javaScriptOutput << "\n";
    }

    if (_typeScript)
    {
        if (_useStdout)
        {
            _typeScriptOutput << "\n";
            _typeScriptOutput << "/** slice2js: " << _fileBase << ".d.ts generated begin module:\"" << module
                              << "\" **/";
            _typeScriptOutput << "\n";
        }
        printHeader(_typeScriptOutput);
        printGeneratedHeader(_typeScriptOutput, _fileBase + ".ice");

        IncludeAggregationVisitor moduleVisitor;
        p->visit(&moduleVisitor);

        map<string, set<string>> importedTypesByInclude = moduleVisitor.importedTypesByTopLevel(p->topLevelFile());

        TypeScriptImportVisitor importVisitor(_typeScriptOutput, importedTypesByInclude);
        p->visit(&importVisitor);
        map<string, string> importedTypes = importVisitor.writeImports();

        TypeScriptVisitor typeScriptVisitor(_typeScriptOutput, importedTypes, importedTypesByInclude);
        p->visit(&typeScriptVisitor);

        if (_useStdout)
        {
            _typeScriptOutput << "\n";
            _typeScriptOutput << "/** slice2js: generated end **/";
            _typeScriptOutput << "\n";
        }
    }
}

Slice::Gen::ImportVisitor::ImportVisitor(IceInternal::Output& out) : JsVisitor(out) {}

bool
Slice::Gen::ImportVisitor::visitClassDefStart(const ClassDefPtr&)
{
    _seenClass = true;
    return true;
}

bool
Slice::Gen::ImportVisitor::visitInterfaceDefStart(const InterfaceDefPtr&)
{
    _seenInterface = true;
    return true;
}

bool
Slice::Gen::ImportVisitor::visitStructStart(const StructPtr&)
{
    _seenStruct = true;
    return false;
}

void
Slice::Gen::ImportVisitor::visitOperation(const OperationPtr&)
{
    _seenOperation = true;
}

bool
Slice::Gen::ImportVisitor::visitExceptionStart(const ExceptionPtr&)
{
    _seenUserException = true;
    return false;
}

void
Slice::Gen::ImportVisitor::visitSequence(const SequencePtr& seq)
{
    BuiltinPtr builtin = dynamic_pointer_cast<Builtin>(seq->type());
    if (builtin)
    {
        switch (builtin->kind())
        {
            case Builtin::KindValue:
                _seenObjectSeq = true;
                break;
            case Builtin::KindObjectProxy:
                _seenObjectProxySeq = true;
                break;
            default:
                break;
        }
    }
    _seenSeq = true;
}

void
Slice::Gen::ImportVisitor::visitDictionary(const DictionaryPtr& dict)
{
    BuiltinPtr builtin = dynamic_pointer_cast<Builtin>(dict->valueType());
    if (builtin)
    {
        switch (builtin->kind())
        {
            case Builtin::KindValue:
                _seenObjectDict = true;
                break;
            case Builtin::KindObjectProxy:
                _seenObjectProxyDict = true;
                break;
            default:
                break;
        }
    }
    _seenDict = true;
}

void
Slice::Gen::ImportVisitor::visitEnum(const EnumPtr&)
{
    _seenEnum = true;
}

set<string>
Slice::Gen::ImportVisitor::writeImports(
    const UnitPtr& p,
    const std::map<std::string, std::map<std::string, std::set<std::string>>>& nestedModulesByTopLevel)
{
    // The JavaScript module we are building as specified by "js:module:" metadata.
    string jsModule = getJavaScriptModule(p->findDefinitionContext(p->topLevelFile()));

    // The set of top-level modules that we need to import in the generated JavaScript code.
    set<string> importedModules = {"Ice"};

    // The imports map maps JavaScript Modules to the set of Slice top-level modules that are imported from the
    // generated JavaScript code. The key is either the JavaScript module specified by "js:module:" metadata or the
    // relative path of the generated JavaScript file.
    map<string, set<string>> imports;

    // The JavaScript files that we import in generated code when building Ice.
    // For user generated code we import the "@zeroc/ice" package.
    set<string> jsIceImports;
    if (jsModule == "@zeroc/ice")
    {
        bool needStreamHelpers = false;
        if (_seenClass || _seenInterface || _seenObjectSeq || _seenObjectDict)
        {
            jsIceImports.insert("DefaultSliceLoader");
            jsIceImports.insert("Object");
            jsIceImports.insert("Value");
            jsIceImports.insert("TypeRegistry");
            needStreamHelpers = true;
        }

        if (_seenInterface)
        {
            jsIceImports.insert("DefaultSliceLoader");
            jsIceImports.insert("ObjectPrx");
            jsIceImports.insert("TypeRegistry");
        }

        if (_seenObjectProxySeq || _seenObjectProxyDict)
        {
            jsIceImports.insert("ObjectPrx");
        }

        if (_seenOperation)
        {
            jsIceImports.insert("Operation");
        }

        if (_seenStruct)
        {
            jsIceImports.insert("Struct");
        }

        if (_seenUserException)
        {
            jsIceImports.insert("DefaultSliceLoader");
            jsIceImports.insert("UserException");
            jsIceImports.insert("TypeRegistry");
        }

        if (_seenEnum)
        {
            jsIceImports.insert("EnumBase");
        }

        if (_seenDict || _seenObjectDict || _seenObjectProxyDict)
        {
            jsIceImports.insert("HashMap");
            jsIceImports.insert("HashUtil");
            needStreamHelpers = true;
        }

        if (_seenSeq || _seenObjectSeq)
        {
            needStreamHelpers = true;
        }

        if (needStreamHelpers)
        {
            jsIceImports.insert("StreamHelpers");
        }
    }
    else
    {
        imports["@zeroc/ice"] = set<string>{"Ice"};
    }

    // Group the top-level modules from each included file by their JavaScript import path.
    // Imports from files in the same js:module (or without a js:module) are tracked as "local" for aggregation.
    set<string> localImports;
    for (const auto& included : p->includeFiles())
    {
        set<string> sliceTopLevelModules = p->getTopLevelModules(included);

        // The JavaScript module corresponding to the 'js:module' metadata in the included file.
        string jsImportedModule = getJavaScriptModule(p->findDefinitionContext(included));

        if (jsModule == jsImportedModule || jsImportedModule.empty())
        {
            // For Slice modules mapped to the same JavaScript module, or Slice files that doesn't use "js:module".
            // We import them using their Slice include relative path.
            string f = toRelativePath(removeExtension(included) + ".js", p->topLevelFile());
            imports[f] = sliceTopLevelModules;
            localImports.insert(f);

            for (const auto& topLevelModule : sliceTopLevelModules)
            {
                importedModules.insert(topLevelModule);
            }
        }
        else
        {
            // When importing a generated Slice file from a different JavaScript module, we need to import all
            // top-level modules from the included file.

            if (imports.find(jsImportedModule) == imports.end())
            {
                imports[jsImportedModule] = set<string>{};
            }

            for (const auto& topLevelModule : sliceTopLevelModules)
            {
                imports[jsImportedModule].insert(topLevelModule);
            }
        }
    }

    // We first import the Ice runtime
    if (jsModule == "@zeroc/ice")
    {
        for (const string& m : jsIceImports)
        {
            _out << nl << "import * as Ice_" << m << " from \"../Ice/" << m << ".js\";";
        }

        for (const auto& [imported, modules] : imports)
        {
            if (modules.find("Ice") != modules.end())
            {
                _out << nl << "import { Ice as Ice_" << importPathToIdentifier(imported) << " } from \"" << imported
                     << "\"";
            }
        }

        _out << sp;
        _out << nl << "const Ice = {";
        _out.inc();
        for (const string& m : jsIceImports)
        {
            _out << nl << "...Ice_" << m << ",";
        }

        for (auto& [imported, modules] : imports)
        {
            if (modules.find("Ice") != modules.end())
            {
                _out << nl << "...Ice_" << importPathToIdentifier(imported) << ",";

                modules.erase("Ice");
            }
        }

        _out.dec();
        _out << nl << "};";
    }
    else
    {
        // Import the required modules from "@zeroc/ice" JavaScript module.
        auto i = imports.find("@zeroc/ice");
        assert(i != imports.end());
        _out << nl << "import { ";
        for (auto j = i->second.begin(); j != i->second.end();)
        {
            _out << (*j);
            if (++j != i->second.end())
            {
                _out << ", ";
            }
        }
        _out << " } from \"@zeroc/ice\";";
        imports.erase(i);
    }

    // Process the remaining imports, after importing "@zeroc/ice".
    set<string> aggregatedModules;
    _out << sp;
    for (const auto& [jsImportedModule, topLevelModules] : imports)
    {
        if (topLevelModules.empty())
        {
            // If topLevelModules is empty it means that the included file is not required by the generated code.
            continue;
        }

        bool isLocal = localImports.count(jsImportedModule) > 0;
        _out << nl << "import { ";
        _out.inc();
        for (const auto& topLevelModule : topLevelModules)
        {
            _out << nl << topLevelModule << " as " << importPathToIdentifier(jsImportedModule) << "_" << topLevelModule
                 << ", ";
            if (isLocal)
            {
                aggregatedModules.insert(topLevelModule);
            }
        }
        _out.dec();
        _out << "} from \"" << jsImportedModule << "\"";
    }

    // Aggregate top-level modules from local imports into a single module object.
    // External js:module imports are not aggregated; their aliased names are used directly.
    for (const string& m : aggregatedModules)
    {
        _out << sp;
        _out << nl << "const " << m << " = {";
        _out.inc();
        for (const auto& [jsImportedModule, topLevelModules] : imports)
        {
            if (localImports.count(jsImportedModule) > 0 && topLevelModules.find(m) != topLevelModules.end())
            {
                _out << nl << "..." << importPathToIdentifier(jsImportedModule) << "_" << m << ",";
            }
        }
        _out.dec();
        _out << nl << "};";
    }

    // Aggregate the sub-modules.
    // If module Foo.Bar was imported from multiple files, we need to aggregate them into a single Foo.Bar module.
    // This must be done in a top-down order.
    // Foo.Bar = { ...Foo_Bar_1.Bar, ...Foo_Bar_2.Bar, ... }
    // Foo.Bar.Baz = { ...Foo_Bar_1.Bar.Baz, ...Foo_Bar_2.Bar.Baz, ... }
    auto it = nestedModulesByTopLevel.find(p->topLevelFile());
    if (it != nestedModulesByTopLevel.end())
    {
        auto& nestedModulesByIncludeKey = it->second;
        // All the unique nested modules that need to be aggregated.
        set<string> uniqueNestedModules;
        // Gather all the include keys that define a given nested module.
        map<string, set<string>> nestedModuleSources;

        for (const auto& [includeKey, nestedModules] : nestedModulesByIncludeKey)
        {
            for (const string& nestedModule : nestedModules)
            {
                uniqueNestedModules.insert(nestedModule);
                nestedModuleSources[nestedModule].insert(includeKey);
            }
        }

        // Sort by nesting depth (number of dots) to aggregate parent modules before their children.
        std::vector<std::string> sortedNestedModules(uniqueNestedModules.begin(), uniqueNestedModules.end());
        std::sort(
            sortedNestedModules.begin(),
            sortedNestedModules.end(),
            [](const std::string& a, const std::string& b)
            {
                auto dotsA = std::count(a.begin(), a.end(), '.');
                auto dotsB = std::count(b.begin(), b.end(), '.');

                return dotsA != dotsB ? dotsA < dotsB : a < b;
            });

        for (const string& nestedModule : sortedNestedModules)
        {
            if (nestedModuleSources[nestedModule].size() == 1)
            {
                // No need to aggregate modules defined in a single file.
                continue;
            }

            _out << sp;
            _out << nl << nestedModule << " = {";
            _out.inc();
            for (const string& includeKey : nestedModuleSources[nestedModule])
            {
                _out << nl << "..." << importPathToIdentifier(includeKey) << "_" << nestedModule << ",";
            }
            _out.dec();
            _out << nl << "};";
        }
    }

    return importedModules;
}

Slice::Gen::ExportsVisitor::ExportsVisitor(::IceInternal::Output& out, std::set<std::string> importedModules)
    : JsVisitor(out),
      _importedModules(std::move(importedModules))
{
}

bool
Slice::Gen::ExportsVisitor::visitModuleStart(const ModulePtr& p)
{
    //
    // For a top-level module we write the following:
    //
    // export const Foo = {}; // When Foo is a module not previously imported
    // export { Foo }; // When Foo is a module previously imported
    //
    // For a nested module we write
    //
    // Foo.Bar = Foo.Bar || {};
    //
    const string scoped = p->mappedScoped(".");
    if (_exportedModules.insert(scoped).second)
    {
        const string scope = p->mappedScope(".");
        const string name = p->mappedName();

        vector<string> subs;
        IceInternal::splitString(string_view{name}, ".", subs);
        const string top = subs.empty() ? name : subs[0];
        string prefix = scope + top;

        _out << nl;

        if (p->isTopLevel())
        {
            if (_importedModules.find(scoped) == _importedModules.end())
            {
                // declare and export
                _out << "export const " << prefix << " = {};";
            }
            else
            {
                // re-export only
                _out << "export { " << prefix << " };";
            }
        }
        else
        {
            // nested module init (no export)
            _out << prefix << " = " << prefix << " || {};";
        }

        // initialize nested levels for remapped modules if any
        for (size_t i = 1; i < subs.size(); ++i)
        {
            prefix += "." + subs[i];
            _out << nl << prefix << " = " << prefix << " || {};";
        }
    }
    return true;
}

set<string>
Slice::Gen::ExportsVisitor::exportedModules() const
{
    return _exportedModules;
}

Slice::Gen::TypesVisitor::TypesVisitor(IceInternal::Output& out, string jsModule) : JsVisitor(out)
{
    _jsModule = std::move(jsModule);
}

bool
Slice::Gen::TypesVisitor::visitClassDefStart(const ClassDefPtr& p)
{
    const string scopedName = p->mappedScoped(".");
    ClassDefPtr base = p->base();
    string baseRef = base ? resolveJsScope(base, _jsModule) : "Ice.Value";

    const DataMemberList dataMembers = p->dataMembers();
    const DataMemberList optionalMembers = p->orderedOptionalDataMembers();

    vector<string> allParamNames;
    for (const auto& member : p->allDataMembers())
    {
        allParamNames.push_back(member->mappedName());
    }

    vector<string> baseParamNames;
    DataMemberList baseDataMembers;

    if (base)
    {
        baseDataMembers = base->allDataMembers();
        for (const auto& baseDataMember : baseDataMembers)
        {
            baseParamNames.push_back(baseDataMember->mappedName());
        }
    }

    _out << sp;
    writeDocSummary(p, {.includeRemarks = false});
    _out << nl << scopedName << " = class";
    _out << " extends " << baseRef;

    _out << sb;
    if (!dataMembers.empty())
    {
        _out << nl << "constructor" << spar;
        for (const auto& baseDataMember : baseDataMembers)
        {
            _out << baseDataMember->mappedName();
        }

        writeOneShotConstructorArguments(dataMembers);

        _out << epar << sb;
        _out << nl << "super" << spar << baseParamNames << epar << ';';
        for (const auto& member : dataMembers)
        {
            const string memberName = member->mappedName();
            _out << nl << "this." << memberName << " = " << memberName << ';';
        }
        _out << eb;
    }

    if (p->compactId() != -1)
    {
        _out << sp;
        _out << nl << "static get _iceCompactId()";
        _out << sb;
        _out << nl << "return " << p->compactId() << ";";
        _out << eb;
    }

    if (!dataMembers.empty())
    {
        _out << sp;
        _out << nl << "_iceWriteMemberImpl(ostr)";
        _out << sb;
        writeMarshalDataMembers(dataMembers, optionalMembers);
        _out << eb;

        _out << sp;
        _out << nl << "_iceReadMemberImpl(istr)";
        _out << sb;
        writeUnmarshalDataMembers(dataMembers, optionalMembers);
        _out << eb;
    }
    _out << eb << ";";

    _out << sp;

    _out << nl << "Ice.defineClass(" << scopedName << ", \"" << p->scoped() << "\"";
    if (p->compactId() != -1)
    {
        _out << ", " << p->compactId();
    }
    _out << ");";

    _out << sp;
    _out << nl << "Ice.TypeRegistry.declareValueType(\"" << scopedName << "\", " << scopedName << ");";

    return false;
}

bool
Slice::Gen::TypesVisitor::visitInterfaceDefStart(const InterfaceDefPtr& p)
{
    const string serviceType = p->mappedScoped(".");
    const string proxyType = serviceType + "Prx";
    const string flattenedIdsName = "iceC" + p->mappedScoped("_", true) + "_ids";

    InterfaceList bases = p->bases();
    StringList ids = p->ids();

    _out << sp;
    _out << nl << "const " << flattenedIdsName << " = [";
    _out.inc();

    for (auto q = ids.begin(); q != ids.end(); ++q)
    {
        if (q != ids.begin())
        {
            _out << ',';
        }
        _out << nl << '"' << *q << '"';
    }

    _out.dec();
    _out << nl << "];";

    //
    // Define servant and proxy types
    //

    _out << sp;
    writeDocSummary(p, {.generateDeprecated = false, .includeRemarks = false});
    _out << nl << serviceType << " = class extends Ice.Object";
    _out << sb;

    if (!bases.empty())
    {
        _out << sp;
        _out << nl << "static get _iceImplements()";
        _out << sb;
        _out << nl << "return [";
        _out.inc();
        for (auto q = bases.begin(); q != bases.end();)
        {
            InterfaceDefPtr base = *q;
            _out << nl << resolveJsScope(base, _jsModule);
            if (++q != bases.end())
            {
                _out << ",";
            }
        }
        _out.dec();
        _out << nl << "];";
        _out << eb;
    }
    _out << eb << ";";

    //
    // Generate a proxy class for interfaces
    //
    _out << sp;
    writeDocSummary(p, {.includeRemarks = false});
    _out << nl << proxyType << " = class extends Ice.ObjectPrx";
    _out << sb;

    if (!bases.empty())
    {
        _out << sp;
        _out << nl << "static get _implements()";
        _out << sb;
        _out << nl << "return [";

        _out.inc();
        for (auto q = bases.begin(); q != bases.end();)
        {
            InterfaceDefPtr base = *q;

            _out << nl << resolveJsScope(base, _jsModule) + "Prx";
            if (++q != bases.end())
            {
                _out << ",";
            }
        }
        _out.dec();
        _out << "];";
        _out << eb;
    }

    _out << eb << ";";
    _out << sp;
    _out << nl << "Ice.TypeRegistry.declareProxyType(\"" << proxyType << "\", " << proxyType << ");";

    _out << sp;
    _out << nl << "Ice.defineOperations(";
    _out.inc();
    _out << nl << serviceType << "," << nl << proxyType << "," << nl << flattenedIdsName << "," << nl << "\""
         << p->scoped() << "\"";

    const OperationList ops = p->operations();
    if (!ops.empty())
    {
        _out << ',';
        _out << sb;
        for (auto q = ops.begin(); q != ops.end(); ++q)
        {
            if (q != ops.begin())
            {
                _out << ',';
            }

            const OperationPtr& op = *q;
            const string opName = op->mappedName();
            const ParameterList paramList = op->parameters();
            const TypePtr ret = op->returnType();
            ParameterList inParams, outParams;
            for (const auto& pli : paramList)
            {
                if (pli->isOutParam())
                {
                    outParams.push_back(pli);
                }
                else
                {
                    inParams.push_back(pli);
                }
            }

            //
            // Each operation descriptor is a property. The key is the "on-the-wire"
            // name, and the value is an array consisting of the following elements:
            //
            //  0: servant method name in case of a keyword conflict (e.g., "_while"),
            //     otherwise an empty string
            //  1: mode (undefined == Normal or int)
            //  2: format (undefined == Default or int)
            //  3: return type (undefined if void, or [type, tag])
            //  4: in params (undefined if none, or array of [type, tag])
            //  5: out params (undefined if none, or array of [type, tag])
            //  6: exceptions (undefined if none, or array of types)
            //  7: sends classes (true or undefined)
            //  8: returns classes (true or undefined)
            //
            _out << nl << "\"" << op->name() << "\": ["; // Operation name over-the-wire.

            if (opName != op->name())
            {
                _out << "\"" << opName << "\""; // Native method name.
            }
            _out << ", " << sliceModeToIceMode(op->mode()); // Mode.
            _out << ", ";

            if (op->format())
            {
                _out << opFormatTypeToString(*op->format()); // Format.
            }
            _out << ", ";

            //
            // Return type.
            //
            if (ret)
            {
                _out << '[' << encodeTypeForOperation(ret);
                const bool isObj = ret->isClassType();
                if (isObj)
                {
                    _out << ", true";
                }
                if (op->returnIsOptional())
                {
                    if (!isObj)
                    {
                        _out << ", ";
                    }
                    _out << ", " << op->returnTag();
                }
                _out << ']';
            }
            _out << ", ";

            //
            // In params.
            //
            if (!inParams.empty())
            {
                _out << '[';
                for (auto pli = inParams.begin(); pli != inParams.end(); ++pli)
                {
                    if (pli != inParams.begin())
                    {
                        _out << ", ";
                    }
                    TypePtr t = (*pli)->type();
                    _out << '[' << encodeTypeForOperation(t);
                    const bool isObj = t->isClassType();
                    if (isObj)
                    {
                        _out << ", true";
                    }
                    if ((*pli)->optional())
                    {
                        if (!isObj)
                        {
                            _out << ", ";
                        }
                        _out << ", " << (*pli)->tag();
                    }
                    _out << ']';
                }
                _out << ']';
            }
            _out << ", ";

            //
            // Out params.
            //
            if (!outParams.empty())
            {
                _out << '[';
                for (auto pli = outParams.begin(); pli != outParams.end(); ++pli)
                {
                    if (pli != outParams.begin())
                    {
                        _out << ", ";
                    }
                    TypePtr t = (*pli)->type();
                    _out << '[' << encodeTypeForOperation(t);
                    const bool isObj = t->isClassType();
                    if (isObj)
                    {
                        _out << ", true";
                    }
                    if ((*pli)->optional())
                    {
                        if (!isObj)
                        {
                            _out << ", ";
                        }
                        _out << ", " << (*pli)->tag();
                    }
                    _out << ']';
                }
                _out << ']';
            }
            _out << ",";

            // User exceptions.
            // Arrange exceptions into most-derived to least-derived order. If we don't
            // do this, a base exception handler can appear before a derived exception
            // handler, causing compiler warnings and resulting in the base exception
            // being marshaled instead of the derived exception.
            ExceptionList throws = op->throws();
            throws.sort(Slice::DerivedToBaseCompare());

            if (throws.empty())
            {
                _out << " ";
            }
            else
            {
                _out << nl << '[';
                _out.inc();
                for (auto eli = throws.begin(); eli != throws.end(); ++eli)
                {
                    if (eli != throws.begin())
                    {
                        _out << ',';
                    }
                    _out << nl << resolveJsScope(*eli, _jsModule);
                }
                _out.dec();
                _out << nl << ']';
            }
            _out << ", ";

            if (op->sendsClasses())
            {
                _out << "true";
            }
            _out << ", ";

            if (op->returnsClasses())
            {
                _out << "true";
            }

            _out << ']';
        }
        _out << eb;
    }
    _out << ");";
    _out.dec();

    return false;
}

void
Slice::Gen::TypesVisitor::visitSequence(const SequencePtr& p)
{
    // Stream helpers for sequences are lazy initialized as the required types might not be available until later.
    const string helperName = p->mappedScoped(".") + "Helper";
    const TypePtr type = p->type();
    const bool fixed = !type->isVariableLength();

    _out << sp;
    _out << nl << helperName << " = Ice.StreamHelpers.generateSeqHelper(" << getHelper(type, _jsModule) << ", "
         << (fixed ? "true" : "false");
    if (type->isClassType())
    {
        // String literal used as a type registry key - no alias resolution needed.
        _out << ", \"" << typeToJsString(type) << "\"";
    }

    _out << ");";
}

bool
Slice::Gen::TypesVisitor::visitExceptionStart(const ExceptionPtr& p)
{
    const string scopedName = p->mappedScoped(".");
    const ExceptionPtr base = p->base();
    string baseRef;

    if (base)
    {
        baseRef = resolveJsScope(base, _jsModule);
    }
    else
    {
        baseRef = "Ice.UserException";
    }

    const DataMemberList dataMembers = p->dataMembers();
    const DataMemberList optionalMembers = p->orderedOptionalDataMembers();

    _out << sp;
    writeDocSummary(p, {.includeRemarks = false});
    _out << nl << scopedName << " = class extends " << baseRef;
    _out << sb;

    if (!dataMembers.empty())
    {
        vector<string> baseParamNames;
        DataMemberList baseDataMembers;

        if (base)
        {
            baseDataMembers = base->allDataMembers();
            for (const auto& baseDataMember : baseDataMembers)
            {
                baseParamNames.push_back(baseDataMember->mappedName());
            }
        }

        _out << nl << "constructor" << spar;

        for (const auto& baseDataMember : baseDataMembers)
        {
            _out << baseDataMember->mappedName();
        }

        writeOneShotConstructorArguments(dataMembers);

        _out << epar;
        _out << sb;
        _out << nl << "super" << spar << baseParamNames << epar << ';';
        for (const auto& member : dataMembers)
        {
            const string memberName = member->mappedName();
            _out << nl << "this." << memberName << " = " << memberName << ';';
        }
        _out << eb;
    }
    // else inherit base constructor

    _out << sp;
    _out << nl << "static get _parent()";
    _out << sb;
    _out << nl << "return " << baseRef << ";";
    _out << eb;

    // TODO: use methods added by Ice.defineClass instead.
    _out << sp;
    _out << nl << "static get _ice_id()";
    _out << sb;
    _out << nl << "return \"" << p->scoped() << "\";";
    _out << eb;

    _out << sp;
    _out << nl << "_mostDerivedType()";
    _out << sb;
    _out << nl << "return " << scopedName << ";";
    _out << eb;

    if (!dataMembers.empty())
    {
        _out << sp;
        _out << nl << "_writeMemberImpl(ostr)";
        _out << sb;
        writeMarshalDataMembers(dataMembers, optionalMembers);
        _out << eb;

        _out << sp;
        _out << nl << "_readMemberImpl(istr)";
        _out << sb;
        writeUnmarshalDataMembers(dataMembers, optionalMembers);
        _out << eb;
    }

    if (p->usesClasses() && !(base && base->usesClasses()))
    {
        _out << sp;
        _out << nl << "_usesClasses()";
        _out << sb;
        _out << nl << "return true;";
        _out << eb;
    }

    _out << eb << ";";
    _out << sp;

    _out << nl << "Ice.defineClass(" << scopedName << ", \"" << p->scoped() << "\");";

    return false;
}

bool
Slice::Gen::TypesVisitor::visitStructStart(const StructPtr& p)
{
    const string scopedName = p->mappedScoped(".");
    const DataMemberList dataMembers = p->dataMembers();

    _out << sp;
    writeDocSummary(p, {.includeRemarks = false});
    _out << nl << scopedName << " = class";
    _out << sb;

    _out << nl << "constructor" << spar;

    writeOneShotConstructorArguments(dataMembers);

    _out << epar;
    _out << sb;
    for (const auto& member : dataMembers)
    {
        const string memberName = member->mappedName();
        _out << nl << "this." << memberName << " = " << memberName << ';';
    }
    _out << eb;

    _out << sp;
    _out << nl << "_write(ostr)";
    _out << sb;
    writeMarshalDataMembers(dataMembers, DataMemberList());
    _out << eb;

    _out << sp;
    _out << nl << "_read(istr)";
    _out << sb;
    writeUnmarshalDataMembers(dataMembers, DataMemberList());
    _out << eb;

    _out << sp;
    _out << nl << "static get minWireSize()";
    _out << sb;
    _out << nl << "return  " << p->minWireSize() << ";";
    _out << eb;

    _out << eb << ";";

    //
    // Only generate hashCode if this structure type is a legal dictionary key type.
    //
    bool legalKeyType = Dictionary::isLegalKeyType(p);

    _out << sp;
    _out << nl << "Ice.defineStruct(" << scopedName << ", " << (legalKeyType ? "true" : "false") << ", "
         << (p->isVariableLength() ? "true" : "false") << ");";
    return false;
}

void
Slice::Gen::TypesVisitor::visitDictionary(const DictionaryPtr& p)
{
    const TypePtr keyType = p->keyType();
    const TypePtr valueType = p->valueType();

    // For some key types, we have to use an equals() method to compare keys
    // rather than the native comparison operators.
    bool keyUseEquals = false;
    BuiltinPtr b = dynamic_pointer_cast<Builtin>(keyType);
    if (dynamic_pointer_cast<Struct>(keyType))
    {
        keyUseEquals = true;
    }

    // Stream helpers for dictionaries of objects are lazy initialized
    // as the required object type might not be available until later.
    const string scopedName = p->mappedScoped(".");
    const string helperName = scopedName + "Helper";
    bool fixed = !keyType->isVariableLength() && !valueType->isVariableLength();

    _out << sp;
    _out << nl << "[" << scopedName << ", " << helperName << "] = Ice.defineDictionary("
         << getHelper(keyType, _jsModule) << ", " << getHelper(valueType, _jsModule) << ", "
         << (fixed ? "true" : "false") << ", " << (keyUseEquals ? "Ice.HashMap.compareEquals" : "undefined");

    if (valueType->isClassType())
    {
        // String literal used as a type registry key - no alias resolution needed.
        _out << ", \"" << typeToJsString(valueType) << "\"";
    }
    _out << ");";
}

void
Slice::Gen::TypesVisitor::visitEnum(const EnumPtr& p)
{
    const string scopedName = p->mappedScoped(".");
    _out << sp;
    writeDocSummary(p, {.includeRemarks = false});
    _out << nl << scopedName << " = Ice.defineEnum([";
    _out.inc();
    _out << nl;

    const EnumeratorList enumerators = p->enumerators();
    int i = 0;
    for (auto en = enumerators.begin(); en != enumerators.end(); ++en)
    {
        if (en != enumerators.begin())
        {
            if (++i % 5 == 0)
            {
                _out << ',' << nl;
            }
            else
            {
                _out << ", ";
            }
        }
        _out << "['" << (*en)->mappedName() << "', " << (*en)->value() << ']';
    }
    _out << "]);";
    _out.dec();

    //
    // EnumBase provides an equals() method
    //
}

void
Slice::Gen::TypesVisitor::visitConst(const ConstPtr& p)
{
    string scope = p->mappedScope(".");
    scope.pop_back(); // Remove the trailing '.' from the scope.

    _out << sp;
    _out << nl << "Object.defineProperty(" << scope << ", '" << p->mappedName() << "', {";
    _out.inc();
    _out << nl << "enumerable: true,";
    _out << nl << "value: ";
    _out << writeConstantValue(p->type(), p->valueType(), p->value());
    _out.dec();
    _out << nl << "});";
}

string
Slice::Gen::TypesVisitor::encodeTypeForOperation(const TypePtr& type)
{
    assert(type);

    static const char* builtinTable[] = {
        "0", // byte
        "1", // bool
        "2", // short
        "3", // int
        "4", // long
        "5", // float
        "6", // double
        "7", // string
        "8", // Ice.ObjectPrx
        "9", // Ice.Value
    };

    BuiltinPtr builtin = dynamic_pointer_cast<Builtin>(type);
    if (builtin)
    {
        return builtinTable[builtin->kind()];
    }

    InterfaceDeclPtr proxy = dynamic_pointer_cast<InterfaceDecl>(type);
    if (proxy)
    {
        // String literal used as a type registry key - no alias resolution needed.
        return "\"" + proxy->mappedScoped(".") + "Prx" + "\"";
    }

    SequencePtr seq = dynamic_pointer_cast<Sequence>(type);
    if (seq)
    {
        return resolveJsScope(seq, _jsModule) + "Helper";
    }

    DictionaryPtr d = dynamic_pointer_cast<Dictionary>(type);
    if (d)
    {
        return resolveJsScope(d, _jsModule) + "Helper";
    }

    EnumPtr e = dynamic_pointer_cast<Enum>(type);
    if (e)
    {
        return resolveJsScope(e, _jsModule) + "._helper";
    }

    StructPtr st = dynamic_pointer_cast<Struct>(type);
    if (st)
    {
        return resolveJsScope(st, _jsModule);
    }

    ClassDeclPtr cl = dynamic_pointer_cast<ClassDecl>(type);
    if (cl)
    {
        // String literal used as a type registry key - no alias resolution needed.
        return "\"" + cl->mappedScoped(".") + "\"";
    }

    return "???";
}

Slice::Gen::TypeScriptImportVisitor::TypeScriptImportVisitor(
    IceInternal::Output& out,
    std::map<std::string, std::set<std::string>> importedTypesByInclude)
    : JsVisitor(out),
      _importedTypesByInclude(std::move(importedTypesByInclude))
{
}

void
Slice::Gen::TypeScriptImportVisitor::addImport(const ContainedPtr& definition)
{
    const string definitionId = definition->mappedScoped(".");

    string jsImportedModule = getJavaScriptModule(definition->definitionContext());
    if (jsImportedModule.empty())
    {
        string definedIn = definition->getMetadataArgs("js:defined-in").value_or("");
        if (!definedIn.empty())
        {
            _importedTypes[definitionId] = "__module_" + importPathToIdentifier(definedIn) + ".";
            _importedModules.insert(removeExtension(definedIn) + ".js");
        }
        else
        {
            const string filename = definition->definitionContext()->filename();
            if (filename == _filename)
            {
                // For types defined in the same unit we use a __global_ prefix to refer to the global scope.
                _importedTypes[definitionId] = "__global_";
            }
            else
            {
                string f = toRelativePath(filename, _filename);
                _importedTypes[definitionId] = "__module_" + importPathToIdentifier(f) + ".";
                _importedModules.insert(removeExtension(f) + ".js");
            }
        }
    }
    else if (_module != jsImportedModule)
    {
        _importedTypes[definitionId] = "__module_" + importPathToIdentifier(jsImportedModule) + ".";
        _importedModules.insert(jsImportedModule);
    }
    else
    {
        // For types defined in the same module, we use a __global_ prefix, to refer to the global scope.
        _importedTypes[definitionId] = "__global_";
    }
}

bool
Slice::Gen::TypeScriptImportVisitor::visitUnitStart(const UnitPtr& unit)
{
    _module = getJavaScriptModule(unit->findDefinitionContext(unit->topLevelFile()));
    _filename = unit->topLevelFile();
    if (_module != "@zeroc/ice")
    {
        _importedModules.insert("@zeroc/ice");
    }

    // Add imports for files that have types we need to re-export.
    // The keys are normalized include paths (e.g., "./First.ice") or js:module names (e.g., "@zeroc/ice").
    for (const auto& [includeKey, types] : _importedTypesByInclude)
    {
        if (includeKey[0] == '.' || includeKey[0] == '/')
        {
            // Relative or absolute path - convert .ice to .js
            _importedModules.insert(removeExtension(includeKey) + ".js");
        }
        else
        {
            // js:module name - use as-is
            _importedModules.insert(includeKey);
        }
    }
    return true;
}

bool
Slice::Gen::TypeScriptImportVisitor::visitClassDefStart(const ClassDefPtr& p)
{
    // Add imports required for the base class type.
    ClassDefPtr base = p->base();
    if (base)
    {
        addImport(dynamic_pointer_cast<Contained>(base));
    }

    // Add imports required for data members types.
    for (const auto& dataMember : p->allDataMembers())
    {
        ContainedPtr type = dynamic_pointer_cast<Contained>(dataMember->type());
        if (type)
        {
            addImport(type);
        }
    }
    return false;
}

bool
Slice::Gen::TypeScriptImportVisitor::visitInterfaceDefStart(const InterfaceDefPtr& p)
{
    // Add imports required for base interfaces types.
    for (const auto& base : p->bases())
    {
        addImport(base);
    }

    // Add imports required for operation parameters and return types.
    for (const auto& op : p->allOperations())
    {
        auto ret = dynamic_pointer_cast<Contained>(op->returnType());
        if (ret)
        {
            addImport(ret);
        }

        for (const auto& param : op->parameters())
        {
            auto type = dynamic_pointer_cast<Contained>(param->type());
            if (type)
            {
                addImport(type);
            }
        }
    }
    return false;
}

bool
Slice::Gen::TypeScriptImportVisitor::visitStructStart(const StructPtr& p)
{
    // Add imports required for data member types.
    for (const auto& dataMember : p->dataMembers())
    {
        auto type = dynamic_pointer_cast<Contained>(dataMember->type());
        if (type)
        {
            addImport(type);
        }
    }
    return false;
}

bool
Slice::Gen::TypeScriptImportVisitor::visitExceptionStart(const ExceptionPtr& p)
{
    // Add imports required for base exception types.
    if (ExceptionPtr base = p->base())
    {
        addImport(dynamic_pointer_cast<Contained>(base));
    }

    for (const auto& dataMember : p->allDataMembers())
    {
        auto type = dynamic_pointer_cast<Contained>(dataMember->type());
        if (type)
        {
            addImport(type);
        }
    }
    return false;
}

void
Slice::Gen::TypeScriptImportVisitor::visitSequence(const SequencePtr& seq)
{
    // Add import required for the sequence element type.
    auto type = dynamic_pointer_cast<Contained>(seq->type());
    if (type)
    {
        addImport(type);
    }
}

void
Slice::Gen::TypeScriptImportVisitor::visitDictionary(const DictionaryPtr& dict)
{
    //
    // Add imports required for the dictionary key and value types
    //
    auto keyType = dynamic_pointer_cast<Contained>(dict->keyType());
    if (keyType)
    {
        addImport(keyType);
    }

    auto valueType = dynamic_pointer_cast<Contained>(dict->valueType());
    if (valueType)
    {
        addImport(valueType);
    }
}

std::map<std::string, std::string>
Slice::Gen::TypeScriptImportVisitor::writeImports()
{
    _out << sp;
    for (const auto& moduleName : _importedModules)
    {
        _out << nl << "import * as __module_" << importPathToIdentifier(moduleName) << " from \"" << moduleName
             << "\";";
    }
    return _importedTypes;
}

string
Slice::Gen::TypeScriptVisitor::importPrefix(const string& s) const
{
    const auto it = _importedTypes.find(s);
    if (it != _importedTypes.end())
    {
        return it->second;
    }
    return "";
}

string
Slice::Gen::TypeScriptVisitor::typeToTsString(const TypePtr& type, bool nullable, bool forParameter, bool optional)
    const
{
    if (!type)
    {
        assert(!optional);
        return "void";
    }

    string t;

    static const char* typeScriptBuiltinTable[] = {
        "number",  // byte
        "boolean", // bool
        "number",  // short
        "number",  // int
        "bigint",  // long
        "number",  // float
        "number",  // double
        "string",
        "Ice.ObjectPrx",
        "Ice.Value"};

    BuiltinPtr builtin = dynamic_pointer_cast<Builtin>(type);
    if (builtin)
    {
        switch (builtin->kind())
        {
            case Builtin::KindLong:
            {
                t = typeScriptBuiltinTable[builtin->kind()];
                // For Slice long parameters we accept both number and bigint.
                if (forParameter)
                {
                    t += " | number";
                }
                break;
            }
            case Builtin::KindObjectProxy:
            case Builtin::KindValue:
            {
                t = _iceImportPrefix + typeScriptBuiltinTable[builtin->kind()];
                if (nullable)
                {
                    t += " | null";
                }
                break;
            }
            default:
            {
                t = typeScriptBuiltinTable[builtin->kind()];
                break;
            }
        }
    }

    ClassDeclPtr cl = dynamic_pointer_cast<ClassDecl>(type);
    if (cl)
    {
        const string scopedName = cl->mappedScoped(".");
        t = importPrefix(scopedName) + scopedName;
        if (nullable)
        {
            t += " | null";
        }
    }

    InterfaceDeclPtr proxy = dynamic_pointer_cast<InterfaceDecl>(type);
    if (proxy)
    {
        const string scopedName = proxy->mappedScoped(".");
        t = importPrefix(scopedName) + scopedName + "Prx";
        if (nullable)
        {
            t += " | null";
        }
    }

    SequencePtr seq = dynamic_pointer_cast<Sequence>(type);
    if (seq)
    {
        builtin = dynamic_pointer_cast<Builtin>(seq->type());
        if (builtin && builtin->kind() == Builtin::KindByte)
        {
            t = "Uint8Array";
        }
        else
        {
            const string seqType = typeToTsString(seq->type(), nullable);
            if (seqType.find('|') != string::npos)
            {
                t = "(" + seqType + ")[]";
            }
            else
            {
                t = seqType + "[]";
            }
        }
    }

    DictionaryPtr d = dynamic_pointer_cast<Dictionary>(type);
    if (d)
    {
        const TypePtr keyType = d->keyType();
        builtin = dynamic_pointer_cast<Builtin>(keyType);
        ostringstream os;
        if (dynamic_pointer_cast<Struct>(keyType))
        {
            os << _iceImportPrefix << "Ice.HashMap<";
        }
        else
        {
            os << "Map<";
        }
        os << typeToTsString(d->keyType(), nullable) << ", " << typeToTsString(d->valueType(), nullable) << ">";
        t = os.str();
    }

    ContainedPtr contained = dynamic_pointer_cast<Contained>(type);
    if (t.empty() && contained)
    {
        const string scopedName = contained->mappedScoped(".");
        t = importPrefix(scopedName) + scopedName;
    }

    // For optional parameters we use "?:" instead of Ice.Optional<T>
    if (optional)
    {
        t += " | undefined";
    }

    return t;
}

Slice::Gen::TypeScriptVisitor::TypeScriptVisitor(
    IceInternal::Output& out,
    std::map<std::string, std::string> importedTypes,
    std::map<std::string, std::set<std::string>> importedTypesByInclude)
    : JsVisitor(out),
      _importedTypes(std::move(importedTypes)),
      _importedTypesByInclude(std::move(importedTypesByInclude))
{
}

bool
Slice::Gen::TypeScriptVisitor::visitUnitStart(const UnitPtr& unit)
{
    _module = getJavaScriptModule(unit->findDefinitionContext(unit->topLevelFile()));
    _iceImportPrefix = _module == "@zeroc/ice" ? "" : "__module__zeroc_ice.";
    if (!_module.empty())
    {
        _out << sp;
        _out << nl << "declare module \"" << _module << "\"" << sb;
    }
    return true;
}

void
Slice::Gen::TypeScriptVisitor::visitUnitEnd(const UnitPtr&)
{
    set<string> globalModules;
    for (const auto& [key, value] : _importedTypes)
    {
        if (value == "__global_")
        {
            // find the top level module for the type.
            auto pos = key.find('.');
            assert(pos != string::npos);

            globalModules.insert(key.substr(0, pos));
        }
    }

    for (const auto& moduleName : globalModules)
    {
        _out << nl << "import __global_" << moduleName << " = " << moduleName << ";";
    }

    if (!_module.empty())
    {
        _out << eb; // module end
    }
}

bool
Slice::Gen::TypeScriptVisitor::visitModuleStart(const ModulePtr& p)
{
    // Track visited modules for nested module export detection.
    _visitedModules.insert(p->mappedScoped("."));

    if (p->isTopLevel() && _module.empty())
    {
        _out << sp;
        _out << nl << "export namespace " << p->mappedName() << sb;
    }
    else
    {
        _out << nl << "namespace " << p->mappedName() << sb;
    }

    // Re-export imported types under the module namespace.
    for (const auto& [key, value] : _importedTypes)
    {
        auto pos = key.rfind('.');
        if (pos != string::npos && key.substr(0, pos) == p->mappedScoped(".") && value.find("__module_") == 0)
        {
            _out << sp;
            _out << nl << "export import " << key.substr(pos + 1) << " = " << value << key << ";";
        }
    }

    return true;
}

void
Slice::Gen::TypeScriptVisitor::visitModuleEnd(const ModulePtr& p)
{
    // Generate export imports for types in nested modules that don't exist in the current file.
    writeNestedModuleExports(p->mappedScoped("."));

    _out << eb; // namespace end
}

bool
Slice::Gen::TypeScriptVisitor::visitClassDefStart(const ClassDefPtr& p)
{
    const DataMemberList dataMembers = p->dataMembers();
    const DataMemberList allDataMembers = p->allDataMembers();
    _out << sp;
    writeDocSummary(p, {.generatedType = "class"});
    _out << nl << "export class " << p->mappedName() << " extends ";
    ClassDefPtr base = p->base();
    if (base)
    {
        const string baseName = base->mappedScoped(".");
        _out << importPrefix(baseName) << baseName;
    }
    else
    {
        _out << _iceImportPrefix << "Ice.Value";
    }
    _out << sb;

    if (!dataMembers.empty())
    {
        _out << nl << "/**";
        _out << nl << " * One-shot constructor to initialize all data members.";
        for (const auto& dataMember : allDataMembers)
        {
            if (const auto& comment = dataMember->docComment())
            {
                _out << nl << " * @param " << dataMember->mappedName() << " " << getFirstSentence(comment->overview());
            }
        }
        _out << nl << " */";
        _out << nl << "constructor" << spar;
        for (const auto& dataMember : allDataMembers)
        {
            _out << (dataMember->mappedName() + "?: " + typeToTsString(dataMember->type()));
        }
        _out << epar << ";";
        for (const auto& dataMember : dataMembers)
        {
            _out << sp;
            writeDocSummary(dataMember);
            const string optionalModifier = dataMember->optional() ? "?" : "";
            _out << nl << dataMember->mappedName() << optionalModifier << ": "
                 << typeToTsString(dataMember->type(), true) << ";";
        }
    }
    // else use inherited constructor

    _out << eb;

    return false;
}

namespace
{
    bool areRemainingParamsOptional(const ParameterList& params, const string& name)
    {
        auto it = params.begin();
        do
        {
            it++;
        } while (it != params.end() && (*it)->name() != name);

        for (; it != params.end(); ++it)
        {
            if (!(*it)->optional())
            {
                return false;
            }
        }
        return true;
    }
}

void
Slice::Gen::TypeScriptVisitor::writeOpDocSummary(Output& out, const OperationPtr& op, bool forDispatch)
{
    out << nl << "/**";

    map<string, StringList> paramDoc;
    const optional<DocComment>& comment = op->docComment();
    if (comment)
    {
        const StringList& overview = comment->overview();
        if (!overview.empty())
        {
            writeDocLines(out, overview, true);
        }

        paramDoc = comment->parameters();
        for (const auto& param : op->inParameters())
        {
            auto q = paramDoc.find(param->name());
            if (q != paramDoc.end())
            {
                out << nl << " * @param " << param->mappedName() << " ";
                writeDocLines(out, q->second, false);
            }
        }
    }

    if (forDispatch)
    {
        const string currentParam = escapeParam(op->inParameters(), "current");
        out << nl << " * @param " + currentParam + " The Current object for the dispatch.";
        out << nl << " * @returns A promise like object representing the result of the dispatch";
    }
    else
    {
        const string contextParam = escapeParam(op->inParameters(), "context");
        out << nl << " * @param " + contextParam + " The Context map to send with the invocation.";
        out << nl << " * @returns An {@link Ice.AsyncResult} object representing the result of the invocation";
    }

    if (op->returnsMultipleValues())
    {
        out << ", which resolves to an array with the following entries:";
    }
    else if (op->returnsAnyValues())
    {
        out << ", which resolves to:";
    }
    else
    {
        out << ".";
    }

    const TypePtr ret = op->returnType();
    if (comment && ret)
    {
        out << nl << " * - " << typeToTsString(ret, true, false, op->returnIsOptional()) << " : ";
        writeDocLines(out, comment->returns(), false, "   ");
    }

    if (comment)
    {
        for (const auto& param : op->outParameters())
        {
            auto q = paramDoc.find(param->name());
            if (q != paramDoc.end())
            {
                out << nl << " * - " << typeToTsString(param->type(), true, false, param->optional()) << " : ";
                writeDocLines(out, q->second, false, "   ");
            }
        }

        writeOpDocExceptions(out, op, *comment);

        for (const string& line : comment->seeAlso())
        {
            out << nl << " * " << line;
        }
    }

    if (!forDispatch)
    {
        writeDeprecated(out, comment, op);
    }

    if (comment)
    {
        writeRemarks(_out, comment->remarks());
    }

    out << nl << " */";
}

void
Slice::Gen::TypeScriptVisitor::writeNestedModuleExports(const string& currentModuleScope)
{
    // Export types from included files that are in nested modules not present in this file.
    // Re-exported types for modules that are visible in this file are handled by TypeScriptVisitor::visitModuleStart.

    // Map from nested module path (relative to currentModuleScope) to (typeName, fullReference) pairs.
    map<string, vector<pair<string, string>>> moduleTypes;

    const string prefix = currentModuleScope + ".";

    for (const auto& [includeFile, types] : _importedTypesByInclude)
    {
        string importPrefix = "__module_" + importPathToIdentifier(includeFile) + ".";

        for (const auto& typeScope : types)
        {
            // Check if this type is under currentModuleScope.
            if (typeScope.find(prefix) != 0)
            {
                continue;
            }

            // Find the type's parent module and type name.
            auto lastDot = typeScope.rfind('.');
            assert(lastDot != string::npos);
            string parentModule = typeScope.substr(0, lastDot);
            string typeName = typeScope.substr(lastDot + 1);

            // Skip if parent module is visible (exists in this file) - handled by that module.
            if (_visitedModules.find(parentModule) != _visitedModules.end())
            {
                continue;
            }

            // Skip if already exported by a previous call.
            if (_exportedTypes.find(typeScope) != _exportedTypes.end())
            {
                continue;
            }

            // Mark as exported and collect for output.
            _exportedTypes.insert(typeScope);

            // Get the nested module path relative to currentModuleScope.
            string nestedModulePath = parentModule.substr(prefix.length());
            moduleTypes[nestedModulePath].emplace_back(typeName, importPrefix + typeScope);
        }
    }

    if (moduleTypes.empty())
    {
        return;
    }

    // Get sorted module paths from map keys.
    vector<string> sortedModules;
    sortedModules.reserve(moduleTypes.size());
    for (const auto& [path, _] : moduleTypes)
    {
        sortedModules.push_back(path);
    }
    sort(sortedModules.begin(), sortedModules.end());

    // Generate namespace declarations incrementally.
    vector<string> currentOpen;
    for (const auto& modulePath : sortedModules)
    {
        vector<string> parts;
        splitString(modulePath, ".", parts);

        // Find common prefix length with currently open namespaces.
        size_t commonLen = 0;
        while (commonLen < currentOpen.size() && commonLen < parts.size() && currentOpen[commonLen] == parts[commonLen])
        {
            ++commonLen;
        }

        // Close namespaces that are no longer needed.
        for (size_t i = currentOpen.size(); i > commonLen; --i)
        {
            _out << eb;
        }

        // Open new namespaces.
        for (size_t i = commonLen; i < parts.size(); ++i)
        {
            _out << nl << "namespace " << parts[i] << sb;
        }

        currentOpen = parts;

        // Emit export imports for types in this module.
        for (const auto& [typeName, fullRef] : moduleTypes[modulePath])
        {
            _out << nl << "export import " << typeName << " = " << fullRef << ";";
        }
    }

    // Close all remaining open namespaces.
    for (size_t i = currentOpen.size(); i > 0; --i)
    {
        _out << eb;
    }
}

bool
Slice::Gen::TypeScriptVisitor::visitInterfaceDefStart(const InterfaceDefPtr& p)
{
    //
    // Define servant and proxy types.
    //
    const string prxName = p->mappedName() + "Prx";
    InterfaceList bases = p->bases();
    _out << sp;
    writeDocSummary(p, {.generatedType = "proxy class"});
    _out << nl << "export class " << prxName << " extends " << _iceImportPrefix << "Ice.ObjectPrx";
    if (!bases.empty())
    {
        _out << " implements ";
        _out.spar("");
        for (const auto& base : bases)
        {
            _out << (importPrefix(base->mappedScoped(".")) + base->mappedScoped(".") + "Prx");
        }
        _out.epar("");
    }

    _out << sb;

    _out << sp;
    _out << nl << "/**";
    _out << nl << " * Constructs a new " << prxName << " proxy.";
    _out << nl << " * @param communicator - The communicator for the new proxy.";
    _out << nl << " * @param proxyString - The string representation of the proxy.";
    _out << nl << " * @returns The new " << prxName << " proxy.";
    _out << nl << " * @throws ParseException - Thrown if the proxyString is not a valid proxy string.";
    _out << nl << " */";
    _out << nl << "constructor(communicator: " << _iceImportPrefix << "Ice.Communicator, proxyString: string);";

    for (const auto& op : p->allOperations())
    {
        const ParameterList paramList = op->parameters();
        const TypePtr ret = op->returnType();
        ParameterList inParams, outParams;
        for (const auto& param : paramList)
        {
            if (param->isOutParam())
            {
                outParams.push_back(param);
            }
            else
            {
                inParams.push_back(param);
            }
        }

        _out << sp;
        writeOpDocSummary(_out, op, false);
        _out << nl << op->mappedName() << spar;
        for (const auto& param : inParams)
        {
            // TypeScript doesn't allow optional parameters with '?' prefix before required parameters.
            const string optionalPrefix =
                param->optional() && areRemainingParamsOptional(paramList, param->name()) ? "?" : "";
            _out
                << (param->mappedName() + optionalPrefix + ": " +
                    typeToTsString(param->type(), true, true, param->optional()));
        }
        _out << "context?: Map<string, string>";
        _out << epar;

        _out << ": " << _iceImportPrefix << "Ice.AsyncResult";
        if (!ret && outParams.empty())
        {
            _out << "<void>";
        }
        else if ((ret && outParams.empty()) || (!ret && outParams.size() == 1))
        {
            TypePtr t = ret ? ret : outParams.front()->type();
            bool optional = ret ? op->returnIsOptional() : outParams.front()->optional();
            _out << "<" << typeToTsString(t, true, false, optional) << ">";
        }
        else
        {
            _out << "<[";
            if (ret)
            {
                _out << typeToTsString(ret, true, false, op->returnIsOptional()) << ", ";
            }

            for (auto i = outParams.begin(); i != outParams.end();)
            {
                _out << typeToTsString((*i)->type(), true, false, (*i)->optional());
                if (++i != outParams.end())
                {
                    _out << ", ";
                }
            }

            _out << "]>";
        }

        _out << ";";
    }

    _out << sp;
    _out << nl << "/**";
    _out << nl << " * Creates a new proxy from an existing proxy.";
    _out << nl << " * @param prx The source proxy.";
    _out << nl << " * @param facet An optional facet name.";
    _out << nl << " * @returns A new proxy with the requested type and facet, or null if the source proxy is null.";
    _out << nl << " */";
    _out << nl << "static uncheckedCast(prx: " << _iceImportPrefix << "Ice.ObjectPrx"
         << ", "
         << "facet?: string): " << prxName << ";";

    _out << sp;
    _out << nl << "/**";
    _out << nl << " * Creates a new proxy from an existing proxy.";
    _out << nl << " * @param prx The source proxy.";
    _out << nl << " * @param facet An optional facet name.";
    _out << nl << " * @returns A new proxy with the requested type and facet, or null if the source proxy is null.";
    _out << nl << " */";
    _out << nl << "static uncheckedCast(prx: " << _iceImportPrefix << "Ice.ObjectPrx | null"
         << ", "
         << "facet?: string): " << prxName << " | null;";

    _out << sp;
    _out << nl << "/**";
    _out << nl
         << " * Creates a new proxy from an existing proxy after confirming the target object's type via a remote "
            "invocation.";
    _out << nl << " * @param prx The source proxy.";
    _out << nl << " * @param facet An optional facet name.";
    _out << nl << " * @param context The request context.";
    _out << nl
         << " * @returns A proxy with the requested type and facet, or null if the target object does not support the "
            "requested type.";
    _out << nl << " */";
    _out << nl << "static checkedCast(prx: " << _iceImportPrefix << "Ice.ObjectPrx | null"
         << ", "
         << "facet?: string, context?: Map<string, string>): " << _iceImportPrefix << "Ice.AsyncResult"
         << "<" << prxName << " | null>;";
    _out << eb;

    _out << sp;
    writeDocSummary(p, {.generateDeprecated = false, .generatedType = "skeleton class"});
    _out << nl << "export abstract class " << p->mappedName() << " extends " << _iceImportPrefix << "Ice.Object";
    if (!bases.empty())
    {
        _out << " implements ";
        _out.spar("");
        for (const auto& base : bases)
        {
            _out << (importPrefix(base->mappedScoped(".")) + base->mappedScoped("."));
        }
        _out.epar("");
    }
    _out << sb;
    for (const auto& op : p->allOperations())
    {
        const ParameterList paramList = op->parameters();
        const TypePtr ret = op->returnType();
        ParameterList inParams, outParams;
        for (const auto& param : paramList)
        {
            if (param->isOutParam())
            {
                outParams.push_back(param);
            }
            else
            {
                inParams.push_back(param);
            }
        }

        _out << sp;
        writeOpDocSummary(_out, op, true);
        _out << nl << "abstract " << op->mappedName() << spar;
        for (const auto& param : inParams)
        {
            _out << (param->mappedName() + ": " + typeToTsString(param->type(), true, true, param->optional()));
        }
        _out << ("current: " + _iceImportPrefix + "Ice.Current");
        _out << epar << ": ";

        if (!ret && outParams.empty())
        {
            _out << "PromiseLike<void> | void";
        }
        else if ((ret && outParams.empty()) || (!ret && outParams.size() == 1))
        {
            TypePtr t = ret ? ret : outParams.front()->type();
            string returnType = typeToTsString(t, true, false, op->returnIsOptional());
            _out << "PromiseLike<" << returnType << "> | " << returnType;
        }
        else
        {
            ostringstream os;
            if (ret)
            {
                os << typeToTsString(ret, true, false, op->returnIsOptional()) << ", ";
            }

            for (auto i = outParams.begin(); i != outParams.end();)
            {
                os << typeToTsString((*i)->type(), true, false, (*i)->optional());
                if (++i != outParams.end())
                {
                    os << ", ";
                }
            }
            _out << "PromiseLike<[" << os.str() << "]> | [" << os.str() << "]";
        }
        _out << ";";
    }

    _out << sp;
    _out << nl << "/**";
    _out << nl << " * Dispatches an incoming request to one of the methods of this generated class, based on the "
         << "operation name carried by the request.";
    _out << nl << " *";
    _out << nl << " * @param request The incoming request.";
    _out << nl << " * @returns The outgoing response (when the selected method returns a value),";
    _out << nl << " * or a promise with the outgoing response (when the selected method returns a promise).";
    _out << nl << " * @remarks Ice marshals any exception thrown by this method into the response.";
    _out << nl << " */";
    _out << nl << "dispatch(request: " << _iceImportPrefix << "Ice.IncomingRequest): " << _iceImportPrefix
         << "Ice.OutgoingResponse | PromiseLike<" << _iceImportPrefix << "Ice.OutgoingResponse>;";

    _out << sp;
    _out << nl << "/**";
    _out << nl << " * Obtains the Slice type ID of this type.";
    _out << nl << " * @returns The return value is always \"" + p->scoped() + "\".";
    _out << nl << " */";
    _out << nl << "static ice_staticId(): string;";
    _out << eb;

    return false;
}

bool
Slice::Gen::TypeScriptVisitor::visitExceptionStart(const ExceptionPtr& p)
{
    const DataMemberList dataMembers = p->dataMembers();
    const DataMemberList allDataMembers = p->allDataMembers();

    ExceptionPtr base = p->base();
    string baseRef;
    if (base)
    {
        const string baseName = base->mappedScoped(".");
        baseRef = importPrefix(baseName) + baseName;
    }
    else
    {
        baseRef = _iceImportPrefix + "Ice.UserException";
    }

    _out << sp;
    writeDocSummary(p, {.generatedType = "exception class"});
    _out << nl << "export class " << p->mappedName() << " extends " << baseRef << sb;
    if (!dataMembers.empty())
    {
        _out << nl << "/**";
        _out << nl << " * One-shot constructor to initialize all data members.";
        for (const auto& dataMember : allDataMembers)
        {
            if (const auto& comment = dataMember->docComment())
            {
                _out << nl << " * @param " << dataMember->mappedName() << " " << getFirstSentence(comment->overview());
            }
        }
        _out << nl << " */";
        _out << nl << "constructor" << spar;
        for (const auto& dataMember : allDataMembers)
        {
            _out << (dataMember->mappedName() + "?: " + typeToTsString(dataMember->type()));
        }
        _out << epar << ";";

        for (const auto& dataMember : dataMembers)
        {
            _out << sp;
            writeDocSummary(dataMember);
            const string optionalModifier = dataMember->optional() ? "?" : "";
            _out << nl << dataMember->mappedName() << optionalModifier << ": "
                 << typeToTsString(dataMember->type(), true) << ";";
        }
    }
    // else inherit base constructor

    _out << eb;
    return false;
}

bool
Slice::Gen::TypeScriptVisitor::visitStructStart(const StructPtr& p)
{
    const string name = p->mappedName();
    const DataMemberList dataMembers = p->dataMembers();

    _out << sp;
    writeDocSummary(p, {.generatedType = "class"});
    _out << nl << "export class " << name << sb;
    _out << nl << "constructor" << spar;
    for (const auto& dataMember : dataMembers)
    {
        // TODO why are all parameters optional?
        _out << (dataMember->mappedName() + "?: " + typeToTsString(dataMember->type()));
    }
    _out << epar << ";";

    _out << sp;
    _out << nl << "/**";
    _out << nl << " * A deep copy of the current object.";
    _out << nl << " *";
    _out << nl << " * @returns A deep copy of the current object.";
    _out << nl << " */";
    _out << nl << "clone(): " << name << ";";

    _out << sp;
    _out << nl << "/**";
    _out << nl << " * Determines whether the specified object is equal to the current object.";
    _out << nl << " *";
    _out << nl << " * @param other The object to compare with the current object.";
    _out << nl << " * @returns `true` if the specified object is equal to the current object, `false` otherwise.";
    _out << nl << " */";
    _out << nl << "equals(other: any): boolean;";

    // Only generate hashCode if this structure type is a legal dictionary key type.
    if (Dictionary::isLegalKeyType(p))
    {
        _out << sp;
        _out << nl << "/**";
        _out << nl << " * Returns the hash code of the object.";
        _out << nl << " *";
        _out << nl << " * @returns The hash code of the object.";
        _out << nl << " */";
        _out << nl << "hashCode(): number;";
    }

    _out << sp;
    _out << nl << "/**";
    _out << nl << " * Writes the {@link " << name << "} value to the given OutputStream.";
    _out << nl << " *";
    _out << nl << " * @param outs The OutputStream to write to.";
    _out << nl << " * @param value The value to write.";
    _out << nl << " */";
    _out << nl << "static write(outs: " << _iceImportPrefix << "Ice.OutputStream, value:" << name << "): void;";

    _out << sp;
    _out << nl << "/**";
    _out << nl << " * Reads {@link " << name << "} from the given InputStream.";
    _out << nl << " *";
    _out << nl << " * @param ins The InputStream to read from.";
    _out << nl << " * @returns The read {@link " << name << "} value.";
    _out << nl << " */";
    _out << nl << "static read(ins: " << _iceImportPrefix << "Ice.InputStream): " << name << ";";

    for (const auto& dataMember : dataMembers)
    {
        _out << sp;
        writeDocSummary(dataMember);
        _out << nl << dataMember->mappedName() << ": " << typeToTsString(dataMember->type(), true) << ";";
    }
    _out << eb;

    return false;
}

void
Slice::Gen::TypeScriptVisitor::visitSequence(const SequencePtr& p)
{
    const string name = p->mappedName();

    _out << sp;
    writeDocSummary(p);
    _out << nl << "export type " << name << " = " << typeToTsString(p) << ";";

    _out << sp;
    _out << nl << "/**";
    _out << nl << " * Helper class for encoding {@link " << name << "} into an OutputStream and";
    _out << nl << " * decoding {@link " << name << "} from an InputStream.";
    _out << nl << " */";
    _out << nl << "export class " << name + "Helper";
    _out << sb;

    _out << nl << "/**";
    _out << nl << " * Writes the {@link " << name << "} value to the given OutputStream.";
    _out << nl << " *";
    _out << nl << " * @param outs The OutputStream to write to.";
    _out << nl << " * @param value The value to write.";
    _out << nl << " */";
    _out << nl << "static write(outs: " << _iceImportPrefix << "Ice.OutputStream, value: " << name << "): void;";

    _out << sp;
    _out << nl << "/**";
    _out << nl << " * Reads {@link " << name << "} from the given InputStream.";
    _out << nl << " *";
    _out << nl << " * @param ins The InputStream to read from.";
    _out << nl << " * @returns The read {@link " << name << "} value.";
    _out << nl << " */";
    _out << nl << "static read(ins: " << _iceImportPrefix << "Ice.InputStream): " << name << ";";
    _out << eb;
}

void
Slice::Gen::TypeScriptVisitor::visitDictionary(const DictionaryPtr& p)
{
    const string name = p->mappedName();
    _out << sp;
    writeDocSummary(p);
    string dictionaryType = typeToTsString(p);
    if (dictionaryType.find("Ice.") == 0)
    {
        dictionaryType = _iceImportPrefix + dictionaryType;
    }
    _out << nl << "export class " << name << " extends " << dictionaryType;
    _out << sb;
    _out << eb;

    _out << sp;
    _out << nl << "/**";
    _out << nl << " * Helper class for encoding {@link " << name << "} into an OutputStream and";
    _out << nl << " * decoding {@link " << name << "} from an InputStream.";
    _out << nl << " */";
    _out << nl << "export class " << name + "Helper";
    _out << sb;

    _out << nl << "/**";
    _out << nl << " * Writes the {@link " << name << "} value to the given OutputStream.";
    _out << nl << " *";
    _out << nl << " * @param outs The OutputStream to write to.";
    _out << nl << " * @param value The value to write.";
    _out << nl << " */";
    _out << nl << "static write(outs: " << _iceImportPrefix << "Ice.OutputStream, value: " << name << "): void;";

    _out << sp;
    _out << nl << "/**";
    _out << nl << " * Reads {@link " << name << "} from the given InputStream.";
    _out << nl << " *";
    _out << nl << " * @param ins The InputStream to read from.";
    _out << nl << " * @returns The read {@link " << name << "} value.";
    _out << nl << " */";
    _out << nl << "static read(ins: " << _iceImportPrefix << "Ice.InputStream): " << name << ";";
    _out << eb;
}

void
Slice::Gen::TypeScriptVisitor::visitEnum(const EnumPtr& p)
{
    const string name = p->mappedName();

    _out << sp;
    writeDocSummary(p, {.generatedType = "enum class"});
    _out << nl << "export class " << name << " extends " << _iceImportPrefix << "Ice.EnumBase";
    _out << sb;
    for (const auto& enumerator : p->enumerators())
    {
        _out << sp;
        writeDocSummary(enumerator);
        _out << nl << "static readonly " << enumerator->mappedName() << ": " << name << ";";
    }
    _out << nl;
    _out << nl << "/**";
    _out << nl << " * Returns the enumerator corresponding to the specified numeric value.";
    _out << nl << " *";
    _out << nl << " * @param value The enumerator value.";
    _out << nl << " * The matching enumerator, or `undefined` if no enumerator corresponds to the given value.";
    _out << nl << " */";
    _out << nl << "static valueOf(value: number): " << name << " | undefined;";
    _out << eb;
}

void
Slice::Gen::TypeScriptVisitor::visitConst(const ConstPtr& p)
{
    _out << sp;
    writeDocSummary(p, {.generatedType = "constant"});
    _out << nl << "export const " << p->mappedName() << ": " << typeToTsString(p->type()) << ";";
}
