"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.TypeScriptZodRenderer = void 0;
const collection_utils_1 = require("collection-utils");
const ConvenienceRenderer_js_1 = require("../../ConvenienceRenderer.js");
const Naming_js_1 = require("../../Naming.js");
const Acronyms_js_1 = require("../../support/Acronyms.js");
const Strings_js_1 = require("../../support/Strings.js");
const Support_js_1 = require("../../support/Support.js");
const index_js_1 = require("../../Type/index.js");
const TypeUtils_js_1 = require("../../Type/TypeUtils.js");
const utils_js_1 = require("../JavaScript/utils.js");
class TypeScriptZodRenderer extends ConvenienceRenderer_js_1.ConvenienceRenderer {
    constructor(targetLanguage, renderContext, _options) {
        super(targetLanguage, renderContext);
        this._options = _options;
        /** TypeRefs of object types that participate in a reference cycle.
         * These must be emitted as z.lazy() schemas with an explicit type
         * annotation, since zod cannot infer recursive types. */
        this._recursiveTypeRefs = new Set();
    }
    forbiddenNamesForGlobalNamespace() {
        return ["Class", "Date", "Object", "String", "Array", "JSON", "Error"];
    }
    nameStyle(original, upper) {
        const acronyms = (0, Acronyms_js_1.acronymStyle)(Acronyms_js_1.AcronymStyleOptions.Camel);
        const words = (0, Strings_js_1.splitIntoWords)(original);
        return (0, Strings_js_1.combineWords)(words, utils_js_1.legalizeName, upper ? Strings_js_1.firstUpperWordStyle : Strings_js_1.allLowerWordStyle, Strings_js_1.firstUpperWordStyle, upper ? (s) => (0, Strings_js_1.capitalize)(acronyms(s)) : Strings_js_1.allLowerWordStyle, acronyms, "", Strings_js_1.isLetterOrUnderscore);
    }
    makeNamedTypeNamer() {
        return (0, Naming_js_1.funPrefixNamer)("types", (s) => this.nameStyle(s, true));
    }
    makeUnionMemberNamer() {
        return (0, Naming_js_1.funPrefixNamer)("properties", (s) => this.nameStyle(s, true));
    }
    namerForObjectProperty() {
        return (0, Naming_js_1.funPrefixNamer)("properties", (s) => this.nameStyle(s, true));
    }
    makeEnumCaseNamer() {
        return (0, Naming_js_1.funPrefixNamer)("enum-cases", (s) => this.nameStyle(s, false));
    }
    importStatement(lhs, moduleName) {
        return ["import ", lhs, " from ", moduleName, ";"];
    }
    emitImports() {
        this.ensureBlankLine();
        this.emitLine(this.importStatement("* as z", '"zod"'));
    }
    typeMapTypeForProperty(p) {
        const typeMap = this.typeMapTypeFor(p.type);
        return p.isOptional ? [typeMap, ".optional()"] : typeMap;
    }
    typeMapTypeFor(t, required = true) {
        if (["class", "object", "enum"].includes(t.kind)) {
            return [this.nameForNamedType(t), "Schema"];
        }
        const match = (0, TypeUtils_js_1.matchType)(t, (_anyType) => "z.any()", (_nullType) => "z.null()", (_boolType) => "z.boolean()", (_integerType) => "z.number()", (_doubleType) => "z.number()", (_stringType) => "z.string()", (arrayType) => [
            "z.array(",
            this.typeMapTypeFor(arrayType.items, false),
            ")",
        ], (_classType) => (0, Support_js_1.panic)("Should already be handled."), (_mapType) => [
            "z.record(z.string(), ",
            this.typeMapTypeFor(_mapType.values, false),
            ")",
        ], (_enumType) => (0, Support_js_1.panic)("Should already be handled."), (unionType) => {
            const children = Array.from(unionType.getChildren()).map((type) => this.typeMapTypeFor(type, false));
            return ["z.union([", ...(0, collection_utils_1.arrayIntercalate)(", ", children), "])"];
        }, (_transformedStringType) => {
            if (_transformedStringType.kind === "date-time") {
                return "z.coerce.date()";
            }
            if (_transformedStringType.kind === "uuid") {
                return "z.string().uuid()";
            }
            return "z.string()";
        });
        if (required) {
            return [match];
        }
        return match;
    }
    /** TypeScript type for `t`, used to annotate recursive schemas for
     * which zod cannot infer the type.
     *
     * This intentionally duplicates neither `typeMapTypeFor` above nor
     * `sourceFor` in the plain TypeScript renderer
     * (`TypeScriptFlowBaseRenderer`): it must mirror exactly what
     * `z.infer` would derive from the schemas emitted by
     * `typeMapTypeFor` (e.g. `z.coerce.date()` implies `Date`), while
     * `sourceFor` is shaped by ts/flow-specific options like
     * `preferConstValues` and `declareUnions` that don't exist here. */
    underlyingTypeFor(t) {
        if (["class", "object", "enum"].includes(t.kind)) {
            return this.nameForNamedType(t);
        }
        return (0, TypeUtils_js_1.matchType)(t, (_anyType) => "any", (_nullType) => "null", (_boolType) => "boolean", (_integerType) => "number", (_doubleType) => "number", (_stringType) => "string", (arrayType) => [
            "Array<",
            this.underlyingTypeFor(arrayType.items),
            ">",
        ], (_classType) => (0, Support_js_1.panic)("Should already be handled."), (mapType) => [
            "Record<string, ",
            this.underlyingTypeFor(mapType.values),
            ">",
        ], (_enumType) => (0, Support_js_1.panic)("Should already be handled."), (unionType) => {
            const children = Array.from(unionType.getChildren()).map((type) => this.underlyingTypeFor(type));
            return (0, collection_utils_1.arrayIntercalate)(" | ", children);
        }, (_transformedStringType) => {
            if (_transformedStringType.kind === "date-time") {
                return "Date";
            }
            return "string";
        });
    }
    isRecursive(t) {
        return this._recursiveTypeRefs.has(t.typeRef);
    }
    emitObject(name, t) {
        if (this.isRecursive(t)) {
            this.emitLazyObject(name, t);
            return;
        }
        this.ensureBlankLine();
        this.emitLine("\nexport const ", name, "Schema = ", "z.object({");
        this.indent(() => {
            this.forEachClassProperty(t, "none", (_, jsonName, property) => {
                this.emitLine(`"${(0, Strings_js_1.utf16StringEscape)(jsonName)}"`, ": ", this.typeMapTypeForProperty(property), ",");
            });
        });
        this.emitLine("});");
        if (!this._options.justSchema) {
            this.emitLine("export type ", name, " = z.infer<typeof ", name, "Schema>;");
        }
    }
    /** Emit a recursive object type. zod cannot infer recursive types, so
     * the type is declared explicitly and the schema is wrapped in
     * z.lazy() with a z.ZodType annotation. */
    emitLazyObject(name, t) {
        this.ensureBlankLine();
        if (this._options.justSchema) {
            this.emitLine("\nexport const ", name, "Schema: z.ZodType<any> = z.lazy(() =>");
        }
        else {
            this.emitLine("\nexport type ", name, " = {");
            this.indent(() => {
                this.forEachClassProperty(t, "none", (_, jsonName, property) => {
                    this.emitLine(`"${(0, Strings_js_1.utf16StringEscape)(jsonName)}"`, property.isOptional ? "?" : "", ": ", this.underlyingTypeFor(property.type), ";");
                });
            });
            this.emitLine("};");
            this.emitLine("export const ", name, "Schema: z.ZodType<", name, "> = z.lazy(() =>");
        }
        this.indent(() => {
            this.emitLine("z.object({");
            this.indent(() => {
                this.forEachClassProperty(t, "none", (_, jsonName, property) => {
                    this.emitLine(`"${(0, Strings_js_1.utf16StringEscape)(jsonName)}"`, ": ", this.typeMapTypeForProperty(property), ",");
                });
            });
            this.emitLine("})");
        });
        this.emitLine(");");
    }
    emitEnum(e, enumName) {
        this.ensureBlankLine();
        this.emitDescription(this.descriptionForType(e));
        this.emitLine("\nexport const ", enumName, "Schema = ", "z.enum([");
        this.indent(() => this.forEachEnumCase(e, "none", (_, jsonName) => {
            this.emitLine('"', (0, Strings_js_1.stringEscape)(jsonName), '",');
        }));
        this.emitLine("]);");
        if (!this._options.justSchema) {
            this.emitLine("export type ", enumName, " = z.infer<typeof ", enumName, "Schema>;");
        }
    }
    /** Static function that extracts underlying type refs for types that form part of the
     * definition of the passed type - used to ensure that these appear in generated source
     * before types that reference them.
     *
     * Primitive types don't need defining and enums are output before other types, hence,
     * these are ignored.
     */
    static extractUnderlyingTyperefs(type) {
        const typeRefs = [];
        // Ignore enums and primitives
        if (!type.isPrimitive() && type.kind !== "enum") {
            // need to extract constituent types for unions and intersections (which both extend SetOperationType)
            // and can ignore the union/intersection itself
            if (type instanceof index_js_1.SetOperationType) {
                type.members.forEach((member) => {
                    // recurse as the underlying type could itself be a union, instersection or array etc.
                    typeRefs.push(...TypeScriptZodRenderer.extractUnderlyingTyperefs(member));
                });
            }
            // need to extract additional properties for object, class and map types (which all extend ObjectType)
            if (type instanceof index_js_1.ObjectType) {
                const addType = type.getAdditionalProperties();
                if (addType) {
                    // recurse as the underlying type could itself be a union, instersection or array etc.
                    typeRefs.push(...TypeScriptZodRenderer.extractUnderlyingTyperefs(addType));
                }
            }
            // need to extract items types for ArrayType
            if (type instanceof index_js_1.ArrayType) {
                const itemsType = type.items;
                if (itemsType) {
                    // recurse as the underlying type could itself be a union, instersection or array etc.
                    typeRefs.push(...TypeScriptZodRenderer.extractUnderlyingTyperefs(itemsType));
                }
            }
            // Finally return the reference to a class as that will need to be defined (where objects, maps, unions, intersections and arrays do not)
            if (type instanceof index_js_1.ClassType) {
                typeRefs.push(type.typeRef);
            }
        }
        return typeRefs;
    }
    emitSchemas() {
        this.ensureBlankLine();
        this.forEachEnum("leading-and-interposing", (u, enumName) => {
            this.emitEnum(u, enumName);
        });
        // All children must be defined before this type to avoid forward references in generated code
        // Build a model that will tell us if a referenced type has been defined then make multiple
        // passes over the defined objects to put them into the correct order for output in the
        // generated sourcecode
        const order = [];
        const mapType = [];
        const mapTypeRef = [];
        const mapName = [];
        const mapChildTypeRefs = [];
        this.forEachObject("none", (type, name) => {
            mapType.push(type);
            mapTypeRef.push(type.typeRef);
            mapName.push(name);
            const children = type.getChildren();
            let childTypeRefs = [];
            children.forEach((child) => {
                childTypeRefs = childTypeRefs.concat(TypeScriptZodRenderer.extractUnderlyingTyperefs(child));
            });
            mapChildTypeRefs.push(childTypeRefs);
        });
        // Find types that participate in a reference cycle: types from
        // which we can get back to the same type by following child type
        // references. Those can never be topologically ordered.
        const indexForTypeRef = new Map();
        mapTypeRef.forEach((typeRef, index) => indexForTypeRef.set(typeRef, index));
        this._recursiveTypeRefs = new Set();
        mapType.forEach((_, startIndex) => {
            const visited = new Set();
            const stack = [...mapChildTypeRefs[startIndex]];
            while (stack.length > 0) {
                const childRef = stack.pop();
                const childIndex = indexForTypeRef.get(childRef);
                if (childIndex === undefined)
                    continue;
                if (childIndex === startIndex) {
                    this._recursiveTypeRefs.add(mapTypeRef[startIndex]);
                    break;
                }
                if (visited.has(childIndex))
                    continue;
                visited.add(childIndex);
                stack.push(...mapChildTypeRefs[childIndex]);
            }
        });
        // Items to process on this pass
        let indices = [];
        mapType.forEach((_, index) => {
            if (this._recursiveTypeRefs.has(mapTypeRef[index])) {
                // Recursive types are emitted first: they are wrapped in
                // z.lazy(), so all their references to other schemas are
                // deferred until parse time and don't constrain the
                // output order, while other schemas can reference them
                // directly.
                order.push(index);
            }
            else {
                indices.push(index);
            }
        });
        // items to process on the next pass
        let deferredIndices = [];
        // defensive: make sure we don't loop forever, even complex sets shouldn't require many passes
        const MAX_PASSES = 999;
        let passNum = 0;
        do {
            indices.forEach((index) => {
                // must be behind all these children
                const childTypeRefs = mapChildTypeRefs[index];
                let foundAllChildren = true;
                childTypeRefs.forEach((childRef) => {
                    // defensive: first check if there is a definition for the referenced type (there should be)
                    if (mapTypeRef.includes(childRef)) {
                        let found = false;
                        // find this childs's ordinal, if it has already been added
                        // faster to go through what we've defined so far than all definitions
                        // FIXME: refactor this
                        // eslint-disable-next-line @typescript-eslint/prefer-for-of
                        for (let j = 0; j < order.length; j++) {
                            const childIndex = order[j];
                            if (mapTypeRef[childIndex] === childRef) {
                                found = true;
                                break;
                            }
                        }
                        foundAllChildren = foundAllChildren && found;
                    }
                    else {
                        console.error("A child type reference was not found amongst all Object definitions! TypeRef: " +
                            childRef);
                    }
                });
                if (foundAllChildren) {
                    // insert index into order as we are safe to define this type
                    order.push(index);
                }
                else {
                    // defer to a subsequent pass as we need to define other types
                    deferredIndices.push(index);
                }
            });
            indices = deferredIndices;
            deferredIndices = [];
            passNum++;
            if (passNum > MAX_PASSES) {
                // giving up: emit the stuck items anyway rather than
                // silently dropping them
                order.push(...indices);
                console.warn("Exceeded maximum number of passes when determining output order, output may contain forward references");
            }
        } while (indices.length > 0 && passNum <= MAX_PASSES);
        // now emit ordered source
        order.forEach((i) => this.emitGatheredSource(this.gatherSource(() => this.emitObject(mapName[i], mapType[i]))));
    }
    emitSourceStructure() {
        if (this.leadingComments !== undefined) {
            this.emitComments(this.leadingComments);
        }
        this.emitImports();
        this.emitSchemas();
    }
}
exports.TypeScriptZodRenderer = TypeScriptZodRenderer;
