<?php
declare(strict_types=1);

namespace PhpList\Core\Composer;

use Composer\Package\PackageInterface;
use Composer\Script\Event;
use PhpList\Core\Core\ApplicationStructure;
use Sensio\Bundle\DistributionBundle\Composer\ScriptHandler as SensioScriptHandler;
use Symfony\Component\Filesystem\Filesystem;

/**
 * This class provides Composer-related functionality for setting up and managing phpList modules.
 *
 * @author Oliver Klee <oliver@phplist.com>
 */
class ScriptHandler extends SensioScriptHandler
{
    /**
     * @var string
     */
    const CORE_PACKAGE_NAME = 'phplist/core';

    /**
     * @var string
     */
    const BUNDLE_CONFIGURATION_FILE = '/config/bundles.yml';

    /**
     * @var string
     */
    const ROUTES_CONFIGURATION_FILE = '/config/routing_modules.yml';

    /**
     * @var string
     */
    const PARAMETERS_CONFIGURATION_FILE = '/config/parameters.yml';

    /**
     * @var string
     */
    const GENERAL_CONFIGURATION_FILE = '/config/config_modules.yml';

    /**
     * @var string
     */
    const PARAMETERS_TEMPLATE_FILE = '/config/parameters.yml.dist';

    /**
     * @return string absolute application root directory without the trailing slash
     *
     * @throws \RuntimeException if there is no composer.json in the application root
     */
    private static function getApplicationRoot(): string
    {
        $applicationStructure = new ApplicationStructure();
        return $applicationStructure->getApplicationRoot();
    }

    /**
     * @return string absolute directory without the trailing slash
     */
    private static function getCoreDirectory(): string
    {
        return static::getApplicationRoot() . '/vendor/' . static::CORE_PACKAGE_NAME;
    }

    /**
     * Creates the "bin/" directory and its contents, copying it from the core package.
     *
     * This method must not be called for the core package itself.
     *
     * @param Event $event
     *
     * @return void
     *
     * @throws \DomainException if this method is called for the core package
     */
    public static function createBinaries(Event $event)
    {
        static::preventScriptFromCorePackage($event);
        static::mirrorDirectoryFromCore('bin');
    }

    /**
     * Creates the "public/" directory and its contents, copying it from the core package.
     *
     * This method must not be called for the core package itself.
     *
     * @param Event $event
     *
     * @return void
     *
     * @throws \DomainException if this method is called for the core package
     */
    public static function createPublicWebDirectory(Event $event)
    {
        static::preventScriptFromCorePackage($event);
        static::mirrorDirectoryFromCore('public');
    }

    /**
     * @param Event $event
     *
     * @return void
     *
     * @throws \DomainException if this method is called for the core package
     */
    private static function preventScriptFromCorePackage(Event $event)
    {
        $composer = $event->getComposer();
        $packageName = $composer->getPackage()->getName();
        if ($packageName === static::CORE_PACKAGE_NAME) {
            throw new \DomainException(
                'This Composer script must not be called for the core package itself.',
                1501240572
            );
        }
    }

    /**
     * Copies a directory from the core package.
     *
     * This method overwrites existing files, but will not delete any files.
     *
     * This method must not be called for the core package itself.
     *
     * @param string $directoryWithoutSlashes directory name (without any slashes) relative to the core package
     *
     * @return void
     */
    private static function mirrorDirectoryFromCore(string $directoryWithoutSlashes)
    {
        $directoryWithSlashes = '/' . $directoryWithoutSlashes . '/';

        $fileSystem = new Filesystem();
        $fileSystem->mirror(
            static::getCoreDirectory() . $directoryWithSlashes,
            static::getApplicationRoot() . $directoryWithSlashes,
            null,
            ['override' => true, 'delete' => false]
        );
    }

    /**
     * Echos the names and version numbers of all installed phpList modules.
     *
     * @param Event $event
     *
     * @return void
     */
    public static function listModules(Event $event)
    {
        $packageRepository = new PackageRepository();
        $packageRepository->injectComposer($event->getComposer());

        $modules = $packageRepository->findModules();
        $maximumPackageNameLength = static::calculateMaximumPackageNameLength($modules);

        foreach ($modules as $module) {
            $paddedName = str_pad($module->getName(), $maximumPackageNameLength + 1);
            echo $paddedName . ' ' . $module->getPrettyVersion() . PHP_EOL;
        }
    }

    /**
     * @param PackageInterface[] $modules
     *
     * @return int
     */
    private static function calculateMaximumPackageNameLength(array $modules): int
    {
        $maximumLength = 0;
        foreach ($modules as $module) {
            $maximumLength = max($maximumLength, strlen($module->getName()));
        }

        return $maximumLength;
    }

    /**
     * Creates config/bundles.yml
     * (the configuration file for the Symfony bundles provided by the modules).
     *
     * @param Event $event
     *
     * @return void
     */
    public static function createBundleConfiguration(Event $event)
    {
        static::createAndWriteFile(
            static::getApplicationRoot() . static::BUNDLE_CONFIGURATION_FILE,
            static::createAndInitializeModuleFinder($event)->createBundleConfigurationYaml()
        );
    }

    /**
     * Writes $contents to the file with the path $path.
     *
     * If the file does not exist yet, it will be created.
     *
     * If the file already exists, it will be overwritten.
     *
     * @param string $path
     * @param string $contents
     *
     * @return void
     *
     * @throws \RuntimeException
     */
    private static function createAndWriteFile(string $path, string $contents)
    {
        $fileHandle = fopen($path, 'wb');
        if ($fileHandle === false) {
            throw new \RuntimeException('The file "' . $path . '" could not be opened for writing.', 1519851153);
        }

        fwrite($fileHandle, $contents);
        fclose($fileHandle);
    }

    /**
     * Creates config/routing_modules.yml
     * (the routes file for the Symfony bundles provided by the modules).
     *
     * @param Event $event
     *
     * @return void
     */
    public static function createRoutesConfiguration(Event $event)
    {
        static::createAndWriteFile(
            static::getApplicationRoot() . static::ROUTES_CONFIGURATION_FILE,
            static::createAndInitializeModuleFinder($event)->createRouteConfigurationYaml()
        );
    }

    /**
     * @param Event $event
     *
     * @return ModuleFinder
     */
    private static function createAndInitializeModuleFinder(Event $event): ModuleFinder
    {
        $packageRepository = new PackageRepository();
        $packageRepository->injectComposer($event->getComposer());

        $bundleFinder = new ModuleFinder();
        $bundleFinder->injectPackageRepository($packageRepository);

        return $bundleFinder;
    }

    /**
     * Clears the caches of all environments. This does not warm the caches.
     *
     * @return void
     */
    public static function clearAllCaches()
    {
        $fileSystem = new Filesystem();
        $fileSystem->remove(static::getApplicationRoot() . '/var/cache');
    }

    /**
     * Warms the production cache.
     *
     * @param Event $event
     *
     * @return void
     */
    public static function warmProductionCache(Event $event)
    {
        $consoleDir = static::getConsoleDir($event, 'warm the cache');
        if ($consoleDir === null) {
            return;
        }

        static::executeCommand($event, $consoleDir, 'cache:warm -e prod');
    }

    /**
     * Creates config/config_modules.yml (the general configuration provided by the modules).
     *
     * @param Event $event
     *
     * @return void
     */
    public static function createGeneralConfiguration(Event $event)
    {
        static::createAndWriteFile(
            static::getApplicationRoot() . static::GENERAL_CONFIGURATION_FILE,
            static::createAndInitializeModuleFinder($event)->createGeneralConfigurationYaml()
        );
    }
}
