/**
 * Phiz3.js - This is a port of Phiz3 to pure JavaScript.  It is intended to
 * support Gears and Jaxer environments.
 *
 * @author R. S. Doiel
 *
 * This file is part of Phiz.
 *
 * copyright (c) 2009 all rights reserved
 *
 *  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
 *   
 *   * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
 *   * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
 *   
 *   * Neither the name of the Phiz nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
 *   
 *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * 
 */

Phiz3 = function (configuration) {
  this.version_name = "felix the cat";
  this.version = "0.1x";
  this.verbose = false;
  this.message_queue = [];
  this.attributes = {};
  this.db = false;
  this.qry = false;
  
  /**
   * message - add a message to the message queue or the queue and clear it.
   * @param msg - the message to add to the queue or if undefined it'll clear the queue.
   * @return true if message added to the queue, text if returning the whole queue or false otherwise.
   */
  this.message = function (msg) {
    var text = '';
    if (this.message_queue === undefined) {
      this.message_queue = [];
    }
    if (msg === undefined) {
      text = this.message_queue.join("\n");
      delete this.message_queue;
      this.message_queue = [];
      return text;
    } else {
      this.message_queue.push(msg);
      return true;
    }
    return false;
  };
  
  /**
   * messageCount - return the number of messages in queue
   * @return count of messages in queue
   */
  this.messageCount = function () {
    if (this.message_queue === undefined) {
      this.message_queue = [];
    }
    return this.message_queue.length;
  };
  
  /**
   * set - set an attribute by key value pair
   * @param key - the attribute to set.
   * @param value - the value to set the attribute to, if undefined unset key
   * @return true if successful, false otherwise
   */
  this.set = function (key, value) {
    if (this.attributes === undefined) {
      this.attributes = {};
    }
    if (key === undefined) {
      this.message("ERROR: must have a key when setting an attribute.");
      return false;
    }
    if (value === undefined && this.attributes[key] !== undefined) {
      delete this.attributes[key];
      return true;
    }
    this.attributes[key] = value;
    return true;
  };
  
  /**
   * get - return the value for a given attribute key.  False if not found.
   * @param key - the key to retreive
   * @return the value if successful, false otherwise.
   */
  this.get = function (key) {
    if (this.attributes === undefined) {
      this.attributes = {};
      return false;
    }
    if (this.attributes[key] === undefined) {
      return false;
    }
    return this.attributes[key];
  };
  
  /**
   * map - map an object into a string
   * @param str - the string containing replacements in the form of {myvar}.
   * @param obj - object which will populate the resulting string
   * @param verbose - send more content to the message queue.
   * @return the processed string.
   */
  this.map = function (str, obj, verbose) {
    var target = ['\\{','','\\}'], re;
    if (typeof verbose === 'undefined') {
      verbose = false;
    }
    for (key in obj) {
      if (typeof key === 'string') {
        target[1] = key;
        re = new RegExp(target.join(''),'gm');
        str = str.replace(re,obj[key]);
      }
    }
    if (verbose === true) {
      this.message(str);
    }
    return str;
  };
  
  /**
   * open - open an SQL database connection
   * @param conf - an object with attributes like db_type, db_name, db_host, db_user, db_password or undefined.
   * @param verbose - if true send more messages to message queue
   * @return true if open is successful, false otherwise
   */
  this.open = function (conf, verbose) {
    var connection_parameters = {}, db_path;
    
    if (typeof conf === 'object' && typeof conf.get === 'function') {
      switch (conf.get("db_type").toLowerCase()) {
      case 'mysql':
        this.set("db_type", "mysql");
        this.set("db_name", conf.get("db_name"));
        this.set("db_host", conf.get("db_host")); 
        this.set("db_user", conf.get("db_user")); 
        this.set("db_password", conf.get("db_password")); 
        break;
      case 'sqlite':
        this.set("db_type", "sqlite");
        this.set("db_name", conf.get("db_name"));
        break;
      default:
        this.message("ERROR: Database type not set.");
        return false;
      }    
    }
    // FIXME: add Gears SQLite support here.
    if (typeof Jaxer !== 'undefined') {
      switch(this.get('db_type')) {
      case 'mysql':
        connection_parameters = {
          IMPLEMENTATION: "MySQL",
          HOST: this.get("db_host"),
          NAME: this.get("db_name"),
          USER: this.get("db_user"),
          PASS: this.get("db_password")
        };
        Jaxer.DB.MySQL.createDB(connection_parameters);
        this.db = new Jaxer.DB.MySQL.Connection(connection_parameters);
        this.db.open();
        if (verbose === true) {
          this.message(this.get("db_name") + " is opened in Jaxer+MySQL.");
        }
        return true;
      case 'sqlite':
        if (this.get("db_name").indexOf('/') === 0) {
          db_path = Jaxer.Dir.resolve(this.get("db_name"));
        } else {
          db_path = Jaxer.Dir.resolve(this.get("db_name"), Jaxer.request.currentFolder);
        }
        connection_parameters = {
          IMPLEMENTATION: "SQLite",
          PATH: db_path
        };
        Jaxer.DB.SQLite.createDB(connection_parameters);
        this.db = new Jaxer.DB.SQLite.Connection(connection_parameters);
        this.db.open();
        if (verbose === true) {
          this.message(this.get("db_name") + " is opened in Jaxer+SQLite.");
        }
        return true;
      default:
        this.message("ERROR: Database type not set.");
        return false;
      }
    } else if (typeof google !== 'undefined' && typeof google.gears !== 'undefined') {
      if (this.db === undefined)  {
        if (this.get("db_name") === false) {
          this.message('ERROR: You must set a database name');
          return false;
        }
        this.db = google.gears.factory.create("beta.database");
        this.db.open(this.get("db_name"));
        if (verbose === true) {
          this.message(this.get("db_name") + " is opened in Gears+SQLite.");
        }
        return true;
      }
      this.message("ERROR: Database connection already created.");
      return false;
    }
    return false;
  };
  
  /**
   * close - Close a SQL database connection.
   */
  this.close = function () {
    // FIXME: add Gears support here.
    if (typeof Jaxer !== 'undefined') {
      switch(this.get('db_type')) {
      case 'mysql':
      case 'sqlite':
        if (typeof this.db.close === 'function') {
          this.db.close();
          return true;
        }
        return false;
      default:
        this.message("ERROR: Database type not set.");
        return false;
      }
    } else if (typeof google !== 'undefined' && typeof google.gears !== 'undefined') {
      if (this.db !== undefined) {
        try {
          this.db.close();
        } catch (err) {
          this.message(err.message);
          return false;
        }
        return true;
      }
      this.message("ERROR: database is not opened.");
      return false;
    }
    return false;
  };
  
  /**
   * execute - execute an SQL statement
   * @param sql - the SQL statement
   * @param verbose - send additional messages to the message queue
   */
  this.execute = function(sql, verbose) {
    // FIXME: add Gears support here.
    if (typeof Jaxer !== 'undefined') {
      switch(this.get('db_type')) {
      case 'mysql':
      case 'sqlite':
        if (typeof this.db.execute === 'function') {
          this.db.rs = this.db.execute(sql);
          this.db.rs_pos = 0;
          if (sql.toUpperCase().indexOf("SELECT ") >= 0) {
            this.set("SQLRowCount", this.db.rs.rows.length);
          } else {
            this.set("SQLRowCount", this.db.rs);
          }
          return true;
        }
        return false;
      default:
        this.message("ERROR: Database type not set.");
        return false;
      }
    } else if (typeof google !== 'undefined' && typeof google.gears !== 'undefined') {
      if (this.db === undefined) {
        this.message("ERROR: You should create the database first.");
        return false;
      }
      try {
        this.rs = this.db.execute(sql);
        // FIXME: need to set SQLRowCount here.
      } catch (err) {
        if (verbose === true) {
          this.message(err.message);
        }
        return false;
      }
      if (verbose === true) {
        this.message("SQL: " + sql);
      }
      return true;
    }
    return false;
  };

  /**
   * mapExecute - execute a mapped SQL statement
   * @param sql - the SQL template
   * @param obj - the object to map into template
   * @param verbose - send additional messages to the message queue
   * @return true if successful, false otherwise
   */
  this.mapExecute = function(sql, obj, verbose) {
    return this.execute(this.map(sql, obj, verbose), verbose);
  };
  
  /**
   * getRow - get an object of results from previously execute SQL statement
   */
  this.getRow = function (verbose) {
    var i = 0, row = {}, fc;

    // FIXME: add Gear support here.
    if (typeof Jaxer !== 'undefined') {
      switch(this.get('db_type')) {
      case 'mysql':
      case 'sqlite':
        if (this.db.rs_pos < this.db.rs.rows.length) {
          row = this.db.rs.rows[this.db.rs_pos];
          this.db.rs_pos += 1;
          return row;
        }
        return false;
      default:
        this.message("ERROR: Database type not set.");
        return false;
      }
    } else if (typeof google !== 'undefined' && typeof google.gears !== 'undefined') {
      if (this.db === undefined) {
        this.message("ERROR: You should create the database first.");
        return false;
      }
      if (this.rs !== undefined) {
        if (this.rs.isValidRow()) {
          fc = this.rs.fieldCount();
          for (i = 0; i < fc; i += 1) {
            row[this.rs.fieldName(i)] = this.rs.field(i);
          }
          this.rs.next();
          if (verbose === true) {
            this.message("Row: " + row.toSource());
          }
          return row;
        } else {
          this.rs.close();
          if (verbose === true) {
            this.message("Now more rows");
          }
          return false;
        }
      }
      this.message("ERROR: No result set available");
      return false;
    }
    return false;
  };

  /**
   * lastInsertRowId - return the last inserted row id.
   * @return id number if successful, false otherwise
   */
  this.lastInsertRowId = function () {
    // FIXME: add Gears support
    if (typeof Jaxer !== 'undefined') {
      return this.db.lastInsertId;
    } else if (typeof google !== 'undefined' && typeof google.gears !== 'undefined') {
      return this.db.lastInsertRowId;
    }
    return false;
  };


  /**
   * errorCount - scan the message queue for strings starting with "ERROR:"
   * @return a numeric count of the errors
   */
  this.errorCount = function () {
    var i, c;
    c = 0;
    for (i in this.message_queue) {
      if (this.message_queue[i].indexOf("ERROR:") === 0) {
        c++;
      }
    }
    return c;
  };

  
  /**
   * simpleConf - read a string which is a colon delimited set of 
   * key/value pairs, with one line per pair and where #
   * delimits comments
   * @param conf - A configuration file's content as string (i.e.
   * what you would get from get_file_contents())
   * @return true if successfully read the file, false otherwise.
   */
  this.simpleConf = function(configuration) {
    var key_value_pair, lines, i, pos, re;
    re = /\w/;
    lines = configuration.split("\n");
    for (i in lines) {
      pos = lines[i].indexOf('#');
      if (pos !== -1) {
        lines[i] = lines[i].substr(0, pos);
      }
      pos = lines[i].indexOf(':');
      if (pos !== -1) {
        this.set(lines[i].substr(0, pos), lines[i].substr(pos+1));
      } else if (lines[i].search(/\s*/) === -1) {
        this.message("ERROR: line " + i + " doesn't make sense [" + lines[i] + "]");
      }
    }
    if (this.errorCount() === 0) {
      return true;
    }
    return false;
  };  

  if (typeof configuration === 'object') {
    if (typeof configuration.getKeys === 'function' &&
        typeof configuration.get === 'function') {
      for (config_key in configuration.getKeys()) {
        this.set(config_key, configuration.get(config_key));
      }
    } else {
      for (config_key in configuration) {
        this.attributes[config_key] = configuration[config_key];
      }
    }
  } else if (typeof configuration === 'string') {
    if (typeof JSON === 'object' && typeof JSON.parse === 'function' && (configuration[0] === '{' || configuration[0] === '[')) {
      try {
        jsonconf = JSON.parse(configuration);
      } catch (err) {
        jsonconf = false;
      }
        
      if (typeof jsonconf === 'object') {
        for (config_key in jsonconf) {
          this.attributes[config_key] = jsonconf[config_key];
        }
      } else {
        this.message("ERROR: can't parse JSON configuration.");
      }
    } else {
      /* If not JSON try simpleConf */ 
      this.simpleConf(configuration);
    }
  }
};