<?php
declare(strict_types=1);
require_once __DIR__ . '/ValueNormalizer.php';

/** Isolated prototype. Requires PDO CASE_LOWER/default ASSOC and paired exact options. */
class ExactStatement extends PDOStatement
{
    private $metadata;
    private $defaultMode = PDO::FETCH_ASSOC;
    protected function __construct() {}

    public function execute($parameters = null)
    {
        $this->metadata = null;
        return parent::execute($parameters);
    }

    public function nextRowset()
    {
        $this->metadata = null;
        return parent::nextRowset();
    }

    public function setFetchMode($mode, $params = null)
    {
        $this->checkMode($mode);
        if ($params !== null || func_num_args()>2) { throw new InvalidArgumentException('Fetch arguments not supported by prototype'); }
        $ok = parent::setFetchMode($mode);
        if ($ok) { $this->defaultMode = $mode; }
        return $ok;
    }

    public function fetch($mode = null, $orientation = PDO::FETCH_ORI_NEXT, $offset = 0)
    {
        $mode = $mode ?: $this->defaultMode;
        $this->checkMode($mode);
        $metadata = $this->columns();
        if ($mode !== PDO::FETCH_NUM) { $this->checkNames($metadata); }
        $row = parent::fetch(PDO::FETCH_NUM, $orientation, $offset);
        if ($row === false) { return false; }
        $result = [];
        foreach ($row as $index => $value) {
            $value = ExactFetchValue::convert($value,$metadata[$index]);
            if ($mode === PDO::FETCH_NUM || $mode === PDO::FETCH_BOTH) { $result[$index] = $value; }
            if ($mode === PDO::FETCH_ASSOC || $mode === PDO::FETCH_BOTH) { $result[strtolower($metadata[$index]['name'])] = $value; }
        }
        return $result;
    }

    public function fetchAll($mode = null, $column = null, $ctorArgs = null)
    {
        $mode = $mode ?: $this->defaultMode;
        if ($mode === PDO::FETCH_COLUMN) {
            if ($ctorArgs !== null || func_num_args()>3) { throw new InvalidArgumentException('One column index required'); }
            $index = $column ?? 0;
            $metadata = $this->column($index);
            $rows = [];
            while (($row = parent::fetch(PDO::FETCH_NUM)) !== false) {
                $rows[] = ExactFetchValue::convert($row[$index],$metadata);
            }
            return $rows;
        }
        $this->checkMode($mode);
        if ($column !== null || $ctorArgs !== null || func_num_args()>3) { throw new InvalidArgumentException('Fetch arguments not supported by prototype'); }
        $rows = [];
        while (($row = $this->fetch($mode)) !== false) { $rows[] = $row; }
        return $rows;
    }

    public function fetchColumn($index = 0)
    {
        $metadata = $this->column($index);
        $row = parent::fetch(PDO::FETCH_NUM);
        return $row === false ? false : ExactFetchValue::convert($row[$index],$metadata);
    }

    private function column($index): array
    {
        $columns = $this->columns();
        if (!is_int($index) || !isset($columns[$index])) { throw new InvalidArgumentException('Invalid column index'); }
        return $columns[$index];
    }

    private function columns(): array
    {
        if ($this->metadata !== null) { return $this->metadata; }
        $columns = [];
        for ($i=0;$i<$this->columnCount();++$i) {
            $meta = parent::getColumnMeta($i);
            if (!is_array($meta) || !isset($meta['name'],$meta['native_type'])) { throw new UnexpectedValueException('Column metadata unavailable'); }
            $columns[] = $meta;
        }
        return $this->metadata = $columns;
    }

    private function checkMode($mode): void
    {
        if (!in_array($mode,[PDO::FETCH_ASSOC,PDO::FETCH_NUM,PDO::FETCH_BOTH],true)) {
            throw new InvalidArgumentException('Fetch mode not supported by prototype');
        }
    }

    private function checkNames(array $metadata): void
    {
        $names = [];
        foreach ($metadata as $meta) {
            $name = strtolower($meta['name']);
            if (isset($names[$name]) || ctype_digit($name)) { throw new UnexpectedValueException('Ambiguous result column name'); }
            $names[$name] = true;
        }
    }
}
