var EXPORTED_SYMBOLS = ["YOONO_KEYVALUEDB"];

Components.utils.import("resource://yoono/yoonoLog.js");
var log = YOONO_LOG;

const YOONO_DIR       = "yoono";
const YOONO_DB_FILE   = "anonUser.sqlite";  // new user nb name

const PERMS_FILE      = 0655;

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

const DIRSERVICE = CL['@mozilla.org/file/directory_service;1'].getService(CI.nsIProperties);


const YOONO_KEYVALUEDB = {};

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

YOONO_KEYVALUEDB.getDBFileName = function() {
  var dbFileName = this.loginToFileName() + '.sqlite';
  return(dbFileName);
};

YOONO_KEYVALUEDB.loginToFileName = function() {
  Components.utils.import("resource://yoono/yoonoService.js"); // strangely needed here too
  var fileName = 'anonUser';
  var userData = YOONO_CMPT.getUserCredential(true); // true to not get the password (since it's in the DB!)
  if(userData && !userData.anonymous) {
    fileName = YOONO_CMPT.escapeFileName(userData.login);
  }
  return(fileName);
}

/**
 * This method closes the database, renames the current file to the new file, and reopens the database
 */
YOONO_KEYVALUEDB.switchDB = function() {
  this.closeDB();
  var currentFileName = this.getDBFileName();
  var filePath = YOONO_DIRSERVICE.get("ProfDS", CI.nsIFile)
  filePath.append('yoono');
  var currentFile = filePath.clone();
  var newFilePath = filePath.clone();
  currentFile.append(currentFileName);
  var newFileName = this.getDBFileName();
  newFilePath.append(newFileName);
  if(newFilePath.exists()) {
    newFilePath.remove(false);
  }  
  currentFile.moveTo(filePath, newFileName);
  // DB will be reopenned when it's needed
}

// this is called only if this DB is needed
YOONO_KEYVALUEDB.initDB = function() {
  try {
    if(this.mDBConn) return;
    log.debug("Initializing DB");
    var dbFile = DIRSERVICE.get("ProfDS", CI.nsIFile)
    dbFile.append(YOONO_DIR);
    var userDbFile = dbFile.clone();
    dbFile.append(YOONO_DB_FILE);
    this.dbFileName = this.getDBFileName();
    userDbFile.append(this.dbFileName);

    // If default anonUser.sqlite file exists and no username.sqlite file exists
    // rename the former to the latter
    if(dbFile.exists() && !userDbFile.exists()) {
      dbFile.moveTo(null, this.dbFileName);
    }

    dbFile = DIRSERVICE.get("ProfDS", CI.nsIFile)
    dbFile.append(YOONO_DIR);
    dbFile.append(this.dbFileName);

    // database not yet created : create it !
    var creation = false;
    if(!dbFile.exists()) {
      dbFile.create(CI.nsIFile.NORMAL_FILE_TYPE, PERMS_FILE);
      creation = true;
    }
    var storageService = CL["@mozilla.org/storage/service;1"].getService(CI.mozIStorageService);
    this.mDBConn = storageService.openDatabase(dbFile);
    // statements will be initialized the first time they are needed... they might never be...
    this.getKeyValueStatement = null;
    this.setKeyValueStatement = null;
    this.deleteKeyValueStatement = null;
    this.getKeyValueListStatement = null;
    if(creation) {
      this.mDBConn.executeSimpleSQL("CREATE TABLE keyvalues (key STRING PRIMARY KEY, value STRING, expire INTEGER)");
    }
  } catch(e) {
    log.exception(e);
    this.mDBConn = null;
  }
};

YOONO_KEYVALUEDB.closeDB = function() {
  try {
    if(!this.mDBConn) return;

    log.debug("Closing DB");
    // Since Gecko 1.9M9, must call finalize on statements before closing 
    if((null != this.getKeyValueStatement) && ('finalize' in this.getKeyValueStatement))
      this.getKeyValueStatement.finalize();
    if((null != this.setKeyValueStatement) && ('finalize' in this.setKeyValueStatement))
      this.setKeyValueStatement.finalize();
    if((null != this.deleteKeyValueStatement) && ('finalize' in this.deleteKeyValueStatement))
      this.deleteKeyValueStatement.finalize();
    if((null != this.getKeyValueListStatement) && ('finalize' in this.getKeyValueListStatement))
      this.getKeyValueListStatement.finalize();

    this.getKeyValueStatement = null;
    this.setKeyValueStatement = null;
    this.deleteKeyValueStatement = null;
    this.getKeyValueListStatement = null;
    
    // Commit current transaction
    if (this.commitTimer)
      this.commitTimer.stop();
    
    // Since Gecko 1.9M8, must call close on db
    if('close' in this.mDBConn)
      this.mDBConn.close();
    
    log.debug("DB Closed!");
    
    this.mDBConn = null;
  } catch(e) {
    log.exception(e);
  }
}

YOONO_KEYVALUEDB.getKeyValue = function(key) {
  var value = null;
  var expire = 0;
  try {
    this.initDB();
    if(!this.mDBConn) return value;
    if(!this.getKeyValueStatement) {
      this.getKeyValueStatement = this.mDBConn.createStatement("SELECT value, expire FROM keyvalues where key = ?1");
    }
    this.getKeyValueStatement.bindUTF8StringParameter(0, key);
    var now = Math.floor((new Date().getTime())/1000);
    // there should be only one...
    while (this.getKeyValueStatement.executeStep()) {
      value = this.getKeyValueStatement.getUTF8String(0);
      expire = this.getKeyValueStatement.getInt32(1);
    }
  } catch(e) {
    log.exception(e);
  } finally {
    this.getKeyValueStatement.reset();
  }
  if((expire != 0) && (expire <= now)) {
    log.debug('Deleting expired key : ' + key + '=' + value);
    this.deleteKeyValue(key);
    value = null;
  } else {
    /*
    var logVal = value;
    if(value && (value.length>500)) {
      logVal = value.substr(0, 230) + ' [... VALUE TOO LARGE TO BE LOGGED ...] ' + value.substr(value.length - 250);
    }
    log.debug('Key found : ' + key + ' valid till ' + expire + ', value : ' + logVal);
    */
  }
  return(value);

};

YOONO_KEYVALUEDB.deleteKeyValue = function(key) {
  try {
    this.initDB();
    if(!this.mDBConn) return;
    if(!this.deleteKeyValueStatement) {
      this.deleteKeyValueStatement = this.mDBConn.createStatement("DELETE FROM keyvalues WHERE key = ?1");
    }
    this.deleteKeyValueStatement.bindUTF8StringParameter(0, key);
    this.deleteKeyValueStatement.execute();
  } catch(e) {
    log.exception(e);
  } finally {
    // if error before execute, reset might be needed ?
    this.deleteKeyValueStatement.reset();
  }
};

// saving a key/value pair in the sqlite database
// expire is the number of day the data is valid. After that, it will be erased when accessed
YOONO_KEYVALUEDB.setKeyValue = function(key, value, daysValid) {
  this.initDB();
  if(!this.mDBConn) return;
  
  if (!this.mDBConn.transactionInProgress)
    this.mDBConn.beginTransactionAs(this.mDBConn.TRANSACTION_DEFERRED);
  
  try { 
    var expire = 0;
    if(daysValid) {
      var now = Math.floor((new Date().getTime())/1000);
      expire = now + (daysValid * 86400); // 86400 seconds in a day
    }
    if(! this.setKeyValueStatement) {
      this.setKeyValueStatement = this.mDBConn.createStatement("INSERT OR REPLACE INTO keyvalues VALUES (?1, ?2, ?3) ");
    }
    this.setKeyValueStatement.bindUTF8StringParameter(0, key);
    this.setKeyValueStatement.bindUTF8StringParameter(1, value);
    this.setKeyValueStatement.bindInt32Parameter(2, expire);
    this.setKeyValueStatement.execute();
  } catch(e) {
    log.error('YOONO_CMPT.setKeyValue : ' + e);
  } finally {
    // if error before execute, reset might be needed ?
    this.setKeyValueStatement.reset();
  }
  
  if (!this.commitTimer) {
    var self=this;
    this.commitTimer={
      timeout : 2000,
      timer : CL['@mozilla.org/timer;1'].createInstance(CI.nsITimer),
      start : function () {
        this.timer.initWithCallback(this, this.timeout, this.timer.TYPE_ONE_SHOT);
      },
      stop : function () {
        this.timer.cancel();
        self.mDBConn.commitTransaction();
        self.commitTimer=null;
      },
      notify : function(timer) {
        try {
          self.mDBConn.commitTransaction();
          self.commitTimer=null;
        } catch(e) {
          log.exception(e);
        }
      }
    };
    this.commitTimer.start();
  }
  
  /*
  var logVal = value;
  if(value && (value.length>500)) {
    logVal = value.substr(0, 230) + ' [... VALUE TOO LARGE TO BE LOGGED ...] ' + value.substr(value.length - 250);
  }
  log.debug('Key saved : ' + key + ' value ' + logVal);
  */
};

YOONO_KEYVALUEDB.getKeyValueList = function(keyPrefix) {
  var result = new Object();
  try {
    this.initDB();
    if(!this.mDBConn) return result;
    if(! this.getKeyValueListStatement) {
      this.getKeyValueListStatement = this.mDBConn.createStatement('SELECT key, value, expire FROM keyvalues WHERE key LIKE ?1');
    }
    log.debug('Key prefix search : ' + keyPrefix);
    this.getKeyValueListStatement.bindUTF8StringParameter(0, keyPrefix + '%');
    var value = '';
    var key = '';
    var expire = 0;
    var now = Math.floor((new Date().getTime())/1000);
    var toDelete = new Array();
    while (this.getKeyValueListStatement.executeStep()) {
      key = this.getKeyValueListStatement.getUTF8String(0);
      value = this.getKeyValueListStatement.getUTF8String(1);
      expire = this.getKeyValueListStatement.getInt32(2);
      if((expire != 0) && (expire <= now)) {
        /*
        if(YOONO_PREFS.get('debug.level') >4) // save perf on string concat in loops
          log.debug('Deleting expired (' + expire + ') key : ' + key + '=' + value);
        */
        // can't delete in table while reading cursor is open
        toDelete.push(key);
      } else {
        result[key] = value;
        /*
        if(YOONO_PREFS.get('debug.level') >4) // save perf on string concat in loops
          log.debug('Key found : ' + key + ' valid till ' + expire);
        */
      }
    }
  } catch(e) {
    log.exception(e);
  } finally {
    this.getKeyValueListStatement.reset();
  }
  // deleting expired values found
  for(var i=0 ; i < toDelete.length; i++) {
    this.deleteKeyValue(toDelete[i]);
  }
  return result;
};
