<?php
/*************************************************************************************/
/*      This file is part of the Thelia package.                                     */
/*                                                                                   */
/*      Copyright (c) OpenStudio                                                     */
/*      email : dev@thelia.net                                                       */
/*      web : http://www.thelia.net                                                  */
/*                                                                                   */
/*      For the full copyright and license information, please view the LICENSE.txt  */
/*      file that was distributed with this source code.                             */
/*************************************************************************************/

namespace Thelia\Module;

use Propel\Runtime\Connection\ConnectionInterface;
use Propel\Runtime\Propel;
use SplFileInfo;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Finder\Finder;
use Thelia\Core\Event\Cache\CacheEvent;
use Thelia\Core\Event\TheliaEvents;
use Thelia\Exception\InvalidModuleException;
use Thelia\Log\Tlog;
use Thelia\Model\Map\ModuleTableMap;
use Thelia\Model\Module;
use Thelia\Model\ModuleQuery;

/**
 * Class ModuleManagement
 * @package Thelia\Module
 * @author  Manuel Raynaud <manu@raynaud.io>
 */
class ModuleManagement
{
    protected $baseModuleDir;
    protected $reflected;

    /** @var ModuleDescriptorValidator $descriptorValidator */
    protected $descriptorValidator;

    protected $container;

    public function __construct(ContainerInterface $container)
    {
        $this->container = $container;
        $this->baseModuleDir = THELIA_MODULE_DIR;
    }

    public function updateModules(ContainerInterface $container)
    {
        $finder = new Finder();

        $finder
            ->name('module.xml')
            ->in($this->baseModuleDir . '*' . DS . 'Config')
        ;

        $errors = [];

        $modulesUpdated = [];

        foreach ($finder as $file) {
            try {
                $this->updateModule($file, $container);
            } catch (\Exception $ex) {
                // Guess module code
                $moduleCode = basename(dirname(dirname($file)));

                $errors[$moduleCode] = $ex;
            }
        }

        if (\count($errors) > 0) {
            throw new InvalidModuleException($errors);
        }

        if (\count($modulesUpdated)) {
            $this->cacheClear();
        }
    }

    /**
     * Update module information, and invoke install() for new modules (e.g. modules
     * just discovered), or update() modules for which version number ha changed.
     *
     * @param SplFileInfo $file the module.xml file descriptor
     * @param ContainerInterface $container the container
     *
     * @return Module
     *
     * @throws \Exception
     * @throws \Propel\Runtime\Exception\PropelException
     */
    public function updateModule($file, ContainerInterface $container)
    {
        $descriptorValidator = $this->getDescriptorValidator();

        $content = $descriptorValidator->getDescriptor($file->getRealPath());
        $reflected = new \ReflectionClass((string)$content->fullnamespace);
        $code = basename(dirname($reflected->getFileName()));
        $version = (string)$content->version;
        $mandatory = \intval($content->mandatory);
        $hidden = \intval($content->hidden);

        $module = ModuleQuery::create()->filterByCode($code)->findOne();

        if (null === $module) {
            $module = new Module();
            $module->setActivate(0);

            $action = 'install';
        } elseif ($version !== $module->getVersion()) {
            $currentVersion = $module->getVersion();
            $action = 'update';
        } else {
            $action = 'none';
        }

        $con = Propel::getWriteConnection(ModuleTableMap::DATABASE_NAME);
        $con->beginTransaction();

        try {
            $module
                ->setCode($code)
                ->setVersion($version)
                ->setFullNamespace((string)$content->fullnamespace)
                ->setType($this->getModuleType($reflected))
                ->setCategory((string)$content->type)
                ->setMandatory($mandatory)
                ->setHidden($hidden)
                ->save($con)
            ;

            // Update the module images, title and description when the module is installed, but not after
            // as these data may have been modified byt the administrator
            if ('install' === $action) {
                $this->saveDescription($module, $content, $con);

                if (isset($content->{"images-folder"}) && !$module->isModuleImageDeployed($con)) {
                    /** @var \Thelia\Module\BaseModule $moduleInstance */
                    $moduleInstance = $reflected->newInstance();
                    $imagesFolder = THELIA_MODULE_DIR . $code . DS . (string)$content->{"images-folder"};
                    $moduleInstance->deployImageFolder($module, $imagesFolder, $con);
                }
            }

            // Tell the module to install() or update()
            $instance = $module->createInstance();

            $instance->setContainer($container);

            if ($action == 'install') {
                $instance->install($con);
            } elseif ($action == 'update') {
                $instance->update($currentVersion, $version, $con);
            }

            if ($action !== 'none') {
                $instance->registerHooks();
            }

            $con->commit();
        } catch (\Exception $ex) {
            Tlog::getInstance()->addError("Failed to update module " . $module->getCode(), $ex);

            $con->rollBack();
            throw $ex;
        }

        return $module;
    }

    /**
     * @return \Thelia\Module\ModuleDescriptorValidator
     */
    public function getDescriptorValidator()
    {
        if (null === $this->descriptorValidator) {
            $this->descriptorValidator = new ModuleDescriptorValidator();
        }

        return $this->descriptorValidator;
    }

    protected function cacheClear()
    {
        $cacheEvent = new CacheEvent(
            $this->container->getParameter('kernel.cache_dir')
        );

        $this->container->get('event_dispatcher')->dispatch(TheliaEvents::CACHE_CLEAR, $cacheEvent);
    }

    private function getModuleType(\ReflectionClass $reflected)
    {
        if (
            $reflected->implementsInterface('Thelia\Module\DeliveryModuleInterface')
                ||
            $reflected->implementsInterface('Thelia\Module\DeliveryModuleWithStateInterface')
        ) {
            return BaseModule::DELIVERY_MODULE_TYPE;
        } elseif ($reflected->implementsInterface('Thelia\Module\PaymentModuleInterface')) {
            return BaseModule::PAYMENT_MODULE_TYPE;
        } else {
            return BaseModule::CLASSIC_MODULE_TYPE;
        }
    }

    private function saveDescription(Module $module, \SimpleXMLElement $content, ConnectionInterface $con)
    {
        foreach ($content->descriptive as $description) {
            $locale = (string)$description->attributes()->locale;

            $module
                ->setLocale($locale)
                ->setTitle($description->title)
                ->setDescription(isset($description->description) ? $description->description : null)
                ->setPostscriptum(isset($description->postscriptum) ? $description->postscriptum : null)
                ->setChapo(isset($description->subtitle) ? $description->subtitle : null)
                ->save($con)
            ;
        }
    }
}
