var EXPORTED_SYMBOLS = ['AppCenter'];

const Cu = Components.utils;
const Cc = Components.classes;
const Ci = Components.interfaces;

Cu.import("resource://gre/modules/XPCOMUtils.jsm");

XPCOMUtils.defineLazyGetter(this, "utils", function() {
  let obj = { };
  Cu["import"]("resource://livemargins/utils.jsm", obj);
  return obj.utils;
});

XPCOMUtils.defineLazyGetter(this, "AppCenterDAO", function() {
  let obj = { };
  Cu["import"]("resource://livemargins/dao.jsm", obj);
  return obj.AppCenterDAO;
});

XPCOMUtils.defineLazyGetter(this, "builtInApps", function() {
  let obj = { };
  Cu["import"]("resource://livemargins/builtinapps.jsm", obj);
  return obj.builtInApps;
});

XPCOMUtils.defineLazyGetter(this, "Tracker", function() {
  let obj = { };
  Cu["import"]("resource://livemargins/track.jsm", obj);
  return obj.Tracker;
});

XPCOMUtils.defineLazyGetter(this, "windowMediator", function() {
  return Cc["@mozilla.org/appshell/window-mediator;1"].getService(Ci.nsIWindowMediator);
});

XPCOMUtils.defineLazyGetter(this, "logger", function() {
  return utils.logger('LM4.AppCenter');
});

// "bind" is not defined in Firefox3
if (!Function.prototype.bind) {
  Function.prototype.bind = function (context) {
    var __method = this, args = Array.prototype.slice.call(arguments, 1);
    return function() {
      return __method.apply(context, args.concat(Array.prototype.slice.call(arguments, 0)));
    }
  };
}

var appHandlers = [];
var webClipAppHandler = {
  appURL: 'chrome://livemargins/content/apps/webclipping/app.xul',
  onBeforeOpen: function(app) {
    if (!app.app_args || !app.app_args.location) {
      return false;
    }

    return true;
  }
};

var flashClipAppHandler = {
  appURL: 'chrome://livemargins/content/apps/flashextractor/app.xul',
  onBeforeOpen: function(app) {
    if (!app.app_args || !app.app_args.location) {
      return false;
    }

    return true;
  }
};

var webAppHandler = {
  appURL: 'chrome://livemargins/content/apps/webapp/app.xul',
  onBeforeOpen: function(app) {
    if (!app.app_args || !app.app_args.location) {
      return false;
    }

    return true;
  }
};

var shortcutAppHandler = {
  onBeforeOpen: function(app) {
    if (!app.app_args || !app.app_args.location) {
      return false;
    }

    return true;
  },

  _findAndSwitchToAppTab: function(app) {
    function _isTabForApp(browser, tab, app) {
      return tab.appUID == app.uid ||
            browser.currentURI.specIgnoringRef == app.app_args.location;
    }

    function _findAndLoadAppTab(wnd,app) {
      var tabs = wnd.gBrowser.tabs;
      for (var i = 0; i < tabs.length; i++) {
        // make sure app tab is reused after restarting firefox.
        if (_isTabForApp(wnd.gBrowser.browsers[i], tabs[i], app)) {
          tabs[i].appUID = app.uid;
          // switch to app tab.
          wnd.gBrowser.tabContainer.selectedIndex = i;
          wnd.focus();

          // reload app if url is changed.
          if (wnd.gBrowser.browsers[i].currentURI.specIgnoringRef != app.app_args.location) {
            wnd.gBrowser.browsers[i].loadURI(app.app_args.location);
          }
          return tabs[i];
        }
      }
      return null;
    }

    var tab = null;
    var windowMediator = Cc["@mozilla.org/appshell/window-mediator;1"].getService(Ci.nsIWindowMediator);
    var winEnum = windowMediator.getEnumerator("navigator:browser");
    while (winEnum.hasMoreElements()) {
      var browserWin = winEnum.getNext();
      if (browserWin.closed) {
        continue;
      }

      tab = _findAndLoadAppTab(browserWin, app);
      if (tab) {
        break;
      }
    }

    return tab;
  },

  open: function(app) {
    // open link directly.
    try {
    var tab = this._findAndSwitchToAppTab(app);
    if (tab) {
      return;
    }

    var wnd = findWindow();
    tab = wnd.gBrowser.loadOneTab(app.app_args.location, {inBackground: false});
    tab.appUID = app.uid;
    // only pin tab after firefox4
//    if (wnd.gBrowser.pinTab) {
//      wnd.gBrowser.pinTab(tab);
//    }
    } catch (e) {
      logger.trace("open shortcut: " + e);
    }
  }
};

function findWindow() {
  var wnd = windowMediator.getMostRecentWindow('navigator:browser');
  if (!wnd)
    wnd = windowMediator.getMostRecentWindow('');
  return wnd;
}

var faviconService = Cc['@mozilla.org/browser/favicon-service;1'].getService(Ci.nsIFaviconService);
var AppCenter = {
  TYPE: {
    'BUILT_IN': 0,
    'WEB_CLIPPING': 1,
    'WEB_APP': 2,
    'FLASH_CLIPPING': 3,
    'SHORTCUT': 4
  },

  // Use 5.1 as the initial version which will be updated automatically.
  version: "5.1",

  addWebClipping: function(args) {
    var uuidGenerator = Components.classes["@mozilla.org/uuid-generator;1"]
          .getService(Components.interfaces.nsIUUIDGenerator);
    var uuid = uuidGenerator.generateUUID();
    var uuidString = uuid.toString();

    var icon = faviconService.getFaviconImageForPage(utils.getNsiURL(args.location)).spec;
    // Because resource page have no priority to access moz-anno:favicon
    // strip "moz-anno:favicon" here
    icon = icon.replace(/moz-anno:favicon:/i, "");
    // Install App
    this.installApp({
      appUID: uuidString,
      appName: args.name,
      iconUrl: icon == 'chrome://mozapps/skin/places/defaultFavicon.png' ? '' : icon,
      appArgs: {
        location   : args.location,
        docX     : args.docX,
        docY     : args.docY,
        width    : args.width,
        height     : args.height,
        browserWidth : args.browserWidth,
        browserHeight: args.browserHeight,
        zoom     : args.zoom
      },
      type: this.TYPE.WEB_CLIPPING,
      is_local: 1
    });

    // Update info about web clipping app, including: screenX and screenY.
    this.updateAppStateWithArg({
      uid:   uuidString,
      screen_x:  args.screenX,
      screen_y:  args.screenY,
      thumbnail: null
    });

    return uuidString;
  },

  addVideoPlayer: function(args) {
    var uuidGenerator = Components.classes["@mozilla.org/uuid-generator;1"]
          .getService(Components.interfaces.nsIUUIDGenerator);
    var uuid = uuidGenerator.generateUUID();
    var uuidString = uuid.toString();

    var icon = faviconService.getFaviconImageForPage(utils.getNsiURL(args.location)).spec;
    // Because resource page have no priority to access moz-anno:favicon
    // strip "moz-anno:favicon" here
    icon = icon.replace(/moz-anno:favicon:/i, "");
    // Install App
    this.installApp({
      appUID: uuidString,
      appName: args.name,
      iconUrl: icon == 'chrome://mozapps/skin/places/defaultFavicon.png' ? '' : icon,
      appArgs: {
        location   : args.location,
        docX     : args.docX,
        docY     : args.docY,
        width    : args.width,
        height     : args.height,
        browserWidth : args.browserWidth,
        browserHeight: args.browserHeight,
        scrollTop  : args.scrollTop,
        scrollLeft   : args.scrollLeft,
        zoom     : args.zoom,
        tagName    : args.tagName,
        elementIndex : args.elementIndex,
        elementHeight: args.elementHeight,
        elementWidth : args.elementWidth,
      },
      type: this.TYPE.FLASH_CLIPPING,
      is_local: 1
    });

    // Update info about web clipping app, including: screenX and screenY.
    this.updateAppStateWithArg({
      uid:   uuidString,
      screen_x:  args.screenX,
      screen_y:  args.screenY,
      thumbnail: null
    });

    return uuidString;
  },

  bookmarkApp: function(uid, bookmark) {
    AppCenterDAO.updateAppBookmark(uid, bookmark === true ? 1 : 0);
    if (true == bookmark) {
      AppCenter.appPref.updatePref('bookmark', uid);
      Tracker.track(Tracker.app.type, Tracker.app.actions.BOOKMARK_APP, uid);
    } else {
      AppCenter.appPref.updatePref('unbookmark', uid);
      Tracker.track(Tracker.app.type, Tracker.app.actions.REMOVE_APP, uid);
    }
  },

  checkLocal: function(uid, isLocal) {
    AppCenterDAO.updateAppLocal(uid, isLocal == true ? 1 : 0);
    if (true == isLocal) {
      AppCenter.appPref.updatePref('local', uid);
    } else {
      AppCenter.appPref.updatePref('unlocal', uid);
    }
  },

  installAndBookmarkApp: function(appInfo) {
    return this.installApp(appInfo, true);
  },

  installApp: function(appInfo, bookmark) {
    // Check if app is built-in
    // If true, get app info from built-in apps module.
    if (appInfo.type == this.TYPE.BUILT_IN) {
      if (!builtInApps[appInfo.appUID]) {
        throw new Error('Built-in app does not exist.');
      }

      appInfo = builtInApps[appInfo.appUID];
    }

    appInfo = utils.extend({
      appUID:  null,
      appName: null,
      iconUrl: null,
      appArgs: null,
      description: null,
      bookmark: bookmark === true ? 1 : 0,
      type: this.TYPE.WEB_APP,
      is_local: 0,
      smallIcon: null
    }, appInfo);

    if (!appInfo.appUID || !appInfo.appName) {
      throw new Error('Install app: Some arguments are missing.' + JSON.stringify(appInfo));
    }

    if (AppCenterDAO.appExists(appInfo.appUID)) {
      throw new Error('"' + appInfo.appUID + '" has been installed.');
    }

    return AppCenterDAO.addApp(appInfo);
  },

  removeApp: function(appUID) {
    if (!AppCenterDAO.appExists(appUID)) {
      return;
    }

    logger.trace('Remove app: ' + appUID);
    AppCenterDAO.deleteAppByUID(appUID);

    // close window
    var appWindow = this.findAppWindow(appUID);
    if (appWindow){
      appWindow.close();
    }

    // TODO: unregister handler
  },

  registerHandler: function(appUID, handler) {
    handler = utils.extend({
      appURL: null,
      onload: utils.emptyFunction,
      onBeforeOpen: utils.emptyFunction
    }, handler);

    if (!handler.appURL) {
      throw new Error('Handler registering: Some arguments are missing.');
    }

    appHandlers[appUID] = handler;

    logger.trace('Handler for "' + appUID + '" has been registered.\n');

    return true;
  },

  appHandlerExists: function(appUID) {
    return !!appHandlers[appUID];
  },

  findAppWindow: function(appUID) {
    var enumerator = windowMediator.getEnumerator("mozilla:favpart_widget");
    while(enumerator.hasMoreElements()) {
      var win = enumerator.getNext();
      if (win.uid == appUID){
        return win;
      }
    }

    return null;
  },

  getAllOpenedApp: function() {
    var enumerator = windowMediator.getEnumerator("mozilla:favpart_widget");

    var apps = [];
    while(enumerator.hasMoreElements()) {
      apps.push(enumerator.getNext().uid);
    }

    return apps;
  },

  getOpenedAppCount: function() {
    var enumerator = windowMediator.getEnumerator("mozilla:favpart_widget");

    var count = 0;
    while(enumerator.hasMoreElements()) {
      enumerator.getNext();
      count++;
    }
    return count;
  },

  appExists: function(appUID) {
    return AppCenterDAO.appExists(appUID);
  },

  showApp: function(appUID) {
    var appWindow = this.findAppWindow(appUID);
    if (appWindow){
      appWindow.focus();
      return true;
    }

    return false;
  },

  checkAutoStart: function() {
    var apps = AppCenterDAO.getAppsAutoStart();
    if (apps.length == 0) {
      return;
    }

    AppCenterDAO.clearAppAutoStart();
    var self = this;
    apps.forEach(function(app) {
      logger.trace("Open app: " + app.uid);
      self.openApp(app.uid);
    });
  },

  /**
   * Check if app exists, if not, fetch the JSON object of the app, install and open.
   * If the app exists, Open it directly.
   */
  installAndOpenApp: function(appUID) {
    if (AppCenterDAO.appExists(appUID)) {
      this.openApp(appUID);
      return;
    }

    if (builtInApps[appUID]) {
      logger.trace("Get app " + appUID + " in the built-in apps module.");
      this.installApp(builtInApps[appUID]);
      this.openApp(appUID);
      return;
    }

    logger.trace("App " + appUID + " does not exists, fetch from server.");
    let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest);
    xhr.mozBackgroundRequest = true;
    xhr.open("GET", utils.getPref("appinfourl") + appUID + "/");
    xhr.channel.loadFlags = Ci.nsIChannel.LOAD_ANONYMOUS;

    var self = this;
    xhr.onerror = function(req) {
      logger.info("Can not get object for app, see: " + req);
    };

    xhr.onload = function(req) {
      try {
        let response = JSON.parse(req.target.responseText);
        if (response.status == 200) {
           self.installApp(response.app);
           self.openApp(appUID);
        } else {
          logger.info("Can not get json object for app " + appUID + " see: " + response.message);
        }
      } catch (e) {
        logger.info("Error occurs when parsing app object, see: " + e);
      }

    };

    xhr.send();
  },

  openApp: function(appUID, updateOpenedTime, extraWidgetArgs) {
    if (this.showApp(appUID)) {
      return;
    }

    var app = AppCenterDAO.getAppByUID(appUID);

    if (!app) {
      logger.trace('App "' + appUID + '" does not exist.');
      return;
    } else {
      try {
        // parse JSON data from app_args
        app.app_args = JSON.parse(app.app_args);
      } catch (e) {
        app.app_args = { };
      }
    }

    // Check if it is an web clipping app.
    var handler = null;

    switch (app.type) {
    case this.TYPE.WEB_CLIPPING:
      handler = webClipAppHandler;
      break;
    case this.TYPE.FLASH_CLIPPING:
      handler = flashClipAppHandler;
      break;
    case this.TYPE.WEB_APP:
      handler = webAppHandler;
      break;
    case this.TYPE.SHORTCUT:
      handler = shortcutAppHandler;
      break;
    }

    handler = !handler ? appHandlers[appUID] : handler;
    if (!handler) {
      logger.info('Handler for app: "' + appUID + '" does not exist.')
      return;
    }

    if (false == handler.onBeforeOpen(app)) {
      logger.info('onBeforeOpen checking failed for app "' + appUID + '"');
      return;
    }

    // if handler defines open action, then invoke it instead.
    if (typeof handler.open == 'function') {
      handler.open(app);
      return;
    }

    var appInfo = AppCenterDAO.getAppInfoById(app.id);

    var POS_NOT_SET = 9999;
    appInfo = !appInfo ? {
      screen_x: POS_NOT_SET,
      screen_y: POS_NOT_SET
    } : appInfo;

    var args = utils.extend({
      id: appUID,
      name: app.name,
      bookmark: app.bookmark,
      type: app.type,
      smallIcon: this.getAppSmallIcon(app)
    }, handler.args);

    // pass screen pos
    args["screenX"] = appInfo.screen_x;
    args["screenY"] = appInfo.screen_y;

    // set widgetArgs
    args['widgetArgs'] = app.app_args || {};
    if (extraWidgetArgs) {
      utils.extend(args['widgetArgs'], extraWidgetArgs);
    }

    // restore the app arguments from appInfo, including width, height etc.
    if (appInfo.app_args) {
      try {
        utils.extend(args['widgetArgs'], JSON.parse(appInfo.app_args));
      } catch (e) {
        logger.error(e);
      }
    }

    // restore the app windowStates from app_status
    if (appInfo.app_status) {
      try {
        args["windowStates"] = utils.extend({}, JSON.parse(appInfo.app_status));
      } catch (e) {
        logger.error(e);
      }
    }

    var wnd = findWindow();

    // Calculate position of APP window.
    var winFeatures = ['chrome,all,dialog=no'];
    if (wnd.navigator.appVersion.indexOf('Mac') > -1 && appInfo.screen_x == POS_NOT_SET) {
      // Mac will not paint window out of screen
      winFeatures.push('left=' + Math.max(wnd.screen.availWidth - 300, 0));
      winFeatures.push('top=' + Math.max(wnd.screen.availHeight - 400, 0));
    } else {
      winFeatures.push('left=' + appInfo.screen_x);
      winFeatures.push('top=' + appInfo.screen_y);
    }

    var appWindow = wnd.openDialog(handler.appURL, appUID, winFeatures.join(','), args);
    appWindow.uid = appUID;

    if (false != updateOpenedTime) {
      this.updateAppStateWithArg({
        uid: appUID,
        last_opened: new Date().getTime()
      });
      AppCenter.appPref.updatePref('open', appUID);
    }

    AppCenterDAO.increateAppVisits(appUID);
    Tracker.track(Tracker.app.type, Tracker.app.actions.OPEN_APP, appUID);
  },

  removeUntrackedApps: function() {
    var windowMediator = Components.classes["@mozilla.org/appshell/window-mediator;1"].getService(Components.interfaces.nsIWindowMediator);

    var enumerator = windowMediator.getEnumerator("mozilla:favpart_widget");
    var ids = [];
    while(enumerator.hasMoreElements()) {
      var win = enumerator.getNext();
      ids.push(win.uid);
    }

    // Keep history list less than 100
    AppCenterDAO.keepHistoryList(100);

    var args = {};
    var sql = [];
    sql.push('DELETE FROM apps');
    sql.push('WHERE bookmark = 0 AND type != 0');
    if (ids.length > 0) {
      sql.push('AND uid NOT IN (');
      for (var i = 0; i < ids.length; i++) {
        if (i > 0) {
          sql.push(',');
        }
        sql.push(':id' + i);
        args['id' + i] = ids[i];
      }
      sql.push(')');
    }
    sql.push('AND id IN (SELECT app_id FROM app_info WHERE last_opened IS NULL)')

    // remove unbookmarked apps which are not opened and not built-in app.
    AppCenterDAO.executeSQL(sql.join(' '), args);
    // remove app info refered empty app.
    AppCenterDAO.executeSQL('DELETE FROM app_info WHERE last_opened IS NULL AND app_id NOT IN (SELECT id FROM apps)')
  },

  updateAppThumbnail: function(uid) {
    var appWindow = this.findAppWindow(uid);
    if (!appWindow){
      return;
    }

    if (!AppCenterDAO.appExists(uid)) {
      logger.trace(uid + ' does not exists.');
      return;
    }

    this.updateAppStateWithArg({
      uid: uid,
      thumbnail: appWindow.widget.getThumbnail()
    });
  },

  /**
   * Update info about app, including: screenX, screenY, thumbnail etc.
   */
  updateAppState: function(appUID) {
    var appWindow = this.findAppWindow(appUID);
    if (!appWindow){
      return;
    }

    if (!AppCenterDAO.appExists(appUID)) {
      logger.trace(appUID + 'does not exists.');
      return;
    }

    this.updateAppStateWithArg({
      uid:   appUID,
      screen_x:  appWindow.screenX,
      screen_y:  appWindow.screenY
    });
  },

  updateAppStateWithArg: function(appInfo) {
    AppCenterDAO.addOrUpdateAppState(appInfo);
  },

  /**
   * Update app args.
   */
  updateAppArgs: function(uid, args) {
    var app_args = AppCenterDAO.getAppArgsByUID(uid);
    try {
      app_args = JSON.parse(app_args);
    } catch (e) {
      app_args = {};
    }

    utils.extend(app_args, args);

    AppCenterDAO.updateAppArgs(uid, JSON.stringify(app_args));
  },

  updateApp: function(appInfo) {
    // update infomation of app, including: iconUrl, appArgs
    // App name could be modified, so appName can not be updated.

    var app = AppCenterDAO.getAppByUID(appInfo.appUID);
    if (!app) {
      logger.trace('App "' + appInfo.appUID + '" does not exist.');
      return;
    }

    // Chec if iconUrl is changed, if true, reset icon_data_url
    if (appInfo.iconUrl != app.icon_url) {
      app.icon_data_url = null;
      // Update icon url only if icon url is not chrome url.
      if (app.icon_url.indexOf("chrome://") == -1) {
        app.icon_url = appInfo.iconUrl;
      }
    }

    app.app_args = JSON.stringify(appInfo.appArgs);
    if (appInfo.smallIcon) {
      app.small_icon = appInfo.smallIcon;
    }

    logger.trace('Start update app info: "' + appInfo.appUID);
    AppCenterDAO.updateApp(app);
    if (appInfo.intent) {
      AppCenterDAO.updateAppIntents(appInfo);
    }
  },

  getAppIconUrl: function(app) {
    var icon_url = app.icon_data_url && app.icon_data_url.indexOf("data:image") == 0 ? app.icon_data_url : app.icon_url;
    if (!icon_url || icon_url == "") {
      icon_url = "chrome://mozapps/skin/places/defaultFavicon.png";   // use default fav icon
    }
    return icon_url;
  },

  getAppSmallIcon: function(app) {
    return !app.small_icon ? this.getAppIconUrl(app) : app.small_icon;
  }
};

// update the version of AppCenter.
Cu["import"]("resource://gre/modules/AddonManager.jsm");
AddonManager.getAddonByID("livemargins@mozillaonline.com", function(addon) {
  AppCenter.version = addon.version;
});

/********* object for app pref change and prefObserver ***********/
AppCenter.appPref = (function() {
  var _observers = [];
  var _maps = {
    'apps.unbookmark': 'onUnbookmark',
    'apps.hideunbookmarkundomsg': 'onHideUnbookmarkUndoMsg',
    'apps.changename': 'onChangeName',
    'apps.bookmark': 'onBookmark',
    'apps.open': 'onOpenApp',
    'apps.clearhistory': 'onClearHistory',
    'apps.local': 'onSetLocal',
    'apps.unlocal': 'onSetUnLocal',
    'apps.moveapp': 'onMoveApp',
    'apps.showbookmarks': 'onShowAppBookmarks'
  };

  var _prefListener = new utils.prefListener('extensions.livemargins@mozillaonline.com.', function(branch, name) {
    if (!_maps[name]) {
      return;
    }

    var value = branch.getCharPref(name);
    _observers.forEach(function(observer) {
      if (!_check_if_trigger_(observer) && typeof observer[_maps[name]] == 'function') {
        try {
          observer[_maps[name]](value.replace(/\d+-/i, ''));
        } catch (e) {
          //
        }
      }
    });
  });
  _prefListener.register();

  var _trigger_ = null;
  function _updatePref(prefName, appUID, trigger) {
    try {
      _trigger_ = trigger;
      utils.setPref("apps." + prefName, new Date().getTime() + '-' + appUID);
    } catch (e) {
      logger.error(e);
    }
  }

  function _check_if_trigger_(observer) {
    if (_trigger_ == observer) {
      _trigger_ = null;
      return true;
    }

    _trigger_ = null;
    return false;
  }

  return {
    addObserver: function(observer) {
      logger.trace('register observer');
      _observers.push(observer);
    },

    removeObserver: function(observer) {
      logger.trace('unregister observer');
      var obs = [];
      _observers.forEach(function(ob) {
        if (observer == ob) {
          return;
        }

        obs.push(ob);
      });
      _observers = obs;
    },

    updatePref: function(prefName, appUID, trigger) {
      _updatePref(prefName, appUID, trigger);
    }
  };
})();
/******* end ********/

/****************************************************************
 * Object for built-in app related operations.
 ****************************************************************/
AppCenter.dnd = {
  galleryAppUID: 'app_gallery',
  searchAppUID: 'app_search',
  musicAppUID: 'app_music',

  doGrabPic: function(picId) {
    var appWindow = AppCenter.findAppWindow(this.galleryAppUID);
    if (appWindow){
      appWindow.widget.doGrabPic(picId);
      appWindow.focus();
      return true;
    }

    AppCenter.openApp(this.galleryAppUID, true, {
      picId: picId
    });
  },

  doSearch: function(data) {
    var appWindow = AppCenter.findAppWindow(this.searchAppUID);
    if (appWindow){
      appWindow.widget.doSearch(data);
      appWindow.focus();
      return true;
    }

    AppCenter.openApp(this.searchAppUID, true, {
      data: data
    });
  },

  playMusic: function(songId) {
    var appWindow = AppCenter.findAppWindow(this.musicAppUID);
    if (appWindow){
      appWindow.widget.playMusic(songId);
      appWindow.focus();
      return true;
    }

    AppCenter.openApp(this.musicAppUID, true, {
      songId: songId
    });
  }
};
/******* end *******/
