<?php

namespace WP_Ultimo\Dependencies\Amp\Cache;

use WP_Ultimo\Dependencies\Amp\Loop;
use WP_Ultimo\Dependencies\Amp\Promise;
use WP_Ultimo\Dependencies\Amp\Struct;
use WP_Ultimo\Dependencies\Amp\Success;
final class ArrayCache implements Cache
{
    /** @var object */
    private $sharedState;
    /** @var string */
    private $ttlWatcherId;
    /** @var int|null */
    private $maxSize;
    /**
     * @param int $gcInterval The frequency in milliseconds at which expired cache entries should be garbage collected.
     * @param int $maxSize The maximum size of cache array (number of elements).
     */
    public function __construct(int $gcInterval = 5000, int $maxSize = null)
    {
        // By using a shared state object we're able to use `__destruct()` for "normal" garbage collection of both this
        // instance and the loop's watcher. Otherwise this object could only be GC'd when the TTL watcher was cancelled
        // at the loop layer.
        $this->sharedState = $sharedState = new class
        {
            use Struct;
            /** @var string[] */
            public $cache = [];
            /** @var int[] */
            public $cacheTimeouts = [];
            /** @var bool */
            public $isSortNeeded = \false;
            public function collectGarbage() : void
            {
                $now = \time();
                if ($this->isSortNeeded) {
                    \asort($this->cacheTimeouts);
                    $this->isSortNeeded = \false;
                }
                foreach ($this->cacheTimeouts as $key => $expiry) {
                    if ($now <= $expiry) {
                        break;
                    }
                    unset($this->cache[$key], $this->cacheTimeouts[$key]);
                }
            }
        };
        $this->ttlWatcherId = Loop::repeat($gcInterval, [$sharedState, "collectGarbage"]);
        $this->maxSize = $maxSize;
        Loop::unreference($this->ttlWatcherId);
    }
    public function __destruct()
    {
        $this->sharedState->cache = [];
        $this->sharedState->cacheTimeouts = [];
        Loop::cancel($this->ttlWatcherId);
    }
    /** @inheritdoc */
    public function get(string $key) : Promise
    {
        if (!isset($this->sharedState->cache[$key])) {
            return new Success(null);
        }
        if (isset($this->sharedState->cacheTimeouts[$key]) && \time() > $this->sharedState->cacheTimeouts[$key]) {
            unset($this->sharedState->cache[$key], $this->sharedState->cacheTimeouts[$key]);
            return new Success(null);
        }
        return new Success($this->sharedState->cache[$key]);
    }
    /** @inheritdoc */
    public function set(string $key, string $value, int $ttl = null) : Promise
    {
        if ($ttl === null) {
            unset($this->sharedState->cacheTimeouts[$key]);
        } elseif ($ttl >= 0) {
            $expiry = \time() + $ttl;
            $this->sharedState->cacheTimeouts[$key] = $expiry;
            $this->sharedState->isSortNeeded = \true;
        } else {
            throw new \Error("Invalid cache TTL ({$ttl}; integer >= 0 or null required");
        }
        unset($this->sharedState->cache[$key]);
        if (\count($this->sharedState->cache) === $this->maxSize) {
            \array_shift($this->sharedState->cache);
        }
        $this->sharedState->cache[$key] = $value;
        /** @var Promise<void> */
        return new Success();
    }
    /** @inheritdoc */
    public function delete(string $key) : Promise
    {
        $exists = isset($this->sharedState->cache[$key]);
        unset($this->sharedState->cache[$key], $this->sharedState->cacheTimeouts[$key]);
        return new Success($exists);
    }
}
