import { encode, stringify } from "@preserves/core";
import * as M from "./meta";
import { CompilerOptions, ModuleContext } from "./compiler/context";
import { Formatter, block, seq, braces, opseq } from "./compiler/block";
import { typeForDefinition } from "./compiler/gentype";
import { converterForDefinition } from "./compiler/genconverter";
import { renderType } from "./compiler/rendertype";
import { genConstructor } from "./compiler/genctor";
import { unconverterForDefinition } from "./compiler/genunconverter";
import { sourceCodeFor } from "./compiler/value";
import { fromSchema } from "./meta";

export function compile(
    env: M.Environment,
    modulePath: M.ModulePath,
    schema: M.Schema,
    options: CompilerOptions = {},
): string {
    const mod = new ModuleContext(env, modulePath, schema, options);

    mod.definePreamble(`let __schema: _.Value | null = null;`);
    mod.definePreamble(seq(`export function _schema() `, block(
        seq(`if (__schema === null) `, block(
            `__schema = _.decode<_.GenericEmbedded>(_.Bytes.fromHex("${encode(fromSchema(schema)).toHex()}"))`)),
        `return __schema`)));

    const embeddedName = schema.embeddedType;
    if (embeddedName._variant !== 'false') {
        mod.defineType(seq(`export type _embedded = `, mod.embeddedType, `;`));
    }

    for (const [name, def] of schema.definitions) {
        const t = typeForDefinition(mod.resolver(), def);
        const nameStr = stringify(name);
        const resultTypeItem = mod.withAsPreserveMixinType(nameStr, t);

        mod.defineType(seq(`export type ${nameStr}`, mod.genericParametersFor(t),
                           ` = `, renderType(mod, t), `;`));

        if (t.kind === 'union') {
            mod.defineFunctions(nameStr, _ctx =>
                [seq(`export namespace ${nameStr} `, block(
                    ... Array.from(t.variants).flatMap(([vn, vt]) =>
                        genConstructor(mod, nameStr, vn, vn, vt, t, resultTypeItem))
                ))]);
        } else {
            mod.defineFunctions(nameStr, _ctx =>
                genConstructor(mod, nameStr, nameStr, void 0, t, t, resultTypeItem));
        }
    }

    for (const [name0, def] of schema.definitions) {
        const t = typeForDefinition(mod.resolver(), def);
        const name = name0 as symbol;
        const nameStr = name0.description!;
        const resultTypeItem = mod.withAsPreserveMixinType(nameStr, t);

        mod.defineFunctions(nameStr, ctx =>
            [seq(`export function as${name.description!}`, mod.genericParameters(),
                 `(v: _.Value<_embedded>): `, resultTypeItem, ` `,
                 ctx.block(() => [
                     seq(`let result = to${name.description!}(v)`),
                     seq(`if (result === void 0) `,
                         `throw new TypeError(\`Invalid ${name.description!}: \${_.stringify(v)}\`)`),
                     seq(`return result`)]))]);

        mod.defineFunctions(nameStr, ctx =>
            [seq(`export function to${name.description!}`, mod.genericParameters(),
                 `(v: _.Value<_embedded>): undefined | `, resultTypeItem, ` `,
                 ctx.block(() => [seq(`let result: undefined | `, resultTypeItem),
                                  ... converterForDefinition(ctx, def, `v`, `result`),
                                  seq(`return result`)])),
             (t.kind === 'union')
                ? seq(`export namespace ${M.jsId(name.description!)} `, block(
                    seq(`export const __from_preserve__ = to${name.description!}`)))
                : seq(`${M.jsId(name.description!)}.__from_preserve__ = to${name.description!};`)]);

        mod.defineFunctions(nameStr, ctx =>
            [seq(`export function from${name.description!}`, mod.genericParameters(),
                 `(_v: `, name.description!, mod.genericArgsFor(t), `): _.Value<_embedded> `,
                 ctx.block(() => unconverterForDefinition(ctx, def, `_v`)))]);
    }

    mod.definePreamble(
        seq(`export const _imports = `, braces(... Array.from(mod.imports.values()).map(
            ([modulePath, identifier, _path, expr]) =>
                seq(stringify(M.formatModulePath(modulePath)), ': ', identifier, expr)))));

    const f = new Formatter();
    f.write(`import * as _ from ${JSON.stringify(options.preservesModule ?? '@preserves/core')};\n`);
    mod.imports.forEach(([_modulePath, identifier, path, _expr]) => {
        f.write(`import * as ${identifier} from ${JSON.stringify(path)};\n`);
    });
    f.newline();

    const sortedLiterals = Array.from(mod.literals);
    sortedLiterals.sort((a, b) => a[1] < b[1] ? -1 : a[1] === b[1] ? 0 : 1);
    for (const [lit, varname] of sortedLiterals) {
        f.write(seq(`export const ${varname} = `, sourceCodeFor(lit), `;\n`));
    }
    f.newline();

    mod.preamble.forEach(i => {
        f.write(i);
        f.newline();
        f.newline();
    });
    f.newline();

    mod.typedefs.forEach(t => {
        f.write(t);
        f.newline();
        f.newline();
    });
    f.newline();

    mod.functiondefs.forEach(p => {
        f.write(p);
        f.newline();
        f.newline();
    });

    return f.toString();
}
