/**
 * @fileoverview Main CLI object which makes use of the Linter's API to access functionality
 * @author Raghav Dua <duaraghav8@gmail.com>
 */

"use strict";

let cli = require("commander"),
    fs = require("fs"),
    fsUtils = require("./utils/fs-utils"),
    path = require("path"),
    { EOL } = require("os"),
    chokidar = require("chokidar"),
    traverse = require("sol-digger"),
    solium = require("./solium"),
    sum = require("lodash/sum"),
    version = require("../package.json").version;

let CWD = process.cwd(),
    SOLIUMRC_FILENAME = ".soliumrc.json",
    SOLIUMRC_FILENAME_ABSOLUTE = path.join(CWD, SOLIUMRC_FILENAME),
    SOLIUMIGNORE_FILENAME = ".soliumignore",
    SOLIUMIGNORE_FILENAME_ABSOLUTE = path.join(CWD, SOLIUMIGNORE_FILENAME),
    DEFAULT_SOLIUMIGNORE_PATH = `${__dirname}/cli-utils/.default-solium-ignore`,
    DEFAULT_SOLIUMRC_PATH = `${__dirname}/cli-utils/.default-soliumrc.json`;

let errorCodes = { ERRORS_FOUND: 1, NO_SOLIUMRC: 3, WRITE_FAILED: 4, INVALID_PARAMS: 5, FILE_NOT_FOUND: 6 };

/**
 * Create default configuration files in the user's directory
 * @returns {void}
 */
function setupDefaultUserConfig() {
    createDefaultConfigJSON();
    createDefaultSoliumIgnore();
}

/**
 * Synchronously write the passed configuration to the file whose absolute path is SOLIUMRC_FILENAME_ABSOLUTE
 * @param {Object} config User Configuration object
 * @returns {void}
 */
function writeConfigFile(config) {
    try {
        fs.writeFileSync(
            SOLIUMRC_FILENAME_ABSOLUTE,
            JSON.stringify(config, null, 2)
        );
    } catch (e) {
        errorReporter.reportFatal(
            `An error occurred while writing to ${SOLIUMRC_FILENAME_ABSOLUTE}:${EOL}${e.message}`);
        process.exit(errorCodes.WRITE_FAILED);
    }
}

/**
 * Copy data from cli-utils/.default-solium-ignore to (newly created) .soliumignore in user's root directory
 * @returns {void}
 */
function createDefaultSoliumIgnore() {
    try {
        fs.writeFileSync(
            SOLIUMIGNORE_FILENAME_ABSOLUTE,
            fs.readFileSync(DEFAULT_SOLIUMIGNORE_PATH)
        );
    } catch (e) {
        errorReporter.reportFatal(
            `An error occurred while writing to ${SOLIUMIGNORE_FILENAME_ABSOLUTE}:${EOL}${e.message}`);
        process.exit(errorCodes.WRITE_FAILED);
    }
}

/**
 * Create default solium configuration JSON in user's current working directory.
 * This file enables all the built-in lint rules
 */
function createDefaultConfigJSON() {
    writeConfigFile(require(DEFAULT_SOLIUMRC_PATH));
}

/**
 * Lint a source code string based on user settings. If autofix is enabled, write the fixed code back to file.
 * @param {String} sourceCode The source code to be linted
 * @param {Object} userConfig User configuration
 * @param {Object} errorReporter The error reporter to use
 * @param {String} fileName (optional) File name to use when reporting errors
 * @returns {Integer} numOfErrors Number of Lint ERRORS that occured.
 */
function lintString(sourceCode, userConfig, errorReporter, fileName) {
    let lintErrors, fixesApplied;

    try {
        if (userConfig.options.autofix || userConfig.options.autofixDryrun) {
            let result = solium.lintAndFix(sourceCode, userConfig);

            lintErrors = result.errorMessages;
            if (userConfig.options.autofix) {
                applyFixes(fileName, result);
                fixesApplied = result.fixesApplied;
            } else {
                errorReporter.reportDiff(fileName,
                    sourceCode, result.fixedSourceCode, result.fixesApplied.length);
            }
        } else {
            lintErrors = solium.lint(sourceCode, userConfig);
        }
    } catch (e) {
        // Don't abort in case of a parse error, just report it as a normal lint issue.
        if (e.name !== "SyntaxError") {
            const messageOrStackrace = userConfig.options.debug ? e.stack : e.message;
            errorReporter.reportFatal(`An error occured while linting over ${fileName}:${EOL}${messageOrStackrace}`);
            process.exit(errorCodes.ERRORS_FOUND);
        }

        lintErrors = [{
            ruleName: "",
            type: "error",
            message: `Syntax error: unexpected token ${e.found}`,
            line: e.location.start.line,
            column: e.location.start.column
        }];
    }

    // If any lint/internal errors/warnings exist, report them
    lintErrors.length &&
        errorReporter.report(fileName, sourceCode, lintErrors, fixesApplied);

    return lintErrors.reduce(function(numOfErrors, err) {
        return err.type === "error" ? numOfErrors+1 : numOfErrors;
    }, 0);
}

function applyFixes(fileName, lintResult) {
    lintResult.fixesApplied.length && fs.writeFileSync(fileName, lintResult.fixedSourceCode);
}

/**
 * Lint a file based on user settings
 * @param {String} fileName The path to the file to be linted
 * @param {Object} userConfig User configuration
 * @param {Object} errorReporter The error reporter to use
 * @returns {Integer} numOfErrors Number of Lint ERRORS that occured (the result returned by lintString())
 */
function lintFile(fileName, userConfig, errorReporter) {
    let sourceCode;

    try {
        sourceCode = fs.readFileSync(fileName, "utf8");
    } catch (e) {
        errorReporter.reportFatal("Unable to read " + fileName + ": " + e.message);
        process.exit(errorCodes.FILE_NOT_FOUND);
    }

    return lintString(sourceCode, userConfig, errorReporter, fileName);
}

/**
 * Function that calls Solium object's linter based on user settings.
 * If not given, we lint the entire directory's (and sub-directories') solidity files.
 * @param {Object} userConfig User's configurations that contain information about which linting rules to apply
 * @param {String} filename (optional) The single file to be linted.
 * @returns {Integer} totalNumOfErrors Total no. of errors found throughout the codebase (directory) linted.
 */
function lint(userConfig, input, ignore, errorReporter) {
    let filesToLint, errorCount;

    //If filename is provided, lint it. Otherwise, lint over current directory & sub-directories
    if (input.file) {

        if (!fsUtils.isFile(input.file)) {
            errorReporter.reportFatal(`${input.file} is not a valid file`);
            process.exit(errorCodes.INVALID_PARAMS);
        }

        filesToLint = [input.file];

    } else if (input.dir) {

        if (!fsUtils.isDirectory(input.dir)) {
            errorReporter.reportFatal(`${input.dir} is not a valid directory`);
            process.exit(errorCodes.INVALID_PARAMS);
        }

        filesToLint = traverse(input.dir, ignore);
    }

    if (filesToLint) {
        errorCount = sum(filesToLint.map(function(file, index) {
            userConfig.options.returnInternalIssues = (index === 0);
            return lintFile(file, userConfig, errorReporter);
        }));
    } else if (input.stdin) {
        // This only works on *nix. Need to fix to enable stdin input in windows.
        let sourceCode = fs.readFileSync("/dev/stdin", "utf-8");

        userConfig.options.returnInternalIssues = true;
        errorCount = lintString(sourceCode, userConfig, errorReporter, "[stdin]");
    } else {
        errorReporter.reportFatal("Must specify input for linter using --file, --dir or --stdin");
        process.exit(errorCodes.INVALID_PARAMS);
    }

    errorReporter.finalize && errorReporter.finalize();
    return errorCount;
}

/**
 * Function responsible for defining all the available commandline options & version information
 * @param {Object} cliObject Commander Object handling the cli
 */
function createCliOptions(cliObject) {
    function collect(val, memo) {
        memo.push(val);
        return memo;
    }

    cliObject
        .version(`Solium version ${version}`)
        .description("Linter to find & fix style and security issues in Solidity smart contracts.")
        .usage("[options] <keyword>")

        .option("-i, --init", "Create default rule configuration files")
        .option("-f, --file [filepath::String]", "Solidity file to lint")
        .option("-d, --dir [dirpath::String]", "Directory containing Solidity files to lint")
        .option("-R, --reporter [name::String]", "Format to report lint issues in (pretty | gcc)", "pretty")
        .option("-c, --config [filepath::String]", "Path to the .soliumrc configuration file")
        .option("-, --stdin", "Read input file from stdin")
        .option("--fix", "Fix Lint issues where possible")
        .option("--fix-dry-run", "Output fix diff without applying it")
        .option("--debug", "Display debug information")
        .option("--watch", "Watch for file changes")
        .option("--hot", "(Deprecated) Same as --watch")
        .option("--no-soliumignore", "Do not look for .soliumignore file")
        .option("--no-soliumrc", "Do not look for soliumrc configuration file")
        .option(
            "--rule [rule]",
            "Rule to execute. This overrides the specified rule's configuration in soliumrc if present",
            collect,
            []
        )
        .option(
            "--plugin [plugin]",
            "Plugin to execute. This overrides the specified plugin's configuration in soliumrc if present",
            collect,
            []
        );
}

/**
 * Takes a name and returns an error reporter
 * @param {String} name Name of the reporter
 * @returns {Object} reporter The reporter whose name was supplied.
 */
function getErrorReporter(name) {
    try {
        return require("./reporters/" + name);
    } catch (e) {
        throw new Error(
            `Invalid reporter "${name}". Valid reporters are "gcc" and "pretty"`
        );
    }
}

/**
 * Entry point to the CLI reponsible for initiating linting process based on command-line arguments
 * @param {Array} programArgs Commandline arguments
 */
function execute(programArgs) {

    let userConfig = {}, ignore, errorReporter;

    createCliOptions(cli);
    programArgs.length === 2 ? cli.help() : cli.parse(programArgs);

    if (cli.init) {
        return setupDefaultUserConfig();
    }

    try {
        errorReporter = getErrorReporter(cli.reporter);
    } catch (e) {
        process.stderr.write(`[Fatal error] ${e.message}${EOL}`);
        process.exit(errorCodes.INVALID_PARAMS);
    }

    if (cli.soliumrc) {
        /**
         * If cli.config option is NOT specified, then resort to .soliumrc in current dir.
         * Else,
         *   If path is absolute, assign as-it-is.
         *   Else (relative pathing) join path with current dir.
         */
        const soliumrcAbsPath = cli.config ?
            (path.isAbsolute(cli.config) ? cli.config : path.join(CWD, cli.config)) :
            SOLIUMRC_FILENAME_ABSOLUTE;

        try {
            userConfig = require(soliumrcAbsPath);
        } catch (e) {
            // Check if soliumrc file exists. If yes, then the file is in an invalid format.
            if (fs.existsSync(soliumrcAbsPath)) {
                errorReporter.reportFatal(`An invalid ${SOLIUMRC_FILENAME} was provided. ${e.message}`);
            } else {
                if (cli.config) {
                    errorReporter.reportFatal(`${soliumrcAbsPath} does not exist.`);
                } else {
                    errorReporter.reportFatal(`Couldn't find ${SOLIUMRC_FILENAME} in the current directory.`);
                }
            }

            process.exit(errorCodes.NO_SOLIUMRC);
        }
    }

    //if custom rules' file is set, make sure we have its absolute path
    if (
        userConfig ["custom-rules-filename"] &&
        !path.isAbsolute(userConfig ["custom-rules-filename"])
    ) {
        userConfig ["custom-rules-filename"] = path.join(
            CWD, userConfig ["custom-rules-filename"]
        );
    }

    // Pass cli arguments that modify the behaviour of upstream functions.
    userConfig.options = {
        autofix: Boolean(cli.fix),
        autofixDryrun: Boolean(cli.fixDryRun),
        debug: Boolean(cli.debug)
    };

    if (userConfig.options.autofixDryrun) {
        if (userConfig.options.autofix) {
            return errorReporter.reportFatal("Cannot use both --fix and --fix-dry-run");
        }
        if (cli.reporter != "pretty") {
            return errorReporter.reportFatal("Option --fix-dry-run is only supported with pretty reporter");
        }
    }

    userConfig.plugins = userConfig.plugins || [];
    userConfig.rules = userConfig.rules || {};

    for (const plugin of cli.plugin) {
        userConfig.plugins.push(plugin);
    }

    for (const rule of cli.rule) {
        // If no ":" was found, it means only the rule's name was specified.
        // Treat it as an error and adopt its default configuration options.
        if (!rule.includes(":")) {
            userConfig.rules[rule] = "error";
            continue;
        }

        let [key, value] = rule.split(":").map(i => i.trim());
        try {
            value = JSON.parse(value);
        } catch (e) {
            errorReporter.reportFatal(`There was an error trying to parse '${rule}': ${e.message}`);
            process.exit(errorCodes.INVALID_PARAMS);
        }
        userConfig.rules[key] = value;
    }

    //get all files & folders to ignore from .soliumignore
    if (cli.soliumignore) {
        try {
            ignore = fs.readFileSync(SOLIUMIGNORE_FILENAME_ABSOLUTE, "utf8").split(EOL);
        } catch (e) {
            if (e.code === "ENOENT") {
                errorReporter.reportInternal(
                    "No '.soliumignore' found. Use --no-soliumignore to make this warning go away.");
            } else {
                errorReporter.reportInternal(`Failed to read '.soliumignore': ${e.message}`);
            }
        }
    }

    if (cli.hot) {
        // --hot is equivalent to --watch in functionality, is a legacy option
        cli.watch = true;
    }

    if (cli.watch) {
        if (cli.stdin) {
            return errorReporter.reportFatal("Cannot watch files when reading from stdin");
        }

        if (cli.fix) {
            return errorReporter.reportFatal("Automatic code formatting is not supported in watch mode.");
        }
    }

    let errorCount = lint(userConfig, { file: cli.file, dir: cli.dir, stdin: cli.stdin }, ignore, errorReporter);

    if (cli.watch) {

        let spy = chokidar.watch(CWD);

        spy.on("change", function() {
            console.log("\x1Bc"); // clear the console
            console.log(`File change detected. Start linting.${EOL}`);
            lint(userConfig, { file: cli.file, dir: cli.dir }, ignore, errorReporter);	//lint on subsequent changes (hot)
            console.log(`Linting complete. Watching for file changes.${EOL}`);
        });

    } else if (errorCount > 0) {
        process.exit(errorCodes.ERRORS_FOUND);
    }
}

module.exports = {
    execute: execute
};


/**
 * @fileoverview Utility functions for examining a rule object
 * @author Raghav Dua <duaraghav8@gmail.com>
 */

"use strict";

let coreRuleSchema = require("../../config/schemas/core-rule"),
    coreRuleResponseSchema = require("../../config/schemas/core-rule-response");

let SchemaValidator = coreRuleSchema.SchemaValidator,
    validateCoreRule = SchemaValidator.compile(coreRuleSchema.Schema),
    validateCoreRuleResponse = SchemaValidator.compile(coreRuleResponseSchema.Schema);

module.exports = {

    /**
	 * Determine whether the supplied argument qualifies as a core rule object
	 * @param {Object} ruleObject The object to validate
	 * @returns {Boolean} isValid True if object is a valid core solium rule, false otherwise.
	 */
    isAValidRuleObject: validateCoreRule,

    /**
	 * Determine whether the response given by a rule's create() method is a set of valid
	 * node names (event listeners) with their corresponding handler functions.
	 * @param {Object} ruleResponseObject Object to validate
	 * @returns {Boolean} isValid True if object is a valid rule response, false otherwise.
	 */
    isAValidRuleResponseObject: validateCoreRuleResponse,

    /**
	 * Determine whether the options object supplied is valid according to the schema passed.
	 * @param {Array} options List of options
	 * @param {Array} listItemsSchema A list of schema objects defining schema for every item in the options list.
	 * @returns {Boolean} isValid True if options list is valid, false otherwise.
	 */
    areValidOptionsPassed: function(options, listItemsSchema) {
        let validateOptionsList = SchemaValidator.compile({
            type: "array",
            minItems: listItemsSchema.length,
            additionalItems: false,
            items: listItemsSchema
        });

        return validateOptionsList(options);
    }

};


/**
 * @fileoverview Ensure that the visibility modifier for a function should come before any custom modifiers.
 * @author Harrison Beckerich <https://github.com/hbeckeri>
 */

"use strict";

module.exports = {
    meta: {
        docs: {
            recommended: true,
            type: "warning",
            description: "Ensure that the visibility modifier for a function should come before any custom modifiers"
        },

        schema: []
    },

    create(context) {
        // Find index of the first visibility modifier in declaration.
        // Find the first non-VM before this first VM found above.
        // If non-VM found, report the VM.
        function inspectFD(emitted) {
            const { node } = emitted,
                visibilityModifiers = ["public", "external", "internal", "private"];
            const modifiers = (node.modifiers || []),
                firstVisibilityModifierIndex = modifiers.findIndex(m => visibilityModifiers.includes(m.name));

            // If no visibility modifiers exist in function declaration, exit now
            if (emitted.exit || firstVisibilityModifierIndex === -1) {
                return;
            }

            const firstNonVisModifBeforeFirstVisModif = modifiers.slice(0, firstVisibilityModifierIndex).find(m => !visibilityModifiers.includes(m.name));

            // TODO: Add fix() for this rule
            if (firstNonVisModifBeforeFirstVisModif) {
                const issue = {
                    node: modifiers[firstVisibilityModifierIndex],
                    message: `Visibility modifier "${modifiers[firstVisibilityModifierIndex].name}" should come before other modifiers.`
                };
                context.report(issue);
            }
        }

        return {
            FunctionDeclaration: inspectFD
        };
    }
};


/**
 * @fileoverview In the case of 4+ elements in the same line require they are instead put on a single line each
 * @author Raghav Dua <duaraghav8@gmail.com>
 */

"use strict";

module.exports = {

    meta: {

        docs: {
            recommended: true,
            type: "warning",
            description: "In the case of 4 or more items in the same line, require they are instead put on a single line each"
        },

        schema: [{
            type: "integer", minimum: 1
        }]

    },

    create: function(context) {

        let MAX_IN_SINGLE_LINE = context.options ? context.options [0] : 3;

        let sourceCode = context.getSourceCode();

        function inspectArrayExpression(emitted) {
            let node = emitted.node, elements = node.elements;
            let endingLineNum = sourceCode.getEndingLine(node);

            if (emitted.exit) {
                return;
            }

            if (sourceCode.getLine(node) === endingLineNum) {
                if (elements.length > MAX_IN_SINGLE_LINE) {
                    context.report({
                        node: node,
                        message: "In case of more than " + MAX_IN_SINGLE_LINE +
							" elements, array expression needs to be spread over multiple lines with 1 element per line."
                    });
                }

                return;
            }
        }

        function inspectStructDeclaration(emitted) {
            let node = emitted.node,
                body = node.body || [],
                endingLineNum = sourceCode.getEndingLine(node);

            if (emitted.exit) {
                return;
            }

            //raise an error and stop linting if more than 3 attributes exist & declaration is on single line
            if (sourceCode.getLine(node) === endingLineNum) {
                if (body.length > MAX_IN_SINGLE_LINE) {
                    context.report({
                        node: node,
                        message: "\"" + node.name + "\": In case of more than " + MAX_IN_SINGLE_LINE + " properties, struct declaration needs to be spread over multiple lines with 1 property per line."
                    });
                }
                return;
            }
        }

        //function params (if on multiple lines)
        function inspectFunctionDeclaration(emitted) {
            let node = emitted.node, params = node.params || [];

            let startLine = sourceCode.getLine(node),
                lastArgLine = params.length ? sourceCode.getEndingLine(params.slice(-1) [0]) : startLine;

            if (emitted.exit) {
                return;
            }

            if (startLine === lastArgLine) {
                if (params.length > MAX_IN_SINGLE_LINE) {
                    context.report({
                        node: node,
                        message: "In case of more than " + MAX_IN_SINGLE_LINE + " parameters, drop each into its own line."
                    });
                }
                return;
            }
        }

        function inspectCallExpression(emitted) {

            let node = emitted.node;
            let endingLineNum = sourceCode.getEndingLine(node);

            if (emitted.exit) {
                return;
            }

            if (sourceCode.getLine(node) === endingLineNum) {
                if (node.arguments.length > MAX_IN_SINGLE_LINE) {
                    context.report({
                        node: node,
                        message: "Function \"" + node.callee.name + "\": in case of more than " + MAX_IN_SINGLE_LINE + " arguments, drop each into its own line."
                    });
                }
                return;
            }
        }

        return {
            CallExpression: inspectCallExpression,
            FunctionDeclaration: inspectFunctionDeclaration,
            ConstructorDeclaration: inspectFunctionDeclaration,
            StructDeclaration: inspectStructDeclaration,
            ArrayExpression: inspectArrayExpression
        };

    }
};


/**
 * @fileoverview Ensure that strings are quoted with double-quotes only
 * @author Raghav Dua <duaraghav8@gmail.com>
 */

"use strict";

/**
 * Determine whether the provided literal is in Hex Notation
 * @param {String} literal The literal to test for Hex Notation
 * @returns {Boolean}
 */
function isHex(literal) {
    let reg = /^[0-9a-f]+$/i;

    //test for '0x' separately because hex notation should not be a part of the standard RegExp
    if (literal.slice(0, 2) !== "0x") {
        return false;
    }

    return reg.test(literal.slice(2));
}

module.exports = {

    meta: {

        docs: {
            recommended: true,
            type: "error",
            description: "Ensure that string are quoted with double-quotes only",
            replacedBy: ["quotes"]
        },

        schema: [],
        deprecated: true

    },

    create: function(context) {

        let doubleQuotesLiteralRegExp = /^\".*\"$/,
            sourceCode = context.getSourceCode();

        function inspectLiteral(emitted) {
            let node = emitted.node, nodeText = sourceCode.getText(node);

            if (emitted.exit ||
				typeof node.value !== "string" ||
				(nodeText [0] !== "'" && nodeText [0] !== "\"" && isHex(node.value))
            ) {
                return;
            }

            if (!doubleQuotesLiteralRegExp.test(nodeText)) {
                context.report({
                    node: node,
                    message: "'" + node.value + "': String Literals must be quoted with \"double quotes\" only."
                });
            }
        }

        return {
            Literal: inspectLiteral
        };

    }

};


'use strict'

var parser = require('note-parser')

/**
 * Create a Soundfont object
 *
 * @param {AudioContext} context - the [audio context](https://developer.mozilla.org/en/docs/Web/API/AudioContext)
 * @param {Function} nameToUrl - (Optional) a function that maps the sound font name to the url
 * @return {Soundfont} a soundfont object
 */
function Soundfont (ctx, nameToUrl) {
  console.warn('new Soundfont() is deprected')
  console.log('Please use Soundfont.instrument() instead of new Soundfont().instrument()')
  if (!(this instanceof Soundfont)) return new Soundfont(ctx)

  this.nameToUrl = nameToUrl || Soundfont.nameToUrl
  this.ctx = ctx
  this.instruments = {}
  this.promises = []
}

Soundfont.prototype.onready = function (callback) {
  console.warn('deprecated API')
  console.log('Please use Promise.all(Soundfont.instrument(), Soundfont.instrument()).then() instead of new Soundfont().onready()')
  Promise.all(this.promises).then(callback)
}

Soundfont.prototype.instrument = function (name, options) {
  console.warn('new Soundfont().instrument() is deprecated.')
  console.log('Please use Soundfont.instrument() instead.')
  var ctx = this.ctx
  name = name || 'default'
  if (name in this.instruments) return this.instruments[name]
  var inst = {name: name, play: oscillatorPlayer(ctx, options)}
  this.instruments[name] = inst
  if (name !== 'default') {
    var promise = Soundfont.instrument(ctx, name, options).then(function (instrument) {
      inst.play = instrument.play
      return inst
    })
    this.promises.push(promise)
    inst.onready = function (cb) {
      console.warn('onready is deprecated. Use Soundfont.instrument().then()')
      promise.then(cb)
    }
  } else {
    inst.onready = function (cb) {
      console.warn('onready is deprecated. Use Soundfont.instrument().then()')
      cb()
    }
  }
  return inst
}

/*
 * Load the buffers of a given instrument name. It returns a promise that resolves
 * to a hash with midi note numbers as keys, and audio buffers as values.
 *
 * @param {AudioContext} ac - the audio context
 * @param {String} name - the instrument name (it accepts an url if starts with "http")
 * @param {Object} options - (Optional) options object
 * @return {Promise} a promise that resolves to a Hash of { midiNoteNum: <AudioBuffer> }
 *
 * The options object accepts the following keys:
 *
 * - nameToUrl {Function}: a function to convert from instrument names to urls.
 * By default it uses Benjamin Gleitzman's package of
 * [pre-rendered sound fonts](https://github.com/gleitz/midi-js-soundfonts)
 * - notes {Array}: the list of note names to be decoded (all by default)
 *
 * @example
 * var Soundfont = require('soundfont-player')
 * Soundfont.loadBuffers(ctx, 'acoustic_grand_piano').then(function(buffers) {
 *  buffers[60] // => An <AudioBuffer> corresponding to note C4
 * })
 */
function loadBuffers (ac, name, options) {
  console.warn('Soundfont.loadBuffers is deprecate.')
  console.log('Use Soundfont.instrument(..) and get buffers properties from the result.')
  return Soundfont.instrument(ac, name, options).then(function (inst) {
    return inst.buffers
  })
}
Soundfont.loadBuffers = loadBuffers

/**
 * Returns a function that plays an oscillator
 *
 * @param {AudioContext} ac - the audio context
 * @param {Hash} defaultOptions - (Optional) a hash of options:
 * - vcoType: the oscillator type (default: 'sine')
 * - gain: the output gain value (default: 0.4)
  * - destination: the player destination (default: ac.destination)
 */
function oscillatorPlayer (ctx, defaultOptions) {
  defaultOptions = defaultOptions || {}
  return function (note, time, duration, options) {
    console.warn('The oscillator player is deprecated.')
    console.log('Starting with version 0.9.0 you will have to wait until the soundfont is loaded to play sounds.')
    var midi = note > 0 && note < 129 ? +note : parser.midi(note)
    var freq = midi ? parser.midiToFreq(midi, 440) : null
    if (!freq) return

    duration = duration || 0.2

    options = options || {}
    var destination = options.destination || defaultOptions.destination || ctx.destination
    var vcoType = options.vcoType || defaultOptions.vcoType || 'sine'
    var gain = options.gain || defaultOptions.gain || 0.4

    var vco = ctx.createOscillator()
    vco.type = vcoType
    vco.frequency.value = freq

    /* VCA */
    var vca = ctx.createGain()
    vca.gain.value = gain

    /* Connections */
    vco.connect(vca)
    vca.connect(destination)

    vco.start(time)
    if (duration > 0) vco.stop(time + duration)
    return vco
  }
}

/**
 * Given a note name, return the note midi number
 *
 * @name noteToMidi
 * @function
 * @param {String} noteName
 * @return {Integer} the note midi number or null if not a valid note name
 */
Soundfont.noteToMidi = parser.midi

module.exports = Soundfont


'use strict'

var load = require('audio-loader')
var player = require('sample-player')

/**
 * Load a soundfont instrument. It returns a promise that resolves to a
 * instrument object.
 *
 * The instrument object returned by the promise has the following properties:
 *
 * - name: the instrument name
 * - play: A function to play notes from the buffer with the signature
 * `play(note, time, duration, options)`
 *
 *
 * The valid options are:
 *
 * - `format`: the soundfont format. 'mp3' by default. Can be 'ogg'
 * - `soundfont`: the soundfont name. 'MusyngKite' by default. Can be 'FluidR3_GM'
 * - `nameToUrl` <Function>: a function to convert from instrument names to URL
 * - `destination`: by default Soundfont uses the `audioContext.destination` but you can override it.
 * - `gain`: the gain of the player (1 by default)
 * - `notes`: an array of the notes to decode. It can be an array of strings
 * with note names or an array of numbers with midi note numbers. This is a
 * performance option: since decoding mp3 is a cpu intensive process, you can limit
 * limit the number of notes you want and reduce the time to load the instrument.
 *
 * @param {AudioContext} ac - the audio context
 * @param {String} name - the instrument name. For example: 'acoustic_grand_piano'
 * @param {Object} options - (Optional) the same options as Soundfont.loadBuffers
 * @return {Promise}
 *
 * @example
 * var Soundfont = require('sounfont-player')
 * Soundfont.instrument('marimba').then(function (marimba) {
 *   marimba.play('C4')
 * })
 */
function instrument (ac, name, options) {
  if (arguments.length === 1) return function (n, o) { return instrument(ac, n, o) }
  var opts = options || {}
  var isUrl = opts.isSoundfontURL || isSoundfontURL
  var toUrl = opts.nameToUrl || nameToUrl
  var url = isUrl(name) ? name : toUrl(name, opts.soundfont, opts.format)

  return load(ac, url, { only: opts.only || opts.notes }).then(function (buffers) {
    var p = player(ac, buffers, opts).connect(opts.destination ? opts.destination : ac.destination)
    p.url = url
    p.name = name
    return p
  })
}

function isSoundfontURL (name) {
  return /\.js(\?.*)?$/i.test(name)
}

/**
 * Given an instrument name returns a URL to to the Benjamin Gleitzman's
 * package of [pre-rendered sound fonts](https://github.com/gleitz/midi-js-soundfonts)
 *
 * @param {String} name - instrument name
 * @param {String} soundfont - (Optional) the soundfont name. One of 'FluidR3_GM'
 * or 'MusyngKite' ('MusyngKite' by default)
 * @param {String} format - (Optional) Can be 'mp3' or 'ogg' (mp3 by default)
 * @returns {String} the Soundfont file url
 * @example
 * var Soundfont = require('soundfont-player')
 * Soundfont.nameToUrl('marimba', 'mp3')
 */
function nameToUrl (name, sf, format) {
  format = format === 'ogg' ? format : 'mp3'
  sf = sf === 'FluidR3_GM' ? sf : 'MusyngKite'
  return 'https://gleitz.github.io/midi-js-soundfonts/' + sf + '/' + name + '-' + format + '.js'
}

// In the 1.0.0 release it will be:
// var Soundfont = {}
var Soundfont = require('./legacy')
Soundfont.instrument = instrument
Soundfont.nameToUrl = nameToUrl

if (typeof module === 'object' && module.exports) module.exports = Soundfont
if (typeof window !== 'undefined') window.Soundfont = Soundfont


/**
taken from https://github.com/Automattic/node-canvas/blob/master/util/has_lib.js
License

(The MIT License)

Copyright (c) 2010 LearnBoost, and contributors <dev@learnboost.com>

Copyright (c) 2014 Automattic, Inc and contributors <dev@automattic.com>

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
var query = process.argv[2]
var fs = require('fs')
var childProcess = require('child_process')

var SYSTEM_PATHS = [
  '/lib',
  '/usr/lib',
  '/usr/local/lib',
  '/opt/local/lib',
  '/usr/lib/x86_64-linux-gnu',
  '/usr/lib/i386-linux-gnu'
]

/**
 * Checks for lib using ldconfig if present, or searching SYSTEM_PATHS
 * otherwise.
 * @param String library name, e.g. 'jpeg' in 'libjpeg64.so' (see first line)
 * @return Boolean exists
 */
function hasSystemLib (lib) {
  var libName = 'lib' + lib + '.+(so|dylib)'
  var libNameRegex = new RegExp(libName)

    // Try using ldconfig on linux systems
  if (hasLdconfig()) {
    try {
      if (childProcess.execSync('ldconfig -p 2>/dev/null | grep -E "' + libName + '"').length) {
        return true
      }
    } catch (err) {
      // noop -- proceed to other search methods
    }
  }

    // Try checking common library locations
  return SYSTEM_PATHS.some(function (systemPath) {
    try {
      var dirListing = fs.readdirSync(systemPath)
      return dirListing.some(function (file) {
        return libNameRegex.test(file)
      })
    } catch (err) {
      return false
    }
  })
}

/**
 * Checks for ldconfig on the path and /sbin
 * @return Boolean exists
 */
function hasLdconfig () {
  try {
    // Add /sbin to path as ldconfig is located there on some systems -- e.g.
    // Debian (and it can still be used by unprivileged users):
    childProcess.execSync('export PATH="$PATH:/sbin"')
    process.env.PATH = '...'
    // execSync throws on nonzero exit
    childProcess.execSync('hash ldconfig 2>/dev/null')
    return true
  } catch (err) {
    return false
  }
}

/**
* Check if cuda is present
* @return Boolean exists
*/
function hasCuda () {
  try {
    return childProcess.execSync('[ -d /usr/local/cuda ] && echo "Yes"').length > 0;
  } catch (err) {
    return false
  }
}

function main (query) {
  switch (query) {
    case 'opencv':
      return hasSystemLib(query)
    case 'cuda':
      return hasCuda();
    default:
      throw new Error('Unknown library: ' + query)
  }
}

process.stdout.write(main(query).toString())


/**
 * Module dependencies.
 */

const debug = require('debug')('koa-ratelimit');
const Limiter = require('ratelimiter');
const ms = require('ms');

/**
 * Expose `ratelimit()`.
 */

module.exports = ratelimit;

/**
 * Initialize ratelimit middleware with the given `opts`:
 *
 * - `duration` limit duration in milliseconds [1 hour]
 * - `max` max requests per `id` [2500]
 * - `db` database connection
 * - `id` id to compare requests [ip]
 * - `headers` custom header names
 * - `remaining` remaining number of requests ['X-RateLimit-Remaining']
 * - `reset` reset timestamp ['X-RateLimit-Reset']
 * - `total` total number of requests ['X-RateLimit-Limit']
 * - `whitelist` whitelist function [false]
 * - `blacklist` blacklist function [false]
 *
 * @param {Object} opts
 * @return {Function}
 * @api public
 */

function ratelimit(opts = {}) {
  const {
    remaining = 'X-RateLimit-Remaining',
    reset = 'X-RateLimit-Reset',
    total = 'X-RateLimit-Limit'
  } = opts.headers || {}

  return async function ratelimit(ctx, next) {
    const id = opts.id ? opts.id(ctx) : ctx.ip;
    const whitelisted = typeof opts.whitelist === 'function' ? await opts.whitelist(ctx) : false;
    const blacklisted = typeof opts.blacklist === 'function' ? await opts.blacklist(ctx) : false;

    if (blacklisted) {
      ctx.throw(403, 'Forbidden')
    }

    if (false === id || whitelisted) return await next();

    // initialize limiter
    const limiter = new Limiter(Object.assign({}, opts, { id }));

    // check limit
    const limit = await thenify(limiter.get.bind(limiter));

    // check if current call is legit
    const calls = limit.remaining > 0 ? limit.remaining - 1 : 0;

    // check if header disabled
    const disableHeader = opts.disableHeader || false;

    let headers = {};
    if (!disableHeader) {
      // header fields
      headers = {
        [remaining]: calls,
        [reset]: limit.reset,
        [total]: limit.total
      };

      ctx.set(headers);
    }

    debug('remaining %s/%s %s', remaining, limit.total, id);
    if (limit.remaining) return await next();

    const delta = (limit.reset * 1000) - Date.now() | 0;
    const after = limit.reset - (Date.now() / 1000) | 0;
    ctx.set('Retry-After', after);

    ctx.status = 429;
    ctx.body = opts.errorMessage || `Rate limit exceeded, retry in ${ms(delta, { long: true })}.`;

    if (opts.throw) {
      ctx.throw(ctx.status, ctx.body, { headers });
    }
  }
}

/**
 * Helper function to convert a callback to a Promise.
 */

async function thenify(fn) {
  return await new Promise(function(resolve, reject) {
    function callback(err, res) {
      if (err) return reject(err);
      return resolve(res);
    }

    fn(callback);
  });
}


const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const TerserPlugin = require('terser-webpack-plugin')

const fileNameTemplate = '[name].[contenthash:8].[ext]'

const babelLoaderOptions = {
  compact: process.env.NODE_ENV === 'production',
  cacheDirectory: true,
  // Don't waste time on Gzipping the cache during dev
  cacheCompression: process.env.NODE_ENV === 'production',
}

/**
 * The thread-loader parallelizes code compilation, useful with babel since
 * it transpiles both application javascript and node_modules javascript
 */
function babel(options = {}) {
  return (context, { addLoader }) =>
    addLoader({
      // setting `test` defaults here, in case there is no `context.match` data
      test: /\.(js|jsx)$/,
      use: [
        {
          loader: 'thread-loader',
          options: {
            // Keep workers alive for more effective watch mode
            ...(process.env.NODE_ENV === 'development' && { poolTimeout: Infinity }),
          },
        },
        {
          loader: 'babel-loader',
          options: Object.assign(babelLoaderOptions, options),
        },
      ],
      ...context.match,
    })
}

/**
 * Extracts the css and puts it in the <head>
 */
function extractCss() {
  return (context, { addLoader }) =>
    addLoader({
      test: /\.css$/,
      use: [
        {
          loader: MiniCssExtractPlugin.loader,
        },
      ],
      ...context.match,
    })
}

/**
 * Images smaller than 10kb are loaded as a base64 encoded url instead of file url
 */
function imageLoader() {
  return (context, { addLoader }) =>
    addLoader({
      test: /\.(gif|ico|jpg|jpeg|png|webp)$/,
      loader: 'url-loader',
      options: {
        limit: 10000,
        name: fileNameTemplate,
      },
    })
}

/**
 * Videos smaller than 10kb are loaded as a base64 encoded url instead of file url
 */
function videoLoader() {
  return (context, { addLoader }) =>
    addLoader({
      test: /\.(mp4|webm)$/,
      loader: 'url-loader',
      options: {
        limit: 10000,
        name: fileNameTemplate,
      },
    })
}

/**
 * Fonts are loaded as file urls
 */
function fontLoader() {
  return (context, { addLoader }) =>
    addLoader({
      test: /\.(eot|ttf|otf|woff|woff2)(\?.*)?$/,
      loader: 'file-loader',
      options: {
        name: fileNameTemplate,
      },
    })
}

/**
 * Pdfs are loaded as file urls
 */
function pdfLoader() {
  return (context, { addLoader }) =>
    addLoader({
      test: /\.pdf$/,
      loader: 'file-loader',
      options: {
        name: fileNameTemplate,
      },
    })
}

/**
 * GLSLify is a node-style module system for WebGL shaders,
 * allowing you to install GLSL modules from npm and use them in your shaders
 */
function glslifyLoader() {
  return (context, { addLoader }) =>
    addLoader({
      test: /\.(glsl|frag|vert)$/,
      use: ['raw-loader', 'glslify-loader'],
    })
}

/**
 * Parse .csv files with PapaParse and return it in a JSON format
 */
function csvLoader() {
  return (context, { addLoader }) =>
    addLoader({
      test: /\.csv$/,
      loader: 'csv-loader',
      options: {
        dynamicTyping: true,
        header: true,
        skipEmptyLines: true,
      },
    })
}

// Allows you to use two kinds of imports for SVG:
// import logoUrl from './logo.svg'; gives you the URL.
// import { ReactComponent as Logo } from './logo.svg'; gives you a component.
function reactSvgLoader() {
  return (context, { addLoader }) =>
    addLoader({
      test: /\.svg$/,
      issuer: {
        test: /\.(js|jsx|ts|tsx)$/,
      },
      use: [
        // TODO this is probably not needed
        // {
        //   loader: 'babel-loader',
        //   options: babelLoaderOptions,
        // },
        {
          loader: '@svgr/webpack',
          options: {
            svgProps: {
              fill: 'currentColor',
            },
            titleProp: true,
            svgoConfig: {
              multipass: true,
              pretty: process.env.NODE_ENV === 'development',
              indent: 2,
              plugins: [
                { sortAttrs: true },
                { removeViewBox: false },
                { removeDimensions: true },
                { convertColors: { currentColor: true } },
                { cleanupIDs: { minify: false } },
              ],
            },
          },
        },
        {
          loader: 'url-loader',
          options: {
            limit: 10000,
            name: fileNameTemplate,
          },
        },
      ],
    })
}

/**
 * Url-loader for svgs in css
 */
function cssSvgLoader() {
  return (context, { addLoader }) =>
    addLoader({
      // This needs to be different form the reactSvgLoader, otherwise it will merge
      test: /(.*)\.svg$/,
      issuer: {
        test: /\.css$/,
      },
      loader: 'url-loader',
      options: {
        limit: 10000,
        name: fileNameTemplate,
      },
    })
}

/**
 * Suppot .json5 files https://json5.org/
 */
function json5Loader() {
  return (context, { addLoader }) =>
    addLoader({
      test: /\.json5$/,
      loader: 'json5-loader',
    })
}

/**
 * You will be able to import starting from the src folder so you don't have to ../../../
 */
function resolveSrc() {
  return (context, { merge }) =>
    merge({
      resolve: {
        modules: ['node_modules', 'src'],
      },
    })
}

/**
 * Import also typescript without specifying the extension
 */
function resolveTs() {
  return (context, { merge }) =>
    merge({
      resolve: {
        extensions: ['.ts', '.tsx'],
      },
    })
}

/**
 * Terser is a mantained fork of uglify-js
 * https://twitter.com/devongovett/status/1013127516943314944
 */
function terser(options) {
  return (context, { merge }) =>
    merge({
      optimization: {
        minimizer: [new TerserPlugin(options)],
      },
    })
}

/**
 * Add entryPoint at beginning of 'entry' array
 */
function prependEntry(entry) {
  const blockFunction = (context, util) => {
    if (!context.entriesToPrepend) context.entriesToPrepend = []
    context.entriesToPrepend.unshift(entry)
    return config => config
  }
  return Object.assign(blockFunction, {
    post: prependEntryPostHook,
  })
}
function prependEntryPostHook(context, util) {
  return config => {
    config.entry.main.unshift(...context.entriesToPrepend)
    return config
  }
}

module.exports = {
  fileNameTemplate,
  babel,
  extractCss,
  imageLoader,
  videoLoader,
  fontLoader,
  pdfLoader,
  glslifyLoader,
  csvLoader,
  reactSvgLoader,
  cssSvgLoader,
  json5Loader,
  resolveSrc,
  resolveTs,
  terser,
  prependEntry,
}


require('dotenv').config() // gives precedence to the env variables already present
process.env.NODE_ENV = 'production'
process.env.PUBLIC_URL = process.env.PUBLIC_URL || ''
process.env.TRANSPILE_NODE_MODULES = process.env.TRANSPILE_NODE_MODULES || 'true'
process.env.GENERATE_SOURCEMAP = process.env.GENERATE_SOURCEMAP === 'true' ? 'true' : 'false'

const path = require('path')
const fs = require('fs-extra')
const chalk = require('chalk')
const { log, coloredBanner, printFileSizes } = require('../utils/logging-utils')
const { createWebpackCompiler, readWebpackConfig } = require('../utils/webpack-utils')
const {
  extractBrowserslistString,
  extractLatestCommitHash,
  extractLatestCommitTimestamp,
  extractLatestTag,
} = require('../utils/git-utils')

process.env.BROWSERSLIST = extractBrowserslistString()
process.env.LATEST_COMMIT = extractLatestCommitHash()
process.env.LATEST_COMMIT_TIMESTAMP = extractLatestCommitTimestamp()
process.env.LATEST_TAG = extractLatestTag()
if (process.env.PUBLIC_URL.endsWith('/')) {
  process.env.PUBLIC_URL = process.env.PUBLIC_URL.slice(0, -1)
}

const appDir = process.cwd()
const config = readWebpackConfig()
const appPublic = path.resolve(appDir, 'public')
const appBuild = config.output.path
const relativeAppBuildPath = `${path.relative(appDir, appBuild)}/`

function clearBuildFolder() {
  log.info(`Cleaning ${chalk.cyan(relativeAppBuildPath)}`)
  fs.emptyDirSync(appBuild)
}

function copyPublicFolder() {
  if (!fs.existsSync(appPublic)) {
    return
  }

  log.info(`Copying ${chalk.cyan('public/')} into ${chalk.cyan(relativeAppBuildPath)}`)
  fs.copySync(appPublic, appBuild, {
    overwrite: true,
    dereference: true,
    filter: file => path.basename(file) !== 'index.html',
  })
}

// Create the production build and print the deployment instructions.
function build() {
  log.info(`Creating an optimized production build...`)
  const compiler = createWebpackCompiler(
    () => {
      log.ok(`The ${chalk.cyan(relativeAppBuildPath)} folder is ready to be deployed.`)
    },
    () => {
      log.err(`Aborting`)
      process.exit(2)
    }
  )

  return new Promise((resolve, reject) => {
    compiler.run((err, stats) => {
      if (err) {
        return reject(err)
      }

      return resolve(stats)
    })
  })
}

console.log(coloredBanner('/||||/| accurapp', ['cyan', 'magenta']))

clearBuildFolder()
copyPublicFolder()
build().then(stats => {
  log.info('File sizes:')
  console.log()
  printFileSizes(stats, appBuild)
  console.log()
})


const path = require('path')
const fs = require('fs')
const webpack = require('webpack')
const chalk = require('chalk')
const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages')
const typescriptFormatter = require('react-dev-utils/typescriptFormatter')
const prettyMs = require('pretty-ms')
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin')
const { log, listLine } = require('./logging-utils')

const appDir = process.cwd()

function readWebpackConfig() {
  return require(path.resolve(appDir, 'webpack.config.js'))
}

/*
 * The webpack "compiler" is a low-level interface to Webpack that lets us listen to
 * some events and provide our own custom messages.
 */
function createWebpackCompiler(onFirstReadyCallback = () => {}, onError = () => {}) {
  let compiler
  try {
    const config = readWebpackConfig()
    compiler = webpack(config)
  } catch (err) {
    log.err(`Failed to compile:\n${err.message || err}`)
    process.exit(1)
  }

  const useTypeScript = fs.existsSync(`${appDir}/tsconfig.json`)

  // You have changed a file, bundle is now "invalidated", and Webpack is recompiling a bundle.
  compiler.hooks.invalid.tap('invalid', filePath => {
    const filePathRelative = path.relative(appDir, filePath)
    console.log()
    log.info(`Compiling ${chalk.cyan(filePathRelative)}...`)
  })

  let isFirstCompile = true
  let tsMessagesPromise // used to wait for the typechecking
  let tsMessagesResolver // used to trigger the messages after the typechecking

  if (useTypeScript) {
    // reset the promise
    compiler.hooks.beforeCompile.tap('beforeCompile', () => {
      tsMessagesPromise = new Promise(resolve => {
        tsMessagesResolver = msgs => resolve(msgs)
      })
    })

    // trigger the rest of done function
    ForkTsCheckerWebpackPlugin.getCompilerHooks(compiler).receive.tap(
      'afterTypeScriptCheck',
      (diagnostics, lints) => {
        const allMsgs = [...diagnostics, ...lints]
        const format = message => `${message.file}\n${typescriptFormatter(message, true)}`

        tsMessagesResolver({
          errors: allMsgs.filter(msg => msg.severity === 'error').map(format),
          warnings: allMsgs.filter(msg => msg.severity === 'warning').map(format),
        })
      }
    )
  }

  // Webpack has finished recompiling the bundle (whether or not you have warnings or errors)
  compiler.hooks.done.tap('done', async stats => {
    const statsJson = stats.toJson({
      all: false,
      warnings: true,
      errors: true,
      timings: true,
    })

    if (useTypeScript && statsJson.errors.length === 0) {
      // push typescript errors and warnings
      const messages = await tsMessagesPromise
      statsJson.errors.push(...messages.errors)
      statsJson.warnings.push(...messages.warnings)

      // Push errors and warnings into compilation result
      // to show them after page refresh triggered by user.
      stats.compilation.errors.push(...messages.errors)
      stats.compilation.warnings.push(...messages.warnings)

      // if (messages.errors.length > 0) {
      //   devSocket.errors(messages.errors);
      // } else if (messages.warnings.length > 0) {
      //   devSocket.warnings(messages.warnings);
      // }
    }

    const messages = formatWebpackMessages(statsJson)
    const time = prettyMs(statsJson.time)
    const isSuccessful = messages.errors.length + messages.warnings.length === 0

    if (isSuccessful) {
      log.ok(`Compiled successfully in ${chalk.cyan(time)}!`)
    } else if (messages.errors.length > 0) {
      log.err('Errors in compiling:')
      // Only log the first error. Others are often indicative
      // of the same problem, but confuse the reader with noise
      console.log(listLine(messages.errors[0], chalk.red))
      onError()
    } else if (messages.warnings.length > 0) {
      log.warn(`Compiled in ${chalk.cyan(time)} with warnings:`)
      messages.warnings.forEach(message => {
        console.log(listLine(message, chalk.yellow))
      })
    }

    // If the first time compiles, also with warnings,
    // call the onFirstReadyCallback
    if (isFirstCompile && messages.errors.length === 0) {
      onFirstReadyCallback()
      isFirstCompile = false
    }
  })

  return compiler
}

module.exports = {
  readWebpackConfig,
  createWebpackCompiler,
}