'use strict';

Object.defineProperty(exports, '__esModule', { value: true });

var utils = require('../utils');
var node = require('../sys/node');

function getConfigFilePath(process, sys, configArg) {
    if (configArg) {
        if (!sys.path.isAbsolute(configArg)) {
            // passed in a custom stencil config location
            // but it's relative, so prefix the cwd
            return utils.normalizePath(sys.path.join(process.cwd(), configArg));
        }
        // config path already an absolute path, we're good here
        return utils.normalizePath(configArg);
    }
    // nothing was passed in, use the current working directory
    return utils.normalizePath(process.cwd());
}
function hasError(diagnostics) {
    if (!diagnostics) {
        return false;
    }
    return diagnostics.some(d => d.level === 'error' && d.type !== 'runtime');
}

function parseFlags(process) {
    const flags = {
        task: null,
        args: [],
        knownArgs: [],
        unknownArgs: null
    };
    // cmd line has more priority over npm scripts cmd
    flags.args = process.argv.slice(2);
    if (flags.args.length > 0 && flags.args[0] && !flags.args[0].startsWith('-')) {
        flags.task = flags.args[0];
    }
    parseArgs(flags, flags.args, flags.knownArgs);
    const npmScriptCmdArgs = getNpmScriptArgs(process);
    parseArgs(flags, npmScriptCmdArgs, flags.knownArgs);
    npmScriptCmdArgs.forEach(npmArg => {
        if (!flags.args.includes(npmArg)) {
            flags.args.push(npmArg);
        }
    });
    if (flags.task != null) {
        const i = flags.args.indexOf(flags.task);
        if (i > -1) {
            flags.args.splice(i, 1);
        }
    }
    flags.unknownArgs = flags.args.filter((arg) => {
        return !flags.knownArgs.includes(arg);
    });
    return flags;
}
function parseArgs(flags, args, knownArgs) {
    ARG_OPTS.boolean.forEach(booleanName => {
        const alias = ARG_OPTS.alias[booleanName];
        const flagKey = configCase(booleanName);
        if (typeof flags[flagKey] !== 'boolean') {
            flags[flagKey] = null;
        }
        args.forEach(cmdArg => {
            if (cmdArg === `--${booleanName}`) {
                flags[flagKey] = true;
                knownArgs.push(cmdArg);
            }
            else if (cmdArg === `--no-${booleanName}`) {
                flags[flagKey] = false;
                knownArgs.push(cmdArg);
            }
            else if (alias && cmdArg === `-${alias}`) {
                flags[flagKey] = true;
                knownArgs.push(cmdArg);
            }
        });
    });
    ARG_OPTS.string.forEach(stringName => {
        const alias = ARG_OPTS.alias[stringName];
        const flagKey = configCase(stringName);
        if (typeof flags[flagKey] !== 'string') {
            flags[flagKey] = null;
        }
        for (let i = 0; i < args.length; i++) {
            const cmdArg = args[i];
            if (cmdArg.startsWith(`--${stringName}=`)) {
                const values = cmdArg.split('=');
                values.shift();
                flags[flagKey] = values.join('=');
                knownArgs.push(cmdArg);
            }
            else if (cmdArg === `--${stringName}`) {
                flags[flagKey] = args[i + 1];
                knownArgs.push(cmdArg);
                knownArgs.push(args[i + 1]);
            }
            else if (alias) {
                if (cmdArg.startsWith(`-${alias}=`)) {
                    const values = cmdArg.split('=');
                    values.shift();
                    flags[flagKey] = values.join('=');
                    knownArgs.push(cmdArg);
                }
                else if (cmdArg === `-${alias}`) {
                    flags[flagKey] = args[i + 1];
                    knownArgs.push(args[i + 1]);
                }
            }
        }
    });
    ARG_OPTS.number.forEach(numberName => {
        const alias = ARG_OPTS.alias[numberName];
        const flagKey = configCase(numberName);
        if (typeof flags[flagKey] !== 'number') {
            flags[flagKey] = null;
        }
        for (let i = 0; i < args.length; i++) {
            const cmdArg = args[i];
            if (cmdArg.startsWith(`--${numberName}=`)) {
                const values = cmdArg.split('=');
                values.shift();
                flags[flagKey] = parseInt(values.join(''), 10);
                knownArgs.push(cmdArg);
            }
            else if (cmdArg === `--${numberName}`) {
                flags[flagKey] = parseInt(args[i + 1], 10);
                knownArgs.push(args[i + 1]);
            }
            else if (alias) {
                if (cmdArg.startsWith(`-${alias}=`)) {
                    const values = cmdArg.split('=');
                    values.shift();
                    flags[flagKey] = parseInt(values.join(''), 10);
                    knownArgs.push(cmdArg);
                }
                else if (cmdArg === `-${alias}`) {
                    flags[flagKey] = parseInt(args[i + 1], 10);
                    knownArgs.push(args[i + 1]);
                }
            }
        }
    });
    return flags;
}
function configCase(prop) {
    prop = utils.dashToPascalCase(prop);
    return prop.charAt(0).toLowerCase() + prop.substr(1);
}
const ARG_OPTS = {
    boolean: [
        'build',
        'cache',
        'check-version',
        'ci',
        'compare',
        'debug',
        'dev',
        'docs',
        'e2e',
        'es5',
        'esm',
        'headless',
        'help',
        'log',
        'open',
        'prerender',
        'prerender-external',
        'prod',
        'profile',
        'service-worker',
        'screenshot',
        'serve',
        'skip-node-check',
        'spec',
        'stats',
        'update-screenshot',
        'version',
        'watch'
    ],
    number: [
        'max-workers',
        'port'
    ],
    string: [
        'address',
        'config',
        'docs-json',
        'emulate',
        'log-level',
        'root',
        'screenshot-connector'
    ],
    alias: {
        'config': 'c',
        'help': 'h',
        'port': 'p',
        'version': 'v'
    }
};
function getNpmScriptArgs(process) {
    // process.env.npm_config_argv
    // {"remain":["4444"],"cooked":["run","serve","--port","4444"],"original":["run","serve","--port","4444"]}
    let args = [];
    try {
        if (process.env) {
            const npmConfigArgs = process.env.npm_config_argv;
            if (npmConfigArgs) {
                args = JSON.parse(npmConfigArgs).original;
                if (args[0] === 'run') {
                    args = args.slice(2);
                }
            }
        }
    }
    catch (e) { }
    return args;
}

/*
 * exit
 * https://github.com/cowboy/node-exit
 *
 * Copyright (c) 2013 "Cowboy" Ben Alman
 * Licensed under the MIT license.
 */

var exit = function exit(exitCode, streams) {
  if (!streams) { streams = [process.stdout, process.stderr]; }
  var drainCount = 0;
  // Actually exit if all streams are drained.
  function tryToExit() {
    if (drainCount === streams.length) {
      process.exit(exitCode);
    }
  }
  streams.forEach(function(stream) {
    // Count drained streams now, but monitor non-drained streams.
    if (stream.bufferSize === 0) {
      drainCount++;
    } else {
      stream.write('', 'utf-8', function() {
        drainCount++;
        tryToExit();
      });
    }
    // Prevent further writing.
    stream.write = function() {};
  });
  // If all streams were already drained, exit now.
  tryToExit();
  // In Windows, when run as a Node.js child process, a script utilizing
  // this library might just exit with a 0 exit code, regardless. This code,
  // despite the fact that it looks a bit crazy, appears to fix that.
  process.on('exit', function() {
    process.exit(exitCode);
  });
};

async function taskBuild(process, config, flags) {
    const { Compiler } = require('../compiler/index.js');
    const compiler = new Compiler(config);
    if (!compiler.isValid) {
        exit(1);
    }
    let devServerStart = null;
    if (config.devServer && flags.serve) {
        try {
            devServerStart = compiler.startDevServer();
        }
        catch (e) {
            config.logger.error(e);
            exit(1);
        }
    }
    const latestVersion = node.getLatestCompilerVersion(config.sys, config.logger);
    const results = await compiler.build();
    let devServer = null;
    if (devServerStart) {
        devServer = await devServerStart;
    }
    if (!config.watch && hasError(results && results.diagnostics)) {
        config.sys.destroy();
        if (devServer) {
            await devServer.close();
        }
        exit(1);
    }
    if (config.watch || devServerStart) {
        process.once('SIGINT', () => {
            config.sys.destroy();
            if (devServer) {
                devServer.close();
            }
        });
    }
    await node.validateCompilerVersion(config.sys, config.logger, latestVersion);
    return results;
}

function taskDocs(config) {
    const { Compiler } = require('../compiler/index.js');
    const compiler = new Compiler(config);
    if (!compiler.isValid) {
        exit(1);
    }
    return compiler.docs();
}

function taskHelp(process, logger) {
    const p = logger.dim((process.platform === 'win32') ? '>' : '$');
    console.log(`
  ${logger.bold('Build:')} ${logger.dim('Build components for development or production.')}

    ${p} ${logger.green('stencil build [--dev] [--watch] [--prerender] [--debug]')}

      ${logger.cyan('--dev')} ${logger.dim('.............')} Development build
      ${logger.cyan('--watch')} ${logger.dim('...........')} Rebuild when files update
      ${logger.cyan('--serve')} ${logger.dim('...........')} Start the dev-server
      ${logger.cyan('--prerender')} ${logger.dim('.......')} Prerender the application
      ${logger.cyan('--docs')} ${logger.dim('............')} Generate component readme.md docs
      ${logger.cyan('--config')} ${logger.dim('..........')} Set stencil config file
      ${logger.cyan('--stats')} ${logger.dim('...........')} Write stencil-stats.json file
      ${logger.cyan('--log')} ${logger.dim('.............')} Write stencil-build.log file
      ${logger.cyan('--debug')} ${logger.dim('...........')} Set the log level to debug


  ${logger.bold('Test:')} ${logger.dim('Run unit and end-to-end tests.')}

    ${p} ${logger.green('stencil test [--spec] [--e2e]')}

      ${logger.cyan('--spec')} ${logger.dim('............')} Run unit tests with Jest
      ${logger.cyan('--e2e')} ${logger.dim('.............')} Run e2e tests with Puppeteer


  ${logger.bold('Examples:')}

    ${p} ${logger.green('stencil build --dev --watch --serve')}
    ${p} ${logger.green('stencil build --prerender')}
    ${p} ${logger.green('stencil test --spec --e2e')}

`);
}

async function taskServe(process, config, flags) {
    const { Compiler } = require('../compiler/index.js');
    const compiler = new Compiler(config);
    if (!compiler.isValid) {
        exit(1);
    }
    config.flags.serve = true;
    config.devServer.openBrowser = false;
    config.devServer.hotReplacement = false;
    config.maxConcurrentWorkers = 1;
    config.devServer.root = process.cwd();
    if (typeof flags.root === 'string') {
        if (!config.sys.path.isAbsolute(config.flags.root)) {
            config.devServer.root = config.sys.path.relative(process.cwd(), flags.root);
        }
    }
    config.devServer.root = utils.normalizePath(config.devServer.root);
    const devServer = await compiler.startDevServer();
    if (devServer) {
        config.logger.info(`dev server: ${devServer.browserUrl}`);
    }
    process.once('SIGINT', () => {
        config.sys.destroy();
        devServer && devServer.close();
        exit(0);
    });
}

async function taskTest(config) {
    // always ensure we have jest modules installed
    const ensureModuleIds = [
        '@types/jest',
        'jest',
        'jest-cli'
    ];
    if (config.flags && config.flags.e2e) {
        // if it's an e2e test, also make sure we're got
        // puppeteer modules installed
        ensureModuleIds.push('@types/puppeteer', 'puppeteer');
        if (config.flags.screenshot) {
            // ensure we've got pixelmatch for screenshots
            config.logger.warn(config.logger.yellow(`EXPERIMENTAL: screenshot visual diff testing is currently under heavy development and has not reached a stable status. However, any assistance testing would be appreciated.`));
        }
    }
    // ensure we've got the required modules installed
    // jest and puppeteer are quite large, so this
    // is an experiment to lazy install these
    // modules only when you need them
    await config.sys.lazyRequire.ensure(config.logger, config.rootDir, ensureModuleIds);
    try {
        const { Testing } = require('../testing/index.js');
        const testing = new Testing(config);
        if (!testing.isValid) {
            exit(1);
        }
        const passed = await testing.runTests();
        await testing.destroy();
        if (!passed) {
            exit(1);
        }
    }
    catch (e) {
        config.logger.error(e);
        exit(1);
    }
}

function taskVersion(config) {
    console.log(config.sys.compiler.version);
}
async function taskCheckVersion(config) {
    try {
        const currentVersion = config.sys.compiler.version;
        const latestVersion = await node.requestLatestCompilerVersion();
        if (config.sys.semver.lt(currentVersion, latestVersion)) {
            node.printUpdateMessage(config.logger, currentVersion, latestVersion);
        }
        else {
            console.log(`${config.logger.cyan(config.sys.compiler.name)} version ${config.logger.green(config.sys.compiler.version)} is the latest version`);
        }
    }
    catch (e) {
        config.logger.error(`unable to load latest compiler version: ${e}`);
        exit(1);
    }
}

async function runTask(process, config, flags) {
    if (flags.help || flags.task === `help`) {
        taskHelp(process, config.logger);
    }
    else if (flags.version) {
        taskVersion(config);
    }
    else if (flags.checkVersion) {
        await taskCheckVersion(config);
    }
    else {
        switch (flags.task) {
            case 'build':
                await taskBuild(process, config, flags);
                break;
            case 'docs':
                await taskDocs(config);
                break;
            case 'serve':
                await taskServe(process, config, flags);
                break;
            case 'test':
                await taskTest(config);
                break;
            default:
                config.logger.error(`Invalid stencil command, please see the options below:`);
                taskHelp(process, config.logger);
                exit(1);
        }
    }
}

async function run(process, sys, logger) {
    process.on(`unhandledRejection`, (r) => {
        if (!utils.shouldIgnoreError(r)) {
            logger.error(`unhandledRejection`, r);
        }
    });
    process.title = `Stencil`;
    const flags = parseFlags(process);
    // load the config file
    let config;
    try {
        const configPath = getConfigFilePath(process, sys, flags.config);
        // if --config is provided we need to check if it exists
        if (flags.config && !sys.fs.existsSync(configPath)) {
            throw new Error(`Stencil configuration file cannot be found at: "${flags.config}"`);
        }
        config = sys.loadConfigFile(configPath, process);
        config.sys = (config.sys || sys);
        config.logger = (config.logger || logger);
    }
    catch (e) {
        logger.error(e);
        exit(1);
    }
    try {
        if (typeof config.logLevel === 'string') {
            config.logger.level = config.logLevel;
        }
        config.flags = flags;
        process.title = `Stencil: ${config.namespace}`;
        await runTask(process, config, flags);
    }
    catch (e) {
        if (!utils.shouldIgnoreError(e)) {
            config.logger.error(`uncaught cli error: ${e}${config.logger.level === 'debug' ? e.stack : ''}`);
            exit(1);
        }
    }
}

exports.run = run;
