// The Antville Project
// http://code.google.com/p/antville
//
// Copyright 2001–2014 by the Workers of Antville.
//
// Licensed under the Apache License, Version 2.0 (the ``License'');
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an ``AS IS'' BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @fileOverview Defines the Sql prototype, a utility for relational queries
 */

/**
 * @constructor
 */
var Sql = function(options) {
  options || (options = {});
  var db = getDBConnection('antville');
  var query;

  var log = new function() {
    var fname = getProperty('sqlLog', 'helma.' + app.getName() + '.sql');
    return Packages.org.apache.commons.logging.LogFactory.getLog(fname);
  }

  var SqlData = function(result) {
    var columns = [];
    this.values = {};

    for (var i=1; i<=result.getColumnCount(); i+=1) {
      columns.push(result.getColumnName(i).toLowerCase());
    }

    this.next = function() {
      for (let key of columns) {
        this.values[key] = result.getColumnItem(key);
      }
      return;
    }

    return this;
  }

  var quote = function(str) {
    if (!options.quote || str === null) {
      return str;
    }
    return str.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
  }

  var value = function(obj) {
    if (obj === null) {
      return obj;
    }
    if (obj === undefined) {
      obj = String(obj);
    }
    switch (obj.constructor) {
      case Number:
      return obj;
      case String:
      return quote(obj);
      case Date:
      return 'from_unixtime(' + (obj.getTime() / 1000) + ')';
      case HopObject:
      case Object:
      return quote(obj.toSource());
    }
    return quote(String(obj));
  }

  var resolve = function(args) {
    var sql = args[0];
    if (args.length > 1) {
      var values = Array.prototype.splice.call(args, 1);
      if (typeof values[0] === 'object') {
        values = values[0];
      }
      sql = sql.replace(/\$(\w*)/g, function() {
        return value(values[arguments[1]]);
      });
    }
    return sql;
  }

  const prepare = function(args) {
    const sql = args[0];

    if (args.length > 1) {
      const values = Array.prototype.splice.call(args, 1);
      const connection = db.getMetaData().getConnection();
      const statement = connection.prepareStatement(sql);

      for (let index = 1, limit = values.length; index <= limit; index += 1) {
        let value = values[index - 1];

        switch (typeof value) {
          case 'number':
            statement.setInt(index, value);
            break;
          case 'string':
            statement.setString(index, value);
          default:
            statement.setString(index, String(value));
        }
      }
    }

    return statement;
  };

  /**
   * Executes an SQL command.
   * @param {String} sql The SQL command.
   * @returns {Object} The result of the SQL command.
   */
  this.execute = function(sql) {
    sql = resolve(arguments);
    log.info(sql);
    if (options.test) {
      return app.log(sql);
    }
    var error;
    var result = db.executeCommand(sql);
    if (error = db.getLastError()) {
      app.log(error);
    }
    return result;
  }

  /**
   * Retrieves an SQL query.
   * @example sql.retrieve('select $1 from $2 order by $1', 'date', 'foo')
   * ===> 'select date from foo order by date'
   * @returns {String}
   */
  this.retrieve = function() {
    if (options.prepared === true) {
      return query = prepare(arguments);
    }
    return log.info(query = resolve(arguments));
  }

  /**
   * Traverses over the results of an SQL query.
   * @param {Function} callback The callback function executed for each record.
   */
  this.traverse = function(callback) {
    let rows;

    if (query instanceof java.sql.PreparedStatement) {
      rows = db.executePreparedRetrieval(query);
    } else {
      rows = db.executeRetrieval(query);
    }

    if (rows && rows.next()) {
      do {
        var sql = new SqlData(rows);
        sql.next();
        if (!options.test) {
          callback.call(sql.values, rows);
        }
      } while (record = rows.next());
      rows.release();
    }
    return;
  }

  /**
   * @return {String}
   */
  this.toString = function() {
    return query;
  }

  return this;
}

/**
 * SQL query for retrieving the amount of records in a table.
 * @constant
 */
Sql.COUNT = 'select count(*) as count from $0';

/**
 * SQL query for retrieving the referrers of a site or a story.
 * @constant
 */
Sql.REFERRERS = 'select referrer, count(*) as requests from ' +
    "log where context_type = '$0' and context_id = $1 and action = " +
    "'main' and created > now() - interval '2 days' group " +
    'by referrer order by requests desc, referrer asc';

/**
 * SQL command for deleting all log entries older than a specific period.
 * @constant
 */
Sql.PURGEREFERRERS = "delete from log where action = 'main' and " +
    "created < now() - interval '2 days'";

/**
 * SQL query for searching stories.
 * @constant
 */
Sql.STORY_SEARCH = "select content.id from content, site, metadata, account as creator, account as modifier where site.id = ? and content.prototype = 'Story' and site.id = content.site_id and content.status in ('public', 'shared', 'open') and content.creator_id = creator.id and content.modifier_id = modifier.id and creator.status <> 'deleted' and modifier.status <> 'deleted' and content.prototype = metadata.parent_type and content.id = metadata.parent_id and metadata.name in ('title', 'text') and lower(metadata.value) like lower(?) group by content.id, content.created order by content.created desc limit ?";

Sql.COMMENT_SEARCH = "select comment.id from content as comment, content as story, site, metadata, account as creator, account as modifier where site.id = ? and comment.prototype = 'Comment' and site.id = comment.site_id and comment.story_id = story.id and story.status in ('public', 'shared', 'open') and story.comment_mode in ('open') and comment.creator_id = creator.id and comment.modifier_id = modifier.id and creator.status <> 'deleted' and modifier.status <> 'deleted' and comment.prototype = metadata.parent_type and comment.id = metadata.parent_id and metadata.name in ('title', 'text') and lower(metadata.value) like lower(?) group by comment.id, comment.created order by comment.created desc limit ?";

/**
 * SQL query for searching accounts which are not already members of the desired site.
 * @constant
 */
Sql.MEMBERSEARCH = "select id, name, created, status from account where status not in ('blocked', 'deleted') and name $0 '$1' order by name asc limit $2";

/**
 * SQL query for retrieving all story IDs in a site’s archive.
 * @constant
 */
Sql.ARCHIVE = "select id from content where site_id = $0 and prototype = 'Story' and " +
    "status in ('public', 'shared', 'open') $1 $2 limit $3 offset $4";

/**
 * SQL command for retrieving the size of a site’s archive.
 * @constant
 */
Sql.ARCHIVESIZE = 'select count(*) as count from content where site_id = $0 ' +
    "and prototype = 'Story' and status in ('public', 'shared', 'open') $1";

/**
 * SQL part filtering the archive query.
 * @see Archive#getFilter
 * @constant
 */
Sql.ARCHIVEPART = ' and extract($0 from created) = $1';

/**
 * SQL part for applying an order to the archive query.
 * @see Archive#stories_macro
 * @constant
 */
Sql.ARCHIVEORDER = 'order by created desc';
