<commit_before>/*! This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

"use strict";

const {getMostRecentBrowserWindow, isBrowser} = require("sdk/window/utils");
const {isPrivate} = require("sdk/private-browsing");
const {WindowTracker} = require("sdk/deprecated/window-utils");
const {Route, coefficient} = require("./route");
const {Recommendation} = require("./recommendation");
const {URL} = require("sdk/url");
const tabs = require("sdk/tabs");
const {Event, getEventData, eventDataAddress} = require("./event");
const {Cu, Cc, Ci} = require("chrome");
const sp = require("sdk/simple-prefs");
const prefs = sp.prefs;
const presenter = require("./presenter");
const { MatchPattern } = require("sdk/util/match-pattern");
const {PersistentRecSet} = require("./recommendation");
const timer = require("./timer");
const utils = require("./utils");
const self = require("./self");
const system = require("sdk/system");
const windows = require("sdk/windows");
const {viewFor} = require("sdk/view/core");
const {handleCmd} = require("./debug");
const {data} = require("sdk/self");
const unload = require("sdk/system/unload").when;
const logger = require("./logger");
const featReport = require("./feature-report");
const events = require("sdk/system/events");
const {pathFor} = require('sdk/system');  
const file = require('sdk/io/file');
const statsEvent = require("./stats").event;
const functional = require("sdk/lang/functional");
const {defer, all} = require("sdk/core/promise")
const {PersistentObject, osFileObjects} = require("./storage");
Cu.import("resource://gre/modules/Downloads.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/AddonManager.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
const devtools = Cu.import("resource://devtools/shared/Loader.jsm").devtools;

const observerService = Cc["@mozilla.org/observer-service;1"]
                      .getService(Ci.nsIObserverService);

const NEWTAB_URL = 'about:newtab';
const HOME_URL = 'about:home';
const BLANK_URL = 'about:blank';

const recSetAddress = "controller.recommData";
const deliveryDataAddress = "delivery.data";

let recommendations; //inited in init()
let deliveryData; //inited in init()

let hs;
let sessObserver;
let contentTypeObserver;
let searchEngineObserver;
let addonListener;
let historyObserver;
let dlView;

const init = function(){
  console.log("initializing controller");

  return all(
    [ 
      initRecs(),
      PersistentObject("osFile", {address: deliveryDataAddress})
    ])
    .then((result)=> {
      deliveryData = result[1];
    }).then(_init);
}

const _init = function(){
  console.time("controller init");

  unload(unloadController);

  listener.start();
  deliverer.init();
  debug.init();

  welcome();

  console.timeEnd("controller init");
}

function initRecs(){

  let {promise, resolve} = defer();

  if (!recommendations){
    PersistentRecSet("osFile", {address: recSetAddress}).then((recSet)=> {
      recommendations = recSet;
    }).then(resolve);
  }
  else
    resolve();

  return promise;
}

/**
 * Listens for events exhibited by the user.
 *
 */
const listener = {
  start: function(){

    console.log("starting listener");

    let that = this;

    this.listenForTabs(function(reason, params){
      let tabsEvent = Event("tabsEvent", {
        route: ["tabs", reason].join(" "),
        reason: reason,
        params: params
      });

      tabsEvent.effect = function(){
        let route = this.options.route;

        //pin is not about tabs being pinned
        //it is about reporting the number of pinned tabs
        //so should not be dispatched as tabs even
        if (this.options.reason == "pin" || this.options.reason == "open") return;
        
        // listener.dispatchRoute(route);
      };

      let multipleTabsEvent = that.multipleRoute(tabsEvent);

      //see above
      multipleTabsEvent.checkPreconditions = function(){
        return (this.preEvent.options.reason != "pin" && this.preEvent.options.reason != "open");
      };

      tabsEvent.postEvents.push(multipleTabsEvent);

      let tabsOpen = Event("tabsOpen");

      tabsOpen.effect = function(){
        let baseRoute = this.preEvent.options.route;

        let route = [baseRoute, "-n", this.preEvent.options.params.number].join(" ");

        listener.dispatchRoute(route);
      };

      tabsOpen.checkPreconditions = function(){
        return (this.preEvent.options.reason == "open");
      };

      tabsEvent.postEvents.push(tabsOpen);

      let tabsPin= Event("tabsPin");

      tabsPin.effect = function(){
        let baseRoute = this.preEvent.options.route;

        let route = [baseRoute, "-n", this.preEvent.options.params.number].join(" ");

        listener.dispatchRoute(route);
      };

      tabsPin.checkPreconditions = function(){
        return (this.preEvent.options.reason == "pin");
      };

      tabsEvent.postEvents.push(tabsPin)

      let tabsSwitchedPosition = Event("tabsSwitchedPosition");

      tabsSwitchedPosition.effect = function(){
        let baseRoute = this.preEvent.options.route;

        let route = [baseRoute, "position", this.preEvent.options.params.position].join(" ");

        this.options.route = route;

        // listener.dispatchRoute(route);
      };

      tabsEvent.postEvents.push(tabsSwitchedPosition);

      tabsSwitchedPosition.checkPreconditions = function(){
        return (this.preEvent.options.reason == "switched"
             && this.preEvent.options.params.position
             && this.preEvent.options.params.number > 5); //temporary workaround
      };

      let multipleTabsSwitchedPosition = that.multipleRoute(tabsSwitchedPosition);

      tabsSwitchedPosition.postEvents.push(multipleTabsSwitchedPosition);

      tabsEvent.wake();

    });

    this.listenForAddonEvents(function(eventType, params){
      let addonEvent = Event("addonEvent", {
        eventType: eventType,
        params: params
      });

      addonEvent.effect = function(){
        let route;
        switch(this.options.eventType){
          case "count":
            route = ["addon", this.options.eventType, this.options.params.type, "-n", this.options.params.number].join(" ");
            break;
          case "pageshow":
            route = ["addon", this.options.eventType].join(" ");
            break;
          default:
            route = ["addon", this.options.eventType, params.addonId].join(" ");
        }
        this.options.route = route;
        listener.dispatchRoute(route);
      };

      addonEvent.wake();
    });

    this.listenForUserActivity(function(){

      let interruptibleMomentEvent = Event("interruptibleMomentEvent",
      {
        route: "moment interruptible"
      });

      interruptibleMomentEvent.checkPreconditions = function(){
        return timer.isRecentlyActive(10, 20*60);
      };

      let multipleInterruptibleMomentEvent = that.multipleRoute(interruptibleMomentEvent, {
        dispatch: {context: true}
      });

      interruptibleMomentEvent.postEvents.push(multipleInterruptibleMomentEvent);

      interruptibleMomentEvent.wake();
    });

    this.listenForNewtab(function(){
      let newtabEvent = Event("newtabEvent", {
        route: 'newtab'
       });

      newtabEvent.effect = function(){
        listener.dispatchRoute(this.options.route);
      };

      newtabEvent.wake();
    });

    this.listenForFirefoxEvents(function(reason, params){

      let firefoxEvent = Event("firefoxEvent", {
        route: ['firefox', reason].join(" "),
        reason: reason,
        params: params
      });

      let updateChannelEvent = Event("updateChannel");

      updateChannelEvent.effect = function(){
        let baseRoute = this.preEvent.options.route;

        let route = [baseRoute, params.channel].join(" ");

        listener.dispatchRoute(route);
      }

      updateChannelEvent.checkPreconditions = function() this.preEvent.options.reason == "channel";

      firefoxEvent.postEvents.push(updateChannelEvent);

      let profilesEvent = Event("profilesEvent");

      profilesEvent.effect = function(){
        let baseRoute = this.preEvent.options.route;

        let route = [baseRoute,
                     "-n",
                     this.preEvent.options.params.number]
                     .join(" ");

        listener.dispatchRoute(route);
      }
      
      profilesEvent.checkPreconditions = function() this.preEvent.options.reason == "profiles";

      firefoxEvent.postEvents.push(profilesEvent);

      let startupEvent = Event("startupEvent");

      startupEvent.effect = function(){

        let baseRoute = this.preEvent.options.route;

        let route = baseRoute;

        this.options.route = route;
      }

      startupEvent.checkPreconditions = function() this.preEvent.options.reason == "startup";
      firefoxEvent.postEvents.push(startupEvent);

      let multipleStartupEvent = that.multipleRoute(startupEvent);

      startupEvent.postEvents.push(multipleStartupEvent);

      firefoxEvent.wake();

    });

    //TODO: merge with chromeEvent
    this.listenForKeyboardEvent(function(evt){

      let hotkeyEvent = Event("hotkeyEvent", {
        event: evt
      });

      hotkeyEvent.effect = function(){
        let e = this.options.event;

        // os-dependent representation, not dispatched currently
        let route = ["hotkey", e.key.toLowerCase(),
                   e.metaKey ? "-meta" : "",
                   e.ctrlKey ? "-ctrl" : "",
                   e.shiftKey ? "-shift" : "",
                   e.altKey ? "-alt" : "" ].filter(function(elm){
                    return elm != "";
                   }).join(" ");

        let osDarwin = system.platform === 'darwin'; //TODO

        // os-independent representation
        route = ["hotkey", e.key.toLowerCase(),
                ((e.metaKey && osDarwin) || (e.ctrlKey && !osDarwin)) ? "-cmd" : "",
                e.shiftKey ? "-shift" : "",
                e.altKey ? "-alt" : ""].filter(function(elm){
                    return elm != "";
                   }).join(" ");

        this.options.route = route; //TOTHINK: the first route is ignored for postEvents
      }

      hotkeyEvent.checkPreconditions = function(){
        let e = this.options.event;
        return (e.shiftKey || e.metaKey || e.ctrlKey || e.altKey || /^[Ff]\d+$/.test(e.key));
      }

      let multipleHotkeyEvent = that.multipleRoute(hotkeyEvent);

      hotkeyEvent.postEvents.push(multipleHotkeyEvent);

      hotkeyEvent.wake();

    });

    this.listenForChromeEvents(function(route, evt){

      let evtName = route.eventName;
      let id = route.id;

      let chromeEvent = Event("chromeEvent", {
        route: ["chrome", evtName, id].join(" "),
        event: evt,
        eventName: evtName,
        id: route.id
      });

      chromeEvent.effect = function(){
        let route = this.options.route;

        // listener.dispatchRoute(route);
      }

      let multipleChromeEvent = that.multipleRoute(chromeEvent);
      chromeEvent.postEvents.push(multipleChromeEvent);

      let chromeEventAnon = Event("chromeEventAnon");

      chromeEventAnon.effect = function(){
        let anonid = this.preEvent.options.event.originalTarget.getAttribute("anonid");
        let baseRoute = this.preEvent.options.route;
        this.options.route = [baseRoute, "anonid", anonid].join(" ");

        let route = this.options.route;

        // listener.dispatchRoute(route);
      }

      chromeEventAnon.checkPreconditions = function(){
        if (~["document", "window"].indexOf(this.preEvent.options.id))
          return false;

        return (!!this.preEvent.options.event.originalTarget.getAttribute("anonid"));
      }

      let multipleChromeEventAnon = that.multipleRoute(chromeEventAnon);

      chromeEventAnon.postEvents.push(multipleChromeEventAnon);

      chromeEvent.postEvents.push(chromeEventAnon);

      chromeEvent.wake();

    });

    this.listenForInternalEvents(function(reason, params){

      let frEvent = Event("frEvent", {
        route: "fr",
        reason: reason,
        params: params
      });
      
      let tickEvent = Event("tick");

      tickEvent.checkPreconditions = function(){
        return this.preEvent.options.reason === "tick";
      }

      tickEvent.effect = function(){
        let baseRoute = this.preEvent.options.route;
        let params = this.preEvent.options.params;
        let route = [baseRoute, "tick", "-et", params.et, "-ett", params.ett].join(" ");
        this.options.route = route;

        listener.dispatchRoute(route);
      }

      frEvent.postEvents.push(tickEvent);

      frEvent.wake(); 
    });

    this.listenForPageVisit(function(type, param){

      let pageVisit = Event("pageVisit", {
        route: ["visit", type, param].join(" "),
        type: type,
        param: param
      });

      let multiplePageVisit = that.multipleRoute(pageVisit);

      pageVisit.postEvents.push(multiplePageVisit);

      pageVisit.wake();

    }, {halfFresh: true});

    this.listenForSearchEngine(function(reason, params){

      let searchEngModify = Event("searchEngModify",
      {
        route: ["search-engine", reason].join(" "),
        reason: reason,
        params: params
      });

      searchEngModify.effect = function(){
        let route = this.options.route;
        let reason = this.options.reason;

        // if (reason != "alias")
        //   listener.dispatchRoute(route);
      };


      let multipleSearchEngModify = that.multipleRoute(searchEngModify);

      multipleSearchEngModify.checkPreconditions = function(){
        return (this.preEvent.options.reason != "alias");
      }

      searchEngModify.postEvents.push(multipleSearchEngModify);

      let searchEngAliasEvent = Event("searchEngModifyAliasEvent");

      searchEngAliasEvent.effect = function(){
        let baseRoute = this.preEvent.options.route;
        let params = this.preEvent.options.params;

        let route = [baseRoute, "-n", params.number].join(" ");

        listener.dispatchRoute(route);
      };

      searchEngAliasEvent.checkPreconditions = function(){
        return (this.preEvent.options.reason == "alias");
      }

      searchEngModify.postEvents.push(searchEngAliasEvent);

      searchEngModify.wake();

    });

    this.listenForDevTools(function(reason, params){
      let devToolsEvent = Event("devToolsEvent", {
        route: ["dev", reason].join(" "),
        reason: reason,
        params: params
      });

      devToolsEvent.effect = function(){
        let route = this.options.route;

        // listener.dispatchRoute(route);
      };

      let multipleDevToolsEvent = that.multipleRoute(devToolsEvent);

      devToolsEvent.postEvents.push(multipleDevToolsEvent);

      let devToolsSelect = Event("devToolsSelect");

      devToolsSelect.effect = function(){
        let baseRoute = this.preEvent.options.route;
        let toolId = this.preEvent.options.params.toolId;

        let route = [baseRoute, "tool", toolId].join(" ");

        this.options.route = route;
        this.options.toolId = toolId;

        // listener.dispatchRoute(route);
      }

      devToolsSelect.checkPreconditions = function(){
        return (this.preEvent.options.reason == "select");
      };

      devToolsEvent.postEvents.push(devToolsSelect);

      let multipleDevToolsSelect = that.multipleRoute(devToolsSelect);
      devToolsSelect.postEvents.push(multipleDevToolsSelect);

      devToolsEvent.wake();

    });

    this.listenForSessionStore(function(reason){
      let sessRestored = Event("sessionRestored", {
        reason: reason,
        route: ["session", reason].join(" ")
      });

      sessRestored.effect = function(){
        let route = this.options.route;
        // listener.dispatchRoute(route);
      }

      sessRestored.checkPreconditions = function(){
        return (this.options.reason == "restored");
      };

      let multipleSessRestored = that.multipleRoute(sessRestored);
      sessRestored.postEvents.push(multipleSessRestored);

      sessRestored.wake();
    });

    this.listenForPrivateBrowsing(function(reason){
      let privateBrowse = Event("privateBrowse", {
        reason: reason,
        route: ["private browse", reason].join(" ")
      });

      privateBrowse.effect = function(){
        let route = this.options.route;
        // listener.dispatchRoute(route);
      }

      let multiplePrivateBrowse = that.multipleRoute(privateBrowse);

      privateBrowse.postEvents.push(multiplePrivateBrowse);
      privateBrowse.wake();
    });

  
    this.listenForTools(function(tool, reason){

      let toolEvent = Event("toolEvent", {
        tool: tool,
        reason: reason,
        route: [tool, reason].join(" ")
      });

      let multipleToolEvent = that.multipleRoute(toolEvent);

      toolEvent.postEvents.push(multipleToolEvent);

      toolEvent.wake();

    });

    this.listenForHistory(function(reason){
      let histInteraction = Event(("historyInteraction"), {
        reason: reason,
        route: ["history", reason].join(" ")
      });

      histInteraction.effect = function(){
        let route = this.options.route;

        // listener.dispatchRoute(route);
      };

      let multipleHistInteraction = that.multipleRoute(histInteraction);
      histInteraction.postEvents.push(multipleHistInteraction);

      //delete or clear
      //TODO: implement as separate and merge using OR operation
      
      let histDelete = Event("historyDelete");

      histDelete.effect = function(){

        this.options.route = ['history', "delete"].join(" ");

        let route = this.options.route;

        // listener.dispatchRoute(route);
      }

      histDelete.checkPreconditions = function(){
        return (["cleared", "deletedURI", "deletedvisits"].indexOf(this.preEvent.options.reason) != -1);
      }

      histInteraction.postEvents.push(histDelete);

      let multipleHistDelete = that.multipleRoute(histDelete);
      histDelete.postEvents.push(multipleHistDelete);

      histInteraction.wake();
    });

    this.listenForDownloads(function(reason){

      let downloadInteraction = Event("downloadInteraction", {
        reason: reason,
        route: ["download", reason].join(" ")
      });

      downloadInteraction.effect = function(){
        let route = this.options.route;

        // listener.dispatchRoute(route);
      };

      let multipleDownloadInteraction = that.multipleRoute(downloadInteraction);
      downloadInteraction.postEvents.push(multipleDownloadInteraction);

      downloadInteraction.wake();

    });
  },
  behavior: undefined, //defined below
  context: undefined, //defined below
  listenForActiveTabHostnameProgress: undefined, //defined below
  listenForDownloads: undefined, //defined blow
  multipleRoute: undefined //defined below
};


const deliverer = {
  init: function(){

    osFileObjects["timer.data"].silence_length_s = 
      timer.tToS(prefs["delivery.mode.silence_length." + osFileObjects["delivery.data"].mode.rate_limit]);

    timer.onTick(this.checkSchedule);

  },
  deliver: function (/* recommendations */) {

    let recomms = Array.prototype.slice.call(arguments);

    if (recomms.length === 0)
      return;

    if (recomms.length > 1){
      console.log("warning: attempted to deliver multiple recommendations at the same time: " + recomms.length);
      statsEvent("multiple-delivery", {type: "delivery"});
    }

    //finding the recommendation with the highest priority

    let minPriority = recomms[0].priority;
    let minRec = recomms[0];

    recomms.forEach(function(aRecommendation){
      if (aRecommendation.priority < minPriority){
        minPriority = aRecommendation.priority;
        minRec = aRecommendation;
      }
    });

    let aRecommendation = minRec;

    let rejectDelivery = false;

    if (prefs["passive_mode"]){
      console.log("delivery rejected due to passive mode");
      statsEvent("passive-mode-reject", {type: "delivery"});
      rejectDelivery = true;
    }

    if (isPrivate(getMostRecentBrowserWindow())){
      console.log("delivery rejected due to private browsing");
      statsEvent("private-reject", {type: "delivery"});
      rejectDelivery = true;
    }

    if (!timer.isCertainlyActive()){
      console.log("delivery rejected due to uncertain user activity status");
      statsEvent("inactive-reject", {type: "delivery"});
      rejectDelivery = true;
    }

    if (self.delMode.observ_only || (timer.isSilent())){
      if (self.delMode.observ_only){
        console.log("delivery rejected due to observe only period: id -> " + aRecommendation.id);
        statsEvent("observe-only-reject", {type: "delivery"});
      }
      else{
        console.log("delivery rejected due to silence: id -> " + aRecommendation.id);
        statsEvent("silence-reject", {type: "delivery"});
      }

      rejectDelivery = true;
    }

    if (rejectDelivery)
      return;

    console.log("delivering " + aRecommendation.id);

    if (aRecommendation.status == "delivered"){
      console.log("warning: delivering a recommendation that has already been delivered");
      logger.logWarning({type: "delivery", message: "delivering a recommendation that has already been delivered: " + aRecommendation.id});
    }

    aRecommendation.status = "delivered";

    recommendations.update(aRecommendation);

    statsEvent("delivery");

    presenter.present(aRecommendation);

    timer.silence();

  },
  checkSchedule: function(et, ett){

    let deliveryTime = deliveryData.randomDeliveryTime;

    if (!deliveryTime){
      deliverer.scheduleRandomDelivery();
      deliveryTime = deliveryData.randomDeliveryTime
    }

    if (deliveryTime < et){
      console.log("rescheduling random delivery");
      deliverer.scheduleRandomDelivery();
      deliveryTime = deliveryData.randomDeliveryTime
    }

    if (deliveryTime == et){
      let randomMomentEvent = new Event("randomMoment", {
        route: "moment random"
      });

      let multipleRandomMomentEvent = listener.multipleRoute(randomMomentEvent, {
        dispatch: {context: true}
      });

      randomMomentEvent.postEvents.push(multipleRandomMomentEvent);
      randomMomentEvent.wake();
    }

  },
  scheduleRandomDelivery: function(){
    let et = timer.elapsedTime();
  
    let deliveryTime = timer.randomTime(et, et + prefs["timer.random_interval_length_tick"]);

    deliveryData.randomDeliveryTime = deliveryTime;

    console.log("random delivery scheduled: " +  deliveryTime + " ticks");  }
};


listener.behavior = function(route){
  console.log("behavior -> route: ", route);

  route = Route(route); // converting to route object

  let behaviorInfo =  function(aRecommendation){ 
    return {id: aRecommendation.id, count: Number(route.c), num: Number(route.n), "if": Number(route.if)};
  }

  //logging loose matches
  let recomms = recommendations.getByRouteIndex('trigBehavior', route, {looseMatch: true});
  if (recomms.length === 0)
    return;

  recomms.forEach(function(aRecommendation){
    let addedInfo = {};
    let aggregates = {};
    let bi = behaviorInfo(aRecommendation);

    if (bi.num || bi.num == 0) {
      addedInfo.num = bi.num;
      aggregates.num = 'average';
    }

    statsEvent(aRecommendation.id, {type: "looseBehavior"}, addedInfo, aggregates);

    if (utils.isPowerOf2(bi.count) || (utils.isPowerOf2(bi.num) && bi.num > 10)){
      logger.logLooseBehavior(bi);
    }
  });


  recomms = recommendations.getByRouteIndex('trigBehavior', route, {status: 'active'});
  if (recomms.length === 0)
    return;

  recomms.forEach(function(aRecommendation){
    aRecommendation.status = 'outstanding';
    recommendations.update(aRecommendation);

    let addedInfo = {};
    let bi = behaviorInfo(aRecommendation);
    if (bi.num) {
      addedInfo.num = bi.num;
    }

    statsEvent(aRecommendation.id, {type: "behavior"}, addedInfo);
    logger.logBehavior(bi);
  });
};

listener.context = function(route){

  console.log("context -> route: ", route);

  route = Route(route); // convert to object
  let header = route.header;

  let mt = self.delMode.moment;

  // handle random moments
  if (mt === 'random'){
    if (header != 'moment random')
      return;
    else {
      route = '*';
      statsEvent(header);
    }
  }

  // handle interruptible moments
  if (mt === 'interruptible'){
    if (header != 'moment interruptible')
      return;
    else {
      route = '*';
      statsEvent(header);
    }
  }
 
  let recomms = recommendations.getByRouteIndex('delivContext', route, {status: 'outstanding'});

  if (recomms.length === 0)
    return;

  //deliver recommendations
  deliverer.deliver.apply(deliverer, recomms);
};

listener.featureUse = function(route){

  console.log("featureUse -> route: ", route);

  route = Route(route);

  let featureUseInfo =  function(aRecommendation){ 
    return {id: aRecommendation.id, count: Number(route.c), num: Number(route.n), "if": Number(route.if)};
  }

  //logging loose matches
  let recomms = recommendations.getByRouteIndex('featUseBehavior', route, {looseMatch: true});
  if (recomms.length === 0)
    return;

  recomms.forEach(function(aRecommendation){<commit_msg>added a workaround for tabgroups and treestyletab not being compatible with tab center<commit_after>/*! This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

"use strict";

const {getMostRecentBrowserWindow, isBrowser} = require("sdk/window/utils");
const {isPrivate} = require("sdk/private-browsing");
const {WindowTracker} = require("sdk/deprecated/window-utils");
const {Route, coefficient} = require("./route");
const {Recommendation} = require("./recommendation");
const {URL} = require("sdk/url");
const tabs = require("sdk/tabs");
const {Event, getEventData, eventDataAddress} = require("./event");
const {Cu, Cc, Ci} = require("chrome");
const sp = require("sdk/simple-prefs");
const prefs = sp.prefs;
const presenter = require("./presenter");
const { MatchPattern } = require("sdk/util/match-pattern");
const {PersistentRecSet} = require("./recommendation");
const timer = require("./timer");
const utils = require("./utils");
const self = require("./self");
const system = require("sdk/system");
const windows = require("sdk/windows");
const {viewFor} = require("sdk/view/core");
const {handleCmd} = require("./debug");
const {data} = require("sdk/self");
const unload = require("sdk/system/unload").when;
const logger = require("./logger");
const featReport = require("./feature-report");
const events = require("sdk/system/events");
const {pathFor} = require('sdk/system');  
const file = require('sdk/io/file');
const statsEvent = require("./stats").event;
const functional = require("sdk/lang/functional");
const {defer, all} = require("sdk/core/promise")
const {PersistentObject, osFileObjects} = require("./storage");
Cu.import("resource://gre/modules/Downloads.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/AddonManager.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
const devtools = Cu.import("resource://devtools/shared/Loader.jsm").devtools;

const observerService = Cc["@mozilla.org/observer-service;1"]
                      .getService(Ci.nsIObserverService);

const NEWTAB_URL = 'about:newtab';
const HOME_URL = 'about:home';
const BLANK_URL = 'about:blank';

const recSetAddress = "controller.recommData";
const deliveryDataAddress = "delivery.data";

let recommendations; //inited in init()
let deliveryData; //inited in init()

let hs;
let sessObserver;
let contentTypeObserver;
let searchEngineObserver;
let addonListener;
let historyObserver;
let dlView;

const init = function(){
  console.log("initializing controller");

  return all(
    [ 
      initRecs(),
      PersistentObject("osFile", {address: deliveryDataAddress})
    ])
    .then((result)=> {
      deliveryData = result[1];
    }).then(_init);
}

const _init = function(){
  console.time("controller init");

  unload(unloadController);

  listener.start();
  deliverer.init();
  debug.init();

  welcome();

  console.timeEnd("controller init");
}

function initRecs(){

  let {promise, resolve} = defer();

  if (!recommendations){
    PersistentRecSet("osFile", {address: recSetAddress}).then((recSet)=> {
      recommendations = recSet;
    }).then(resolve);
  }
  else
    resolve();

  return promise;
}

/**
 * Listens for events exhibited by the user.
 *
 */
const listener = {
  start: function(){

    console.log("starting listener");

    let that = this;

    this.listenForTabs(function(reason, params){
      let tabsEvent = Event("tabsEvent", {
        route: ["tabs", reason].join(" "),
        reason: reason,
        params: params
      });

      tabsEvent.effect = function(){
        let route = this.options.route;

        //pin is not about tabs being pinned
        //it is about reporting the number of pinned tabs
        //so should not be dispatched as tabs even
        if (this.options.reason == "pin" || this.options.reason == "open") return;
        
        // listener.dispatchRoute(route);
      };

      let multipleTabsEvent = that.multipleRoute(tabsEvent);

      //see above
      multipleTabsEvent.checkPreconditions = function(){
        return (this.preEvent.options.reason != "pin" && this.preEvent.options.reason != "open");
      };

      tabsEvent.postEvents.push(multipleTabsEvent);

      let tabsOpen = Event("tabsOpen");

      tabsOpen.effect = function(){
        let baseRoute = this.preEvent.options.route;

        let route = [baseRoute, "-n", this.preEvent.options.params.number].join(" ");

        listener.dispatchRoute(route);
      };

      tabsOpen.checkPreconditions = function(){
        return (this.preEvent.options.reason == "open");
      };

      tabsEvent.postEvents.push(tabsOpen);

      let tabsPin= Event("tabsPin");

      tabsPin.effect = function(){
        let baseRoute = this.preEvent.options.route;

        let route = [baseRoute, "-n", this.preEvent.options.params.number].join(" ");

        listener.dispatchRoute(route);
      };

      tabsPin.checkPreconditions = function(){
        return (this.preEvent.options.reason == "pin");
      };

      tabsEvent.postEvents.push(tabsPin)

      let tabsSwitchedPosition = Event("tabsSwitchedPosition");

      tabsSwitchedPosition.effect = function(){
        let baseRoute = this.preEvent.options.route;

        let route = [baseRoute, "position", this.preEvent.options.params.position].join(" ");

        this.options.route = route;

        // listener.dispatchRoute(route);
      };

      tabsEvent.postEvents.push(tabsSwitchedPosition);

      tabsSwitchedPosition.checkPreconditions = function(){
        return (this.preEvent.options.reason == "switched"
             && this.preEvent.options.params.position
             && this.preEvent.options.params.number > 5); //temporary workaround
      };

      let multipleTabsSwitchedPosition = that.multipleRoute(tabsSwitchedPosition);

      tabsSwitchedPosition.postEvents.push(multipleTabsSwitchedPosition);

      tabsEvent.wake();

    });

    this.listenForAddonEvents(function(eventType, params){
      let addonEvent = Event("addonEvent", {
        eventType: eventType,
        params: params
      });

      addonEvent.effect = function(){
        let route;
        switch(this.options.eventType){
          case "count":
            route = ["addon", this.options.eventType, this.options.params.type, "-n", this.options.params.number].join(" ");
            break;
          case "pageshow":
            route = ["addon", this.options.eventType].join(" ");
            break;
          default:
            route = ["addon", this.options.eventType, params.addonId].join(" ");
        }
        this.options.route = route;
        listener.dispatchRoute(route);
      };

      addonEvent.wake();
    });

    this.listenForUserActivity(function(){

      let interruptibleMomentEvent = Event("interruptibleMomentEvent",
      {
        route: "moment interruptible"
      });

      interruptibleMomentEvent.checkPreconditions = function(){
        return timer.isRecentlyActive(10, 20*60);
      };

      let multipleInterruptibleMomentEvent = that.multipleRoute(interruptibleMomentEvent, {
        dispatch: {context: true}
      });

      interruptibleMomentEvent.postEvents.push(multipleInterruptibleMomentEvent);

      interruptibleMomentEvent.wake();
    });

    this.listenForNewtab(function(){
      let newtabEvent = Event("newtabEvent", {
        route: 'newtab'
       });

      newtabEvent.effect = function(){
        listener.dispatchRoute(this.options.route);
      };

      newtabEvent.wake();
    });

    this.listenForFirefoxEvents(function(reason, params){

      let firefoxEvent = Event("firefoxEvent", {
        route: ['firefox', reason].join(" "),
        reason: reason,
        params: params
      });

      let updateChannelEvent = Event("updateChannel");

      updateChannelEvent.effect = function(){
        let baseRoute = this.preEvent.options.route;

        let route = [baseRoute, params.channel].join(" ");

        listener.dispatchRoute(route);
      }

      updateChannelEvent.checkPreconditions = function() this.preEvent.options.reason == "channel";

      firefoxEvent.postEvents.push(updateChannelEvent);

      let profilesEvent = Event("profilesEvent");

      profilesEvent.effect = function(){
        let baseRoute = this.preEvent.options.route;

        let route = [baseRoute,
                     "-n",
                     this.preEvent.options.params.number]
                     .join(" ");

        listener.dispatchRoute(route);
      }
      
      profilesEvent.checkPreconditions = function() this.preEvent.options.reason == "profiles";

      firefoxEvent.postEvents.push(profilesEvent);

      let startupEvent = Event("startupEvent");

      startupEvent.effect = function(){

        let baseRoute = this.preEvent.options.route;

        let route = baseRoute;

        this.options.route = route;
      }

      startupEvent.checkPreconditions = function() this.preEvent.options.reason == "startup";
      firefoxEvent.postEvents.push(startupEvent);

      let multipleStartupEvent = that.multipleRoute(startupEvent);

      startupEvent.postEvents.push(multipleStartupEvent);

      firefoxEvent.wake();

    });

    //TODO: merge with chromeEvent
    this.listenForKeyboardEvent(function(evt){

      let hotkeyEvent = Event("hotkeyEvent", {
        event: evt
      });

      hotkeyEvent.effect = function(){
        let e = this.options.event;

        // os-dependent representation, not dispatched currently
        let route = ["hotkey", e.key.toLowerCase(),
                   e.metaKey ? "-meta" : "",
                   e.ctrlKey ? "-ctrl" : "",
                   e.shiftKey ? "-shift" : "",
                   e.altKey ? "-alt" : "" ].filter(function(elm){
                    return elm != "";
                   }).join(" ");

        let osDarwin = system.platform === 'darwin'; //TODO

        // os-independent representation
        route = ["hotkey", e.key.toLowerCase(),
                ((e.metaKey && osDarwin) || (e.ctrlKey && !osDarwin)) ? "-cmd" : "",
                e.shiftKey ? "-shift" : "",
                e.altKey ? "-alt" : ""].filter(function(elm){
                    return elm != "";
                   }).join(" ");

        this.options.route = route; //TOTHINK: the first route is ignored for postEvents
      }

      hotkeyEvent.checkPreconditions = function(){
        let e = this.options.event;
        return (e.shiftKey || e.metaKey || e.ctrlKey || e.altKey || /^[Ff]\d+$/.test(e.key));
      }

      let multipleHotkeyEvent = that.multipleRoute(hotkeyEvent);

      hotkeyEvent.postEvents.push(multipleHotkeyEvent);

      hotkeyEvent.wake();

    });

    this.listenForChromeEvents(function(route, evt){

      let evtName = route.eventName;
      let id = route.id;

      let chromeEvent = Event("chromeEvent", {
        route: ["chrome", evtName, id].join(" "),
        event: evt,
        eventName: evtName,
        id: route.id
      });

      chromeEvent.effect = function(){
        let route = this.options.route;

        // listener.dispatchRoute(route);
      }

      let multipleChromeEvent = that.multipleRoute(chromeEvent);
      chromeEvent.postEvents.push(multipleChromeEvent);

      let chromeEventAnon = Event("chromeEventAnon");

      chromeEventAnon.effect = function(){
        let anonid = this.preEvent.options.event.originalTarget.getAttribute("anonid");
        let baseRoute = this.preEvent.options.route;
        this.options.route = [baseRoute, "anonid", anonid].join(" ");

        let route = this.options.route;

        // listener.dispatchRoute(route);
      }

      chromeEventAnon.checkPreconditions = function(){
        if (~["document", "window"].indexOf(this.preEvent.options.id))
          return false;

        return (!!this.preEvent.options.event.originalTarget.getAttribute("anonid"));
      }

      let multipleChromeEventAnon = that.multipleRoute(chromeEventAnon);

      chromeEventAnon.postEvents.push(multipleChromeEventAnon);

      chromeEvent.postEvents.push(chromeEventAnon);

      chromeEvent.wake();

    });

    this.listenForInternalEvents(function(reason, params){

      let frEvent = Event("frEvent", {
        route: "fr",
        reason: reason,
        params: params
      });
      
      let tickEvent = Event("tick");

      tickEvent.checkPreconditions = function(){
        return this.preEvent.options.reason === "tick";
      }

      tickEvent.effect = function(){
        let baseRoute = this.preEvent.options.route;
        let params = this.preEvent.options.params;
        let route = [baseRoute, "tick", "-et", params.et, "-ett", params.ett].join(" ");
        this.options.route = route;

        listener.dispatchRoute(route);
      }

      frEvent.postEvents.push(tickEvent);

      frEvent.wake(); 
    });

    this.listenForPageVisit(function(type, param){

      let pageVisit = Event("pageVisit", {
        route: ["visit", type, param].join(" "),
        type: type,
        param: param
      });

      let multiplePageVisit = that.multipleRoute(pageVisit);

      pageVisit.postEvents.push(multiplePageVisit);

      pageVisit.wake();

    }, {halfFresh: true});

    this.listenForSearchEngine(function(reason, params){

      let searchEngModify = Event("searchEngModify",
      {
        route: ["search-engine", reason].join(" "),
        reason: reason,
        params: params
      });

      searchEngModify.effect = function(){
        let route = this.options.route;
        let reason = this.options.reason;

        // if (reason != "alias")
        //   listener.dispatchRoute(route);
      };


      let multipleSearchEngModify = that.multipleRoute(searchEngModify);

      multipleSearchEngModify.checkPreconditions = function(){
        return (this.preEvent.options.reason != "alias");
      }

      searchEngModify.postEvents.push(multipleSearchEngModify);

      let searchEngAliasEvent = Event("searchEngModifyAliasEvent");

      searchEngAliasEvent.effect = function(){
        let baseRoute = this.preEvent.options.route;
        let params = this.preEvent.options.params;

        let route = [baseRoute, "-n", params.number].join(" ");

        listener.dispatchRoute(route);
      };

      searchEngAliasEvent.checkPreconditions = function(){
        return (this.preEvent.options.reason == "alias");
      }

      searchEngModify.postEvents.push(searchEngAliasEvent);

      searchEngModify.wake();

    });

    this.listenForDevTools(function(reason, params){
      let devToolsEvent = Event("devToolsEvent", {
        route: ["dev", reason].join(" "),
        reason: reason,
        params: params
      });

      devToolsEvent.effect = function(){
        let route = this.options.route;

        // listener.dispatchRoute(route);
      };

      let multipleDevToolsEvent = that.multipleRoute(devToolsEvent);

      devToolsEvent.postEvents.push(multipleDevToolsEvent);

      let devToolsSelect = Event("devToolsSelect");

      devToolsSelect.effect = function(){
        let baseRoute = this.preEvent.options.route;
        let toolId = this.preEvent.options.params.toolId;

        let route = [baseRoute, "tool", toolId].join(" ");

        this.options.route = route;
        this.options.toolId = toolId;

        // listener.dispatchRoute(route);
      }

      devToolsSelect.checkPreconditions = function(){
        return (this.preEvent.options.reason == "select");
      };

      devToolsEvent.postEvents.push(devToolsSelect);

      let multipleDevToolsSelect = that.multipleRoute(devToolsSelect);
      devToolsSelect.postEvents.push(multipleDevToolsSelect);

      devToolsEvent.wake();

    });

    this.listenForSessionStore(function(reason){
      let sessRestored = Event("sessionRestored", {
        reason: reason,
        route: ["session", reason].join(" ")
      });

      sessRestored.effect = function(){
        let route = this.options.route;
        // listener.dispatchRoute(route);
      }

      sessRestored.checkPreconditions = function(){
        return (this.options.reason == "restored");
      };

      let multipleSessRestored = that.multipleRoute(sessRestored);
      sessRestored.postEvents.push(multipleSessRestored);

      sessRestored.wake();
    });

    this.listenForPrivateBrowsing(function(reason){
      let privateBrowse = Event("privateBrowse", {
        reason: reason,
        route: ["private browse", reason].join(" ")
      });

      privateBrowse.effect = function(){
        let route = this.options.route;
        // listener.dispatchRoute(route);
      }

      let multiplePrivateBrowse = that.multipleRoute(privateBrowse);

      privateBrowse.postEvents.push(multiplePrivateBrowse);
      privateBrowse.wake();
    });

  
    this.listenForTools(function(tool, reason){

      let toolEvent = Event("toolEvent", {
        tool: tool,
        reason: reason,
        route: [tool, reason].join(" ")
      });

      let multipleToolEvent = that.multipleRoute(toolEvent);

      toolEvent.postEvents.push(multipleToolEvent);

      toolEvent.wake();

    });

    this.listenForHistory(function(reason){
      let histInteraction = Event(("historyInteraction"), {
        reason: reason,
        route: ["history", reason].join(" ")
      });

      histInteraction.effect = function(){
        let route = this.options.route;

        // listener.dispatchRoute(route);
      };

      let multipleHistInteraction = that.multipleRoute(histInteraction);
      histInteraction.postEvents.push(multipleHistInteraction);

      //delete or clear
      //TODO: implement as separate and merge using OR operation
      
      let histDelete = Event("historyDelete");

      histDelete.effect = function(){

        this.options.route = ['history', "delete"].join(" ");

        let route = this.options.route;

        // listener.dispatchRoute(route);
      }

      histDelete.checkPreconditions = function(){
        return (["cleared", "deletedURI", "deletedvisits"].indexOf(this.preEvent.options.reason) != -1);
      }

      histInteraction.postEvents.push(histDelete);

      let multipleHistDelete = that.multipleRoute(histDelete);
      histDelete.postEvents.push(multipleHistDelete);

      histInteraction.wake();
    });

    this.listenForDownloads(function(reason){

      let downloadInteraction = Event("downloadInteraction", {
        reason: reason,
        route: ["download", reason].join(" ")
      });

      downloadInteraction.effect = function(){
        let route = this.options.route;

        // listener.dispatchRoute(route);
      };

      let multipleDownloadInteraction = that.multipleRoute(downloadInteraction);
      downloadInteraction.postEvents.push(multipleDownloadInteraction);

      downloadInteraction.wake();

    });
  },
  behavior: undefined, //defined below
  context: undefined, //defined below
  listenForActiveTabHostnameProgress: undefined, //defined below
  listenForDownloads: undefined, //defined blow
  multipleRoute: undefined //defined below
};


const deliverer = {
  init: function(){

    osFileObjects["timer.data"].silence_length_s = 
      timer.tToS(prefs["delivery.mode.silence_length." + osFileObjects["delivery.data"].mode.rate_limit]);

    timer.onTick(this.checkSchedule);

  },
  deliver: function (/* recommendations */) {

    let recomms = Array.prototype.slice.call(arguments);

    if (recomms.length === 0)
      return;

    if (recomms.length > 1){
      console.log("warning: attempted to deliver multiple recommendations at the same time: " + recomms.length);
      statsEvent("multiple-delivery", {type: "delivery"});
    }

    //finding the recommendation with the highest priority

    let minPriority = recomms[0].priority;
    let minRec = recomms[0];

    recomms.forEach(function(aRecommendation){
      if (aRecommendation.priority < minPriority){
        minPriority = aRecommendation.priority;
        minRec = aRecommendation;
      }
    });

    let aRecommendation = minRec;

    let rejectDelivery = false;

    if (prefs["passive_mode"]){
      console.log("delivery rejected due to passive mode");
      statsEvent("passive-mode-reject", {type: "delivery"});
      rejectDelivery = true;
    }

    if (isPrivate(getMostRecentBrowserWindow())){
      console.log("delivery rejected due to private browsing");
      statsEvent("private-reject", {type: "delivery"});
      rejectDelivery = true;
    }

    if (!timer.isCertainlyActive()){
      console.log("delivery rejected due to uncertain user activity status");
      statsEvent("inactive-reject", {type: "delivery"});
      rejectDelivery = true;
    }

    if (self.delMode.observ_only || (timer.isSilent())){
      if (self.delMode.observ_only){
        console.log("delivery rejected due to observe only period: id -> " + aRecommendation.id);
        statsEvent("observe-only-reject", {type: "delivery"});
      }
      else{
        console.log("delivery rejected due to silence: id -> " + aRecommendation.id);
        statsEvent("silence-reject", {type: "delivery"});
      }

      rejectDelivery = true;
    }

    if (rejectDelivery)
      return;

    console.log("delivering " + aRecommendation.id);

    if (aRecommendation.status == "delivered"){
      console.log("warning: delivering a recommendation that has already been delivered");
      logger.logWarning({type: "delivery", message: "delivering a recommendation that has already been delivered: " + aRecommendation.id});
    }

    aRecommendation.status = "delivered";

    recommendations.update(aRecommendation);

    statsEvent("delivery");

    presenter.present(aRecommendation);

    timer.silence();

  },
  checkSchedule: function(et, ett){

    let deliveryTime = deliveryData.randomDeliveryTime;

    if (!deliveryTime){
      deliverer.scheduleRandomDelivery();
      deliveryTime = deliveryData.randomDeliveryTime
    }

    if (deliveryTime < et){
      console.log("rescheduling random delivery");
      deliverer.scheduleRandomDelivery();
      deliveryTime = deliveryData.randomDeliveryTime
    }

    if (deliveryTime == et){
      let randomMomentEvent = new Event("randomMoment", {
        route: "moment random"
      });

      let multipleRandomMomentEvent = listener.multipleRoute(randomMomentEvent, {
        dispatch: {context: true}
      });

      randomMomentEvent.postEvents.push(multipleRandomMomentEvent);
      randomMomentEvent.wake();
    }

  },
  scheduleRandomDelivery: function(){
    let et = timer.elapsedTime();
  
    let deliveryTime = timer.randomTime(et, et + prefs["timer.random_interval_length_tick"]);

    deliveryData.randomDeliveryTime = deliveryTime;

    console.log("random delivery scheduled: " +  deliveryTime + " ticks");  }
};


listener.behavior = function(route){
  console.log("behavior -> route: ", route);

  route = Route(route); // converting to route object

  let behaviorInfo =  function(aRecommendation){ 
    return {id: aRecommendation.id, count: Number(route.c), num: Number(route.n), "if": Number(route.if)};
  }

  //logging loose matches
  let recomms = recommendations.getByRouteIndex('trigBehavior', route, {looseMatch: true});
  if (recomms.length === 0)
    return;

  recomms.forEach(function(aRecommendation){
    let addedInfo = {};
    let aggregates = {};
    let bi = behaviorInfo(aRecommendation);

    if (bi.num || bi.num == 0) {
      addedInfo.num = bi.num;
      aggregates.num = 'average';
    }

    statsEvent(aRecommendation.id, {type: "looseBehavior"}, addedInfo, aggregates);

    if (utils.isPowerOf2(bi.count) || (utils.isPowerOf2(bi.num) && bi.num > 10)){
      logger.logLooseBehavior(bi);
    }
  });


  recomms = recommendations.getByRouteIndex('trigBehavior', route, {status: 'active'});
  if (recomms.length === 0)
    return;

  recomms.forEach(function(aRecommendation){
    aRecommendation.status = 'outstanding';
    recommendations.update(aRecommendation);

    let addedInfo = {};
    let bi = behaviorInfo(aRecommendation);
    if (bi.num) {
      addedInfo.num = bi.num;
    }

    statsEvent(aRecommendation.id, {type: "behavior"}, addedInfo);
    logger.logBehavior(bi);
  });
};

listener.context = function(route){

  console.log("context -> route: ", route);

  route = Route(route); // convert to object
  let header = route.header;

  let mt = self.delMode.moment;

  // handle random moments
  if (mt === 'random'){
    if (header != 'moment random')
      return;
    else {
      route = '*';
      statsEvent(header);
    }
  }

  // handle interruptible moments
  if (mt === 'interruptible'){
    if (header != 'moment interruptible')
      return;
    else {
      route = '*';
      statsEvent(header);
    }
  }
 
  let recomms = recommendations.getByRouteIndex('delivContext', route, {status: 'outstanding'});

  if (recomms.length === 0)
    return;

  //deliver recommendations
  deliverer.deliver.apply(deliverer, recomms);
};

listener.featureUse = function(route){

  console.log("featureUse -> route: ", route);

  route = Route(route);

  // temporary workaround to address the compatibility issues with tab center
  if (route.header == "addon has <EMAIL>"){
    let treeStyleTab = recommendations['treeStyleTab'];
    let tabGroups = recommendations['tabGroups'];

    treeStyleTab.status = 'inactive';
    tabGroups.status = 'inactive';

    recommendations.update(treeStyleTab);
    recommendations.update(tabGroups)
  }

  let featureUseInfo =  function(aRecommendation){ 
    return {id: aRecommendation.id, count: Number(route.c), num: Number(route.n), "if": Number(route.if)};
  }

  //logging loose matches
  let recomms = recommendations.getByRouteIndex('featUseBehavior', route, {looseMatch: true});
  if (recomms.length === 0)
    return;

  recomms.forEach(function(aRecommendation){
{'fraction_non_alphanumeric': 0.13552664838904122, 'fraction_numerical': 0.001049991250072916, 'mean_word_length': 2.9292535717014285, 'pattern_counts': {'":': 8, '<': 8, '<?xml version=': 0, '>': 24, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 24, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2110916', 'n_tokens_mistral': 15149, 'n_tokens_neox': 14413, 'n_words': 2860}