<?php

/*
 * Copyright (C) 2015 IRSTEA
 * All rights reserved.
 */
namespace Irstea\PlantUmlBundle\Command;

use Doctrine\ORM\EntityManagerInterface;
use Irstea\PlantUmlBundle\Doctrine\AssociationDecorator;
use Irstea\PlantUmlBundle\Doctrine\DoctrineNamespace;
use Irstea\PlantUmlBundle\Doctrine\EntityDecorator;
use Irstea\PlantUmlBundle\Doctrine\EntityFinder;
use Irstea\PlantUmlBundle\Finder\ClassFinder;
use Irstea\PlantUmlBundle\Finder\FilteringFinder;
use Irstea\PlantUmlBundle\Finder\FinderInterface;
use Irstea\PlantUmlBundle\Model\ClassFilterInterface;
use Irstea\PlantUmlBundle\Model\ClassVisitor;
use Irstea\PlantUmlBundle\Model\Decorator\CompositeDecorator;
use Irstea\PlantUmlBundle\Model\Decorator\FilteringDecorator;
use Irstea\PlantUmlBundle\Model\Decorator\InheritanceDecorator;
use Irstea\PlantUmlBundle\Model\Decorator\InterfaceDecorator;
use Irstea\PlantUmlBundle\Model\Decorator\NullDecorator;
use Irstea\PlantUmlBundle\Model\Decorator\TraitDecorator;
use Irstea\PlantUmlBundle\Model\DecoratorInterface;
use Irstea\PlantUmlBundle\Model\Filter\AcceptAllFilter;
use Irstea\PlantUmlBundle\Model\Filter\Composite\AllFilter;
use Irstea\PlantUmlBundle\Model\Filter\DirectoryFilter;
use Irstea\PlantUmlBundle\Model\Filter\NamespaceFilter;
use Irstea\PlantUmlBundle\Model\Graph;
use Irstea\PlantUmlBundle\Model\Namespace_\BundleNamespace;
use Irstea\PlantUmlBundle\Model\Namespace_\FlatNamespace;
use Irstea\PlantUmlBundle\Model\Namespace_\Php\RootNamespace;
use Irstea\PlantUmlBundle\Model\NamespaceInterface;
use Irstea\PlantUmlBundle\Writer\OutputWriter;
use ReflectionClass;
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Config\Definition\Exception\Exception;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Finder\SplFileInfo;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Security\Core\Exception\InvalidArgumentException;
use Symfony\Component\Security\Core\Exception\RuntimeException;


/**
 * Description of ImportAffiliationCommand
 *
 * @author Guillaume Perréal <guillaume.perreal@irstea.fr>
 */
class GenerateCommand extends ContainerAwareCommand
{
    /**
     * @var string[]
     */
    private $bundles;

    /**
     * @var KernelInterface
     */
    private $kernel;

    /**
     * @var EntityManagerInterface
     */
    private $entityManager;

    protected function configure()
    {
        $this
            ->setName('irstea:plantuml:generate')
            ->setDescription("Génère un graphe en PlantUML.")
            ->addArgument('graph', InputArgument::REQUIRED, 'Nom du graphe à générer');
    }

    protected function initialize(InputInterface $input, OutputInterface $output)
    {
        parent::initialize($input, $output);

        // @todo: DI
        $this->bundles = $this->getContainer()->getParameter('kernel.bundles');
        $this->kernel =  $this->getContainer()->get('kernel');
        $this->entityManager =  $this->getContainer()->get('doctrine.orm.entity_manager');
    }

    /**
     *
     * @param InputInterface $input
     * @param OutputInterface $output
     *
     * @SuppressWarnings(UnusedFormalParameter)
     */
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $name = $input->getArgument('graph');
        $graphs = $this->getContainer()->getParameter('irstea_plant_uml.graphs');
        if (!isset($graphs[$name])) {
            throw new InvalidArgumentException("Le graphe '$name' n'est pas défini.");
        }

        $config = $graphs[$name];

        $graph = new Graph(
            new ClassVisitor(
                $this->buildDecorator($config['decoration']),
                $this->buildFilter($config['layout']),
                $this->buildNamespace($config['layout']['namespaces'])
            ),
            $this->buildFinder($config['sources'])
        );

        $graph->visitAll();

        $writer = new OutputWriter($output);
        $graph->outputTo($writer);
    }

    /**
     * @param array $config
     * @return FinderInterface
     */
    protected function buildFinder(array $config)
    {
        switch($config['type']) {
            case 'entities':
                $finder = $this->buildEntityFinder($config['entity_manager']);
                break;
            case 'classes':
                $finder = $this->buildClassFinder($config['directories']);
                break;
        }

        $filter = $this->buildFilter($config);
        if (!$filter) {
            return $finder;
        }

        return new FilteringFinder($finder, $filter);
    }

    /**
     * @param array $directories
     * @return FinderInterface
     */
    protected function buildClassFinder(array $directories)
    {
        return new ClassFinder($this->parseDirectories($directories));
    }

    /**
     * @param string $managerName
     * @return FinderInterface
     */
    protected function buildEntityFinder($managerName)
    {
        return new EntityFinder(
            $this->getContainer()->get('doctrine')->getManager($managerName)
        );
    }

    /**
     * @param array $config
     * @return DecoratorInterface
     * @throws RuntimeException
     */
    protected function buildDecorator(array $config)
    {
        if (empty($config['decorators'])) {
            return NullDecorator::instance();
        }

        $decorators = [];
        foreach($config['decorators'] as $type) {
            $decorators[] = $this->buildTypedDecorator($type);
        }

        if (count($decorators) === 1) {
            $decorator = $decorators[0];
        } else {
            $decorator = new CompositeDecorator($decorators);
        }

        $filter = $this->buildFilter($config);
        if ($filter) {
            $decorator = new FilteringDecorator($decorator, $filter);
        }

        return $decorator;
    }

    /**
     * @param type $type
     * @return DecoratorInterface
     */
    protected function buildTypedDecorator($type)
    {
        switch($type) {
            case 'inheritance':
                return new InheritanceDecorator();
            case 'interfaces':
                return new InterfaceDecorator();
            case 'traits':
                return new TraitDecorator();
            case 'entity':
                return new EntityDecorator($this->entityManager->getMetadataFactory());
            case 'associations':
                return new AssociationDecorator($this->entityManager->getMetadataFactory());
        }
    }

    /**
     * @param string $config
     * @return NamespaceInterface
     */
    protected function buildNamespace($config)
    {
        switch($config) {
            case 'php':
                return new RootNamespace();
            case 'flat':
                return new FlatNamespace();
            case 'entities':
                return new DoctrineNamespace($this->entityManager->getConfiguration()->getEntityNamespaces());
            case 'bundles':
                return new BundleNamespace($this->bundles);
        }
    }

    /**
     * @param array $config
     * @return ClassFilterInterface|null
     */
    protected function buildFilter(array $config)
    {
        $filters = array_merge(
            isset($config['include']) ? $this->buildSubFilters($config['include'], false) : [],
            isset($config['exclude']) ? $this->buildSubFilters($config['exclude'], true) : []
        );

        switch(count($filters)) {
            case 0:
                return null;
            case 1:
                return $filters[0];
            default:
                return new AllFilter($filters);
        }
    }

    /**
     * @param array $config
     * @param boolean $notFound
     * @return ClassFilterInterface|null
     */
    protected function buildSubFilters(array $config, $notFound)
    {
        $filters = [];

        if (!empty($config['directories'])) {
            $paths = $this->parseDirectories($config['directories']);
            $filters[] = new DirectoryFilter($paths, $notFound);
        }

        if (!empty($config['namespaces'])) {
            $namespaces = $this->parseNamespaces($config['namespaces']);
            $filters[] = new NamespaceFilter($namespaces, $notFound);
        }

        return $filters;
    }

    /**
     * @param array $paths
     * @return array
     */
    protected function parseDirectories(array $paths)
    {
        $actualPaths = [];
        foreach($paths as $path) {
            if (preg_match('/^@(\w+)(.*)$/', $path, $groups)) {
                $bundle = $this->kernel->getBundle($groups[1]);
                $path = $bundle->getPath() . $groups[2];
            }
            $actualPaths[] = realpath($path);
        }
        return $actualPaths;
    }

    /**
     * @param array $paths
     * @return array
     */
    protected function parseNamespaces(array $namespaces)
    {
        $actualNamespaces = [];
        foreach($namespaces as $namespace) {
            if (preg_match('/^@(\w+)(.*)$/', $namespace, $groups)) {
                $bundle = $this->kernel->getBundle($groups[1]);
                $namespace = $bundle->getNamespace() . $groups[2];
            }
            $actualNamespaces[] = $namespace;
        }
        return $actualNamespaces;
    }

}