<?php

  
require_once("ExpressionNode.class.php");

  class 
QuerySet implements ArrayAccess {

    
/**
     * Store the query arguments for this query
     *
     * @var array
     */
    
protected $args = array();

    
/**
     * Store the parent query that is being filtered
     *
     * @var QuerySet
     */
    
protected $parent null;

    
/**
     * Store the SQL query order that should be used
     * default is ascending, descending is defined by - in front of field name
     * i.e. order_by('make') or order_by('-make')
     *
     * @var string
     */
    
protected $order_by;

    
/**
     * Store the SQL query limit using the ArrayAccess interface
     * such as
     *
     * $test = SomeClass::get();
     * $test["5:10"]
     *
     * @see http://docs.djangoproject.com/en/1.2/topics/db/queries/#limiting-querysets
     *
     * @var string
     */
    
protected $limit;

    
/**
     * Create a new query using provided parent (either class name for root
     * QuerySet object, or another QuerySet object) and arguments
     *
     * @param QuerySet $parent
     * @param array $args
     */
    
public function __construct($parent$args null) {
      if (!(
$parent instanceof QuerySet) && !class_exists($parent))
        throw new 
Exception("Invalid QuerySet parent. Must either be valid class " .
                
"name or QuerySet object.");

      
$this->parent $parent;
      
// perfect valid to have a query that returns all items
      
if (sizeof($args)) {
        
// loop through each provided argument
        
foreach($args as $arg) {
          if ((
$arg instanceof Expression) || ($arg instanceof ExpressionNode)) {
            
// standard field argument, like array("field__contains" => $value)
            // or Q or F object
            
$this->args[] = $arg;
          } else {
            throw new 
Exception("Invalid QuerySet argument: " var_export($argtrue));
          }
        }
      }
    }

    
/**
     * Filter this query
     *
     * @return QueryFilter
     */
    
public function filter() {
      if (
$this->limit || $this->order_by)
        throw new 
Exception("Cannot further filter query after limiting or ordering.");
      return new 
QueryFilter($thisfunc_get_args());
    }

    
/**
     * Exclude items from this query
     *
     * @return QueryExclude
     */
    
public function exclude() {
      if (
$this->limit || $this->order_by)
        throw new 
Exception("Cannot further exclude query after limiting or ordering.");
      return new 
QueryExclude($thisfunc_get_args());
    }

    
/**
     * Set what field to order by for this query, using
     * the - symbol to define ASC/DESC
     * i.e. order_by("make") or order_by("-make")
     *
     * @param string $field
     */
    
public function order_by($field) {
      
$this->order_by $field;
      return 
$this;
    }

    
/**
     * Format a query argument (i.e. array("field__contains" => "value"))
     * into a MySQL compatible WHERE test
     *
     * @param Expression $arg
     * @return string
     */
    
public static function format_argument($arg) {

      
$field $arg->getField();
      
$value $arg->getValue();
      
$test $arg->getOperator();

      
$field "{$field}";

      if (
$value instanceof F) {

        if (
$arg->isNot()) $field "NOT ({$field}";

        
$value $value->create_where();
        if (
$arg->isNot()) $value .= ")";

        switch(
strtoupper($test)) {
          case 
"EXACT":
            return 
"{$field} = {$value}";
          case 
"GT":
            return 
"{$field} > {$value}";
          case 
"GTE":
            return 
"{$field} >= {$value}";
          case 
"LT":
            return 
"{$field} < {$value}";
          case 
"LTE":
            return 
"{$field} <= {$value}";
        }

      } else {

        if (
$arg->isNot()) $field "NOT {$field}";

        
/**
         * @see http://docs.djangoproject.com/en/1.2/ref/models/querysets/#field-lookups
         */
        
switch(strtoupper($test)) {
          case 
"EXACT":
            if (
$value == null)
              return 
"{$field} IS NULL";
            else
              return 
"{$field} = '{$value}'";
            break;
          case 
"IEXACT":
            return 
"{$field} ILIKE '{$value}'";
            break;
          case 
"CONTAINS":
            return 
"{$field} LIKE '%{$value}%'";
            break;
          case 
"ICONTAINS":
            return 
"{$field} ILIKE '%{$value}%'";
            break;
          case 
"IN":
            if (
$value instanceof QuerySet)
              return 
"{$field} IN ($value->createExpressionNodetatement())";
            elseif (
is_array($value))
              return 
"{$field} IN (" implode (", "$value) . ")";
            else
              throw new 
Exception("Invalid value for {$field} IN " var_export($valuetrue));
            break;
          case 
"GT":
            return 
"{$field} > '{$value}'";
            break;
          case 
"GTE":
            return 
"{$field} >= '{$value}'";
            break;
          case 
"LT":
            return 
"{$field} < '{$value}'";
            break;
          case 
"LTE":
            return 
"{$field} <= '{$value}'";
            break;
          case 
"STARTSWITH":
            return 
"{$field} LIKE '{$value}%'";
            break;
          case 
"ISTARTSWITH":
            return 
"{$field} ILIKE '{$value}%'";
            break;
          case 
"ENDSWITH":
            return 
"{$field} LIKE '%{$value}'";
            break;
          case 
"IENDSWITH":
            return 
"{$field} ILIKE '%{$value}'";
            break;
          case 
"RANGE":
            if (
is_array($value) && sizeof($value) == 2)
              return 
"{$field} BETWEEN '{$value[0]}' AND '{$value[1]}'";
            else
              throw new 
Exception("Invalid value for {$field} RANGE " var_export($valuetrue));
            break;
          case 
"YEAR":
            return 
"EXTRACT('year' FROM {$field}) = '{$value}'";
            break;
          case 
"MONTH":
            return 
"EXTRACT('month' FROM {$field}) = '{$value}'";
            break;
          case 
"DAY":
            return 
"EXTRACT ('day' FROM {$field}) = '{$value}'";
            break;
          case 
"WEEK_DAY":
            return 
"EXTRACT('dayofweek' FROM {$field}) = '{$value}'";
            break;
          case 
"ISNULL":
            if (
$value === true)
              return 
"{$field} IS NULL";
            else
              return 
"{$field} IS NOT NULL";
            break;
          case 
"SEARCH":
            return 
"MATCH(tablename, {$field}) AGAINST ({$value} IN BOOLEAN MODE)";
            break;
          case 
"REGEX":
            return 
"{$field} REGEXP BINARY {$value}";
            break;
          case 
"IREGEX":
            return 
"{$field} REGEXP {$value}";
            break;
        }
      }
    }

    
/**
     * Create only the portion of the MySQL statement after the 'WHERE' clause
     * @return string
     */
    
protected function create_where() {
      
$stmt "";

      if (
sizeof($this->args)) {
        
// again, if root Query, needs to define 'WHERE'
        
if (!($this->parent instanceof QuerySet))
          
$stmt .= " WHERE ";

        
// encompass the statement with brackets for filtering and excluding
        
if (sizeof($this->args) > 1)
          
$stmt .= "(";

        
// create array of tests
        
$tests = array();
        foreach(
$this->args as $arg) {
          if (
$arg instanceof ExpressionNode) {
            
// Q or F object
            
if ($arg->isNot()) $test " NOT (";
            else 
$test "";
            
$test .= "{$arg->create_where()}";
            if (
$arg->isNot()) $test .= ")";
            
$tests[] = $test;
          } else {
            
$tests[] = QuerySet::format_argument($arg);
          }
        }

        
// dump the tests together with "AND"
        
$stmt .= implode(" AND "$tests);

        if (
sizeof($this->args) > 1)
          
$stmt .= ")";
      }

      return 
$stmt;
    }

    
/**
     * Create a full or partial MySQL statement using this Query
     * object
     *
     * @return string
     */
    
public function create_statement() {
      
// if this has no parent, it is a root Query and should begin the stmt
      
if (!($this->parent instanceof QuerySet))
        
$stmt "SELECT id FROM " . eval("return {$this->parent}::table_name();");
      else
        
$stmt $this->parent->create_statement();

      
$stmt .= $this->create_where();

      
// add order_by parameter
      
if ($this->order_by) {
        
$stmt .= " ORDER BY ";
        if (
substr($this->order_by01) == "-")
          
$stmt .= substr($this->order_by1) . " DESC";
        else
          
$stmt .= $this->order_by " ASC";
      }

      
// add limiting parameter
      
if ($this->limit) {
        list(
$offset$limit) = split(":"$this->limit);
        
$offset = (int) $offset;
        
$limit = (int) $limit;
        if (
$offset) {
          
$stmt .= " LIMIT " . ($limit $offset) . " OFFSET {$offset} ";
        } else {
          
$stmt .= " LIMIT {$limit}";
        }
      }

      return 
$stmt;
    }
    
    
/**
     * Limit this query set using offset and limit parameters
     *
     * @see http://docs.djangoproject.com/en/1.2/topics/db/queries/#limiting-querysets
     *
     * @param int $offset
     * @param int $limit
     * @return QuerySet
     */
    
public function limit($offset null$limit null) {
      
$offset = (int) $offset;
      
$limit = (int) $limit;

      if (
$limit 1) {
        throw new 
Exception("Limit must be greater than zero");
      } elseif (
$offset && $limit <= $offset) {
        throw new 
Exception("Limit must be greater than offset");
      }

      
$this->limit "{$offset}:{$limit}";
      return 
$this;
    }

    
/**
     * Create MySQL DELETE statement in a similar fashion to create_statement()
     * using provided arguments
     *
     * @return string
     */
    
public function delete() {
      
$stmt "";
      
// if this has no parent, it is a root Query and should begin the stmt
      
if (!($this->parent instanceof QuerySet))
        
$stmt "DELETE FROM " . eval("return {$this->parent}::table_name();");
      else
        
$stmt $this->parent->delete();

      
$stmt .= $this->create_where();

      return 
$stmt;
    }

    
/**
     * Create MySQL UPDATE statement using supplied arguments as fields to
     * update in the form of
     *
     * array("field1" => "newvalue", "field2" => "newvalue2")
     *
     * Also supports use of F()
     *
     * @param array $args
     * @return string
     */
    
public function update($args) {
      
$stmt "";
      
// if this has no parent, it is a root Query and should begin the stmt
      
if (!($this->parent instanceof Query)) {

        
$stmt "UPDATE " . eval("return {$this->parent}::table_name();") . " SET ";

        
$updates = array();
        foreach(
$args as $field => $value) {
          if (
$value instanceof F)
            
$value $value->create_where();
          else
            
$value "'{$value}'";

          
$updates[] = "{$field} = {$value}";
        }

        
$stmt .= implode(", "$updates) . " ";

      } else {

        
$stmt $this->parent->update($args);

      }

      
$stmt .= $this->create_where();

      return 
$stmt;
    }

    
/**
     * Return a MySQL statement that will count the number of results
     * using the basic WHERE creation as used elsewhere
     *
     * @return string
     */
    
public function count() {
      
$stmt "";
      
// if this has no parent, it is a root Query and should begin the stmt
      
if (!($this->parent instanceof QuerySet))
        
$stmt "SELECT COUNT(id) FROM " . eval("return {$this->parent}::table_name();");
      else
        
$stmt $this->parent->count();

      
$stmt .= $this->create_where();

      return 
$stmt;
    }
    
    
/**
     * ARRAYACCESS
     */
    
    
public function offsetSet($offset$value) {}

    public function 
offsetExists($offset) {}

    public function 
offsetUnset($offset) {}

    
/**
     * Handles array access when using array shortform, i.e.
     * $somequery["5:10"]
     *
     * @param string $limit
     * @return QuerySet
     */
    
public function offsetGet($limit) {
      list(
$offset$limit) = split(":"$limit);
      
$this->limit($offset$limit);
      return 
$this;
    }
    
  }

  abstract class 
QuerySub extends QuerySet {

    
/**
     * Create new subquery (as from ->filter() and ->exclude())
     */
    
public function __construct($parent$args) {
      
// subqueries require arguments
      
if (!sizeof($args))
        throw new 
Exception("Cannot exclude or filter query without arguments.");
      
parent::__construct($parent$args);
    }

  }

  class 
QueryFilter extends QuerySub {

    
/**
     * Return the where portion of the statement prepended
     * this QueryFilter object
     *
     * @return string
     */
    
protected function create_where() {
      return 
" AND " parent::create_where() . " ";
    }

  }

  class 
QueryExclude extends QuerySub {

     
/**
     * Return the where portion of the statement prepended
     * this QueryExclude object
     *
     * @return string
     */
    
protected function create_where() {
      return 
" AND NOT " parent::create_where() . " ";
    }
    
  }