/**
 * Context used/shared by tests run a specific system
 */
var TestContext = function() {
    this.versionAppletLauncher = null

    this.lastVerificationHistory = this.readPersistedVerificationHistory();
    this.verificationHistory= extend({}, this.lastVerificationHistory); // make a copy
}

TestContext.LAST_TEST_KEY = "last_test";
TestContext.TEST_KEY = TestContext.LAST_TEST_KEY;
TestContext.LAST_SUCCESS_KEY = "last_success";
TestContext.SUCCESS_KEY = TestContext.LAST_SUCCESS_KEY;
TestContext.LAST_CODE_KEY = "last_code";
TestContext.CODE_KEY = TestContext.LAST_CODE_KEY;
TestContext.JAVA_CONTEXT_KEY = "java_context_id";

TestContext.prototype = {
    recordTestStart : function(testName) {
        this.verificationHistory[TestContext.LAST_TEST_KEY] = testName;
        this.verificationHistory[TestContext.LAST_SUCCESS_KEY] = null;
        this.verificationHistory[TestContext.LAST_CODE_KEY] = null;

        this.persistVerificationHistory();
    },

    recordTestResult : function(success, code) {
        this.verificationHistory[TestContext.LAST_SUCCESS_KEY] = success;
        this.verificationHistory[TestContext.LAST_CODE_KEY] = code;

        this.persistVerificationHistory();
    },

    recordTestDatapoint : function(name, value) {
        this.verificationHistory[name] = value;

        this.persistVerificationHistory();
    },

    /**
     * Returns a js hash containing the state of the running verification session
     */
    getVerificationState : function() {
        return this.verificationHistory;
    },

    getVerificationHistory : function() {
        return this.lastVerificationHistory;
    },

    persistVerificationHistory : function() {
        createCookie("verification_history", JSON.stringify(this.verificationHistory), 100);
    },

    getLastVerificationHistory : function() {
        return this.lastVerificationHistory;
    },

    /**
     * Reads a verification history object from the cookie store.
     */
    readPersistedVerificationHistory: function() {
        var cookieValue = readCookie("verification_history");

        var history = {}

        if(cookieValue) {
            try {
                history = JSON.parse(cookieValue)
            } catch(e) {
            }
        }

        return history;
    }
}


/**
 * Base class extended by all verifier tests.
 *
 * This base class currently serves two main purposes.
 *
 * 1. provide common functions that allow signalling of test results
 * 2. convience methods of accessing certain properties from the TestContext in use.
 */
var BaseTest = function() {
}

BaseTest.prototype = {
    _initialize : function(name) {
        this.id = name;

        this.progressListenerFunction = null;
        this.resultListener = null;
        this.warningListener = null;

        this.loggingTarget = null; // target logging object, probably expect methods like info(message)
        this.loggingTargetInfoFunction = null; //actual logging function for infomation messages

        this.progressEvent = new TestProgressEvent();
    },

    notifyFailure : function(stepName, failureCode, params) {
        params = this.normalizeParams(params);

        if (this.resultListener)
            this.resultListener.testFailed(new TestFailureEvent(stepName, failureCode, params))
    },

    notifySuccess : function(message, params) {
        params = this.normalizeParams(params);

        this.logInfo("Test Success {" + this.id + "} reporting params " + paramsToString(params));

        if (this.resultListener)
            this.resultListener.testSuccessful(new TestSuccessEvent(message, params))
    },

    notifyWarning: function(code, msg, params) {
        params = this.normalizeParams(params);

        if(this.warningListener)
            this.warningListener.warning({code: code, params: params});
    },

    /**
     * Creates an empty params array if necessary, and calls this.createExternalRepresentation on the param list
     * @param params
     */
    normalizeParams: function(params) {
        if (!params) params = {}
        params = this.createExternalRepresentation(params);

        return params;
    },

    /**
     * Allows params object to return a specific params instance by calling "getParameters" on the param parameter,
     * otherwise, return params itself.
     *
     * Performs other conversions on the parameters, preparing them for external consumption
     * 
     * @param params
     */
    createExternalRepresentation : function(params) {
        if (params.getParameters && typeof params.getParameters == "function")
            params = params.getParameters();

        // expand params with name "e" or containing "exception" to create similarly named
        for(var name in params) {
            var lowercaseName = name.toLowerCase();
            if(lowercaseName == "e" || lowercaseName.indexOf("exception") >= 0) {
                var value = params[name];

                if(value.name && value.message) {
                    params[name + "Name"] = value.name;
                    params[name + "Message"] = value.message;
                }
            }
        }

        return params;
    },

    setContext : function(context) {
        this.context = context;
    },

    fireProgressUpdate : function(percentage, step, message) {
        if (this.progressListenerFunction) {
            this.progressEvent.setStep(step)
            this.progressEvent.setPercentage(percentage)
            this.progressEvent.setMessage(message)

            this.progressListenerFunction(this.progressEvent);
        }
    },

    setProgressListener : function(listenerFunction) {
        this.progressListenerFunction = listenerFunction;
    },

    /**
     * Sets a result listener on this test instance.  A ResultListener is an object with methods: testSuccessful, testFailed 
     */
    setResultListener : function(resultListenerFunction) {
        this.resultListener = resultListenerFunction;
    },

    setWarningListener: function(warningListener) {
        this.warningListener = warningListener;
    },

    logInfo: function(message) {
        if(!this.loggingTargetInfoFunction) { // init
            if(this.loggingTarget && this.loggingTarget.info)
                this.loggingTargetInfoFunction = this.loggingTarget.info;
            else
                this.loggingTargetInfoFunction = Mixamo.console.info;
        }

        this.loggingTargetInfoFunction(message);
    },

    //
    // candidate for removal, -ewebb, 090601
    //

//    /**
//     * Executes the passed function.  If the function throws an exception, this calls t
//     * @param func
//     */
//    callFunction : function(stepName, func) {
//        try {
//            func();
//
//            this.notifySuccess()
//
//            return true;
//        } catch (e) {
//            this.notifyFailure(stepName, "EXCEPTION", {exception : e});
//            return false;
//        }
//    },

    /**
     * Populates the state of the destination test with appropriate state from this test
     * (can be used to facilitate chaining tests internally by copy appropriate context and listeners)
     * 
     * @param destTest
     */
    populate: function(destTest) {
        destTest.progressListenerFunction = this.progressListenerFunction;
        destTest.resultListener = this.resultListener;
        destTest.warningListener = this.warningListener;

        destTest.loggingTarget = this.loggingTarget;
        destTest.loggingTargetInfoFunction = this.loggingTargetInfoFunction;

        destTest.setContext(this.context);

        return destTest;
    },

    populateFrom: function(srcTest) {
        return srcTest.populate(this);
    },

    /*
        TestContext helper methods
     */
    getVersionApplet : function() {
        return this.context.versionApplet;
    },

    setVersionApplet : function(applet) {
        this.context.versionApplet = applet;
    },

    getVerifierAppletLauncher : function() {
        return this.context.verifierAppletLauncher;
    },

    setVerifierAppletLauncher : function(launcher) {
        this.context.verifierAppletLauncher = launcher;
    },

    getVerifierApplet : function() {
        return this.context.verifierAppletLauncher.appletElement;
    },

    getDetectedJavaVersion: function() {
        return this.context.detectedJavaVersion;
    },

    setDetectedJavaVersion: function(version) {
        this.context.detectedJavaVersion = version;
    },

    testDirectX: function() {
        return !this.context.mac;
    },

    setMac: function(isMac) {
        this.context.mac = isMac;
    }
}

var TestProgressEvent = function() {
    this.step = null; // the name of the current test step
    this.percentage = 0; // percentage completed (0 - 100)
    this.message = null; // the message to display to the user
    this.eta = -1; // ETA of task completion (or -1 if no ETA known)
}

TestProgressEvent.prototype = {
    setPercentage : function(percentage) {
        this.percentage = percentage;
    },

    setMessage : function(message) {
        this.message = message;
    },

    setStep : function(step) {
        this.step = step;
    }
}

var TestSuccessEvent = function(message, params) {
    this.message = message
    this.params = params
}

TestSuccessEvent.prototype = {

}

var TestFailureEvent = function(stepName, failureCode, params) {
    this.step = stepName;

    this.code = failureCode;
    this.params = params;
}

TestFailureEvent.prototype = {
    getCode : function() {
        return this.code;
    },

    getStep : function() {
        return this.step;
    },

    getParams : function() {
        return this.params;
    }
}

var TestParameters = function() {
    this.parameters = {};
};

extend(TestParameters.prototype, {
    merge: function(source) {
        extend(this.parameters, source);

        return this;
    },

    getParameters: function() {
        return this.parameters;
    }
})

function getJavaVersionSafe() {
    var javaVersion = "UNKNOWN";

    var params = new Array()
    params.push("Java")

    if(!isMac())
        params.push("/jars/getJavaInfo.jar")
    
    try {
        var rawJavaVersion = PluginDetect.getVersion('Java')
        if (rawJavaVersion != null) {
            javaVersion = rawJavaVersion.replace(/,/g, '.');
            var index = javaVersion.lastIndexOf('.');
            if (index >= 0)
                javaVersion = javaVersion.slice(0, index) + "_" + javaVersion.slice(index + 1);
        }
    } catch(e) {
    }

    return javaVersion;
}

function isMac() {
    return navigator.appVersion.indexOf("Mac") != -1;
}

var MethodRetrier = function(destination) {
    this.destination = destination;
    
    this.default_retry_count = 10;
}

extend(MethodRetrier.prototype, {
    call_noargs: function(methodName, try_count) {
        if(typeof try_count == "undefined") {
            try_count = this.default_retry_count;
        }

        var last_exception = null;
        for(var i = 0; i < try_count; i++) {
            try {
                var return_value = this.destination[methodName]();

                return return_value;
            } catch(e) {
                last_exception = e;
            }
        }

        // if we never returned above, throw the last exception we recorded
        throw last_exception;
    }
})

/*
    conditionally add Array.indexOf function to Array class
 */
if (!Array.prototype.indexOf)
{
  Array.prototype.indexOf = function(elt /*, from*/)
  {
    var len = this.length >>> 0;

    var from = Number(arguments[1]) || 0;
    from = (from < 0)
         ? Math.ceil(from)
         : Math.floor(from);
    if (from < 0)
      from += len;

    for (; from < len; from++)
    {
      if (from in this &&
          this[from] === elt)
        return from;
    }
    return -1;
  };
}

// stacktrace helper code
// http://eriwen.com/javascript/js-stack-trace/
function printStackTrace() {
  var callstack = [];
  var isCallstackPopulated = false;
  try {
    i.dont.exist+=0; //doesn't exist- that's the point
  } catch(e) {
    if (e.stack) { //Firefox
      var lines = e.stack.split("\n");
      for (var i=0, len=lines.length; i<len; i++) {
        if (lines[i].match(/^\s*[A-Za-z0-9\-_\$]+\(/)) {
          callstack.push(lines[i]);
        }
      }
      //Remove call to printStackTrace()
      callstack.shift();
      isCallstackPopulated = true;
    }
    else if (window.opera && e.message) { //Opera
      var lines = e.message.split("\n");
      for (var i=0, len=lines.length; i<len; i++) {
        if (lines[i].match(/^\s*[A-Za-z0-9\-_\$]+\(/)) {
          var entry = lines[i];
          //Append next line also since it has the file info
          if (lines[i+1]) {
            entry += " at " + lines[i+1];
            i++;
          }
          callstack.push(entry);
        }
      }
      //Remove call to printStackTrace()
      callstack.shift();
      isCallstackPopulated = true;
    }
  }
  if (!isCallstackPopulated) { //IE and Safari
    var currentFunction = arguments.callee.caller;
    while (currentFunction) {
      var fn = currentFunction.toString();
      var fname = fn.substring(fn.indexOf("function") + 8, fn.indexOf("(")) || "anonymous";
      callstack.push(fname);
      currentFunction = currentFunction.caller;
    }
  }
  output(callstack);
}

function output(arr) {
    Mixamo.console.info(arr.join("nn"));
}
