<?php
/**
 * @link http://www.yiiframework.com/
 * @copyright Copyright (c) 2008 Yii Software LLC
 * @license http://www.yiiframework.com/license/
 */

namespace yii\twig;

use yii\base\InvalidCallException;
use yii\helpers\Inflector;
use yii\helpers\StringHelper;
use yii\helpers\Url;

/**
 * Extension provides Yii-specific syntax for Twig templates.
 *
 * @author Andrey Grachov <andrey.grachov@gmail.com>
 * @author Alexander Makarov <sam@rmcreative.ru>
 */
class Extension extends \Twig_Extension
{
    /**
     * @var array used namespaces
     */
    protected $namespaces = [];
    /**
     * @var array used class aliases
     */
    protected $aliases = [];
    /**
     * @var array used widgets
     */
    protected $widgets = [];


    /**
     * Creates new instance
     *
     * @param array $uses namespaces and classes to use in the template
     */
    public function __construct(array $uses = [])
    {
        $this->addUses($uses);
    }

    /**
     * @inheritdoc
     */
    public function getNodeVisitors()
    {
        return [
            new Optimizer(),
        ];
    }

    /**
     * @inheritdoc
     */
    public function getFunctions()
    {
        $options = [
            'is_safe' => ['html'],
        ];
        $functions = [
            new \Twig_SimpleFunction('use', [$this, 'addUses'], $options),
            new \Twig_SimpleFunction('*_begin', [$this, 'beginWidget'], $options),
            new \Twig_SimpleFunction('*_end', [$this, 'endWidget'], $options),
            new \Twig_SimpleFunction('widget_end', [$this, 'endWidget'], $options),
            new \Twig_SimpleFunction('*_widget', [$this, 'widget'], $options),
            new \Twig_SimpleFunction('path', [$this, 'path']),
            new \Twig_SimpleFunction('url', [$this, 'url']),
            new \Twig_SimpleFunction('void', function(){}),
            new \Twig_SimpleFunction('set', [$this, 'setProperty']),
        ];

        $options = array_merge($options, [
            'needs_context' => true,
        ]);
        $functions[] = new \Twig_SimpleFunction('register_*', [$this, 'registerAsset'], $options);
        foreach (['begin_page', 'end_page', 'begin_body', 'end_body', 'head'] as $helper) {
            $functions[] = new \Twig_SimpleFunction($helper, [$this, 'viewHelper'], $options);
        }
        return $functions;
    }

    public function registerAsset($context, $asset)
    {
        return $this->resolveAndCall($asset, 'register', [
            isset($context['this']) ? $context['this'] : null,
        ]);
    }

    public function beginWidget($widget, $config = [])
    {
        $widget = $this->resolveClassName($widget);
        $this->widgets[] = $widget;
        return $this->call($widget, 'begin', [
            $config,
        ]);
    }

    public function endWidget($widget = null)
    {
        if ($widget === null) {
            if (empty($this->widgets)) {
                throw new InvalidCallException('Unexpected end_widget() call. A matching begin_widget() is not found.');
            }
            $this->call(array_pop($this->widgets), 'end');
        } else {
            array_pop($this->widgets);
            $this->resolveAndCall($widget, 'end');
        }
    }

    public function widget($widget, $config = [])
    {
        return $this->resolveAndCall($widget, 'widget', [
            $config,
        ]);
    }

    public function viewHelper($context, $name = null)
    {
        if ($name !== null && isset($context['this'])) {
            $this->call($context['this'], Inflector::variablize($name));
        }
    }

    public function resolveAndCall($className, $method, $arguments = null)
    {
        return $this->call($this->resolveClassName($className), $method, $arguments);
    }

    public function call($className, $method, $arguments = null)
    {
        $callable = [$className, $method];
        if ($arguments === null) {
            return call_user_func($callable);
        } else {
            return call_user_func_array($callable, $arguments);
        }
    }

    public function resolveClassName($className)
    {
        $className = Inflector::id2camel($className, '_');
        if (isset($this->aliases[$className])) {
            return $this->aliases[$className];
        }
        foreach ($this->namespaces as $namespace) {
            $resolvedClassName = $namespace . '\\' . $className;
            if (class_exists($resolvedClassName)) {
                return $this->aliases[$className] = $resolvedClassName;
            }
        }
        return $className;
    }

    public function addUses($args)
    {
        foreach ((array) $args as $key => $value) {
            $value = str_replace('/', '\\', $value);
            if (is_int($key)) {
                // namespace or class import
                if (class_exists($value)) {
                    // class import
                    $this->aliases[StringHelper::basename($value)] = $value;
                } else {
                    // namespace
                    $this->namespaces[] = $value;
                }
            } else {
                // aliased class import
                $this->aliases[$key] = $value;
            }
        }
    }

    /**
     * Generates relative URL
     *
     * @param string $path the parameter to be used to generate a valid URL
     * @param array $args arguments
     * @return string the generated relative URL
     */
    public function path($path, $args = [])
    {
        if ($args !== []) {
            $path = array_merge([$path], $args);
        }
        return Url::to($path);
    }

    /**
     * Generates absolute URL
     *
     * @param string $path the parameter to be used to generate a valid URL
     * @param array $args arguments
     * @return string the generated absolute URL
     */
    public function url($path, $args = [])
    {
        if ($args !== []) {
            $path = array_merge([$path], $args);
        }
        return Url::to($path, true);
    }

    public function setProperty($object, $property, $value)
    {
        $object->$property = $value;
    }

    /**
     * @inheritdoc
     */
    public function getName()
    {
        return 'yii2-twig';
    }
}