<?php

/**
 * Copyright © 2023 ExacTI Technology Solutions. All rights reserved.
 * GPLv3 General License.
 * https://exacti.com.br
 * Phacil PHP Framework - https://github.com/exacti/phacil-framework
 */

namespace Phacil\Framework\MagiQL\Manipulation;

use Phacil\Framework\MagiQL\Syntax\SyntaxFactory;
use Phacil\Framework\MagiQL\Syntax\Table;
use Phacil\Framework\MagiQL\Syntax\Where;
use Phacil\Framework\MagiQL\Syntax\OrderBy;

/**
 * Class Select.
 */
class Select extends AbstractBaseQuery
{
    /**
     * @var Table
     */
    protected $table;

    /**
     * @var array
     */
    protected $groupBy = [];

    /**
     * @var string
     */
    protected $camelCaseTableName = '';

    /**
     * @var Where
     */
    protected $having;

    /**
     * @var string
     */
    protected $havingOperator = 'AND';

    /**
     * @var bool
     */
    protected $isDistinct = false;

    /**
     * @var Where
     */
    protected $where;

    /**
     * @var JoinQuery
     */
    protected $joinQuery;

    /**
     * @var ColumnQuery
     */
    protected $columnQuery;

    /**
     * @var ParentQuery
     */
    protected $parentQuery;

    /**
     * @param string $table
     * @param array  $columns
     */
    public function __construct($table = null, array $columns = null)
    {
        if (isset($table)) {
            $this->setTable($table);
        }

        $this->joinQuery = new JoinQuery($this);
        $this->columnQuery = new ColumnQuery($this, $this->joinQuery, $columns);
    }

    /**
     * This __clone method will create an exact clone but without the object references due to the fact these
     * are lost in the process of serialization and un-serialization.
     *
     * @return Select
     */
    public function __clone()
    {
        return \unserialize(\serialize($this));
    }

    /**
     * @return string
     */
    public function partName()
    {
        return 'SELECT';
    }

    /**
     * @param string   $table
     * @param string   $selfColumn
     * @param string   $refColumn
     * @param string[] $columns
     *
     * @return Select
     */
    public function leftJoin($table, $selfColumn = null, $refColumn = null, $columns = [])
    {
        return $this->joinQuery->leftJoin($table, $selfColumn, $refColumn, $columns);
    }

    /**
     * @param string   $table
     * @param string   $selfColumn
     * @param string   $refColumn
     * @param string[] $columns
     * @param string   $joinType
     *
     * @return Select
     */
    public function join(
        $table,
        $selfColumn = null,
        $refColumn = null,
        $columns = [],
        $joinType = null
    ) {
        return $this->joinQuery->join($table, $selfColumn, $refColumn, $columns, $joinType);
    }

    /**
     * WHERE constrains used for the ON clause of a (LEFT/RIGHT/INNER/CROSS) JOIN.
     *
     * @return Where
     */
    public function joinCondition()
    {
        return $this->joinQuery->joinCondition();
    }

    /**
     * @param Select $select
     * @param string $selfColumn
     * @param string $refColumn
     *
     * @return Select
     */
    public function addJoin(Select $select, $selfColumn, $refColumn)
    {
        return $this->joinQuery->addJoin($select, $selfColumn, $refColumn);
    }

    /**
     * Transforms Select in a joint.
     *
     * @param bool $isJoin
     *
     * @return JoinQuery
     */
    public function isJoin($isJoin = true)
    {
        return $this->joinQuery->setJoin($isJoin);
    }

    /**
     * @param string   $table
     * @param string   $selfColumn
     * @param string   $refColumn
     * @param string[] $columns
     *
     * @internal param null $selectClass
     *
     * @return Select
     */
    public function rightJoin($table, $selfColumn = null, $refColumn = null, $columns = [])
    {
        return $this->joinQuery->rightJoin($table, $selfColumn, $refColumn, $columns);
    }

    /**
     * @param string   $table
     * @param string   $selfColumn
     * @param string   $refColumn
     * @param string[] $columns
     *
     * @return Select
     */
    public function crossJoin($table, $selfColumn = null, $refColumn = null, $columns = [])
    {
        return $this->joinQuery->crossJoin($table, $selfColumn, $refColumn, $columns);
    }

    /**
     * @param string   $table
     * @param string   $selfColumn
     * @param string   $refColumn
     * @param string[] $columns
     *
     * @return Select
     */
    public function innerJoin($table, $selfColumn = null, $refColumn = null, $columns = [])
    {
        return $this->joinQuery->innerJoin($table, $selfColumn, $refColumn, $columns);
    }

    /**
     * Alias to joinCondition.
     *
     * @return Where
     */
    public function on()
    {
        return $this->joinQuery->joinCondition();
    }

    /**
     * @return bool
     */
    public function isJoinSelect()
    {
        return $this->joinQuery->isJoin();
    }

    /**
     * @return array
     */
    public function getAllColumns()
    {
        return $this->columnQuery->getAllColumns();
    }

    /**
     * @return \Phacil\Framework\MagiQL\Syntax\Column[]
     *
     * @throws QueryException
     */
    public function getColumns()
    {
        return $this->columnQuery->getColumns();
    }

    /**
     * Sets the column names used to write the SELECT statement.
     * If key is set, key is the column's alias. Value is always the column names.
     *
     * @param string[] $columns
     *
     * @return ColumnQuery
     */
    public function setColumns(array $columns)
    {
        return $this->columnQuery->setColumns($columns);
    }
    
    /**
     * Sets the all columns used to write the SELECT statement.
     *
     * @return ColumnQuery
     */
    public function setAllColumns()
    {
        return $this->setColumns([\Phacil\Framework\MagiQL\Syntax\Column::ALL]);
    }

    /**
     * Allows setting a Select query as a column value.
     *
     * @param array $column
     *
     * @return ColumnQuery
     */
    public function setSelectAsColumn(array $column)
    {
        return $this->columnQuery->setSelectAsColumn($column);
    }

    /**
     * @return array
     */
    public function getColumnSelects()
    {
        return $this->columnQuery->getColumnSelects();
    }

    /**
     * Allows setting a value to the select statement.
     *
     * @param string $value
     * @param string $alias
     *
     * @return ColumnQuery
     */
    public function setValueAsColumn($value, $alias)
    {
        return $this->columnQuery->setValueAsColumn($value, $alias);
    }

    /**
     * @return array
     */
    public function getColumnValues()
    {
        return $this->columnQuery->getColumnValues();
    }

    /**
     * Allows calculation on columns using predefined SQL functions.
     *
     * @param string   $funcName
     * @param string[] $arguments
     * @param string   $alias
     *
     * @return ColumnQuery
     */
    public function setFunctionAsColumn($funcName, array $arguments, $alias)
    {
        return $this->columnQuery->setFunctionAsColumn($funcName, $arguments, $alias);
    }

    /**
     * @return array
     */
    public function getColumnFuncs()
    {
        return $this->columnQuery->getColumnFuncs();
    }

    /**
     * Returns all the Where conditions to the BuilderInterface class in order to write the SQL WHERE statement.
     *
     * @return array
     */
    public function getAllWheres()
    {
        return $this->getAllOperation($this->where, 'getAllWheres');
    }

    /**
     * @param null|Where $data
     * @param string     $operation
     *
     * @return array
     */
    protected function getAllOperation($data, $operation)
    {
        $collection = [];

        if (!is_null($data)) {
            $collection[] = $data;
        }

        foreach ($this->joinQuery->getJoins() as $join) {
            $collection = \array_merge($collection, $join->$operation());
        }

        return $collection;
    }

    /**
     * @return array
     */
    public function getAllHavings()
    {
        return $this->getAllOperation($this->having, 'getAllHavings');
    }

    /**
     * @param string $columnName
     * @param string $alias
     *
     * @return ColumnQuery
     */
    public function count($columnName = '*', $alias = '')
    {
        return $this->columnQuery->count($columnName, $alias);
    }

    /**
     * @return bool
     */
    public function isCount()
    {
        return $this->columnQuery->isCount();
    }

    /**
     * @param int $start
     * @param int $count
     *
     * @return $this
     */
    public function limit($start, $count = null)
    {
        $this->limitStart = $count === null ? 0 : $start;
        $this->limitCount = $count === null ? $start :$count;

        return $this;
    }

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

        return $this;
    }

    /**
     * @return array
     */
    public function getAllJoins()
    {
        return $this->joinQuery->getAllJoins();
    }

    /**
     * @return array
     */
    public function getGroupBy()
    {
        return SyntaxFactory::createColumns($this->groupBy, $this->getTable());
    }

    /**
     * @param string[] $columns
     *
     * @return $this
     */
    public function groupBy(array $columns)
    {
        $this->groupBy = $columns;

        return $this;
    }

    /**
     * @return Where
     */
    public function getJoinCondition()
    {
        return $this->joinQuery->getJoinCondition();
    }

    /**
     * @return string
     */
    public function getJoinType()
    {
        return $this->joinQuery->getJoinType();
    }

    /**
     * @param string|null $joinType
     *
     * @return $this
     */
    public function setJoinType($joinType)
    {
        $this->joinQuery->setJoinType($joinType);

        return $this;
    }

    /**
     * @param $havingOperator
     *
     * @throws QueryException
     *
     * @return Where
     */
    public function having($havingOperator = 'AND')
    {
        if (!isset($this->having)) {
            $this->having = QueryFactory::createWhere($this);
        }

        if (!in_array($havingOperator, array(Where::CONJUNCTION_AND, Where::CONJUNCTION_OR))) {
            throw new QueryException(
                "Invalid conjunction specified, must be one of AND or OR, but '".$havingOperator."' was found."
            );
        }

        $this->havingOperator = $havingOperator;

        return $this->having;
    }

    /**
     * @return string
     */
    public function getHavingOperator()
    {
        return $this->havingOperator;
    }

    /**
     * @return $this
     */
    public function distinct()
    {
        $this->isDistinct = true;

        return $this;
    }

    /**
     * @return bool
     */
    public function isDistinct()
    {
        return $this->isDistinct;
    }

    /**
     * @return array
     */
    public function getAllOrderBy()
    {
        return $this->orderBy;
    }

    /**
     * @return ParentQuery
     */
    public function getParentQuery()
    {
        return $this->parentQuery;
    }

    /**
     * @param Select $parentQuery
     *
     * @return $this
     */
    public function setParentQuery(Select $parentQuery)
    {
        $this->parentQuery = $parentQuery;

        return $this;
    }

    /**
     * @param string $column
     * @param string $direction
     * @param null   $table
     *
     * @return $this
     */
    public function orderBy($column, $direction = OrderBy::ASC, $table = null)
    {
        $current = parent::orderBy($column, $direction, $table);
        if ($this->getParentQuery() != null) {
            $this->getParentQuery()->orderBy($column, $direction, \is_null($table) ? $this->getTable() : $table);
        }
        return $current;
    }
}