importClass(java.sql.DatabaseMetaData);
importClass(java.sql.PreparedStatement);
importClass(java.sql.ResultSet);
importClass(java.util.List);
importClass(java.util.ArrayList);
importClass(java.util.Map);

/**
   Rhino implementation of ActiveRecord.
   By extending this, fields from the table of the same
   name in the DB will automatically be managed.
   
   author: Tom Austin
 */
function RhinoRecord(caller, properties) {
   if (!properties) properties = {};
   // Gets the table name by parsing the function definition.
   // Ugly, but I was not sure how else to do it.
   if (caller) {
      var namePat = /\s*function (\w+).*/
      var funText = caller.toString();
      var result = funText.match(namePat);
      
      this.tableName = (result[1] + 's').toLowerCase();
      
      //Now load the field names from the table.
      this.loadTableFields(properties);
      
      //Add class methods and properties
      caller.findFirst = RhinoRecord.findFirst;
      caller.findAll = RhinoRecord.findAll;
      caller.tableName = this.tableName;
      caller.attributes = this.attributes;
      
      if (RhinoRecord.enableMop) {
        manageWildcardProperty(caller);
        caller.getWildcardProperty = RhinoRecord.prototype.staticGetWildcardProperty;
      }
   }
}
// Holds connection details
RhinoRecord.db = {};
// If set to true, this will enable advanced Metaobject-Protocol features.  By default this is disabled.
RhinoRecord.enableMop = false;

// Handles the details of connecting to the DB.
RhinoRecord.connect = function() {
   java.lang.Class.forName(this.db.jdbcDriver);
   return java.sql.DriverManager.getConnection(this.db.jdbcUrl, this.db.jdbcUser, this.db.jdbcPassword);
}

// Converts field names to the equivalent db field name.  For
// example, someField will become some_field.
RhinoRecord.camelToUnderscore = function(s) {
   return s.replace(/([a-z])([A-Z])/g, '$1_$2').toLowerCase();
}

// Converts db field names to js field names.  So some_field will be
//  converted to someField.
RhinoRecord.underscoreToCamel = function(s) {
   var portions = s.split('_');
   s = "";
   for (var i in portions) {
      firstChar = portions[i].slice(0,1)
      if (s != "") firstChar = firstChar.toUpperCase();
      s += firstChar + portions[i].slice(1);
   }
   return s;
}

RhinoRecord.capitalize = function (s) {
  return s.slice(0,1).toUpperCase() + s.slice(1);
}

RhinoRecord.calcConstrNameFromPlural = function (s) {
  return s.slice(0,1).toUpperCase() + s.slice(1,s.length-1);
}

// Initializes fields when this is first loaded.
RhinoRecord.prototype.loadTableFields = function(props) {
   this.attributes = new Array();

   var con = RhinoRecord.connect();

   var md = con.getMetaData();
   var rs = md.getColumns(null, null, this.tableName, null);

   while (rs.next()) {
      var name = RhinoRecord.underscoreToCamel(rs.getString("COLUMN_NAME"));
      this.attributes.push(name);
      this[name] = props[name];
   }
   
   con.close();
}

// Saves a record to the DB.
RhinoRecord.prototype.save = function() {
   //Update the record if it has already been saved, insert it otherwise.
   if (this.id !== undefined) this.updateInDatabase();
   else this.insertToDatabase();
}

// Inserts a new record into the DB.
RhinoRecord.prototype.insertToDatabase = function() {
   var con = RhinoRecord.connect();
   
   var insert = "INSERT INTO " + this.tableName + " (";
   var values = "   VALUES (";
   for (var i in this.attributes) {
      var name = this.attributes[i];
      if (name != 'id') {
         insert += RhinoRecord.camelToUnderscore(name);
         values += "?";
         if (i != this.attributes.length - 1) {
            insert += ", ";
            values += ", ";
         }
      }
   }
   var stmt = con.prepareStatement(insert + ")" + values + ")");
   
   var key = 1;
   for (var i=0; i<this.attributes.length; i++) {
      var name = this.attributes[i];
      if (name != 'id') {
         if (this[name] !== undefined) {
            stmt.setString(key, this[name]);
         }
         else if (name.match(/Id$/)) {
            var parentRecordName = name.replace(/Id$/,'');
            if (this[parentRecordName] !== undefined) {
               var parentRecord = this[parentRecordName];
               if (parentRecord.id === undefined) {
                  try {
                     parentRecord.save();
                  }
                  catch (e) {}
               }
               recordId = (parentRecord.id !== undefined) ? parentRecord.id : null;
               stmt.setString(key, recordId);
            }
            else {
               stmt.setString(key, null);
            }
         }
         else {
            stmt.setString(key, null);
         }
         key++;
      }
   }
   
   var result = stmt.executeUpdate();
   
   //The ad has been saved now, so we need to fetch and save the id
   rs = stmt.getGeneratedKeys();
   if (rs.next()) {
      this.id = rs.getInt(1);
   }
   else {
      //Probably should throw an exception here.
   }
   
   con.close();
}

// Updates database record
RhinoRecord.prototype.updateInDatabase = function() {
  var con = RhinoRecord.connect();
   
  var sql = "UPDATE " + this.tableName + "    SET ";
  for (var i in this.attributes) {
    var name = this.attributes[i];
    if (name != 'id') {
      sql += name + "=?"; 
      if (i != this.attributes.length - 1) {
        sql += ", ";
      }
    }
  }
  sql += "   WHERE id = " + this.id;
   
  var stmt = con.prepareStatement(sql);
  var key = 1;
  for (var i=0; i<this.attributes.length; i++) {
    var name = this.attributes[i];
    if (name != 'id') {
      if (this[name] !== undefined) {
        stmt.setString(key, this[name]);
      }
      else {
        stmt.setString(key, null);
      }
      key++;
    }
  }
   
  var result = stmt.executeUpdate();
  con.close();
}

// Delete a record from the database
RhinoRecord.prototype.removeFromDatabase = function() {
  //TODO: check for the existence of an ID first.
  var con = RhinoRecord.connect();
   
  var stmt = con.createStatement();
  stmt.execute("DELETE FROM " + this.tableName + " WHERE id = "  + this.id);
  con.close();
}

// Prints attributes, for troubleshooting
RhinoRecord.prototype.printRecord = function() {
  for (var i in this.attributes) {
    prop = this.attributes[i];
    print(prop + ": " + this[prop]);
  }
}

// This allows records to be interconnected in a lazy manner.
// album.songs, for instance, would associate the Album object
// with its ArrayList of Song objects.  Likewise, song.album
// would connect the song to its album.
RhinoRecord.prototype.getWildcardProperty = function(propName) {
  if (this[propName]) return this[propName];
  
  if (this.hasOwnProperty(propName + 'Id')) {
    var constr = eval(RhinoRecord.capitalize(propName));
    this[propName] = constr.findFirst({id: this[propName+'Id']});
    return this[propName];
  }
  if (propName.match(/s$/)) {
    var constr = eval(RhinoRecord.calcConstrNameFromPlural(propName));
    if (constr) {
      var options = {};
      options.params = {};
      options.params[this.tableName.slice(0,this.tableName.length-1)+'Id'] = this.id;
      this[propName] = constr.findAll(options);
      return this[propName];
    }
  }
  
  return undefined;
}

// Used for the constructors instead of the instances.
RhinoRecord.prototype.staticGetWildcardProperty = function(propName) {
  if (this[propName]) return this[propName];

  if (propName.match(/^findBy/)) {
    var field = propName.match(/^findBy(.*)$/)[1];
    this[propName] = function (val) {
      var params = {};
      params[field] = val;
      return this.findFirst(params);
    }
    return this[propName];
  }

  return undefined;
}

// Get a record matching the specified criteria 
RhinoRecord.findFirst = function(params) {
  var con = RhinoRecord.connect();
  try {
    var values = new Array();
    
    var sql = "SELECT * FROM " + this.tableName;
    var keyword = " WHERE ";
    for (var name in params) {
      sql += keyword + RhinoRecord.camelToUnderscore(name) + "=?";
      values.push(params[name]); 
      keyword = " AND ";
    }
    
    var stmt = con.prepareStatement(sql);
    for (var i=0; i<values.length; i++) {
      stmt.setString(i+1, values[i]);
    }
   
    var rs = stmt.executeQuery();
   
    var record = undefined;
    if (rs.next()) {
      record = {};
      for (var i in this.attributes) {
        var name = this.attributes[i];
        var dbName = RhinoRecord.camelToUnderscore(name);
        var val = rs.getString(dbName);
        if (val === null) val = undefined;
        record[name] = val;
      }
    }
  }
  finally {
    con.close();
  }
   
  return new this(record);
}

// Get a record matching the specified criteria 
RhinoRecord.findAll = function(options) {
  if (!options) options = {};
  var con = RhinoRecord.connect();
  try {
    var values = new Array();
   
    var sql = "SELECT * FROM " + this.tableName;
    var keyword = " WHERE ";
    for (var name in options.params) {
      sql += keyword + RhinoRecord.camelToUnderscore(name) + "=?";
      values.push(options.params[name]); 
      keyword = " AND ";
    }
    if (options['orderBy']) {
      var orderByField = RhinoRecord.camelToUnderscore(options['orderBy']);
      sql += " ORDER BY " + orderByField;
    }
    else if (options['orderByDesc']) {
      var orderByField = RhinoRecord.camelToUnderscore(options['orderByDesc']);
      sql += " ORDER BY " + orderByField + " DESC";
    }
    
    var stmt = con.prepareStatement(sql);
    for (var i=0; i<values.length; i++) {
      stmt.setString(i+1, values[i]);
    }
    
    var rs = stmt.executeQuery();
   
    var records = new ArrayList();
    while (rs.next()) {
      var rec = {};
      for (var i in this.attributes) {
        var name = this.attributes[i];
        var dbName = RhinoRecord.camelToUnderscore(name);
          var val = rs.getString(dbName);
          if (val === null) val = undefined;
          rec[name] = val;
      }
      var s = new this(rec);
      records.add(s);
    }
  }
  finally { 
    con.close();
  }
   
  return records;
}


/**
   Returns a string that needs to be eval'd to initialized the class.
   If this feature is available, this will setup the wildcard properties for the class.
   e.g.  eval(manageRecord("Song"));
 */
function manageRecord(className) {
  var evalStr =  "function " + className + "(props) { \
      this.superclass = RhinoRecord; \
      this.superclass(arguments.callee, props);";
  
  if (RhinoRecord.enableMop) {
      evalStr += "manageWildcardProperty(this); \
        this.getWildcardProperty = RhinoRecord.prototype.getWildcardProperty;";
  }
  
  evalStr += "} \
      " + className + ".prototype = new RhinoRecord(); \
      " + className + ".prototype.constructor = " + className + "; \
      new " + className + "();";
   
  return evalStr;
}
