<?php

declare(strict_types=1);

namespace Laminas\Mvc\Plugin\FlashMessenger;

use ArrayIterator;
use Countable;
use IteratorAggregate;
use Laminas\Mvc\Controller\Plugin\AbstractPlugin;
use Laminas\Session\Container;
use Laminas\Session\ManagerInterface as Manager;
use Laminas\Stdlib\SplQueue;
use ReturnTypeWillChange;

use function assert;
use function count;

/**
 * Flash Messenger - implement session-based messages
 *
 * @template-implements IteratorAggregate<array-key, string>
 * @psalm-type MessageList = SplQueue<array-key, string>
 */
class FlashMessenger extends AbstractPlugin implements IteratorAggregate, Countable
{
    /**
     * Default messages namespace
     */
    public const NAMESPACE_DEFAULT = 'default';

    /**
     * Success messages namespace
     */
    public const NAMESPACE_SUCCESS = 'success';

    /**
     * Warning messages namespace
     */
    public const NAMESPACE_WARNING = 'warning';

    /**
     * Error messages namespace
     */
    public const NAMESPACE_ERROR = 'error';

    /**
     * Info messages namespace
     */
    public const NAMESPACE_INFO = 'info';

    /** @var Container|null */
    protected $container;

    /**
     * Messages from previous request
     *
     * @var array<string, MessageList>
     */
    protected $messages = [];

    /** @var Manager|null */
    protected $session;

    /**
     * Whether a message has been added during this request
     *
     * @var bool
     */
    protected $messageAdded = false;

    /**
     * Instance namespace, default is 'default'
     *
     * @var string
     */
    protected $namespace = self::NAMESPACE_DEFAULT;

    /**
     * Set the session manager
     *
     * @return FlashMessenger
     */
    public function setSessionManager(Manager $manager)
    {
        $this->session = $manager;
        return $this;
    }

    /**
     * Retrieve the session manager
     *
     * If none composed, lazy-loads a SessionManager instance
     *
     * @return Manager
     */
    public function getSessionManager()
    {
        if (! $this->session instanceof Manager) {
            $this->setSessionManager(Container::getDefaultManager());
        }

        assert($this->session instanceof Manager);

        return $this->session;
    }

    /**
     * Get session container for flash messages
     *
     * @return Container
     */
    public function getContainer()
    {
        if ($this->container instanceof Container) {
            return $this->container;
        }

        $manager         = $this->getSessionManager();
        $this->container = new Container('FlashMessenger', $manager);

        return $this->container;
    }

    /**
     * Change the namespace messages are added to
     *
     * Useful for per action controller messaging between requests
     *
     * @param  string         $namespace
     * @return FlashMessenger Provides a fluent interface
     */
    public function setNamespace($namespace = 'default')
    {
        $this->namespace = $namespace;
        return $this;
    }

    /**
     * Get the message namespace
     *
     * @return string
     */
    public function getNamespace()
    {
        return $this->namespace;
    }

    /**
     * Add a message
     *
     * @param  string         $message
     * @param  null|string    $namespace
     * @param  int            $hops
     * @return FlashMessenger Provides a fluent interface
     */
    public function addMessage($message, $namespace = null, $hops = 1)
    {
        $container = $this->getContainer();

        if (null === $namespace) {
            $namespace = $this->getNamespace();
        }

        if (! $this->messageAdded) {
            $this->getMessagesFromContainer();
            $container->setExpirationHops($hops, null);
        }

        if (
            ! isset($container->{$namespace})
            || ! $container->{$namespace} instanceof SplQueue
        ) {
            $container->{$namespace} = new SplQueue();
        }

        $container->{$namespace}->push($message);

        $this->messageAdded = true;

        return $this;
    }

    /**
     * Add a message with "info" type
     *
     * @param  string         $message
     * @return FlashMessenger
     */
    public function addInfoMessage($message)
    {
        $this->addMessage($message, self::NAMESPACE_INFO);
        return $this;
    }

    /**
     * Add a message with "success" type
     *
     * @param  string         $message
     * @return FlashMessenger
     */
    public function addSuccessMessage($message)
    {
        $this->addMessage($message, self::NAMESPACE_SUCCESS);
        return $this;
    }

    /**
     * Add a message with "warning" type
     *
     * @param string        $message
     * @return FlashMessenger
     */
    public function addWarningMessage($message)
    {
        $this->addMessage($message, self::NAMESPACE_WARNING);
        return $this;
    }

    /**
     * Add a message with "error" type
     *
     * @param  string         $message
     * @return FlashMessenger
     */
    public function addErrorMessage($message)
    {
        $this->addMessage($message, self::NAMESPACE_ERROR);
        return $this;
    }

    /**
     * Whether a specific namespace has messages
     *
     * @param  string         $namespace
     * @return bool
     */
    public function hasMessages($namespace = null)
    {
        if (null === $namespace) {
            $namespace = $this->getNamespace();
        }

        $this->getMessagesFromContainer();

        return isset($this->messages[$namespace]);
    }

    /**
     * Whether "info" namespace has messages
     *
     * @return bool
     */
    public function hasInfoMessages()
    {
        return $this->hasMessages(self::NAMESPACE_INFO);
    }

    /**
     * Whether "success" namespace has messages
     *
     * @return bool
     */
    public function hasSuccessMessages()
    {
        return $this->hasMessages(self::NAMESPACE_SUCCESS);
    }

    /**
     * Whether "warning" namespace has messages
     *
     * @return bool
     */
    public function hasWarningMessages()
    {
        return $this->hasMessages(self::NAMESPACE_WARNING);
    }

    /**
     * Whether "error" namespace has messages
     *
     * @return bool
     */
    public function hasErrorMessages()
    {
        return $this->hasMessages(self::NAMESPACE_ERROR);
    }

    /**
     * Get messages from a specific namespace
     *
     * @param  string         $namespace
     * @return array<array-key, string>
     */
    public function getMessages($namespace = null)
    {
        if (null === $namespace) {
            $namespace = $this->getNamespace();
        }

        if ($this->hasMessages($namespace)) {
            return $this->messages[$namespace]->toArray();
        }

        return [];
    }

    /**
     * Get messages from "info" namespace
     *
     * @return array
     */
    public function getInfoMessages()
    {
        return $this->getMessages(self::NAMESPACE_INFO);
    }

    /**
     * Get messages from "success" namespace
     *
     * @return array<array-key, string>
     */
    public function getSuccessMessages()
    {
        return $this->getMessages(self::NAMESPACE_SUCCESS);
    }

    /**
     * Get messages from "warning" namespace
     *
     * @return array<array-key, string>
     */
    public function getWarningMessages()
    {
        return $this->getMessages(self::NAMESPACE_WARNING);
    }

    /**
     * Get messages from "error" namespace
     *
     * @return array<array-key, string>
     */
    public function getErrorMessages()
    {
        return $this->getMessages(self::NAMESPACE_ERROR);
    }

    /**
     * Clear all messages from the previous request & current namespace
     *
     * @param  string|null $namespace
     * @return bool True if messages were cleared, false if none existed
     */
    public function clearMessages($namespace = null)
    {
        if (null === $namespace) {
            $namespace = $this->getNamespace();
        }

        if ($this->hasMessages($namespace)) {
            unset($this->messages[$namespace]);
            return true;
        }

        return false;
    }

    /**
     * Clear all messages from specific namespace
     *
     * @param  string $namespaceToClear
     * @return bool True if messages were cleared, false if none existed
     */
    public function clearMessagesFromNamespace($namespaceToClear)
    {
        return $this->clearMessages($namespaceToClear);
    }

    /**
     * Clear all messages from the container
     *
     * @return bool True if messages were cleared, false if none existed
     */
    public function clearMessagesFromContainer()
    {
        $this->getMessagesFromContainer();
        if (empty($this->messages)) {
            return false;
        }

        unset($this->messages);
        $this->messages = [];

        return true;
    }

    /**
     * Check to see if messages have been added to the current
     * namespace within this request
     *
     * @param  string|null $namespace
     * @return bool
     */
    public function hasCurrentMessages($namespace = null)
    {
        $container = $this->getContainer();
        if (null === $namespace) {
            $namespace = $this->getNamespace();
        }

        return isset($container->{$namespace});
    }

    /**
     * Check to see if messages have been added to "info"
     * namespace within this request
     *
     * @return bool
     */
    public function hasCurrentInfoMessages()
    {
        return $this->hasCurrentMessages(self::NAMESPACE_INFO);
    }

    /**
     * Check to see if messages have been added to "success"
     * namespace within this request
     *
     * @return bool
     */
    public function hasCurrentSuccessMessages()
    {
        return $this->hasCurrentMessages(self::NAMESPACE_SUCCESS);
    }

    /**
     * Check to see if messages have been added to "warning"
     * namespace within this request
     *
     * @return bool
     */
    public function hasCurrentWarningMessages()
    {
        return $this->hasCurrentMessages(self::NAMESPACE_WARNING);
    }

    /**
     * Check to see if messages have been added to "error"
     * namespace within this request
     *
     * @return bool
     */
    public function hasCurrentErrorMessages()
    {
        return $this->hasCurrentMessages(self::NAMESPACE_ERROR);
    }

    /**
     * Get messages that have been added to the current
     * namespace within this request
     *
     * @param  string|null $namespace
     * @return array<array-key, string>
     */
    public function getCurrentMessages($namespace = null)
    {
        if (null === $namespace) {
            $namespace = $this->getNamespace();
        }

        if ($this->hasCurrentMessages($namespace)) {
            $container = $this->getContainer();
            /** @psalm-var MessageList $queue */
            $queue = $container->{$namespace};

            return $queue->toArray();
        }

        return [];
    }

    /**
     * Get messages that have been added to the "info"
     * namespace within this request
     *
     * @return array<array-key, string>
     */
    public function getCurrentInfoMessages()
    {
        return $this->getCurrentMessages(self::NAMESPACE_INFO);
    }

    /**
     * Get messages that have been added to the "success"
     * namespace within this request
     *
     * @return array<array-key, string>
     */
    public function getCurrentSuccessMessages()
    {
        return $this->getCurrentMessages(self::NAMESPACE_SUCCESS);
    }

    /**
     * Get messages that have been added to the "warning"
     * namespace within this request
     *
     * @return array<array-key, string>
     */
    public function getCurrentWarningMessages()
    {
        return $this->getCurrentMessages(self::NAMESPACE_WARNING);
    }

    /**
     * Get messages that have been added to the "error"
     * namespace within this request
     *
     * @return array<array-key, string>
     */
    public function getCurrentErrorMessages()
    {
        return $this->getCurrentMessages(self::NAMESPACE_ERROR);
    }

    /**
     * Get messages that have been added to the current
     * namespace in specific namespace
     *
     * @param  string $namespaceToGet
     * @return array<array-key, string>
     */
    public function getCurrentMessagesFromNamespace($namespaceToGet)
    {
        return $this->getCurrentMessages($namespaceToGet);
    }

    /**
     * Clear messages from the current request and current namespace
     *
     * @param  string $namespace
     * @return bool True if current messages were cleared, false if none existed.
     */
    public function clearCurrentMessages($namespace = null)
    {
        if (null === $namespace) {
            $namespace = $this->getNamespace();
        }

        if ($this->hasCurrentMessages($namespace)) {
            $container = $this->getContainer();
            unset($container->{$namespace});
            return true;
        }

        return false;
    }

    /**
     * Clear messages from the current namespace
     *
     * @param  string $namespaceToClear
     * @return bool True if current messages were cleared from the given namespace, false if none existed.
     */
    public function clearCurrentMessagesFromNamespace($namespaceToClear)
    {
        return $this->clearCurrentMessages($namespaceToClear);
    }

    /**
     * Clear messages from the container
     *
     * @return bool True if current messages were cleared from the container, false if none existed.
     */
    public function clearCurrentMessagesFromContainer()
    {
        $container = $this->getContainer();

        $namespaces = [];
        foreach ($container as $namespace => $messages) {
            $namespaces[] = $namespace;
        }

        if (empty($namespaces)) {
            return false;
        }

        foreach ($namespaces as $namespace) {
            unset($container->{$namespace});
        }

        return true;
    }

    /**
     * Complete the IteratorAggregate interface, for iterating
     *
     * @return ArrayIterator<array-key, string>
     */
    #[ReturnTypeWillChange]
    public function getIterator()
    {
        if ($this->hasMessages()) {
            return new ArrayIterator($this->getMessages());
        }

        return new ArrayIterator();
    }

    /**
     * Complete the countable interface
     *
     * @return int
     */
    #[ReturnTypeWillChange]
    public function count()
    {
        if ($this->hasMessages()) {
            return count($this->getMessages());
        }

        return 0;
    }

    /**
     * Get messages from a specific namespace
     *
     * @param  string $namespaceToGet
     * @return array<array-key, string>
     */
    public function getMessagesFromNamespace($namespaceToGet)
    {
        return $this->getMessages($namespaceToGet);
    }

    /**
     * Pull messages from the session container
     *
     * Iterates through the session container, removing messages into the local
     * scope.
     *
     * @return void
     */
    protected function getMessagesFromContainer()
    {
        if (! empty($this->messages) || $this->messageAdded) {
            return;
        }

        $container = $this->getContainer();

        $namespaces = [];
        foreach ($container as $namespace => $messages) {
            $this->messages[$namespace] = $messages;
            $namespaces[]               = $namespace;
        }

        foreach ($namespaces as $namespace) {
            unset($container->{$namespace});
        }
    }
}
