#!/usr/bin/env node

import cp from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import url from "node:url";
import which from "which";
import type {
    MetaModel,
    Notification,
    OrType,
    Property,
    Request,
    Structure,
    Type,
} from "./metaModelSchema.mts";

const __filename = url.fileURLToPath(new URL(import.meta.url));
const __dirname = path.dirname(__filename);

const out = path.resolve(__dirname, "../lsp_generated.go");
const metaModelPath = path.resolve(__dirname, "metaModel.json");

if (!fs.existsSync(metaModelPath)) {
    console.error("Meta model file not found; did you forget to run fetchModel.mjs?");
    process.exit(1);
}

const model: MetaModel = JSON.parse(fs.readFileSync(metaModelPath, "utf-8"));

// Preprocess the model to inline extends/mixins contents
function preprocessModel() {
    const structureMap = new Map<string, Structure>();
    for (const structure of model.structures) {
        structureMap.set(structure.name, structure);
    }

    function collectInheritedProperties(structure: Structure, visited = new Set<string>()): Property[] {
        if (visited.has(structure.name)) {
            return []; // Avoid circular dependencies
        }
        visited.add(structure.name);

        const properties: Property[] = [];
        const inheritanceTypes = [...(structure.extends || []), ...(structure.mixins || [])];

        for (const type of inheritanceTypes) {
            if (type.kind === "reference") {
                const inheritedStructure = structureMap.get(type.name);
                if (inheritedStructure) {
                    properties.push(
                        ...collectInheritedProperties(inheritedStructure, new Set(visited)),
                        ...inheritedStructure.properties,
                    );
                }
            }
        }

        return properties;
    }

    // Inline inheritance for each structure
    for (const structure of model.structures) {
        const inheritedProperties = collectInheritedProperties(structure);

        // Merge properties with structure's own properties taking precedence
        const propertyMap = new Map<string, Property>();

        inheritedProperties.forEach(prop => propertyMap.set(prop.name, prop));
        structure.properties.forEach(prop => propertyMap.set(prop.name, prop));

        structure.properties = Array.from(propertyMap.values());
        structure.extends = undefined;
        structure.mixins = undefined;
    }
}

// Preprocess the model before proceeding
preprocessModel();

interface GoType {
    name: string;
    needsPointer: boolean;
}

interface TypeInfo {
    types: Map<string, GoType>;
    literalTypes: Map<string, string>;
    unionTypes: Map<string, { name: string; type: Type; containedNull: boolean; }[]>;
    typeAliasMap: Map<string, Type>;
}

const typeInfo: TypeInfo = {
    types: new Map(),
    literalTypes: new Map(),
    unionTypes: new Map(),
    typeAliasMap: new Map(),
};

function titleCase(s: string) {
    return s.charAt(0).toUpperCase() + s.slice(1);
}

function resolveType(type: Type): GoType {
    switch (type.kind) {
        case "base":
            switch (type.name) {
                case "integer":
                    return { name: "int32", needsPointer: false };
                case "uinteger":
                    return { name: "uint32", needsPointer: false };
                case "string":
                    return { name: "string", needsPointer: false };
                case "boolean":
                    return { name: "bool", needsPointer: false };
                case "URI":
                    return { name: "URI", needsPointer: false };
                case "DocumentUri":
                    return { name: "DocumentUri", needsPointer: false };
                case "decimal":
                    return { name: "float64", needsPointer: false };
                case "null":
                    return { name: "any", needsPointer: false };
                default:
                    throw new Error(`Unsupported base type: ${type.name}`);
            }

        case "reference":
            const typeAliasOverride = typeAliasOverrides.get(type.name);
            if (typeAliasOverride) {
                return typeAliasOverride;
            }

            // Check if this is a type alias that resolves to a union type
            const aliasedType = typeInfo.typeAliasMap.get(type.name);
            if (aliasedType) {
                return resolveType(aliasedType);
            }

            let refType = typeInfo.types.get(type.name);
            if (!refType) {
                refType = { name: type.name, needsPointer: true };
                typeInfo.types.set(type.name, refType);
            }
            return refType;

        case "array": {
            const elementType = resolveType(type.element);
            const arrayTypeName = elementType.needsPointer
                ? `[]*${elementType.name}`
                : `[]${elementType.name}`;
            return {
                name: arrayTypeName,
                needsPointer: false,
            };
        }

        case "map": {
            const keyType = resolveType(type.key);
            const valueType = resolveType(type.value);
            const valueTypeName = valueType.needsPointer ? `*${valueType.name}` : valueType.name;

            return {
                name: `map[${keyType.name}]${valueTypeName}`,
                needsPointer: false,
            };
        }

        case "tuple": {
            if (
                type.items.length === 2 &&
                type.items[0].kind === "base" && type.items[0].name === "uinteger" &&
                type.items[1].kind === "base" && type.items[1].name === "uinteger"
            ) {
                return { name: "[2]uint32", needsPointer: false };
            }

            throw new Error("Unsupported tuple type: " + JSON.stringify(type));
        }

        case "stringLiteral": {
            const typeName = `StringLiteral${titleCase(type.value)}`;
            typeInfo.literalTypes.set(String(type.value), typeName);
            return { name: typeName, needsPointer: false };
        }

        case "integerLiteral": {
            const typeName = `IntegerLiteral${type.value}`;
            typeInfo.literalTypes.set(String(type.value), typeName);
            return { name: typeName, needsPointer: false };
        }

        case "booleanLiteral": {
            const typeName = `BooleanLiteral${type.value ? "True" : "False"}`;
            typeInfo.literalTypes.set(String(type.value), typeName);
            return { name: typeName, needsPointer: false };
        }
        case "literal":
            if (type.value.properties.length === 0) {
                return { name: "struct{}", needsPointer: false };
            }

            throw new Error("Unexpected non-empty literal object: " + JSON.stringify(type.value));

        case "or": {
            return handleOrType(type);
        }

        default:
            throw new Error(`Unsupported type kind: ${type.kind}`);
    }
}

function flattenOrTypes(types: Type[]): Type[] {
    const flattened = new Set<Type>();

    for (const rawType of types) {
        let type = rawType;

        // Dereference reference types that point to OR types
        if (rawType.kind === "reference") {
            const aliasedType = typeInfo.typeAliasMap.get(rawType.name);
            if (aliasedType && aliasedType.kind === "or") {
                type = aliasedType;
            }
        }

        if (type.kind === "or") {
            // Recursively flatten OR types
            for (const subType of flattenOrTypes(type.items)) {
                flattened.add(subType);
            }
        }
        else {
            flattened.add(rawType);
        }
    }

    return Array.from(flattened);
}

function handleOrType(orType: OrType): GoType {
    // First, flatten any nested OR types
    const types = flattenOrTypes(orType.items);

    // Check for nullable types (OR with null)
    const nullIndex = types.findIndex(item => item.kind === "base" && item.name === "null");
    let containedNull = nullIndex !== -1;

    // If it's nullable, remove the null type from the list
    let nonNullTypes = types;
    if (containedNull) {
        nonNullTypes = types.filter((_, i) => i !== nullIndex);
    }

    // If no types remain after filtering null, this shouldn't happen
    if (nonNullTypes.length === 0) {
        throw new Error("Union type with only null is not supported: " + JSON.stringify(types));
    }

    // Even if only one type remains after filtering null, we still need to create a union type
    // to preserve the nullable behavior (all fields nil = null)

    let memberNames = nonNullTypes.map(type => {
        if (type.kind === "reference") {
            return type.name;
        }
        else if (type.kind === "base") {
            return titleCase(type.name);
        }
        else if (
            type.kind === "array" &&
            (type.element.kind === "reference" || type.element.kind === "base")
        ) {
            return `${titleCase(type.element.name)}s`;
        }
        else if (type.kind === "array") {
            // Handle more complex array types
            const elementType = resolveType(type.element);
            return `${elementType.name}Array`;
        }
        else if (type.kind === "literal" && type.value.properties.length === 0) {
            return "EmptyObject";
        }
        else if (type.kind === "tuple") {
            return "Tuple";
        }
        else {
            throw new Error(`Unsupported type kind in union: ${type.kind}`);
        }
    });

    // Find longest common prefix of member names chunked by PascalCase
    function findLongestCommonPrefix(names: string[]): string {
        if (names.length === 0) return "";
        if (names.length === 1) return "";

        // Split each name into PascalCase chunks
        function splitPascalCase(name: string): string[] {
            const chunks: string[] = [];
            let currentChunk = "";

            for (let i = 0; i < name.length; i++) {
                const char = name[i];
                if (char >= "A" && char <= "Z" && currentChunk.length > 0) {
                    // Start of a new chunk
                    chunks.push(currentChunk);
                    currentChunk = char;
                }
                else {
                    currentChunk += char;
                }
            }

            if (currentChunk.length > 0) {
                chunks.push(currentChunk);
            }

            return chunks;
        }

        const allChunks = names.map(splitPascalCase);
        const minChunkLength = Math.min(...allChunks.map(chunks => chunks.length));

        // Find the longest common prefix of chunks
        let commonChunks: string[] = [];
        for (let i = 0; i < minChunkLength; i++) {
            const chunk = allChunks[0][i];
            if (allChunks.every(chunks => chunks[i] === chunk)) {
                commonChunks.push(chunk);
            }
            else {
                break;
            }
        }

        return commonChunks.join("");
    }

    const commonPrefix = findLongestCommonPrefix(memberNames);

    let unionTypeName = "";

    if (commonPrefix.length > 0) {
        const trimmedMemberNames = memberNames.map(name => name.slice(commonPrefix.length));
        if (trimmedMemberNames.every(name => name)) {
            unionTypeName = commonPrefix + trimmedMemberNames.join("Or");
            memberNames = trimmedMemberNames;
        }
        else {
            unionTypeName = memberNames.join("Or");
        }
    }
    else {
        unionTypeName = memberNames.join("Or");
    }

    if (containedNull) {
        unionTypeName += "OrNull";
    }
    else {
        containedNull = false;
    }

    const union = memberNames.map((name, i) => ({ name, type: nonNullTypes[i], containedNull }));

    typeInfo.unionTypes.set(unionTypeName, union);

    return {
        name: unionTypeName,
        needsPointer: false,
    };
}

const typeAliasOverrides = new Map([
    ["LSPAny", { name: "any", needsPointer: false }],
    ["LSPArray", { name: "[]any", needsPointer: false }],
    ["LSPObject", { name: "map[string]any", needsPointer: false }],
]);

/**
 * First pass: Resolve all type information
 */
function collectTypeDefinitions() {
    // Process all enumerations first to make them available for struct fields
    for (const enumeration of model.enumerations) {
        typeInfo.types.set(enumeration.name, {
            name: enumeration.name,
            needsPointer: false,
        });
    }

    const valueTypes = new Set([
        "Position",
        "Range",
        "Location",
        "Color",
        "TextDocumentIdentifier",
        "NotebookDocumentIdentifier",
        "PreviousResultId",
        "VersionedNotebookDocumentIdentifier",
        "VersionedTextDocumentIdentifier",
        "OptionalVersionedTextDocumentIdentifier",
    ]);

    // Process all structures
    for (const structure of model.structures) {
        typeInfo.types.set(structure.name, {
            name: structure.name,
            needsPointer: !valueTypes.has(structure.name),
        });
    }

    // Process all type aliases
    for (const typeAlias of model.typeAliases) {
        if (typeAliasOverrides.has(typeAlias.name)) {
            continue;
        }

        // Store the alias mapping so we can resolve it later
        typeInfo.typeAliasMap.set(typeAlias.name, typeAlias.type);
    }
}

function formatDocumentation(s: string | undefined): string {
    if (!s) return "";

    let lines: string[] = [];

    for (let line of s.split("\n")) {
        line = line.trimEnd();
        line = line.replace(/(\w ) +/g, "$1");
        line = line.replace(/\{@link(?:code)?.*?([^} ]+)\}/g, "$1");
        line = line.replace(/^@(since|proposed|deprecated)(.*)/, (_, tag, rest) => {
            lines.push("");
            return `${titleCase(tag)}${rest ? ":" + rest : "."}`;
        });
        lines.push(line);
    }

    // filter out contiguous empty lines
    while (true) {
        const toRemove = lines.findIndex((line, index) => {
            if (line) return false;
            if (index === 0) return true;
            if (index === lines.length - 1) return true;
            return !(lines[index - 1] && lines[index + 1]);
        });
        if (toRemove === -1) break;
        lines.splice(toRemove, 1);
    }

    return lines.length > 0 ? "// " + lines.join("\n// ") + "\n" : "";
}

function methodNameIdentifier(name: string) {
    return name.split("/").map(v => v === "$" ? "" : titleCase(v)).join("");
}

/**
 * Generate the Go code
 */
function generateCode() {
    const parts: string[] = [];

    function write(s: string) {
        parts.push(s);
    }

    function writeLine(s = "") {
        parts.push(s + "\n");
    }

    // File header
    writeLine("// Code generated by generate.mts; DO NOT EDIT.");
    writeLine("");
    writeLine("package lsproto");
    writeLine("");
    writeLine(`import (`);
    writeLine(`\t"fmt"`);
    writeLine("");
    writeLine(`\t"github.com/go-json-experiment/json"`);
    writeLine(`\t"github.com/go-json-experiment/json/jsontext"`);
    writeLine(`)`);
    writeLine("");
    writeLine("// Meta model version " + model.metaData.version);
    writeLine("");

    // Generate structures
    writeLine("// Structures\n");

    for (const structure of model.structures) {
        function generateStructFields(name: string, includeDocumentation: boolean) {
            if (includeDocumentation) {
                write(formatDocumentation(structure.documentation));
            }

            writeLine(`type ${name} struct {`);

            // Properties are now inlined, no need to embed extends/mixins
            for (const prop of structure.properties) {
                if (includeDocumentation) {
                    write(formatDocumentation(prop.documentation));
                }

                const type = resolveType(prop.type);
                const goType = prop.optional || type.needsPointer ? `*${type.name}` : type.name;

                writeLine(`\t${titleCase(prop.name)} ${goType} \`json:"${prop.name}${prop.optional ? ",omitzero" : ""}"\``);

                if (includeDocumentation) {
                    writeLine("");
                }
            }

            writeLine("}");
            writeLine("");
        }

        generateStructFields(structure.name, true);
        writeLine("");

        if (hasTextDocumentURI(structure)) {
            // Generate TextDocumentURI method
            writeLine(`func (s *${structure.name}) TextDocumentURI() DocumentUri {`);
            writeLine(`\treturn s.TextDocument.Uri`);
            writeLine(`}`);
            writeLine("");
        }

        // Generate UnmarshalJSONFrom method for structure validation
        const requiredProps = structure.properties?.filter(p => !p.optional) || [];
        if (requiredProps.length > 0) {
            writeLine(`\tvar _ json.UnmarshalerFrom = (*${structure.name})(nil)`);
            writeLine("");

            writeLine(`func (s *${structure.name}) UnmarshalJSONFrom(dec *jsontext.Decoder) error {`);
            writeLine(`\tvar (`);
            for (const prop of requiredProps) {
                writeLine(`\t\tseen${titleCase(prop.name)} bool`);
            }
            writeLine(`\t)`);
            writeLine("");

            writeLine(`\tif k := dec.PeekKind(); k != '{' {`);
            writeLine(`\t\treturn fmt.Errorf("expected object start, but encountered %v", k)`);
            writeLine(`\t}`);
            writeLine(`\tif _, err := dec.ReadToken(); err != nil {`);
            writeLine(`\t\treturn err`);
            writeLine(`\t}`);
            writeLine("");

            writeLine(`\tfor dec.PeekKind() != '}' {`);
            writeLine("name, err := dec.ReadValue()");
            writeLine(`\t\tif err != nil {`);
            writeLine(`\t\t\treturn err`);
            writeLine(`\t\t}`);
            writeLine(`\t\tswitch string(name) {`);

            for (const prop of structure.properties) {
                writeLine(`\t\tcase \`"${prop.name}"\`:`);
                if (!prop.optional) {
                    writeLine(`\t\t\tseen${titleCase(prop.name)} = true`);
                }
                writeLine(`\t\t\tif err := json.UnmarshalDecode(dec, &s.${titleCase(prop.name)}); err != nil {`);
                writeLine(`\t\t\t\treturn err`);
                writeLine(`\t\t\t}`);
            }

            writeLine(`\t\tdefault:`);
            writeLine(`\t\t// Ignore unknown properties.`);
            writeLine(`\t\t}`);
            writeLine(`\t}`);
            writeLine("");

            writeLine(`\tif _, err := dec.ReadToken(); err != nil {`);
            writeLine(`\t\treturn err`);
            writeLine(`\t}`);
            writeLine("");

            for (const prop of requiredProps) {
                writeLine(`\tif !seen${titleCase(prop.name)} {`);
                writeLine(`\t\treturn fmt.Errorf("required property '${prop.name}' is missing")`);
                writeLine(`\t}`);
            }

            writeLine("");
            writeLine(`\treturn nil`);
            writeLine(`}`);
            writeLine("");
        }
    }

    // Generate enumerations
    writeLine("// Enumerations\n");

    for (const enumeration of model.enumerations) {
        write(formatDocumentation(enumeration.documentation));

        let baseType;
        switch (enumeration.type.name) {
            case "string":
                baseType = "string";
                break;
            case "integer":
                baseType = "int32";
                break;
            case "uinteger":
                baseType = "uint32";
                break;
            default:
                throw new Error(`Unsupported enum type: ${enumeration.type.name}`);
        }

        writeLine(`type ${enumeration.name} ${baseType}`);
        writeLine("");

        // Get the pre-processed enum entries map that avoids duplicates

        const enumValues = enumeration.values.map(value => ({
            value: String(value.value),
            identifier: `${enumeration.name}${value.name}`,
            documentation: value.documentation,
            deprecated: value.deprecated,
        }));

        writeLine("const (");

        // Process entries with unique identifiers
        for (const entry of enumValues) {
            write(formatDocumentation(entry.documentation));

            let valueLiteral;
            // Handle string values
            if (enumeration.type.name === "string") {
                valueLiteral = `"${entry.value.replace(/^"|"$/g, "")}"`;
            }
            else {
                valueLiteral = entry.value;
            }

            writeLine(`\t${entry.identifier} ${enumeration.name} = ${valueLiteral}`);
        }

        writeLine(")");
        writeLine("");
    }

    const requestsAndNotifications: (Request | Notification)[] = [...model.requests, ...model.notifications];

    // Generate unmarshalParams function
    writeLine("func unmarshalParams(method Method, data []byte) (any, error) {");
    writeLine("\tswitch method {");

    // Requests and notifications
    for (const request of requestsAndNotifications) {
        const methodName = methodNameIdentifier(request.method);

        if (!request.params) {
            writeLine(`\tcase Method${methodName}:`);
            writeLine(`\t\treturn unmarshalEmpty(data)`);
            continue;
        }
        if (Array.isArray(request.params)) {
            throw new Error("Unexpected array type for request params: " + JSON.stringify(request.params));
        }

        const resolvedType = resolveType(request.params);

        writeLine(`\tcase Method${methodName}:`);
        if (resolvedType.name === "any") {
            writeLine(`\t\treturn unmarshalAny(data)`);
        }
        else {
            writeLine(`\t\treturn unmarshalPtrTo[${resolvedType.name}](data)`);
        }
    }

    writeLine("\tdefault:");
    writeLine(`\t\treturn unmarshalAny(data)`);
    writeLine("\t}");
    writeLine("}");
    writeLine("");

    writeLine("// Methods");
    writeLine("const (");
    for (const request of requestsAndNotifications) {
        write(formatDocumentation(request.documentation));

        const methodName = methodNameIdentifier(request.method);

        writeLine(`\tMethod${methodName} Method = "${request.method}"`);
    }
    writeLine(")");
    writeLine("");

    // Generate request response types
    writeLine("// Request response types");
    writeLine("");

    for (const request of requestsAndNotifications) {
        const methodName = methodNameIdentifier(request.method);

        let responseTypeName: string | undefined;

        if ("result" in request) {
            if (request.typeName && request.typeName.endsWith("Request")) {
                responseTypeName = request.typeName.replace(/Request$/, "Response");
            }
            else {
                responseTypeName = `${methodName}Response`;
            }

            writeLine(`// Response type for \`${request.method}\``);

            // Special case for response types that are explicitly base type "null"
            if (request.result.kind === "base" && request.result.name === "null") {
                writeLine(`type ${responseTypeName} = Null`);
            }
            else {
                const resultType = resolveType(request.result);
                const goType = resultType.needsPointer ? `*${resultType.name}` : resultType.name;
                writeLine(`type ${responseTypeName} = ${goType}`);
            }
            writeLine("");
        }

        if (Array.isArray(request.params)) {
            throw new Error("Unexpected request params for " + methodName + ": " + JSON.stringify(request.params));
        }

        const paramType = request.params ? resolveType(request.params) : undefined;
        const paramGoType = paramType ? (paramType.needsPointer ? `*${paramType.name}` : paramType.name) : "any";

        writeLine(`// Type mapping info for \`${request.method}\``);
        if (responseTypeName) {
            writeLine(`var ${methodName}Info = RequestInfo[${paramGoType}, ${responseTypeName}]{Method: Method${methodName}}`);
        }
        else {
            writeLine(`var ${methodName}Info = NotificationInfo[${paramGoType}]{Method: Method${methodName}}`);
        }

        writeLine("");
    }

    // Generate union types
    writeLine("// Union types\n");

    for (const [name, members] of typeInfo.unionTypes.entries()) {
        writeLine(`type ${name} struct {`);
        const uniqueTypeFields = new Map(); // Maps type name -> field name

        for (const member of members) {
            const type = resolveType(member.type);
            const memberType = type.name;

            // If this type name already exists in our map, skip it
            if (!uniqueTypeFields.has(memberType)) {
                const fieldName = titleCase(member.name);
                uniqueTypeFields.set(memberType, fieldName);
                writeLine(`\t${fieldName} *${memberType}`);
            }
        }

        writeLine(`}`);
        writeLine("");

        // Get the field names and types for marshal/unmarshal methods
        const fieldEntries = Array.from(uniqueTypeFields.entries()).map(([typeName, fieldName]) => ({ fieldName, typeName }));

        // Marshal method
        writeLine(`var _ json.MarshalerTo = (*${name})(nil)`);
        writeLine("");

        writeLine(`func (o *${name}) MarshalJSONTo(enc *jsontext.Encoder) error {`);

        // Determine if this union contained null (check if any member has containedNull = true)
        const unionContainedNull = members.some(member => member.containedNull);
        if (unionContainedNull) {
            write(`\tassertAtMostOne("more than one element of ${name} is set", `);
        }
        else {
            write(`\tassertOnlyOne("exactly one element of ${name} should be set", `);
        }

        // Create assertion to ensure at most one field is set at a time

        // Write the assertion conditions
        for (let i = 0; i < fieldEntries.length; i++) {
            if (i > 0) write(", ");
            write(`o.${fieldEntries[i].fieldName} != nil`);
        }
        writeLine(`)`);
        writeLine("");

        for (const entry of fieldEntries) {
            writeLine(`\tif o.${entry.fieldName} != nil {`);
            writeLine(`\t\treturn json.MarshalEncode(enc, o.${entry.fieldName})`);
            writeLine(`\t}`);
        }

        // If all fields are nil, marshal as null (only for unions that can contain null)
        if (unionContainedNull) {
            writeLine(`\treturn enc.WriteToken(jsontext.Null)`);
        }
        else {
            writeLine(`\tpanic("unreachable")`);
        }
        writeLine(`}`);
        writeLine("");

        // Unmarshal method
        writeLine(`var _ json.UnmarshalerFrom = (*${name})(nil)`);
        writeLine("");

        writeLine(`func (o *${name}) UnmarshalJSONFrom(dec *jsontext.Decoder) error {`);
        writeLine(`\t*o = ${name}{}`);
        writeLine("");

        writeLine("\tdata, err := dec.ReadValue()");
        writeLine("\tif err != nil {");
        writeLine("\t\treturn err");
        writeLine("\t}");

        if (unionContainedNull) {
            writeLine(`\tif string(data) == "null" {`);
            writeLine(`\t\treturn nil`);
            writeLine(`\t}`);
            writeLine("");
        }

        for (const entry of fieldEntries) {
            writeLine(`\tvar v${entry.fieldName} ${entry.typeName}`);
            writeLine(`\tif err := json.Unmarshal(data, &v${entry.fieldName}); err == nil {`);
            writeLine(`\t\to.${entry.fieldName} = &v${entry.fieldName}`);
            writeLine(`\t\treturn nil`);
            writeLine(`\t}`);
        }

        // Match the error format from the original script
        writeLine(`\treturn fmt.Errorf("invalid ${name}: %s", data)`);
        writeLine(`}`);
        writeLine("");
    }

    // Generate literal types
    writeLine("// Literal types\n");

    for (const [value, name] of typeInfo.literalTypes.entries()) {
        const jsonValue = JSON.stringify(value);

        writeLine(`// ${name} is a literal type for ${jsonValue}`);
        writeLine(`type ${name} struct{}`);
        writeLine("");

        writeLine(`var _ json.MarshalerTo = ${name}{}`);
        writeLine("");

        writeLine(`func (o ${name}) MarshalJSONTo(enc *jsontext.Encoder) error {`);
        writeLine(`\treturn enc.WriteValue(jsontext.Value(\`${jsonValue}\`))`);
        writeLine(`}`);
        writeLine("");

        writeLine(`var _ json.UnmarshalerFrom = &${name}{}`);
        writeLine("");

        writeLine(`func (o *${name}) UnmarshalJSONFrom(dec *jsontext.Decoder) error {`);
        writeLine(`\tv, err := dec.ReadValue();`);
        writeLine(`\tif err != nil {`);
        writeLine(`\t\treturn err`);
        writeLine(`\t}`);
        writeLine(`\tif string(v) != \`${jsonValue}\` {`);
        writeLine(`\t\treturn fmt.Errorf("expected ${name} value %s, got %s", \`${jsonValue}\`, v)`);
        writeLine(`\t}`);
        writeLine(`\treturn nil`);
        writeLine(`}`);
        writeLine("");
    }

    return parts.join("");
}

function hasTextDocumentURI(structure: Structure) {
    return structure.properties?.some(p =>
        !p.optional &&
        p.name === "textDocument" &&
        p.type.kind === "reference" &&
        p.type.name === "TextDocumentIdentifier"
    );
}

/**
 * Main function
 */
function main() {
    try {
        collectTypeDefinitions();
        const generatedCode = generateCode();
        fs.writeFileSync(out, generatedCode);

        // Format with gofmt
        const gofmt = which.sync("go");
        cp.execFileSync(gofmt, ["tool", "mvdan.cc/gofumpt", "-lang=go1.25", "-w", out]);

        console.log(`Successfully generated ${out}`);
    }
    catch (error) {
        console.error("Error generating code:", error);
        process.exit(1);
    }
}

main();
