<?php
namespace Misuzu\Config;

use RuntimeException;
use InvalidArgumentException;
use Index\Data\DataException;
use Index\Data\DbStatementCache;
use Index\Data\DbTools;
use Index\Data\IDbConnection;
use Index\Data\IDbStatement;
use Index\Data\IDbResult;
use Misuzu\Pagination;

class DbConfig implements IConfig {
    private DbStatementCache $cache;
    private array $values = [];

    public function __construct(IDbConnection $dbConn) {
        $this->cache = new DbStatementCache($dbConn);
    }

    public static function validateName(string $name): bool {
        // this should better validate the format, this allows for a lot of shittery
        return preg_match('#^([a-z][a-zA-Z0-9._]+)$#', $name) === 1;
    }

    public function reset(): void {
        $this->values = [];
    }

    public function unload(string|array $names): void {
        if(empty($names))
            return;
        if(is_string($names))
            $names = [$names];

        foreach($names as $name)
            unset($this->values[$name]);
    }

    public function scopeTo(string $prefix): IConfig {
        return new ScopedConfig($this, $prefix);
    }

    public function hasValues(string|array $names): bool {
        if(empty($names))
            return true;
        if(is_string($names))
            $names = [$names];

        $cachedNames = array_keys($this->values);
        $names = array_diff($names, $cachedNames);

        if(!empty($names)) {
            // array_diff preserves keys, the for() later would fuck up without it
            $names = array_values($names);
            $nameCount = count($names);

            $stmt = $this->cache->get(sprintf(
                'SELECT COUNT(*) FROM msz_config WHERE config_name IN (%s)',
                DbTools::prepareListString($nameCount)
            ));
            for($i = 0; $i < $nameCount; ++$i)
                $stmt->addParameter($i + 1, $names[$i]);
            $stmt->execute();

            $result = $stmt->getResult();
            if($result->next())
                return $result->getInteger(0) >= $nameCount;
        }

        return true;
    }

    public function removeValues(string|array $names): void {
        if(empty($names))
            return;
        if(is_string($names))
            $names = [$names];

        foreach($names as $name)
            unset($this->values[$name]);

        $nameCount = count($names);
        $stmt = $this->cache->get(sprintf(
            'DELETE FROM msz_config WHERE config_name IN (%s)',
            DbTools::prepareListString($nameCount)
        ));

        for($i = 0; $i < $nameCount; ++$i)
            $stmt->addParameter($i + 1, $names[$i]);

        $stmt->execute();
    }

    public function getAllValueInfos(?Pagination $pagination = null): array {
        $this->reset();
        $infos = [];

        $hasPagination = $pagination !== null;

        $query = 'SELECT config_name, config_value FROM msz_config';
        if($hasPagination)
            $query .= ' LIMIT ? RANGE ?';

        $stmt = $this->cache->get($query);
        if($hasPagination) {
            $stmt->addParameter(1, $pagination->getRange());
            $stmt->addParameter(2, $pagination->getOffset());
        }
        $stmt->execute();

        $result = $stmt->getResult();

        while($result->next()) {
            $name = $result->getString(0);
            $infos[] = $this->values[$name] = new DbConfigValueInfo($result);
        }

        return $infos;
    }

    public function getValueInfos(string|array $names): array {
        if(empty($names))
            return [];
        if(is_string($names))
            $names = [$names];

        $infos = [];
        $skip = [];

        foreach($names as $name)
            if(array_key_exists($name, $this->values)) {
                $infos[] = $this->values[$name];
                $skip[] = $name;
            }

        $names = array_diff($names, $skip);

        if(!empty($names)) {
            // array_diff preserves keys, the for() later would fuck up without it
            $names = array_values($names);
            $nameCount = count($names);

            $stmt = $this->cache->get(sprintf(
                'SELECT config_name, config_value FROM msz_config WHERE config_name IN (%s)',
                DbTools::prepareListString($nameCount)
            ));
            for($i = 0; $i < $nameCount; ++$i)
                $stmt->addParameter($i + 1, $names[$i]);
            $stmt->execute();

            $result = $stmt->getResult();
            while($result->next()) {
                $name = $result->getString(0);
                $infos[] = $this->values[$name] = new DbConfigValueInfo($result);
            }
        }

        return $infos;
    }

    public function getValueInfo(string $name): ?IConfigValueInfo {
        $infos = $this->getValueInfos($name);
        return empty($infos) ? null : $infos[0];
    }

    public function getValues(array $specs): array {
        $names = [];

        foreach($specs as $key => $spec) {
            if(is_string($spec)) {
                $name = $spec;
                $default = null;
                $alias = null;
            } elseif(is_array($spec) && !empty($spec)) {
                $name = $spec[0];
                $default = $spec[1] ?? null;
                $alias = $spec[2] ?? null;
            } else
                throw new InvalidArgumentException('$specs array contains an invalid entry.');

            if(($colon = strpos($name, ':')) !== false) {
                $type = substr($name, $colon + 1, 1);
                $name = substr($name, 0, $colon);
            } else $type = '';

            $names[] = $name;
            $specs[$key] = [
                'name' => $name,
                'type' => $type,
                'default' => $default,
                'alias' => $alias,
            ];
        }

        $infos = $this->getValueInfos($names);
        $results = [];

        foreach($specs as $spec) {
            foreach($infos as $infoTest)
                if($infoTest->getName() === $spec['name']) {
                    $info = $infoTest;
                    break;
                }

            $resultName = $spec['alias'] ?? $spec['name'];

            if(!isset($info)) {
                $defaultValue = $spec['default'] ?? null;
                if($spec['type'] !== '')
                    settype($defaultValue, match($spec['type']) {
                        's' => 'string',
                        'a' => 'array',
                        'i' => 'int',
                        'b' => 'bool',
                        'f' => 'float',
                        'd' => 'double',
                        default => throw new RuntimeException('Invalid type letter encountered.'),
                    });

                $results[$resultName] = $defaultValue;
                continue;
            }

            $results[$resultName] = match($spec['type']) {
                's' => $info->getString(),
                'a' => $info->getArray(),
                'i' => $info->getInteger(),
                'b' => $info->getBoolean(),
                'f' => $info->getFloat(),
                'd' => $info->getFloat(),
                '' => $info->getValue(),
                default => throw new RuntimeException('Unknown type encountered in $specs.'),
            };

            unset($info);
        }

        return $results;
    }

    public function getString(string $name, string $default = ''): string {
        $valueInfo = $this->getValueInfo($name);
        return $valueInfo?->isString() ? $valueInfo->getString() : $default;
    }

    public function getInteger(string $name, int $default = 0): int {
        $valueInfo = $this->getValueInfo($name);
        return $valueInfo?->isInteger() ? $valueInfo->getInteger() : $default;
    }

    public function getFloat(string $name, float $default = 0): float {
        $valueInfo = $this->getValueInfo($name);
        return $valueInfo?->isFloat() ? $valueInfo->getFloat() : $default;
    }

    public function getBoolean(string $name, bool $default = false): bool {
        $valueInfo = $this->getValueInfo($name);
        return $valueInfo?->isBoolean() ? $valueInfo->getBoolean() : $default;
    }

    public function getArray(string $name, array $default = []): array {
        $valueInfo = $this->getValueInfo($name);
        return $valueInfo?->isArray() ? $valueInfo->getArray() : $default;
    }

    public function setValues(array $values): void {
        if(empty($values))
            return;

        $valueCount = count($values);

        $stmt = $this->cache->get(sprintf(
            'REPLACE INTO msz_config (config_name, config_value) VALUES %s',
            DbTools::prepareListString($valueCount, '(?, ?)')
        ));

        $args = 0;
        foreach($values as $name => $value) {
            if(!self::validateName($name))
                throw new InvalidArgumentException('Invalid name encountered in $values.');

            if(is_array($value)) {
                foreach($value as $entry)
                    if(!is_scalar($entry))
                        throw new InvalidArgumentException('An array value in $values contains a non-scalar type.');
            } elseif(!is_scalar($value))
                throw new InvalidArgumentException('Invalid value type encountered in $values.');

            $stmt->addParameter(++$args, $name);
            $stmt->addParameter(++$args, serialize($value));
        }

        $stmt->execute();
    }

    public function setString(string $name, string $value): void {
        $this->setValues([$name => $value]);
    }

    public function setInteger(string $name, int $value): void {
        $this->setValues([$name => $value]);
    }

    public function setFloat(string $name, float $value): void {
        $this->setValues([$name => $value]);
    }

    public function setBoolean(string $name, bool $value): void {
        $this->setValues([$name => $value]);
    }

    public function setArray(string $name, array $value): void {
        $this->setValues([$name => $value]);
    }
}
