<?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 TheliaSmarty\Template\Plugins;

use Propel\Runtime\Util\PropelModelPager;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Thelia\Core\HttpFoundation\Request;
use Thelia\Core\Security\SecurityContext;
use Thelia\Core\Template\Element\BaseLoop;
use Thelia\Core\Template\Element\Exception\ElementNotFoundException;
use Thelia\Core\Template\Element\Exception\InvalidElementException;
use Thelia\Core\Template\Element\LoopResult;
use Thelia\Core\Translation\Translator;
use Thelia\Log\Tlog;
use TheliaSmarty\Template\AbstractSmartyPlugin;
use TheliaSmarty\Template\SmartyPluginDescriptor;

class TheliaLoop extends AbstractSmartyPlugin
{
    /** @var PropelModelPager[] */
    protected static $pagination = null;

    protected $loopDefinition = array();

    /**
     * @var Request
     * @deprecated since 2.3, please use requestStack
     */
    protected $request;

    /** @var EventDispatcherInterface */
    protected $dispatcher;

    /** @var SecurityContext */
    protected $securityContext;

    /** @var Translator */
    protected $translator;

    /** @var ContainerInterface Service Container */
    protected $container = null;

    /** @var LoopResult[]  */
    protected $loopstack = array();

    protected $varstack = array();

    /** @var bool */
    protected $isDebugActive;

    /**
     * TheliaLoop constructor.
     * @param ContainerInterface $container
     */
    public function __construct(ContainerInterface $container)
    {
        $this->container = $container;
        $this->request = $container->get('request_stack')->getCurrentRequest();
        $this->dispatcher = $container->get('event_dispatcher');
        $this->securityContext = $container->get('thelia.securityContext');
        $this->translator = $container->get("thelia.translator");
        $this->isDebugActive = $container->getParameter("kernel.debug");
    }

    /**
     * @param  string                    $loopName
     * @return PropelModelPager
     * @throws \InvalidArgumentException if no pagination was found for loop
     */
    public static function getPagination($loopName)
    {
        if (array_key_exists($loopName, self::$pagination)) {
            return self::$pagination[$loopName];
        } else {
            throw new \InvalidArgumentException(
                Translator::getInstance()->trans("No pagination currently defined for loop name '%name'", ['%name' => $loopName ])
            );
        }
    }

    /**
     * Process the count function: executes a loop and return the number of items found
     *
     * @param array                     $params   parameters array
     * @param \Smarty_Internal_Template $template
     *
     * @return int                       the item count
     * @throws \InvalidArgumentException if a parameter is missing
     *
     */
    public function theliaCount($params, /** @noinspection PhpUnusedParameterInspection */ $template)
    {
        $type = $this->getParam($params, 'type');

        if (null == $type) {
            throw new \InvalidArgumentException(
                $this->translator->trans("Missing 'type' parameter in {count} loop arguments")
            );
        }

        try {
            $loop = $this->createLoopInstance($params);

            return $loop->count();
        } catch (ElementNotFoundException $ex) {
            // If loop is not found, when in development mode, rethrow the exception to make it visible
            if ($this->isDebugActive) {
                throw $ex;
            }

            // Otherwise, log the problem and return a count of 0
            Tlog::getInstance()->error($ex->getMessage());

            return 0;
        }
    }

    /**
     * Process {loop name="loop name" type="loop type" ... } ... {/loop} block
     *
     * @param array                     $params
     * @param string                    $content
     * @param \Smarty_Internal_Template $template
     * @param boolean                   $repeat
     *
     * @throws \InvalidArgumentException
     *
     * @return void|string
     */
    public function theliaLoop($params, $content, $template, &$repeat)
    {
        $name = $this->getParam($params, 'name');

        if (null == $name) {
            throw new \InvalidArgumentException(
                $this->translator->trans("Missing 'name' parameter in loop arguments")
            );
        }

        $type = $this->getParam($params, 'type');

        if (null == $type) {
            throw new \InvalidArgumentException(
                $this->translator->trans("Missing 'type' parameter in loop arguments")
            );
        }

        if ($content === null) {
            // Check if a loop with the same name exists in the current scope, and abort if it's the case.
            if (array_key_exists($name, $this->varstack)) {
                throw new \InvalidArgumentException(
                    $this->translator->trans("A loop named '%name' already exists in the current scope.", ['%name' => $name])
                );
            }

            try {
                $loop = $this->createLoopInstance($params);

                self::$pagination[$name] = null;

                // We have to clone the result, as exec() returns a cached LoopResult object, which may cause side effects
                // if loops with the same argument set are nested (see https://github.com/thelia/thelia/issues/2213)
                $loopResults = clone($loop->exec(self::$pagination[$name]));

                $loopResults->rewind();

            } catch (ElementNotFoundException $ex) {
                // If loop is not found, when in development mode, rethrow the exception to make it visible
                if ($this->isDebugActive) {
                    throw $ex;
                }

                // Otherwise, log the problem and simulate an empty result.
                Tlog::getInstance()->error($ex->getMessage());

                // Provide an empty result
                $loopResults = new LoopResult(null);
            }

            $this->loopstack[$name] = $loopResults;

            // No results ? The loop is terminated, do not evaluate loop text.
            if ($loopResults->isEmpty()) {
                $repeat = false;
            }
        } else {
            $loopResults = $this->loopstack[$name];

            $loopResults->next();
        }

        if ($loopResults->valid()) {
            $loopResultRow = $loopResults->current();

            // On first iteration, save variables that may be overwritten by this loop
            if (! isset($this->varstack[$name])) {
                $saved_vars = array();

                $varlist = $loopResultRow->getVars();

                foreach ($varlist as $var) {
                    $saved_vars[$var] = $template->getTemplateVars($var);
                }

                $this->varstack[$name] = $saved_vars;
            }

            foreach ($loopResultRow->getVarVal() as $var => $val) {
                $template->assign($var, $val);
            }

            $repeat = true;
        }

        // Loop is terminated. Cleanup.
        if (! $repeat) {
            // Restore previous variables values before terminating
            if (isset($this->varstack[$name])) {
                foreach ($this->varstack[$name] as $var => $value) {
                    $template->assign($var, $value);
                }

                unset($this->varstack[$name]);
            }
        }

        if ($content !== null) {
            if ($loopResults->isEmpty()) {
                $content = "";
            }

            return $content;
        }

        return '';
    }

    /**
     * Process {elseloop rel="loopname"} ... {/elseloop} block
     *
     * @param  array                     $params   loop parameters
     * @param  string                    $content  loop text content
     * @param  \Smarty_Internal_Template $template the Smarty object
     * @param  boolean                   $repeat   repeat indicator (see Smarty doc.)
     * @return string                    the loop output
     */
    public function theliaElseloop($params, $content, /** @noinspection PhpUnusedParameterInspection */ $template, &$repeat)
    {
        //Block the smarty interpretation in the elseloop
        if ($content === null) {
            if (! $this->checkEmptyLoop($params)) {
                $repeat = false;

                return '';
            }
        }

        return $content;
    }

    /**
     * Process {ifloop rel="loopname"} ... {/ifloop} block
     *
     * @param  array                     $params   loop parameters
     * @param  string                    $content  loop text content
     * @param  \Smarty_Internal_Template $template the Smarty object
     * @param  boolean                   $repeat   repeat indicator (see Smarty doc.)
     * @return string                    the loop output
     */
    public function theliaIfLoop($params, $content, /** @noinspection PhpUnusedParameterInspection */ $template, &$repeat)
    {
        // When encountering close tag, check if loop has results.
        if ($repeat === false) {
            return $this->checkEmptyLoop($params) ? '' : $content;
        }

        return '';
    }

    /**
     * Process {pageloop rel="loopname"} ... {/pageloop} block
     *
     * @param  array                     $params   loop parameters
     * @param  string                    $content  loop text content
     * @param  \Smarty_Internal_Template $template the Smarty object
     * @param  boolean                   $repeat   repeat indicator (see Smarty doc.)
     * @return string                    the loop output
     * @throws \InvalidArgumentException
     */
    public function theliaPageLoop($params, $content, $template, &$repeat)
    {
        $loopName = $this->getParam($params, 'rel');

        if (null == $loopName) {
            throw new \InvalidArgumentException($this->translator->trans("Missing 'rel' parameter in page loop"));
        }

        // Find pagination
        $pagination = self::getPagination($loopName);

        if ($pagination === null || $pagination->getNbResults() == 0) {
            // No need to paginate
            return '';
        }

        $startPage          = intval($this->getParam($params, 'start-page', 1));
        $displayedPageCount = intval($this->getParam($params, 'limit', 10));

        if (intval($displayedPageCount) == 0) {
            $displayedPageCount = PHP_INT_MAX;
        }

        $totalPageCount = $pagination->getLastPage();

        if ($content === null) {
            // The current page
            $currentPage = $pagination->getPage();

            // Get the start page.
            if ($totalPageCount > $displayedPageCount) {
                $startPage = $currentPage - round($displayedPageCount / 2);

                if ($startPage <= 0) {
                    $startPage = 1;
                }
            }

            // This is the iterative page number, the one we're going to increment in this loop
            $iterationPage = $startPage;

            // The last displayed page number
            $endPage = $startPage + $displayedPageCount - 1;

            if ($endPage > $totalPageCount) {
                $endPage = $totalPageCount;
            }

            // The first displayed page number
            $template->assign('START', $startPage);
            // The previous page number
            $template->assign('PREV', $currentPage > 1 ? $currentPage-1 : $currentPage);
            // The next page number
            $template->assign('NEXT', $currentPage < $totalPageCount ? $currentPage+1 : $totalPageCount);
            // The last displayed page number
            $template->assign('END', $endPage);
            // The overall last page
            $template->assign('LAST', $totalPageCount);
        } else {
            $iterationPage = $template->getTemplateVars('PAGE');

            $iterationPage++;
        }

        if ($iterationPage <= $template->getTemplateVars('END')) {
            // The iterative page number
            $template->assign('PAGE', $iterationPage);

            // The overall current page number
            $template->assign('CURRENT', $pagination->getPage());

            $repeat = true;
        }

        if ($content !== null) {
            return $content;
        }

        return '';
    }

    /**
     * Check if a loop has returned results. The loop shoud have been executed before, or an
     * InvalidArgumentException is thrown
     *
     * @param array $params
     *
     * @return boolean                   true if the loop is empty
     * @throws \InvalidArgumentException
     */
    protected function checkEmptyLoop($params)
    {
        $loopName = $this->getParam($params, 'rel');

        if (null == $loopName) {
            throw new \InvalidArgumentException(
                $this->translator->trans("Missing 'rel' parameter in ifloop/elseloop arguments")
            );
        }

        if (! isset($this->loopstack[$loopName])) {
            throw new \InvalidArgumentException(
                $this->translator->trans("Related loop name '%name'' is not defined.", ['%name' => $loopName])
            );
        }

        return $this->loopstack[$loopName]->isEmpty();
    }

    /**
     * @param $smartyParams
     *
     * @return BaseLoop
     * @throws \Thelia\Core\Template\Element\Exception\InvalidElementException
     * @throws \Thelia\Core\Template\Element\Exception\ElementNotFoundException
     * @throws \ReflectionException
     */
    protected function createLoopInstance($smartyParams)
    {
        $type = strtolower($smartyParams['type']);

        if (! isset($this->loopDefinition[$type])) {
            throw new ElementNotFoundException(
                $this->translator->trans("Loop type '%type' is not defined.", ['%type' => $type])
            );
        }

        $class = new \ReflectionClass($this->loopDefinition[$type]);

        if ($class->isSubclassOf("Thelia\Core\Template\Element\BaseLoop") === false) {
            throw new InvalidElementException(
                $this->translator->trans("'%type' loop class should extends Thelia\Core\Template\Element\BaseLoop", ['%type' => $type])
            );
        }

        /** @var BaseLoop $loop */
        $loop = $class->newInstance(
            $this->container
        );

        $loop->initializeArgs($smartyParams);

        return $loop;
    }

    /**
     *
     * Injects an associative array containing information for loop execution
     *
     * key is loop name
     * value is the class implementing/extending base loop classes
     *
     * ex :
     *
     * $loop = array(
     *  "product" => "Thelia\Loop\Product",
     *  "category" => "Thelia\Loop\Category",
     *  "myLoop" => "My\Own\Loop"
     * );
     *
     * @param  array                     $loopDefinition
     * @throws \InvalidArgumentException if loop name already exists
     */
    public function setLoopList(array $loopDefinition)
    {
        foreach ($loopDefinition as $name => $className) {
            if (array_key_exists($name, $this->loopDefinition)) {
                throw new \InvalidArgumentException(
                    $this->translator->trans("The loop name '%name' is already defined in %className class", [
                            '%name' => $name,
                            '%className' => $className
                        ])
                );
            }

            $this->loopDefinition[$name] = $className;
        }
    }

    /**
     * Defines the various smarty plugins hendled by this class
     *
     * @return \TheliaSmarty\Template\SmartyPluginDescriptor[] smarty plugin descriptors
     */
    public function getPluginDescriptors()
    {
        return array(

            new SmartyPluginDescriptor('function', 'count', $this, 'theliaCount'),
            new SmartyPluginDescriptor('block', 'loop', $this, 'theliaLoop'),
            new SmartyPluginDescriptor('block', 'elseloop', $this, 'theliaElseloop'),
            new SmartyPluginDescriptor('block', 'ifloop', $this, 'theliaIfLoop'),
            new SmartyPluginDescriptor('block', 'pageloop', $this, 'theliaPageLoop'),
        );
    }
}
