// Prototype Array.indexOf to bring IE into the 21st century.  OK, it will take more than this.
// http://developer.mozilla.org/En/Core_JavaScript_1.5_Reference:Objects:Array:indexOf
if( !Array.prototype.indexOf ){
  Array.prototype.indexOf = function(elt /*, from*/){
    var len = this.length;
    var from = Number(arguments[1]) || 0;
    from = (from < 0) ? Math.ceil(from) : Math.floor(from);
    if( from < 0 )  from += len;
    for( ; from < len; from++ ){
      if( from in this && this[from] === elt )  return from;
    }
    return -1;
  };
}


// Story: AJAX interface for retrieving a JSON representation of a story by ID.
function Story(credentials, id, success_callback, error_callback){
  // PRIVATE INSTANCE VARIABLES.
  // Parameters passed to the object constructor are implicit private instance variables.
  var that = this;  // Make this object available to private methods.
  // credentials - Your Share Your Story username:password encoded as a Base64 string.
  // id - The ID of the story you would like to request.
  // success_callback - A reference to the JavaScript function to call on success.
  // error_callback - A reference to the JavaScript function to call on failure.
  
  // PRIVATE METHODS.
  // Callable only by privileged methods and other private methods.
  function hasCredentials(){ return (credentials != null); }
  
  // PRIVILEGED METHODS.
  // Public methods with access to private instance variables and methods.
  this.setCredentials = function(value){ credentials = value; }
  this.setId = function(value){ id = value; }
  this.getId = function(){ return id; }
  this.setSuccessCallback = function(func){ success_callback = func; }
  this.setErrorCallback = function(func){ error_callback = func; }
  
  // Perform the retrieval.
  this.exec = function(){
    // Construct and execute JSONP query.
    var query_url = 'https://stories.consumersunion.org/stories/' + id;
    var query = 'credentials=' + credentials;

    var options = { 
      type: 'GET',
      url: query_url,
      data: query,
      dataType: 'jsonp',
      processData: false,
      contentType: 'application/json',
      success: that.processResponse,
      error: that.processError
    };
    jQuery.ajax(options);
  }
  
  this.processResponse = function(responseBody, status){
    if( success_callback != null ){ success_callback.call(null, responseBody); }
  }

  this.processError = function(xml_http_request, status, exception){
    if( error_callback != null ){ error_callback.call(null, xml_http_request, status, exception); }
  }
}


// SearchRequest: JavaScript object for construction of a search request.
function SearchRequest(criteria, page, per_page){
  // PRIVATE INSTANCE VARIABLES.
  // Parameters passed to the object constructor are implicit private instance variables.
  var that = this;  // Make this object available to private methods.
  // criteria - A hash of search criteria; create an empty hash if not defined in construction.
  if( arguments.length == 0 )  var criteria = {};
  var sorting_orders = [];  // An array of hashes describing the sort order.
  // page - the requested page of search results; set default if not defined in construction.
  if( arguments.length < 2 )  var page = 1;
  // per_page - the number of search results per page; set default if not defined in construction.
  if( arguments.length < 3 )  var per_page = 10;
  // Properties for which search expressions may be generated.
  var valid_properties = [ 'content', 'topic', 'date', 'geography' ];
  // Properties for which sorting orders may be generated.
  var valid_sorting_columns = { created_at: 2, updated_at: 3, published_at: 4, approved_at: 5, distance: 7 };

  // PRIVILEGED METHODS.
  // Public methods with access to private instance variables and methods.
  this.setPage = function(requested_page){ page = requested_page; }
  this.getPage = function(){ return page; }
  this.setPerPage = function(items_per_page){ per_page = items_per_page; }
  this.getPerPage = function(){ return per_page; }
  this.validProperty = function(property){ return (valid_properties.indexOf(property) >= 0) ? true : false; }
  
  // Determine if this SearchRequest has been configured for the specified
  // criterion.  Can be called in three ways:
  // (no arguments): are there any criteria at all?
  // (property): are there any criteria for the specified property?
  // (property, subproperty): is this exact criterion present?
  this.hasCriteria = function(property, subproperty){
    // Are there any expressions at all?
    if( arguments.length == 0 ){
      for( var index = 0; index < valid_properties.length; index++ ){
        if( that.hasCriteria(valid_properties[index]) )  return true;
      }
      return false;
    }
    
    // Short-circuit test for invalid property.
    if( ! that.validProperty(property) )  return false;
    
    // Are there any expressions for the specified property?
    if( criteria[property] == null )  return false;
    
    // Is the exact specified expression present?
    if( (arguments.length == 2) && (criteria[property][subproperty] == null) )  return false;
    
    // Otherwise, we have a value for the specified property/expression.
    return true;
  }
  
  // Set the specified property.
  this.setCriterion = function(property, subproperty, value){
    if( that.validProperty(property) ){
      if( ! that.hasCriteria(property) )  criteria[property] = {};
      criteria[property][subproperty] = value;
    }
  }

  // Retrieve the specified property.
  this.getCriterion = function(property, subproperty){
    return that.hasCriteria(property, subproperty) ? criteria[property][subproperty] : null;
  }
  
  // Append a sorting order.
  this.addSortingOrder = function(column, is_ascending){
    if( valid_sorting_columns[column] == null )  return;
    var new_sorting_order = {
      operand_id: valid_sorting_columns[column],
      ascending: is_ascending,
      position: sorting_orders.length + 1
    }
    sorting_orders.push(new_sorting_order);
  }
  
  // Retrieve the sorting orders.
  this.getSortingOrders = function(){ return sorting_orders; }
}


// Search: AJAX interface for searching the story database.
//         Returns an Array of JSON representations of stories.
function Search(credentials, request, success_callback, error_callback){
  // PRIVATE INSTANCE VARIABLES.
  // Parameters passed to the object constructor are implicit private instance variables.
  var that = this;   // Make this object available to private methods.
  // credentials - Your Share Your Story username:password encoded as a Base64 string.
  // request - A SearchRequest object instance.
  // success_callback - A reference to the JavaScript function to call on success.
  // error_callback - A reference to the JavaScript function to call on failure.
  var results = {};  // Placeholder for search result object.
  
  // PRIVATE METHODS.
  // Callable only by privileged methods and other private methods.
  function hasCredentials(){ return (credentials != null); }
  
  function getExpression(type, operand_id, matching_operator_id, operand_2_name, operand_2){
    var expression = {
      type: type,
      operand_id: operand_id,
      matching_operator_id: matching_operator_id
    };
    expression[operand_2_name] = operand_2;
    return expression;
  }
  
  function getContentExpressions(){
    var ors = [];
    
    // Short-circuit.
    if( ! request.hasCriteria('content') )  return ors;
    
    if( request.hasCriteria('content', 'and') ){
      var terms = request.getCriterion('content', 'and');
      for( var index = 0; index < terms.length; index++ ){
        ors.push({ 'expressions': [ getExpression('StaticStringExpression', 1, 7, 'op2string', terms[index]) ]});
      }
    }
    
    if( request.hasCriteria('content', 'or') ){
      var terms = request.getCriterion('content', 'or');
      var expressions = [];
      for( var index = 0; index < terms.length; index++ ){
        expressions.push(getExpression('StaticStringExpression', 1, 7, 'op2string', terms[index]));
      }
      ors.push({ 'expressions': expressions });
    }
    
    if( request.hasCriteria('content', 'not') ){
      var terms = request.getCriterion('content', 'not');
      for( var index = 0; index < terms.length; index++ ){
        ors.push({ 'expressions': [ getExpression('StaticStringExpression', 1, 8, 'op2string', terms[index]) ]});
      }
    }
    
    if( request.hasCriteria('content', 'phrase') ){
      var phrase = request.getCriterion('content', 'phrase');
      ors.push({ 'expressions': [ getExpression('StaticStringExpression', 1, 7, 'op2string', phrase) ]});
    }
    
    return ors;
  }
  
  function getTopicExpressions(){
    var ors = [];

    // Short-circuit.
    if( ! request.hasCriteria('topic') )  return ors;
    
    if( request.hasCriteria('topic', 'campaign') ){
      var terms = request.getCriterion('topic', 'campaign');
      for( var index = 0; index < terms.length; index++ ){
        ors.push({ 'expressions': [ getExpression('StaticStringExpression', 11, 3, 'op2string', terms[index]) ]});
      }
    }
    
    return ors;
  }
  
  function getDateExpressions(){
    var ors = [];
    
    // Short-circuit.
    if( ! request.hasCriteria('date') )  return ors;
    
    var date_type = request.getCriterion('date', 'type');    
    if( request.hasCriteria('date', 'start') ){
      var start_date = request.getCriterion('date', 'start');
      ors.push({ 'expressions': [ getExpression('StaticDatetimeExpression', date_type, 5, 'op2datetime', start_date) ]});
    }
    
    if( request.hasCriteria('date', 'end') ){
      var end_date = request.getCriterion('date', 'end');
      ors.push({ 'expressions': [ getExpression('StaticDatetimeExpression', date_type, 2, 'op2datetime', end_date) ]});
    }

    return ors;
  }
  
  function getGeographyExpressions(){
    var ors = [];
    
    // Short-circuit.
    if( ! request.hasCriteria('geography') )  return ors;
    
    if( request.hasCriteria('geography', 'location') ){
      var location = request.getCriterion('geography', 'location');
      ors.push({ 'expressions': [ getExpression('StaticStringExpression', 6, 3, 'op2string', location) ]});
    }
    
    if( request.hasCriteria('geography', 'proximity') ){
      var proximity = request.getCriterion('geography', 'proximity');
      ors.push({ 'expressions': [ getExpression('StaticIntegerExpression', 7, 2, 'op2integer', proximity) ]});
    }
    
    return ors;
  }

  // PRIVILEGED METHODS.
  // Public methods with access to private instance variables and methods.
  this.setCredentials = function(value){ credentials = value; }
  this.setRequest = function(search_request){ request = search_request; }
  this.getRequest = function(){ return request; }
  this.setSuccessCallback = function(func){ success_callback = func; }
  this.setErrorCallback = function(func){ error_callback = func; }
  this.getResults = function(){ return results; }
  
  // Perform the search.
  this.exec = function(){
    var expressions = [].concat(getContentExpressions())
                        .concat(getTopicExpressions())
                        .concat(getDateExpressions())
                        .concat(getGeographyExpressions());
    
    // Construct and execute AJAX query.
    var is_local = ((window.location.host == 'localhost:3000') || 
                    (window.location.host == 'stories.consumersunion.org')) ? true : false;
    var query_protocol = is_local ? window.location.protocol : 'https:';
    var query_host = is_local ? window.location.host : 'stories.consumersunion.org';
    var query_controller = hasCredentials() ? '/searches/remote_create' : '/admin/searches';
    var query_url = query_protocol + '//' + query_host + query_controller;
    var search_request_hash = { title: 'Search', ors: expressions, sorting_orders: request.getSortingOrders() };

    // Remote API queries must be handled using JSONP and GET query criteria.
    if( hasCredentials() ){
      var query = 'search=' + escape(JSON.stringify(search_request_hash)) + '&' +
                  'page=' + request.getPage() + '&' +
                  'per_page=' + request.getPerPage() + '&' +
                  'credentials=' + credentials;
      var http_method = 'GET';
      var ajax_data_type = 'jsonp';
    }
    else {
      var parameters = { 
        search: search_request_hash, 
        page: request.getPage(), 
        per_page: request.getPerPage()
      };
      var query = JSON.stringify(parameters);
      var http_method = 'POST';
      var ajax_data_type = 'json';
    }

    var options = { 
      type: http_method,
      url: query_url,
      dataType: ajax_data_type,
      data: query,
      processData: false,
      contentType: 'application/json',
      success: that.processResponse,
      error: that.processError
    };
    jQuery.ajax(options);
  }
  
  this.processResponse = function(responseBody, status){
    results = responseBody;
    if( success_callback != null ){ success_callback.call(null, request, results); }
  }

  this.processError = function(xml_http_request, message, exception){
    if( error_callback != null ){ error_callback.call(null, xml_http_request, message, exception); }
  }
}
