'use strict';

/* Given code,
 * return a list of the line numbers that are missing semicolons.
 * `expected_a_b` is the name of the error that JSLINT returns.
 * This error is where it's expecting `a` but `b` is found. We test when
 * this error exists and `a` is a semicolon, so we know that it's missing
 * a semicolon.
 */
// TODO handle this in the linter module.
function linesWithoutSemicolons(code) {
    // TODO: Shim JSLINT
    var lintResults = jslint(code);
    var warnings = lintResults.warnings;
    var lines = [];
    if (warnings && warnings.length) {
        _.each(warnings, function(error) {
            if (error && error.code == 'expected_a_b' && error.a == ';') {
                // Gotcha here. jslint line numbers start at 0.
                lines.push(error.line + 1);
            }
        });
    }
    return lines;
}

/* This code adapted from
 * http://james.padolsey.com/javascript/removing-comments-in-javascript/
 * Removes comments from a string of javascript text. */
function removeComments(str) {
    str = str.split('');
    var mode = {
        singleQuote: false,
        doubleQuote: false,
        blockComment: false,
        lineComment: false,
        condComp: false
    };
    for (var i = 0, l = str.length; i < l; i++) {

        if (mode.singleQuote) {
            if (str[i] === "'" && str[i - 1] !== '\\') {
                mode.singleQuote = false;
            }
            continue;
        }

        if (mode.doubleQuote) {
            if (str[i] === '"' && str[i - 1] !== '\\') {
                mode.doubleQuote = false;
            }
            continue;
        }

        if (mode.blockComment) {
            if (str[i] === '*' && str[i + 1] === '/') {
                if (str[i + 1] !== '\n') {
                    str[i + 1] = '';
                }
                mode.blockComment = false;
            }
            if (str[i] !== '\n') {
                    str[i] = '';
            }
            continue;
        }

        if (mode.lineComment) {
            if (str[i + 1] === '\n' || str[i + 1] === '\r') {
                mode.lineComment = false;
            }
            if (str[i] !== '\n') {
                str[i] = '';
            }
            continue;
        }

        if (mode.condComp) {
            if (str[i - 2] === '@' && str[i - 1] === '*' &&
                str[i] === '/') {
                mode.condComp = false;
            }
            continue;
        }

        mode.doubleQuote = str[i] === '"';
        mode.singleQuote = str[i] === "'";

        if (str[i] === '/') {

            if (str[i + 1] === '*' && str[i + 2] === '@') {
                mode.condComp = true;
                continue;
            }
            if (str[i + 1] === '*') {
                if (str[i] !== '\n') {
                    str[i] = '';
                }
                mode.blockComment = true;
                continue;
            }
            if (str[i + 1] === '/') {
                if (str[i] !== '\n') {
                    str[i] = '';
                }
                mode.lineComment = true;
                continue;
            }
        }

    }
    return str.join('');
}

// If we generate an error from within the user's code
// (e.g. when we inject infinite loop detection into their
// loops), the line number for the error will be incorrect.
// For this case, we need to examine the stack trace of the
// error to get the line number within the user's code.
function lineNumberFromError(error) {
    var realLineNo = -1;

    // User code shows up as <anonymous> in stack trace.
    var stack = error.stack.replace(
            /<anonymous>:(.*):/gm,
            function(match, p1, offset, string) {
                if (realLineNo == -1) {
                    realLineNo = parseInt(p1);
                }
            }
        );
    return realLineNo;
}

// Splits a code string on newline characters and returns an array.
// Okay to run after comments are stripped, as
// the removeComments() function leaves a newline character if a line contains
// only a comment.
function generateCodeArray(code) {
    var lines = code.split('\n');
    return lines;
}

/**
 * Safely call a function which is in some way executing student (unsafe) code.
 * Safely implies that any errors will be caught and handled appropriately by
 * the editor.
 *
 * @param {String} fnToTry
 *     Unsafe function to call. Unsafe because it is filled with student code.
 * @param {Object} options
 *   Where:
 *   - **`fnOnError`** – {Function} – This is function that gets called if
 *       there was an error when calling fnToTry.
 *   - **`toTryArgs`** – {Array(like)} – This is an array like object that is
 *       passed to the fnToTry function. toTryArgs is not garunteed to be an
 *       Array because it might be the javascript arguments object.
 *       https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/arguments.
 * @return: {Anything}
 *     returns whatever fnToTry or fnOnError returns.
 *
 */
function safeEditorCode(fnToTry, options) {
    var trace;
    var traceArray;
    var message;

    options = options || {};
    // fnOnError is noop if not passed.
    var fnOnError = options.fnOnError || function() {};
    // toTryArgs is empty array if not passed.
    var toTryArgs = options.toTryArgs || [];

    try {
        return fnToTry.apply(null, toTryArgs);
    } catch (e) {
        try {
            traceArray = printStackTrace({e: e});
            // We add 'Uncaught' to beginning for backwards compat with old
            // error messages. `Uncaught` is often looked for in the regex of
            // the error translators.
            // traceArray[0] is the message of the given error.
            message = 'Uncaught ' + traceArray[0];
            trace = traceArray.join('\n');

            // Pass error message and trace to the editorErrors handler.
            // The editorErrors will display an info modal about the given error.
            CHS.PubSub.trigger('editorError:handle', message, trace);
        } catch (e) {
            alert('Error in code');
        }

        // Pass error message and trace to the editorErrors handler.
        // The editorErrors will display an info modal about the given error.

        return fnOnError();
    }
}

/**
 * Safely eval a students code. Safely implies that any errors will be caught
 * and handled appropriately by the editor.
 *
 * @param {String} code
 *     student's code.
 * @param {Object} context
 *     context to inject into environment where code is eval'd.
 * @param {String} contextName
 *     identifier to refer to injected context.
 * @return: {Object}
 *     {
 *         hasError: <boolean>,
 *         result: <anything> // The results of eval'ing code.
 *     }.
 *
 */
function safeEval(code, context, contextName) {
    /* We need to inject another context into the code being eval'd, so we wrap
    the code in an IIFE with the param `contextName` and the arg `__context__` .
    We are able to reference the var `__context__` in the eval environment b/c
    eval() scopes to the calling context. This step necessary because of how
    the karelrunner and graphicsrunner add functionality to the eval execution
    environment. */
    var __context__ = context;
    code = '(function(' + contextName + '){' + code;
    code += '})(__context__);';

    console.log('hey bud');
    console.log(code);

    var fnToTry = function() {
        return {
            result: eval(code),
            hasError: false
        };
    };

    var fnOnError = function() {
        return {
            hasError: true
        };
    };

    return safeEditorCode(fnToTry, {
        fnOnError: fnOnError
    });
}


/**
 * Wrap an unsafe function in a safe function. Safe implies that any errors
 * will be caught and handled appropriately by the editor.
 * @param {String} fnToTry
 *     Unsafe function to wrap. Unsafe because it is filled with student code.
 * @param {Function} fnOnError
 *     --Optional--
 *     This is function that gets called if there was an error when calling
 *     the fnToTry function.
 * @return: {Function}
 *     A function that can safely be called. 'Safely' implies that any errors
 *     will be caught and handled appropriately by the editor.
 *
 */
function safeCallback(fnToTry, fnOnError) {
    return function() {
        var options = {};
        options.fnOnError = fnOnError;
        options.toTryArgs = arguments;
        return safeEditorCode(fnToTry, options);
    }
}

/**
 * Set a safe interval from an unsafe function. Safe implies that any errors
 * will be caught and handled appropriately by the editor.
 * @param {String} fnToTry
 *     Unsafe function to set as interval callback.
 * @param {Anything} data
 *     Data to pass to callback of setInterval.
 * @param {int} time
 *     Delay for setInterval.
 * @return: {int}
 *     intervalId.
 *
 */
function safeSetInterval(fnToTry, data, time) {
    var fnOnError = function() {
        // Has access to intervalId becuase of closure.
        clearInterval(intervalId);
    };
    var callback = safeCallback(fnToTry, fnOnError);
    var intervalId = setInterval(callback, time, data);

    return intervalId;
}

/**
 * Editor utils export
 */
module.exports = {
    linesWithoutSemicolons: linesWithoutSemicolons,
    removeComments: removeComments,
    lineNumberFromError: lineNumberFromError,
    generateCodeArray: generateCodeArray,
    safeEval: safeEval,
    safeCallback: safeCallback,
    safeSetInterval: safeSetInterval
};
