<?php namespace Laravel\Database\Grammars;

use Laravel\Arr;
use Laravel\Database\Query;
use Laravel\Database\Expression;

class Grammar {

	/**
	 * The keyword identifier for the database system.
	 *
	 * @var string
	 */
	protected $wrapper = '"';

	/**
	 * All of the query componenets in the order they should be built.
	 *
	 * Each derived compiler may adjust these components and place them in the
	 * order needed for its particular database system, providing greater
	 * control over how the query is structured.
	 *
	 * @var array
	 */
	protected $components = array('aggregate', 'selects', 'from', 'joins', 'wheres', 'orderings', 'limit', 'offset');

	/**
	 * Compile a SQL SELECT statement from a Query instance.
	 *
	 * The query will be compiled according to the order of the elements specified
	 * in the "components" property. The entire query is passed into each component
	 * compiler for convenience.
	 *
	 * @param  Query   $query
	 * @return string
	 */
	final public function select(Query $query)
	{
		$sql = array();

		// Iterate through each query component, calling the compiler for that
		// component and passing the query instance into the compiler.
		foreach ($this->components as $component)
		{
			if ( ! is_null($query->$component))
			{
				$sql[] = call_user_func(array($this, $component), $query);
			}
		}

		return implode(' ', Arr::without($sql, array(null, '')));
	}

	/**
	 * Compile the SELECT clause for a query.
	 *
	 * @param  Query   $query
	 * @return string
	 */
	protected function selects(Query $query)
	{
		$select = ($query->distinct) ? 'SELECT DISTINCT ' : 'SELECT ';

		return $select.$this->columnize($query->selects);
	}

	/**
	 * Compile an aggregating SELECT clause for a query.
	 *
	 * If an aggregate function is called on the query instance, no select
	 * columns will be set, so it is safe to assume that the "selects"
	 * compiler function will not be called. We can simply build the
	 * aggregating select clause within this function.
	 *
	 * @param  Query   $query
	 * @return string
	 */
	protected function aggregate(Query $query)
	{
		return 'SELECT '.$query->aggregate['aggregator'].'('.$this->wrap($query->aggregate['column']).')';
	}

	/**
	 * Compile the FROM clause for a query.
	 *
	 * This method should not handle the construction of "join" clauses.
	 * The join clauses will be constructured by their own compiler.
	 *
	 * @param  Query   $query
	 * @return string
	 */
	protected function from(Query $query)
	{
		return 'FROM '.$this->wrap($query->from);
	}

	/**
	 * Compile the JOIN clauses for a query.
	 *
	 * @param  Query   $query
	 * @return string
	 */
	protected function joins(Query $query)
	{
		// Since creating a JOIN clause using string concatenation is a little
		// cumbersome, we will create a format we can pass to "sprintf" to
		// make things cleaner.
		$format = '%s JOIN %s ON %s %s %s';

		foreach ($query->joins as $join)
		{
			$table = $this->wrap($join['table']);

			$column1 = $this->wrap($join['column1']);

			$column2 = $this->wrap($join['column2']);

			$sql[] = sprintf($format, $join['type'], $table, $column1, $join['operator'], $column2);
		}

		return implode(' ', $sql);
	}

	/**
	 * Compile the WHERE clause for a query.
	 *
	 * @param  Query   $query
	 * @return string
	 */
	final protected function wheres(Query $query)
	{
		// Each WHERE clause array has a "type" that is assigned by the query
		// builder, and each type has its own compiler function. We will simply
		// iterate through the where clauses and call the appropriate compiler
		// for each clause.
		foreach ($query->wheres as $where)
		{
			$sql[] = $where['connector'].' '.$this->{$where['type']}($where);
		}

		if (isset($sql)) return implode(' ', array_merge(array('WHERE 1 = 1'), $sql));
	}

	/**
	 * Compile a simple WHERE clause.
	 *
	 * This method handles the compilation of the structures created by the
	 * "where" and "or_where" methods on the query builder.
	 *
	 * This method also handles database expressions, so care must be taken
	 * to implement this functionality in any derived database grammars.
	 *
	 * @param  array   $where
	 * @return string
	 */
	protected function where($where)
	{
		return $this->wrap($where['column']).' '.$where['operator'].' '.$this->parameter($where['value']);
	}

	/**
	 * Compile a WHERE IN clause.
	 *
	 * @param  array   $where
	 * @return string
	 */
	protected function where_in($where)
	{
		return $this->wrap($where['column']).' IN ('.$this->parameterize($where['values']).')';
	}

	/**
	 * Compile a WHERE NOT IN clause.
	 *
	 * @param  array   $where
	 * @return string
	 */
	protected function where_not_in($where)
	{
		return $this->wrap($where['column']).' NOT IN ('.$this->parameterize($where['values']).')';
	}

	/**
	 * Compile a WHERE NULL clause.
	 *
	 * @param  array   $where
	 * @return string
	 */
	protected function where_null($where)
	{
		return $this->wrap($where['column']).' IS NULL';
	}

	/**
	 * Compile a WHERE NULL clause.
	 *
	 * @param  array   $where
	 * @return string
	 */
	protected function where_not_null($where)
	{
		return $this->wrap($where['column']).' IS NOT NULL';
	}

	/**
	 * Compile a raw WHERE clause.
	 *
	 * @param  string  $where
	 * @return string
	 */
	protected function where_raw($where)
	{
		return $where;
	}

	/**
	 * Compile the ORDER BY clause for a query.
	 *
	 * @param  Query   $query
	 * @return string
	 */
	protected function orderings(Query $query)
	{
		foreach ($query->orderings as $ordering)
		{
			$sql[] = $this->wrap($ordering['column']).' '.strtoupper($ordering['direction']);
		}

		return 'ORDER BY '.implode(', ', $sql);
	}

	/**
	 * Compile the LIMIT clause for a query.
	 *
	 * @param  Query   $query
	 * @return string
	 */
	protected function limit(Query $query)
	{
		return 'LIMIT '.$query->limit;
	}

	/**
	 * Compile the OFFSET clause for a query.
	 *
	 * @param  Query   $query
	 * @return string
	 */
	protected function offset(Query $query)
	{
		return 'OFFSET '.$query->offset;
	}

	/**
	 * Compile a SQL INSERT statment from a Query instance.
	 *
	 * Note: This method handles the compilation of single row inserts and batch inserts.
	 *
	 * @param  Query   $query
	 * @param  array   $values
	 * @return string
	 */
	public function insert(Query $query, $values)
	{
		// Force every insert to be treated like a batch insert. This simple makes
		// creating the SQL syntax a little easier on us since we can always treat
		// the values as if it is an array containing multiple inserts.
		if ( ! is_array(reset($values))) $values = array($values);

		// Since we only care about the column names, we can pass any of the insert
		// arrays into the "columnize" method. The names should be the same for
		// every insert to the table.
		$columns = $this->columnize(array_keys(reset($values)));

		// We need to create a string of comma-delimited insert segments. Each segment
		// contains PDO place-holders for each value being inserted into the table.
		$parameters = implode(', ', array_fill(0, count($values), '('.$this->parameterize(reset($values)).')'));

		return 'INSERT INTO '.$this->wrap($query->from).' ('.$columns.') VALUES '.$parameters;
	}

	/**
	 * Compile a SQL UPDATE statment from a Query instance.
	 *
	 * Note: Since UPDATE statements can be limited by a WHERE clause,
	 *       this method will use the same WHERE clause compilation
	 *       functions as the "select" method.
	 *
	 * @param  Query   $query
	 * @param  array   $values
	 * @return string
	 */
	public function update(Query $query, $values)
	{
		foreach ($values as $column => $value)
		{
			$columns = $this->wrap($column).' = '.$this->parameter($value);
		}

		return trim('UPDATE '.$this->wrap($query->from).' SET '.$columns.' '.$this->wheres($query));
	}

	/**
	 * Compile a SQL DELETE statment from a Query instance.
	 *
	 * @param  Query   $query
	 * @return string
	 */
	public function delete(Query $query)
	{
		return trim('DELETE FROM '.$this->wrap($query->from).' '.$this->wheres($query));
	}

	/**
	 * The following functions primarily serve as utility functions for
	 * the grammar. They perform tasks such as wrapping values in keyword
	 * identifiers or creating variable lists of bindings.
	 */

	/**
	 * Create a comma-delimited list of wrapped column names.
	 *
	 * @param  array   $columns
	 * @return string
	 */
	public function columnize($columns)
	{
		return implode(', ', array_map(array($this, 'wrap'), $columns));
	}

	/**
	 * Wrap a value in keyword identifiers.
	 *
	 * They keyword identifier used by the method is specified as
	 * a property on the grammar class so it can be conveniently
	 * overriden without changing the wrapping logic itself.
	 *
	 * @param  string  $value
	 * @return string
	 */
	public function wrap($value)
	{
		if (strpos(strtolower($value), ' as ') !== false) return $this->alias($value);

		// Expressions should be injected into the query as raw strings, so we
		// do not want to wrap them in any way. We will just return the string
		// value from the expression.
		if ($value instanceof Expression) return $value->get();

		foreach (explode('.', $value) as $segment)
		{
			$wrapped[] = ($segment !== '*') ? $this->wrapper.$segment.$this->wrapper : $segment;
		}

		return implode('.', $wrapped);
	}

	/**
	 * Wrap an alias in keyword identifiers.
	 *
	 * @param  string  $value
	 * @return string
	 */
	protected function alias($value)
	{
		$segments = explode(' ', $value);

		return $this->wrap($segments[0]).' AS '.$this->wrap($segments[2]);
	}

	/**
	 * Create query parameters from an array of values.
	 *
	 * @param  array   $values
	 * @return string
	 */
	public function parameterize($values)
	{
		return implode(', ', array_map(array($this, 'parameter'), $values));
	}

	/**
	 * Get the appropriate query parameter string for a value.
	 *
	 * If the value is an expression, the raw expression string should
	 * be returned, otherwise, the parameter place-holder will be
	 * returned by the method.
	 *
	 * @param  mixed   $value
	 * @return string
	 */
	public function parameter($value)
	{
		return ($value instanceof Expression) ? $value->get() : '?';
	}

}