<?php
// Copyright (c) Pickware GmbH. All rights reserved.
// This file is part of software that is released under a proprietary license.
// You must not copy, modify, distribute, make publicly available, or execute
// its contents or parts thereof without express permission by the copyright
// holder, unless otherwise permitted by law.

namespace Shopware\Plugins\ViisonCommon\Components\SubApplicationLoading;

use Doctrine\Common\Collections\ArrayCollection;
use Enlight_Event_EventManager;
use Shopware\Plugins\ViisonCommon\Components\SubApplicationLoading\SubApplicationBuilder\SubApplicationBuilder;
use Shopware\Plugins\ViisonCommon\Components\SubApplicationLoading\SubApplicationCodeGenerator\SubApplicationCodeGeneratorFactoryService;

class JsLoaderService
{
    const EVENT_NOTIFY_FINISH_SUB_APPLICATION_REGISTRATION = 'Shopware_Plugins_ViisonCommon_FinishSubApplicationRegistration';
    const EVENT_NOTIFY_UNTIL_REGISTER_SUB_APPLICATION = 'Shopware_Plugins_ViisonCommon_RegisterSubApplication';
    const EVENT_COLLECT_SUB_APPLICATION_BUILDERS = 'ViisonCommon_Event_JsLoader_CollectSubApplicationBuilders';

    /**
     * @var Enlight_Event_EventManager
     */
    private $eventManager;

    /**
     * @var LegacyJsLoaderService
     */
    private $legacyJsLoaderService;

    /**
     * @var SubApplicationCodeGeneratorFactoryService
     */
    private $codeGeneratorFactory;

    /**
     * @param Enlight_Event_EventManager $eventManager
     * @param LegacyJsLoaderService $legacyJsLoaderService
     * @param SubApplicationCodeGeneratorFactoryService $codeGeneratorFactory
     */
    public function __construct(
        Enlight_Event_EventManager $eventManager,
        LegacyJsLoaderService $legacyJsLoaderService,
        SubApplicationCodeGeneratorFactoryService $codeGeneratorFactory
    ) {
        $this->eventManager = $eventManager;
        $this->legacyJsLoaderService = $legacyJsLoaderService;
        $this->codeGeneratorFactory = $codeGeneratorFactory;
    }

    /**
     * Extends the $jsCodeToExtend of a Shopware SubApplication request.
     *
     * @param string $requestedSubAppName Name of the requested SubApplication
     * @param string $jsCodeToExtend The already rendered Code of the SubApplication
     * @return string The extended JS code.
     */
    public function extendSubAppRequest(
        $requestedSubAppName,
        $jsCodeToExtend
    ) {
        $subApplicationRegistry = $this->collectSubApplications();
        $subApplication = $subApplicationRegistry->getSubApplicationByName($requestedSubAppName);

        if (!$subApplication) {
            // When a SubApplication is requested, that is neither a StandAloneSubApplication nor has any extension
            // (what usually is a SW SubApplication we do not touch), there will be no SubApplication with the
            // $requestedSubAppName in the $subApplicationRegistry.
            return $jsCodeToExtend;
        }

        // This method is implemented recursively via self::extendSubAppRequestRecursive() and exists only to provide
        // a method with a clean signature.
        return $this->extendSubAppRequestRecursively($requestedSubAppName, $jsCodeToExtend, $subApplicationRegistry);
    }

    /**
     * The actual recursive implementation of self::extendSubAppRequest().
     *
     * @param string $requestedSubAppName
     * @param string $jsCodeToExtend
     * @param SubApplicationRegistry $subApplicationRegistry
     * @param array &$renderedSubApplications A reference to an array containing the names of all already rendered
     * SubApplications.
     * @return string
     */
    private function extendSubAppRequestRecursively(
        $requestedSubAppName,
        $jsCodeToExtend,
        SubApplicationRegistry $subApplicationRegistry,
        array &$renderedSubApplications = []
    ) {
        $subApplication = $subApplicationRegistry->getSubApplicationByName($requestedSubAppName);

        if (!$subApplication) {
            throw new \RuntimeException(sprintf(
                'SubApplication with name "%s" was requested to be loaded, but no such SubApplication was found. ' .
                'Usually this happens when a SubApplication has a dependency on a SubApplication that was not ' .
                'registered in the SubApplicationRegistry.',
                $requestedSubAppName
            ));
        }

        if (in_array($subApplication->getName(), $renderedSubApplications)) {
            return $jsCodeToExtend;
        }
        $renderedSubApplications[] = $subApplication->getName();

        $dependencyCode = '';
        foreach ($subApplication->getDependencyNames() as $dependencyName) {
            $dependencyCode .= $this->extendSubAppRequestRecursively(
                $dependencyName,
                '',
                $subApplicationRegistry,
                $renderedSubApplications
            );
        }

        $actualCode = $subApplication->getCode();

        $extensionCode = '';
        foreach ($subApplication->getExtensionNames() as $extensionName) {
            $extensionCode .= $this->extendSubAppRequestRecursively(
                $extensionName,
                '',
                $subApplicationRegistry,
                $renderedSubApplications
            );
        }

        return $dependencyCode . $actualCode . $jsCodeToExtend . $extensionCode;
    }

    /**
     * Collects all SubApplications of all active plugins.
     *
     * @return SubApplicationRegistry
     */
    private function collectSubApplications()
    {
        // Fire collect event to collect all SubApplicationBuilders of every plugin
        $subApplicationBuilders = array_merge(
            $this->eventManager->collect(
                self::EVENT_COLLECT_SUB_APPLICATION_BUILDERS,
                new ArrayCollection()
            )->toArray(),
            // Load the SubApplicationBuilders for sub applications that were registered via the legacy way
            // LegacyJsLoaderService::registerSubApplication()
            $this->legacyJsLoaderService->collectSubApplicationBuilders()
        );

        // Fire an event to make it possible for plugins to block the loading of one or more sub applications
        array_filter($subApplicationBuilders, function (SubApplicationBuilder $subApplicationBuilder) {
            $eventResult = $this->eventManager->notifyUntil(
                self::EVENT_NOTIFY_UNTIL_REGISTER_SUB_APPLICATION,
                [
                    'name' => $subApplicationBuilder->getSubApplicationName(),
                ]
            );

            return $eventResult === null || $eventResult->getReturn() !== false;
        });

        // Build all SubApplications and put them into an new SubApplicationRegistry
        $subApplicationRegistry = new SubApplicationRegistry();
        /** @var SubApplicationBuilder $subApplicationBuilder */
        foreach ($subApplicationBuilders as $subApplicationBuilder) {
            $subApplicationBuilder->registerInSubApplicationRegistry(
                $subApplicationRegistry,
                $this->codeGeneratorFactory
            );
        }

        // Fire event to notify that the sub application registration has finished
        $this->eventManager->notify(self::EVENT_NOTIFY_FINISH_SUB_APPLICATION_REGISTRATION, [
            'subApplicationRegistry' => $subApplicationRegistry,
        ]);

        return $subApplicationRegistry;
    }
}
