<?php
/**
 * core.framework
 *
 * @category   Core
 * @package    Core_Application
 * @copyright  Copyright (c) 2011. Burza d.o.o. (http://web.burza.hr/en/)
 * @license    proprietary
 */

// include compatibilty layer
require_once dirname(__FILE__) . DIRECTORY_SEPARATOR . 'Compat.php';

/**
 * @uses      Core_Config
 * @uses      Core_Event_Dispatcher
 * @uses      Core_Loader
 * @uses      Core_Loader_PluginLoader
 * @uses      Yadif_Container
 * @category  Core
 * @package   Core_Application
 * @copyright Copyright (c) 2011. Burza d.o.o. (http://web.burza.hr/en/)
 * @license   proprietary
 */
class Core_Application
{
    const EVENT_STARTUP   = 'application.startup';
    const EVENT_SHUTDOWN  = 'application.shutdown';
    const EVENT_EXCEPTION = 'application.exception';

    const RUNTIME_WEB     = 'web';
    const RUNTIME_CLI     = 'cli';

    /**
     *  @var Core_Application
     */
    static protected $_instance;

    /**
     * Holds application configuration (or the path to the configuration)
     *
     * @var Zend_Config
     */
    protected $_config;

    /**
     * Application runtime (web, cli, gui)
     *
     * @var string
     */
    protected $_runtime;

    /**
     * Application context (web, cli, gui)
     *
     * @var string
     */
    protected $_context;

    /**
     * Application language
     *
     * @var string
     */
    protected $_language;

    /**
     * Application environment
     *
     * @var string
     */
    protected $_environment;

    /**
     * Application root
     *
     * @var string
     */
    protected $_root;

    /**
     * Application document root
     *
     * @var string
     */
    protected $_documentRoot;

    /**
     * @var Core_Event_Dispatcher
     */
    protected $_eventDispatcher;

    /**
     * @var Core_Yadif_Container_Interface
     */
    protected $_container;

    /**
     * @var Core_Loader
     */
    protected $_loader;

    /**
     * @var Core_Loader_PluginLoader
     */
    protected $_pluginLoader;

    /**
     * @var array(Core_Application_Plugin_Interface)
     */
    protected $_plugins = array();

    /**
     * @return Core_Application
     */
    static public function getInstance(Core_Application $instance = null)
    {
        if (null === self::$_instance) {
            if (null === $instance) {
                $instance = new self;
            }
            self::$_instance = $instance;
            // self::$_instance->getLoader()->registerBasicAutoloader();
        }

        return self::$_instance;
    }

    /**
     * Reset the singleton instance.
     *
     * @return void
     */
    static public function resetInstance()
    {
        self::$_instance = null;
    }
    
    /**
     * Proxy to {Core_Yadif_Container_Interface}
     * 
     * @param string $component Component name to test
     * 
     * @return boolean
     */
    public static function has($component)
    {
        $container = self::getInstance()->getContainer();
        return $container->hasComponent($component);
    }

    /**
     * Proxy to {Core_Yadif_Container_Interface}
     *
     * @param string $component Component name to fetch
     * @param array  $params    Component parameters
     *
     * @return object
     */
    public static function get($component, $params = null)
    {
        $container = self::getInstance()->getContainer();
        if (is_array($params)) {
            foreach ($params as $param => $value) {
                $container->bindParam(':'. $param, $value);
            }
        }
        return $container->getComponent($component);
    }

    /**
     * @return Core_Application_Component
     */
    public static function getComponents()
    {
        return Core_Application_Component::getInstance();
    }

    /**
     * @param array $options
     *
     * @return Core_Application
     */
    public function setOptions(array $options)
    {
        return $this->setConfig(new Zend_Config($options));
    }

    /**
     * @param Zend_Config $config
     *
     * @return Core_Application
     * @throws InvalidArgumentException
     */
    public function setConfig($config)
    {
        $config = $this->_normalizeConfig($config, true, false);

        if (isset($config->php)) {
            $this->_configureIniConfig($config->php);
        }

        if (isset($config->classmap)) {
            $this->_configureLoaderClassMap($config->classmap, $config);
        }

        if (isset($config->container)) {
            $this->_configureContainer($config->container, $config);
        }

        if (isset($config->plugins)) {
            $this->addPlugins($config->plugins->toArray());
        }

        $forbidden = array(
            'Options',
            'Config',
            'Container',
            'Loader',
            'PluginLoader',
            'Environment',
            'Classmap',
            'Plugins',
            'App',
            'Php',
        );
        foreach ($config as $key => $value) {
            $normalized = ucfirst($key);
            if (in_array($normalized, $forbidden)) {
                continue;
            }

            $method   = 'set' . $normalized;
            $callable = array($this, $method);
            // need to use both method_exists() and is_callable()
            // because we have __call()
            if (method_exists($this, $method) && is_callable($callable)) {
                call_user_func($callable, $value);
            } else {
                throw new InvalidArgumentException(sprintf('Invalid config "%s" passed', $key));
            }
        }
        $this->_config = $config;

        return $this;
    }

    /**
     * @return Zend_Config
     * @throws RuntimeException
     */
    public function getConfig()
    {
        if (null === $this->_config) {
            throw new RuntimeException('Failed fetching application config, no config was given');
        }
        return $this->_config;
    }

    /**
     * @return string
     */
    public function getRuntime()
    {
        if (0 === strcasecmp(self::RUNTIME_CLI, php_sapi_name())) {
            $runtime = self::RUNTIME_CLI;
        } else {
            $runtime = self::RUNTIME_WEB;
        }
        return $runtime;
    }

    /**
     * @param string $context
     *
     * @return \Core_Application
     */
    public function setContext($context)
    {
        $this->_context = $context;
        return $this;
    }

    /**
     * @return string
     */
    public function getContext()
    {
        if (null === $this->_context) {
            return $this->getRuntime();
        }
        return $this->_context;
    }

    /**
     * @param string $root
     *
     * @return \Core_Application
     */
    public function setRoot($root)
    {
        $this->_root = $root;
        return $this;
    }

    /**
     * @return string
     *
     * @throws RuntimeException
     */
    public function getRoot()
    {
        if (null === $this->_root) {
            if (!defined('APPLICATION_ROOT')) {
                throw new RuntimeException('Fetching application root failed: no APPLICATION_ROOT constant set');
            }
            $this->_root = APPLICATION_ROOT;
        }
        return $this->_root;
    }

    /**
     * @param string $documentRoot
     *
     * @return \Core_Application
     */
    public function setDocumentRoot($documentRoot)
    {
        $this->_documentRoot = $documentRoot;
        return $this;
    }

    /**
     * @return string
     * @throws RuntimeException
     */
    public function getDocumentRoot()
    {
        if (null === $this->_documentRoot) {
            throw new RuntimeException('Fetching document root failed, no document root set');
        }
        return $this->_documentRoot;
    }

    /**
     * @param string $environment
     *
     * @return \Core_Application
     */
    public function setEnvironment($environment)
    {
        $this->_environment = $environment;
        return $this;
    }

    /**
     * Will:
     * - read property
     * - read constant APPLICATION_ENV
     * - read environment variable APPLICATION_ENV
     * - throw an RuntimeException
     *
     * @return string
     * @throws RuntimeException
     */
    public function getEnvironment()
    {
        if (null === $this->_environment) {
            if (!defined('APPLICATION_ENV')) {
                if (false === ($env = getenv('APPLICATION_ENV'))) {
                    // set this to production if not in CLI mode, it should be pretty safe
                    throw new RuntimeException(
                        'Error fetching application environment, set the APPLICATION_ENV environment variable'
                    );
                }
                define('APPLICATION_ENV', $env);
            }
            $this->_environment = APPLICATION_ENV;
        }
        return $this->_environment;
    }

    /**
     * Get language
     *
     * @return string
     * @throws RuntimeException
     */
    public function getLanguage()
    {
        if (null === $this->_language) {
            switch (true) {
                case isset($this->getConfig()->app->default_language):
                    $this->_language = $this->getConfig()->app->default_language;

                    break;

                case true:
                    $locale             = setlocale(LC_MESSAGES, 0);
                    list($locale)       = explode('.', $locale);
                    if (array_key_exists($locale, $this->getConfig()->app->languages->toArray())) {
                        $this->_language = $locale;
                        break;
                    }

                    // no break

                default:
                    throw new RuntimeException('Error fetching application language, none is set');
            }
        }

        return $this->_language;
    }

    /**
     * Set application language
     *
     * @param string $language
     *
     * @return \Core_Application
     */
    public function setLanguage($language)
    {
        $this->_language = $language;

        return $this;
    }

    /**
     * @param Core_Loader $loader
     *
     * @return \Core_Application
     */
    public function setLoader(Core_Loader $loader)
    {
        $this->_loader = $loader;
        return $this;
    }

    /**
     * @return \Core_Loader
     */
    public function getLoader()
    {
        if (null === $this->_loader) {
            include_once dirname(__FILE__) . DIRECTORY_SEPARATOR . 'Loader.php';
            $this->_loader = new Core_Loader;
        }
        return $this->_loader;
    }

    /**
     * @param Core_Yadif_Container_Interface $container
     *
     * @return \Core_Application
     */
    public function setContainer(Core_Yadif_Container_Interface $container)
    {
        $this->_container = $container;
        return $this;
    }

    /**
     * @return Core_Yadif_Container_Interface
     */
    public function getContainer()
    {
        if (null === $this->_container) {
            $this->_container = new Core_Yadif_Container;
        }
        return $this->_container;
    }

    /**
     * @param Core_Loader_PluginLoader $pluginLoader
     *
     * @return \Core_Application
     */
    public function setPluginLoader(Core_Loader_PluginLoader $pluginLoader)
    {
        $this->_pluginLoader = $pluginLoader;
        return $this;
    }

    /**
     * @return Core_Loader_PluginLoader
     */
    public function getPluginLoader()
    {
        if (null === $this->_pluginLoader) {
            $this->_pluginLoader = Core_Application::get('PluginLoader');
        }
        return $this->_pluginLoader;
    }

    /**
     * @param Core_Event_Dispatcher_Interface $eventDispatcher
     *
     * @return \Core_Application
     */
    public function setEventDispatcher(Core_Event_Dispatcher_Interface $eventDispatcher)
    {
        $this->_eventDispatcher = $eventDispatcher;
        return $this;
    }

    /**
     * @return Core_Event_Dispatcher_Interface
     */
    public function getEventDispatcher()
    {
        if (null === $this->_eventDispatcher) {
            $this->_eventDispatcher = self::get('EventDispatcher');
        }
        return $this->_eventDispatcher;
    }

    /**
     * @param Core_Application_Plugin_Interface $plugin
     *
     * @return \Core_Application
     */
    public function addPlugin(Core_Application_Plugin_Interface $plugin)
    {
        $this->_plugins[strtolower($plugin->getName())] = $plugin;
        $plugin->init();

        return $this;
    }

    /**
     * @param array $plugins
     *
     * @return \Core_Application
     * @throws InvalidArgumentException
     */
    public function addPlugins(Array $plugins)
    {
        foreach ($plugins as $name => $plugin) {
            if ($plugin instanceof Core_Application_Plugin_Interface) {
                $this->addPlugin($plugin);
            } else {
                // we wish to be able to override certain plugins with nothing
                // so skip those
                if (isset($plugin['type'])) {
                    $options = (isset($plugin['options']) ? $plugin['options'] : null);
                    $this->createPlugin($plugin['type'], $name, $options);
                } else {
                    throw new InvalidArgumentException(sprintf('No type passed for plugin "%s"', $name));
                }
            }
        }
        return $this;
    }

    /**
     *
     * @param string $type    Plugin type
     * @param string $name    Plugin name
     * @param array  $options Plugin options
     *
     * @return Core_Application_Plugin_Interface
     */
    public function createPlugin($type, $name, $options = array())
    {
        // first try to pull plugin from DIC
        $container  = $this->getContainer();
        $pluginType = 'app'. $type;
        if ($container->hasComponent($pluginType)) {
            $plugin = $container->getComponent($pluginType);
        } else {
            $plugin = $this->getPluginLoader()->initializeApplicationPluginPlugin(ucfirst($type), $name, $options);
        }

        $this->addPlugin($plugin);
        return $plugin;
    }

    /**
     * @param string $name
     * @return bool
     */
    public function hasPlugin($name)
    {
        return array_key_exists(strtolower($name), $this->_plugins);
    }

    /**
     * @param string $name
     *
     * @return Core_Application_Plugin_Interface
     * @throws InvalidArgumentException If named plugin does not exist.
     */
    public function getPlugin($name)
    {
        if (!$this->hasPlugin($name)) {
            throw new InvalidArgumentException(sprintf('Fetching application plugin "%s" failed: no such plugin', $name));
        }
        return $this->_plugins[strtolower($name)];
    }

    /**
     * @return array
     */
    public function getPlugins()
    {
        return $this->_plugins;
    }

    /**
     * @param string $name
     *
     * @return \Core_Application
     * @throws InvalidArgumentException If named plugin does not exist.
     */
    public function removePlugin($name)
    {
        if (!$this->hasPlugin($name)) {
            throw new InvalidArgumentException(sprintf('Removing app plugin "%s" failed: no such plugin', $name));
        }
        unset($this->_plugins[strtolower($name)]);
        return $this;
    }

    /**
     * Proxy to Core_Application::get()
     *
     * @param strong $name   Method name in  get<component>() pattern
     * @param array  $params Param to proxy
     *
     * @return object
     * @throws InvalidArgumentException If method name does not match get<component>() pattern.
     */
    public function __call($name, $params)
    {
        // accessing container items!
        // getView() => getContainer()->get('view')
        // ...
        if (0 === strpos($name, 'get')) {
            $name = strtolower(substr($name, 3));
            return self::get($name, $params);
        }

        // no such method!
        throw new InvalidArgumentException(sprintf('Call to undefined method %s::%s()', get_class($this), $name));
    }

    /**
     * @param string $path
     *
     * @return Core_Response
     */
    public function run($path = null)
    {
        // startup event
        $event      = new Core_Event($this, self::EVENT_STARTUP);
        $this->getEventDispatcher()->dispatch($event);

        // shutdown event
        if ($this->getEventDispatcher()->hasEventSubscribers(self::EVENT_SHUTDOWN)) {
            $event = new Core_Event($this, self::EVENT_SHUTDOWN);
            register_shutdown_function(array($this, 'shutdown'), $event);
        }

        // router
        $router     = self::get('Router');
        if (null === $path) {
            $request = self::get('Request');
            $path    = $request->getUri();
        }
        $route      = $router->route($path);

        // dispatcher
        /**
         * @var Core_Dispatcher $dispatcher
         */
        $dispatcher = self::get('Dispatcher');
        $location   = new Core_Dispatcher_Location($route->getValues());
        $response   = $dispatcher->dispatch($location);

        return $response;
    }

    /**
     * @param Core_Event_Interface $event
     *
     * @return mixed
     */
    public function shutdown(Core_Event_Interface $event)
    {
        return $this->getEventDispatcher()->dispatch($event);
    }

    /**
     *
     * @return int system load
     */
    public function getSystemLoad()
    {
        if (empty($this->_systemLoad) && function_exists('sys_getloadavg')) {
            $this->_systemLoad = sys_getloadavg();
        }

        if (!isset($this->_systemLoad[0])) {
            return false;
        }

        return $this->_systemLoad[0];
    }

    /**
     * @return bool
     */
    public function isHighSystemLoad()
    {
        $load = $this->getSystemLoad();

        return ($load > 70);
    }

    /**
     * @param array $iniConfig
     *
     * @return \Core_Application
     */
    protected function _configureIniConfig($iniConfig)
    {
        foreach ($iniConfig as $name => $value) {
            ini_set(str_replace('\\', '.', $name), $value);
        }
        return $this;
    }

    /**
     * @param array|string|Zend_Config $config
     *
     * @return \Core_Application
     */
    protected function _configureLoaderClassMap($config, Zend_Config $appConfig)
    {
        $config = $this->_normalizeConfig($config, false, true, $appConfig);
        $this->getLoader()->setClassMap($config);

        return $this;
    }
    /**
     *
     * @param array|string|Zend_Config $config    The Container config
     * @param Zend_Config              $appConfig Application-level config
     *
     * @return \Core_Application
     */
    protected function _configureContainer($config, Zend_Config $appConfig)
    {
        $config    = $this->_normalizeConfig($config, false, true, $appConfig);
        $container = $this->getContainer();
        $container->addComponents($config);
        $container->setComponent('config', $appConfig);

        return $this;
    }

    /**
     * @param array|string|Zend_Config $config      Config in question
     * @param bool                     $environment Do we use the current environment name as config section
     * @param bool                     $dataArray   Return array if true, else return Zend_Config
     *
     * @return array|Zend_Config
     */
    protected function _normalizeConfig($config, $environment = false, $dataArray = true, Zend_Config $appConfig = null)
    {
        if (is_string($config)) {
            if ($environment) {
                $environment = $this->getEnvironment();
            } else {
                $environment = null;
            }
            $options     = array('rawDataArray' => $dataArray, 'inject' => $appConfig);
            $config      = new Core_Config($config, $environment, $options);

            if ($dataArray) {
                $config = $config->dataArray;
            }
            return $config;
        } else if ($dataArray && $config instanceof Zend_Config) {
            $config = $config->toArray();
        }
        return $config;
    }
}
