'use strict';

const PrettyError = require('pretty-error');
const chalk = require('chalk');
const indentString = require('indent-string');
const mergeOptions = require('merge-options');
const checkHumanErrors = require('./util/checkHumanErrors');

// Symbols table
// On windows we hav to use more simple symbols otherwise they won't display correctly
const symbols = process.platform !== 'win32' ?
{
    ok: '✓',
    fail: '✖',
    bullet: '•',
    hr: '━',
} :
/* istanbul ignore next */
{
    ok: '√',
    fail: '×',
    bullet: '*',
    hr: '━',
};

// Configure prettyError instance
const prettyError = new PrettyError();

prettyError.appendStyle({
    'pretty-error > header': {
        display: 'none',
    },
    'pretty-error > trace': {
        marginTop: 0,
    },
    'pretty-error > trace > item': {
        marginBottom: 0,
    },
});

function calculateDuration(clientStats, serverStats) {
    return Math.max(
        clientStats.endTime - clientStats.startTime,
        serverStats.endTime - serverStats.endTime
    );
}

function renderGenericError(err) {
    let str = '';

    if (err.code || (err.name && err.name !== 'Error')) {
        str += chalk.dim(`${err.code || err.name}: `);
    }

    str += `${err.message}\n`;

    if (!err.hideStack) {
        const prettyErrStr = prettyError
        .render(err)
        .trim()
        .split('\n')
        .slice(0, -1)
        .join('\n');

        str += `${prettyErrStr}\n`;
    }

    return str;
}

function renderError(err, statsOptions) {
    let str;

    // If there's stats & compilation errors, then we just render them
    if (err.stats && err.stats.hasErrors && err.stats.hasErrors()) {
        str = `${err.message}\n\n`;
        str += renderStats(err.stats, {
            assets: false,
            colors: statsOptions ? statsOptions.colors : chalk.enabled,
        });
    // Render standard error
    } else {
        str = renderGenericError(err);
    }

    return str;
}

function renderStats(stats, statsOptions) {
    statsOptions = Object.assign({
        assets: true,
        chunks: false,
        version: false,
        children: false,
        modules: false,
        colors: chalk.enabled,
        timings: false,
        hash: false,
    }, statsOptions);

    return `${stats.toString(statsOptions).trim()}\n`;
}

function renderBanner(label) {
    let str = '';

    str += chalk.inverse(` ${label} ${' '.repeat(35 - label.length - 1)}`);
    str += '\n';
    str += chalk.dim(symbols.hr.repeat(35));
    str += '\n';

    return str;
}

// -----------------------------------------------------------

function getOptions(options) {
    return mergeOptions({
        humanErrors: true,  // Display human errors
        stats: true, // Display output assets, can be `true`, `false` and `once`
        statsOptions: {  // Stats object to use for stats.toString(),
            assets: true,
            chunks: false,
            version: false,
            children: false,
            modules: false,
            timings: false,
            hash: false,
            colors: chalk.enabled,
        },
        output: process.stderr,  // Writable stream to print stuff
    }, options);
}

function reporter(compiler, options) {
    options = getOptions(options);

    let displayStats = options.stats === true || options.stats === 'once';

    function onBeginOnce() {
        options.humanErrors && checkHumanErrors(compiler, options);
    }

    function onBegin() {
        const str = `${chalk.dim(symbols.bullet)} Compiling...\n`;

        options.output.write(str);
    }

    function onEnd({ client, server }) {
        let str;

        str = `${chalk.green(symbols.ok)} Compilation succeeded`;
        str += ` ${chalk.dim(`(${calculateDuration(client, server)}ms)`)}\n\n`;

        options.output.write(str);

        if (displayStats) {
            if (options.stats === 'once') {
                displayStats = false;
            }

            str = '';
            str += renderBanner('CLIENT');
            str += renderStats(client, options.statsOptions);
            str += '\n';
            str += renderBanner('SERVER');
            str += renderStats(server, options.statsOptions);
            str += '\n';

            options.output.write(indentString(str, 4));
        }
    }

    function onError(err) {
        let str;

        str = '';
        str += `${chalk.red(symbols.fail)} Compilation failed\n\n`;
        str += indentString(renderError(err, options.statsOptions), 4);
        str += '\n';

        options.output.write(str);
    }

    compiler
    .once('begin', onBeginOnce)
    .on('begin', onBegin)
    .on('end', onEnd)
    .on('error', onError);

    return () => {
        compiler
        .removeListener('begin', onBeginOnce)
        .removeListener('begin', onBegin)
        .removeListener('end', onEnd)
        .removeListener('error', onError);
    };
}

module.exports = reporter;
module.exports.getOptions = getOptions;
module.exports.renderStats = renderStats;
module.exports.renderError = renderError;

@@ -898,40 +898,134 @@
 tats
-) %7B%0A    return Math.max(%0A       
+, beginAt) %7B%0A    const leniantBeginAt = beginAt - 100;%0A%0A    return Math.max(%0A        clientStats.startTime %3E= leniantBeginAt ?
  cli
@@ -1064,16 +1064,20 @@
 tartTime
+ : 0
 ,%0A      
@@ -1077,16 +1077,58 @@
 %0A       
+ serverStats.startTime %3E= leniantBeginAt ?
  serverS
@@ -1154,23 +1154,29 @@
 erStats.
-endTime
+startTime : 0
 %0A    );%0A
@@ -3481,16 +3481,38 @@
  'once';
+%0A    let beginAt = -1;
 %0A%0A    fu
@@ -3630,24 +3630,55 @@
 onBegin() %7B%0A
+        beginAt = Date.now();%0A%0A
         cons
@@ -3965,16 +3965,25 @@
 , server
+, beginAt
 )%7Dms)%60)%7D

Fix duration in case of mutually exclusive client/server changes.