<?php

class TemplateAccess implements ArrayAccess, JsonSerializable
{
    /**
     * Implements ArrayAccess interface.
     */
    private $cached_offsets = [];

    public function offsetSet($offset, $value): void
    {
        $this->{$offset} = $this->cached_offsets[$offset] = $value;
    }

    public function offsetExists($offset): bool
    {
        return isset($this->{$offset}) || method_exists($this, $this->offsetMethod($offset));
    }

    public function offsetUnset($offset): void
    {
        unset($this->{$offset});
    }

    public function offsetMethod($offset)
    {
        return 'fetch'.ucfirst($offset);
    }

    public function offsetGet($offset): mixed
    {
        if (array_key_exists($offset, $this->cached_offsets)) {
            return $this->cached_offsets[$offset];
        }

        $method = $this->offsetMethod($offset);
        if (method_exists($this, $method)) {
            $res = call_user_func([$this, $method]);
            $this->cached_offsets[$offset] = $res;

            return $res;
        }

        if (isset($this->{$offset})) {
            return $this->{$offset};
        }

        return null;
    }

    public function injectCache($offset, $value)
    {
        return $this->cached_offsets[$offset] = $value;
    }

    public function jsonSerialize(): mixed
    {
        $data = get_object_vars($this);
        $data['_cache'] = $data['cached_offsets'];
        unset($data['cached_offsets']);

        $methods = array_filter(get_class_methods($this), function ($x) {
            return substr($x, 0, 5) == 'fetch';
        });
        $methods = array_values(array_map(function ($x) {
            return lcfirst(substr($x, 5));
        }, $methods));

        return $data + ['_attributes' => $methods];
    }
}
