<?php

namespace ShSo\Lacassa\Query;

use Cassandra;
use Illuminate\Support\Arr;
use ShSo\Lacassa\Connection;
use InvalidArgumentException;
use Illuminate\Database\Query\Builder as BaseBuilder;

class Builder extends BaseBuilder
{
    /**
     * The current query value bindings.
     *
     * @var array
     */
    public $bindings = [
        'select' => [],
        'where'  => [],
        'updateCollection' => [],
        'insertCollection' => [],
    ];

    public $allowFiltering = false;

    public $distinct = false;

    /**
     * The where constraints for the query.
     *
     * @var array
     */
    public $updateCollections;

    /**
     * The where constraints for the query.
     *
     * @var array
     */
    public $insertCollections;

    /**
     * All of the available clause operators.
     *
     * @var array
     */
    public $operators = [
        '=',
        '<',
        '>',
        '<=',
        '>=',
        'like',
        'contains',
        'contains key',
    ];

    /**
     * Operator conversion.
     *
     * @var array
     */
    protected $conversion = [
        '=' => '$eq',
        '!=' => '$ne',
        '<>' => '$ne',
        '<' => '$lt',
        '<=' => '$lte',
        '>' => '$gt',
        '>=' => '$gte',
    ];

    /**
     * @var array
     */
    public $collectionTypes = ['set', 'list', 'map'];

    /**
     * @param Connection $connection
     */
    public function __construct(Connection $connection)
    {
        $this->grammar = $connection->getQueryGrammar();
        $this->connection = $connection;
    }

    public function distinct()
    {
        $this->distinct = true;

        return $this;
    }

    public function allowFiltering()
    {
        $this->allowFiltering = true;

        return $this;
    }

    /**
     * Execute the query as a "select" statement.
     *
     * @param array $columns
     *
     * @return Cassandra\Rows
     */
    public function get($columns = ['*'])
    {
        if (is_null($this->columns)) {
            $this->columns = $columns;
        }
        $cql = $this->grammar->compileSelect($this);

        return $this->execute($cql);
    }

    /**
     * Execute the query as a "select" statement.
     *
     * @param array $columns
     *
     * @return Cassandra\FutureRows
     */
    public function getAsync($columns = ['*'])
    {
        if (is_null($this->columns)) {
            $this->columns = $columns;
        }
        $cql = $this->grammar->compileSelect($this);

        return $this->executeAsync($cql);
    }

    /**
     * Execute the CQL query.
     *
     * @param string $cql
     *
     * @return Cassandra\Rows
     */
    private function execute($cql)
    {
        return $this->connection->execute($cql, ['arguments' => $this->getBindings()]);
    }

    /**
     * Execute the CQL query asyncronously.
     *
     * @param string $cql
     *
     * @return Cassandra\FutureRows
     */
    private function executeAsync($cql)
    {
        return $this->connection->executeAsync($cql, ['arguments' => $this->getBindings()]);
    }

    /**
     * Delete a record from the database.
     *
     * @return Cassandra\Rows
     */
    public function deleteRow()
    {
        $query = $this->grammar->compileDelete($this);

        return $this->executeAsync($query);
    }

    /**
     * Delete a column from the database.
     *
     * @param array $columns
     *
     * @return Cassandra\Rows
     */
    public function deleteColumn($columns)
    {
        $this->delParams = $columns;
        $query = $this->grammar->compileDelete($this);

        return $this->executeAsync($query);
    }

    /**
     * Retrieve the "count" result of the query.
     *
     * @param string $columns
     *
     * @return Cassandra\Rows
     */
    public function count($columns = '*')
    {
        $count = 0;
        $result = $this->get(array_wrap($columns));
        while (true) {
            $count += $result->count();
            if ($result->isLastPage()) {
                break;
            }
            $result = $result->nextPage();
        }

        return $count;
    }

    /**
     * Used to update the colletions like set, list and map.
     *
     * @param string $type
     * @param string $column
     * @param string $operation
     * @param string $value
     *
     * @return string
     */
    public function updateCollection($type, $column, $operation = null, $value = null)
    {
        //Check if the type is anyone in SET, LIST or MAP. else throw ERROR.
        if (!in_array(strtolower($type), $this->collectionTypes)) {
            throw new InvalidArgumentException("Invalid binding type: {$type}, Should be any one of ".implode(', ', $this->collectionTypes));
        }

        // Here we will make some assumptions about the operator. If only 2 values are
        // passed to the method, we will assume that the operator is an equals sign
        // and keep going. Otherwise, we'll require the operator to be passed in.
        if (func_num_args() == 3) {
            $value = $operation;
            $operation = null;
        }

        $updateCollection = compact('type', 'column', 'value', 'operation');
        $this->updateCollections[] = $updateCollection;
        $this->addCollectionBinding($updateCollection, 'updateCollection');

        return $this;
    }

    /**
     * Add a binding to the query.
     *
     * @param array $value
     * @param string $type
     *
     * @return $this
     *
     * @throws \InvalidArgumentException
     */
    public function addCollectionBinding($value, $type = 'updateCollection')
    {
        if (!array_key_exists($type, $this->bindings)) {
            throw new InvalidArgumentException("Invalid binding type: {$type}.");
        }
        $this->bindings[$type][] = $value;

        return $this;
    }

    /**
     * Update a record in the database.
     *
     * @param array $values
     *
     * @return int
     */
    public function update(array $values = [])
    {
        $cql = $this->grammar->compileUpdate($this, $values);

        return $this->connection->update($cql, $this->cleanBindings(
            $this->grammar->prepareBindingsForUpdate($this->bindings, $values)
        ));
    }

    /**
     * Insert a new record into the database.
     *
     * @param array $values
     *
     * @return bool
     */
    public function insert(array $values = [])
    {
        $insertCollectionArray = [];
        // Since every insert gets treated like a batch insert, we will make sure the
        // bindings are structured in a way that is convenient when building these
        // inserts statements by verifying these elements are actually an array.
        if (empty($values)) {
            return true;
        }

        if (!is_array(reset($values))) {
            $values = [$values];
        } else {
            // Here, we will sort the insert keys for every record so that each insert is
            // in the same order for the record. We need to make sure this is the case
            // so there are not any errors or problems when inserting these records.
            foreach ($values as $key => $value) {
                ksort($value);
                $values[$key] = $value;
            }
        }

        // Finally, we will run this query against the database connection and return
        // the results. We will need to also flatten these bindings before running
        // the query so they are all in one huge, flattened array for execution.
        return $this->connection->insert(
            $this->grammar->compileInsert($this, $values),
            $this->cleanBindings(Arr::flatten($values, 1))
        );
    }

    /**
     * Insert a colletion type in cassandra.
     *
     * @param string $type
     * @param string $column
     * @param string $value
     *
     * @return $this
     */
    public function insertCollection($type, $column, $value)
    {
        $insertCollection = compact('type', 'column', 'value');
        $this->insertCollections[] = $insertCollection;
        $this->addCollectionBinding($insertCollection, 'insertCollection');

        return $this;
    }

    /**
     * @param array $columns
     *
     * @return Cassandra\Rows
     */
    public function index($columns = [])
    {
        $cql = $this->grammar->compileIndex($this, $columns);

        return $this->execute($cql);
    }
}