"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PythonRenderer = void 0;
const collection_utils_1 = require("collection-utils");
const ConvenienceRenderer_js_1 = require("../../ConvenienceRenderer.js");
const Naming_js_1 = require("../../Naming.js");
const Strings_js_1 = require("../../support/Strings.js");
const Support_js_1 = require("../../support/Support.js");
const Transformers_js_1 = require("../../Transformers.js");
const index_js_1 = require("../../Type/index.js");
const TypeUtils_js_1 = require("../../Type/TypeUtils.js");
const constants_js_1 = require("./constants.js");
const utils_js_1 = require("./utils.js");
class PythonRenderer extends ConvenienceRenderer_js_1.ConvenienceRenderer {
    constructor(targetLanguage, renderContext, pyOptions) {
        super(targetLanguage, renderContext);
        this.pyOptions = pyOptions;
        this.imports = new Map();
        this.declaredTypes = new Set();
    }
    forbiddenNamesForGlobalNamespace() {
        return constants_js_1.forbiddenTypeNames;
    }
    forbiddenForObjectProperties(_, _classNamed) {
        return {
            names: constants_js_1.forbiddenPropertyNames,
            includeGlobalForbidden: false,
        };
    }
    makeNamedTypeNamer() {
        return (0, Naming_js_1.funPrefixNamer)("type", utils_js_1.classNameStyle);
    }
    namerForObjectProperty() {
        return (0, Naming_js_1.funPrefixNamer)("property", (s) => (0, utils_js_1.snakeNameStyle)(s, false, this.pyOptions.nicePropertyNames));
    }
    makeUnionMemberNamer() {
        return null;
    }
    makeEnumCaseNamer() {
        return (0, Naming_js_1.funPrefixNamer)("enum-case", (s) => (0, utils_js_1.snakeNameStyle)(s, true, this.pyOptions.nicePropertyNames));
    }
    get commentLineStart() {
        return "# ";
    }
    emitDescriptionBlock(lines) {
        if (lines.length === 1) {
            this.emitComments([
                { customLines: lines, lineStart: '"""', lineEnd: '"""' },
            ]);
        }
        else {
            this.emitCommentLines(lines, {
                firstLineStart: '"""',
                lineStart: "",
                afterComment: '"""',
            });
        }
    }
    get needsTypeDeclarationBeforeUse() {
        return true;
    }
    canBeForwardDeclared(t) {
        const kind = t.kind;
        return kind === "class" || kind === "enum";
    }
    emitBlock(line, f) {
        this.emitLine(line);
        this.indent(f);
    }
    string(s) {
        const openQuote = '"';
        return [openQuote, (0, Strings_js_1.stringEscape)(s), '"'];
    }
    withImport(module, name) {
        if (this.pyOptions.features.typeHints || module !== "typing") {
            // FIXME: This is ugly.  We should rather not generate that import in the first
            // place, but right now we just make the type source and then throw it away.  It's
            // not a performance issue, so it's fine, I just bemoan this special case, and
            // potential others down the road.
            (0, collection_utils_1.mapUpdateInto)(this.imports, module, (s) => s ? (0, collection_utils_1.setUnionInto)(s, [name]) : new Set([name]));
        }
        return name;
    }
    withTyping(name) {
        return this.withImport("typing", name);
    }
    namedType(t) {
        const name = this.nameForNamedType(t);
        if (this.declaredTypes.has(t))
            return name;
        return ["'", name, "'"];
    }
    pythonType(t, _isRootTypeDef = false) {
        const actualType = (0, Transformers_js_1.followTargetType)(t);
        return (0, TypeUtils_js_1.matchType)(actualType, (_anyType) => this.withTyping("Any"), (_nullType) => "None", (_boolType) => "bool", (_integerType) => "int", (_doubletype) => "float", (_stringType) => "str", (arrayType) => [
            this.withTyping("List"),
            "[",
            this.pythonType(arrayType.items),
            "]",
        ], (classType) => this.namedType(classType), (mapType) => [
            this.withTyping("Dict"),
            "[str, ",
            this.pythonType(mapType.values),
            "]",
        ], (enumType) => this.namedType(enumType), (unionType) => {
            const [hasNull, nonNulls] = (0, TypeUtils_js_1.removeNullFromUnion)(unionType);
            const memberTypes = Array.from(nonNulls).map((m) => this.pythonType(m));
            if (hasNull !== null) {
                const rest = [];
                if (!this.getAlphabetizeProperties() &&
                    (this.pyOptions.features.dataClasses ||
                        this.pyOptions.pydanticBaseModel) &&
                    _isRootTypeDef) {
                    // Only push "= None" if this is a root level type def
                    //   otherwise we may get type defs like List[Optional[int] = None]
                    //   which are invalid
                    rest.push(" = None");
                }
                if (nonNulls.size > 1) {
                    this.withImport("typing", "Union");
                    return [
                        this.withTyping("Optional"),
                        "[Union[",
                        (0, collection_utils_1.arrayIntercalate)(", ", memberTypes),
                        "]]",
                        ...rest,
                    ];
                }
                return [
                    this.withTyping("Optional"),
                    "[",
                    (0, Support_js_1.defined)((0, collection_utils_1.iterableFirst)(memberTypes)),
                    "]",
                    ...rest,
                ];
            }
            return [
                this.withTyping("Union"),
                "[",
                (0, collection_utils_1.arrayIntercalate)(", ", memberTypes),
                "]",
            ];
        }, (transformedStringType) => {
            if (transformedStringType.kind === "date-time") {
                return this.withImport("datetime", "datetime");
            }
            if (transformedStringType.kind === "uuid") {
                return this.withImport("uuid", "UUID");
            }
            return (0, Support_js_1.panic)(`Transformed type ${transformedStringType.kind} not supported`);
        });
    }
    declarationLine(t) {
        if (t instanceof index_js_1.ClassType) {
            if (this.pyOptions.pydanticBaseModel) {
                return [
                    "class ",
                    this.nameForNamedType(t),
                    "(",
                    this.withImport("pydantic", "BaseModel"),
                    "):",
                ];
            }
            return ["class ", this.nameForNamedType(t), ":"];
        }
        if (t instanceof index_js_1.EnumType) {
            return [
                "class ",
                this.nameForNamedType(t),
                "(",
                this.withImport("enum", "Enum"),
                "):",
            ];
        }
        return (0, Support_js_1.panic)(`Can't declare type ${t.kind}`);
    }
    declareType(t, emitter) {
        this.emitBlock(this.declarationLine(t), () => {
            this.emitDescription(this.descriptionForType(t));
            emitter();
        });
        this.declaredTypes.add(t);
    }
    emitClassMembers(t) {
        if (this.pyOptions.features.dataClasses ||
            this.pyOptions.pydanticBaseModel)
            return;
        const args = [];
        this.forEachClassProperty(t, "none", (name, _, cp) => {
            args.push([name, this.typeHint(": ", this.pythonType(cp.type))]);
        });
        this.emitBlock([
            "def __init__(self, ",
            (0, collection_utils_1.arrayIntercalate)(", ", args),
            ")",
            this.typeHint(" -> None"),
            ":",
        ], () => {
            if (args.length === 0) {
                this.emitLine("pass");
            }
            else {
                this.forEachClassProperty(t, "none", (name) => {
                    this.emitLine("self.", name, " = ", name);
                });
            }
        });
    }
    typeHint(...sl) {
        if (this.pyOptions.features.typeHints) {
            return sl;
        }
        return [];
    }
    typingDecl(name, type) {
        return [name, this.typeHint(": ", this.withTyping(type))];
    }
    typingReturn(type) {
        return this.typeHint(" -> ", this.withTyping(type));
    }
    sortClassProperties(properties, propertyNames) {
        if (this.pyOptions.features.dataClasses ||
            this.pyOptions.pydanticBaseModel) {
            return (0, collection_utils_1.mapSortBy)(properties, (p) => {
                return (p.type instanceof index_js_1.UnionType &&
                    (0, TypeUtils_js_1.nullableFromUnion)(p.type) != null) ||
                    p.isOptional
                    ? 1
                    : 0;
            });
        }
        return super.sortClassProperties(properties, propertyNames);
    }
    emitClass(t) {
        if (this.pyOptions.features.dataClasses &&
            !this.pyOptions.pydanticBaseModel) {
            this.emitLine("@", this.withImport("dataclasses", "dataclass"));
        }
        this.declareType(t, () => {
            if (this.pyOptions.features.typeHints) {
                if (t.getProperties().size === 0) {
                    this.emitLine("pass");
                }
                else {
                    this.forEachClassProperty(t, "none", (name, jsonName, cp) => {
                        this.emitLine(name, this.typeHint(": ", this.pythonType(cp.type, true)));
                        this.emitDescription(this.descriptionForClassProperty(t, jsonName));
                    });
                }
                this.ensureBlankLine();
            }
            this.emitClassMembers(t);
        });
    }
    emitEnum(t) {
        this.declareType(t, () => {
            this.forEachEnumCase(t, "none", (name, jsonName) => {
                this.emitLine([name, " = ", this.string(jsonName)]);
            });
        });
    }
    emitImports() {
        this.imports.forEach((names, module) => {
            this.emitLine("from ", module, " import ", Array.from(names).join(", "));
        });
    }
    emitSupportCode() {
        return;
    }
    emitClosingCode() {
        return;
    }
    emitSourceStructure(_givenOutputFilename) {
        const declarationLines = this.gatherSource(() => {
            this.forEachNamedType(["interposing", 2], (c) => this.emitClass(c), (e) => this.emitEnum(e), (_u) => {
                return;
            });
        });
        const closingLines = this.gatherSource(() => this.emitClosingCode());
        const supportLines = this.gatherSource(() => this.emitSupportCode());
        if (this.leadingComments !== undefined) {
            this.emitComments(this.leadingComments);
        }
        this.ensureBlankLine();
        this.emitImports();
        this.ensureBlankLine(2);
        this.emitGatheredSource(supportLines);
        this.ensureBlankLine(2);
        this.emitGatheredSource(declarationLines);
        this.ensureBlankLine(2);
        this.emitGatheredSource(closingLines);
    }
}
exports.PythonRenderer = PythonRenderer;
