(function(context,$) {

    "use strict";

    // Change underscore's default templating characters as they
    // conflict withe the ERB rendering. Templates will use:
    //    {{ interpolate }}
    //    {% evaluate %}
    //    {{- escape }}
    //
    context._.templateSettings = {
        evaluate    : /\{%([\s\S]+?)%\}/g,
        interpolate : /\{\{([\s\S]+?)\}\}/g,
        escape      : /\{\{-([\s\S]+?)\}\}/g
    };

    context.JK = context.JK || {};

    var JamKazam = context.JK.JamKazam = function() {
        var app;
        var logger = context.JK.logger;
        var heartbeatInterval=null;
        var heartbeatMS=null;
        var heartbeatMissedMS= 10000; // if 5 seconds go by and we haven't seen a heartbeat ack, get upset
        var inBadState=false;
        var lastHeartbeatAckTime=null;
        var lastHeartbeatFound=false;
        var heartbeatAckCheckInterval = null;

        var opts = {
            layoutOpts: {}
        };

        /**
        * Dynamically build routes from markup. Any layout="screen" will get a route corresponding to
        * his layout-id attribute. If a layout-arg attribute is present, that will be named as a data
        * section of the route.
        */
        function routing() {
            var routes = context.RouteMap,
                rules = {},
                rule,
                routingContext = {};
            $('div[layout="screen"]').each(function() {
                var target = $(this).attr('layout-id'),
                    targetUrl = target,
                    targetArg = $(this).attr('layout-arg'),
                    fn = function(data) {
                        app.layout.changeToScreen(target, data);
                    };
                if (targetArg) {
                    targetUrl += "/:" + targetArg;
                }
                rules[target] = {route: '/' + targetUrl + '/d:?', method: target};
                routingContext[target] = fn;
            });
            routes.context(routingContext);
            for (rule in rules) if (rules.hasOwnProperty(rule)) routes.add(rules[rule]);
                $(context).bind('hashchange', routes.handler);
                $(routes.handler);
        }

        function _heartbeatAckCheck() {

            // if we've seen an ack to the latest heartbeat, don't bother with checking again
            // this makes us resilient to front-end hangs
            if (lastHeartbeatFound) { return; }

            // check if the server is still sending heartbeat acks back down
            // this logic equates to 'if we have not received a heartbeat within heartbeatMissedMS, then get upset
            if(new Date().getTime() - lastHeartbeatAckTime.getTime() > heartbeatMissedMS) {
                logger.error("no heartbeat ack received from server after ", heartbeatMissedMS, " seconds . giving up on socket connection");
                context.JK.JamServer.close(true);
            }
            else {
                lastHeartbeatFound = true;
            }
        }

        function _heartbeat() {
            if (app.heartbeatActive) {

                var message = context.JK.MessageFactory.heartbeat();
                context.JK.JamServer.send(message);
                lastHeartbeatFound = false;
            }
        }

        function loggedIn(header, payload) {
            app.clientId = payload.client_id;
            $.cookie('client_id', payload.client_id);
            // $.cookie('remember_token', payload.token); // removed per vrfs-273/403

            heartbeatMS = payload.heartbeat_interval * 1000;
            logger.debug("jamkazam.js.loggedIn(): clientId now " + app.clientId + "; Setting up heartbeat every " + heartbeatMS + " MS");
            heartbeatInterval = context.setInterval(_heartbeat, heartbeatMS);
            heartbeatAckCheckInterval = context.setInterval(_heartbeatAckCheck, 1000);
            lastHeartbeatAckTime = new Date(new Date().getTime() + heartbeatMS); // add a little forgiveness to server for initial heartbeat
        }

        function heartbeatAck(header, payload) {
            lastHeartbeatAckTime = new Date();
        }

        /**
         * This occurs when the websocket gateway loses a connection to the backend messaging system,
         * resulting in severe loss of functionality
         */
        function serverBadStateError() {
            if(!inBadState) {
                inBadState = true;
                app.notify({title: "Server Unstable", text: "The server is currently unstable, resulting in feature loss.  If you are experiencing any problems, please try to use JamKazam later."})
            }
        }

        /**
         * This occurs when the websocket gateway loses a connection to the backend messaging system,
         * resulting in severe loss of functionality
         */
        function serverBadStateRecovered() {
            if(inBadState) {
                inBadState = false;
                app.notify({title: "Server Recovered", text: "The server is now stable again.  If you are still experiencing problems, either create a new music session or restart the client altogether."})
            }
        }

        /**
         * Called whenever the websocket closes; this gives us a chance to cleanup things that should be stopped/cleared
         * @param in_error did the socket close abnormally?
         */
        function socketClosed(in_error) {

            // stop future heartbeats
            if(heartbeatInterval != null) {
                clearInterval(heartbeatInterval);
                heartbeatInterval = null;
            }

            // stop checking for heartbeat acks
            if(heartbeatAckCheckInterval != null) {
                clearTimeout(heartbeatAckCheckInterval);
                heartbeatAckCheckInterval = null;
            }
        }

        function registerLoginAck() {
            logger.debug("register for loggedIn to set clientId");
            context.JK.JamServer.registerMessageCallback(context.JK.MessageType.LOGIN_ACK, loggedIn);
        }

        function registerHeartbeatAck() {
            logger.debug("register for heartbeatAck");
            context.JK.JamServer.registerMessageCallback(context.JK.MessageType.HEARTBEAT_ACK, heartbeatAck);
        }

        function registerBadStateError() {
            logger.debug("register for server_bad_state_error");
            context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SERVER_BAD_STATE_ERROR, serverBadStateError);
        }

        function registerBadStateRecovered() {
            logger.debug("register for server_bad_state_recovered");
            context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SERVER_BAD_STATE_RECOVERED, serverBadStateRecovered);
        }

        function registerSocketClosed() {
            logger.debug("register for socket closed");
            context.JK.JamServer.registerOnSocketClosed(socketClosed);
        }

        /**
        * Generic error handler for Ajax calls.
        */
        function ajaxError(jqXHR, textStatus, errorMessage) {
            logger.error("Unexpected ajax error: " + textStatus);
            app.notify({title: textStatus, text: errorMessage, detail: jqXHR.responseText});
        }

        /**
        * Expose ajaxError.
        */
        this.ajaxError = ajaxError;

        /**
        * Provide a handler object for events related to a particular screen
        * being shown or hidden.
        * @screen is a string corresponding to the screen's layout-id attribute
        * @handler is an object with up to four optional keys:
        *    beforeHide, afterHide, beforeShow, afterShow, which should all have
        *    functions as values. If there is data provided by the screen's route
        *    it will be provided to these functions.
        */
        this.bindScreen = function(screen, handler) {
            this.layout.bindScreen(screen, handler);
        };
        this.bindDialog = function(dialog, handler) {
            this.layout.bindDialog(dialog, handler);
        };
        /**
        * Allow individual wizard steps to register a function to be invokes
        * when they are shown.
        */
        this.registerWizardStepFunction = function(stepId, showFunction) {
            this.layout.registerWizardStepFunction(stepId, showFunction);
        };

        /**
        * Switch to the wizard step with the provided id.
        */
        this.setWizardStep = function(targetStepId) {
            this.layout.setWizardStep(targetStepId);
        };

        /**
        * Show a notification. Expects an object with a
        * title property and a text property.
        */
        this.notify = function(message, descriptor) {
            this.layout.notify(message, descriptor);
        };

        /**
        * Initialize any common events.
        */
        function events() {
            // Hook up the screen navigation controls.
            $(".content-nav .arrow-left").click(function(evt) {
                evt.preventDefault();
                context.history.back();
                return false;
            });
            $(".content-nav .arrow-right").click(function(evt) {
                evt.preventDefault();
                context.history.forward();
                return false;
            });

            context.JK.popExternalLinks();
        }

        // Due to timing of initialization, this must be called externally
        // after all screens have been given a chance to initialize.
        // It is called from index.html.erb after connecting, and initialization
        // of other screens.
        function initialRouting() {
            routing();

            var hash = context.location.hash;

            var url = '#/home';
            if (hash) {
                url = hash;
            }
            logger.debug("Changing screen to " + url);
            context.location = url;

            if (!(context.jamClient.FTUEGetStatus())) {
                app.layout.showDialog('ftue');
            }
        }

        this.unloadFunction = function() {
            logger.debug("window.unload function called.");

            context.JK.JamServer.close(false);

            if (context.jamClient) {
                // Unregister for callbacks.
                context.jamClient.SessionRegisterCallback("");
                context.jamClient.SessionSetAlertCallback("");
                context.jamClient.FTUERegisterVUCallbacks("", "", "");
                context.jamClient.FTUERegisterLatencyCallback("");
            }
        };

        this.initialize = function(inOpts) {
            var url, hash;
            app = this;
            this.opts = $.extend(opts, inOpts);
            this.layout = new context.JK.Layout();
            this.layout.initialize(this.opts.layoutOpts);
            registerLoginAck();
            registerHeartbeatAck();
            registerBadStateRecovered();
            registerBadStateError();
            registerSocketClosed();
            events();
            context.JK.FaderHelpers.initialize();
            context.window.onunload = this.unloadFunction;
        };


        // enable temporary suspension of heartbeat for fine-grained control
        this.heartbeatActive = true;

        /**
        * Expose clientId as a public variable.
        * Will be set upon LOGIN_ACK
        */
        this.clientId = null;
        this.initialRouting = initialRouting;

        return this;
    };

 })(window, jQuery);