<?php

namespace Acms\Services\Facades;

use Acms\Services\Container;

abstract class Facade
{
    /**
     * @var \Acms\Services\Container
     */
    protected static $container;

    /**
     * alias → 解決済みファサードクラス名の逆引き表。
     *
     * どのサブクラスも再宣言していないため、全ファサード共通で Facade 基底の
     * 1 テーブルを共有する。bind 変更時にどのファサードの static $instance を
     * 破棄すべきか特定するのに使う。値は class-string => true の連想配列にして
     * 重複登録を自然に排除する。
     *
     * @var array<string, array<class-string<self>, true>>
     */
    protected static $resolvedFacades = [];

    /**
     * @return string
     *
     * @throws \RuntimeException
     */
    protected static function getServiceAlias()
    {
        throw new \RuntimeException('Service does not defined getServiceAlias method.');
    }

    /**
     * @return bool
     */
    protected static function isCache()
    {
        return false;
    }

    /**
     * @param string $alias
     *
     * @return mixed
     */
    protected static function getServiceInstance($alias)
    {
        return static::$container->make($alias);
    }

    /**
     * get service instance
     *
     * @return mixed
     */
    public static function getInstance()
    {
        return static::getServiceInstance(static::getServiceAlias());
    }

    /**
     * @param \Acms\Services\Container $container
     *
     * @return void
     */
    public static function setContainer(Container $container)
    {
        static::$container = $container;
        $container->onBindingChanged(static function (string $alias): void {
            foreach (array_keys(self::$resolvedFacades[$alias] ?? []) as $facadeClass) {
                $facadeClass::clearInstance();
            }
        });
    }

    /**
     * テスト用: サービスインスタンスをスタブ等へ差し替える。
     *
     * DI コンテナのエイリアスを差し替えたうえで、ファサードがキャッシュしている解決済み
     * インスタンス（isCache() が true の場合）を破棄し、次回アクセス時に差し替え後の
     * インスタンスを再解決させる。テスト後は元のインスタンスを渡して再度呼び出し復元する。
     *
     * @param object $instance
     * @return void
     */
    public static function swap(object $instance): void
    {
        static::$container->instance(static::getServiceAlias(), $instance);
        static::clearInstance();
    }

    /**
     * テスト用: ファサードがキャッシュしている解決済みインスタンスを破棄する。
     *
     * isCache() が true のファサードは解決済みインスタンスを静的プロパティ $instance に
     * 保持するため、コンテナ側だけを差し替えても反映されない。本メソッドでキャッシュを
     * 無効化し、次回アクセスでコンテナから再解決させる。
     *
     * @return void
     */
    public static function clearInstance(): void
    {
        $class = get_called_class();
        if (property_exists($class, 'instance')) {
            $class::$instance = null;
        }
    }

    /**
     * @param string $method
     * @param array $arguments
     *
     * @return mixed
     */
    public static function __callStatic($method, array $arguments)
    {
        $class = get_called_class();
        if (static::isCache() && property_exists($class, 'instance') && $class::$instance) {
            $instance = $class::$instance;
        } else {
            $instance = static::getServiceInstance(static::getServiceAlias());
        }
        if (static::isCache()) {
            $class::$instance = $instance;
            self::$resolvedFacades[static::getServiceAlias()][$class] = true;
        }
        if (is_callable([$instance, $method])) {
            /** @var callable $callback */
            $callback = [$instance, $method];
            return call_user_func_array($callback, $arguments);
        }
        throw new \BadMethodCallException("Method {$method} does not exist on " . get_class($instance));
    }
}
