/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";

const {classes: Cc, interfaces: Ci, utils: Cu} = Components;

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

XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
                                  "resource://devtools/shared/event-emitter.js");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                  "resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
                                  "resource://gre/modules/PlacesUtils.jsm");

const {
  normalizeTime,
  SingletonEventManager,
} = ExtensionUtils;

let nsINavHistoryService = Ci.nsINavHistoryService;
const TRANSITION_TO_TRANSITION_TYPES_MAP = new Map([
  ["link", nsINavHistoryService.TRANSITION_LINK],
  ["typed", nsINavHistoryService.TRANSITION_TYPED],
  ["auto_bookmark", nsINavHistoryService.TRANSITION_BOOKMARK],
  ["auto_subframe", nsINavHistoryService.TRANSITION_EMBED],
  ["manual_subframe", nsINavHistoryService.TRANSITION_FRAMED_LINK],
]);

let TRANSITION_TYPE_TO_TRANSITIONS_MAP = new Map();
for (let [transition, transitionType] of TRANSITION_TO_TRANSITION_TYPES_MAP) {
  TRANSITION_TYPE_TO_TRANSITIONS_MAP.set(transitionType, transition);
}

function getTransitionType(transition) {
  // cannot set a default value for the transition argument as the framework sets it to null
  transition = transition || "link";
  let transitionType = TRANSITION_TO_TRANSITION_TYPES_MAP.get(transition);
  if (!transitionType) {
    throw new Error(`|${transition}| is not a supported transition for history`);
  }
  return transitionType;
}

function getTransition(transitionType) {
  return TRANSITION_TYPE_TO_TRANSITIONS_MAP.get(transitionType) || "link";
}

/*
 * Converts a nsINavHistoryResultNode into a HistoryItem
 *
 * https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryResultNode
 */
function convertNodeToHistoryItem(node) {
  return {
    id: node.pageGuid,
    url: node.uri,
    title: node.title,
    lastVisitTime: PlacesUtils.toDate(node.time).getTime(),
    visitCount: node.accessCount,
  };
}

/*
 * Converts a nsINavHistoryResultNode into a VisitItem
 *
 * https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryResultNode
 */
function convertNodeToVisitItem(node) {
  return {
    id: node.pageGuid,
    visitId: node.visitId,
    visitTime: PlacesUtils.toDate(node.time).getTime(),
    referringVisitId: node.fromVisitId,
    transition: getTransition(node.visitType),
  };
}

/*
 * Converts a nsINavHistoryContainerResultNode into an array of objects
 *
 * https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryContainerResultNode
 */
function convertNavHistoryContainerResultNode(container, converter) {
  let results = [];
  container.containerOpen = true;
  for (let i = 0; i < container.childCount; i++) {
    let node = container.getChild(i);
    results.push(converter(node));
  }
  container.containerOpen = false;
  return results;
}

var _observer;

function getObserver() {
  if (!_observer) {
    _observer = {
      onDeleteURI: function(uri, guid, reason) {
        this.emit("visitRemoved", {allHistory: false, urls: [uri.spec]});
      },
      onVisit: function(uri, visitId, time, sessionId, referringId, transitionType, guid, hidden, visitCount, typed) {
        let data = {
          id: guid,
          url: uri.spec,
          title: "",
          lastVisitTime: time / 1000,  // time from Places is microseconds,
          visitCount,
          typedCount: typed,
        };
        this.emit("visited", data);
      },
      onBeginUpdateBatch: function() {},
      onEndUpdateBatch: function() {},
      onTitleChanged: function() {},
      onClearHistory: function() {
        this.emit("visitRemoved", {allHistory: true, urls: []});
      },
      onPageChanged: function() {},
      onFrecencyChanged: function() {},
      onManyFrecenciesChanged: function() {},
      onDeleteVisits: function(uri, time, guid, reason) {
        this.emit("visitRemoved", {allHistory: false, urls: [uri.spec]});
      },
    };
    EventEmitter.decorate(_observer);
    PlacesUtils.history.addObserver(_observer, false);
  }
  return _observer;
}

extensions.registerSchemaAPI("history", "addon_parent", context => {
  return {
    history: {
      addUrl: function(details) {
        let transition, date;
        try {
          transition = getTransitionType(details.transition);
        } catch (error) {
          return Promise.reject({message: error.message});
        }
        if (details.visitTime) {
          date = normalizeTime(details.visitTime);
        }
        let pageInfo = {
          title: details.title,
          url: details.url,
          visits: [
            {
              transition,
              date,
            },
          ],
        };
        try {
          return PlacesUtils.history.insert(pageInfo).then(() => undefined);
        } catch (error) {
          return Promise.reject({message: error.message});
        }
      },

      deleteAll: function() {
        return PlacesUtils.history.clear();
      },

      deleteRange: function(filter) {
        let newFilter = {
          beginDate: normalizeTime(filter.startTime),
          endDate: normalizeTime(filter.endTime),
        };
        // History.removeVisitsByFilter returns a boolean, but our API should return nothing
        return PlacesUtils.history.removeVisitsByFilter(newFilter).then(() => undefined);
      },

      deleteUrl: function(details) {
        let url = details.url;
        // History.remove returns a boolean, but our API should return nothing
        return PlacesUtils.history.remove(url).then(() => undefined);
      },

      search: function(query) {
        let beginTime = (query.startTime == null) ?
                          PlacesUtils.toPRTime(Date.now() - 24 * 60 * 60 * 1000) :
                          PlacesUtils.toPRTime(normalizeTime(query.startTime));
        let endTime = (query.endTime == null) ?
                        Number.MAX_VALUE :
                        PlacesUtils.toPRTime(normalizeTime(query.endTime));
        if (beginTime > endTime) {
          return Promise.reject({message: "The startTime cannot be after the endTime"});
        }

        let options = PlacesUtils.history.getNewQueryOptions();
        options.sortingMode = options.SORT_BY_DATE_DESCENDING;
        options.maxResults = query.maxResults || 100;

        let historyQuery = PlacesUtils.history.getNewQuery();
        historyQuery.searchTerms = query.text;
        historyQuery.beginTime = beginTime;
        historyQuery.endTime = endTime;
        let queryResult = PlacesUtils.history.executeQuery(historyQuery, options).root;
        let results = convertNavHistoryContainerResultNode(queryResult, convertNodeToHistoryItem);
        return Promise.resolve(results);
      },

      getVisits: function(details) {
        let url = details.url;
        if (!url) {
          return Promise.reject({message: "A URL must be provided for getVisits"});
        }

        let options = PlacesUtils.history.getNewQueryOptions();
        options.sortingMode = options.SORT_BY_DATE_DESCENDING;
        options.resultType = options.RESULTS_AS_VISIT;

        let historyQuery = PlacesUtils.history.getNewQuery();
        historyQuery.uri = NetUtil.newURI(url);
        let queryResult = PlacesUtils.history.executeQuery(historyQuery, options).root;
        let results = convertNavHistoryContainerResultNode(queryResult, convertNodeToVisitItem);
        return Promise.resolve(results);
      },

      onVisited: new SingletonEventManager(context, "history.onVisited", fire => {
        let listener = (event, data) => {
          context.runSafe(fire, data);
        };

        getObserver().on("visited", listener);
        return () => {
          getObserver().off("visited", listener);
        };
      }).api(),

      onVisitRemoved: new SingletonEventManager(context, "history.onVisitRemoved", fire => {
        let listener = (event, data) => {
          context.runSafe(fire, data);
        };

        getObserver().on("visitRemoved", listener);
        return () => {
          getObserver().off("visitRemoved", listener);
        };
      }).api(),
    },
  };
});
