<?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 yii\base\InvalidConfigException;
use yii\db\ActiveQueryInterface;
use yii\db\ActiveQueryTrait;
use yii\db\ActiveRelationTrait;
use yii\db\ArrayExpression;
use yii\helpers\ArrayHelper;

/**
 * ActiveQuery represents a DB query associated with an Active Record class.
 *
 * An ActiveQuery can be a normal query or be used in a relational context.
 *
 * ActiveQuery instances are usually created by [[ActiveRecord::find()]] and [[ActiveRecord::findBySql()]].
 * Relational queries are created by [[ActiveRecord::hasOne()]] and [[ActiveRecord::hasMany()]].
 *
 * Normal Query
 * ------------
 *
 * ActiveQuery mainly provides the following methods to retrieve the query results:
 *
 * - [[one()]]: returns a single record populated with the first row of data.
 * - [[all()]]: returns all records based on the query results.
 * - [[count()]]: returns the number of records.
 * - [[sum()]]: returns the sum over the specified column.
 * - [[average()]]: returns the average over the specified column.
 * - [[min()]]: returns the min over the specified column.
 * - [[max()]]: returns the max over the specified column.
 * - [[scalar()]]: returns the value of the first column in the first row of the query result.
 * - [[column()]]: returns the value of the first column in the query result.
 * - [[exists()]]: returns a value indicating whether the query result has data or not.
 *
 * Because ActiveQuery extends from [[Query]], one can use query methods, such as [[where()]],
 * [[orderBy()]] to customize the query options.
 *
 * ActiveQuery also provides the following additional query options:
 *
 * - [[with()]]: list of relations that this query should be performed with.
 * - [[joinWith()]]: reuse a relation query definition to add a join to a query.
 * - [[indexBy()]]: the name of the column by which the query result should be indexed.
 * - [[asArray()]]: whether to return each record as an array.
 *
 * These options can be configured using methods of the same name. For example:
 *
 * ```php
 * $customers = Customer::find()->with('orders')->asArray()->all();
 * ```
 *
 * Relational query
 * ----------------
 *
 * In relational context ActiveQuery represents a relation between two Active Record classes.
 *
 * Relational ActiveQuery instances are usually created by calling [[ActiveRecord::hasOne()]] and
 * [[ActiveRecord::hasMany()]]. An Active Record class declares a relation by defining
 * a getter method which calls one of the above methods and returns the created ActiveQuery object.
 *
 * A relation is specified by [[link]] which represents the association between columns
 * of different tables; and the multiplicity of the relation is indicated by [[multiple]].
 *
 * If a relation involves a junction table, it may be specified by [[via()]] or [[viaTable()]] method.
 * These methods may only be called in a relational context. Same is true for [[inverseOf()]], which
 * marks a relation as inverse of another relation and [[onCondition()]] which adds a condition that
 * is to be added to relational query join condition.
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @author Carsten Brandt <mail@cebe.cc>
 * @since 2.0
 */
class ActiveQuery extends Query implements ActiveQueryInterface {
    use ActiveQueryTrait;
    use ActiveRelationTrait;

    /**
     * @event Event an event that is triggered when the query is initialized via [[init()]].
     */
    const EVENT_INIT = 'init';

    /**
     * Constructor.
     * @param string $modelClass the model class associated with this query
     * @param array $config configurations to be applied to the newly created query object
     */
    public function __construct($modelClass, $config = []) {
        $this->modelClass = $modelClass;
        // Yii::debug('ActiveQuery construct: '.json_encode($config), __METHOD__);
        parent::__construct($config);
    }

    /**
     * Initializes the object.
     * This method is called at the end of the constructor. The default implementation will trigger
     * an [[EVENT_INIT]] event. If you override this method, make sure you call the parent implementation at the end
     * to ensure triggering of the event.
     */
    public function init() {
        parent::init();
        $this->trigger(self::EVENT_INIT);
    }

    /**
     * Executes query and returns all results as an array.
     * @param Connection $db the DB connection used to create the DB command.
     * If null, the DB connection returned by [[modelClass]] will be used.
     * @return array|ActiveRecord[] the query results. If the query results in nothing, an empty array will be returned.
     */
    public function all($db = null) {
        return parent::all($db);
    }

    /**
     * {@inheritdoc}
     */
    public function prepare($builder) {
        if (empty($this->from)) {
            $this->from = $this->getPrimaryTableName();
        }

        if (empty($this->select)) {
            /** @var $modelClass AzureActiveRecord */
            $modelClass = $this->modelClass;
            $this->select = array_keys($modelClass::columns());
        }

        if ($this->primaryModel === null) {
            // eager loading
            $query = Query::create($this);
        } else {
            // lazy loading of a relation
            $where = $this->where;

            if ($this->via instanceof self) {
                // via junction table
                $viaModels = $this->via->findJunctionRows([$this->primaryModel]);
                $this->filterByModels($viaModels);
            } elseif (is_array($this->via)) {
                // via relation
                /* @var $viaQuery ActiveQuery */
                list($viaName, $viaQuery, $viaCallableUsed) = $this->via;
                if ($viaQuery->multiple) {
                    if ($viaCallableUsed) {
                        $viaModels = $viaQuery->all();
                    } elseif ($this->primaryModel->isRelationPopulated($viaName)) {
                        $viaModels = $this->primaryModel->$viaName;
                    } else {
                        $viaModels = $viaQuery->all();
                        $this->primaryModel->populateRelation($viaName, $viaModels);
                    }
                } else {
                    if ($viaCallableUsed) {
                        $model = $viaQuery->one();
                    } elseif ($this->primaryModel->isRelationPopulated($viaName)) {
                        $model = $this->primaryModel->$viaName;
                    } else {
                        $model = $viaQuery->one();
                        $this->primaryModel->populateRelation($viaName, $model);
                    }
                    $viaModels = $model === null ? [] : [$model];
                }
                $this->filterByModels($viaModels);
            } else {
                $this->filterByModels([$this->primaryModel]);
            }

            $query = Query::create($this);
            $this->where = $where;
        }

        if (!empty($this->on)) {
            $query->andWhere($this->on);
        }

        $query->defaultSort = $this->defaultSort;

        return $query;
    }

    /**
     * {@inheritdoc}
     */
    public function populate($rows) {
        if (empty($rows)) {
            return [];
        }

        $rows = parent::populate($rows);


        // if (!empty($this->join) && $this->indexBy === null) {
        //     $models = $this->removeDuplicatedModels($models);
        // }
        // if (!empty($this->with)) {
        //     $this->findWith($this->with, $models);
        // }

        // if ($this->inverseOf !== null) {
        //     $this->addInverseRelations($models);
        // }

        if (!$this->asArray) {
            return $this->createModels($rows);
        }

        return $rows;
    }

    /**
     * Converts found rows into model instances.
     * @param array $rows
     * @return array|ActiveRecord[]
     * @since 2.0.11
     */
    protected function createModels($rows) {
        if ($this->asArray) {
            return $rows;
        } else {
            /* @var $class ActiveRecord */
            $class = $this->modelClass;
            foreach ($rows as &$row) {
                $model = $class::instantiate($row);
                $modelClass = get_class($model);
                $modelClass::populateRecord($model, $row);
                $model->afterFind();
                $row = $model;
            }
            return $rows;
        }
    }

    /**
     * Removes duplicated models by checking their primary key values.
     * This method is mainly called when a join query is performed, which may cause duplicated rows being returned.
     * @param array $models the models to be checked
     * @throws InvalidConfigException if model primary key is empty
     * @return array the distinctive models
     */
    // private function removeDuplicatedModels($models)
    // {
    //     $hash = [];
    //     /* @var $class ActiveRecord */
    //     $class = $this->modelClass;
    //     $pks = $class::primaryKey();

    //     if (count($pks) > 1) {
    //         // composite primary key
    //         foreach ($models as $i => $model) {
    //             $key = [];
    //             foreach ($pks as $pk) {
    //                 if (!isset($model[$pk])) {
    //                     // do not continue if the primary key is not part of the result set
    //                     break 2;
    //                 }
    //                 $key[] = $model[$pk];
    //             }
    //             $key = serialize($key);
    //             if (isset($hash[$key])) {
    //                 unset($models[$i]);
    //             } else {
    //                 $hash[$key] = true;
    //             }
    //         }
    //     } elseif (empty($pks)) {
    //         throw new InvalidConfigException("Primary key of '{$class}' can not be empty.");
    //     } else {
    //         // single column primary key
    //         $pk = reset($pks);
    //         foreach ($models as $i => $model) {
    //             if (!isset($model[$pk])) {
    //                 // do not continue if the primary key is not part of the result set
    //                 break;
    //             }
    //             $key = $model[$pk];
    //             if (isset($hash[$key])) {
    //                 unset($models[$i]);
    //             } elseif ($key !== null) {
    //                 $hash[$key] = true;
    //             }
    //         }
    //     }

    //     return array_values($models);
    // }

    /**
     * Executes query and returns a single row of result.
     * @param Connection|null $db the DB connection used to create the DB command.
     * If `null`, the DB connection returned by [[modelClass]] will be used.
     * @return ActiveRecord|array|null a single row of query result. Depending on the setting of [[asArray]],
     * the query result may be either an array or an ActiveRecord object. `null` will be returned
     * if the query results in nothing.
     */
    public function one($db = null) {
        // $row = parent::one($db);
        // if ($row !== false) {
        //     $models = $this->populate([$row]);
        //     return reset($models) ?: null;
        // }

        // return null;
        return parent::one($db);
    }

    /**
     * Returns the number of records.
     * @param string $q the COUNT expression. Defaults to '*'.
     * @param Connection $db the database connection used to execute the query.
     * If this parameter is not given, the `db` application component will be used.
     * @return int number of records.
     */
    public function count($q = '*', $db = null) {
        if ($this->emulateExecution) {
            return [];
        }

        // $tempSelect = $this->select;
        // $this->select('pk');
        $command = $this->createCommand($db);
        if (ArrayHelper::getValue($command->commandData, 'clientWhere') != null || ArrayHelper::getValue($command->commandData, 'sort') != null) {
            $rows = $command->queryAll();
            return count(parent::populate($rows));
        } else {
            $command->commandData['selectFields'] = ['PartitionKey', 'RowKey', 'Timestamp'];
            $rows = $command->queryAll();
            return count($rows);
        }
    }

    /**
     * Creates a DB command that can be used to execute this query.
     * @param Connection|null $db the DB connection used to create the DB command.
     * If `null`, the DB connection returned by [[modelClass]] will be used.
     * @return Command the created DB command instance.
     */
    public function createCommand($adb = null) {
        /* @var $modelClass ActiveRecord */
        $modelClass = $this->modelClass;
        if ($adb === null) {
            $adb = $modelClass::getDb();
        }

        return parent::createCommand($adb);
    }

    /**
     * {@inheritdoc}
     */
    protected function queryScalar($selectExpression, $db) {
        /* @var $modelClass ActiveRecord */
        $modelClass = $this->modelClass;
        if ($db === null) {
            $db = $modelClass::getDb();
        }

        if ($this->sql === null) {
            return parent::queryScalar($selectExpression, $db);
        }

        $command = (new Query())->select([$selectExpression])
            ->from(['c' => "({$this->sql})"])
            ->params($this->params)
            ->createCommand($db);
        $this->setCommandCache($command);

        return $command->queryScalar();
    }


    /**
     * Returns the table name and the table alias for [[modelClass]].
     * @return array the table name and the table alias.
     * @since 2.0.16
     */
    // protected function getTableNameAndAlias()
    // {
    //     if (empty($this->from)) {
    //         $tableName = $this->getPrimaryTableName();
    //     } else {
    //         $tableName = '';
    //         // if the first entry in "from" is an alias-tablename-pair return it directly
    //         foreach ($this->from as $alias => $tableName) {
    //             if (is_string($alias)) {
    //                 return [$tableName, $alias];
    //             }
    //             break;
    //         }
    //     }

    //     if (preg_match('/^(.*?)\s+({{\w+}}|\w+)$/', $tableName, $matches)) {
    //         $alias = $matches[2];
    //     } else {
    //         $alias = $tableName;
    //     }

    //     return [$tableName, $alias];
    // }

    /**
     * Sets the ON condition for a relational query.
     * The condition will be used in the ON part when [[ActiveQuery::joinWith()]] is called.
     * Otherwise, the condition will be used in the WHERE part of a query.
     *
     * Use this method to specify additional conditions when declaring a relation in the [[ActiveRecord]] class:
     *
     * ```php
     * public function getActiveUsers()
     * {
     *     return $this->hasMany(User::className(), ['id' => 'user_id'])
     *                 ->onCondition(['active' => true]);
     * }
     * ```
     *
     * Note that this condition is applied in case of a join as well as when fetching the related records.
     * Thus only fields of the related table can be used in the condition. Trying to access fields of the primary
     * record will cause an error in a non-join-query.
     *
     * @param string|array $condition the ON condition. Please refer to [[Query::where()]] on how to specify this parameter.
     * @param array $params the parameters (name => value) to be bound to the query.
     * @return $this the query object itself
     */
    public function onCondition($condition, $params = []) {
        $this->whereDb = $condition;
        // $this->addParams($params);
        return $this;
    }

    /**
     * Adds an additional ON condition to the existing one.
     * The new condition and the existing one will be joined using the 'AND' operator.
     * @param string|array $condition the new ON condition. Please refer to [[where()]]
     * on how to specify this parameter.
     * @param array $params the parameters (name => value) to be bound to the query.
     * @return $this the query object itself
     * @see onCondition()
     * @see orOnCondition()
     */
    public function andOnCondition($condition, $params = []) {
        if ($this->whereDb === null) {
            $this->whereDb = $condition;
        } else {
            $this->whereDb = ['and', $this->whereDb, $condition];
        }
        // $this->addParams($params);
        return $this;
    }

    /**
     * Adds an additional ON condition to the existing one.
     * The new condition and the existing one will be joined using the 'OR' operator.
     * @param string|array $condition the new ON condition. Please refer to [[where()]]
     * on how to specify this parameter.
     * @param array $params the parameters (name => value) to be bound to the query.
     * @return $this the query object itself
     * @see onCondition()
     * @see andOnCondition()
     */
    public function orOnCondition($condition, $params = []) {
        if ($this->whereDb === null) {
            $this->whereDb = $condition;
        } else {
            $this->whereDb = ['or', $this->whereDb, $condition];
        }
        // $this->addParams($params);
        return $this;
    }

    /**
     * {@inheritdoc}
     * @since 2.0.12
     */
    public function getTablesUsedInFrom() {
        if (empty($this->from)) {
            return $this->cleanUpTableNames([$this->getPrimaryTableName()]);
        }

        return parent::getTablesUsedInFrom();
    }

    /**
     * @return string primary table name
     * @since 2.0.12
     */
    protected function getPrimaryTableName() {
        /* @var $modelClass ActiveRecord */
        $modelClass = $this->modelClass;
        return $modelClass::tableName();
    }

    /**
     * @param array $models
     */
    private function filterByModels($models) {
        $attributes = array_keys($this->link);

        $attributes = $this->prefixKeyColumns($attributes);

        $values = [];
        if (count($attributes) === 1) {
            // single key
            $attribute = reset($this->link);
            foreach ($models as $model) {
                if (($value = $model[$attribute]) !== null) {
                    if (is_array($value)) {
                        $values = array_merge($values, $value);
                    } elseif ($value instanceof ArrayExpression && $value->getDimension() === 1) {
                        $values = array_merge($values, $value->getValue());
                    } else {
                        $values[] = $value;
                    }
                }
            }
            if (empty($values)) {
                $this->emulateExecution();
            }
        } else {
            // composite keys

            // ensure keys of $this->link are prefixed the same way as $attributes
            $prefixedLink = array_combine($attributes, $this->link);
            foreach ($models as $model) {
                $v = [];
                foreach ($prefixedLink as $attribute => $link) {
                    $v[$attribute] = $model[$link];
                }
                $values[] = $v;
                if (empty($v)) {
                    $this->emulateExecution();
                }
            }
        }

        if (!empty($values)) {
            $scalarValues = [];
            $nonScalarValues = [];
            foreach ($values as $value) {
                if (is_scalar($value)) {
                    $scalarValues[] = $value;
                } else {
                    $nonScalarValues[] = $value;
                }
            }

            $scalarValues = array_unique($scalarValues);
            $values = array_merge($scalarValues, $nonScalarValues);
        }
        // Yii::debug('Attributes: '.json_encode($attributes).' - '.json_encode($values), __METHOD__);

        // normalise the formats
        $normalised = [];
        if (is_array($attributes)) {
            foreach ($attributes as $attribute) {
                if ($values == null) {
                    $normalised[$attribute] = null;
                } else if (is_array($values)) {
                    if (is_array($values[0])) {
                        $normalised[$attribute] = ArrayHelper::getColumn($values, $attribute);
                    } else {
                        $normalised[$attribute] = $values;
                    }
                } else {
                    $normalised[$attribute] = $values;
                }
            }
        } else {
            $normalised[$attributes] = $values;
        }
        // Yii::debug('Normalised: '.json_encode($normalised), __METHOD__);
        $this->andWhereDb($normalised);
    }

    /**
     * @param array $attributes the attributes to prefix
     * @return array
     */
    private function prefixKeyColumns($attributes) {
        // if ($this instanceof ActiveQuery) {
        //     if (empty($this->from)) {
        //         /* @var $modelClass ActiveRecord */
        //         $modelClass = $this->modelClass;
        //         $alias = $modelClass::tableName();
        //     } else {
        //         foreach ($this->from as $alias => $table) {
        //             if (!is_string($alias)) {
        //                 $alias = $table;
        //             }
        //             break;
        //         }
        //     }
        //     if (isset($alias)) {
        //         foreach ($attributes as $i => $attribute) {
        //             $attributes[$i] = "$alias.$attribute";
        //         }
        //     }
        // }

        return $attributes;
    }
}
