import ceylon.ast.core {
    CompilationUnit,
    AnyCompilationUnit,
    Node,
    WideningTransformer,
    Visitor
}
import ceylon.ast.redhat {
    anyCompilationUnitToCeylon
}
import ceylon.collection {
    HashMap,
    LinkedList,
    linked,
    MutableMap
}
import ceylon.file {
    Directory,
    parsePath,
    lines,
    File,
    forEachLine,
    temporaryDirectory
}
import ceylon.interop.java {
    CeylonIterable,
    javaClass,
    JavaIterable,
    javaString,
    CeylonList
}
import ceylon.json {
    JsonObject=Object,
    Array,
    ObjectValue
}
import ceylon.language.meta {
    type
}

import com.redhat.ceylon.cmr.api {
    RepositoryManager,
    ArtifactContext
}
import com.redhat.ceylon.cmr.ceylon {
    CeylonUtils
}
import com.redhat.ceylon.cmr.impl {
    ShaSigner
}
import com.redhat.ceylon.common {
    FileUtil,
    ModuleUtil
}
import com.redhat.ceylon.common.tool {
    ToolError
}
import com.redhat.ceylon.compiler.typechecker {
    TypeCheckerBuilder
}
import com.redhat.ceylon.compiler.typechecker.analyzer {
    ModuleSourceMapper
}
import com.redhat.ceylon.compiler.typechecker.context {
    PhasedUnit
}
import com.redhat.ceylon.compiler.typechecker.io {
    VirtualFile
}
import com.redhat.ceylon.compiler.typechecker.tree {
    TCVisitor=Visitor,
    TreeNode=Node,
    Tree
}
import com.redhat.ceylon.compiler.typechecker.util {
    WarningSuppressionVisitor
}
import com.redhat.ceylon.model.typechecker.context {
    TypeCache
}
import com.redhat.ceylon.model.typechecker.model {
    ModuleModel=Module
}
import com.vasileff.ceylon.dart.compiler.core {
    CompilationContext,
    augmentNode,
    computeCaptures,
    computeClassCaptures,
    isForDartBackend,
    moduleImportPrefix,
    ModelGenerator
}
import com.vasileff.ceylon.dart.compiler.dartast {
    DartCompilationUnitMember,
    DartCompilationUnit,
    DartSimpleIdentifier,
    DartImportDirective,
    DartSimpleStringLiteral,
    DartTypeName,
    DartMethodInvocation,
    DartFunctionExpression,
    DartFunctionDeclaration,
    DartArgumentList,
    CodeWriter,
    DartFormalParameterList,
    DartSimpleFormalParameter,
    DartBlockFunctionBody,
    DartBlock,
    DartExpressionStatement,
    DartPropertyAccess,
    DartFunctionExpressionInvocation
}
import com.vasileff.ceylon.dart.compiler.loader {
    DartModuleManagerFactory,
    MetamodelVisitor
}
import com.vasileff.ceylon.structures {
    LinkedListMultimap
}

import java.io {
    JFile=File,
    JPrintWriter=PrintWriter
}
import java.lang {
    System,
    Runnable,
    JString=String,
    JBoolean=Boolean,
    JFloat=Float,
    JDouble=Double,
    JInteger=Integer,
    JLong=Long
}
import java.util {
    EnumSet,
    JMap=Map,
    JList=List
}

// TODO produce error on import of modules with conflicting versions, even if non-shared.

shared
[[DartCompilationUnit*], CompilationStatus] compileDart(
        virtualFiles = [],
        sourceDirectories = [],
        sourceFiles = [],
        moduleFilters = [],
        repositoryManager = null,
        outputRepositoryManager = null,
        standardOutWriter = JPrintWriter(System.\iout),
        standardErrorWriter = JPrintWriter(System.\ierr),
        generateSourceArtifact = false,
        suppressWarning = [],
        doWithoutCaching = false,
        suppressMainFunction = false,
        verboseAst = false,
        verboseRhAst = false,
        verboseCode = false,
        verboseProfile = false,
        verboseFiles = false,
        quiet = true,
        baselinePerfTest = false) {

    {VirtualFile*} virtualFiles;
    {JFile*} sourceFiles; // for typechecker
    {JFile*} sourceDirectories; // for typechecker

    "A list of modules to compile, or the empty list to compile all modules."
    {String*} moduleFilters;

    RepositoryManager? repositoryManager;
    RepositoryManager? outputRepositoryManager;

    JPrintWriter standardOutWriter;
    JPrintWriter standardErrorWriter;

    Boolean generateSourceArtifact;

    {Warning*} suppressWarning;

    Boolean doWithoutCaching;

    Boolean suppressMainFunction;

    Boolean verboseAst;
    Boolean verboseRhAst;
    Boolean verboseCode;
    Boolean verboseProfile;
    Boolean verboseFiles;
    Boolean quiet;

    "Include 'count nodes' visitors to determine baseline performance."
    Boolean baselinePerfTest;

    suppressWarnings("unusedDeclaration")
    void logOut(Object message = "") {
        standardOutWriter.println(message);
        standardOutWriter.flush();
    }

    void logError(Object message = "") {
        standardErrorWriter.println(message);
        standardErrorWriter.flush();
    }

    void logInfo(String message) {
        if (!quiet) {
            logError(message);
        }
    }

    value mainFunctionHack
        =   DartFunctionDeclaration {
                false;
                DartTypeName {
                    DartSimpleIdentifier("void");
                };
                null;
                DartSimpleIdentifier("main");
                DartFunctionExpression {
                    DartFormalParameterList {
                        false; false;
                        [DartSimpleFormalParameter {
                            false;
                            true;
                            null;
                            DartSimpleIdentifier("arguments");
                        }];
                    };
                    DartBlockFunctionBody {
                        null; false;
                        DartBlock {
                            [DartExpressionStatement {
                                DartFunctionExpressionInvocation {
                                    DartPropertyAccess {
                                        DartSimpleIdentifier("$ceylon$language");
                                        DartSimpleIdentifier("initializeProcess");
                                    };
                                    DartArgumentList {
                                        [DartSimpleIdentifier("arguments")];
                                    };
                                };
                            },
                            DartExpressionStatement {
                                DartMethodInvocation {
                                    null;
                                    DartSimpleIdentifier("run");
                                    DartArgumentList([]);
                                };
                            }];
                        };
                    };
                };
            };

    value mainFunctionHackNoRun
        =   DartFunctionDeclaration {
                false;
                DartTypeName {
                    DartSimpleIdentifier("void");
                };
                null;
                DartSimpleIdentifier("main");
                DartFunctionExpression {
                    DartFormalParameterList {
                        false; false;
                        [DartSimpleFormalParameter {
                            false;
                            true;
                            null;
                            DartSimpleIdentifier("arguments");
                        }];
                    };
                    DartBlockFunctionBody {
                        null; false;
                        DartBlock {
                            [DartExpressionStatement {
                                DartFunctionExpressionInvocation {
                                    DartPropertyAccess {
                                        DartSimpleIdentifier("$dart$core");
                                        DartSimpleIdentifier("print");
                                    };
                                    DartArgumentList {
                                        [DartSimpleStringLiteral {
                                            "A shared toplevel run() function \
                                             was not found.";
                                        }];
                                    };
                                };
                            }];
                        };
                    };
                };
            };

    function nativeCode(Directory directory) {
        // Concatinate *.dart files. Filter import and library directives. Return.
        value sb = StringBuilder();

        for (file in directory
                .children("*.dart")
                .narrow<File>() // TODO support Links
                .filter(File.readable)) {

            sb.append("\n");
            sb.append("/".repeat(70) + "\n");
            sb.append("//\n");
            sb.append("// Stitched file: ``file.name``\n");
            sb.append("//\n");
            sb.append("/".repeat(70) + "\n");

            lines(file)
                .filter((line)
                    => !line.startsWith("import") && !line.startsWith("library"))
                .interpose("\n")
                .each(sb.append);
        }
        return sb.string;
    }

    // make sure Dart backend is registered
    noop(dartBackend);

    // timers
    value timer = Timer();
    value timerStages = Timer();

    value swTypeCheckerCreation = timerStages.Measurement("Typechecker creation");

    value builder = TypeCheckerBuilder();

    // the typechecker output is mostly redundant with our measurements
    // (although it does also provide 'parse' time)
    //builder.statistics(verboseProfile);

    virtualFiles.each((f) => builder.addSrcDirectory(f));
    sourceDirectories.each((f) => builder.addSrcDirectory(f));
    builder.setSourceFiles(javaList(sourceFiles));
    if (!moduleFilters.empty) {
        builder.setModuleFilters(javaList(moduleFilters.map(javaString)));
    }
    builder.setRepositoryManager(repositoryManager);
    builder.moduleManagerFactory(DartModuleManagerFactory());

    // Typecheck, silently.
    value typeChecker = builder.typeChecker;

    swTypeCheckerCreation.destroy(null);

    try (timerStages.Measurement("TypeChecker processing"),
            timer.Measurement("typeChecker.process")) {
        if (doWithoutCaching) {
            TypeCache.doWithoutCaching(object satisfies Runnable {
                run() => typeChecker.process(true);
            });
        }
        else {
            try {
                typeChecker.process(true);
            }
            catch (Throwable t) {
                if (t is ReportableException | ToolError) {
                    throw t;
                }
                logError(
                   "------------------------------------------------------------
                                        ** Compiler bug! **
                    ------------------------------------------------------------
                    ``t.message``\n
                    was encountered while typechecking
                    ------------------------------------------------------------");
                throw t;
            }
        }
    }

    value swDartCompilerCreation = timerStages.Measurement("Dart compiler creation");

    value phasedUnits = CeylonIterable(typeChecker.phasedUnits.phasedUnits);

    // suppress warnings
    value suppressedWarnings = EnumSet.noneOf(javaClass<Warning>());
    suppressedWarnings.addAll(javaList(suppressWarning));
    value warningSuppressionVisitor = WarningSuppressionVisitor<Warning>(
                javaClass<Warning>(), suppressedWarnings);

    // exit early if errors exist
    value errorVisitor = ErrorCollectingVisitor();
    phasedUnits.map(PhasedUnit.compilationUnit).each((cu) => cu.visit(errorVisitor));
    if (errorVisitor.errorCount > 0) {
        // if there are dependency errors, report only them
        value dependencyErrors
            =   errorVisitor.positionedMessages
                .filter((pm)
                    => pm.message is ModuleSourceMapper.ModuleDependencyAnalysisError)
                .sequence();
        if (dependencyErrors nonempty) {
            printErrors {
                (String s) => standardErrorWriter.print(s);
                true; true;
                dependencyErrors;
                typeChecker;
            };
            standardErrorWriter.flush();
        }
        else {
            // otherwise, print all the errors
            phasedUnits.map(PhasedUnit.compilationUnit).each((cu)
                =>  cu.visit(warningSuppressionVisitor));

            printErrors {
                (String s) => standardErrorWriter.print(s);
                true; true;
                errorVisitor.positionedMessages;
                typeChecker;
            };
            standardErrorWriter.flush();
        }
        return [[], CompilationStatus.errorTypeChecker];
    }
    errorVisitor.clear();

    value moduleMembers
        =   HashMap<ModuleModel, LinkedList<DartCompilationUnitMember>>();

    value moduleSources
        =   LinkedListMultimap<ModuleModel, JFile>();

    value metamodelVisitors
        =   HashMap<ModuleModel, MetamodelVisitor>();

    swDartCompilerCreation.destroy(null);
    value swDartCompilation = timerStages.Measurement("Dart compilation");

    variable Integer nodeCountTransformer = 0;
    variable Integer nodeCountVisitor = 0;
    variable Integer nodeCountTcVisitor = 0;

    for (phasedUnit in phasedUnits) {
        phasedUnit.compilationUnit.visit(typeConstructorVisitor);

        value path => phasedUnit.pathRelativeToSrcDir else "<null>";

        // FIXME virtual files? skip if not saving src?
        moduleSources.put(phasedUnit.\ipackage.\imodule, JFile(phasedUnit.unit.fullPath));

        if (verboseFiles) {
            logError("-- begin " + path);
        }

        if (verboseRhAst) {
            logError("-- Redhat AST " + path);
            logError(phasedUnit.compilationUnit);
        }

        Integer start = system.nanoseconds;

        try {
            value ctx
                =   CompilationContext {
                        phasedUnit.unit;
                        CeylonList(phasedUnit.tokens);
                    };

            AnyCompilationUnit unit;
            try (timer.Measurement("anyCompilationUnitToCeylon")) {
                unit = anyCompilationUnitToCeylon {
                    phasedUnit.compilationUnit;
                    augmentNode;
                };
            }

            if (verboseAst) {
                logError("-- Ceylon AST " + path);
                logError(unit);
            }

            if (is CompilationUnit unit) {
                // ignore packages and modules for now

                value m = phasedUnit.\ipackage.\imodule;

                LinkedList<DartCompilationUnitMember> declarations;
                if (exists d = moduleMembers.get(m)) {
                    declarations = d;
                }
                else {
                    declarations = LinkedList<DartCompilationUnitMember>();
                    moduleMembers.put(m, declarations);
                }

                MetamodelVisitor metamodelVisitor;
                if (exists v = metamodelVisitors.get(m)) {
                    metamodelVisitor = v;
                }
                else {
                    metamodelVisitor = MetamodelVisitor(m);
                    metamodelVisitors.put(m, metamodelVisitor);
                }

                try (timer.Measurement("metamodelVisitor")) {
                    phasedUnit.compilationUnit.visit(metamodelVisitor);
                }

                try (timer.Measurement("computeCaptures")) {
                    computeCaptures(unit, ctx);
                }

                try (timer.Measurement("computeClassCaptures")) {
                    computeClassCaptures(unit, ctx);
                }

                try (timer.Measurement("transformCompilationUnit")) {
                    unit.visit(ctx.topLevelVisitor);
                }

                if (baselinePerfTest) {
                    try (timer.Measurement("countNodesTransformer")) {
                        nodeCountTransformer += countNodesTransformer(unit);
                    }

                    try (timer.Measurement("countNodesTransformerNull")) {
                        nodeCountTransformer += countNodesTransformerNull(unit);
                    }

                    try (timer.Measurement("countNodesVisitor")) {
                        nodeCountVisitor += countNodesVisitor(unit);
                    }

                    try (timer.Measurement("countNodesTcVisitor")) {
                        nodeCountTcVisitor += countNodesTcVisitor(
                                phasedUnit.compilationUnit);
                    }
                }

                declarations.addAll(ctx.compilationUnitMembers.sequence());
            }
        }
        catch (Throwable t) {
            logError(
               "------------------------------------------------------------
                                    ** Compiler bug! **
                ------------------------------------------------------------
                ``t.message``\n
                was encountered while compiling the file:

                    ``phasedUnit.unit.fullPath``
                ------------------------------------------------------------");
            throw t;
        }

        // collect warnings and errors
        try (timer.Measurement("errorVisitor")) {
            phasedUnit.compilationUnit.visit(errorVisitor);
        }

        // verboseFiles output
        if (verboseFiles) {
            Integer end = system.nanoseconds;
            logError("-- end   " + path
                + " (``((end-start)/10^6).string``ms)");
        }
    }

    // add runtime model info
    for (mod -> members in moduleMembers) {
        // jump through hoops to support default modules
        value unit
            =   if (mod.default)
                then mod.packages.get(0).units.iterator().next()
                else mod.unit;

        value pkg
            =   if (mod.default)
                then mod.packages.get(0)
                else mod.unit.\ipackage;

        value ctx
            =   CompilationContext(unit, []);

        members.addAll(ModelGenerator(ctx).generateRuntimeModel(mod, pkg));
    }

    value dartCompilationUnits = LinkedList<DartCompilationUnit>();

    // we'll print errors at the end, but determine count now
    value errorCount = errorVisitor.errorCount;

    swDartCompilation.destroy(null);
    value swDartSerialization = timerStages.Measurement("Dart serialization");

    for (m -> ds in moduleMembers) {
        if (!suppressMainFunction) {
            if (hasRunFunction(m)) {
                ds.add(mainFunctionHack);
            }
            else {
                ds.add(mainFunctionHackNoRun);
            }
        }

        function dartPackageLocationForModule(ModuleModel m)
            =>  "package:" + CeylonIterable(m.name)
                    .map(Object.string)
                    .interpose("/")
                    .fold("")(plus)
                    .plus("/")
                    .plus(m.name.get(m.name.size() - 1).string)
                    .plus(".dart");

        value importedModules
            =   gatherCompileDependencies(m).keys.rest
                .map((m) =>
                    if (m.name.size() == 1
                            && m.name.get(0).string.startsWith("dart:")) then
                        let (name = m.name.get(0).string)
                        DartImportDirective {
                            DartSimpleStringLiteral(name);
                            DartSimpleIdentifier {
                                "$" + name.replace(":", "$");
                            };
                        }
                    // Hack to have "dart.x" modules mean "dart:x" for interop
                    else if (m.name.size() == 2
                            && m.name.get(0).string == "dart") then
                        DartImportDirective {
                            DartSimpleStringLiteral("dart:" + m.name.get(1).string);
                            DartSimpleIdentifier {
                                moduleImportPrefix(m);
                            };
                        }
                    else
                        DartImportDirective {
                            DartSimpleStringLiteral {
                                dartPackageLocationForModule(m);
                            };
                            DartSimpleIdentifier {
                                moduleImportPrefix(m);
                            };
                        });

        value dcu
            =   DartCompilationUnit {
                    // Make dart.core, ceylon.interop.dart, and ceylon.dart.runtime.model
                    // available, even if not imported in module.ceylon.
                    {DartImportDirective {
                        DartSimpleStringLiteral("dart:core");
                        DartSimpleIdentifier("$dart$core");
                    },
                    DartImportDirective {
                        DartSimpleStringLiteral("package:ceylon/interop/dart/dart.dart");
                        DartSimpleIdentifier("$ceylon$interop$dart");
                    },
                    DartImportDirective {
                        DartSimpleStringLiteral(
                            "package:ceylon/dart/runtime/model/model.dart");
                        DartSimpleIdentifier("$ceylon$dart$runtime$model");
                    },
                    *importedModules}.coalesced.distinct.sequence();
                    ds.sequence();
                };

        dartCompilationUnits.add(dcu);

        // TODO use the *virtual* filesystem. See JsCompiler.findFile()
        value native
            =   if (exists unit = m.unit,
                    is Directory directory = parsePath(unit.fullPath).parent.resource)
                then nativeCode(directory)
                else "";

        // don't bother serializing if we don't have to, or if errors exist
        if (!errorCount.positive && (outputRepositoryManager exists || verboseCode)) {

            // use a tempfile rather than a StringBuffer, since ShaSigner needs a file
            try (dFile = temporaryDirectory.TemporaryFile(
                            "ceylon-dart-dart-", ".dart"),
                 mFile = temporaryDirectory.TemporaryFile(
                            "ceylon-dart-model-", ".json")) {

                value dartFile = dFile;
                value modelFile = mFile;

                // write to the temp file
                try (appender = dartFile.Appender("utf-8")) {
                    dcu.write(CodeWriter(appender.write));
                    appender.write(native);
                }

                // persist to output repository
                if (exists outputRepositoryManager) {
                    // the code
                    value artifact = ArtifactContext(m.nameAsString, m.version,
                            ArtifactContext.\iDART);

                    artifact.forceOperation = true; // what does this do?

                    outputRepositoryManager.putArtifact(artifact, javaFile(dartFile));

                    ShaSigner.signArtifact(outputRepositoryManager, artifact,
                            javaFile(dartFile), null);

                    // the model
                    value modelArtifact = ArtifactContext(m.nameAsString, m.version,
                            ArtifactContext.\iDART_MODEL);

                    modelArtifact.forceOperation = true; // what does this do?

                    // persist the json model
                    try (appender = modelFile.Appender("utf-8")) {
                        assert (exists metamodelVisitor = metamodelVisitors.get(m));
                        if (m.nameAsString in
                                ["ceylon.dart.runtime.web",
                                 "ceylon.dart.runtime.standard"]) {
                            // ceylon.dart.runtime.standard and
                            // ceylon.dart.runtime.web masquerade as
                            // ceylon.dart.runtime.core. Only "core" is used at compile
                            // time, and only "standard" and "web" are used at runtime.
                            // Have the metadata for "standard" and "web" make them look
                            // like "core", since that is what will be expected at
                            // runtime.
                            metamodelVisitor.model.put(
                                javaString("$mod-name"),
                                javaString("ceylon.dart.runtime.core"));
                            metamodelVisitor.model.put(
                                javaString("ceylon.dart.runtime.core"),
                                metamodelVisitor.model.remove(
                                    javaString(m.nameAsString)));
                        }
                        encodeModel(metamodelVisitor.model, appender);
                    }

                    // persist
                    outputRepositoryManager.putArtifact(
                            modelArtifact, javaFile(modelFile));

                    // and hash
                    ShaSigner.signArtifact(outputRepositoryManager, modelArtifact,
                            javaFile(modelFile), null);

                    if (generateSourceArtifact) {
                        // the source artifact (*must* have this for language module)
                        value sac = CeylonUtils.makeSourceArtifactCreator(
                                outputRepositoryManager,
                                JavaIterable(sourceDirectories),
                                m.nameAsString, m.version,
                                // TODO verboseCMR and logging
                                false, null);

                        sac.copy(FileUtil.filesToPathList(
                            javaList(moduleSources.get(m))));
                    }
                }

                // write code to console
                if (verboseCode) {
                    forEachLine(dartFile, process.writeErrorLine);
                }
            }
            logInfo("Note: Created module \
                     ``ModuleUtil.makeModuleName(m.nameAsString, m.version)``");
        }
    }

    swDartSerialization.destroy(null);

    // suppress warnings *after* generating Dart code since Dart backend
    // may add warnings.
    try (timerStages.Measurement("Warning Suppression")) {
        try (timer.Measurement("warningSuppressionVisitor")) {
            phasedUnits.map(PhasedUnit.compilationUnit).each((cu)
                =>  cu.visit(warningSuppressionVisitor));
        }
    }

    if (verboseProfile) {
        void printTiming(String key, Integer nanos) {
            process.writeErrorLine(key.plus(":").padTrailing(30)
                + formatFloat(nanos.float/1M, 2, 2).padLeading(10) + "ms");
        }

        process.writeErrorLine("");
        process.writeErrorLine("Profiling Information (Cross Sections)");
        process.writeErrorLine("------------------------------------------");
        for (sw in timer.totals) {
            printTiming(sw.key, sw.item);
        }
        printTiming("Total", timer.totalNanoseconds);

        process.writeErrorLine("");
        process.writeErrorLine("Profiling Information (Stages)");
        process.writeErrorLine("------------------------------------------");
        for (sw in timerStages.totals) {
            printTiming(sw.key, sw.item);
        }
        printTiming("Total", timerStages.totalNanoseconds);
    }

    // print errors last, to make them easy to find
    printErrors {
        (String s) => standardErrorWriter.print(s);
        true; true;
        errorVisitor.positionedMessages;
        typeChecker;
    };

    standardOutWriter.flush();
    standardErrorWriter.flush();

    return [dartCompilationUnits.sequence(),
        if (errorVisitor.errorCount > 0)
        then CompilationStatus.errorTypeChecker
        else CompilationStatus.success];
}

shared
[Warning*] allWarnings
    =   CeylonIterable<Warning>(EnumSet.allOf(javaClass<Warning>())).sequence();

shared
class CompilationStatus
        of success | errorTypeChecker | errorDartBackend {
    shared new success {}
    shared new errorTypeChecker {}
    shared new errorDartBackend {}
}

void encodeModel(JMap<JString, Object> model, File.Appender appender) {
    ObjectValue? javaToJson(Anything javaObject) {
        switch (javaObject)
        case (is JString) {
            return javaObject.string;
        }
        case (is JBoolean) {
            return javaObject.booleanValue();
        }
        case (is JLong) {
            return javaObject.longValue();
        }
        case (is JDouble) {
            return javaObject.doubleValue();
        }
        case (is JInteger) {
            return javaObject.longValue();
        }
        case (is JFloat) {
            return javaObject.doubleValue();
        }
        case (is Null) {
            return javaObject;
        }
        else if (is JMap<out Anything, out Anything> javaObject) {
            return JsonObject {
                CeylonIterable {
                    javaObject.entrySet();
                }.collect((entry)
                    =>  (entry.key?.string else "<null>") -> javaToJson(entry.\ivalue));
            };
        }
        else if (is JList<out Anything> javaObject) {
            return Array {
                CeylonIterable(javaObject).collect(javaToJson);
            };
        }
        throw AssertionError("Unsupported type for json: ``type(javaObject)``");
    }

    if (exists s = javaToJson(model)?.string) {
        appender.write(s);
    }
}

class Timer() {
    shared
    HashMap<String, Integer> totals = HashMap<String, Integer>(linked);

    shared
    class Measurement(String id) satisfies Destroyable {
        value startNanos = system.nanoseconds;

        shared actual
        void destroy(Throwable? error) {
            value elapsed = system.nanoseconds - startNanos;
            totals.put(id, elapsed + totals.getOrDefault(id, 0));
        }
    }

    shared
    T time<T>(String id, T run()) {
        value start = system.nanoseconds;
        value result = run();
        value stop = system.nanoseconds;
        totals.put(id, stop - start + totals.getOrDefault(id, 0));
        return result;
    }

    shared
    Integer totalNanoseconds
        =>  sum { 0, *totals.map(Entry.item) };
}

"Count nodes. Useful for determining baseline performance of WideningTransformers."
Integer countNodesTransformer(CompilationUnit unit) {
    object transformer satisfies WideningTransformer<Integer> {
        shared actual Integer transformNode(Node that)
            =>  sum { 1, *that.transformChildren(this) };
    }
    return unit.transform(transformer);
}

"Count nodes. Useful for determining baseline performance of WideningTransformers that
 are used like Visitors."
Integer countNodesTransformerNull(CompilationUnit unit) {
    variable Integer count = 0;
    object transformer satisfies WideningTransformer<Null> {
        shared actual Null transformNode(Node that) {
            count++;
            that.transformChildren(this);
            return null;
        }
    }
    unit.transform(transformer);
    return count;
}

"Count nodes. Useful for determining baseline performance of Visitors."
Integer countNodesVisitor(CompilationUnit unit) {
    variable Integer count = 0;
    object visitor satisfies Visitor {
        shared actual void visitNode(Node that) {
            count++;
            that.visitChildren(this);
        }
    }
    unit.visit(visitor);
    return count;
}

"Count nodes. Useful for determining baseline performance of TC Visitors."
Integer countNodesTcVisitor(Tree.CompilationUnit unit) {
    variable Integer count = 0;
    object visitor extends TCVisitor() {
        shared actual
        void visitAny(TreeNode that) {
            count++;
            that.visitChildren(this);
        }
    }
    unit.visit(visitor);
    return count;
}

"Gather all depencies of the given module, including exported transitive dependencies.
 The returned map will include the passed in module, unless the module is not for the
 Dart backend."
Map<ModuleModel, String> gatherCompileDependencies(
        ModuleModel moduleModel,
        Boolean excludeNonExported = false,
        MutableMap<ModuleModel, String> dependencies
            =   HashMap<ModuleModel, String> { stability = linked; }) {

    value previousVersion = dependencies[moduleModel];
    if (exists previousVersion) {
        if (moduleModel.version != previousVersion) {
            throw ReportableException(
                "Two versions of the same module cannot be imported. Module \
                 ``ModuleUtil.makeModuleName(
                        moduleModel.nameAsString, moduleModel.version)`` conflicts \
                 with ``ModuleUtil.makeModuleName(
                        moduleModel.nameAsString, previousVersion)``");
        }
        return dependencies;
    }

    dependencies.put(moduleModel, moduleModel.version);

    for (dependency in moduleModel.imports) {
        if (!isForDartBackend(dependency)) {
            continue;
        }
        // TODO we should search for version incompatibilites in non-exported
        //      versions too.
        if (!excludeNonExported || dependency.export) {
            gatherCompileDependencies(dependency.\imodule, true, dependencies);
        }
    }

    return dependencies;
}