<?php declare(strict_types=1);

namespace Newland\ToubizWidgetRendering\Renderer;

use Neos\Flow\Log\PsrLoggerFactory;
use Neos\Flow\Log\ThrowableStorageInterface;
use Neos\Flow\Utility\Environment;
use Newland\ToubizWidgetRendering\RendererFactory;
use Newland\ToubizWidgetRendering\Utility\PrefixedOutput;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Neos\Flow\Annotations as Flow;
use function Safe\usort as usort;

class FallbackStack implements ToubizWidgetRenderer
{

    /**
     * @var RendererFactory
     * @Flow\Inject
     */
    protected $factory;

    /**
     * @var ThrowableStorageInterface
     * @Flow\Inject
     */
    protected $throwableLogger;

    /**
     * @var Environment
     * @Flow\Inject()
     */
    protected $environment;

    /** @var LoggerInterface */
    protected $logger;

    public function injectLogger(PsrLoggerFactory $factory): void
    {
        $this->logger = $factory->get('newlandToubizWidgetRendering');
    }

    /** @var (string|ToubizWidgetRenderer)[][] */
    protected $renderers = [];

    public function setConfiguration(array $configuration): void
    {
        // Saving the key as __key property in order to convert associative array into
        // sequenced array sorted by priority.
        $rendererConfig = [];
        foreach ($configuration['renderers'] ?? [] as $key => $rawConfiguration) {
            if (!$rawConfiguration) {
                continue;
            }
            $rawConfiguration['__key'] = $key;
            $rendererConfig[] = $rawConfiguration;
        }
        usort(
            $rendererConfig,
            static function ($a, $b) {
                return ($b['priority'] ?? 0) <=> ($a['priority'] ?? 0);
            }
        );

        $renderers = [];
        foreach ($rendererConfig as $config) {
            $renderer = $this->initializeRenderer($config);
            if ($renderer) {
                $renderers[] = [ $config['__key'], $renderer ];
            }
        }
        $this->renderers = $renderers;
    }

    public function render(string $tagName, array $arguments): string
    {
        $messages = [];

        foreach ($this->renderers as [, $renderer]) {
            try {
                $result = $renderer->render($tagName, $arguments);
                if ($this->environment->getContext()->isDevelopment()) {
                    $result = implode("\n", $messages) . $result;
                }
                return $result;
            } catch (\Throwable $e) {
                $message = $this->throwableLogger->logThrowable($e);
                $messages[] = $message;
                $this->logger->error($message);
            }
        }

        throw new FallbackStackExceededException(
            sprintf(
                "All renderers in fallback stack failed.\n\n%s",
                implode("\n\n", $messages)
            )
        );
    }

    public function precompile(OutputInterface $output): void
    {
        foreach ($this->renderers as [$key, $renderer]) {
            $renderer->precompile(
                new PrefixedOutput(sprintf('[%s] ', $key), $output)
            );
        }
    }

    private function initializeRenderer(?array $config): ?ToubizWidgetRenderer
    {
        if ($config === null || $config['renderer'] === null) {
            return null;
        }

        if ($config['renderer'] instanceof ToubizWidgetRenderer) {
            $config['renderer']->setConfiguration($config['rendererConfiguration'] ?? []);
            return $config['renderer'];
        }

        return $this->factory->initialize($config['renderer'], $config['rendererConfiguration'] ?? []);
    }
}
