<?php
/*
 * Copyright (c) 2013, webvariants GbR, http://www.webvariants.de
 *
 * This file is released under the terms of the MIT license. You can find the
 * complete text in the attached LICENSE file or online at:
 *
 * http://www.opensource.org/licenses/mit-license.php
 */

class WV_QueryBuilder {
	protected $filter;    ///< WV_IFilter
	protected $prefix;    ///< string
	protected $select;    ///< string
	protected $distinct;  ///< boolean
	protected $tableName; ///< string
	protected $limit;     ///< int
	protected $offset;    ///< int
	protected $sortBy;    ///< string
	protected $joins;     ///< array

	/**
	 * constructor
	 *
	 * @param WV_IFilter $filter  the filter to use
	 */
	public function __construct(WV_IFilter $filter) {
		$this->filter = $filter;
		$this->joins  = array();
	}

	/**
	 * get new instance
	 *
	 * @param  WV_IFilter $filter  the filter to use
	 * @return QueryBuilder        new builder instance
	 */
	public static function getInstance(WV_IFilter $filter) {
		return new self($filter);
	}

	/**
	 * select
	 *
	 * @param  string $select
	 * @return QueryBuilder    this
	 */
	public function select($select) {
		$this->select = $select;
		return $this;
	}

	/**
	 * distinct
	 *
	 * @param  boolean $flag
	 * @return QueryBuilder    this
	 */
	public function distinct($flag = true) {
		$this->distinct = (boolean) $flag;
		return $this;
	}

	/**
	 * from
	 *
	 * @param  string $tableName
	 * @return QueryBuilder       this
	 */
	public function from($tableName) {
		$this->tableName = $tableName;
		return $this;
	}

	/**
	 * where
	 *
	 * @param  WV_IFilter    $filter
	 * @return QueryBuilder  this
	 */
	public function where(WV_IFilter $filter) {
		$this->filter = $filter;
		return $this;
	}

	/**
	 * limit
	 *
	 * @param  int $limit
	 * @return QueryBuilder  this
	 */
	public function limit($limit) {
		$this->limit = $limit;
		return $this;
	}

	/**
	 * offset
	 *
	 * @param  int $offset
	 * @return QueryBuilder  this
	 */
	public function offset($offset) {
		$this->offset = $offset;
		return $this;
	}

	/**
	 * sort by
	 *
	 * @param  string $sortBy
	 * @return QueryBuilder    this
	 */
	public function sortBy($sortBy) {
		$this->sortBy = $sortBy;
		return $this;
	}

	/**
	 * add a join
	 *
	 * This method is used to make the query builder aware of other tables. You
	 * need to provide join statements for all used tables inside your filter.
	 *
	 * Use collectJoins() to prefill the instance with some already known joins.
	 *
	 * @param  string $masterTable  the master table
	 * @param  string $joinedTable  the table to join
	 * @param  string $joinStmt     how to join the $joinedTable to $masterTable
	 * @return QueryBuilder         this
	 */
	public function addJoin($masterTable, $joinedTable, $joinStmt) {
		$this->joins[$masterTable][$joinedTable] = $joinStmt;
		return $this;
	}

	public function collectJoins() {
		// some shortcuts
		$p  = sly_Core::getTablePrefix();
		$a  = $p.'article';
		$as = $p.'article_slice';
		$c  = $p.'clang';
		$f  = $p.'file';
		$fc = $p.'file_category';
		$s  = $p.'slice';
		$sv = $p.'slice_value';

		// default joins
		$this->joins[$a][$as] = 'JOIN %slave% AS %alias% ON %master%.id = %slave%.article_id AND %master%.clang = %slave%.clang';
		$this->joins[$a][$c]  = 'JOIN %slave% AS %alias% ON %master%.clang = %slave%.id';
		$this->joins[$as][$c] = $this->joins[$a][$c];
		$this->joins[$f][$fc] = 'LEFT JOIN %slave% AS %alias% ON %master%.category_id = %slave%.id';
		$this->joins[$as][$s] = 'JOIN %slave% AS %alias% ON %master%.slice_id = %slave%.id';
		$this->joins[$s][$sv] = 'JOIN %slave% AS %alias% ON %master%.id = %slave%.slice_id';

		// extra joins from addOns
		$this->joins = sly_Core::dispatcher()->filter('WV_QUERYBUILDER_JOINS', $this->joins, array(
			'builder' => $this,
			'filter'  => $this->filter,
			'master'  => $this->tableName
		));

		return $this;
	}

	/**
	 * get query
	 *
	 * @return string
	 */
	public function getQuery() {
		if ($this->tableName === null) {
			throw new Exception('No table name given. Did you forget to call from()?');
		}

		if ($this->select === null) {
			throw new Exception('No SELECT statement given. Did you forget to call select()?');
		}

		$joins = $this->getJoins();
		$query = sprintf('SELECT %s%s FROM %s', $this->distinct ? 'DISTINCT ' : '', $this->select, $this->tableName);

		foreach ($joins as $join) {
			$query .= ' '.$join;
		}

		$filterSQL = $this->filter->toSQL();
		$query    .= ' WHERE '.(mb_strlen($filterSQL) === 0 ? '1' : $filterSQL);

		if ($this->sortBy !== null) {
			$query .= ' '.$this->sortBy;
		}

		if ($this->limit !== null && $this->offset !== null) {
			$query .= ' LIMIT '.$this->offset.','.$this->limit;
		}
		elseif ($this->limit !== null) {
			$query .= ' LIMIT '.$this->limit;
		}
		elseif ($this->offset !== null) {
			$query .= ' LIMIT '.$this->offset.',64576351735613';
		}

		return $query;
	}

	/**
	 * @return array
	 */
	protected function getJoins() {
		$requiredTables = $this->filter->getRequiredTables();
		$requiredTables = array_unique($requiredTables);
		$joins          = array();
		$master         = $this->tableName;

		foreach ($requiredTables as $table) {
			// tables can contain both the table name and an alias.
			$parts = explode('/', $table, 2);
			if (!isset($parts[1])) $parts[1] = $parts[0];

			$table = $parts[0];
			$alias = $parts[1];

			if ($table === $master) {
				continue;
			}

			if (!isset($this->joins[$master][$table])) {
				throw new Exception(sprintf('Don\'t know how to join %s to %s. Did you forget to call addJoin()?', $table, $master));
			}

			$placeholders = array('%master%', '%slave%', '%alias%');
			$replacements = array($master, $table, $alias);
			$join         = $this->joins[$master][$table];

			$joins[] = str_replace($placeholders, $replacements, $join);
		}

		return $joins;
	}
}
