<?php

/**
 * This file is part of the Nette Framework (http://nette.org)
 * Copyright (c) 2004 David Grudl (http://davidgrudl.com)
 */

namespace Nette\Bridges\Framework;

use Nette,
	Nette\DI\ContainerBuilder,
	Nette\Utils\Validators,
	Latte;


/**
 * Core Nette Framework services.
 *
 * @author     David Grudl
 */
class NetteExtension extends Nette\DI\CompilerExtension
{
	public $defaults = array(
		'http' => array(
			'proxy' => array(),
			'headers' => array(
				'X-Powered-By' => 'Nette Framework',
				'Content-Type' => 'text/html; charset=utf-8',
			),
		),
		'session' => array(
			'debugger' => FALSE,
			'autoStart' => 'smart', // true|false|smart
			'expiration' => NULL,
		),
		'application' => array(
			'debugger' => TRUE,
			'errorPresenter' => 'Nette:Error',
			'catchExceptions' => '%productionMode%',
			'mapping' => NULL
		),
		'routing' => array(
			'debugger' => TRUE,
			'routes' => array(), // of [mask => action]
		),
		'security' => array(
			'debugger' => TRUE,
			'frames' => 'SAMEORIGIN', // X-Frame-Options
			'users' => array(), // of [user => password] or [user => ['password' => password, 'roles' => [role]]]
			'roles' => array(), // of [role => parents]
			'resources' => array(), // of [resource => parents]
		),
		'mailer' => array(
			'smtp' => FALSE,
			'host' => NULL,
			'port' => NULL,
			'username' => NULL,
			'password' => NULL,
			'secure' => NULL,
			'timeout' => NULL,
		),
		'database' => array(), // BC
		'forms' => array(
			'messages' => array(),
		),
		'latte' => array(
			'xhtml' => FALSE,
			'macros' => array(),
		),
		'container' => array(
			'debugger' => FALSE,
			'accessors' => TRUE,
		),
		'debugger' => array(
			'email' => NULL,
			'editor' => NULL,
			'browser' => NULL,
			'strictMode' => NULL,
			'maxLen' => NULL,
			'maxDepth' => NULL,
			'showLocation' => NULL,
			'scream' => NULL,
			'bar' => array(), // of class name
			'blueScreen' => array(), // of callback
		),
	);


	public function loadConfiguration()
	{
		$container = $this->getContainerBuilder();
		$config = $this->getConfig($this->defaults);

		if (isset($config['xhtml'])) {
			$config['latte']['xhtml'] = $config['xhtml'];
			unset($config['xhtml']);
		}
		$container->addDefinition('nette')->setClass('Nette\Bridges\Framework\NetteAccessor', array('@container'));

		$this->validate($config, $this->defaults, 'nette');

		$this->setupCache($container);
		$this->setupHttp($container, $config['http']);
		$this->setupSession($container, $config['session']);
		$this->setupSecurity($container, $config['security']);
		$this->setupApplication($container, $config['application']);
		$this->setupRouting($container, $config['routing']);
		$this->setupMailer($container, $config['mailer']);
		$this->setupLatte($container, $config['latte']);
		$this->setupContainer($container, $config['container']);
	}


	private function setupCache(ContainerBuilder $container)
	{
		$container->addDefinition($this->prefix('cacheJournal'))
			->setClass('Nette\Caching\Storages\FileJournal', array($container->expand('%tempDir%')));

		$container->addDefinition('cacheStorage') // no namespace for back compatibility
			->setClass('Nette\Caching\Storages\FileStorage', array($container->expand('%tempDir%/cache')));

		if (class_exists('Nette\Caching\Storages\PhpFileStorage')) {
			$container->addDefinition($this->prefix('templateCacheStorage'))
				->setClass('Nette\Caching\Storages\PhpFileStorage', array($container->expand('%tempDir%/cache')))
				->addSetup('::trigger_error', array('Service templateCacheStorage is deprecated.', E_USER_DEPRECATED))
				->setAutowired(FALSE);
		}

		$container->addDefinition($this->prefix('cache'))
			->setClass('Nette\Caching\Cache', array(1 => $container::literal('$namespace')))
			->addSetup('::trigger_error', array('Service cache is deprecated.', E_USER_DEPRECATED))
			->setParameters(array('namespace' => NULL))
			->setAutowired(FALSE);
	}


	private function setupHttp(ContainerBuilder $container, array $config)
	{
		$this->validate($config, $this->defaults['http'], 'nette.http');

		$container->addDefinition($this->prefix('httpRequestFactory'))
			->setClass('Nette\Http\RequestFactory')
			->addSetup('setProxy', array($config['proxy']));

		$container->addDefinition('httpRequest') // no namespace for back compatibility
			->setClass('Nette\Http\Request')
			->setFactory('@Nette\Http\RequestFactory::createHttpRequest');

		$container->addDefinition('httpResponse') // no namespace for back compatibility
			->setClass('Nette\Http\Response');

		$container->addDefinition($this->prefix('httpContext'))
			->setClass('Nette\Http\Context');
	}


	private function setupSession(ContainerBuilder $container, array $config)
	{
		$session = $container->addDefinition('session') // no namespace for back compatibility
			->setClass('Nette\Http\Session');

		if (isset($config['expiration'])) {
			$session->addSetup('setExpiration', array($config['expiration']));
		}

		if ($container->parameters['debugMode'] && $config['debugger']) {
			$session->addSetup('Tracy\Debugger::getBar()->addPanel(?)', array(
				new Nette\DI\Statement('Nette\Bridges\HttpTracy\SessionPanel')
			));
		}

		unset($config['expiration'], $config['autoStart'], $config['debugger']);
		if (!empty($config)) {
			$session->addSetup('setOptions', array($config));
		}
	}


	private function setupSecurity(ContainerBuilder $container, array $config)
	{
		$this->validate($config, $this->defaults['security'], 'nette.security');

		$container->addDefinition($this->prefix('userStorage'))
			->setClass('Nette\Http\UserStorage');

		$user = $container->addDefinition('user') // no namespace for back compatibility
			->setClass('Nette\Security\User');

		if ($container->parameters['debugMode'] && $config['debugger']) {
			$user->addSetup('Tracy\Debugger::getBar()->addPanel(?)', array(
				new Nette\DI\Statement('Nette\Bridges\SecurityTracy\UserPanel')
			));
		}

		if ($config['users']) {
			$usersList = $usersRoles = array();
			foreach ($config['users'] as $username => $data) {
				$data = is_array($data) ? $data : array('password' => $data);
				$this->validate($data, array('password' => NULL, 'roles' => NULL), $this->prefix("security.users.$username"));
				$usersList[$username] = $data['password'];
				$usersRoles[$username] = isset($data['roles']) ? $data['roles'] : NULL;
			}

			$container->addDefinition($this->prefix('authenticator'))
				->setClass('Nette\Security\SimpleAuthenticator', array($usersList, $usersRoles));
		}

		if ($config['roles'] || $config['resources']) {
			$authorizator = $container->addDefinition($this->prefix('authorizator'))
				->setClass('Nette\Security\Permission');
			foreach ($config['roles'] as $role => $parents) {
				$authorizator->addSetup('addRole', array($role, $parents));
			}
			foreach ($config['resources'] as $resource => $parents) {
				$authorizator->addSetup('addResource', array($resource, $parents));
			}
		}
	}


	private function setupApplication(ContainerBuilder $container, array $config)
	{
		$this->validate($config, $this->defaults['application'], 'nette.application');

		$application = $container->addDefinition('application') // no namespace for back compatibility
			->setClass('Nette\Application\Application')
			->addSetup('$catchExceptions', array($config['catchExceptions']))
			->addSetup('$errorPresenter', array($config['errorPresenter']));

		if ($config['debugger']) {
			$application->addSetup('Nette\Bridges\ApplicationTracy\RoutingPanel::initializePanel');
		}

		$presenterFactory = $container->addDefinition($this->prefix('presenterFactory'))
			->setClass('Nette\Application\PresenterFactory', array(
				isset($container->parameters['appDir']) ? $container->parameters['appDir'] : NULL
			));
		if ($config['mapping']) {
			$presenterFactory->addSetup('setMapping', array($config['mapping']));
		}
	}


	private function setupRouting(ContainerBuilder $container, array $config)
	{
		$this->validate($config, $this->defaults['routing'], 'nette.routing');

		$router = $container->addDefinition('router') // no namespace for back compatibility
			->setClass('Nette\Application\Routers\RouteList');

		foreach ($config['routes'] as $mask => $action) {
			$router->addSetup('$service[] = new Nette\Application\Routers\Route(?, ?);', array($mask, $action));
		}

		if ($container->parameters['debugMode'] && $config['debugger']) {
			$container->getDefinition('application')->addSetup('Tracy\Debugger::getBar()->addPanel(?)', array(
				new Nette\DI\Statement('Nette\Bridges\ApplicationTracy\RoutingPanel')
			));
		}
	}


	private function setupMailer(ContainerBuilder $container, array $config)
	{
		$this->validate($config, $this->defaults['mailer'], 'nette.mailer');

		if (empty($config['smtp'])) {
			$container->addDefinition($this->prefix('mailer'))
				->setClass('Nette\Mail\SendmailMailer');
		} else {
			$container->addDefinition($this->prefix('mailer'))
				->setClass('Nette\Mail\SmtpMailer', array($config));
		}
	}


	private function setupLatte(ContainerBuilder $container, array $config)
	{
		$this->validate($config, $this->defaults['latte'], 'nette.latte');

		$latteFactory = $container->addDefinition($this->prefix('latteFactory'))
			->setClass('Latte\Engine')
			->addSetup('setTempDirectory', array($container->expand('%tempDir%/cache/latte')))
			->addSetup('setAutoRefresh', array($container->parameters['debugMode']))
			->addSetup('setContentType', array($config['xhtml'] ? Latte\Compiler::CONTENT_XHTML : Latte\Compiler::CONTENT_HTML))
			->setImplement('Nette\Bridges\ApplicationLatte\ILatteFactory');

		$container->addDefinition($this->prefix('templateFactory'))
			->setClass('Nette\Bridges\ApplicationLatte\TemplateFactory');

		$latte = $container->addDefinition($this->prefix('latte')) // deprecated
			->setClass('Latte\Engine')
			->addSetup('setTempDirectory', array($container->expand('%tempDir%/cache/latte')))
			->addSetup('setAutoRefresh', array($container->parameters['debugMode']))
			->addSetup('setContentType', array($config['xhtml'] ? Latte\Compiler::CONTENT_XHTML : Latte\Compiler::CONTENT_HTML))
			->setAutowired(FALSE);

		foreach ($config['macros'] as $macro) {
			if (strpos($macro, '::') === FALSE && class_exists($macro)) {
				$macro .= '::install';
			} else {
				Validators::assert($macro, 'callable');
			}
			$latte->addSetup('?->onCompile[] = function($engine) { ' . $macro . '($engine->getCompiler()); }', array('@self'));
			$latteFactory->addSetup('?->onCompile[] = function($engine) { ' . $macro . '($engine->getCompiler()); }', array('@self'));
		}

		if (class_exists('Nette\Templating\FileTemplate')) {
			$container->addDefinition($this->prefix('template')) // deprecated
				->setClass('Nette\Templating\FileTemplate')
				->addSetup('registerFilter', array(new Nette\DI\Statement(array($latteFactory, 'create'))))
				->addSetup('registerHelperLoader', array('Nette\Templating\Helpers::loader'))
				->setAutowired(FALSE);
		}
	}


	private function setupContainer(ContainerBuilder $container, array $config)
	{
		$this->validate($config, $this->defaults['container'], 'nette.container');

		if ($config['accessors']) {
			$container->parameters['container']['accessors'] = TRUE;
		}
	}


	public function afterCompile(Nette\PhpGenerator\ClassType $class)
	{
		$initialize = $class->methods['initialize'];
		$container = $this->getContainerBuilder();
		$config = $this->getConfig($this->defaults);

		// debugger
		foreach (array('email', 'editor', 'browser', 'strictMode', 'maxLen', 'maxDepth', 'showLocation', 'scream') as $key) {
			if (isset($config['debugger'][$key])) {
				$initialize->addBody('Tracy\Debugger::$? = ?;', array($key, $config['debugger'][$key]));
			}
		}

		if ($container->parameters['debugMode']) {
			if ($config['container']['debugger']) {
				$config['debugger']['bar'][] = 'Nette\Bridges\DITracy\ContainerPanel';
			}

			foreach ((array) $config['debugger']['bar'] as $item) {
				$initialize->addBody($container->formatPhp(
					'Tracy\Debugger::getBar()->addPanel(?);',
					Nette\DI\Compiler::filterArguments(array(is_string($item) ? new Nette\DI\Statement($item) : $item))
				));
			}
		}

		foreach ((array) $config['debugger']['blueScreen'] as $item) {
			$initialize->addBody($container->formatPhp(
					'Tracy\Debugger::getBlueScreen()->addPanel(?);',
				Nette\DI\Compiler::filterArguments(array($item))
			));
		}

		if (!empty($container->parameters['tempDir'])) {
			$initialize->addBody('Nette\Caching\Storages\FileStorage::$useDirectories = ?;', array($this->checkTempDir($container->expand('%tempDir%/cache'))));
		}

		foreach ((array) $config['forms']['messages'] as $name => $text) {
			$initialize->addBody('Nette\Forms\Rules::$defaultMessages[Nette\Forms\Form::?] = ?;', array($name, $text));
		}

		if ($config['session']['autoStart'] === 'smart') {
			$initialize->addBody('$this->getByType("Nette\Http\Session")->exists() && $this->getByType("Nette\Http\Session")->start();');
		} elseif ($config['session']['autoStart']) {
			$initialize->addBody('$this->getByType("Nette\Http\Session")->start();');
		}

		if ($config['latte']['xhtml']) {
			$initialize->addBody('Nette\Utils\Html::$xhtml = ?;', array(TRUE));
		}

		if (isset($config['security']['frames']) && $config['security']['frames'] !== TRUE) {
			$frames = $config['security']['frames'];
			if ($frames === FALSE) {
				$frames = 'DENY';
			} elseif (preg_match('#^https?:#', $frames)) {
				$frames = "ALLOW-FROM $frames";
			}
			$initialize->addBody('header(?);', array("X-Frame-Options: $frames"));
		}

		foreach ($container->findByTag('run') as $name => $on) {
			if ($on) {
				$initialize->addBody('$this->getService(?);', array($name));
			}
		}

		if (!empty($config['container']['accessors'])) {
			$definitions = $container->definitions;
			ksort($definitions);
			foreach ($definitions as $name => $def) {
				if (Nette\PhpGenerator\Helpers::isIdentifier($name)) {
					$type = $def->implement ?: $def->class;
					$class->addDocument("@property $type \$$name");
				}
			}
		}

		foreach ($config['http']['headers'] as $key => $value) {
			if ($value != NULL) { // intentionally ==
				$initialize->addBody('header(?);', array("$key: $value"));
			}
		}

		$initialize->addBody('Nette\Utils\SafeStream::register();');
		$initialize->addBody('Nette\Reflection\AnnotationsParser::setCacheStorage($this->getByType("Nette\Caching\IStorage"));');
		$initialize->addBody('Nette\Reflection\AnnotationsParser::$autoRefresh = ?;', array($container->parameters['debugMode']));
	}


	private function checkTempDir($dir)
	{
		// checks whether directory is writable
		$uniq = uniqid('_', TRUE);
		if (!@mkdir("$dir/$uniq")) { // @ - is escalated to exception
			throw new Nette\InvalidStateException("Unable to write to directory '$dir'. Make this directory writable.");
		}

		// checks whether subdirectory is writable
		$isWritable = @file_put_contents("$dir/$uniq/_", '') !== FALSE; // @ - error is expected
		if ($isWritable) {
			unlink("$dir/$uniq/_");
		}
		rmdir("$dir/$uniq");
		return $isWritable;
	}


	private function validate(array $config, array $expected, $name)
	{
		if ($extra = array_diff_key($config, $expected)) {
			$extra = implode(", $name.", array_keys($extra));
			throw new Nette\InvalidStateException("Unknown option $name.$extra.");
		}
	}

}
