var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
import { mapFirst } from "collection-utils";
import { initTypeNames } from "./attributes/TypeNames.js";
import { InputData } from "./input/Inputs.js";
import * as targetLanguages from "./language/All.js";
import { combineClasses } from "./rewrites/CombineClasses.js";
import { expandStrings } from "./rewrites/ExpandStrings.js";
import { flattenStrings } from "./rewrites/FlattenStrings.js";
import { flattenUnions } from "./rewrites/FlattenUnions.js";
import { inferMaps } from "./rewrites/InferMaps.js";
import { replaceObjectType } from "./rewrites/ReplaceObjectType.js";
import { resolveIntersections } from "./rewrites/ResolveIntersections.js";
import { assert } from "./support/Support.js";
import { gatherNames } from "./GatherNames.js";
import { defaultInferenceFlags, inferenceFlagNames, inferenceFlags, } from "./Inference.js";
import { makeTransformations } from "./MakeTransformations.js";
import { messageError } from "./Messages.js";
import { TypeBuilder } from "./Type/TypeBuilder.js";
import { TypeGraph } from "./Type/TypeGraph.js";
import { noneToAny, optionalToNullable, removeIndirectionIntersections, } from "./Type/TypeGraphUtils.js";
export function getTargetLanguage(nameOrInstance) {
    if (typeof nameOrInstance === "object") {
        return nameOrInstance;
    }
    const language = targetLanguages.languageNamed(nameOrInstance);
    if (language !== undefined) {
        return language;
    }
    return messageError("DriverUnknownOutputLanguage", {
        lang: nameOrInstance,
    });
}
const defaultOptions = {
    lang: "ts",
    inputData: new InputData(),
    alphabetizeProperties: false,
    allPropertiesOptional: false,
    fixedTopLevels: false,
    noRender: false,
    leadingComments: undefined,
    rendererOptions: {},
    indentation: undefined,
    outputFilename: "stdout",
    debugPrintGraph: false,
    checkProvenance: false,
    debugPrintReconstitution: false,
    debugPrintGatherNames: false,
    debugPrintTransformations: false,
    debugPrintTimes: false,
    debugPrintSchemaResolving: false,
};
class Run {
    constructor(options) {
        // We must not overwrite defaults with undefined values, which
        // we sometimes get.
        this._options = Object.fromEntries(Object.entries(Object.assign({}, defaultOptions, defaultInferenceFlags)).map(([k, v]) => { var _a; return [k, (_a = options[k]) !== null && _a !== void 0 ? _a : v]; }));
    }
    get stringTypeMapping() {
        const targetLanguage = getTargetLanguage(this._options.lang);
        const mapping = new Map(targetLanguage.stringTypeMapping);
        for (const flag of inferenceFlagNames) {
            const stringType = inferenceFlags[flag].stringType;
            if (!this._options[flag] && stringType !== undefined) {
                mapping.set(stringType, "string");
            }
        }
        return mapping;
    }
    get debugPrintReconstitution() {
        return this._options.debugPrintReconstitution === true;
    }
    get debugPrintTransformations() {
        return this._options.debugPrintTransformations;
    }
    get debugPrintSchemaResolving() {
        return this._options.debugPrintSchemaResolving;
    }
    timeSync(name, f) {
        return __awaiter(this, void 0, void 0, function* () {
            const start = Date.now();
            const result = yield f();
            const end = Date.now();
            if (this._options.debugPrintTimes) {
                console.log(`${name} took ${end - start}ms`);
            }
            return result;
        });
    }
    time(name, f) {
        const start = Date.now();
        const result = f();
        const end = Date.now();
        if (this._options.debugPrintTimes) {
            console.log(`${name} took ${end - start}ms`);
        }
        return result;
    }
    makeGraphInputs() {
        const targetLanguage = getTargetLanguage(this._options.lang);
        const stringTypeMapping = this.stringTypeMapping;
        const conflateNumbers = !targetLanguage.supportsUnionsWithBothNumberTypes;
        const typeBuilder = new TypeBuilder(stringTypeMapping, this._options.alphabetizeProperties, this._options.allPropertiesOptional, this._options.checkProvenance, false);
        typeBuilder.typeGraph = new TypeGraph(typeBuilder, 0, this._options.checkProvenance);
        return {
            targetLanguage,
            stringTypeMapping,
            conflateNumbers,
            typeBuilder,
        };
    }
    makeGraph(allInputs) {
        return __awaiter(this, void 0, void 0, function* () {
            const graphInputs = this.makeGraphInputs();
            yield this.timeSync("read input", () => __awaiter(this, void 0, void 0, function* () {
                return yield allInputs.addTypes(this, graphInputs.typeBuilder, this._options.inferMaps, this._options.inferEnums, this._options.fixedTopLevels);
            }));
            return this.processGraph(allInputs, graphInputs);
        });
    }
    makeGraphSync(allInputs) {
        const graphInputs = this.makeGraphInputs();
        this.time("read input", () => allInputs.addTypesSync(this, graphInputs.typeBuilder, this._options.inferMaps, this._options.inferEnums, this._options.fixedTopLevels));
        return this.processGraph(allInputs, graphInputs);
    }
    processGraph(allInputs, graphInputs) {
        const { targetLanguage, stringTypeMapping, conflateNumbers, typeBuilder, } = graphInputs;
        let graph = typeBuilder.finish();
        if (this._options.debugPrintGraph) {
            graph.setPrintOnRewrite();
            graph.printGraph();
        }
        const debugPrintReconstitution = this.debugPrintReconstitution;
        if (typeBuilder.didAddForwardingIntersection ||
            !this._options.ignoreJsonRefs) {
            this.time("remove indirection intersections", () => {
                graph = removeIndirectionIntersections(graph, stringTypeMapping, debugPrintReconstitution);
            });
        }
        let unionsDone = false;
        if (allInputs.needSchemaProcessing || !this._options.ignoreJsonRefs) {
            let intersectionsDone = false;
            do {
                const graphBeforeRewrites = graph;
                if (!intersectionsDone) {
                    this.time("resolve intersections", () => {
                        [graph, intersectionsDone] = resolveIntersections(graph, stringTypeMapping, debugPrintReconstitution);
                    });
                }
                if (!unionsDone) {
                    this.time("flatten unions", () => {
                        [graph, unionsDone] = flattenUnions(graph, stringTypeMapping, conflateNumbers, true, debugPrintReconstitution);
                    });
                }
                if (graph === graphBeforeRewrites) {
                    assert(intersectionsDone && unionsDone, "Graph didn't change but we're not done");
                }
            } while (!intersectionsDone || !unionsDone);
        }
        this.time("replace object type", () => {
            graph = replaceObjectType(graph, stringTypeMapping, conflateNumbers, targetLanguage.supportsFullObjectType, debugPrintReconstitution);
        });
        do {
            this.time("flatten unions", () => {
                [graph, unionsDone] = flattenUnions(graph, stringTypeMapping, conflateNumbers, false, debugPrintReconstitution);
            });
        } while (!unionsDone);
        if (this._options.combineClasses) {
            const combinedGraph = this.time("combine classes", () => combineClasses(this, graph, this._options.alphabetizeProperties, true, false, debugPrintReconstitution));
            if (combinedGraph === graph) {
                graph = combinedGraph;
            }
            else {
                this.time("combine classes cleanup", () => {
                    graph = combineClasses(this, combinedGraph, this._options.alphabetizeProperties, false, true, debugPrintReconstitution);
                });
            }
        }
        if (this._options.inferMaps) {
            for (;;) {
                const newGraph = this.time("infer maps", () => inferMaps(graph, stringTypeMapping, true, debugPrintReconstitution));
                if (newGraph === graph) {
                    break;
                }
                graph = newGraph;
            }
        }
        const enumInference = allInputs.needSchemaProcessing
            ? "all"
            : this._options.inferEnums
                ? "infer"
                : "none";
        this.time("expand strings", () => {
            graph = expandStrings(this, graph, enumInference);
        });
        this.time("flatten unions", () => {
            [graph, unionsDone] = flattenUnions(graph, stringTypeMapping, conflateNumbers, false, debugPrintReconstitution);
        });
        assert(unionsDone, "We should only have to flatten unions once after expanding strings");
        if (allInputs.needSchemaProcessing) {
            this.time("flatten strings", () => {
                graph = flattenStrings(graph, stringTypeMapping, debugPrintReconstitution);
            });
        }
        this.time("none to any", () => {
            graph = noneToAny(graph, stringTypeMapping, debugPrintReconstitution);
        });
        if (!targetLanguage.supportsOptionalClassProperties) {
            this.time("optional to nullable", () => {
                graph = optionalToNullable(graph, stringTypeMapping, debugPrintReconstitution);
            });
        }
        this.time("fixed point", () => {
            graph = graph.rewriteFixedPoint(false, debugPrintReconstitution);
        });
        this.time("make transformations", () => {
            graph = makeTransformations(this, graph, targetLanguage);
        });
        this.time("flatten unions", () => {
            [graph, unionsDone] = flattenUnions(graph, stringTypeMapping, conflateNumbers, false, debugPrintReconstitution);
        });
        assert(unionsDone, "We should only have to flatten unions once after making transformations");
        // Sometimes we combine classes in ways that will the order come out
        // differently compared to what it would be from the equivalent schema,
        // so we always just garbage collect to get a defined order and be done
        // with it.
        // FIXME: We don't actually have to do this if any of the above graph
        // rewrites did anything.  We could just check whether the current graph
        // is different from the one we started out with.
        this.time("GC", () => {
            graph = graph.garbageCollect(this._options.alphabetizeProperties, debugPrintReconstitution);
        });
        if (this._options.debugPrintGraph) {
            console.log("\n# gather names");
        }
        this.time("gather names", () => gatherNames(graph, !allInputs.needSchemaProcessing, this._options.debugPrintGatherNames));
        if (this._options.debugPrintGraph) {
            graph.printGraph();
        }
        return graph;
    }
    makeSimpleTextResult(lines) {
        return new Map([
            [this._options.outputFilename, { lines, annotations: [] }],
        ]);
    }
    preRun() {
        // FIXME: This makes quicktype not quite reentrant
        initTypeNames();
        const targetLanguage = getTargetLanguage(this._options.lang);
        const inputData = this._options.inputData;
        const needIR = inputData.needIR || !targetLanguage.names.includes("schema");
        const schemaString = needIR
            ? undefined
            : inputData.singleStringSchemaSource();
        if (schemaString !== undefined) {
            const lines = JSON.stringify(JSON.parse(schemaString), undefined, 4).split("\n");
            lines.push("");
            const srr = { lines, annotations: [] };
            return new Map([
                [this._options.outputFilename, srr],
            ]);
        }
        return [inputData, targetLanguage];
    }
    run() {
        return __awaiter(this, void 0, void 0, function* () {
            const preRunResult = this.preRun();
            if (!Array.isArray(preRunResult)) {
                return preRunResult;
            }
            const [inputData, targetLanguage] = preRunResult;
            const graph = yield this.makeGraph(inputData);
            return this.renderGraph(targetLanguage, graph);
        });
    }
    runSync() {
        const preRunResult = this.preRun();
        if (!Array.isArray(preRunResult)) {
            return preRunResult;
        }
        const [inputData, targetLanguage] = preRunResult;
        const graph = this.makeGraphSync(inputData);
        return this.renderGraph(targetLanguage, graph);
    }
    renderGraph(targetLanguage, graph) {
        if (this._options.noRender) {
            return this.makeSimpleTextResult(["Done.", ""]);
        }
        return targetLanguage.renderGraphAndSerialize(graph, this._options.outputFilename, this._options.alphabetizeProperties, this._options.leadingComments, this._options.rendererOptions, this._options.indentation);
    }
}
/**
 * Run quicktype and produce one or more output files.
 *
 * @param options Partial options.  For options that are not defined, the
 * defaults will be used.
 */
export function quicktypeMultiFile(options) {
    return __awaiter(this, void 0, void 0, function* () {
        return yield new Run(options).run();
    });
}
export function quicktypeMultiFileSync(options) {
    return new Run(options).runSync();
}
function offsetLocation(loc, lineOffset) {
    return { line: loc.line + lineOffset, column: loc.column };
}
function offsetSpan(span, lineOffset) {
    return {
        start: offsetLocation(span.start, lineOffset),
        end: offsetLocation(span.end, lineOffset),
    };
}
/**
 * Combines a multi-file render result into a single output.  All the files
 * are concatenated and prefixed with a `//`-style comment giving the
 * filename.
 */
export function combineRenderResults(result) {
    if (result.size <= 1) {
        const first = mapFirst(result);
        if (first === undefined) {
            return { lines: [], annotations: [] };
        }
        return first;
    }
    let lines = [];
    let annotations = [];
    for (const [filename, srr] of result) {
        const offset = lines.length + 2;
        lines = lines.concat([`// ${filename}`, ""], srr.lines);
        annotations = annotations.concat(srr.annotations.map((ann) => ({
            annotation: ann.annotation,
            span: offsetSpan(ann.span, offset),
        })));
    }
    return { lines, annotations };
}
/**
 * Run quicktype like `quicktypeMultiFile`, but if there are multiple
 * output files they will all be squashed into one output, with comments at the
 * start of each file.
 *
 * @param options Partial options.  For options that are not defined, the
 * defaults will be used.
 */
export function quicktype(options) {
    return __awaiter(this, void 0, void 0, function* () {
        const result = yield quicktypeMultiFile(options);
        return combineRenderResults(result);
    });
}
