/* -*- 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/PlacesUtils.jsm");
var Bookmarks = PlacesUtils.bookmarks;

Cu.import("resource://gre/modules/ExtensionUtils.jsm");
var {
  runSafe,
} = ExtensionUtils;

XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                  "resource://gre/modules/Task.jsm");

function getTree(rootGuid, onlyChildren) {
  function convert(node, parent) {
    let treenode = {
      id: node.guid,
      title: node.title || "",
      index: node.index,
      dateAdded: node.dateAdded / 1000,
    };

    if (parent && node.guid != Bookmarks.rootGuid) {
      treenode.parentId = parent.guid;
    }

    if (node.type == PlacesUtils.TYPE_X_MOZ_PLACE) {
      // This isn't quite correct. Recently Bookmarked ends up here ...
      treenode.url = node.uri;
    } else {
      treenode.dateGroupModified = node.lastModified / 1000;

      if (node.children && !onlyChildren) {
        treenode.children = node.children.map(child => convert(child, node));
      }
    }

    return treenode;
  }

  return PlacesUtils.promiseBookmarksTree(rootGuid, {
    excludeItemsCallback: item => {
      if (item.type == PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR) {
        return true;
      }
      return item.annos &&
             item.annos.find(a => a.name == PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO);
    },
  }).then(root => {
    if (onlyChildren) {
      let children = root.children || [];
      return children.map(child => convert(child, root));
    } else {
      // It seems like the array always just contains the root node.
      return [convert(root, null)];
    }
  });
}

function convert(result) {
  let node = {
    id: result.guid,
    title: result.title || "",
    index: result.index,
    dateAdded: result.dateAdded.getTime(),
  };

  if (result.guid != Bookmarks.rootGuid) {
    node.parentId = result.parentGuid;
  }

  if (result.type == Bookmarks.TYPE_BOOKMARK) {
    node.url = result.url.href; // Output is always URL object.
  } else {
    node.dateGroupModified = result.lastModified.getTime();
  }

  return node;
}

extensions.registerPrivilegedAPI("bookmarks", (extension, context) => {
  return {
    bookmarks: {
      get: function(idOrIdList, callback) {
        let list = Array.isArray(idOrIdList) ? idOrIdList : [idOrIdList];

        Task.spawn(function* () {
          let bookmarks = [];
          for (let id of list) {
            let bookmark;
            try {
              bookmark = yield Bookmarks.fetch({guid: id});
              if (!bookmark) {
                // TODO: set lastError, not found
                return [];
              }
            } catch (e) {
              // TODO: set lastError, probably an invalid guid
              return [];
            }
            bookmarks.push(convert(bookmark));
          }
          return bookmarks;
        }).then(results => runSafe(context, callback, results));
      },

      getChildren: function(id, callback) {
        // TODO: We should optimize this.
        getTree(id, true).then(result => {
          runSafe(context, callback, result);
        }, reason => {
          // TODO: Set lastError
          runSafe(context, callback, []);
        });
      },

      getTree: function(callback) {
        getTree(Bookmarks.rootGuid, false).then(result => {
          runSafe(context, callback, result);
        }, reason => {
          runSafe(context, callback, []);
        });
      },

      getSubTree: function(id, callback) {
        getTree(id, false).then(result => {
          runSafe(context, callback, result);
        }, reason => {
          runSafe(context, callback, []);
        });
      },

      // search

      create: function(bookmark, callback) {
        let info = {
          title: bookmark.title || "",
        };

        // If url is NULL or missing, it will be a folder.
        if ("url" in bookmark && bookmark.url !== null) {
          info.type = Bookmarks.TYPE_BOOKMARK;
          info.url = bookmark.url || "";
        } else {
          info.type = Bookmarks.TYPE_FOLDER;
        }

        if ("index" in bookmark) {
          info.index = bookmark.index;
        }

        if ("parentId" in bookmark) {
          info.parentGuid = bookmark.parentId;
        } else {
          info.parentGuid = Bookmarks.unfiledGuid;
        }

        let failure = reason => {
          // TODO: set lastError.
          if (callback) {
            runSafe(context, callback, null);
          }
        };

        try {
          Bookmarks.insert(info).then(result => {
            if (callback) {
              runSafe(context, callback, convert(result));
            }
          }, failure);
        } catch (e) {
          failure(e);
        }
      },

      move: function(id, destination, callback) {
        let info = {
          guid: id,
        };

        if ("parentId" in destination) {
          info.parentGuid = destination.parentId;
        }
        if ("index" in destination) {
          info.index = destination.index;
        }

        let failure = reason => {
          if (callback) {
            runSafe(context, callback, null);
          }
        };

        try {
          Bookmarks.update(info).then(result => {
            if (callback) {
              runSafe(context, callback, convert(result));
            }
          }, failure);
        } catch (e) {
          failure(e);
        }
      },

      update: function(id, changes, callback) {
        let info = {
          guid: id,
        };

        if ("title" in changes) {
          info.title = changes.title;
        }
        if ("url" in changes) {
          info.url = changes.url;
        }

        let failure = reason => {
          if (callback) {
            runSafe(context, callback, null);
          }
        };

        try {
          Bookmarks.update(info).then(result => {
            if (callback) {
              runSafe(context, callback, convert(result));
            }
          }, failure);
        } catch (e) {
          failure(e);
        }
      },

      remove: function(id, callback) {
        let info = {
          guid: id,
        };

        let failure = reason => {
          if (callback) {
            runSafe(context, callback, null);
          }
        };

        try {
          Bookmarks.remove(info).then(result => {
            if (callback) {
              // The API doesn't give you the old bookmark at the moment
              runSafe(context, callback);
            }
          }, failure);
        } catch (e) {
          failure(e);
        }
      },
    },
  };
});


