#!/usr/bin/env node
import * as child_process from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import * as url from 'url';

import * as ts from 'typescript';

import packageJson from '../package.json';

import { mainCommand, MultiProjectOptions, ProjectOptions } from './CommandLineOptions';
import { inferTsconfig } from './inferTsconfig';
import * as lsif from './lsif';
import { ProjectIndexer } from './ProjectIndexer';
import { Counter } from './Counter';
import { Metadata, Project, ToolInfo } from './lsif-data/lsif';

export const lsiftyped = lsif.lib.codeintel.lsiftyped;

export function main(): void {
    mainCommand((projects, options) => indexCommand(projects, options)).parse(process.argv);
    return;
}

export function indexCommand(projects: string[], options: MultiProjectOptions): void {
    if (options.yarnWorkspaces) {
        projects.push(...listYarnWorkspaces(options.cwd));
    } else if (options.yarnBerryWorkspaces) {
        projects.push(...listYarnBerryWorkspaces(options.cwd));
    } else if (projects.length === 0) {
        projects.push(options.cwd);
    }
    options.cwd = makeAbsolutePath(process.cwd(), options.cwd);
    options.output = makeAbsolutePath(options.cwd, options.output);
    if (!options.indexedProjects) {
        options.indexedProjects = new Set();
    }
    const output = fs.openSync(options.output, 'w');
    let documentCount = 0;
    let counter = new Counter();
    const writeIndex = (index: any): void => {
        if (index.documents) {
            documentCount += index.documents.length;
        }
        fs.writeSync(output, JSON.stringify(index.toObject()) + '\n');
        // SourceField --> change to output json
    };
    try {
        writeIndex(
            new Metadata({
                id: counter.next(),
                type: 'vertex',
                label: 'metaData',
                version: '0.6.0-sourcefield',
                projectRoot: url.pathToFileURL(options.cwd).toString(),
                positionEncoding: 'utf-8',
                toolInfo: new ToolInfo({ name: 'lsif-typescript', version: packageJson.version }),
            })
        );
        writeIndex(new Project({ id: counter.next(), type: 'vertex', label: 'project', kind: 'Js/TS' }));
        // NOTE: we may want index these projects in parallel in the future.
        // We need to be careful about which order we index the projects because
        // they can have dependencies.
        for (const projectRoot of projects) {
            const projectDisplayName = projectRoot === '.' ? options.cwd : projectRoot;
            indexSingleProject({
                ...options,
                projectRoot,
                projectDisplayName,
                writeIndex,
                counter,
            });
        }
    } finally {
        fs.close(output);
        console.log(`done ${options.output}`);
    }
}

function makeAbsolutePath(cwd: string, relativeOrAbsolutePath: string): string {
    if (path.isAbsolute(relativeOrAbsolutePath)) {
        return relativeOrAbsolutePath;
    }
    return path.resolve(cwd, relativeOrAbsolutePath);
}

function indexSingleProject(options: ProjectOptions): void {
    if (options.indexedProjects.has(options.projectRoot)) {
        return;
    }
    options.indexedProjects.add(options.projectRoot);
    let config = ts.parseCommandLine(['-p', options.projectRoot], (relativePath: string) =>
        path.resolve(options.projectRoot, relativePath)
    );
    let tsconfigFileName: string | undefined;
    if (config.options.project) {
        const projectPath = path.resolve(config.options.project);
        if (ts.sys.directoryExists(projectPath)) {
            tsconfigFileName = path.join(projectPath, 'tsconfig.json');
        } else {
            tsconfigFileName = projectPath;
        }
        if (!ts.sys.fileExists(tsconfigFileName)) {
            if (options.inferTsconfig) {
                fs.writeFileSync(tsconfigFileName, inferTsconfig(projectPath));
            } else {
                console.error(`- ${options.projectDisplayName} (missing tsconfig.json)`);
                return;
            }
        }
        config = loadConfigFile(tsconfigFileName);
    }

    for (const projectReference of config.projectReferences || []) {
        indexSingleProject({
            ...options,
            projectRoot: projectReference.path,
            projectDisplayName: projectReference.path,
        });
    }

    if (config.fileNames.length > 0) {
        new ProjectIndexer(config, options).index();
    }
}

if (require.main === module) {
    main();
}

function loadConfigFile(file: string): ts.ParsedCommandLine {
    const absolute = path.resolve(file);

    const readResult = ts.readConfigFile(absolute, (path) => ts.sys.readFile(path));

    if (readResult.error) {
        throw new Error(ts.formatDiagnostics([readResult.error], ts.createCompilerHost({})));
    }
    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
    const config = readResult.config;
    // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
    if (config.compilerOptions !== undefined) {
        // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
        config.compilerOptions = {
            // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
            ...config.compilerOptions,
            ...defaultCompilerOptions(file),
        };
    }
    const basePath = path.dirname(absolute);
    const result = ts.parseJsonConfigFileContent(config, ts.sys, basePath);
    const errors: ts.Diagnostic[] = [];
    for (const error of result.errors) {
        if (error.code === 18003) {
            // Ignore errors about missing 'input' fields, example:
            // > TS18003: No inputs were found in config file 'tsconfig.json'. Specified 'include' paths were '[]' and 'exclude' paths were '["out","node_modules","dist"]'.
            // The reason we ignore this error here is because we report the same
            // error at a higher-level.  It's common to hit on a single TypeScript
            // project with no sources when using the --yarnWorkspaces option.
            // Instead of failing fast at that single project, we only report this
            // error if all projects have no files.
            continue;
        }
        errors.push(error);
    }
    if (errors.length > 0) {
        console.log({ absolute });
        throw new Error(ts.formatDiagnostics(errors, ts.createCompilerHost({})));
    }
    return result;
}

function defaultCompilerOptions(configFileName?: string): ts.CompilerOptions {
    const options: ts.CompilerOptions =
        // Not a typo, jsconfig.json is a thing https://sourcegraph.com/search?q=context:global+file:jsconfig.json&patternType=literal
        configFileName && path.basename(configFileName) === 'jsconfig.json'
            ? {
                  allowJs: true,
                  maxNodeModuleJsDepth: 2,
                  allowSyntheticDefaultImports: true,
                  skipLibCheck: true,
                  noEmit: true,
              }
            : {};
    return options;
}

function listYarnBerryWorkspaces(directory: string): string[] {
    const result: string[] = [];
    const lines = child_process
        .execSync('yarn workspaces list --json', {
            cwd: directory,
            encoding: 'utf-8',
        })
        .split('\n');
    for (const line of lines) {
        if (!line) {
            continue;
        }
        const location = 'location';
        const json = JSON.parse(line);
        if (json[location] !== undefined) {
            // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access
            result.push(path.join(directory, json[location]));
        }
    }
    return result;
}

function listYarnWorkspaces(directory: string): string[] {
    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
    const json = JSON.parse(
        // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access
        JSON.parse(
            child_process.execSync('yarn --silent --json workspaces info', {
                cwd: directory,
                encoding: 'utf-8',
            })
        ).data
    );

    const result: string[] = [];
    // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
    for (const key of Object.keys(json)) {
        const location = 'location';
        // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
        if (json[key][location] !== undefined) {
            // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access
            result.push(path.join(directory, json[key][location]));
        }
    }
    return result;
}
