<?php

/*
 * This file is part of the overtrue/wechat.
 *
 * (c) overtrue <i@overtrue.me>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */
namespace EasyWeChat\Kernel\Traits;

use EasyWeChat\Kernel\Clauses\Clause;
use EasyWeChat\Kernel\Contracts\EventHandlerInterface;
use EasyWeChat\Kernel\Decorators\FinallyResult;
use EasyWeChat\Kernel\Decorators\TerminateResult;
use EasyWeChat\Kernel\Exceptions\InvalidArgumentException;
use EasyWeChat\Kernel\ServiceContainer;
/**
 * Trait Observable.
 *
 * @author overtrue <i@overtrue.me>
 */
trait Observable
{
    /**
     * @var array
     */
    protected $handlers = [];
    /**
     * @var array
     */
    protected $clauses = [];
    /**
     * @param \Closure|EventHandlerInterface|callable|$handler
     * @param \Closure|EventHandlerInterface|callable|$condition
     *
     * @return \EasyWeChat\Kernel\Clauses\Clause
     *
     * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
     * @throws \ReflectionException
     */
    public function push($handler, $condition = '*')
    {
        list($handler, $condition) = $this->resolveHandlerAndCondition($handler, $condition);
        if (!isset($this->handlers[$condition])) {
            $this->handlers[$condition] = [];
        }
        array_push($this->handlers[$condition], $handler);
        return $this->newClause($handler);
    }
    /**
     * @param array $handlers
     *
     * @return $this
     */
    public function setHandlers(array $handlers = [])
    {
        $this->handlers = $handlers;
        return $this;
    }
    /**
     * @param \Closure|EventHandlerInterface|$handler
     * @param \Closure|EventHandlerInterface|$condition
     *
     * @return \EasyWeChat\Kernel\Clauses\Clause
     *
     * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
     * @throws \ReflectionException
     */
    public function unshift($handler, $condition = '*')
    {
        list($handler, $condition) = $this->resolveHandlerAndCondition($handler, $condition);
        if (!isset($this->handlers[$condition])) {
            $this->handlers[$condition] = [];
        }
        array_unshift($this->handlers[$condition], $handler);
        return $this->newClause($handler);
    }
    /**
     * @param                                $condition
     * @param \Closure|EventHandlerInterface|$handler
     *
     * @return \EasyWeChat\Kernel\Clauses\Clause
     *
     * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
     * @throws \ReflectionException
     */
    public function observe($condition, $handler)
    {
        return $this->push($handler, $condition);
    }
    /**
     * @param                                $condition
     * @param \Closure|EventHandlerInterface|$handler
     *
     * @return \EasyWeChat\Kernel\Clauses\Clause
     *
     * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
     * @throws \ReflectionException
     */
    public function on($condition, $handler)
    {
        return $this->push($handler, $condition);
    }
    /**
     * @param string|int $event
     * @param mixed      ...$payload
     *
     * @return mixed|null
     */
    public function dispatch($event, $payload)
    {
        return $this->notify($event, $payload);
    }
    /**
     * @param string|int $event
     * @param mixed      ...$payload
     *
     * @return mixed|null
     */
    public function notify($event, $payload)
    {
        $result = null;
        foreach ($this->handlers as $condition => $handlers) {
            if ('*' === $condition || ($condition & $event) === $event) {
                foreach ($handlers as $handler) {
                    if ($clause = isset($this->clauses[$this->getHandlerHash($handler)]) ? $this->clauses[$this->getHandlerHash($handler)] : null) {
                        if ($clause->intercepted($payload)) {
                            continue;
                        }
                    }
                    $response = $this->callHandler($handler, $payload);
                    switch (true) {
                        case $response instanceof TerminateResult:
                            return $response->content;
                        case true === $response:
                            continue 2;
                        case false === $response:
                            break 3;
                        case !empty($response) && !$result instanceof FinallyResult:
                            $result = $response;
                    }
                }
            }
        }
        return $result instanceof FinallyResult ? $result->content : $result;
    }
    /**
     * @return array
     */
    public function getHandlers()
    {
        return $this->handlers;
    }
    /**
     * @param mixed $handler
     *
     * @return \EasyWeChat\Kernel\Clauses\Clause
     */
    protected function newClause($handler)
    {
        return $this->clauses[$this->getHandlerHash($handler)] = new Clause();
    }
    /**
     * @param mixed $handler
     *
     * @return string
     */
    protected function getHandlerHash($handler)
    {
        if (is_string($handler)) {
            return $handler;
        }
        if (is_array($handler)) {
            return is_string($handler[0]) ? $handler[0] . '::' . $handler[1] : get_class($handler[0]) . $handler[1];
        }
        return spl_object_hash($handler);
    }
    /**
     * @param callable $handler
     * @param mixed    $payload
     *
     * @return mixed
     */
    protected function callHandler(callable $handler, $payload)
    {
        try {
            return call_user_func_array($handler, [$payload]);
        } catch (\Exception $e) {
            if (property_exists($this, 'app') && $this->app instanceof ServiceContainer) {
                $this->app['logger']->error($e->getCode() . ': ' . $e->getMessage(), ['code' => $e->getCode(), 'message' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine()]);
            }
        }
    }
    /**
     * @param mixed $handler
     *
     * @return \Closure
     *
     * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
     * @throws \ReflectionException
     */
    protected function makeClosure($handler)
    {
        if (is_callable($handler)) {
            return $handler;
        }
        if (is_string($handler) && '*' !== $handler) {
            if (!class_exists($handler)) {
                throw new InvalidArgumentException(sprintf('Class "%s" not exists.', $handler));
            }
            if (!in_array(EventHandlerInterface::class, (new \ReflectionClass($handler))->getInterfaceNames(), true)) {
                throw new InvalidArgumentException(sprintf('Class "%s" not an instance of "%s".', $handler, EventHandlerInterface::class));
            }
            return function ($payload) use($handler) {
                return (new $handler(isset($this->app) ? $this->app : null))->handle($payload);
            };
        }
        if ($handler instanceof EventHandlerInterface) {
            return function () use($handler) {
                return $handler->handle(...func_get_args());
            };
        }
        throw new InvalidArgumentException('No valid handler is found in arguments.');
    }
    /**
     * @param mixed $handler
     * @param mixed $condition
     *
     * @return array
     *
     * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
     * @throws \ReflectionException
     */
    protected function resolveHandlerAndCondition($handler, $condition)
    {
        if (is_int($handler) || is_string($handler) && !class_exists($handler)) {
            list($handler, $condition) = [$condition, $handler];
        }
        return [$this->makeClosure($handler), $condition];
    }
}