import {
    CompilerNode,
    Node,
    RawScope,
    simplifyAST,
    setRootScope,
    getScope,
    getRawExp,
    AST,
} from './ast';
import { Scope, file } from './file';
import {
    Package,
    PackageInfo,
    fetchPackage,
    installPackages,
    validatePackage,
} from './package';
import { asciiToUtf8 } from './utils/asciiToUtf8';
import { resolveLib, resolveMain } from './utils/resolveEntryPoint';

export type Motoko = ReturnType<typeof wrapMotoko>;

type Compiler = any; // TODO: generate from `js_of_ocaml`?

export type Diagnostic = {
    source: string;
    range: {
        start: { line: number; character: number };
        end: { line: number; character: number };
    };
    severity: number;
    code: string;
    category: string;
    message: string;
};

export type WasmMode = 'ic' | 'wasi';

export type Result = {
    value?: any;
    error: {
        message?: string;
    } | null;
};

export default function wrapMotoko(compiler: Compiler) {
    const version = compiler.version || '(unknown)';
    const debug = require('debug')(`motoko:${version}`);

    const invoke = (key: string, unwrap: boolean, args: any[]) => {
        if (typeof compiler[key] !== 'function') {
            throw new Error(`Unknown compiler function: '${key}'`);
        }
        let result;
        try {
            result = compiler[key](...args);
        } catch (err) {
            if (err instanceof Error) {
                throw err;
            }
            throw new Error(
                `Unable to execute ${key}(${[...args]
                    .map((x) => typeof x)
                    .join(', ')}):\n${JSON.stringify(err)}`,
            );
        }
        if (!unwrap) {
            return result;
        }
        if (!result.code) {
            throw new Error(
                result.diagnostics
                    ? result.diagnostics
                          .map(({ message }: Diagnostic) => message)
                          .join('; ')
                    : '(no diagnostics)',
            );
        }
        return result.code;
    };

    /* Note: [parseMotokoTyped] is an old API which
       doesn't support scope cache and recovery */
    // Function signatures for `mo.parseMotokoTyped()`
    type ParseMotokoTypedResult = { ast: Node; type: Node };
    function parseMotokoTyped(paths: string): ParseMotokoTypedResult;
    function parseMotokoTyped(paths: string[]): ParseMotokoTypedResult[];
    function parseMotokoTyped(
        paths: string | string[],
    ): ParseMotokoTypedResult | ParseMotokoTypedResult[] {
        if (typeof paths === 'string') {
            return mo.parseMotokoTyped([paths])[0];
        }
        return invoke('parseMotokoTyped', true, [paths]).map(
            ({ ast, typ }: { ast: CompilerNode; typ: CompilerNode }) => {
                return {
                    ast: simplifyAST(ast),
                    type: simplifyAST(typ),
                };
            },
        );
    }

    // Function signatures for `mo.parseMotokoTypedWithScopeCache()`
    type ParseMotokoTypedWithScopeCacheResult = {
        ast: Node;
        type: Node;
        immediateImports: string[];
    };
    function parseMotokoTypedWithScopeCache(
        paths: string,
        scopeCache: Map<string, Scope>,
        enableRecovery?: boolean,
    ): [ParseMotokoTypedWithScopeCacheResult, Map<string, Scope>];
    function parseMotokoTypedWithScopeCache(
        paths: string[],
        scopeCache: Map<string, Scope>,
        enableRecovery?: boolean,
    ): [ParseMotokoTypedWithScopeCacheResult[], Map<string, Scope>];
    function parseMotokoTypedWithScopeCache(
        paths: string | string[],
        scopeCache: Map<string, Scope>,
        enableRecovery?: boolean,
    ): [
        (
            | ParseMotokoTypedWithScopeCacheResult
            | ParseMotokoTypedWithScopeCacheResult[]
        ),
        Map<string, Scope>,
    ] {
        if (enableRecovery === undefined) {
            enableRecovery = false;
        }
        if (typeof paths === 'string') {
            const [progs, outCache] = mo.parseMotokoTypedWithScopeCache(
                [paths],
                scopeCache,
                enableRecovery,
            );
            return [progs[0], outCache];
        }
        const [progs, outCache] = invoke(
            'parseMotokoTypedWithScopeCache',
            true,
            [enableRecovery, paths, scopeCache],
        );
        return [
            progs.map(
                ({
                    ast,
                    typ,
                    immediateImports,
                    scope,
                }: {
                    ast: CompilerNode;
                    typ: CompilerNode;
                    immediateImports: string[];
                    scope: RawScope;
                }) => {
                    const simplifiedAst = simplifyAST(ast);
                    setRootScope(simplifiedAst, scope);
                    return {
                        ast: simplifiedAst,
                        type: simplifyAST(typ),
                        immediateImports,
                    };
                },
            ),
            outCache,
        ];
    }

    const mo = {
        version,
        compiler,
        file(path: string) {
            return file(mo, path);
        },
        read(path: string): string {
            return invoke('readFile', false, [path]);
        },
        write(path: string, content: string = '') {
            if (typeof content !== 'string') {
                throw new Error('Non-string file content');
            }
            debug('+file', path);
            invoke('saveFile', false, [path, content]);
        },
        rename(path: string, newPath: string) {
            invoke('renameFile', false, [path, newPath]);
        },
        delete(path: string) {
            debug('-file', path);
            invoke('removeFile', false, [path]);
        },
        list(directory: string): string[] {
            return invoke('readDir', false, [directory]);
        },
        async fetchPackage(name: string, info: string | PackageInfo) {
            if (!info) {
                throw new Error('Please specify both a name and source');
            }
            return fetchPackage(name, info);
        },
        async installPackages(packages: Record<string, string | PackageInfo>) {
            return installPackages(mo, packages);
        },
        loadPackage(pkg: Package) {
            debug('+package', pkg.name);
            mo.validatePackage(pkg);
            const directory = `.node-motoko/${pkg.name}/${pkg.version}`;
            Object.entries(pkg.files).forEach(([path, file]) => {
                mo.write(`${directory}/${path}`, file.content);
            });
            mo.usePackage(pkg.name, directory);
        },
        usePackage(name: string, directory: string) {
            debug('@package', name, directory);
            invoke('addPackage', false, [name, directory]);
        },
        clearPackages() {
            debug('-packages');
            invoke('clearPackage', false, []);
        },
        validatePackage(pkg: Package) {
            validatePackage(pkg);
        },
        setAliases(directory: string, aliases: Record<string, string>) {
            debug('aliases', directory, aliases);
            invoke('setCandidPath', false, [directory]);
            invoke('setActorAliases', false, [Object.entries(aliases)]);
        },
        setPublicMetadata(values: string[]) {
            invoke('setPublicMetadata', false, [values]);
        },
        setRunStepLimit(limit: number) {
            invoke('setRunStepLimit', false, [limit]);
        },
        setTypecheckerCombineSrcs(combineSrcs: boolean) {
            invoke('setTypecheckerCombineSrcs', false, [combineSrcs]);
        },
        setExtraFlags(argv: string[]) {
            if (!Array.isArray(argv)) {
                throw new Error('extra flags must be an array of strings');
            }
            invoke('setExtraFlags', false, [argv]);
        },
        check(path: string): Diagnostic[] {
            const result = invoke('check', false, [path]);
            return result.diagnostics;
        },
        checkWithScopeCache(
            path: string,
            scopeCache: Map<string, Scope>,
        ): {
            diagnostics: Diagnostic[];
            scopeCache: Map<string, Scope> | null;
        } {
            return invoke('checkWithScopeCache', false, [path, scopeCache]);
        },
        run(
            path: string,
            libPaths?: string[] | undefined,
        ): { stdout: string; stderr: string; result: Result } {
            const run = invoke('run', false, [libPaths || [], path]);
            run.stdout = asciiToUtf8(run.stdout);
            run.stderr = asciiToUtf8(run.stderr);
            return run;
        },
        candid(path: string): string {
            return invoke('candid', true, [path]);
        },
        wasm(path: string, mode: WasmMode) {
            if (!mode) {
                mode = 'ic';
            } else if (mode !== 'ic' && mode !== 'wasi') {
                throw new Error(`Invalid WASM format: ${mode}`);
            }
            return invoke('compileWasm', true, [mode, path]);
        },
        parseCandid(content: string): object {
            return invoke('parseCandid', true, [content]);
        },
        parseMotoko(content: string, enableRecovery = false): Node {
            const ast = invoke('parseMotoko', true, [enableRecovery, content]);
            return simplifyAST(ast);
        },
        parseMotokoWithDeps(
            path: string,
            content: string,
            enableRecovery = false,
        ): { ast: Node; immediateImports: string[] } {
            const { ast, immediateImports } = invoke(
                'parseMotokoWithDeps',
                true,
                [enableRecovery, path, content],
            );
            return { ast: simplifyAST(ast), immediateImports };
        },
        parseMotokoTyped,
        parseMotokoTypedWithScopeCache,
        contextualDotSuggestions(
            node: Node,
            program: { ast: AST },
        ):
            | {
                  moduleUri: string;
                  funcName: string;
                  funcType: string;
              }[]
            | undefined {
            const rawExp = getRawExp(node);
            const scope = getScope(program.ast);
            if (!rawExp || !scope) return undefined;
            return invoke('contextualDotSuggestions', false, [scope, rawExp]);
        },
        contextualDotModule(
            node: Node,
        ): { moduleNameOrUri: string; funcName: string } | undefined {
            const rawExp = getRawExp(node);
            if (!rawExp) return undefined;
            return invoke('contextualDotModule', false, [rawExp]) ?? undefined;
        },
        resolveMain(directory: string = ''): string | undefined {
            return resolveMain(mo, directory);
        },
        resolveLib(directory: string = ''): string | undefined {
            return resolveLib(mo, directory);
        },
    };
    // @ts-ignore
    mo.default = mo;
    return mo;
}
