// BEGIN FLOCK GPL
//
// Copyright Flock Inc. 2005-2008
// http://flock.com
//
// This file may be used under the terms of the
// GNU General Public License Version 2 or later (the "GPL"),
// http://www.gnu.org/licenses/gpl.html
//
// Software distributed under the License is distributed on an "AS IS" basis,
// WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
// for the specific language governing rights and limitations under the
// License.
//
// END FLOCK GPL

const MS_CONTRACTID = "@flock.com/metrics-service;1";
const MS_CLASSID    = Components.ID("{bc1358fa-6e36-4d0d-a401-b0b02111114c}");
const MS_CLASSNAME  = "Flock Metrics Service";


const ENABLED_BY_DEFAULT = true;

const STORE_FILENAME = "mstore.sqlite";
const OLD_STORE_FILENAME = "mstore.js";

const LOGGING_URL = "http://metrics.flock.com/anonusage.php";

const PREF_FLOCK_METRICS_ENABLED = "flock.metrics.enabled";

const PREF_FLOCK_METRICS_INTERVAL = "flock.metrics.interval";
const DEFAULT_METRICS_INTERVAL = 86400;

const PREF_FLOCK_FIRSTRUN_UUID = "flock.firstrun.uuid";

const PREF_GENERAL_USERAGENT_EDITION = "general.useragent.edition";
const PREF_GENERAL_USERAGENT_LOCALE = "general.useragent.locale";

const PREF_DEFAULT_SEARCH_ENGINE = "browser.search.selectedEngine";
const PREF_DEFAULT_ENGINE_NAME = "browser.search.defaultenginename";
const PREF_BROWSER_STARTUP_HOMEPAGE = "browser.startup.homepage";
const PREF_FEED_SELECTED_ACTION = "browser.feeds.handler";

const PREF_FLOCK_FIRST_RUN_BIGDATE = "flock.first_run.bigDate";

const PREF_FLOCK_AUTO_SET_PRIMARY_ACCOUNT = "flock.webmail.autoSetPrimaryAccount";
const PREF_FLOCK_DEFAULT_MAILER = "flock.webmail.useAsDefaultMailer";

const PREF_FLOCK_MICRO_EDITION = "flock.microEdition";

const URI_BRAND_PROPERTIES = "chrome://branding/locale/brand.properties";

const MYWORLD_URL = "about:myworld";

const HTTP_CODE_OK = 200;

const IS_AUTHENTICATED_RSC = "http://flock.com/rdf#isAuthenticated";
const IS_TRANSIENT_RSC = "http://flock.com/rdf#isTransient";


const CC = Components.classes;
const CI = Components.interfaces;
const CR = Components.results;
const CU = Components.utils;

CU.import("resource:///modules/FlockXPCOMUtils.jsm");
CU.import("resource://gre/modules/JSON.jsm");
CU.import("resource:///modules/FlockCryptoHash.jsm");


var gApp = null;
var gPref = null;
var gABI = null;
var gOSVersion = null;


function getObserverService() {
  return CC["@mozilla.org/observer-service;1"]
         .getService(CI.nsIObserverService);
}

function getCharPref(aPrefName, aDefaultValue) {
  if (gPref.getPrefType(aPrefName) == CI.nsIPrefBranch.PREF_STRING) {
    return gPref.getCharPref(aPrefName);
  } else {
    return aDefaultValue;
  }
}

function getIntPref(aPrefName, aDefaultValue) {
  if (gPref.getPrefType(aPrefName) == CI.nsIPrefBranch.PREF_INT) {
    return gPref.getIntPref(aPrefName);
  } else {
    return aDefaultValue;
  }
}

function getBoolPref(aPrefName, aDefaultValue) {
  if (gPref.getPrefType(aPrefName) == CI.nsIPrefBranch.PREF_BOOL) {
    return gPref.getBoolPref(aPrefName);
  } else {
    return aDefaultValue;
  }
}

function createStatement(aDBConn, aSQL) {
  var stmt = aDBConn.createStatement(aSQL);
  var wrapper = CC["@mozilla.org/storage/statement-wrapper;1"]
                .createInstance(CI.mozIStorageStatementWrapper);
  wrapper.initialize(stmt);
  return wrapper;
}


function MetricsService() {
  gApp = CC["@mozilla.org/xre/app-info;1"]
         .getService(CI.nsIXULAppInfo)
         .QueryInterface(CI.nsIXULRuntime);

  gPref = CC["@mozilla.org/preferences-service;1"]
           .getService(CI.nsIPrefBranch2)

  try {
    gABI = gApp.XPCOMABI;

    var macutils = CC["@mozilla.org/xpcom/mac-utils;1"];
    if (macutils) {
      if (macutils.getService(CI.nsIMacUtils).isUniversalBinary) {
        gABI = "Universal-gcc3";
      }
    }
  } catch (ex) {
  }

  try {
    var sysInfo = CC["@mozilla.org/system-info;1"]
                  .getService(CI.nsIPropertyBag2);
    gOSVersion = sysInfo.getProperty("name") + " " +
                 sysInfo.getProperty("version");
  } catch (ex) {
  }

  var obs = getObserverService();

  obs.addObserver(this, "flock-data-ready", false);
  obs.addObserver(this, "xpcom-shutdown", false);

  this.observe(null, "nsPref:changed", PREF_FLOCK_METRICS_ENABLED);
}

MetricsService.prototype = new FlockXPCOMUtils.genericComponent(
  MS_CLASSNAME,
  MS_CLASSID,
  MS_CONTRACTID,
  MetricsService,
  CI.nsIClassInfo.SINGLETON,
  [
    CI.flockIMetricsService,
    CI.flockIRDFObserver,
    CI.nsITimerCallback,
    CI.nsIObserver
  ]
);

MetricsService.prototype._xpcom_categories = [
  { category: "flock-startup", service: true }
];


MetricsService.prototype._start =
function MS__start() {
  this._logger = CC["@flock.com/logger;1"].createInstance(CI.flockILogger);
  this._logger.init("metrics");
  this._logger.info("starting up...");

  this._deleteOldStore();

  this.getUserUUID();

  this._coop = CC["@flock.com/singleton;1"]
               .getService(CI.flockISingleton)
               .getSingleton("chrome://flock/content/common/load-faves-coop.js")
               .wrappedJSObject;

  gPref.addObserver(PREF_FLOCK_METRICS_ENABLED, this, false);
  this.observe(null, "nsPref:changed", PREF_FLOCK_METRICS_ENABLED);

  this._watchLoginEvents();

  this.report("Browser-Start");

  gPref.addObserver(PREF_DEFAULT_SEARCH_ENGINE, this, false);
  gPref.addObserver(PREF_BROWSER_STARTUP_HOMEPAGE, this, false);
  gPref.addObserver(PREF_FEED_SELECTED_ACTION, this, false);
  gPref.addObserver(PREF_FLOCK_AUTO_SET_PRIMARY_ACCOUNT, this, false);
  gPref.addObserver(PREF_FLOCK_DEFAULT_MAILER, this, false);

  var tm = CC["@mozilla.org/updates/timer-manager;1"]
           .getService(CI.nsIUpdateTimerManager);
  var interval = getIntPref(PREF_FLOCK_METRICS_INTERVAL,
                            DEFAULT_METRICS_INTERVAL);
  tm.registerTimer("background-metrics-timer", this, interval);
}

MetricsService.prototype._shutdown =
function MS__shutdown() {
  gApp = null;
  gPref = null;
}

MetricsService.prototype.notify =
function MS_notify(aTimer) {
  this._sendReport();
}

MetricsService.prototype._configure =
function MS__configure() {
  this._enabled = getBoolPref(PREF_FLOCK_METRICS_ENABLED, ENABLED_BY_DEFAULT);

  if (this._enabled) {
    this._createStore();
  } else {
    this._deleteStore();
  }
}

MetricsService.prototype._prefChanged =
function MS__prefChanged(aPref) {
  switch (aPref) {
    case PREF_DEFAULT_SEARCH_ENGINE:
      this._reportDefaultSearchEngine();
      break;

    case PREF_BROWSER_STARTUP_HOMEPAGE:
      this._reportMyWorldStartPage();
      break;

    case PREF_FEED_SELECTED_ACTION:
      this._reportFeedReader();
      break;

    case PREF_FLOCK_METRICS_ENABLED:
      this._configure();
      break;

    case PREF_FLOCK_AUTO_SET_PRIMARY_ACCOUNT:
      this._reportWebMailAutoSetPrimaryAccount();
      break;

    case PREF_FLOCK_DEFAULT_MAILER:
      this._reportWebMailFlockDefaultMailer();
      break;
  }
}

MetricsService.prototype.observe =
function MS_observe(aSubject, aTopic, aState) {
  var obs = getObserverService();

  switch (aTopic) {
    case "flock-data-ready":
      obs.removeObserver(this, "flock-data-ready");
      this._start();
      break;

    case "xpcom-shutdown":
      obs.removeObserver(this, "xpcom-shutdown");
      this._shutdown();
      break;

    case "nsPref:changed":
      this._prefChanged(aState);
      break;
  }
}

MetricsService.prototype._getProfileFile =
function MS__getProfileFile(aFileName) {
  var file = CC["@mozilla.org/file/directory_service;1"]
             .getService(CI.nsIProperties)
             .get("ProfD", CI.nsILocalFile);
  file.append(aFileName);
  return file;
}

MetricsService.prototype._deleteOldStore =
function MS__deleteOldStore() {
  var oldFile = this._getProfileFile(OLD_STORE_FILENAME);

  try {
    oldFile.remove(false);
  } catch (ex) {
  }
}

MetricsService.prototype._getMetricsDBFile =
function MS__getMetricsDBFile() {
  return this._getProfileFile(STORE_FILENAME);
}

MetricsService.prototype._createStore =
function MS__createStore() {
  var dbfile = this._getMetricsDBFile();

  var storageService = CC["@mozilla.org/storage/service;1"]
                       .getService(CI.mozIStorageService);
  this._DBConn = storageService.openDatabase(dbfile);

  var schema = "key TEXT, value TEXT, type TEXT, time NUMBER";

  var alreadyCreated = this._DBConn.tableExists("metrics");

  if (!alreadyCreated) {
    try {
      this._DBConn.createTable("metrics", schema);
    } catch (ex) {
      this._DBConn = null;
      this._enabled = false;
      return;
    }
  }

  this._insertEvent = createStatement(this._DBConn,
    "INSERT INTO metrics (key, value, type, time) " +
    "VALUES (:key, :value, :type, :time)");
  this._getEvents = createStatement(this._DBConn,
    "SELECT * FROM metrics WHERE time <= :time");
  this._deleteEvents = createStatement(this._DBConn,
    "DELETE FROM metrics WHERE time <= :time");

  if (!alreadyCreated) {
    this._fillPrefInfo();
  }
}

MetricsService.prototype._deleteStore =
function MS__deleteStore() {
  this._DBConn = null;

  var dbfile = this._getMetricsDBFile();
  try {
    dbfile.remove(false);
  } catch (ex) {
  }
}

MetricsService.prototype._expireStore =
function MS__expireStore(aExpire) {
  if (!aExpire) {
    return;
  }

  var stmt = this._deleteEvents;
  stmt.reset();
  stmt.params.time = aExpire;
  stmt.step();
}

MetricsService.prototype.getUserUUID =
function MS_getUserUUID() {
  var currentUUID = getCharPref(PREF_FLOCK_FIRSTRUN_UUID);

  if (!currentUUID) {
    var uuidGen = CC["@mozilla.org/uuid-generator;1"]
                  .createInstance(CI.nsIUUIDGenerator);
    var uuid;
    try {
      uuid = uuidGen.generateUUID();
    } catch (ex) {
      // Doh, not enough randomness, we'll try again in the future
      return "0";
    }

    currentUUID = String(uuid).replace(/[{}]/g, "");
    gPref.setCharPref(PREF_FLOCK_FIRSTRUN_UUID, currentUUID);
  }

  return currentUUID;
}

MetricsService.prototype._getBaseInfo =
function MS__getBaseInfo() {
  var sbs = CC["@mozilla.org/intl/stringbundle;1"]
            .getService(CI.nsIStringBundleService);
  var bundle = sbs.createBundle(URI_BRAND_PROPERTIES);
  var product = bundle.GetStringFromName("brandShortName");

  var edition = getCharPref(PREF_GENERAL_USERAGENT_EDITION, "");
  var microEdition = getCharPref(PREF_FLOCK_MICRO_EDITION, "");
  var uuid = this.getUserUUID();

  var locale;
  try {
    locale = gPref.getComplexValue(PREF_GENERAL_USERAGENT_LOCALE,
                                   nsIPrefLocalizedString).data;
  } catch (ex) {
    locale = getCharPref(PREF_GENERAL_USERAGENT_LOCALE);
  }

  var platform = gApp.OS + "_" + gABI;

  var defaultBrowser;
  try {
    if (CC["@mozilla.org/browser/shell-service;1"]
        .getService(CI.nsIShellService)
        .isDefaultBrowser(false))
    {
      defaultBrowser = "true";
    } else {
      defaultBrowser = "false";
    }
  } catch (ex) {
    defaultBrowser = "error";
  }

  var info = { "Browser-Product": product,
               "Browser-Version": gApp.version,
               "Browser-BuildID": gApp.appBuildID,
               "Browser-Edition": edition,
               "Browser-MicroEdition": microEdition,
               "Browser-Locale": locale,
               "Browser-Platform": platform,
               "Browser-OSVersion": gOSVersion,
               "Browser-MetricsEnabled": this._enabled,
               "User-UUID": uuid,
               "Browser-ClientTime": null,
               "Browser-DefaultBrowser": defaultBrowser
             };

  var oldFirstRun = getCharPref(PREF_FLOCK_FIRST_RUN_BIGDATE);
  if (oldFirstRun) {
    info["User-FirstRun"] = oldFirstRun;
  }

  return info;
}

MetricsService.prototype._getFriendCounts =
function MS__getFriendCounts() {
  var friendCounts = [];
  var catMgr = CC["@mozilla.org/categorymanager;1"]
               .getService(CI.nsICategoryManager);
  var svcEnum = catMgr.enumerateCategory("flockWebService");
  while (svcEnum.hasMoreElements()) {
    var entry = svcEnum.getNext().QueryInterface(CI.nsISupportsCString);
    if (entry) {
      var contractID = catMgr.getCategoryEntry("flockWebService", entry.data);
      var svc = CC[contractID].getService(CI.flockIWebService);
      if (svc instanceof CI.flockISocialWebService) {
        var accounts = svc.getAccounts();
        while (accounts.hasMoreElements()) {
          var account = accounts.getNext()
                        .QueryInterface(CI.flockISocialWebServiceAccount);
          friendCounts.push({service: svc.shortName,
                             account: FlockCryptoHash.md5(account.urn),
                             count: account.getFriendCount()});
        }
      }
    }
  }
  return friendCounts;
}

MetricsService.prototype._fillPrefInfo =
function MS__fillPrefInfo() {
  this._reportDefaultSearchEngine();
  this._reportMyWorldStartPage();
  this._reportFeedReader();
  this._reportWebMailAutoSetPrimaryAccount();
  this._reportWebMailFlockDefaultMailer();
}

MetricsService.prototype._reportWebMailAutoSetPrimaryAccount =
function MS_reportWebMailAutoSetPrimaryAccount() {
  this.report("WebMail-PrimaryAccountOnLogin",
              gPref.getBoolPref(PREF_FLOCK_AUTO_SET_PRIMARY_ACCOUNT, false));
}

MetricsService.prototype._reportWebMailFlockDefaultMailer =
function MS_reportWebMailFlockDefaultMailer() {
  this.report("WebMail-MailtoPrefChange",
              gPref.getBoolPref(PREF_FLOCK_DEFAULT_MAILER, true));
}

MetricsService.prototype._reportDefaultSearchEngine =
function MS__reportDefaultSearchEngine() {
  var defaultEngine;
  try {
    defaultEngine = gPref.getCharPref(PREF_DEFAULT_SEARCH_ENGINE);
  } catch (ex) {
    defaultEngine = gPref.getComplexValue(PREF_DEFAULT_ENGINE_NAME,
                                          CI.nsIPrefLocalizedString).data;
  }

  if (!defaultEngine)
    defaultEngine = "";

  this.report("SearchBox-DefaultSearchEngine", defaultEngine);
}

MetricsService.prototype._reportMyWorldStartPage =
function MS__reportMyWorldStartPage() {
  var startPageHasMyWorld = false;
  try {
    var startPage = gPref.getComplexValue(PREF_BROWSER_STARTUP_HOMEPAGE,
                                          CI.nsIPrefLocalizedString).data;
    var startPages = startPage.split("|");
    for each (var url in startPages) {
      // Trim leading/trailing whitespace from URL
      url = url.replace(/(^\s+)|(\s+$)/g, "");
      if (url == MYWORLD_URL) {
        startPageHasMyWorld = true;
        break;
      }
    }
  } catch (ex) {
  }

  this.report("Browser-MyWorldIsHomePage", startPageHasMyWorld);
}

MetricsService.prototype._reportFeedReader =
function MS__reportFeedReader() {
  var feedReader, action = getCharPref(PREF_FEED_SELECTED_ACTION);

  switch (action) {
    case "ask":
      feedReader = "news";
      break;

    case "bookmarks":
      feedReader = "livemarks";
      break;

    default:
      feedReader = "other";
      break;
  }

  this.report("Feeds-DefaultNewsReader", feedReader);
}

MetricsService.prototype._watchLoginEvents =
function MS__watchLoginEvents() {
  const RDFS = CC["@mozilla.org/rdf/rdf-service;1"]
               .getService(CI.nsIRDFService);

  var faves = RDFS.GetDataSource("rdf:flock-favorites");
  faves.QueryInterface(CI.flockIRDFObservable);

  faves.addArcObserver(CI.flockIRDFObserver.TYPE_CHANGE, null,
                       RDFS.GetResource(IS_AUTHENTICATED_RSC),
                       null, this);
  faves.addArcObserver(CI.flockIRDFObserver.TYPE_CHANGE, null,
                       RDFS.GetResource(IS_TRANSIENT_RSC),
                       null, this);
}

MetricsService.prototype.rdfChanged =
function MS_rdfChanged(aDS, aType, aSource, aPredicate, aTarget, aOldTarget) {
  if (aTarget instanceof CI.nsIRDFLiteral) {
    var account = this._coop.get_from_resource(aSource);

    if (aPredicate.ValueUTF8 == IS_AUTHENTICATED_RSC) {
      if (account.isAuthenticated) {
        this.report("Account-Login", account.serviceId);
      } else {
        this.report("Account-Logout", account.serviceId);
      }
    } else if (aPredicate.ValueUTF8 == IS_TRANSIENT_RSC) {
      if (!account.isTransient) {
        // The isTransient flag has changed.  Please note that this will
        // only ever change from "true" to "false"; when the user opts to
        // "keep" the service.  This means that we have to detect that the
        // user opted to "FORGET" The service we have to detect is elsewher
        // (as the whole account is deleted and the onChange event never
        // fires).
        this.report("Account-Keep", account.serviceId);
      }
    }
  }
}

MetricsService.prototype._getValueAndTypeForStorage =
function MS__getValueAndTypeForStorage(aValue) {
  var value, type = typeof(aValue);

  switch (type) {
    case "undefined":
      value = null;
      break;

    case "string":
      value = aValue;
      break;

    case "boolean":
    case "number":
      value = aValue.toString();
      break;

    case "object":
      if (aValue) {
        var realValue;

        // Pull a single object out of a 1-element array
        if ("length" in aValue &&
            aValue.length === 1 &&
            aValue[0] &&
            typeof(aValue[0]) == "object")
        {
          realValue = aValue[0];
        } else {
          realValue = aValue;
        }

        // Need to make sure the object is JSON-safe, so we actually
        // do a full JSON convert here instead of .toSource()
        // and fall through to return null value if it fails
        try {
          value = "(" + JSON.toString(realValue) + ")";
          break;
        } catch (ex) {
        }
      }
      // fall through

    default:
      value = null;
      type = "undefined";
      break;
  }

  return [value, type];
}

MetricsService.prototype.report =
function MS_report(aKey, aValue) {
  if (!this._enabled) {
    return;
  }

  try {
    var [value, type] = this._getValueAndTypeForStorage(aValue);

    var stmt = this._insertEvent;
    stmt.reset();
    pp = stmt.params;
    pp.key = aKey;
    pp.value = value;
    pp.type = type;
    pp.time = Date.now();
    stmt.step();
  } catch (ex) {
  }
}

MetricsService.prototype.getCurrentReport =
function MS_getCurrentReport() {
  var [data, expire] = this._createReport();
  return data;
}

MetricsService.prototype.reportNow =
function MS_reportNow(aKey, aValue) {
  /* XXX: only here to catch any users of the old API */
  this.report("old-reportNow-" + aKey, aValue);
}

MetricsService.prototype.reportCount =
function MS_reportCount(aKey) {
  /* XXX: only here to catch any users of the old API */
  this.report("old-reportCount-" + aKey, null);
}

MetricsService.prototype._createReport =
function MS__createReport() {
  var data = [];
  var baseInfo = this._getBaseInfo();
  var now = Date.now();
  var expire = 0;

  if (this._enabled) {
    expire = now;

    var sandbox = new CU.Sandbox("about:blank");

    var stmt = this._getEvents;
    stmt.reset();
    stmt.params.time = expire;

    while (stmt.step()) {
      var row = stmt.row;
      var entry = { key: row.key, time: row.time };
      if (row.type == "string" || row.type == "undefined") {
        entry.value = row.value;
      } else {
        entry.value = CU.evalInSandbox(row.value, sandbox);
      }
      data.push(entry);
    }

    var friendCounts = this._getFriendCounts();
    for each (var value in friendCounts) {
      var entry = {key: "Account-FriendCount",
                   value: value,
                   time: now};
      data.push(entry);
    }
  }
  
  for (let [key, value] in Iterator(baseInfo)) {
    var entry = { key: key, value: value, time: now };
    data.push(entry);
  }

  return [data, expire];
}

MetricsService.prototype._sendReport =
function MS__sendReport() {
  var [data, expire] = this._createReport();

  if (gApp.appBuildID == "0000000000") {
    this._logger.debug("metrics packet: " + JSON.toString(data));
    this._expireStore(expire);
    return;
  }

  try {
    var compressor = CC["@flock.com/compress-content;1"]
                     .createInstance(CI.flockICompressContent);
    var compressedData = compressor.compressString(JSON.toString(data));

    var xhr = CC["@mozilla.org/xmlextras/xmlhttprequest;1"]
              .createInstance(CI.nsIXMLHttpRequest);

    var self = this;
    xhr.onload = function MS__sendReportOnLoad(aEvent) {
      self._onLoad(aEvent, expire);
    };
    xhr.onerror = function MS__sendReportOnError(aEvent) {
      self._onError(aEvent);
    };

    xhr.backgroundRequest = true;
    xhr.open("POST", LOGGING_URL);
    xhr.send(compressedData);
  } catch (ex) {
    this._onError(null);
  }
}

MetricsService.prototype._onLoad =
function MS__onLoad(aEvent, aExpire) {
  var xhr = aEvent.target;
  if (xhr.status == HTTP_CODE_OK) {
    this._expireStore(aExpire);
  }
}

MetricsService.prototype._onError =
function MS__onError(aEvent) {
  // Some error happened while sending, don't clear the data
}

var gComponentsArray = [MetricsService];

var NSGetModule = FlockXPCOMUtils.generateNSGetModule(MS_CLASSNAME,
                                                      gComponentsArray);
