Components.utils.import("resource://yoono/yoonoStorageAsync.js");

var EXPORTED_SYMBOLS = ["YOONO_STORAGE"];

// Globals
const CI = Components.interfaces;
const CL = Components.classes;

/////
const YOONO_DIRSERVICE = Components.classes['@mozilla.org/file/directory_service;1'].getService(Components.interfaces.nsIProperties);
const CONSOLESERVICE = CL["@mozilla.org/consoleservice;1"].getService(CI.nsIConsoleService);

const PERMS_FILE      = 0655;
/////

var yoono = {};

function YoonoStorage() {
  this.dataBase = null;
  this.mainThread = Components.classes["@mozilla.org/thread-manager;1"].getService().mainThread;
  this.backgroundThread = Components.classes["@mozilla.org/thread-manager;1"].getService().newThread(0);
  this.dbFileName = "";
  this.dbLastBackupFileName = ""; // slightly useless so far
  this.lowPriorityRequestQueue = [] ; // so that we can handle priorities (chat request must not wait)
  this.highPriorityRequestQueue = [] ; // so that we can handle priorities (chat request must not wait)
  this.requestIsRunning = false;
  this._debugDb = false;
}

YoonoStorage.prototype.init = function (y) {
  yoono = y;
}

/**
 *
 * @param aStatement string for the statement, with placeholders ?1, ?2, etc
 * @param aValues array of arrays , each holding the values to bind to the placeholders for one execution of the statement
 */
YoonoStorage.prototype.executeSql = function(aStatement, aDataSet, aPriority) {
  // Init the database the first time it's needed
  var statusObject = {};
  var statement = null;
  statusObject.errorString = "";
  statusObject.errorCode = 0;
   if(!this.dataBase) {
    this.openStorage();   // is that a good idea ?
  }
  if(!this.dataBase) {
    statusObject.errorString = "Unable to open the database file";
    statusObject.errorCode = 14;
    yoono.log.error("Unable to execute : "+aStatement+"\nbecause DB is not open!");
    return [statusObject];
  }
  try {
    statement = this.dataBase.createStatement(aStatement);
  } catch(e) {
    try {
      statusObject.errorString = this.dataBase.lastErrorString;
      statusObject.errorCode = this.dataBase.lastError;
    } catch(e1) {
      statusObject.errorString = e1.message || e1;
      statusObject.errorCode = -1;
    }
    yoono.log.error("Bad SQL query : "+aStatement+"\nbecause of : "+statusObject.errorString+" (" + statusObject.errorCode + ")\n");
    return [statusObject];
  }
  if(!aDataSet) aDataSet = [[]]; // all statements don't need values... But they need to be executed at least once...
  var nbDataSet = aDataSet.length; // always at least 1
  // The VACUUM statement can't be performed from within a transaction
  if('vacuum' != aStatement) {
    try {
      this.dataBase.beginTransaction(); // will fail if a transaction is already running
    } catch(e) {
      if(0 != this.dataBase.lastError) {
        statusObject.errorString = this.dataBase.lastErrorString;
        statusObject.errorCode = this.dataBase.lastError;
      } else {
        statusObject.errorString = e.message;
        statusObject.errorCode = -1;
      }
      yoono.log.error("Unable to start sql transaction : "+e);
      return [statusObject];
    }
  }

  var resultSets = [];
  var rollback = false;
  var startTime = 0, endTime = 0;

  if (this._debugDb) {
    startTime = new Date().getTime();
    yoono.log.debug("Executing SQL " + (aPriority?' high priority ':'') + nbDataSet + " times the statement : " + aStatement );
//    if(aDataSet && (1 == aDataSet.length)) {
//      yoono.log.debug("Executing SQL data " + aDataSet[0].toSource());
//    }
  }
  /*
  var lowerCaseStatement = aStatement.toLowerCase();
  var oneStepStatement = (0 == lowerCaseStatement.indexOf('insert'))
                         || (0 == lowerCaseStatement.indexOf('delete'))
                         || (0 == lowerCaseStatement.indexOf('update'))
  */
  try {
    for(var idx = 0 ; idx < nbDataSet; idx ++) {
      var resultSet = [];
      // Number of data in this dataset = number of placeholders to bind
      var nbData = aDataSet[idx].length;
      if (nbData != statement.parameterCount) {
        yoono.log.error("Warning : this SQL query -> <<"+aStatement+">> does'nt get his placeholders correctly set. "+nbData+" instead of "+statement.parameterCount+"!");
      }
      // set the values for place holders, if any

      for(var jdx = 0 ; jdx < nbData ; jdx ++) {
        var aData = aDataSet[idx][jdx];
        if(null == aData) {
          statement.bindNullParameter(jdx);
        } else {
          statement.bindUTF8StringParameter(jdx, aData);
        }
      }
      // Execute the statement
      var notDone = true;
      do {
        try {
          notDone = statement.executeStep();
        } catch (e) {
          statusObject.errorString = this.dataBase.lastErrorString;
          statusObject.errorCode = this.dataBase.lastError;
          yoono.log.error("Error on executeStep for SQL query : "+aStatement+"\niteration " + idx + " of " + nbDataSet + " because of : "
            + statusObject.errorString
            + " (" + statusObject.errorCode + ")\nor because of : "+e+"\n"
            + "Data : " + aDataSet[idx].join(' , '));
          break;
        }
        if(!notDone) break;
        var result = []; // one line of result
        var column = null;

        for(var offset = 0; offset < statement.columnCount; offset++) {
          column = statement.getUTF8String(offset);
          result.push(column);
        }
        // If no results, do not put an empty array in the results
        if(statement.columnCount)
          resultSet.push(result);
        // if(oneStepStatement) notDone = false;
      }
      while(notDone);

      resultSets.push(resultSet);
      statement.reset();
    }
  } catch(e) {
    if(0 != this.dataBase.lastError) {
      statusObject.errorString = this.dataBase.lastErrorString;
      statusObject.errorCode = this.dataBase.lastError;
    } else {
      statusObject.errorString = e.message;
      statusObject.errorCode = -1;
    }
    yoono.log.error("Unable to execute SQL query : "+aStatement+"\niteration " + idx + " because of : "
            + this.dataBase.lastErrorString
            + " (" + this.dataBase.lastError + ")\nor because of : "+e+"\n");

    yoono.log.error("Data offset " + idx + " on " + nbData + " : " + aDataSet[idx].join(' , '));
  }

  try {
    statement.finalize();
  } catch(e) {
    yoono.log.error("Unable to execute finalize for statement : "+aStatement+"\nbecause of : "
            + this.dataBase.lastErrorString
            + " (" + this.dataBase.lastError + ")\nor because of : "+e+"\n");

    if(0 != this.dataBase.lastError) {
      statusObject.errorString = this.dataBase.lastErrorString;
      statusObject.errorCode = this.dataBase.lastError;
    } else {
      statusObject.errorString = e.message;
      statusObject.errorCode = -1;
    }
  }
  // No transaction to commit or rollback if running a vacuum statement
  if('vacuum' != aStatement) {
    try {
      if(rollback) {
        this.dataBase.rollbackTransaction();
      } else {
        this.dataBase.commitTransaction();
      }
      statusObject.lastInsertRowID = this.dataBase.lastInsertRowID ;
    } catch(e) {
      yoono.log.error("Unable to execute rollback or commit for statement : "+aStatement+"\nbecause of : "
              + this.dataBase.lastErrorString
              + " (" + this.dataBase.lastError + ")\nor because of : "+e+"\n");

      if(0 != this.dataBase.lastError) {
        statusObject.errorString = this.dataBase.lastErrorString;
        statusObject.errorCode = this.dataBase.lastError;
      } else {
        statusObject.errorString = e.message;
        statusObject.errorCode = -1;
      }
    }
  }
  if (this._debugDb) {
    endTime = new Date().getTime();
    yoono.log.debug("Executing SQL took : "+ (endTime - startTime)+"\n");
  }
  resultSets.unshift(statusObject)
  return(resultSets);
}

/**
 *
 * @param aStatement
 * @param aDataSet
 * @param aCallback : object with property 'callback' holding the actuall callback function (that's because of IE)
 */
YoonoStorage.prototype.executeAsyncSql = function(aStatement, aDataSet, aCallback, aHighPriority) {
  var req = {
    'statement' : aStatement,
    'dataSet' : aDataSet,
    'callback' : aCallback,
    'priority' : aHighPriority
  };

  if(aHighPriority) {
    this.highPriorityRequestQueue.push(req);
  } else {
    this.lowPriorityRequestQueue.push(req);
  }

  this.processNextInQueue();

}

function YoonoStorageWorkingThread(statement, dataSet, priority, storage, callback) {
  this.statement = statement;
  this.dataSet = dataSet;
  this.priority = priority;
  this.storage = storage;
  this.callback = callback;
}

YoonoStorageWorkingThread.prototype = {
  run: function() {
    var results = this.storage.executeSql(this.statement, this.dataSet, this.priority);
    this.storage.mainThread.dispatch(new YoonoStorageMainThread(this.storage, this.callback, results), this.storage.mainThread.DISPATCH_NORMAL);
  },

  QueryInterface: function(iid) {
    if (iid.equals(Components.interfaces.nsIRunnable) ||
        iid.equals(Components.interfaces.nsISupports)) {
            return this;
    }
    throw Components.results.NS_ERROR_NO_INTERFACE;
  }
}

function YoonoStorageMainThread(storage, callback, result) {
  this.storage = storage;
  this.callback = callback;
  this.result = result;
}

YoonoStorageMainThread.prototype = {
  run: function() {
    this.storage.processNextInQueue()
    try {
      if (this.callback && (typeof this.callback.callback == "function")) {
        this.callback.callback(this.result);
      }
    } catch(e) {
      yoono.log.exception(e);
    }
  },

  QueryInterface: function(iid) {
    if (iid.equals(Components.interfaces.nsIRunnable) ||
        iid.equals(Components.interfaces.nsISupports)) {
            return this;
    }
    throw Components.results.NS_ERROR_NO_INTERFACE;
  }
}

YoonoStorage.prototype.executeThreadSql = function(aReq) {
  var statement = aReq.statement;
  var dataSet = aReq.dataSet;
  var callback = aReq.callback;
  var priority = aReq.priority; // for logging purposes
  var storage = this;
  this._debugDb = yoono.prefs.get("debug.db");
  storage.backgroundThread.dispatch( new YoonoStorageWorkingThread(statement, dataSet, priority, storage, callback), storage.backgroundThread.DISPATCH_NORMAL);
  // In case no-thread testing is required, comment above and uncomment below :
//  var r = storage.executeSql(statement, dataSet, priority);
//  if (callback && (typeof callback.callback == "function")) {
//    callback.callback(r);
//  }


}

YoonoStorage.prototype.processNextInQueue = function() {
  var req = null;
  if(this.highPriorityRequestQueue.length) {
    if (this._debugDb) {
      yoono.log.debug("IN HIGH PRIORITY QUEUE : " + this.highPriorityRequestQueue.length);
    }
    req = this.highPriorityRequestQueue.shift();
  } else {
    if (this._debugDb) {
      yoono.log.debug("IN NORMAL PRIORITY QUEUE : " + this.lowPriorityRequestQueue.length);
    }
    req = this.lowPriorityRequestQueue.shift();
  }
  if(req) {
    this.executeThreadSql(req);
  }
}

YoonoStorage.prototype.closeStorage = function() {
  if (!this.dataBase)
    return;
  this.dataBase.close();
  this.dataBase = null;
}

/**
 * Backup the DB before a schema migration.
 * If backup already exists, it's not overwritten
 * @param aVersion
 */
YoonoStorage.prototype.backupDB = function(aVersion) {
  var storageService = CL["@mozilla.org/storage/service;1"].getService(CI.mozIStorageService);

  var dbFile = this.getDBFile(); // Make sure this.dbFileName is initialized
  var newNameElts = this.dbFileName.split('.');
  newNameElts[0] += '_' + aVersion + '.';
  var newFile = storageService.backupDatabaseFile(dbFile, newNameElts.join(''));
  this.dbLastBackupFileName = newFile.leafName;
  return(this.dbLastBackupFileName);
}

YoonoStorage.prototype.openStorage = function() {
  if (this.dataBase)
    return;
  var dbFile = this.getDBFile();
  if(!dbFile.exists()) {
    this.createAndOpenStorage(dbFile);
  } else {
    this.openDBStorage(dbFile);
  }
}

YoonoStorage.prototype.getCurrentDBFileName = function() {
  return(this.dbFileName);
}

YoonoStorage.prototype.getDBFile = function() {
  var dbFile = YOONO_DIRSERVICE.get("ProfDS", CI.nsIFile)
  dbFile.append('yoono');
  var userCredentials = yoono.main.getUserCredential();
  var dbName = 'yoono_friends_db.sqlite';
  if(userCredentials) {
    if(userCredentials.anonymous) {
      dbName = userCredentials.userId + '_friends_db.sqlite';
    } else {
      dbName = yoono.main.escapeFileName(userCredentials.login) + '_friends_db.sqlite';
    }
  }
  dbFile.append(dbName);
  // Fix some weird bug in yoono desktop 6.2 -> 7 migration
  if (dbFile.exists() && dbFile.isDirectory())
    dbFile.remove(true);
  this.dbFileName = dbName;
  return(dbFile);
}

/**
 * This method closes the database, renames the current file to the new file, and reopens the database
 */
YoonoStorage.prototype.switchDBStorage = function() {
  this.closeStorage();
  var currentFileName = this.getCurrentDBFileName();
  var filePath = YOONO_DIRSERVICE.get("ProfDS", CI.nsIFile)
  filePath.append('yoono');
  var currentFile = filePath.clone();
  var newFilePath = filePath.clone();
  currentFile.append(currentFileName);
  var aFile = this.getDBFile();
  var newFileName = this.getCurrentDBFileName(); // was updated by getDBFile()
  newFilePath.append(newFileName);
  if(newFilePath.exists()) {
    newFilePath.remove(false);
  }
  if(!currentFileName) {
    this.createAndOpenStorage(newFilePath);
  } else {
    currentFile.moveTo(filePath, newFileName);
    this.openDBStorage(aFile);
  }
}

YoonoStorage.prototype.openDBStorage = function(aFile) {
  var storageService = CL["@mozilla.org/storage/service;1"].getService(CI.mozIStorageService);
  // Open sqlite DB in unshared mode for FTS3
  try {
    this.dataBase = storageService.openUnsharedDatabase(aFile);
  } catch(e) {
    yoono.log.exception(e);
  }
}

YoonoStorage.prototype.createAndOpenStorage = function(aFile) {
  aFile.create(CI.nsIFile.NORMAL_FILE_TYPE, PERMS_FILE);
  this.openDBStorage(aFile);
}

var YOONO_STORAGE = null;

// IF Firefox 3...
// TODO : use a better test !
if("@mozilla.org/extensions/manager;1" in Components.classes) {
  CONSOLESERVICE.logStringMessage("Using thread API (FF3)");
  YOONO_STORAGE = new YoonoStorage();
} else {
  CONSOLESERVICE.logStringMessage("Using Async DB API (FF4)");
  YOONO_STORAGE = new YoonoStorageAsync();
}
