<?php declare(strict_types=1);

namespace Plainwork\Core;

use Plainwork\Core\ObjectProxy;

include_once __DIR__ . '/Func.php';

/*
injects dependencies into public properties without type hints that equal to the class name
class One
{
    public $two = Two::class;
    public $thee = Three::class;
}
*/
class App
{
    private array $env = [];
    private array $modules = [];

    private string $mainFile;
    private $version;
    private $name;
    private $startTime;

    private array $implementors = [];

    private array $decorators = [];
    private array $filterGetDecorators = [];

    private array $listeners = [];
    private array $filterGetListeners = [];

// funcs to run on end of request
    private array $end = [];

// dependency injection container
// already made proxies
    private array $made = [];

// already constructed objects
    private array $constructed = [];

    public function __construct(string $mainFile, array $env = [])
    {
        $this->startTime = microtime(true);

        $this->env = $env;

        $this->mainFile = $mainFile;
        $this->version = File::versionStringFromFile($this->mainFile);
        $this->name = File::appNameFromFile($this->mainFile);

        $this->bind(__CLASS__, $this);

        if ('cli' === php_sapi_name()) {
            // register_shutdown_function([$this, 'shutdown']);
        }
    }

    public function shutdown()
    {}

    public function getStartTime()
    {
        return $this->startTime;
    }

    public function end()
    {
        foreach ($this->end as $func) {
            $this->call($func);
        }
    }

    public function onEnd($func): self
    {
        $this->end[] = $func;

        return $this;
    }

    public function env($k)
    {
        return $this->env[$k] ?? null;
    }

    public function version()
    {
        return $this->getVersion();
    }

    public function getVersion()
    {
        return $this->version;
    }

    public function name()
    {
        return $this->getName();
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function setName(string $v): self
    {
        $this->name = $v;

        return $this;
    }

// makes an ObjectProxy
    public function make(string $class, ...$args)
    {
    // already made?
        if (isset($this->made[$class])) {
            return $this->made[$class];
        }

        // $ret = $this->construct($class);
        $ret = new ObjectProxy($class, [$this, 'call']);

        $this->made[$class] = $ret;

        return $ret;
    }

    public function call($func, ...$args)
    {
        return $this->makeFunc($func)(...$args);
    }

    public function callArray($func, array $args = [])
    {
        return $this->call($func, ...$args);
    }

    public function makeFunc($func)
    {
        if ($func instanceof \Closure) {
            return $func;
        }

    // construct if needed
        if (is_array($func)) {
            if (!is_object($func[0])) {
                $func[0] = $this->construct($func[0]);
            }
            $className = get_class($func[0]);
            $methodName = $func[1];
        } else {
            if (!is_object($func)) {
                $func = $this->construct($func);
            }
            $className = get_class($func);
            $methodName = '__invoke';
        }

    // decorators?
        if ($decorators = $this->getDecorators($className, $methodName)) {
            foreach ($decorators as $decorator) {
                $func = function (...$args) use ($decorator, $func) {
                    $decorator = $this->makeFunc($decorator);
                    $args[] = $func;

                    return $decorator(...$args);
                };
            }
        }

    // listeners?
        if ($listeners = $this->getListeners($className, $methodName)) {
            $func = function (...$args) use ($listeners, $func) {
                $ret = $func(...$args);
                foreach ($listeners as $listener) {
                    $listener = $this->makeFunc($listener);

                    return $listener(...$args);
                }
                return $ret;
            };
        }

        return $func;
    }

// bind already instantiated object
    public function bind(string $class, object $instance)
    {
        if (!($instance instanceof $class)) {
            throw new \InvalidArgumentException(get_class($instance) . ' should be ' . $class);
        }
        $this->made[$class] = $instance;

        return $this;
    }

    public function makePipeline(array $funcs): callable
    {
        $func = array_shift($funcs);
        $func = $this->makeFunc($func);

        foreach ($funcs as $decorator) {
            $func = function (...$args) use ($decorator, $func) {
                $args[] = $func;
                $decorator = $this->makeFunc($decorator);

                return $decorator(...$args);
            };
        }

        return $func;
    }

    public function implement(string $interfaceName, string $className): self
    {
        $this->implementors[$interfaceName] = $className;

        return $this;
    }

    private function construct(string $class): object
    {
    // already constructed?
        if (isset($this->constructed[$class])) {
            return $this->constructed[$class];
        }

    // interface?
        $concreteClass = $this->implementors[$class] ?? $class;

        $ret = $this->constructPublicPropInjector($concreteClass);
        $this->constructed[$class] = $ret;

    // any on construct listeners?
        // $srcFuncId = $class . '::__construct';
        // $decorators = $this->getDecorators($srcFuncId);
        $decorators = $this->getDecorators($class, '__construct');
        foreach ($decorators as $func) {
            $this->call($func, $ret);
        }

        return $ret;
    }

    /**
     * constuct strategy:
     * inject through class public string properties assuming they contain the required class names
     * assume the class constructor has no params
     */
    private function constructPublicPropInjector(string $class)
    {
        $ret = new $class();

        $vars = get_class_vars($class);
        foreach ($vars as $propName => $propClass) {
            if ('self' === $propName) {
                $propClass = $class;
            }
            if (!is_string($propClass)) {
                continue;
            }
            $ret->{$propName} = $this->make($propClass);
        }

        return $ret;
    }

    public function onBoot(string $class, $func): self
    {
        return $this->decorate($class, '__construct', $func);
    }

// register a decorator function: one that wraps an original func
    public function decorate(string $className, string $methodName, $decoratorFunc, bool $append = true): self
    {
        $srcFunc = $className . '::' . $methodName;

        $this->decorators[$srcFunc] ??= [];

        if ($append) {
            $this->decorators[$srcFunc][] = $decoratorFunc;
        } else {
            array_unshift($this->decorators[$srcFunc], $decoratorFunc);
        }

    // already instantiated? call it now
        if (('__construct' === $methodName) && isset($this->made[$className])) {
            $this->call($decoratorFunc, $this->made[$class]);
        }

        return $this;
    }

// register a listener function: one that runs after the original func
    public function listen(string $className, string $methodName, $listenerFunc): self
    {
        $srcFunc = $className . '::' . $methodName;

        $this->listeners[$srcFunc] ??= [];
        $this->listeners[$srcFunc][] = $listenerFunc;

    // already instantiated? call it now
        if (('__construct' === $methodName) && isset($this->made[$className])) {
            $this->call($listenerFunc, $this->made[$class]);
        }

        return $this;
    }

    private function getDecorators(string $class, string $method): array
    {
        $funcId = $class . '::' . $method;
        $ret = $this->decorators[$funcId] ?? [];

    // any plug-in decorator finder like WordPress?
        foreach ($this->filterGetDecorators as $func) {
            $ret = $func($funcId, $ret);
        }

    // parent classes?
        $parentClasses = class_parents($class);
        if ($parentClasses) {
            foreach ($parentClasses as $parentClass) {
                $ret = array_merge($ret, $this->getDecorators($parentClass, $method));
            }
        }

// if ('currentUserId' === $method) {
    // _print_r($ret);
// }

        return $ret;
    }

    private function getListeners(string $class, string $method): array
    {
        $funcId = $class . '::' . $method;
        $ret = $this->listeners[$funcId] ?? [];

    // any plug-in listener finder like WordPress?
        foreach ($this->filterGetListeners as $func) {
            $ret = $func($funcId, $ret);
        }

    // parent classes?
        $parentClasses = class_parents($class);
        if ($parentClasses) {
            foreach ($parentClasses as $parentClass) {
                $ret = array_merge($ret, $this->getListeners($parentClass, $method));
            }
        }

        return $ret;
    }

    public function filterGetDecorators(callable $func): self
    {
        $this->filterGetDecorators[] = $func;

        return $this;
    }

    public function filterGetListeners(callable $func): self
    {
        $this->filterGetListeners[] = $func;

        return $this;
    }

    public function getMainFile(): string
    {
        return $this->mainFile;
    }
}
