<?php

namespace Laminas\EventManager;

use function array_keys;
use function array_merge;
use function gettype;
use function is_object;
use function is_string;
use function sprintf;

/**
 * Shared/contextual EventManager
 *
 * Allows attaching to EMs composed by other classes without having an instance first.
 * The assumption is that the SharedEventManager will be injected into EventManager
 * instances, and then queried for additional listeners when triggering an event.
 */
class SharedEventManager implements SharedEventManagerInterface
{
    /**
     * Identifiers with event connections
     *
     * @var array
     */
    protected $identifiers = [];

    /**
     * Attach a listener to an event emitted by components with specific identifiers.
     *
     * As an example, the following connects to the "getAll" event of both an
     * AbstractResource and EntityResource:
     *
     * <code>
     * $sharedEventManager = new SharedEventManager();
     * foreach (['My\Resource\AbstractResource', 'My\Resource\EntityResource'] as $identifier) {
     *     $sharedEventManager->attach(
     *         $identifier,
     *         'getAll',
     *         function ($e) use ($cache) {
     *             if (!$id = $e->getParam('id', false)) {
     *                 return;
     *             }
     *             if (!$data = $cache->load(get_class($resource) . '::getOne::' . $id )) {
     *                 return;
     *             }
     *             return $data;
     *         }
     *     );
     * }
     * </code>
     *
     * @param  string $identifier Identifier for event emitting component.
     * @param  string $event
     * @param  callable $listener Listener that will handle the event.
     * @param  int $priority Priority at which listener should execute
     * @return void
     * @throws Exception\InvalidArgumentException For invalid identifier arguments.
     * @throws Exception\InvalidArgumentException For invalid event arguments.
     */
    public function attach($identifier, $event, callable $listener, $priority = 1)
    {
        if (! is_string($identifier) || empty($identifier)) {
            throw new Exception\InvalidArgumentException(sprintf(
                'Invalid identifier provided; must be a string; received "%s"',
                is_object($identifier) ? $identifier::class : gettype($identifier)
            ));
        }

        if (! is_string($event) || empty($event)) {
            throw new Exception\InvalidArgumentException(sprintf(
                'Invalid event provided; must be a non-empty string; received "%s"',
                is_object($event) ? $event::class : gettype($event)
            ));
        }

        $this->identifiers[$identifier][$event][(int) $priority][] = $listener;
    }

    /**
     * @inheritDoc
     */
    public function detach(callable $listener, $identifier = null, $eventName = null, $force = false)
    {
        // No identifier or wildcard identifier: loop through all identifiers and detach
        if (null === $identifier || ('*' === $identifier && ! $force)) {
            foreach (array_keys($this->identifiers) as $identifier) {
                $this->detach($listener, $identifier, $eventName, true);
            }
            return;
        }

        if (! is_string($identifier) || empty($identifier)) {
            throw new Exception\InvalidArgumentException(sprintf(
                'Invalid identifier provided; must be a string, received %s',
                is_object($identifier) ? $identifier::class : gettype($identifier)
            ));
        }

        // Do we have any listeners on the provided identifier?
        if (! isset($this->identifiers[$identifier])) {
            return;
        }

        if (null === $eventName || ('*' === $eventName && ! $force)) {
            foreach (array_keys($this->identifiers[$identifier]) as $eventName) {
                $this->detach($listener, $identifier, $eventName, true);
            }
            return;
        }

        if (! is_string($eventName) || empty($eventName)) {
            throw new Exception\InvalidArgumentException(sprintf(
                'Invalid event name provided; must be a string, received %s',
                is_object($eventName) ? $eventName::class : gettype($eventName)
            ));
        }

        if (! isset($this->identifiers[$identifier][$eventName])) {
            return;
        }

        foreach ($this->identifiers[$identifier][$eventName] as $priority => $listeners) {
            foreach ($listeners as $index => $evaluatedListener) {
                if ($evaluatedListener !== $listener) {
                    continue;
                }

                // Found the listener; remove it.
                unset($this->identifiers[$identifier][$eventName][$priority][$index]);

                // Is the priority queue empty?
                if (empty($this->identifiers[$identifier][$eventName][$priority])) {
                    unset($this->identifiers[$identifier][$eventName][$priority]);
                    break;
                }
            }

            // Is the event queue empty?
            if (empty($this->identifiers[$identifier][$eventName])) {
                unset($this->identifiers[$identifier][$eventName]);
                break;
            }
        }

        // Is the identifier queue now empty? Remove it.
        if (empty($this->identifiers[$identifier])) {
            unset($this->identifiers[$identifier]);
        }
    }

    /**
     * Retrieve all listeners for a given identifier and event
     *
     * @param  string[] $identifiers
     * @param  string   $eventName
     * @return array[]
     * @throws Exception\InvalidArgumentException
     */
    public function getListeners(array $identifiers, $eventName)
    {
        if ('*' === $eventName || ! is_string($eventName) || empty($eventName)) {
            throw new Exception\InvalidArgumentException(sprintf(
                'Event name passed to %s must be a non-empty, non-wildcard string',
                __METHOD__
            ));
        }

        $returnListeners = [];

        foreach ($identifiers as $identifier) {
            if ('*' === $identifier || ! is_string($identifier) || empty($identifier)) {
                throw new Exception\InvalidArgumentException(sprintf(
                    'Identifier names passed to %s must be non-empty, non-wildcard strings',
                    __METHOD__
                ));
            }

            if (isset($this->identifiers[$identifier])) {
                $listenersByIdentifier = $this->identifiers[$identifier];
                if (isset($listenersByIdentifier[$eventName])) {
                    foreach ($listenersByIdentifier[$eventName] as $priority => $listeners) {
                        $returnListeners[$priority][] = $listeners;
                    }
                }
                if (isset($listenersByIdentifier['*'])) {
                    foreach ($listenersByIdentifier['*'] as $priority => $listeners) {
                        $returnListeners[$priority][] = $listeners;
                    }
                }
            }
        }

        if (isset($this->identifiers['*'])) {
            $wildcardIdentifier = $this->identifiers['*'];
            if (isset($wildcardIdentifier[$eventName])) {
                foreach ($wildcardIdentifier[$eventName] as $priority => $listeners) {
                    $returnListeners[$priority][] = $listeners;
                }
            }
            if (isset($wildcardIdentifier['*'])) {
                foreach ($wildcardIdentifier['*'] as $priority => $listeners) {
                    $returnListeners[$priority][] = $listeners;
                }
            }
        }

        foreach ($returnListeners as $priority => $listOfListeners) {
            $returnListeners[$priority] = array_merge(...$listOfListeners);
        }

        return $returnListeners;
    }

    /**
     * @inheritDoc
     */
    public function clearListeners($identifier, $eventName = null)
    {
        if (! isset($this->identifiers[$identifier])) {
            return false;
        }

        if (null === $eventName) {
            unset($this->identifiers[$identifier]);
            return;
        }

        if (! isset($this->identifiers[$identifier][$eventName])) {
            return;
        }

        unset($this->identifiers[$identifier][$eventName]);
    }
}
