(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 rest = context.JK.Rest();
    var inBadState = false;
    var userDeferred = null;
    var userData = null;
    var self = this;

    var opts = {
      inClient: true, // specify false if you want the app object but none of the client-oriented features
      layoutOpts: {
        layoutFooter: true // specify false if you want footer to be left alone
      }
    };

    /**
     * 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) {
            data.screen = target;
            app.layout.changeToScreen(target, data);
          };
        if (targetArg) {
          targetUrl += "/:" + targetArg;
        }
        rules[target] = {route: '/' + targetUrl + '/:d?', method: target};
        routingContext[target] = fn;

        // allow dialogs to take an optional argument
        rules[target+'opt'] = {route: '/' + targetUrl + '/:d?/d1:', method: target};
        routingContext[target + 'opt'] = fn;
      });
      routes.context(routingContext);
      for (rule in rules) if (rules.hasOwnProperty(rule)) routes.add(rules[rule]);
      $(context).bind('hashchange', function(e) { app.layout.onHashChange(e, routes.handler)});
      $(routes.handler);
    }

    /**
     * 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."})
      }
    }

    /**
     * This occurs when a new download from a recording has become available
     */
    function downloadAvailable() {
      context.jamClient.OnDownloadAvailable();
    }

    /**
     * This most likely occurs when multiple tabs in the same browser are open, until we make a fix for this...
     */
    function duplicateClientError() {
      context.JK.Banner.showAlert("Duplicate Window (Development Mode Only)", "You have logged in another window in this browser.  This window will continue to work but with degraded functionality.  This limitation will soon be fixed.")
      context.JK.JamServer.noReconnect = true;
    }

    function registerBadStateError() {
      context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SERVER_BAD_STATE_ERROR, serverBadStateError);
    }

    function registerBadStateRecovered() {
      context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SERVER_BAD_STATE_RECOVERED, serverBadStateRecovered);
    }

    function registerDownloadAvailable() {
      context.JK.JamServer.registerMessageCallback(context.JK.MessageType.DOWNLOAD_AVAILABLE, downloadAvailable);
    }

    function registerDuplicateClientError() {
      context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SERVER_DUPLICATE_CLIENT_ERROR, duplicateClientError);
    }

    /**
     * Generic error handler for Ajax calls.
     */
    function ajaxError(jqXHR, textStatus, errorMessage) {

      if (!textStatus) {
        textStatus = 'n/a'
      }
      if (!errorMessage) {
        errorMessage = 'n/a'
      }
      //var err;
      //try { throw new Error('lurp'); } catch(e) { err = e };
      //console.log("TRACE", JSON.stringify(err))

      if (jqXHR.status === 404) {
        logger.error("Unexpected ajax error: " + textStatus + ", msg:" + errorMessage);
        app.notify({title: "Oops!", text: "What you were looking for is gone now."});
      }
      else if(jqXHR.status === 403) {
        logger.debug("not logged in or something else");

        try {
          var responseJson = JSON.parse(jqXHR.responseText)

          if (responseJson['message'] == 'IP blacklisted') {
            app.notify({title: 'Suspicious Activity', text: 'The IP address you are accessing JamKazam from has shown suspicious activity in the past.  If you believe this is in error, please contact support@jamkazam.com.', icon_url: "/assets/content/icon_alert_big.png"})
            return;
          }
          if (responseJson['message'] == 'User blacklisted') {
            app.notify({title: 'Suspicious Activity', text: 'The user account that you are accessing JamKazam from has shown suspicious activity in the past.  If you believe this is in error, please contact support@jamkazam.com.', icon_url: "/assets/content/icon_alert_big.png"})
            return;
          }
        }catch(e) { }

        app.notify({title: 'Not Logged In', text: 'Please log in.', icon_url: "/assets/content/icon_alert_big.png"})
      }
      else if (jqXHR.status === 422) {
        logger.error("Unexpected ajax error: " + textStatus + ", msg: " + errorMessage + ", response: " + jqXHR.responseText);
        // present a nicer message
        try {
          var text = "<ul>";
          var errorResponse = JSON.parse(jqXHR.responseText)["errors"];
          for (var key in errorResponse) {
            var errorsForKey = errorResponse[key];
            logger.debug("key: " + key);
            var prettyKey = context.JK.entityToPrintable[key];
            if (!prettyKey) {
              prettyKey = key;
            }
            for (var i = 0; i < errorsForKey.length; i++) {

              text += "<li>" + prettyKey + " " + errorsForKey[i] + "</li>";
            }
          }

          text += "<ul>";

          app.notify({title: "Oops!", text: text, "icon_url": "/assets/content/icon_alert_big.png"});
        }
        catch (e) {
          // give up; not formatted correctly
          app.notify({title: textStatus, text: errorMessage, detail: jqXHR.responseText});
        }
      }
      else {

        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, beforeDisconnect, 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, true);
    };

    /** Shows an alert notification. Expects text, title */
    this.notifyAlert = function (title, text) {
      this.notify({title: title, text: text, icon_url: "/assets/content/icon_alert_big.png"});
    }

    /** Using the standard rails style error object, shows an alert with all seen errors */
    this.notifyServerError = function (jqXHR, title) {
      if (!title) {
        title = "Server Error";
      }
      if (jqXHR.status == 422) {
        var errors = JSON.parse(jqXHR.responseText);
        var $errors = context.JK.format_all_errors(errors);
        logger.debug("Unprocessable entity sent from server:", JSON.stringify(errors))
        this.notify({title: title || "Validation Error", text: $errors, icon_url: "/assets/content/icon_alert_big.png"})
      }
      else if(jqXHR.status == 403) {
        logger.debug("permission error sent from server:", jqXHR.responseText)

        try {
          var responseJson = JSON.parse(jqXHR.responseText)

          if (responseJson['message'] == 'IP blacklisted') {
            this.notify({title: 'Suspicious Activity', text: 'The IP address you are accessing JamKazam from has shown suspicious activity in the past.  If you believe this is in error, please contact support@jamkazam.com.', icon_url: "/assets/content/icon_alert_big.png"})
            return;
          }
          if (responseJson['message'] == 'User blacklisted') {
            this.notify({title: 'Suspicious Activity', text: 'The user account that you are accessing JamKazam from has shown suspicious activity in the past.  If you believe this is in error, please contact support@jamkazam.com.', icon_url: "/assets/content/icon_alert_big.png"})
            return;
          }
          if (responseJson['message'] == 'You need to verify your email.') {
            this.notify({title: 'Please Verify Your Email', text: 'Please click the verification link in the email you received when you signed up for JamKazam.  If you believe this is in error, please contact support@jamkazam.com.', icon_url: "/assets/content/icon_alert_big.png"})
            return;
          }
        }catch(e) { }

        // default
        this.notify({title: 'Permission Error', text: 'You do not have permission to access this information', icon_url: "/assets/content/icon_alert_big.png"})
      }
      else {
        if (jqXHR.responseText.indexOf('<!DOCTYPE html>') == 0 || jqXHR.responseText.indexOf('<html')) {
          // we need to check more status codes and make tailored messages at this point
          var showMore = $('<a href="#">Show Error Detail</a>');
          showMore.data('responseText', jqXHR.responseText);
          showMore.click(function () {
            var self = $(this);
            var text = self.data('responseText');
            var bodyIndex = text.indexOf('<body');
            if(bodyIndex > -1) {
              text = text.substr(bodyIndex);
            }
            logger.debug("html", text);
            $('#server-error-dialog .error-contents').html(text);
            app.layout.showDialog('server-error-dialog')
            return false;
          });
          this.notify({title: title, text: showMore, icon_url: "/assets/content/icon_alert_big.png"})
        }
        else {
          this.notify({title: title, text: "status=" + jqXHR.status + ", message=" + jqXHR.responseText, icon_url: "/assets/content/icon_alert_big.png"})
        }
      }
    }

    /**
     * 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 screen = 'home'
      try {
        var location = context.RouteMap.parse(hash);


        screen = location.page.substring(1); // remove leading slash
      }
      catch (e) {
        logger.debug("ignoring bogus screen name: %o", hash)
        hash = null;
      }


      var $destination = $('[layout-id="' + screen + '"]');

      if(!context.JK.currentUserId && !$destination.is('.no-login-required')) {
        logger.debug("not logged in so redirected to login from screen: " + screen)
        var redirectPath= '?redirect-to=' + encodeURIComponent(JK.locationPath());
        if(gon.isNativeClient) {
          window.location.href = '/signin' + redirectPath;
        }
        else {
          window.location.href = '/signin' + redirectPath;
        }
        return;
      }

      var url = '/client#/' + screen;
      if (hash) {
        url = hash;
      }

      logger.debug("jamkazam: Changing screen to " + url + " (hash=" + hash + ")") ;
      context.location = url;
    }

    this.updateUserModel = function(userUpdateData) {

      var update = rest.updateUser(userUpdateData)
      update.done(function() {
        logger.debug("updating user info")
        userDeferred = update; // update the global user object if this succeeded
      })
      update.done(this.updateUserCache)
      return update;
    }

    // call .done/.fail on this to wait for safe user data
    this.user = function() {
      return userDeferred;
    }

    // gets the most recent user data.  can be null when app is still initializing.
    // user app.user() if initialize sequence is unknown/asynchronous
    this.currentUser = function() {
      if(userData == null) {
        throw "currentUser has null user data"
      }
      return userData;
    }

    this.refreshUser = function() {
      userDeferred = rest.getUserDetail();
      if (userDeferred) {
        userDeferred.done(this.updateUserCache)
      }
      else {
        userDeferred = new $.Deferred();
        userDeferred.reject('not_logged_in');
      }
      return userDeferred;
    }

    this.activeElementEvent = function(evtName, data) {
      return this.layout.activeElementEvent(evtName, data);
    }

    this.updateNotificationSeen = function(notificationId, notificationCreatedAt) {
      context.JK.JamServer.updateNotificationSeen(notificationId, notificationCreatedAt);
    }

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

      context.JK.JamServer.close(false);

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

    this.updateUserCache = function(_userData) {
      userData = _userData
    }

    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);
      events();
      this.layout.handleDialogState();

      userDeferred = rest.getUserDetail();
      if (userDeferred) {
        userDeferred.done(this.updateUserCache)

        if (opts.inClient) {
          registerBadStateRecovered();
          registerBadStateError();
          registerDownloadAvailable();
          registerDuplicateClientError();
          context.JK.FaderHelpers.initialize();
          context.window.onunload = this.unloadFunction;

          userDeferred.fail(function(jqXHR) {
            app.notify({title: "Unable to Load User", text: "You should reload the page."})
          });
        }
      }
      else {
        userDeferred = new $.Deferred();
        userDeferred.reject('not_logged_in');
      }

      $(document).triggerHandler('JAMKAZAM_READY', {app:app})

    };

    // Holder for a function to invoke upon successfully completing the FTUE.
    // See createSession.submitForm as an example.
    this.afterFtue = null;

    // 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;

    $(document).triggerHandler('JAMKAZAM_CONSTRUCTED', {app:this})
    return this;
  };

})(window, jQuery);