<?php
// DbTools.php
// Created: 2021-05-02
// Updated: 2022-02-28

namespace Index\Data;

use InvalidArgumentException;
use Index\Type;

/**
 * Common database actions.
 */
final class DbTools {
    private const DB_PROTOS = [
        'null' => NullDb\NullDbBackend::class,
        'mariadb' => MariaDB\MariaDBBackend::class,
        'mysql' => MariaDB\MariaDBBackend::class,
        'sqlite' => SQLite\SQLiteBackend::class,
        'sqlite3' => SQLite\SQLiteBackend::class,
    ];

    public static function create(string $dsn): IDbConnection {
        static $backends = [];

        $uri = parse_url($dsn);
        if($uri === false)
            throw new InvalidArgumentException('$dsn is not a valid uri.');

        $scheme = $uri['scheme'];

        if(in_array($scheme, $backends))
            $backend = $backends[$scheme];
        else {
            $backend = null;

            if(array_key_exists($scheme, self::DB_PROTOS))
                $name = self::DB_PROTOS[$scheme];
            else
                $name = str_replace('-', '\\', $scheme);

            if(class_exists($name) && is_subclass_of($name, IDbBackend::class)) {
                $backend = new $name;
                $name = get_class($backend);
            }

            if($backend === null)
                throw new DataException('No implementation is available for the specified scheme.');
            if(!$backend->isAvailable())
                throw new DataException('Requested database backend is not available, likely due to missing dependencies.');

            $backends[$name] = $backend;
        }

        return $backend->createConnection(
            $backend->parseDsn($uri)
        );
    }

    /**
     * Transaction wrapper.
     *
     * Takes a database connection with transaction support and a callable that may return a boolean based on the success of the actions.
     * If the callable returns nothing, nothing will happen.
     * If the callable returns true, commit will be called.
     * If the callable returns false, rollback will be called.
     *
     * @param IDbTransactions $connection A database connection with transaction support.
     * @param callable $callable A callable that handles the transaction, may return a bool.
     */
    public static function transaction(IDbTransactions $connection, callable $callable): void {
        $connection->beginTransaction();
        $result = $callable($connection) ?? null;
        if(is_bool($result)) {
            if($result)
                $connection->commit();
            else
                $connection->rollback();
        }
    }

    /**
     * Detects the DbType of the passed argument. Should be used for DbType::AUTO.
     *
     * @param mixed $value A value of unknown type.
     * @return int DbType of the value passed in the argument.
     */
    public static function detectType(mixed $value): int {
        if(is_null($value))
            return DbType::NULL;
        if(is_float($value))
            return DbType::FLOAT;
        if(is_int($value))
            return DbType::INTEGER;
        // ┌ should probably also check for Stringable, length should also be taken into consideration
        // ↓ though maybe with that it's better to assume that when an object is passed it'll always be Massive
        if(is_string($value))
            return DbType::STRING;
        return DbType::BLOB;
    }
}