<?php

/**
 * @link http://www.yiiframework.com/
 * @copyright Copyright (c) 2008 Yii Software LLC
 * @license http://www.yiiframework.com/license/
 */

namespace onespace\tools\components\azuredb;

use MicrosoftAzure\Storage\Table\Models\EdmType;
use yii\base\InvalidParamException;
use yii\base\NotSupportedException;
use yii\db\ExpressionInterface;
use yii\db\QueryBuilder as DbQueryBuilder;
use yii\helpers\ArrayHelper;

/**
 * QueryBuilder builds a SELECT SQL statement based on the specification given as a [[Query]] object.
 *
 * SQL statements are created from [[Query]] objects using the [[build()]]-method.
 *
 * QueryBuilder is also used by [[Command]] to build SQL statements such as INSERT, UPDATE, DELETE, CREATE TABLE.
 *
 * For more details and usage information on QueryBuilder, see the [guide article on query builders](guide:db-query-builder).
 *
 * @property string[] $conditionClasses Map of condition aliases to condition classes. For example: ```php
 * ['LIKE' => yii\db\condition\LikeCondition::class] ``` . This property is write-only.
 * @property string[] $expressionBuilders Array of builders that should be merged with the pre-defined ones in
 * [[expressionBuilders]] property. This property is write-only.
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @since 2.0
 */
class QueryBuilder extends DbQueryBuilder {
    const OPERATOR_EQUAL = 'eq';
    const OPERATOR_GREATER_THAN = 'gt';
    const OPERATOR_GREATER_THAN_EQUAL = 'ge';
    const OPERATOR_LESS_THAN = 'lt';
    const OPERATOR_LESS_THAN_EQUAL = 'le';
    const OPERATOR_NOT_EQUAL = 'ne';

    public $operators = [
        self::OPERATOR_EQUAL,
        self::OPERATOR_GREATER_THAN,
        self::OPERATOR_GREATER_THAN_EQUAL,
        self::OPERATOR_LESS_THAN,
        self::OPERATOR_LESS_THAN_EQUAL,
        self::OPERATOR_NOT_EQUAL
    ];

    /**
     * @var array
     */
    protected $conditionFilters = [
        'NOT' => 'buildNotCondition',
        'AND' => 'buildAndCondition',
        'OR' => 'buildOrCondition',
        'BETWEEN' => 'buildBetweenCondition',
        'NOT BETWEEN' => 'buildBetweenCondition',
        'IN' => 'buildInCondition',
        'NOT IN' => 'buildInCondition',
    ];

    /**
     * Constructor.
     * @param Connection $connection the database connection.
     * @param array $config name-value pairs that will be used to initialize the object properties
     */
    public function __construct($connection, $config = []) {
        // $this->adb = $connection;
        parent::__construct($config);
    }

    /**
     * Generates a SELECT SQL statement from a [[Query]] object.
     *
     * @param Query $query the [[Query]] object from which the SQL statement will be generated.
     * @param array $params the parameters to be bound to the generated SQL statement. These parameters will
     * be included in the result with the additional parameters generated during the query building process.
     * @return array the generated SQL statement (the first array element) and the corresponding
     * parameters to be bound to the SQL statement (the second array element). The parameters returned
     * include those provided in `$params`.
     */
    public function build($query, $params = []) {
        $query = $query->prepare($this);
        // Yii::debug('Building query: '.json_encode($query), __METHOD__);

        $commandData = [
            'table' => $query->from,
            'selectFields' => $query->select,
            'clientWhere' => $query->where,
            'limit' => $query->limit,
            'offset' => $query->offset,
            'sort' => $query->orderBy,
            'serverOffsettingLimiting' => ($query->defaultSort && $query->where == null),
            'filterString' => (($query->whereDb != null) ? $this->buildCondition($query->whereDb) : '')
        ];

        return $commandData;
    }

    public function buildQueryFunction($condition) {
        if (is_array($condition)) {
            if (empty($condition)) {
                return '';
            }

            $condition = $this->buildCondition($condition);
        }

        if ($condition instanceof ExpressionInterface) {
            throw new NotSupportedException('Azure does not support expressions');
        }

        $stringExpression = (string)$condition;

        return $stringExpression;
    }

    /**
     * Creates an INSERT SQL statement.
     * For example,
     * ```php
     * $sql = $queryBuilder->insert('user', [
     *     'name' => 'Sam',
     *     'age' => 30,
     * ], $params);
     * ```
     * The method will properly escape the table and column names.
     *
     * @param string $table the table that new rows will be inserted into.
     * @param array $columns the column data ([name => value], or [name => ['value' => value, 'type' => type]]) to be inserted into the table. MUST be in their correct format already.
     * @param array $params Legacy, no longer used.
     * @return string the INSERT SQL
     */
    public function insert($table, $columns, &$params = []) {
        $commandData = [
            'mode' => Command::MODE_INSERT_ENTITY,
            'table' => $table,
            'entity' => []
        ];

        foreach ($columns as $name => $definition) {
            if (is_array($definition)) {
                $value = ArrayHelper::getValue($definition, 'value', null);
                $entity = [
                    'value' => $value,
                    'type' => ArrayHelper::getValue($definition, 'type', $value)
                ];
            } else {
                $entity = [
                    'value' => $definition,
                    'type' => Query::propertyType($definition)
                ];
            }
            $commandData['entity'][$name] = $entity;
        }

        return $commandData;
    }

    /**
     * Creates an SQL statement to insert rows into a database table if
     * they do not already exist (matching unique constraints),
     * or update them if they do.
     *
     * For example,
     *
     * ```php
     * $sql = $queryBuilder->upsert('pages', [
     *     'name' => 'Front page',
     *     'url' => 'http://example.com/', // url is unique
     *     'visits' => 0,
     * ], [
     *     'visits' => new \yii\db\Expression('visits + 1'),
     * ], $params);
     * ```
     *
     * The method will properly escape the table and column names.
     *
     * @param string $table the table that new rows will be inserted into/updated in.
     * @param array|Query $insertColumns the column data (name => value) to be inserted into the table or instance
     * of [[Query]] to perform `INSERT INTO ... SELECT` SQL statement.
     * @param array|bool $updateColumns the column data (name => value) to be updated if they already exist.
     * If `true` is passed, the column data will be updated to match the insert column data.
     * If `false` is passed, no update will be performed if the column data already exists.
     * @param array $params the binding parameters that will be generated by this method.
     * They should be bound to the DB command later.
     * @return string the resulting SQL.
     * @throws NotSupportedException if this is not supported by the underlying DBMS.
     * @since 2.0.14
     */
    public function upsert($table, $insertColumns, $updateColumns = true, &$params = []) {
        throw new NotSupportedException($this->db->getDriverName() . ' does not support upsert statements.');
    }


    /**
     * Creates an UPDATE SQL statement.
     *
     * For example,
     *
     * ```php
     * $params = [];
     * $sql = $queryBuilder->update('user', ['status' => 1], 'age > 30', $params);
     * ```
     *
     * The method will properly escape the table and column names.
     *
     * @param string $table the table to be updated.
     * @param array $columns the column data ([name => value], or [name => ['value' => value, 'type' => type]]) to be updated.
     * @param array $condition the primary key of the record ([PartitionKey, RowKey]) to be updated
     * @param array $params the binding parameters that will be modified by this method
     * so that they can be bound to the DB command later.
     * @return string the UPDATE SQL
     */
    public function update($table, $columns, $condition = [], &$params = []) {
        $commandData = [
            'mode' => Command::MODE_UPDATE_ENTITY,
            'table' => $table,
            Command::ENTITY_PARTITION_KEY => $condition[0],
            Command::ENTITY_ROW_KEY => $condition[1],
            'entity' => []
        ];

        foreach ($columns as $name => $definition) {
            if (is_array($definition)) {
                $value = ArrayHelper::getValue($definition, 'value', null);
                $entity = [
                    'value' => $value,
                    'type' => ArrayHelper::getValue($definition, 'type', $value)
                ];
            } else {
                $entity = [
                    'value' => $definition,
                    'type' => Query::propertyType($definition)
                ];
            }
            $commandData['entity'][$name] = $entity;
        }

        return $commandData;
    }

    /**
     * Creates a DELETE SQL statement.
     *
     * For example,
     *
     * ```php
     * $sql = $queryBuilder->delete('user', 'status = 0');
     * ```
     *
     * The method will properly escape the table and column names.
     *
     * @param string $table the table where the data will be deleted from.
     * @param array|string $condition the condition that will be put in the WHERE part. Please
     * refer to [[Query::where()]] on how to specify condition.
     * @param array $params the binding parameters that will be modified by this method
     * so that they can be bound to the DB command later.
     * @return string the DELETE SQL
     */
    public function delete($table, $condition, &$params = []) {
        $commandData = [
            'mode' => Command::MODE_DELETE_ENTITY,
            'table' => $table,
            Command::ENTITY_PARTITION_KEY => ArrayHelper::getValue($condition, 'PartitionKey'),
            Command::ENTITY_ROW_KEY => ArrayHelper::getValue($condition, 'RowKey'),
        ];

        return $commandData;
    }


    /**
     * Builds filter conditions.
     *
     * @param array $data data to be filtered
     * @param array $condition filter condition
     *
     * @return array filtered data
     *
     * @throws InvalidParamException
     */
    public function buildCondition($condition, &$params = []) {
        if (!is_array($condition)) {
            throw new InvalidParamException('Condition must be an array');
        }

        if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ...
            $operator = strtoupper($condition[0]);
            if (isset($this->conditionFilters[$operator])) {
                $method = $this->conditionFilters[$operator];
            } else {
                $method = 'buildSimpleCondition';
            }

            array_shift($condition);

            return $this->$method($operator, $condition);
        } else { // hash format: 'column1' => 'value1', 'column2' => 'value2', ...
            return $this->buildHashCondition($condition);
        }
    }

    /**
     * Builds a condition based on column-value pairs.
     *
     * @param array $data data to be filtered
     * @param array $condition the condition specification
     *
     * @return array filtered data
     */
    public function buildHashCondition($condition, &$params = []) {
        $comparisons = [];
        foreach ($condition as $column => $value) {
            if (is_array($value)) {
                // IN condition
                $comparisons[] = $this->buildInCondition('IN', [$column, $value]);
            } else {
                $comparisons[] = $this->buildComparison($column, self::OPERATOR_EQUAL, $value);
            }
        }

        return '(' . implode(') and (', $comparisons) . ')';
    }

    /**
     * Builds 2 or more conditions using 'AND' logic.
     *
     * @param array $data data to be filtered
     * @param string $operator operator
     * @param array $operands conditions to be united
     *
     * @return array filtered data
     */
    public function buildAndCondition($operator, $operands, &$params = []) {
        $conditions = [];
        foreach ($operands as $operand) {
            if (is_array($operand)) {
                $conditions[] = $this->buildCondition($operand);
            }
        }

        return '(' . implode(') and (', $conditions) . ')';
    }

    /**
     * Builds 2 or more conditions using 'OR' logic.
     *
     * @param array $data data to be filtered
     * @param string $operator operator
     * @param array $operands conditions to be united
     *
     * @return array filtered data
     */
    protected function buildOrCondition($operator, $operands) {
        $parts = [];
        foreach ($operands as $operand) {
            if (is_array($operand)) {
                $parts[] = $this->buildCondition($operand);
            }
        }

        if (empty($parts)) {
            return '';
        }

        return '(' . implode(') or (', $parts) . ') ';
    }

    /**
     * Inverts a filter condition.
     *
     * @param array $data data to be filtered
     * @param string $operator operator
     * @param array $operands operands to be inverted
     *
     * @return array filtered data
     *
     * @throws InvalidParamException if wrong number of operands have been given
     */
    public function buildNotCondition($operator, $operands, &$params = []) {
        if (count($operands) != 1) {
            throw new InvalidParamException("Operator '$operator' requires exactly one operand.");
        }

        $operand = reset($operands);
        $condition = $this->buildCondition($operand);
        return 'not (' . $condition . ')';
    }

    /**
     * Builds `BETWEEN` condition.
     *
     * @param array $data data to be filtered
     * @param string $operator operator
     * @param array $operands the first operand is the column name. The second and third operands
     * describe the interval that column value should be in
     *
     * @return array filtered data
     *
     * @throws InvalidParamException if wrong number of operands have been given
     */
    public function buildBetweenCondition($operator, $operands, &$params = []) {
        if (!isset($operands[0], $operands[1], $operands[2])) {
            throw new InvalidParamException("Operator '$operator' requires three operands.");
        }

        list($column, $value1, $value2) = $operands;

        if (strncmp('NOT', $operator, 3) === 0) {
            $comparisons = [
                $this->buildComparison($column, self::OPERATOR_LESS_THAN, $value1),
                $this->buildComparison($column, self::OPERATOR_GREATER_THAN, $value2)
            ];
            $join = 'or';
        } else {
            $comparisons = [
                $this->buildComparison($column, self::OPERATOR_GREATER_THAN_EQUAL, $value1),
                $this->buildComparison($column, self::OPERATOR_LESS_THAN_EQUAL, $value2)
            ];
            $join = 'and';
        }
        return '(' . implode(") $join (", $comparisons) . ')';
    }

    /**
     * Builds 'IN' condition.
     *
     * @param array $data data to be filtered
     * @param string $operator operator
     * @param array $operands the first operand is the column name.
     * The second operand is an array of values that column value should be among
     *
     * @return array filtered data
     *
     * @throws InvalidParamException if wrong number of operands have been given
     */
    public function buildInCondition($operator, $operands, &$params = []) {
        if (!isset($operands[0], $operands[1])) {
            throw new InvalidParamException("Operator '$operator' requires two operands.");
        }

        list($column, $values) = $operands;

        if ($values === [] || $column === []) {
            return '';
        }

        $values = (array) $values;

        if (count((array) $column) > 1) {
            throw new InvalidParamException("Operator '$operator' allows only a single column.");
        }

        if (is_array($column)) {
            $column = reset($column);
        }

        foreach ($values as $i => $value) {
            if (is_array($value)) {
                $values[$i] = isset($value[$column]) ? $value[$column] : null;
            }
        }

        if (strncmp('NOT', $operator, 3) === 0) {
            $liveOperator = self::OPERATOR_NOT_EQUAL;
            $join = 'and';
        } else {
            $liveOperator = self::OPERATOR_EQUAL;
            $join = 'or';
        }

        $comparisons = [];
        foreach ($values as $value) {
            $comparisons[] = $this->buildComparison($column, $liveOperator, $value);
        }
        return '(' . implode(") $join (", $comparisons) . ')';
    }

    /**
     * Builds comparison condition, e.g. `column operator value`.
     *
     * @param array $data data to be filtered
     * @param string $operator operator
     * @param array $operands
     *
     * @return array filtered data
     *
     * @throws InvalidParamException if wrong number of operands have been given or operator is not supported
     *
     * @since 1.0.4
     */
    public function buildSimpleCondition($operator, $operands, &$params = []) {
        if (count($operands) !== 2) {
            throw new InvalidParamException("Operator '$operator' requires two operands.");
        }
        list($column, $value) = $operands;


        switch ($operator) {
            case '=':
            case '==':
            case self::OPERATOR_EQUAL:
                $liveOperator = self::OPERATOR_EQUAL;
                break;
            case '!=':
            case '<>':
            case self::OPERATOR_NOT_EQUAL:
                $liveOperator = self::OPERATOR_NOT_EQUAL;
                break;
            case '>':
            case self::OPERATOR_GREATER_THAN:
                $liveOperator = self::OPERATOR_GREATER_THAN;
                break;
            case '<':
            case self::OPERATOR_LESS_THAN:
                $liveOperator = self::OPERATOR_LESS_THAN;
                break;
            case '>=':
            case self::OPERATOR_GREATER_THAN_EQUAL:
                $liveOperator = self::OPERATOR_GREATER_THAN_EQUAL;
                break;
            case '<=':
            case self::OPERATOR_LESS_THAN_EQUAL:
                $liveOperator = self::OPERATOR_LESS_THAN_EQUAL;
                break;
            default:
                throw new InvalidParamException("Operator '$operator' is not supported.");
        }

        return $this->buildComparison($column, $liveOperator, $value);
    }

    public function buildComparison($column, $operator, $value) {
        if ($value === null) {
            $value = '';
        }


        return $column . ' ' . $operator . ' ' . EdmType::serializeQueryValue(Query::propertyType($value), $value);
    }

    public function createTable($table, $columns, $options = null) {
        $commandData = [
            'mode' => Command::MODE_CREATE_TABLE,
            'table' => $table
        ];
        return $commandData;
    }


    public function dropTable($table) {
        $commandData = [
            'mode' => Command::MODE_DELETE_TABLE,
            'table' => $table
        ];
        return $commandData;
    }
}
