import { compile } from '../index';
import fs from 'fs';
import path from 'path';
import minimatch from 'minimatch';
import { Command } from 'commander';
import * as M from '../meta';
import chalk from 'chalk';
import { is, Position } from '@preserves/core';
import chokidar from 'chokidar';
import { changeExt, Diagnostic, inputToInputGlob, expandInputGlob, formatFailures } from './cli-utils';

export type CommandLineArguments = {
    inputs: string[];
    xrefs: string[];
    output: string | undefined;
    stdout: boolean;
    core: string;
    watch: boolean;
    traceback: boolean;
    module: string[];
};

export type CompilationResult = {
    options: CommandLineArguments,
    inputFiles: Array<TranslatedFile>,
    failures: Array<Diagnostic>,
    baseDirs: Array<string>,
};

export type TranslatedFile = {
    inputBaseDir: string,
    inputFilePath: string,
    outputFilePath: string,
    schemaPath: M.ModulePath,
    schema: M.Schema,
};

function failureCount(type: 'warn' | 'error', r: CompilationResult): number {
    return r.failures.filter(f => f.type === type).length;
}

export function run(options: CommandLineArguments): void {
    if (!options.watch) {
        if (failureCount('error', runOnce(options)) > 0) {
            process.exit(1);
        }
    } else {
        function runWatch() {
            console.clear();
            console.log(chalk.gray(new Date().toISOString()) +
                ' Compiling Schemas in watch mode...\n');
            const r = runOnce(options);
            const warningCount = failureCount('warn', r);
            const errorCount = failureCount('error', r);
            const wMsg = (warningCount > 0) && chalk.yellowBright(`${warningCount} warning(s)`);
            const eMsg = (errorCount > 0) && chalk.redBright(`${errorCount} error(s)`);
            const errorSummary =
                (wMsg && eMsg) ? `with ${eMsg} and ${wMsg}` :
                (wMsg) ? `with ${wMsg}` :
                (eMsg) ? `with ${eMsg}` :
                chalk.greenBright('successfully');
            console.log(chalk.gray(new Date().toISOString()) +
                ` Processed ${r.inputFiles.length} file(s) ${errorSummary}. Waiting for changes.`);
            let triggered = false;
            const watchers = r.baseDirs.map(base => chokidar.watch(base, {
                ignoreInitial: true,
            }).on('all', (_event, filename) => {
                const relevant = options.inputs.some(i => {
                    const { baseDir, glob } = inputToInputGlob(i);
                    return minimatch(filename, baseDir + glob);
                });
                if (relevant) {
                    if (!triggered) {
                        triggered = true;
                        watchers.map(w => w.close());
                        runWatch();
                    }
                }
            }));
        }
        runWatch();
    }
}

export function modulePathTo(file1: string, file2: string): string {
    let naive = path.relative(path.dirname(file1), file2);
    if (naive[0] !== '.' && naive[0] !== '/') naive = './' + naive;
    return changeExt(naive, '');
}

function isAbsoluteOrExplicitlyRelative(p: string) {
    return p[0] === '.' || p[0] === '/';
}

export function runOnce(options: CommandLineArguments): CompilationResult {
    const { baseDirs, failures, inputFiles: inputFiles0, xrefFiles: xrefFiles0 } =
        expandInputGlob(options.inputs, options.xrefs);

    let xrefFiles = xrefFiles0; // filtered during construction of extensionEnv

    const extensionEnv: M.Environment = options.module.map(arg => {
        const i = arg.indexOf('=');
        if (i === -1) throw new Error(`--module argument must be Namespace=path or Namespace=path:expr; got ${arg}`);
        const ns = arg.slice(0, i);
        const pathAndExpr = arg.slice(i + 1);
        const j = pathAndExpr.lastIndexOf(':');
        const path = (j === -1) ? pathAndExpr : pathAndExpr.slice(0, j);
        const modulePath = ns.split('.').map(Symbol.for);
        const expr = (j === -1) ? null : pathAndExpr.slice(j + 1);
        const e = xrefFiles.find(x => is(x.modulePath, modulePath));
        if (e) xrefFiles = xrefFiles.filter(e0 => !Object.is(e0, e));
        return {
            schema: e ? e.schema : null,
            schemaModulePath: modulePath,
            typescriptModulePath: path,
            typescriptModuleExpr: expr,
        };
    });

    const inputFiles: Array<TranslatedFile> = inputFiles0.map(i => {
        const { inputBaseDir, inputFilePath, baseRelPath, modulePath, schema } = i;
        const outputFilePath = path.join(options.output ?? '.', changeExt(baseRelPath, '.ts'));
        return { inputBaseDir, inputFilePath, outputFilePath, schemaPath: modulePath, schema };
    });

    inputFiles.forEach(c => {
        const env: M.Environment = [
            ... extensionEnv.flatMap(e => {
                const p = isAbsoluteOrExplicitlyRelative(e.typescriptModulePath)
                    ? modulePathTo(c.outputFilePath, e.typescriptModulePath)
                    : e.typescriptModulePath /* assuming it names something in node_modules */;
                if (p === null) return [];
                return [{... e, typescriptModulePath: p}];
            }),
            ... xrefFiles.map(x => ({
                schema: x.schema,
                schemaModulePath: x.modulePath,
                typescriptModulePath: 'xref-typescript-modules-not-available',
                typescriptModuleExpr: null,
            })),
            ... inputFiles.map(cc => ({
                schema: cc.schema,
                schemaModulePath: cc.schemaPath,
                typescriptModulePath: modulePathTo(c.outputFilePath, cc.outputFilePath),
                typescriptModuleExpr: null,
            })),
        ];
        fs.mkdirSync(path.dirname(c.outputFilePath), { recursive: true });
        let compiledModule;
        try {
            compiledModule = compile(env, c.schemaPath, c.schema, {
                preservesModule: options.core,
                warn: (message: string, pos: Position | null) =>
                    failures.push({ type: 'warn', file: c.inputFilePath, detail: { message, pos } }),
            });
        } catch (e) {
            failures.push({ type: 'error', file: c.inputFilePath, detail: e as Error });
        }
        if (compiledModule !== void 0) {
            if (options.stdout) {
                console.log('////------------------------------------------------------------');
                console.log('//// ' + c.outputFilePath);
                console.log();
                console.log(compiledModule);
            } else {
                fs.writeFileSync(c.outputFilePath, compiledModule, 'utf-8');
            }
        }
    });

    formatFailures(failures, options.traceback);

    return { options, inputFiles, failures, baseDirs };
}

export function main(argv: Array<string>) {
    new Command()
        .arguments('[input...]')
        .description('Compile Preserves schema definitions to TypeScript', {
            input: 'Input directory, optionally with :<glob> on the end',
        })
        .option('--xref <glob>', 'Cross-reference other textual Preserves schema definitions',
                (glob, prev: string[]) => [... prev, glob], [])
        .option('--output <directory>', 'Output directory for modules')
        .option('--stdout', 'Prints each module to stdout one after the other instead ' +
            'of writing them to files in the `--output` directory')
        .option('--core <path>', 'Import path for @preserves/core', '@preserves/core')
        .option('--watch', 'Watch base directory for changes')
        .option('--traceback', 'Include stack traces in compiler errors')
        .option('--module <namespace=path>', 'Additional Namespace=path import',
                (nsPath: string, previous: string[]): string[] => [... previous, nsPath],
                [])
        .action((inputs: string[], rawOptions) => {
            if ((rawOptions.output === void 0 && !rawOptions.stdout) ||
                (rawOptions.output !== void 0 && rawOptions.stdout))
            {
                throw new Error("Either --output or --stdout (but not both) must be supplied.");
            }
            const options: CommandLineArguments = {
                inputs: inputs.map(i => path.normalize(i)),
                xrefs: rawOptions.xref.map((x: string) => path.normalize(x)),
                output: rawOptions.output,
                stdout: rawOptions.stdout,
                core: rawOptions.core,
                watch: rawOptions.watch,
                traceback: rawOptions.traceback,
                module: rawOptions.module,
            };
            Error.stackTraceLimit = Infinity;
            run(options);
        })
        .parse(argv, { from: 'user' });
}
