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

module.metadata = {
  "stability": "experimental",
  "engines": {
    "Palemoon": "*",
    "Firefox": "*",
    "SeaMonkey": "*"
  }
};

const { Cc, Ci } = require('chrome');
const { all } = require('../../core/promise');
const { safeMerge, omit } = require('../../util/object');
const historyService = Cc['@mozilla.org/browser/nav-history-service;1']
                     .getService(Ci.nsINavHistoryService);
const bookmarksService = Cc['@mozilla.org/browser/nav-bookmarks-service;1']
                         .getService(Ci.nsINavBookmarksService);
const { request, response } = require('../../addon/host');
const { newURI } = require('../../url/utils');
const { send } = require('../../addon/events');
const { on, emit } = require('../../event/core');
const { filter } = require('../../event/utils');

const ROOT_FOLDERS = [
  bookmarksService.unfiledBookmarksFolder, bookmarksService.toolbarFolder,
  bookmarksService.bookmarksMenuFolder
];

const EVENT_MAP = {
  'sdk-places-query': queryReceiver
};

// Properties that need to be manually
// copied into a nsINavHistoryQuery object
const MANUAL_QUERY_PROPERTIES = [
  'uri', 'folder', 'tags', 'url', 'folder'
];

const PLACES_PROPERTIES = [
  'uri', 'title', 'accessCount', 'time'
];

function execute (queries, options) {
  return new Promise(resolve => {
    let root = historyService
        .executeQueries(queries, queries.length, options).root;
    // Let's extract an eventual uri wildcard, if both domain and uri are set.
    // See utils.js::urlQueryParser() for more details.
    // In case of multiple queries, we only retain the first found wildcard.
    let uriWildcard = queries.reduce((prev, query) => {
      if (query.uri && query.domain) {
        if (!prev)
          prev = query.uri.spec;
        query.uri = null;
      }
      return prev;
    }, "");
    resolve(collect([], root, uriWildcard));
  });
}

function collect (acc, node, uriWildcard) {
  node.containerOpen = true;
  for (let i = 0; i < node.childCount; i++) {
    let child = node.getChild(i);

    if (!uriWildcard || child.uri.startsWith(uriWildcard)) {
      acc.push(child);
    }
    if (child.type === child.RESULT_TYPE_FOLDER) {
      let container = child.QueryInterface(Ci.nsINavHistoryContainerResultNode);
      collect(acc, container, uriWildcard);
    }
  }
  node.containerOpen = false;
  return acc;
}

function query (queries, options) {
  return new Promise((resolve, reject) => {
    queries = queries || [];
    options = options || {};
    let optionsObj, queryObjs;

    optionsObj = historyService.getNewQueryOptions();
    queryObjs = [].concat(queries).map(createQuery);
    if (!queryObjs.length) {
      queryObjs = [historyService.getNewQuery()];
    }
    safeMerge(optionsObj, options);

    /*
     * Currently `places:` queries are not supported
     */
    optionsObj.excludeQueries = true;

    execute(queryObjs, optionsObj).then((results) => {
      if (optionsObj.queryType === 0) {
        return results.map(normalize);
      }
      else if (optionsObj.queryType === 1) {
        // Formats query results into more standard
        // data structures for returning
        return all(results.map(({itemId}) =>
          send('sdk-places-bookmarks-get', { id: itemId })));
      }
    }).then(resolve, reject);
  });
}
exports.query = query;

function createQuery (query) {
  query = query || {};
  let queryObj = historyService.getNewQuery();

  safeMerge(queryObj, omit(query, MANUAL_QUERY_PROPERTIES));

  if (query.tags && Array.isArray(query.tags))
    queryObj.tags = query.tags;
  if (query.uri || query.url)
    queryObj.uri = newURI(query.uri || query.url);
  if (query.folder)
    queryObj.setFolders([query.folder], 1);
  return queryObj;
}

function queryReceiver (message) {
  let queries = message.data.queries || message.data.query;
  let options = message.data.options;
  let resData = {
    id: message.id,
    event: message.event
  };

  query(queries, options).then(results => {
    resData.data = results;
    respond(resData);
  }, reason => {
    resData.error = reason;
    respond(resData);
  });
}

/*
 * Converts a nsINavHistoryResultNode into a plain object
 *
 * https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryResultNode
 */
function normalize (historyObj) {
  return PLACES_PROPERTIES.reduce((obj, prop) => {
    if (prop === 'uri')
      obj.url = historyObj.uri;
    else if (prop === 'time') {
      // Cast from microseconds to milliseconds
      obj.time = Math.floor(historyObj.time / 1000)
    }
    else if (prop === 'accessCount')
      obj.visitCount = historyObj[prop];
    else
      obj[prop] = historyObj[prop];
    return obj;
  }, {});
}

/*
 * Hook into host
 */

var reqStream = filter(request, data => /sdk-places-query/.test(data.event));
on(reqStream, 'data', function (e) {
  if (EVENT_MAP[e.event]) EVENT_MAP[e.event](e);
});

function respond (data) {
  emit(response, 'data', data);
}