<?php

declare(strict_types=1);

namespace Atk4\Data\Persistence\Sql\Postgresql;

use Atk4\Data\Persistence;
use Doctrine\DBAL\Statement;
use Doctrine\DBAL\Types\Type;

trait ExpressionTrait
{
    #[\Override]
    protected function escapeStringLiteral(string $value): string
    {
        $dummyPersistence = (new \ReflectionClass(Persistence\Sql::class))->newInstanceWithoutConstructor();
        if (\Closure::bind(static fn () => $dummyPersistence->explicitCastIsEncodedBinary($value), null, Persistence\Sql::class)()) {
            $value = \Closure::bind(static fn () => $dummyPersistence->explicitCastDecode($value), null, Persistence\Sql::class)();

            return 'decode(\'' . bin2hex($value) . '\', \'hex\')';
        }

        $parts = [];
        foreach (preg_split('~((?:\x00+[^\x00]{1,100})*\x00+)~', $value, -1, \PREG_SPLIT_DELIM_CAPTURE) as $i => $v) {
            if (($i % 2) === 1) {
                // will raise SQL error, PostgreSQL does not support \0 character
                $parts[] = 'convert_from(decode(\'' . bin2hex($v) . '\', \'hex\'), \'UTF8\')';
            } elseif ($v !== '') {
                // workaround https://github.com/php/php-src/issues/13958
                foreach (preg_split('~(\\\+)(?=\'|$)~', $v, -1, \PREG_SPLIT_DELIM_CAPTURE) as $i2 => $v2) {
                    if (($i2 % 2) === 1) {
                        $parts[] = strlen($v2) === 1
                            ? 'chr(' . ord('\\') . ')'
                            : 'repeat(chr(' . ord('\\') . '), ' . strlen($v2) . ')';
                    } elseif ($v2 !== '') {
                        $parts[] = '\'' . str_replace('\'', '\'\'', $v2) . '\'';
                    }
                }
            }
        }

        if ($parts === []) {
            $parts = ['\'\''];
        }

        return $this->makeNaryTree($parts, 10, static function (array $parts) {
            if (count($parts) === 1) {
                return array_first($parts);
            }

            return 'concat(' . implode(', ', $parts) . ')';
        });
    }

    #[\Override]
    protected function updateRenderBeforeExecute(array $render): array
    {
        [$sql, $params] = parent::updateRenderBeforeExecute($render);

        $sql = preg_replace_callback(
            '~' . self::QUOTED_TOKEN_REGEX . '\K|(?<!:):\w+~',
            function ($matches) use ($params) {
                if ($matches[0] === '') {
                    return '';
                }

                $sql = $matches[0];
                $value = $params[$sql];

                // fix pgsql/pdo_pgsql param type bind
                // TODO open php-src issue
                if (is_bool($value)) {
                    $sql = 'cast(' . $sql . ' as BOOLEAN)';
                } elseif (is_int($value)) {
                    $sql = 'cast(' . $sql . ' as BIGINT)';
                } elseif (is_float($value)) {
                    $sql = 'cast(' . $sql . ' as DOUBLE PRECISION)';
                } elseif (is_string($value)) {
                    $dummyPersistence = (new \ReflectionClass(Persistence\Sql::class))->newInstanceWithoutConstructor();
                    if (\Closure::bind(static fn () => $dummyPersistence->explicitCastIsEncoded($value), null, Persistence\Sql::class)()) {
                        if (\Closure::bind(static fn () => $dummyPersistence->explicitCastIsEncodedBinary($value), null, Persistence\Sql::class)()) {
                            $sql = 'cast(' . $sql . ' as bytea)';
                        } else {
                            $typeString = \Closure::bind(static fn () => $dummyPersistence->explicitCastDecodeType($value), null, Persistence\Sql::class)();
                            $type = Type::getType($typeString);
                            $dbType = $type->getSQLDeclaration([], $this->connection->getDatabasePlatform());
                            $sql = 'cast(' . $sql . ' as ' . $dbType . ')';
                        }
                    } else {
                        $sql = 'cast(' . $sql . ' as citext)';
                    }
                } else {
                    $sql = 'cast(' . $sql . ' as unknown)';
                }

                return $sql;
            },
            $sql
        );

        return [$sql, $params];
    }

    #[\Override]
    protected function _executeStatement(Statement $statement, bool $fromExecuteStatement)
    {
        $sql = \Closure::bind(static fn () => $statement->sql, null, Statement::class)();
        if (preg_match('~^\s*+select(?=\s|$)~i', $sql)) {
            return parent::_executeStatement($statement, $fromExecuteStatement);
        }

        return $this->connection->atomic(function () use ($statement, $fromExecuteStatement) {
            return parent::_executeStatement($statement, $fromExecuteStatement);
        });
    }
}
