# console/console.js

    /* Console
     * =====================
     * Written as an immediately invoked function expression
     * http://benalman.com/news/2010/11/immediately-invoked-function-expression/
     * so that we can properly hid the private functions. The public functions
     * are returned at the bottom.
     *
     * A console represents a text console that allows the user to print, println,
     * and read an integer using readInt, and read a float using readFloat. These
     * functions pop up prompt dialogs and make sure that the results are actually
     * of the desired type.
     *
     * @author  Jeremy Keeshin    July 9, 2012
     *
     */

    'use strict';

    var $ = require('jquery');

    window.CHS = window.CHS || {};
    CHS.log = CHS.log || function() {};
    CHS.Utils = CHS.Utils || {assert: function() {} };

    var lines = [];
    var solution = null;
    var TESTER_MESSAGE = "#tester-message";
    var PUBLIC_METHODS = [];

    /**
     * Set up an instance of the console library.
     * @constructor
     */
    function CodeHSConsole() {

    }

    /**
     * Adds a method to the public methods constant.
     * @param {string} name - Name of the method.
     */
    CodeHSConsole.registerPublicMethod = function(name) {
        PUBLIC_METHODS.push(name);
    };

    /**
     * Generate strings for the public methods to bring them to the
     * public namespace without having to call them with the console instance.
     * @returns {string} Line broken function definitions.
     */
    CodeHSConsole.getNamespaceModifcationString = function() {
        var result = '';
        for(var i = 0; i < PUBLIC_METHODS.length; i++){
            var curMethod = PUBLIC_METHODS[i];

            // Actually create a method in this scope with the name of the
            // method so the student can easily access it. For example, we
            // might have a method like CodeHSConsole.prototype.print, but we
            // want the student to be able to access it with just `print`, but
            // the proper context for this.
            result += "function " + curMethod + "(){\n"+
                "\treturn __console__." + curMethod + ".apply(__console__, arguments);\n" +
                "}\n";

        }
        return result;
    };

    /**
     * Set the solution code for a given exercise.
     * @param {string} soln - Solution code.
     */
    CodeHSConsole.setSolution = function(soln){
        solution = soln;
    };

    /**
     * Check the console output for correctness against solution code.
     * returns {object} Dictionary containing boolean of success and message.
     */
    CodeHSConsole.prototype.checkOutput = function(){
        if (!solution){
            return;
        }
        var graded = {
            success: true,
            message: "<strong>Nice job!</strong> You got it!"
        };

        if ($("#console").html().length === 0){
            graded.success = false;
            graded.message = "You didn't print anything.";
        }
        else if (lines.length != solution.length) {
            graded.success = false;
            graded.message = "<strong>Not quite.</strong> Take a look at the example output in the exercise tab.";
        }
        else {
            for(var i = 0; i < lines.length; i++){
                var line = lines[i];
                var correct = solution[i];
                var regex = new RegExp(correct);
                if (line.search(regex) !== 0) {
                    graded.success = false;
                    graded.message = "<strong>Not quite.</strong> Take a look at the example output in the exercise tab.";
                }
            }
        }

        $(TESTER_MESSAGE).html(graded.message);
        if (graded.success) {
            $(TESTER_MESSAGE).removeClass("gone").removeClass("alert-error").addClass("alert-info");
        } else {
            $(TESTER_MESSAGE).removeClass("gone").removeClass("alert-info").addClass("alert-error");
        }

        return graded;
    };

    /**
     * Get the output from the console.
     * @returns {string}
     */
    CodeHSConsole.getOutput = function() {
        return $('#console').text();
    };

    /**
     * Check if the console exists.
     * Important to check before attempting to select and extract output.
     */
    CodeHSConsole.exists = function(){
        return $("#console").exists();
    };

    /**
     * Clear the console's text.
     */
    CodeHSConsole.clear = function(){
        lines = [];
        $("#console").html("");
        $(TESTER_MESSAGE).addClass("gone");
    };

    /**
     * Private method used to read a line.
     * @param {string} str - The line to be read.
     * @param {boolean} looping - Unsure. This is a messy method.
     */
    CodeHSConsole.prototype.readLinePrivate = function(str, looping){
        if(typeof looping === "undefined" || !looping){
            this.print(str);
        }
        $("#console").css('margin-top', '180px');

        var result = prompt(str);

        $("#console").css('margin-top', '0px');

        if(typeof looping === "undefined" || !looping){
            this.println(result);
        }
        return result;
    };

    /**
     * This is how you run the code, but get access to the
     * state of the console library. The current instance
     * becomes accessible in the code.
     * @param {string} code - The code from the editor.
     */
    CodeHSConsole.prototype.runCode = function(code) {
        var publicMethodStrings = CodeHSConsole.getNamespaceModifcationString();

        var wrap = '';
        wrap += publicMethodStrings;

        wrap += code;
        wrap += '\n\nif(typeof start == \'function\') {start();} ';
        wrap += '__console__.checkOutput();';

        return CHS.Utils.safeEval(wrap, this, '__console__');

    };

    /*************** PUBLIC METHODS *******************/

    /**
     * Print a line to the console.
     * @param {string} ln - The string to print.
     */
    CodeHSConsole.prototype.print = function(ln){
        if (arguments.length !== 1) {
            throw new Error('You should pass exactly 1 argument to print');
        }

        var console = $("#console");
        if (console.length) {
            console.html($("#console").html() + ln);
            console.scrollTop($("#console")[0].scrollHeight);
            lines = console.html().split("\n");
            lines.splice(lines.length - 1, 1);
        } else {
            // we must be running outside of the site.
            window.console.log(ln);
        }
    };
    CodeHSConsole.registerPublicMethod('print');

    /**
     * Print a line to the console.
     * @param {string} ln - The string to print.
     */
    CodeHSConsole.prototype.println = function(ln){
        if (arguments.length === 0) {
            ln = '';
        } else if (arguments.length !== 1) {
            throw new Error('You should pass exactly 1 argument to println');
        } else {
            this.print(ln + "\n");
            var scrollTop = $("#console").scrollTop();
        }
    };
    CodeHSConsole.registerPublicMethod('println');

    /* Read a number from the user using JavaScripts prompt function.
     * We make sure here to check a few things.
     *
     *    1. If the user checks "Prevent this page from creating additional dialogs," we handle
     *       that gracefully, by checking for a loop, and then returning a DEFAULT value.
     *    2. That we can properly parse a number according to the parse function PARSEFN passed in
     *       as a parameter. For floats it is just parseFloat, but for ints it is our special parseInt
     *       which actually does not even allow floats, even they they can properly be parsed as ints.
     *    3. The errorMsgType is a string helping us figure out what to print if it is not of the right
     *       type.
     */
    CodeHSConsole.prototype.readNumber = function(str, parseFn, errorMsgType){
        var DEFAULT = 0; // If we get into an infinite loop, return DEFAULT.
        var INFINITE_LOOP_CHECK = 100;

        var prompt = str, looping = false, loopCount = 0;
        while(true){
            var result = this.readLinePrivate(prompt, looping);
            if(result === null){
              return null;
            }
            result = parseFn(result);

            // Then it was okay.
            if(!isNaN(result)){
              return result;
            }

            if(result === null) return DEFAULT;
            if(loopCount > INFINITE_LOOP_CHECK) return DEFAULT;
            prompt = "That was not " + errorMsgType + ". Please try again. " + str;
            looping = true;
            loopCount++;
       }
    };
    CodeHSConsole.registerPublicMethod('readNumber');

    /**
     * Read a line from the user.
     * @param {str} str - A message associated with the modal asking for input.
     * @returns {str} The result of the readLine prompt.
     */
    CodeHSConsole.prototype.readLine = function(str){
        if (arguments.length !== 1) {
            throw new Error('You should pass exactly 1 argument to readLine');
        }

        return this.readLinePrivate(str, false);
    };
    CodeHSConsole.registerPublicMethod('readLine');

    /**
     * Read a bool from the user.
     * @param {str} str - A message associated with the modal asking for input.
     * @returns {str} The result of the readBoolean prompt.
     */
    CodeHSConsole.prototype.readBoolean = function(str){
      if (arguments.length !== 1) {
        throw new Error('You should pass exactly 1 argument to readBoolean');
      }
      return this.readNumber(str, function(x){
            if(x === null) return NaN;
            x = x.toLowerCase();
            if(x == "true" || x == "yes") return true;
            if(x == "false" || x == "no") return false;
            return NaN;
      }, "a boolean (true/false)");
    };
    CodeHSConsole.registerPublicMethod('readBoolean');

    /* Read an int with our special parseInt function which doesnt allow floats, even
     * though they are successfully parsed as ints.
     * @param {str} str - A message associated with the modal asking for input.
     * @returns {str} The result of the readInt prompt.
     */
    CodeHSConsole.prototype.readInt = function(str){
        if (arguments.length !== 1) {
            throw new Error('You should pass exactly 1 argument to readInt');
        }

        return this.readNumber(str, function(x){
            var resultInt = parseInt(x);
            var resultFloat = parseFloat(x);
            // Make sure the value when parsed as both an int and a float are the same
            if(resultInt == resultFloat) return resultInt;
            return NaN;
        }, "an integer");
    };
    CodeHSConsole.registerPublicMethod('readInt');

    /* Read a float with our safe helper function.
     * @param {str} str - A message associated with the modal asking for input.
     * @returns {str} The result of the readFloat prompt.
     */
    CodeHSConsole.prototype.readFloat = function(str){
        if (arguments.length !== 1) {
            throw new Error('You should pass exactly 1 argument to readFloat');
        }

        return CodeHSConsole.readNumber(str, parseFloat, "a float");
    };
    CodeHSConsole.registerPublicMethod('readFloat');

    module.exports = {
        CodeHSConsole: CodeHSConsole,
        PUBLIC_METHODS: PUBLIC_METHODS
    };