<?php

/**
 * ChianQueryBuilder
 *
 * Copyright (c) 2010 BarsMaster
 * e-mail: barsmaster@gmail.com, arthur.borisow@gmail.com
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 * @author BarsMaster
 * @copyright 2010
 * @version 1.1.1
 * @access public
 */
class BarsMaster_ChainQueryBuilder
{
  private $_operation = '';
  private $_fields = [];
  private $_values = [];
  private $_tables = [];

  private $_join = '';
  private $_using = '';
  private $_on = [];

  private $_where = [];
  private $_limit = 0;
  private $_offset = 0;

  private $_orderBy = [];
  private $_order = 'ASC';
  private $_groupBy = [];

  public function __toString()
  {
    return $this->build();
  }

  public function orderBy($orderBy)
  {
    $args = $this->_getArgs(func_get_args());
    $this->_orderBy = $args;
    return $this;
  }

  public function order($order)
  {
    $order = strtoupper($order);
    if (in_array($order, ['ASC', 'DESC'])) {
      $this->_order = $order;
    }
    return $this;
  }

  public function groupBy($groupBy)
  {
    $args = $this->_getArgs(func_get_args());
    $this->_groupBy = $args;
    return $this;
  }

  public function offset($offset = 0)
  {
    $offset = (int) abs($offset);
    if ($offset) {
      $this->_offset = $offset;
    }
    return $this;
  }

  public function join($table)
  {
    $this->_join = 'JOIN ' . $table;
    return $this;
  }

  public function leftJoin($table)
  {
    $this->_join = 'LEFT JOIN ' . $table;
    return $this;
  }

  public function rightJoin($table)
  {
    $this->_join = 'RIGHT JOIN ' . $table;
    return $this;
  }

  public function using($field)
  {
    $this->_using = $field;
    return $this;
  }

  public function on($c1, $operand, $c2)
  {
    return $this->_addWhereOn($c1, $operand, $c2, '', 'on');
  }

  public function andOn($c1, $operand, $c2)
  {
    return $this->_addWhereOn($c1, $operand, $c2, 'AND', 'on');
  }

  public function orOn($c1, $operand, $c2)
  {
    return $this->_addWhereOn($c1, $operand, $c2, 'AND', 'on');
  }

  public function limit($limit = 0)
  {
    $limit = (int) abs($limit);
    $this->_limit = $limit;
    return $this;
  }

  public function select($fields)
  {
    $this->_setOperation('select');
    $args = $this->_getArgs(func_get_args());
    foreach ($args as $arg) {
      $this->addField($arg);
    }
    return $this;
  }

  public function from($tables)
  {
    if ($this->_operation != 'SELECT') {
      throw new Exception('Only SELECT operators.');
    }
    $this->_tables = $this->_getArgs(func_get_args());
    return $this;
  }

  public function where($cond1, $operand, $cond2)
  {
    return $this->_addWhereOn($cond1, $operand, $cond2, '', 'where');
  }

  public function andWhere($cond1, $operand, $cond2)
  {
    return $this->_addWhereOn($cond1, $operand, $cond2, 'AND', 'where');
  }

  public function orWhere($cond1, $operand, $cond2)
  {
    return $this->_addWhereOn($cond1, $operand, $cond2, 'OR', 'where');
  }

  private function _addWhereOn($cond1, $operand, $cond2, $type, $property)
  {
    $operand = strtoupper($operand);
    if (!in_array($operand, ['=', '>', '<', '<>', '!=', '<=', '>=', 'LIKE', 'IN'])) {
      throw new Exception('Unsupported operand:' . $operand);
    }
    $this->{'_' . $property}[] = [
      'cond1' => $cond1,
      'operand' => $operand,
      'cond2' => $cond2,
      'type' => $type,
    ];
    return $this;
  }

  public function addField($field)
  {
    if (!in_array($field, $this->_fields)) {
      $this->_fields[] = $field;
    }
    return $this;
  }
  public function setFields($fields)
  {
    $this->_fields = is_array($fields) ? $fields : [$fields];
    return $this;
  }

  protected function _sanitizeValue($val, $search = false)
  {
    if (!is_numeric($val)) {
      $val = '\'' . $val . '\'';
    }
    return $val;
  }

  public function insertInto($table)
  {
    $this->_setOperation('insert');
    $this->_tables[] = $table;
    return $this;
  }

  public function fields($fields)
  {
    if (!in_array($this->_operation, ['INSERT', 'UPDATE'])) {
      throw new Exception('Only INSERT and Update operations.');
    }
    $args = $this->_getArgs(func_get_args());
    $this->_fields = $args;
    return $this;
  }

  public function values($values)
  {
    if (!in_array($this->_operation, ['INSERT', 'UPDATE'])) {
      throw new Exception('Only INSERT and Update operations.');
    }
    $args = $this->_getArgs(func_get_args());
    if (count($args) != count($this->_fields)) {
      throw new Exception('Number of values has to be equal to the number of fields.');
    }
    if ($this->_operation == 'INSERT') {
      $this->_values[] = $args;
    } elseif ($this->_operation == 'UPDATE') {
      $this->_values = $args;
    }
    return $this;
  }

  public function deleteFrom($table)
  {
    $this->_setOperation('delete');
    $args = $this->_getArgs(func_get_args());
    $this->_tables = $args;
    return $this;
  }

  public function update($table)
  {
    $this->_setOperation('update');
    $this->_tables = [$table];
    return $this;
  }

  public function set($field)
  {
    $args = func_get_args();
    if (count($args) == 2) {
      $args = [$args[0] => $args[1]];
    } else {
      $args = $this->_getArgs(func_get_args());
    }
    foreach ($args as $field => $val) {
      if (!in_array($field, $this->_fields)) {
        $this->_fields[] = $field;
        $this->_values[] = $val;
      }
    }

    return $this;
  }

  private function _setOperation($operation)
  {
    if ($this->_operation) {
      throw new Exception('Can\'t modify the operator.');
    } elseif (!in_array($operation, ['select', 'insert', 'delete', 'update'])) {
      throw new Exception('Unsupported operator:' . strtoupper($operation));
    } else {
      $operation = strtoupper($operation);
      $this->_operation = $operation;
    }
  }

  private function _getArgs($args)
  {
    $argsCnt = count($args);
    if (!$argsCnt) {
      return [];
    }

    if ($argsCnt == 1) {
      if (!is_array($args[0])) {
        return [$args[0]];
      }
      return $args[0];
    } else {
      $return = [];

      foreach ($args as $arg) {
        $return[] = $arg;
      }

      return $return;
    }
  }

  public function build()
  {
    $statement = [];
    $this->_buildOperator($statement);
    $op = '_build' . $this->_operation;
    $this->$op($statement);

    $this->_buildJoin($statement);

    $this->_buildWhereOn($statement, 'where');

    $this->_buildGroupBy($statement);

    $this->_buildOrderBy($statement);

    $this->_buildLimit($statement);

    return implode(' ', $statement);
  }

  private function _buildJoin(&$statement)
  {
    if (!$this->_join) {
      return;
    }
    $statement[] = $this->_join;
    if ($this->_using) {
      $statement[] = 'USING(' . $this->_using . ')';
    }
    $this->_buildWhereOn($statement, 'on');
  }
  private function _buildUpdate(&$statement)
  {
    $statement[] = implode(', ', $this->_tables);
    $statement[] = 'SET';
    $set = [];
    foreach ($this->_fields as $k => $f) {
      $set[] = $f . ' = ' . $this->_sanitizeValue($this->_values[$k]);
    }
    $statement[] = implode(', ', $set);
  }
  private function _buildDELETE(&$statement)
  {
    $statement[] = 'FROM ' . implode(', ', $this->_tables);
  }
  private function _buildSELECT(&$statement)
  {
    $statement[] = implode(', ', $this->_fields);
    $statement[] = 'FROM ' . implode(', ', $this->_tables);
  }

  private function _buildINSERT(&$statement)
  {
    $statement[] = 'INTO';
    $statement[] = implode(', ', $this->_tables);
    $this->_buildINSERTFields($statement);
    $statement[] = 'VALUES';
    $this->_buildINSERTValues($statement);
  }

  private function _buildINSERTFields(&$statement)
  {
    $statement[] = '(' . implode(', ', $this->_fields) . ')';
  }

  private function _buildINSERTValues(&$statement)
  {
    $values = [];
    foreach ($this->_values as $val) {
      foreach ($val as &$v) {
        $v = $this->_sanitizeValue($v);
      }
      $values[] = '(' . implode(', ', $val) . ')';
    }
    $statement[] = implode(', ', $values);
  }

  private function _buildOperator(&$statement)
  {
    $statement[] = $this->_operation;
  }

  private function _buildWhereOn(&$statement, $type)
  {
    if (!in_array($this->_operation, ['UPDATE', 'DELETE', 'SELECT'])) {
      return;
    }
    if (count($this->{'_' . strtolower($type)})) {
      $statement[] = strtoupper($type);
      foreach ($this->{'_' . strtolower($type)} as $where) {
        $tmp = [$where['type'], $where['cond1'], $where['operand']];
        if ($where['operand'] != 'IN') {
          if ($type == 'where') {
            $tmp[] = $this->_sanitizeValue($where['cond2'], $where['operand'] == 'LIKE');
          } else {
            $tmp[] = $where['cond2'];
          }
        } else {
          $ins = [];
          if (!is_array($where['cond2'])) {
            $ins = [$where['cond2']];
          } else {
            foreach ($where['cond2'] as $c2) {
              $ins[] = $this->_sanitizeValue($c2, false);
            }
          }
          $tmp[2] = $tmp[2] . '(' . implode(', ', $ins) . ')';
        }
        $statement[] = implode(' ', $tmp);
      }
    }
  }

  private function _buildGroupBy(&$statement)
  {
    if ($this->_operation != 'SELECT') {
      return;
    }
    if (count($this->_groupBy)) {
      $statement[] = 'GROUP BY';
      $gbs = [];
      foreach ($this->_groupBy as $gb) {
        $gbs[] = $gb;
      }
      $statement[] = implode(', ', $gbs);
    }
  }

  private function _buildOrderBy(&$statement)
  {
    if ($this->_operation != 'SELECT') {
      return;
    }
    if (count($this->_orderBy)) {
      $statement[] = 'ORDER BY';
      $obs = [];
      foreach ($this->_orderBy as $ob) {
        $obs[] = $ob;
      }
      $statement[] = implode(', ', $obs);
      $statement[] = $this->_order;
    }
  }

  private function _buildLimit(&$statement)
  {
    if ($this->_offset > 0 && $this->_limit > 0) {
      $statement[] = 'LIMIT ' . $this->_offset . ', ' . $this->_limit;
    } elseif ($this->_offset > 0) {
      $statement[] = 'OFFSET ' . $this->_offset;
    } elseif ($this->_limit > 0) {
      $statement[] = 'LIMIT ' . $this->_limit;
    }
  }
}
