const EXPORTED_SYMBOLS = ["User"];
const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
function User(strUsername) {
this._name = strUsername.toLowerCase();
this._database = User._db;
}

;
const initPrefsQuery = "CREATE TABLE IF NOT EXISTS prefs (         name VARCHAR(255) UNIQUE,         value VARCHAR(255),         userName VARCHAR(255))";
const initBookmarksQuery = "CREATE TABLE IF NOT EXISTS bookmarks (        id INTEGER UNIQUE,         parentId INTEGER,         name VARCHAR(255),         type VARCHAR(20),         tags TEXT,         url VARCHAR(255),         desc TEXT,         userName VARCHAR(255),         FOREIGN KEY(parentId)         REFERENCES bookmarks(id))";
const initVersionsQuery = "CREATE TABLE IF NOT EXISTS versions (         tableName VARCHAR(50) UNIQUE,         version VARCHAR(255),         userName VARCHAR(255))";
User.init = function User_init(api) {
if (User._initialized)
return;
User._api = api;
var cacheFile = utils.contentDir;
cacheFile.append("users.sqlite");
var database = new Database(cacheFile);
database.execQuery(initPrefsQuery);
database.execQuery(initBookmarksQuery);
database.execQuery(initVersionsQuery);
database.addFunction("regexp",2,regExpFunc);
User.__defineGetter__("_db",function () database);
User._initialized = true;
}
;
User.prototype = {
get name() {
return this._name;
}
,
_SELECT_PREF_QUERY: "SELECT value FROM prefs WHERE name = :name AND userName = :userName",
_INSERT_PREF_QUERY: "INSERT INTO prefs (name, value, userName) VALUES (:name, :value, :userName)",
_UPDATE_PREF_QUERY: "UPDATE prefs SET value = :value WHERE name = :name AND userName = :userName",
savePref: function User_savePref(strName, value) {
var pref = this.selectPref(strName);
var query = typeof pref == "undefined" ? this._INSERT_PREF_QUERY : this._UPDATE_PREF_QUERY;
this._database.execQuery(query,{
name: this.name + "###" + strName,
value: "" + value,
userName: this.name});
}
,
selectPref: function User_selectPref(strName) {
var row = this._database.execQuery(this._SELECT_PREF_QUERY,{
name: this.name + "###" + strName,
userName: this.name})[0];
return row && row.value;
}
,
selectPrefInt: function User_selectPrefInt(strName) {
return parseInt(this.selectPref(strName),10) || 0;
}
,
_INSERT_BOOKMARK_QUERY: "INSERT INTO bookmarks (id, parentId, name, type, tags, url, desc, userName)                              VALUES (:id, :parentId, :name, :type, :tags, :url, :desc, :userName)",
_UPDATE_BOOKMARKS_VERSION: "INSERT OR REPLACE INTO versions (tableName, version, userName)                                 VALUES('bookmarks', :version, :userName)",
insertBookmarksTree: function User_insertBookmarksTree(tree) {
this._cleanTable("bookmarks");
this._database.execQuery(this._INSERT_BOOKMARK_QUERY,{
id: tree.id,
parentId: null,
name: tree.name,
type: tree.type,
tags: "",
url: tree.url,
desc: tree.desc,
userName: this.name});
this._insertBookmarksRcr(tree);
this._database.execQuery(this._UPDATE_BOOKMARKS_VERSION,{
version: Date.now(),
userName: this.name});
}
,
insertBookmarksTreeOpt: function User_insertBookmarksTreeOpt(tree, callback) {
this._cleanTable("bookmarks");
var arrQuery = [this._generateBookmarksSelectForInsertQuery(tree.id,null,tree.name,tree.type,null,tree.url,tree.desc,this.name)];
this._generateBookmarksSelectForInsertQueryRcr(tree,arrQuery);
var numberOfQueries = Math.ceil(arrQuery.length / 200) >> 0;
var counter = 0;
var finalWrapper = (function () {
counter++;
if (counter == numberOfQueries)
{
this._database.execQuery(this._UPDATE_BOOKMARKS_VERSION,{
version: Date.now(),
userName: this.name});
if (callback)
callback();
}

this._log("finalWrapper:" + counter);
}
).bind(this);
for (let i = 0;i < numberOfQueries;i++) {
let arrQueries = arrQuery.slice(i * 200,Math.min((i + 1) * 200,arrQuery.length));
this._database.execQueryAsync("INSERT OR REPLACE INTO bookmarks " + arrQueries.join(" UNION "),null,finalWrapper);
}

}
,
_generateBookmarksSelectForInsertQueryRcr: function User__generateBookmarksSelectForInsertQueryRcr(bookmark, arrQuery) {
bookmark.children.forEach((function (child) {
arrQuery.push(this._generateBookmarksSelectForInsertQuery(child.id,bookmark.id,child.name,child.type,child.tags,child.url,child.desc,this.name));
if (child.type == "folder")
{
this._generateBookmarksSelectForInsertQueryRcr(child,arrQuery);
}

}
).bind(this));
}
,
_generateBookmarksSelectForInsertQuery: function User__generateBookmarksSelectForInsertQuery(id, parentId, name, type, tags, url, desc, userName) {
return "SELECT " + id + " as 'id'," + (parentId || "NULL") + " as 'parentId'," + (name ? "'" + name.replace(/'/g,"''") + "'" : "NULL") + " as 'name','" + type + "' as 'type'," + (tags ? "'" + tags.replace(/'/g,"''") + "'" : "NULL") + " as 'tags'," + (url ? "'" + url.replace(/'/g,"''") + "'" : "NULL") + " as 'url'," + (desc ? "'" + desc.replace(/'/g,"''") + "'" : "NULL") + " as 'desc','" + userName + "' as 'userName'";
}
,
_SELECT_BOOKMARKS_VERSION: "SELECT version FROM versions where tableName = 'bookmarks' AND userName = :userName",
getBookmarkTreeVersion: function User_getBookmarkTreeVersion() {
try {
let rows = this._database.execQuery(this._SELECT_BOOKMARKS_VERSION,{
userName: this.name});
if (rows.length)
{
return parseInt(rows[0].version,10);
}

return 0;
}
catch (ex) {
this._log(utils.formatError(ex));
}

}
,
_insertBookmarksRcr: function User__insertBookmarksRcr(bookmark, async) {
var children = bookmark.children;
var executor = async ? this._database.execQueryAsync : this._database.execQuery;
children.forEach((function (child) {
executor.apply(this,this._INSERT_BOOKMARK_QUERY,{
id: child.id,
parentId: bookmark.id,
name: child.name,
type: child.type,
tags: child.tags,
url: child.url,
desc: child.desc,
userName: this.name});
if (child.type == "folder")
{
this._insertBookmarksRcr(child);
}

}
).bind(this));
}
,
_SELECT_TREE_QUERY: "SELECT * FROM bookmarks WHERE parentId = :parentId AND userName = :userName",
selectBookmarkTree: function User_selectBookmarkTree(folderId) {
try {
let rows = this._database.execQuery(this._SELECT_TREE_QUERY,{
parentId: folderId,
userName: this.name});
let children = [];
for each(let row in rows) children.push(this._fillBookmark(row));
return children;
}
catch (ex) {
this._log(utils.formatError(ex));
}

return null;
}
,
_SELECT_FOLDERS: "SELECT * FROM bookmarks WHERE type = 'folder' AND userName = :userName",
selectFolders: function User_selectFolders() {
try {
let rows = this._database.execQuery(this._SELECT_FOLDERS,{
userName: this.name});
let children = [];
for each(let row in rows) children.push(this._fillBookmark(row));
return children;
}
catch (ex) {
this._log(utils.formatError(ex));
}

return null;
}
,
_SELECT_BOOKMARK: "SELECT * FROM bookmarks WHERE id = :id AND userName  = :userName",
selectBookmark: function User_selectBookmark(id) {
try {
let rows = this._database.execQuery(this._SELECT_BOOKMARK,{
id: parseInt(id,10),
userName: this.name});
let bookmark = null;
if (rows.length)
return this._fillBookmark(rows[0]);
return bookmark;
}
catch (ex) {
this._log(utils.formatError(ex));
}

return null;
}
,
_SELECT_BOOKMARK_BY_URL: "SELECT * FROM bookmarks WHERE regexp(url, '@REGEXP') AND userName = :userName",
selectBookmarkByURL: function User_selectBookmarkByURL(strURL) {
try {
let regExp = strURL.replace(/([.*+?^=!:${}()|[\]\/\\])/g,"\\$1").replace(/(https?\\:\\\/\\\/)(www\\.)?/,"$1(www\\.)?");
let query = this._SELECT_BOOKMARK_BY_URL.replace("@REGEXP",regExp);
let rows = this._database.execQuery(query,{
userName: this.name});
let bookmark = null;
if (rows.length)
bookmark = this._fillBookmark(rows[0]);
return bookmark;
}
catch (ex) {
this._log(utils.formatError(ex));
}

return null;
}
,
_fillBookmark: function User__fillBookmark(data) {
return {
id: data.id,
parentId: data.parentId,
name: data.name,
type: data.type,
tags: data.tags,
url: data.url,
desc: data.desc};
}
,
_UPDATE_BOOKMARK_QUERY: "UPDATE bookmarks                              SET name = :name, tags = :tags, type = :type, url = :url, desc = :desc                               WHERE id = :id AND userName = :userName",
saveBookmark: function User_saveBookmark(bookmark) {
try {
if (this.selectBookmark(bookmark.id))
{
this._database.execQuery(this._UPDATE_BOOKMARK_QUERY,{
name: bookmark.name,
tags: bookmark.tags,
type: bookmark.type,
url: bookmark.url,
desc: bookmark.desc,
id: bookmark.id,
userName: this.name});
}
 else
{
this._database.execQuery(this._INSERT_BOOKMARK_QUERY,{
parentId: bookmark.parentId,
id: bookmark.id,
name: bookmark.name,
type: bookmark.type,
tags: bookmark.tags,
url: bookmark.url,
desc: bookmark.desc,
userName: this.name});
}

return true;
}
catch (ex) {
this._log(utils.formatError(ex));
}

}
,
_MOVE_BOOKMARK_QUERY: "UPDATE bookmarks SET parentId = :parentId WHERE id = :id AND userName = :userName",
moveBookmark: function User_moveBookmark(bookmark) {
try {
if (this.selectBookmark(bookmark.id))
{
this._database.execQuery(this._MOVE_BOOKMARK_QUERY,{
parentId: bookmark.parentId,
id: bookmark.id,
userName: this.name});
}

return true;
}
catch (ex) {
this._log(utils.formatError(ex));
}

}
,
_DELETE_REDUNDANT_FOLDERS_QUERY: "DELETE FROM bookmarks                                       WHERE (type = 'folder' AND NOT id IN (:ids))                                       OR (type = 'link' AND NOT parentId in (:ids))",
updateFolders: function User_updateFolders(folders) {
var ids = [folder.id for each (folder in folders)].concat(0);
this._log("updateFolders:" + ids.join(","));
try {
this._database.execQuery(this._DELETE_REDUNDANT_FOLDERS_QUERY.replace(":ids",ids.join(",")));
let numberOfQueries = Math.ceil(folders.length / 200) >> 0;
let arrQuery = [];
folders.forEach((function (folder) {
arrQuery.push(this._generateBookmarksSelectForInsertQuery(folder.id,folder.parentId,folder.name,folder.type,folder.tags,folder.url,folder.desc,this.name));
}
).bind(this));
for (let i = 0;i < numberOfQueries;i++) {
let arrQueries = arrQuery.slice(i * 200,Math.min((i + 1) * 200,arrQuery.length));
this._database.execQueryAsync("INSERT OR REPLACE INTO bookmarks " + arrQueries.join(" UNION "),null);
}

return true;
}
catch (ex) {
this._log(utils.formatError(ex));
}

}
,
_DELETE_BOOKMARK_QUERY: "DELETE FROM bookmarks WHERE id = :id AND userName = :userName",
deleteBookmark: function User_deleteBookmark(id) {
try {
this._database.execQuery(this._DELETE_BOOKMARK_QUERY,{
id: parseInt(id,10),
userName: this.name});
return true;
}
catch (ex) {
this._log(utils.formatError(ex));
}

}
,
_SELECT_BOOKMARKS_TAGS_QUERY: "SELECT tags FROM bookmarks WHERE userName = :userName",
selectBookmarksTags: function User_selectBookmarksTags() {
try {
let rows = this._database.execQuery(this._SELECT_BOOKMARKS_TAGS_QUERY,{
userName: this.name});
let arrTags = [];
for each(let row in rows) {
if (row.tags)
{
arrTags.push(row.tags.replace(/,$/,""));
}

}

return arrTags.join(",");
}
catch (ex) {
this._log(utils.formatError(ex));
}

return null;
}
,
_DELETE_FROM_TABLE_QUERY: "DELETE FROM @TABLE WHERE userName = :userName",
_cleanTable: function User__cleanTable(strTable) {
var preparedQuery = this._DELETE_FROM_TABLE_QUERY.replace("@TABLE",strTable);
this._database.execQuery(preparedQuery,{
userName: this.name});
}
,
_log: function User__log(msg) {
User._api.logger.debug(msg);
}
};
function Database(storageFile, initStatement) {
if (storageFile)
this.open(storageFile,initStatement);
}

;
Database.prototype = {
open: function Database_open(storageFile, initStatement) {
if (! (storageFile instanceof Ci.nsILocalFile))
throw new TypeError("nsILocalFile required");
this.close();
try {
this._connectAndInit(storageFile,initStatement);
}
catch (e) {
Cu.reportError(utils.formatError(e));
if (this._connection)
{
this._connection.close();
}

let journalFile = storageFile.parent;
journalFile.append(storageFile.leafName + "-journal");
utils.removeFileSafe(journalFile);
switch (e.result) {
case Cr.NS_ERROR_FILE_CORRUPTED:

case Cr.NS_ERROR_STORAGE_IOERR:
Cu.reportError("Database: will try to remove corrupt DB file");
storageFile.remove(false);
Cu.reportError("Database: corrupt DB file removed");
}

this._connectAndInit(storageFile,initStatement);
}

this._storageFile = storageFile;
}
,
close: function Database_close(callback) {
if (this._connection)
{
if ("asyncClose" in this._connection)
this._connection.asyncClose(callback ? {
complete: function Database_closeComplete() {
callback();
}
} : undefined); else
{
this._connection.close();
if (callback)
callback();
}

this._connection = null;
}

}
,
execQuery: function Database_execQuery(query, parameters) {
var result = [];
var statement = this._createStatement(query,parameters);
try {
let colNames = [];
for (let i = statement.columnCount;i--;) colNames.push(statement.getColumnName(i));
while (statement.executeStep()) {
let row = {
};
for (let i = colNames.length;i--;) {
let colName = colNames[i];
row[colName] = statement.row[colName];
}

result.push(row);
}

}
 finally {
statement.reset();
statement.finalize();
}

return result;
}
,
execSimpleQuery: function Database_execSimpleQuery(query, parameters) {
var firstResult = this.execQuery(query,parameters)[0];
if (firstResult)
for(let p in firstResult) if (firstResult.hasOwnProperty(p))
return firstResult[p];
return;
}
,
execQueryAsync: function Database_execAsync(query, parameters, onCompletion) {
if (onCompletion && typeof onCompletion != "function")
throw new TypeError("Third argument must be a function.");
var statement = this._createStatement(query,parameters,true);
try {
return statement.executeAsync(this._createStmtCallback(statement,onCompletion));
}
 finally {
statement.reset();
statement.finalize();
}

}
,
addFunction: function Database_addFunction(name, nParams, utilFunc) {
if (! this._connection)
throw new Error("Can't add function. Database is closed.");
this._connection.createFunction(name,nParams,utilFunc);
}
,
_connection: null,
_connectAndInit: function Database__connectAndInit(storageFile, initStatement) {
this._connection = this._storageService.openDatabase(storageFile);
if (! this._connection.connectionReady)
throw new Error(this._connection.lastError);
if (initStatement)
this.execQuery(initStatement);
}
,
_createStmtCallback: function Database__createStmtCallback(statement, onCompletion) {
var colNames = [];
for (let i = 0, len = statement.columnCount;i < len;i++) colNames.push(statement.getColumnName(i));
var numNames = colNames.length;
return {
_result: [],
_error: undefined,
handleResult: function Database_callback_handleResult(aResultSet) {
var row;
while (row = aResultSet.getNextRow()) {
let resultRow = {
};
for (let nameIdx = 0;nameIdx < numNames;nameIdx++) {
let colName = colNames[nameIdx];
resultRow[colName] = row.getResultByName(colName);
}

this._result.push(resultRow);
}

}
,
handleError: function Database_callback_handleError(aError) {
this._error = aError;
}
,
handleCompletion: function Database_callback_handleCompletion(aReason) {
if (aReason == Ci.mozIStorageStatementCallback.REASON_CANCELED)
return;
if (onCompletion)
onCompletion(this._result,this._error);
}
};
}
,
_createStatement: function Database__createStatement(query, parameters, asyncStatement) {
if (! this._connection)
throw new Error("Can't create statement. Database is closed.");
var statement = this._connection.createStatement(query);
if (parameters)
{
for(let name in parameters) statement.params[name] = parameters[name];
}

return statement;
}
,
get _storageService() {
var storageSvc = Cc["@mozilla.org/storage/service;1"].getService(Ci.mozIStorageService);
this.__defineGetter__("_storageService",function Database__getStorageService() storageSvc);
return storageSvc;
}
};
const regExpFunc = {
onFunctionCall: function (args) {
var url = args.getString(0);
var expr = new RegExp(args.getString(1));
var match = expr.exec(url);
if (match)
{
let endOfUrl = url.replace(match[0],"");
if (! endOfUrl || (/^\/$/.test(endOfUrl) || /^\/?#/.test(endOfUrl)))
return true;
}

return false;
}
};
const utils = {
formatError: function utils_formatError(e) {
if (! (e instanceof Ci.nsIException))
{
if (typeof e !== "object" || ! ("name" in e) || ! ("message" in e))
{
return String(e);
}

}

var text = e.name + ": " + e.message;
var fileName = e.fileName || e.filename;
if (fileName)
text += "\nin " + fileName + "@" + e.lineNumber;
return text;
}
,
removeFileSafe: function FileUtils_removeFileSafe(file) {
if (! file.exists())
return;
file = file.clone();
try {
file.remove(true);
if (! file.exists())
return;
}
catch (e) {
Cu.reportError("Can not remove file [" + file.path + "] \n" + e);
}

var trash = Cc["@mozilla.org/file/directory_service;1"].getService(Ci.nsIProperties).get("TmpD",Ci.nsIFile);
trash.append("trash");
trash.createUnique(Ci.nsIFile.DIRECTORY_TYPE,parseInt("0755",8));
try {
file.moveTo(trash,file.leafName);
}
catch (e) {
try {
file.remove(true);
}
catch (e) {
Cu.reportError(this.formatError(e));
}

}

try {
trash.remove(true);
}
catch (e) {
Cu.reportError(this.formatError(e));
}

}
,
get contentDir() {
var contentDir = Cc["@mozilla.org/file/directory_service;1"].getService(Ci.nsIProperties).get("ProfD",Ci.nsIFile);
contentDir.append("yandex");
if (! contentDir.exists())
{
contentDir.create(Ci.nsIFile.DIRECTORY_TYPE,parseInt("0755",8));
}

delete this.contentDir;
this.__defineGetter__("contentDir",function () contentDir);
return contentDir;
}
};
