import fs = require("fs");
import yargs = require("yargs");
import headerParser = require("@definitelytyped/header-parser");
import path = require("path");
import cp = require("child_process");
import {
    dtsCritic,
    dtToNpmName,
    getNpmInfo,
    parseExportErrorKind,
    CriticError,
    ExportErrorKind,
    Mode,
    checkSource,
    findDtsName,
    CheckOptions,
    parseMode} from "./index";

const sourcesDir = "sources";
const downloadsPath = path.join(sourcesDir, "dts-critic-internal/downloads.json");
const isNpmPath = path.join(sourcesDir, "dts-critic-internal/npm.json");

function getPackageDownloads(dtName: string): number {
    const npmName = dtToNpmName(dtName);
    const url = `https://api.npmjs.org/downloads/point/last-month/${npmName}`;
    const result = JSON.parse(
        cp.execFileSync(
            "curl",
            ["--silent", "-L", url],
            { encoding: "utf8" })) as { downloads?: number };
    return result.downloads || 0;
}

interface DownloadsJson { [key: string]: number | undefined }

function getAllPackageDownloads(dtPath: string): DownloadsJson {
    if (fs.existsSync(downloadsPath)) {
        return JSON.parse(fs.readFileSync(downloadsPath, { encoding: "utf8" })) as DownloadsJson;
    }

    initDir(path.dirname(downloadsPath));
    const downloads: DownloadsJson = {};
    const dtTypesPath = getDtTypesPath(dtPath);
    for (const item of fs.readdirSync(dtTypesPath)) {
        const d = getPackageDownloads(item);
        downloads[item] = d;
    }
    fs.writeFileSync(downloadsPath, JSON.stringify(downloads), { encoding: "utf8" });

    return downloads;
}

function initDir(path: string): void {
    if (!fs.existsSync(path)) {
        fs.mkdirSync(path);
    }
}

function getDtTypesPath(dtBasePath: string): string {
    return path.join(dtBasePath, "types");
}

function compareDownloads(downloads: DownloadsJson, package1: string, package2: string): number {
    const count1 = downloads[package1] || 0;
    const count2 = downloads[package2] || 0;
    return count1 - count2;
}

interface IsNpmJson { [key: string]: boolean | undefined }

function getAllIsNpm(dtPath: string): IsNpmJson {
    if (fs.existsSync(isNpmPath)) {
        return JSON.parse(fs.readFileSync(isNpmPath, { encoding: "utf8" })) as IsNpmJson;
    }
    initDir(path.dirname(isNpmPath));
    const isNpm: IsNpmJson = {};
    const dtTypesPath = getDtTypesPath(dtPath);
    for (const item of fs.readdirSync(dtTypesPath)) {
        isNpm[item] = getNpmInfo(item).isNpm;
    }
    fs.writeFileSync(isNpmPath, JSON.stringify(isNpm), { encoding: "utf8" });
    return isNpm;
}

function getPopularNpmPackages(count: number, dtPath: string): string[] {
    const dtPackages = getDtNpmPackages(dtPath);
    const downloads = getAllPackageDownloads(dtPath);
    dtPackages.sort((a, b) => compareDownloads(downloads, a, b));
    return dtPackages.slice(dtPackages.length - count);
}

function getUnpopularNpmPackages(count: number, dtPath: string): string[] {
    const dtPackages = getDtNpmPackages(dtPath);
    const downloads = getAllPackageDownloads(dtPath);
    dtPackages.sort((a, b) => compareDownloads(downloads, a, b));
    return dtPackages.slice(0, count);
}

function getDtNpmPackages(dtPath: string): string[] {
    const dtPackages = fs.readdirSync(getDtTypesPath(dtPath));
    const isNpmJson = getAllIsNpm(dtPath);
    return dtPackages.filter(pkg => isNpmPackage(pkg, /* header */ undefined, isNpmJson));
}

function getNonNpm(args: { dtPath: string }): void {
    const nonNpm: string[] = [];
    const dtTypesPath = getDtTypesPath(args.dtPath);
    const isNpmJson = getAllIsNpm(args.dtPath);
    for (const item of fs.readdirSync(dtTypesPath)) {
        const entry = path.join(dtTypesPath, item);
        const dts = fs.readFileSync(entry + "/index.d.ts", "utf8");
        let header;
        try {
            header = headerParser.parseHeaderOrFail(dts);
        }
        catch (e) {
            header = undefined;
        }
        if (!isNpmPackage(item, header, isNpmJson)) {
            nonNpm.push(item);
        }
    }
    console.log(`List of non-npm packages on DT:\n${nonNpm.map(name => `DT name: ${name}\n`).join("")}`);
}

interface CommonArgs {
    dtPath: string,
    mode: string,
    enableError: string[] | undefined,
    debug: boolean,
    json: boolean,
}

function checkAll(args: CommonArgs): void {
    const dtPackages = fs.readdirSync(getDtTypesPath(args.dtPath));
    checkPackages({ packages: dtPackages, ...args });
}

function checkPopular(args: { count: number } & CommonArgs): void {
    checkPackages({ packages: getPopularNpmPackages(args.count, args.dtPath), ...args });
}

function checkUnpopular(args: { count: number } & CommonArgs): void {
    checkPackages({ packages: getUnpopularNpmPackages(args.count, args.dtPath), ...args });
}

function checkPackages(args: { packages: string[] } & CommonArgs): void {
    const results = args.packages.map(pkg => doCheck({ package: pkg, ...args }));
    printResults(results, args.json);
}

function checkPackage(args: { package: string } & CommonArgs): void {
    printResults([doCheck(args)], args.json);
}

function doCheck(args: { package: string, dtPath: string, mode: string, enableError: string[] | undefined, debug: boolean }): Result {
    const dtPackage = args.package;
    const opts = getOptions(args.mode, args.enableError || []);
    try {
        const dtsPath = path.join(getDtTypesPath(args.dtPath), dtPackage, "index.d.ts");
        const errors = dtsCritic(dtsPath, /* sourcePath */ undefined, opts, args.debug);
        return { package: args.package, output: errors };
    }
    catch (e) {
        return { package: args.package, output: e.toString() };
    }
}

function getOptions(modeArg: string, enabledErrors: string[]): CheckOptions {
    const mode = parseMode(modeArg);
    if (!mode) {
        throw new Error(`Could not find mode named '${modeArg}'.`);
    }
    switch (mode) {
        case Mode.NameOnly:
            return { mode };
        case Mode.Code:
            const errors = getEnabledErrors(enabledErrors);
            return { mode, errors };
    }
}

function getEnabledErrors(errorNames: string[]): Map<ExportErrorKind, boolean> {
    const errors: ExportErrorKind[] = [];
    for (const name of errorNames) {
        const error = parseExportErrorKind(name);
        if (error === undefined) {
            throw new Error(`Could not find error named '${name}'.`);
        }
        errors.push(error);
    }
    return new Map(errors.map(err => [err, true]));
}

function checkFile(args: { jsFile: string, dtsFile: string, debug: boolean }): void {
    console.log(`\tChecking JS file ${args.jsFile} and declaration file ${args.dtsFile}`);
    try {
        const errors = checkSource(findDtsName(args.dtsFile), args.dtsFile, args.jsFile, new Map(), args.debug);
        console.log(formatErrors(errors));
    }
    catch (e) {
        console.log(e);
    }
}

interface Result {
    package: string,
    output: CriticError[] | string,
}

function printResults(results: Result[], json: boolean): void {
    if (json) {
        console.log(JSON.stringify(results));
        return;
    }

    for (const result of results) {
        console.log(`\tChecking package ${result.package} ...`);
        if (typeof result.output === "string") {
            console.log(`Exception:\n${result.output}`);
        }
        else {
            console.log(formatErrors(result.output));
        }
    }
}

function formatErrors(errors: CriticError[]): string {
    const lines: string[] = [];
    for (const error of errors) {
        lines.push("Error: " + error.message);
    }
    if (errors.length === 0) {
        lines.push("No errors found! :)");
    }
    return lines.join("\n");
}

function isNpmPackage(name: string, header?: headerParser.Header, isNpmJson: IsNpmJson = {}): boolean {
    if (header && header.nonNpm) return false;
    const isNpm = isNpmJson[name];
    if (isNpm !== undefined) {
        return isNpm;
    }
    return getNpmInfo(name).isNpm;
}

function main() {
    // eslint-disable-next-line no-unused-expressions
    yargs
        .usage("$0 <command>")
        .command("check-all", "Check source and declaration of all DT packages that are on NPM.", {
            dtPath: {
                type: "string",
                default: "../DefinitelyTyped",
                describe: "Path of DT repository cloned locally.",
            },
            mode: {
                type: "string",
                required: true,
                choices: [Mode.NameOnly, Mode.Code],
                describe: "Mode that defines which group of checks will be made.",
            },
            enableError: {
                type: "array",
                string: true,
                describe: "Enable checking for a specific export error."
            },
            debug: {
                type: "boolean",
                default: false,
                describe: "Turn debug logging on.",
            },
            json: {
                type: "boolean",
                default: false,
                describe: "Format output result as json."
            },
        }, checkAll)
        .command("check-popular", "Check source and declaration of most popular DT packages that are on NPM.", {
            count: {
                alias: "c",
                type: "number",
                required: true,
                describe: "Number of packages to be checked.",
            },
            dtPath: {
                type: "string",
                default: "../DefinitelyTyped",
                describe: "Path of DT repository cloned locally.",
            },
            mode: {
                type: "string",
                required: true,
                choices: [Mode.NameOnly, Mode.Code],
                describe: "Mode that defines which group of checks will be made.",
            },
            enableError: {
                type: "array",
                string: true,
                describe: "Enable checking for a specific export error."
            },
            debug: {
                type: "boolean",
                default: false,
                describe: "Turn debug logging on.",
            },
            json: {
                type: "boolean",
                default: false,
                describe: "Format output result as json."
            },
        }, checkPopular)
        .command("check-unpopular", "Check source and declaration of least popular DT packages that are on NPM.", {
            count: {
                alias: "c",
                type: "number",
                required: true,
                describe: "Number of packages to be checked.",
            },
            dtPath: {
                type: "string",
                default: "../DefinitelyTyped",
                describe: "Path of DT repository cloned locally.",
            },
            mode: {
                type: "string",
                required: true,
                choices: [Mode.NameOnly, Mode.Code],
                describe: "Mode that defines which group of checks will be made.",
            },
            enableError: {
                type: "array",
                string: true,
                describe: "Enable checking for a specific export error."
            },
            debug: {
                type: "boolean",
                default: false,
                describe: "Turn debug logging on.",
            },
            json: {
                type: "boolean",
                default: false,
                describe: "Format output result as json."
            },
        }, checkUnpopular)
        .command("check-package", "Check source and declaration of a DT package.", {
            package: {
                alias: "p",
                type: "string",
                required: true,
                describe: "DT name of a package."
            },
            dtPath: {
                type: "string",
                default: "../DefinitelyTyped",
                describe: "Path of DT repository cloned locally.",
            },
            mode: {
                type: "string",
                required: true,
                choices: [Mode.NameOnly, Mode.Code],
                describe: "Mode that defines which group of checks will be made.",
            },
            enableError: {
                type: "array",
                string: true,
                describe: "Enable checking for a specific export error."
            },
            debug: {
                type: "boolean",
                default: false,
                describe: "Turn debug logging on.",
            },
            json: {
                type: "boolean",
                default: false,
                describe: "Format output result as json."
            },
        }, checkPackage)
        .command("check-file", "Check a JavaScript file and its matching declaration file.", {
            jsFile: {
                alias: "j",
                type: "string",
                required: true,
                describe: "Path of JavaScript file.",
            },
            dtsFile: {
                alias: "d",
                type: "string",
                required: true,
                describe: "Path of declaration file.",
            },
            debug: {
                type: "boolean",
                default: false,
                describe: "Turn debug logging on.",
            },
        }, checkFile)
        .command("get-non-npm", "Get list of DT packages whose source package is not on NPM", {
            dtPath: {
                type: "string",
                default: "../DefinitelyTyped",
                describe: "Path of DT repository cloned locally.",
            },
        }, getNonNpm)
        .demandCommand(1)
        .help()
        .argv;
}
main();
