<?php
namespace DatabaseHelper;

use DatabaseHelper\enums\Aggregation;
use DatabaseHelper\enums\Join;
use DatabaseHelper\enums\Order;
use http\Exception\InvalidArgumentException;
use SimplePie\Exception;

class Query
{
    use Conditionable;

    protected Schema $schema;
    protected array $columns = ['*'];
    protected array $aggregations = [];
    protected array $joins = [];
    public array $orderBy;

    public function __construct(Schema $table) {
        $this->schema = $table;
    }

    public function select(string ...$cols): Query {
        if (!empty($cols))
            $this->columns = $cols;
        foreach ($cols as $col)
            $this->schema->requireColumn($col);
        return $this;
    }

    public function orderBy(string $col, Order $order): Query {
        $this->schema->requireColumn($col);
        $this->orderBy = [
            'name'  => $col,
            'order' => $order
        ];
        return $this;
    }

    public function join(Join $join, Schema $other): Query {
        $foreignKey = null;
        if($this->schema->existsReference($other))
            $foreignKey = $this->schema->foreignKeys[$other->name];
        if ($other->existsReference($this->schema))
            $foreignKey = $other->foreignKeys[$this->schema->name];

        if (is_null($foreignKey))
            throw new InvalidArgumentException('Joins can only applied to referencing columns.');

        // TODO: Implement include instead of merge
        $this->schema->include($other);
        $this->joins[] = [
            'table' => $other->name,
            'type' => $join
        ];

        return $this;
    }

    protected function isOrdered(): bool {
        return !empty($this->orderBy);
    }

    protected function isJoined(): bool {
        return !empty($this->joins);
    }

    public function toSql(): string {
        // Merge any aggregations with the standard columns.
        $selectColumns = $this->columns;
        if ($this->hasAggregations())
            $selectColumns = array_merge($selectColumns, $this->aggregations);

        // Build the SELECT clause.
        $columns = implode(", ", $selectColumns);
        $primaryTable = $this->schema->name;
        $query = "SELECT $columns FROM $primaryTable";

        // Append join clauses, if any.
        if ($this->isJoined())
            foreach ($this->joins as $join)
                $query .= " " . $join['type']->toString() . " NATURAL JOIN " . $join['table'];

        // Append the WHERE clause if conditions exist.
        if ($this->isConditioned()) {
            $whereClause = $this->combineConditions();
            $query .= " WHERE $whereClause";
        }

        // Append the ORDER BY clause if ordering is set.
        if ($this->isOrdered()) {
            $orderClause = $this->orderBy['name'] . ' ' . $this->orderBy['order'];
            $query .= " ORDER BY $orderClause";
        }

        return $query;
    }

    public function aggregate(string $col, string $alias, Aggregation $func): Query {
        if ($col != '*')
            $this->schema->requireColumn($col);
        $this->aggregations[] = strtoupper($func->toString()) . "($col) AS $alias";
        return $this;
    }

    public function hasAggregations(): bool {
        return !empty($this->aggregations);
    }

    public function query(): mixed {
        global $wpdb;
        $query = $this->toSql();
        $results = $wpdb->get_results($query, ARRAY_A);
        return $this->formatResults($results);
    }

    protected function formatResults(array $results) {
        $formatted = [];

        foreach ($results as $row) {
            // Apply type casting to each column in each row.
            foreach ($row as $column => &$value)
                if (isset($this->columnTypes[$column]))
                    $value = $this->columnTypes[$column]->valCast($value);
            // Use the primary key for row indexing
            $primaryKey = $this->schema->primaryKey();
            $formatted[$row[$primaryKey]] = $row;
        }

        if (count($formatted) === 1) {
            // Unpack single row results
            $row = array_shift($formatted);
            if (count($row) === 1)
                // Unpack single column results
                return array_shift($row);
            return $row;
        }

        return $formatted;
    }
}