<?php

/** @noinspection PhpUndefinedFunctionInspection */
namespace WP_Ultimo\Dependencies\Amp\Cache;

use WP_Ultimo\Dependencies\Amp\File;
use WP_Ultimo\Dependencies\Amp\File\Driver;
use WP_Ultimo\Dependencies\Amp\Loop;
use WP_Ultimo\Dependencies\Amp\Promise;
use WP_Ultimo\Dependencies\Amp\Sync\KeyedMutex;
use WP_Ultimo\Dependencies\Amp\Sync\Lock;
use function WP_Ultimo\Dependencies\Amp\call;
final class FileCache implements Cache
{
    private static function getFilename(string $key) : string
    {
        return \hash('sha256', $key) . '.cache';
    }
    /** @var string */
    private $directory;
    /** @var KeyedMutex */
    private $mutex;
    /** @var string */
    private $gcWatcher;
    /** @var bool */
    private $ampFileVersion2;
    public function __construct(string $directory, KeyedMutex $mutex)
    {
        $this->directory = $directory = \rtrim($directory, "/\\");
        $this->mutex = $mutex;
        if (!\interface_exists(Driver::class)) {
            throw new \Error(__CLASS__ . ' requires amphp/file to be installed');
        }
        $this->ampFileVersion2 = $ampFileVersion2 = \function_exists('WP_Ultimo\\Dependencies\\Amp\\File\\listFiles');
        $gcWatcher = static function () use($directory, $mutex, $ampFileVersion2) : \Generator {
            try {
                /** @psalm-suppress UndefinedFunction */
                $files = (yield $ampFileVersion2 ? File\listFiles($directory) : File\scandir($directory));
                foreach ($files as $file) {
                    if (\strlen($file) !== 70 || \substr($file, -\strlen('.cache')) !== '.cache') {
                        continue;
                    }
                    /** @var Lock $lock */
                    $lock = (yield $mutex->acquire($file));
                    try {
                        /** @var File\File $handle */
                        /** @psalm-suppress UndefinedFunction */
                        $handle = (yield $ampFileVersion2 ? File\openFile($directory . '/' . $file, 'r') : File\open($directory . '/' . $file, 'r'));
                        $ttl = (yield $handle->read(4));
                        if ($ttl === null || \strlen($ttl) !== 4) {
                            (yield $handle->close());
                            continue;
                        }
                        $ttl = \unpack('Nttl', $ttl)['ttl'];
                        if ($ttl < \time()) {
                            /** @psalm-suppress UndefinedFunction */
                            (yield $ampFileVersion2 ? File\deleteFile($directory . '/' . $file) : File\unlink($directory . '/' . $file));
                        }
                    } catch (\Throwable $e) {
                        // ignore
                    } finally {
                        $lock->release();
                    }
                }
            } catch (\Throwable $e) {
                // ignore
            }
        };
        // trigger once, so short running scripts also GC and don't grow forever
        Loop::defer($gcWatcher);
        $this->gcWatcher = Loop::repeat(300000, $gcWatcher);
    }
    public function __destruct()
    {
        if ($this->gcWatcher !== null) {
            Loop::cancel($this->gcWatcher);
        }
    }
    /** @inheritdoc */
    public function get(string $key) : Promise
    {
        return call(function () use($key) {
            $filename = $this->getFilename($key);
            /** @var Lock $lock */
            $lock = (yield $this->mutex->acquire($filename));
            try {
                /** @psalm-suppress UndefinedFunction */
                $cacheContent = (yield $this->ampFileVersion2 ? File\read($this->directory . '/' . $filename) : File\get($this->directory . '/' . $filename));
                if (\strlen($cacheContent) < 4) {
                    return null;
                }
                $ttl = \unpack('Nttl', \substr($cacheContent, 0, 4))['ttl'];
                if ($ttl < \time()) {
                    /** @psalm-suppress UndefinedFunction */
                    (yield $this->ampFileVersion2 ? File\deleteFile($this->directory . '/' . $filename) : File\unlink($this->directory . '/' . $filename));
                    return null;
                }
                $value = \substr($cacheContent, 4);
                \assert(\is_string($value));
                return $value;
            } catch (\Throwable $e) {
                return null;
            } finally {
                $lock->release();
            }
        });
    }
    /** @inheritdoc */
    public function set(string $key, string $value, int $ttl = null) : Promise
    {
        if ($ttl < 0) {
            throw new \Error("Invalid cache TTL ({$ttl}); integer >= 0 or null required");
        }
        return call(function () use($key, $value, $ttl) {
            $filename = $this->getFilename($key);
            /** @var Lock $lock */
            $lock = (yield $this->mutex->acquire($filename));
            if ($ttl === null) {
                $ttl = \PHP_INT_MAX;
            } else {
                $ttl = \time() + $ttl;
            }
            $encodedTtl = \pack('N', $ttl);
            try {
                /** @psalm-suppress UndefinedFunction */
                (yield $this->ampFileVersion2 ? File\write($this->directory . '/' . $filename, $encodedTtl . $value) : File\put($this->directory . '/' . $filename, $encodedTtl . $value));
            } finally {
                $lock->release();
            }
        });
    }
    /** @inheritdoc */
    public function delete(string $key) : Promise
    {
        return call(function () use($key) {
            $filename = $this->getFilename($key);
            /** @var Lock $lock */
            $lock = (yield $this->mutex->acquire($filename));
            try {
                /** @psalm-suppress UndefinedFunction */
                return (yield $this->ampFileVersion2 ? File\deleteFile($this->directory . '/' . $filename) : File\unlink($this->directory . '/' . $filename));
            } finally {
                $lock->release();
            }
        });
    }
}
