<?php
namespace SabaiApps\Framework\Application;

use SabaiApps\Framework\Exception;
//use Psr\Log\LoggerAwareInterface;
//use Psr\Log\LoggerInterface;

/**
 * Abstract class for applications
 * 
 * This class provides a base implementation for applications, including logging,
 * event handling, and helper management.
 */
abstract class AbstractApplication // implements LoggerAwareInterface
{
    /**
     * The name of the request parameter that contains the route
     * @var string
     */
    private $_routeParam;
    /**
     * Directories where application helpers are located, indexed by directory path
     * @var array
     */
    private $_helperDir = [];
    /**
     * Event listeners, indexed by event type and priority
     * @var array
     */
    private $_eventListeners = [];
    /**
     * Sorted event listeners, indexed by event type
     * @var array
     */
    private $_eventListenersSorted = [];
    /**
     * Application helpers, indexed by helper name
     * @var array
     */
    protected $_helpers = [];
    /**
     * Logger instance for logging messages
     * @var LoggerInterface|null
     */
    protected $_logger;

    /**
     * Constructor
     * @param string $routeParam The name of the request parameter that contains the route
     */
    protected function __construct($routeParam)
    {
        $this->_routeParam = $routeParam;
    }

    /**
     * Get the name of the request parameter that contains the route
     * @return string
     */
    public function getRouteParam(): string
    {
        return $this->_routeParam;
    }
    
    /**
     * @param LoggerInterface $logger
     * @return void
     */
    //public function setLogger(LoggerInterface $logger): void
    public function setLogger($logger)
    {
        $this->_logger = $logger;
    }
    
    /**
     * @return LoggerInterface
     */
    public function getLogger()
    {
        return $this->_logger;
    }

    /**
     * Log a debug level message
     * @param string $message
     * @param array $context
     */
    public function logDebug($message, array $context = []): void
    {
        if ($this->_logger) $this->_logger->debug($message, $context);
    }
    
    /**
     * Log a notice level message
     * @param string $message
     * @param array $context
     */
    public function logNotice($message, array $context = []): void
    {
        if ($this->_logger) $this->_logger->notice($message, $context);
    }
    
    /**
     * Log an warning level message
     * @param string $message
     * @param array $context
     */
    public function logWarning($message, array $context = []): void
    {
        if ($this->_logger) $this->_logger->warning($message, $context);
    }
    
    /**
     * Log an error level message
     * @param string|\Exception $message
     * @param array $context
     */
    public function logError($message, array $context = []): void
    {
        if (!$this->_logger) return;
            
        if ($message instanceof \Exception) {
            $this->_logger->critical($message->getMessage(), $context + ['exception' => (string)$message]);
        } else {
            $this->_logger->error($message, $context);
        }
    }
    
    /**
     * Start a session
     * @param string|null $path The path to save the session files, if null, uses the default session save path
     */
    public static function startSession($path = null): void
    {
        if (session_id()) return;
        
        if ($path && is_string($path)) {
            session_save_path($path);
        }
        @ini_set('session.use_only_cookies', 1);
        @ini_set('session.use_trans_sid', 0);
        @ini_set('session.hash_function', 1);
        @ini_set('session.cookie_httponly', 1);
        session_start();
    }

    /**
     * Magic method to call application helpers
     * @param string $name The name of the helper to call
     * @param array $args The arguments to pass to the helper
     * @return mixed
     */
    public function __call($name, $args)
    {
        return $this->callHelper($name, $args);
    }

    /**
     * Run the application
     * @param IController $controller The controller to execute
     * @param Context $context The context of the request
     * @param string|null $route The route to execute, if null, fetches from request
     * @return AbstractResponse
     */
    public function run(IController $controller, Context $context, $route = null): AbstractResponse
    {
        register_shutdown_function([$this, 'shutdown'], $context);

        // Fetch route from request if none specified
        if (!isset($route)) $route = $context->getRequest()->asStr($this->getRouteParam());

        $controller->setApplication($this)->setRoute($route)->execute($context);

        return $this->_createResponse()->setApplication($this);
    }

    /**
     * Shutdown function to handle fatal errors
     * @param Context $context The context of the request
     */
    public function shutdown(Context $context): void
    {
        if (($error = error_get_last())
            && $error['type'] === E_ERROR
        ) {
            $context->setError(sprintf('Fatal error: %s in %s on line %d.', $error['message'], $error['file'], $error['line']));
            $this->_createResponse()->setApplication($this)->send($context);
        }
    }
    
    /**
     * Create a response object
     * @return AbstractResponse
     */
    abstract protected function _createResponse(): AbstractResponse;
    
    /**
     * Add an event listener
     * @param string $eventType The type of the event to listen for
     * @param mixed $eventListener The event listener, which must implement IEventListener
     * @param int $priority The priority of the event listener, lower number means higher priority
     * @return AbstractApplication
     */
    public function addEventListener($eventType, $eventListener, $priority = 10): AbstractApplication
    {
        $this->_eventListeners[$eventType][$priority][] = $eventListener;
        return $this;
    }
    
    /**
     * Check if an event listener exists for a specific event type
     * @param string $eventType The type of the event to check for
     * @return bool True if an event listener exists for the specified event type, false otherwise
     */
    public function hasEventListner($eventType): bool
    {
        return !empty($this->_eventListeners[$eventType]);
    }
    
    /**
     * Dispatch an event to all registered listeners
     * @param string $eventType The type of the event to dispatch
     * @param array $eventArgs The arguments to pass to the event listeners
     * @return AbstractApplication
     */
    public function dispatchEvent($eventType, array $eventArgs = []): AbstractApplication
    {
        if (!isset($this->_eventListenersSorted[$eventType])) {
            ksort($this->_eventListeners[$eventType]);
            $this->_eventListenersSorted[$eventType] = true;
        }
        foreach ($this->_eventListeners[$eventType] as $listeners) {
            foreach ($listeners as $listener) {
                try {
                    $this->_getEventListener($listener)->handleEvent($eventType, $eventArgs);
                } catch (\Exception $e) {
                    $this->logError($e);
                }
            }
        }
        return $this;
    }
    
    /**
     * Get the event listener object
     * @param mixed $eventListener
     * @return IEventListener
     */
    protected function _getEventListener($eventListener): IEventListener
    {
        return $eventListener;
    }

    /**
     * Clear all event listeners
     * @return AbstractApplication
     */
    public function clearEventListeners(): AbstractApplication
    {
        $this->_eventListeners = [];
        return $this;
    }
    
    /**
     * Add a helper directory
     * @param string $dir The directory to add
     * @param string $prefix The prefix to use for the helper class names
     * @return AbstractApplication
     */
    public function addHelperDir($dir, $prefix): AbstractApplication
    {
        $this->_helperDir = [$dir => $prefix] + $this->_helperDir;
        return $this;
    }
    
    /**
     * Call a helper method
     * @param string $name The name of the helper to call
     * @param array $args The arguments to pass to the helper
     * @return mixed
     */
    public function callHelper($name, array $args = [])
    {
        array_unshift($args, $this);
        $callback = $this->_getHelper($name);
        // Append additional args if any
        if (is_array($callback) && is_array($callback[1])) {
            $args = empty($args) ? $callback[1] : array_merge($args, $callback[1]);
            $callback = $callback[0];
        }
        return call_user_func_array($callback, $args);
    }

    /**
     * Get an application helper
     * @param string $name The name of the helper to get
     * @return mixed The helper callable
     * @throws Exception If the helper does not exist
     */
    protected function _getHelper($name)
    {
        if (isset($this->_helpers[$name])) return $this->_helpers[$name];

        foreach ($this->_helperDir as $helper_dir => $helper_prefix) {
            $class = $helper_prefix . $name . 'Helper';
            if (!class_exists($class, false)) {
                if (!@include sprintf('%s/%sHelper.php', $helper_dir, $name)) {
                    continue;
                }
            }
            $this->setHelper($name, array(new $class($this), 'help'));
            return $this->_helpers[$name];            
        }
        throw new Exception(sprintf('Call to undefined application helper %s.', $name));
    }

    /**
     * Set an application helper
     * @param $name string
     * @param $helper Callable method or function
     * @return AbstractApplication
     */
    public function setHelper($name, $helper): AbstractApplication
    {
        $this->_helpers[$name] = $helper;
        return $this;
    }

    /**
     * Check if a helper exists
     * @param string $name The name of the helper to check for
     * @return bool True if the helper exists, false otherwise
     */
    public function hasHelper($name): bool
    {
        return isset($this->_helpers[$name]);
    }
}