<?php

namespace Sitecake;

/**
 * Minimal dependency-injection container.
 *
 * Replaces Pimple\Container / Silex\Application for SiteCake's needs:
 *  - $app['key'] = value            store a raw value
 *  - $app['key'] = function($c){…}  store a lazy-loaded service (closure)
 *  - $app['key']                    retrieve value (closures evaluated once, then cached)
 *  - isset($app['key'])             check existence
 *  - $app->extend('key', fn)        decorate an existing service
 *  - $app->register($provider)      call $provider->register($this)
 */
class Container implements \ArrayAccess
{
    /** @var array<string, mixed> Raw (possibly closure) values */
    private array $values = [];

    /** @var array<string, mixed> Resolved (cached) service instances */
    private array $resolved = [];

    // ------------------------------------------------------------------ //
    //  ArrayAccess
    // ------------------------------------------------------------------ //

    public function offsetSet($id, $value): void
    {
        unset($this->resolved[$id]);   // invalidate cached value
        $this->values[$id] = $value;
    }

    public function offsetGet($id)
    {
        if (!isset($this->values[$id])) {
            throw new \InvalidArgumentException(
                sprintf('Container: identifier "%s" is not defined.', $id)
            );
        }

        if (!array_key_exists($id, $this->resolved)) {
            $val = $this->values[$id];
            $this->resolved[$id] = ($val instanceof \Closure) ? $val($this) : $val;
        }

        return $this->resolved[$id];
    }

    public function offsetExists($id): bool
    {
        return isset($this->values[$id]);
    }

    public function offsetUnset($id): void
    {
        unset($this->values[$id], $this->resolved[$id]);
    }

    // ------------------------------------------------------------------ //
    //  Extra helpers
    // ------------------------------------------------------------------ //

    /**
     * Decorate an existing service.
     *
     * The callback receives ($currentResolvedValue, $container) and must return
     * the new (decorated) value.
     *
     * @param string   $id       Service identifier
     * @param \Closure $callback fn($existing, Container $c): mixed
     */
    public function extend(string $id, \Closure $callback): void
    {
        $existing = $this->values[$id] ?? null;

        $this->values[$id] = function (Container $c) use ($existing, $callback) {
            $current = ($existing instanceof \Closure) ? $existing($c) : $existing;
            return $callback($current, $c);
        };

        unset($this->resolved[$id]);
    }

    /**
     * Register a service provider (any object exposing register(Container $app)).
     */
    public function register(object $provider): void
    {
        $provider->register($this);
    }
}
