<?php
namespace SabaiApps\Directories;

use SabaiApps\Directories\Component\AbstractComponent;
use SabaiApps\Framework\Application\AbstractHttpApplication;
use SabaiApps\Framework\Application\Context as FrameworkContext;
use SabaiApps\Framework\Application\IController;
use SabaiApps\Framework\User\User;
use SabaiApps\Directories\Request;
use SabaiApps\Framework\Application\Url;

/**
 * Application class for the main application logic.
 * Handles component loading, user management, helpers, templates, and platform integration.
 */
class Application extends AbstractHttpApplication {

	/**
	 * @var mixed Static property for global access.
	 */
	public static $p;

	/**
	 * @var bool Indicates if the application is currently running.
	 */
	private $_running = false;
	/**
	 * @var Platform\AbstractPlatform The platform instance.
	 */
	private $_platform;

	private $_template, $_user,
		$_components = array(), $_componentsLoaded = array(), $_componentsLoadedTimestamp,
		$_mobileDetect, $_packagePath;

	// System version constants
	const VERSION = '2.0.6', PACKAGE = 'directories-pro';

	// Regular expression for component names
	const COMPONENT_NAME_REGEX = '/^[a-zA-Z0-9]{2,}$/';

	// Route types and constants
	const ROUTE_NORMAL     = 0, ROUTE_TAB = 1, ROUTE_MENU = 2, ROUTE_CALLBACK = 3,
		ROUTE_ACCESS_LINK  = 0, ROUTE_ACCESS_CONTENT = 1,
		ROUTE_TITLE_NORMAL = 0, ROUTE_TITLE_TAB = 1, ROUTE_TITLE_MENU = 3, ROUTE_TITLE_INFO = 4;
	// Column types
	const COLUMN_INTEGER = 'integer', COLUMN_BOOLEAN = 'boolean', COLUMN_VARCHAR = 'text',
		COLUMN_TEXT      = 'clob', COLUMN_DECIMAL = 'decimal', COLUMN_DATETIME = 'datetime', COLUMN_JSON = 'json';

	/**
	 * Constructor.
	 *
	 * @param Platform\AbstractPlatform $platform Platform instance.
	 */
	public function __construct( Platform\AbstractPlatform $platform ) {
		parent::__construct( $platform->getRouteParam() );
		$this->_platform = $platform;
		spl_autoload_register( array( $this, 'autoload' ), true, true );
		// Set helper directory and default helper functions
		$this->addHelperDir( __DIR__ . '/Helper', 'SabaiApps\\Directories\\Helper\\' )
			->setHelper( 'Platform', array( $this, 'getPlatform' ) )
			->setHelper(
				'H',
				function ( Application $application, $str, $quoteStyle = ENT_QUOTES, $doubleEncode = false ) {
					return htmlspecialchars( (string) $str, $quoteStyle, 'UTF-8', $doubleEncode );
				}
			)
			->setHelper(
				'Htmlize',
				function ( Application $application, $str, $restrictTags = false, array $disallowedTags = array(), $forCaching = false ) {
					return $application->getPlatform()->htmlize( $str, $restrictTags, $disallowedTags, $forCaching );
				}
			)
			->setHelper(
				'Attr',
				function ( Application $application, array $attr, $exclude = null, $attrPrefix = '', $prefix = ' ', $quoteStyle = ENT_COMPAT ) {
					if ( empty( $attr ) ) {
						return '';
					}

					if ( isset( $exclude ) ) {
						foreach ( (array) $exclude as $k ) {
							unset( $attr[ $k ] );
						}
					}
					if ( $attrPrefix ) {
						$attrPrefix = $application->H( $attrPrefix );
					}
					foreach ( $attr as $k => $v ) {
						$attr[ $k ] = sprintf( '%s%s="%s"', $attrPrefix, $application->H( $k ), $application->H( (string) $v, $quoteStyle ) );
					}

					return $prefix . implode( ' ', $attr );
				}
			)
			->setHelper(
				'Render',
				function ( Application $application, $templateName, array $vars = array(), $css = null, $extension = '.html' ) {
					$package = null;
					if ( is_array( $templateName ) ) {
						$package      = $templateName[1];
						$templateName = $templateName[0];
					}
					if ( ! empty( $css ) ) {
						if ( ! is_string( $css ) ) {
							$css = $templateName;
						}
						$application->getPlatform()->addCssFile( $css . '.min.css', 'drts-' . $css, 'drts', $package );
					}
					if ( isset( $package ) ) {
						$templateName = $application->getPlatform()->getAssetsDir( $package ) . '/templates/' . $templateName;
					}
					return $application->getTemplate()->render( $templateName, $vars, $extension );
				}
			)
			->setHelper( 'Url', array( __CLASS__, 'urlHelper' ) )
			->setHelper( 'MainUrl', array( __CLASS__, 'mainUrlHelper' ) )
			->setHelper( 'AdminUrl', array( __CLASS__, 'adminUrlHelper' ) );
	}

	/**
	 * Autoloads component classes as needed.
	 *
	 * @param string $class Class name to autoload.
	 */
	public function autoload( $class ) {
		if ( empty( $this->_componentsLoaded )
			|| 0 !== strpos( $class = trim( $class, '\\' ), __NAMESPACE__ . '\\Component\\' )
		) {
			return;
		}

		$_class = substr( $class, strlen( __NAMESPACE__ . '\\Component\\' ) );
		if ( strpos( $_class, '\\' ) ) {
			$_parts    = explode( '\\', $_class );
			$component = array_shift( $_parts );
			if ( isset( $this->_componentsLoaded[ $component ]['path'] ) ) {
				include $this->getPackagePath() . $this->_componentsLoaded[ $component ]['path']
					. DIRECTORY_SEPARATOR . implode( DIRECTORY_SEPARATOR, $_parts ) . '.php';
			}
		} elseif ( isset( $this->_componentsLoaded[ $_class ]['path'] ) ) {
				include $this->getPackagePath() . $this->_componentsLoaded[ $_class ]['path'] . '.php';
		}
	}

	/**
	 * Loads all components.
	 *
	 * @return $this
	 */
	public function loadComponents() {
		if ( ! isset( $this->_componentsLoadedTimestamp ) ) {
			$this->_loadComponents();
		}

		return $this;
	}

	/**
	 * Reloads all components, optionally clearing the object cache.
	 *
	 * @param bool $clearObjectCache Whether to clear the object cache.
	 * @return $this
	 */
	public function reloadComponents( $clearObjectCache = true ) {
		if ( $clearObjectCache ) {
			$this->_components = array();
		}
		$this->_loadComponents( true );

		return $this;
	}

	/**
	 * Runs the application with the given controller and context.
	 *
	 * @param IController      $controller
	 * @param FrameworkContext $context
	 * @param mixed            $route
	 * @return Response
	 */
	public function run( IController $controller, FrameworkContext $context, $route = null ): Response {
		$this->_running = true;

		$this->Action( 'core_run', array( $context, $controller, $route ) );

		$response = parent::run( $controller, $context, $route );

		$this->Action( 'core_run_complete', array( $context, $route ) );

		$this->_running = false;

		return $response;
	}

	/**
	 * Checks if the application is currently running.
	 *
	 * @return bool
	 */
	public function isRunning() {
		return $this->_running;
	}

	/**
	 * Returns a backtrace or prints it.
	 *
	 * @param bool $print Whether to print the backtrace.
	 * @param int  $limit Number of stack frames to return.
	 * @return array|null
	 */
	public function backtrace( $print = false, $limit = 10 ) {
		return $print ? debug_print_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS, $limit ) : debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS, $limit );
	}

	/**
	 * Gets the platform instance.
	 *
	 * @return Platform\AbstractPlatform
	 */
	public function getPlatform() {
		return $this->_platform;
	}

	/**
	 * Gets the database instance.
	 *
	 * @return \SabaiApps\Framework\DB\AbstractDB
	 */
	public function getDB() {
		return $this->_platform->getDB();
	}

	/**
	 * Normalizes and returns a path.
	 *
	 * @param string $path
	 * @return string
	 */
	public function getPath( $path ) {
		if ( strpos( $path, '\\' ) === false ) {
			// Not a windows path
			// Make sure to start with single slash, some servers return // for some reason
			return '/' . ltrim( $path, '/' );
		}
		$path = str_replace( '\\', '/', $path );
		if ( $first_slash_pos = strpos( $path, '/' ) ) {
			$path = substr( $path, $first_slash_pos );  // remove c: part
		}
		return $path;
	}

	/**
	 * Gets the package path.
	 *
	 * @return string
	 */
	public function getPackagePath() {
		if ( ! isset( $this->_packagePath ) ) {
			$this->_packagePath = $this->getPath( $this->getPlatform()->getPackagePath() );
		}
		return $this->_packagePath;
	}

	/**
	 * Sets the current user.
	 *
	 * @param User $user
	 * @return $this
	 */
	public function setUser( User $user ) {
		$user_changed = $this->_user && $this->_user->id !== $user->id;
		// Notify that the current user object has been initialized
		$this->Action( 'core_user_initialized', array( $user, $user_changed ) );

		$this->_user = $user;

		return $this;
	}

	/**
	 * Gets the current user.
	 *
	 * @return User
	 */
	public function getUser() {
		if ( ! isset( $this->_user ) ) {
			// Initialize the current user object if not any set
			if ( $user = $this->_platform->getCurrentUser() ) {
				$user->setAdministrator( $this->_platform->isAdministrator( $user->id ) );
			} else {
				$user = new User( $this->_platform->getUserIdentityFetcher()->getAnonymous() );
			}
			$this->setUser( $user );
		}

		return $this->_user;
	}

	/**
	 * Gets the roles for the current user.
	 *
	 * @return array
	 */
	public function getRoles() {
		$user = $this->getUser();
		if ( $user->isAnonymous()
			|| ( false === $roles = $this->_platform->getRoles( $user->getIdentity()->id ) )
		) {
			$roles = array( '_guest_' );
		}
		return $roles;
	}

	/**
	 * Shortcut method for fetching the model object of a component.
	 *
	 * @param string $modelName
	 * @param string $componentName
	 * @return Component\Model
	 */
	public function getModel( $modelName = null, $componentName = null ) {
		return $this->getComponent( $componentName )->getModel( $modelName );
	}

	/**
	 * Gets the template instance.
	 *
	 * @return Template
	 */
	public function getTemplate() {
		if ( ! isset( $this->_template ) ) {
			$template_dirs = array();
			foreach ( $this->_platform->getCustomAssetsDir() as $dir ) {
				$template_dirs[] = $dir;
			}
			$template_dirs[] = $this->_platform->getAssetsDir() . '/templates';
			$this->_template = new Template( $this, $template_dirs );
		}
		return $this->_template;
	}

	/**
	 * Checks if a component is loaded.
	 *
	 * @param string $componentName
	 * @return bool
	 */
	public function isComponentLoaded( $componentName ) {
		return isset( $this->_componentsLoaded[ $componentName ] );
	}

	/**
	 * Shortcut method for fetching a component object.
	 *
	 * @param string $componentName
	 * @return Component\AbstractComponent
	 */
	public function getComponent( $componentName ) {
		if ( ! isset( $this->_components[ $componentName ] ) ) {
			// Create component
			$component = $this->_createComponent( $componentName );
			// Let the component have chance to initialize itself
			$component->init( $this->_componentsLoaded[ $componentName ]['config'] );
			// Add to memory cache
			$this->_components[ $componentName ] = $component;
		}

		return $this->_components[ $componentName ];
	}

	/**
	 * Gets a component which is not yet installed.
	 *
	 * @param string $componentName
	 * @param array  $config
	 * @return Component\AbstractComponent
	 */
	public function fetchComponent( $componentName, array $config = array() ) {
		$component = $this->_createComponent( $componentName );
		// Let the component have chance to initialize itself
		$component->init( $config + $component->getDefaultConfig() );

		return $component;
	}

	/**
	 * Creates a component instance.
	 *
	 * @param string $componentName
	 * @return Component\AbstractComponent
	 */
	private function _createComponent( $componentName ) {
		// Instantiate component
		$class = '\SabaiApps\Directories\Component\\' . $componentName . '\\' . $componentName . 'Component';
		if ( ! class_exists( $class, false ) ) {
			$component_file_path = $this->getComponentPath( $componentName ) . '/' . $componentName . 'Component.php';
			if ( ! @include $component_file_path ) {
				// Clear component info cache to prevent loop caused by InstalledComponents helper trying to fetch System component when component info is reloaded
				if ( $componentName === 'System' ) {
					$this->clearComponentInfoCache();
				}
				// Reload component info and try again
				$this->_reloadComponentInfo( $componentName );
				if ( ! @include $component_file_path ) {
					$this->clearComponentInfoCache();
					throw new Exception\ComponentNotFoundException( 'Component file for component ' . $componentName . ' was not found at ' . $component_file_path );
				}
			}
		}
		$reflection = new \ReflectionClass( $class );
		return $reflection->newInstanceArgs( array( $componentName, $this ) );
	}

	/**
	 * Returns the full path to a component directory.
	 *
	 * @param string $componentName
	 * @return string
	 */
	public function getComponentPath( $componentName ) {
		if ( $path = $this->_getComponentInfo( $componentName, 'path' ) ) {
			return $this->getPackagePath() . $path;
		}
		throw new Exception\ComponentNotFoundException( 'Component info for component ' . $componentName . ' was not found.' );
	}

	/**
	 * Gets component info from the loaded components.
	 *
	 * @param string $componentName
	 * @param string $key
	 * @return mixed|null
	 */
	protected function _getComponentInfo( $componentName, $key ) {
		if ( ! isset( $this->_componentsLoaded[ $componentName ] ) ) {
			// Fetch from local file
			$local_components = $this->LocalComponents();
			if ( isset( $local_components[ $componentName ] ) ) {
				return isset( $local_components[ $componentName ][ $key ] ) ? $local_components[ $componentName ][ $key ] : null;
			}

			// No local file data, so force reload
			$this->_reloadComponentInfo( $componentName );
		}
		return isset( $this->_componentsLoaded[ $componentName ][ $key ] ) ? $this->_componentsLoaded[ $componentName ][ $key ] : null;
	}

	/**
	 * Reloads component info for a specific component.
	 *
	 * @param string $componentName
	 * @throws Exception\ComponentNotInstalledException
	 */
	protected function _reloadComponentInfo( $componentName ) {
		$this->_loadComponents( true );
		if ( isset( $this->_componentsLoaded[ $componentName ] ) ) {
			return;
		}

		$this->clearComponentInfoCache();
		throw new Exception\ComponentNotInstalledException( 'The following component is not installed or loaded: ' . $componentName );
	}

	/**
	 * Clears the component info cache.
	 */
	public function clearComponentInfoCache() {
		$this->_platform->deleteCache( 'core_components_loaded' . md5( __FILE__ ) )
			->deleteCache( 'core_components_local' )
			->deleteCache( 'core_components_installed' );
	}

	/**
	 * Loads all components and attaches events.
	 *
	 * @param bool $force Whether to force reload.
	 * @throws Exception\NotInstalledException
	 */
	private function _loadComponents( $force = false ) {
		if ( $force
			|| ( ! $info = $this->_platform->getCache( 'core_components_loaded' . md5( __FILE__ ) ) )
			|| empty( $info['components'] )
		) {
			$info = array(
				'components' => array(),
				'timestamp'  => time(),
				'events'     => array(),
			);
			$this->clearEventListeners();
			$local                = $this->LocalComponents( true );
			$installed_components = $this->InstalledComponents( true );
			foreach ( array_keys( $installed_components ) as $component_name ) {
				if ( ! isset( $local[ $component_name ] ) ) {
					continue;
				}

				$component_local                       = $local[ $component_name ];
				$info['components'][ $component_name ] = array(
					'path'    => $component_local['path'],
					'config'  => $installed_components[ $component_name ]['config'],
					'package' => $component_local['package'],
				);
				if ( ! empty( $installed_components[ $component_name ]['events'] ) ) {
					$info['events'][ $component_name ] = $installed_components[ $component_name ]['events'];
				}
			}
			$this->_platform->setCache( $info, 'core_components_loaded' . md5( __FILE__ ), 0 );
		}

		if ( empty( $info['components'] ) ) {
			$this->clearComponentInfoCache();
			throw new Exception\NotInstalledException();
		}

		// Load components
		$this->_componentsLoaded          = $info['components'];
		$this->_componentsLoadedTimestamp = $info['timestamp'];

		// Attach events
		if ( ! empty( $info['events'] ) ) {
			foreach ( array_keys( $info['events'] ) as $component_name ) {
				foreach ( $info['events'][ $component_name ] as $event ) {
					$this->addEventListener( $event[0], $component_name, $event[1] );
				}
			}
		}

		$this->Action( 'core_components_loaded' );
	}

	/**
	 * Creates a new response object.
	 *
	 * @return Response
	 */
	protected function _createResponse(): Response {
		return new Response();
	}

	/**
	 * Gets the event listener for a given event.
	 *
	 * @param string $eventListener
	 * @return AbstractComponent
	 */
	protected function _getEventListener( $eventListener ): AbstractComponent {
		return $this->getComponent( $eventListener );
	}

	/**
	 * Gets a helper by name, searching component helpers if needed.
	 *
	 * @param string $name
	 * @return mixed
	 * @throws Exception\RuntimeException
	 */
	protected function _getHelper( $name ) {
		if ( strpos( $name, '_', 1 ) ) {
			// Search component's helper directory
			if ( ( $name_arr = explode( '_', $name ) )
				&& $this->isComponentLoaded( $name_arr[0] )
			) {
				if ( isset( $name_arr[2] ) ) {
					if ( ! isset( $this->_helpers[ $name ] ) ) {
						$_name = $name_arr[0] . '_' . $name_arr[1];
						if ( ! isset( $this->_helpers[ $_name ] ) ) {
							$this->setHelper( $_name, array( $this->_createHelper( $name_arr[0], $name_arr[1] ), 'help' ) );
						}
						$this->setHelper( $name, array( $this->_helpers[ $_name ][0], $name_arr[2] ) );
					}
				} elseif ( ! isset( $this->_helpers[ $name ] ) ) {
						$this->setHelper( $name, array( $this->_createHelper( $name_arr[0], $name_arr[1] ), 'help' ) );
				}
				return $this->_helpers[ $name ];
			}
			throw new Exception\RuntimeException( sprintf( 'Call to undefined application helper %s.', $name ) );
		}

		return parent::_getHelper( $name );
	}

	/**
	 * Creates a helper instance for a component.
	 *
	 * @param string $component
	 * @param string $helper
	 * @return mixed
	 */
	protected function _createHelper( $component, $helper ) {
		$class = '\SabaiApps\Directories\Component\\' . $component . '\Helper\\' . $helper . 'Helper';
		return new $class( $this );
	}

	/**
	 * Checks if the current device is mobile.
	 *
	 * @return bool
	 */
	public function isMobile() {
		return ( $md = $this->isMobileDetectable() ) ? $md->isMobile() : false;
	}

	/**
	 * Checks if mobile detection is available.
	 *
	 * @return mixed
	 */
	public function isMobileDetectable() {
		if ( ! isset( $this->_mobileDetect ) ) {
			if ( class_exists( '\Detection\MobileDetect' )
				&& $this->Filter( 'core_use_mobile_detect', true )
			) {
				$this->_mobileDetect = new \Detection\MobileDetect();
			} else {
				$this->_mobileDetect = false;
			}
		}
		return $this->_mobileDetect;
	}

	/**
	 * Generates a URL object for a route or external URL.
	 *
	 * @param Application $application
	 * @param mixed       $route Route string, array, or Url object.
	 * @param array       $params Query parameters.
	 * @param string      $fragment URL fragment.
	 * @param string      $separator Query separator.
	 * @param string|null $script Script name.
	 * @return Url
	 */
	public static function urlHelper( Application $application, $route = '', array $params = array(), $fragment = '', $separator = '&amp;', $script = null ) {
		if ( $route instanceof Url ) {
			return $route;
		}

		if ( is_array( $route ) ) {
			return $application->createUrl( $route );
		}

		if ( is_string( $route ) && strlen( $route ) && strpos( $route, '/' ) !== 0 ) {
			if ( filter_var( $route, FILTER_VALIDATE_URL ) === false ) {
				$application->logWarning( 'Rejecting invalid URL in link: ' . $route );
				return $application->createUrl();
			}
			if ( strpos( $route, $application->getScriptUrl( $application->getCurrentScriptName() ) ) !== 0 ) {
				// External URL
				return $application->createUrl(
					array(
						'script_url' => $route,
						'params'     => $params,
						'fragment'   => $fragment,
						'separator'  => $separator,
					)
				);
			}
			if ( ! $parsed = parse_url( str_replace( '&amp;', '&', $route ) ) ) {
				$application->logWarning( 'Failed parsing URL in link: ' . $route );
				return $application->createUrl();
			}

			$fragment = ! isset( $fragment ) && isset( $parsed['fragment'] ) ? $parsed['fragment'] : (string) $fragment;
			if ( ! empty( $parsed['query'] ) ) {
				$query = array();
				parse_str( $parsed['query'], $query );
				unset( $query[ Request::PARAM_AJAX ] );
				$params += $query;
				if ( isset( $params[ $application->getRouteParam() ] ) ) {
					$route = $params[ $application->getRouteParam() ];
					unset( $params[ $application->getRouteParam() ] );

					return $application->createUrl(
						array(
							'route'     => $route,
							'params'    => $params,
							'fragment'  => $fragment,
							'separator' => $separator,
							'script'    => $script,
						)
					);
				}
			}

			$url = $parsed['scheme'] . '://' . $parsed['host'];
			if ( isset( $parsed['port'] ) ) {
				$url .= ':' . $parsed['port'];
			}
			if ( isset( $parsed['path'] ) ) {
				$url .= $parsed['path'];
			}
			return $application->createUrl(
				array(
					'script_url' => $url,
					'params'     => $params,
					'fragment'   => $fragment,
					'separator'  => $separator,
				)
			);
		}

		return $application->createUrl(
			array(
				'route'     => $route,
				'params'    => $params,
				'fragment'  => $fragment,
				'separator' => $separator,
				'script'    => $script,
			)
		);
	}

	/**
	 * Gets a main URL for the given route and parameters.
	 *
	 * @param Application $application
	 * @param string      $route
	 * @param array       $params
	 * @param string      $fragment
	 * @param string      $separator
	 * @return mixed
	 */
	public static function mainUrlHelper( Application $application, $route = '/', array $params = array(), $fragment = '', $separator = '&amp;' ) {
		return $application->Url(
			array(
				'route'     => $route,
				'script'    => 'main',
				'params'    => $params,
				'fragment'  => $fragment,
				'separator' => $separator,
			)
		);
	}

	/**
	 * Generates an admin URL.
	 *
	 * @param Application $application
	 * @param string      $route
	 * @param array       $params
	 * @param string      $fragment
	 * @param string      $separator
	 * @return mixed
	 */
	public static function adminUrlHelper( Application $application, $route = '/', array $params = array(), $fragment = '', $separator = '&amp;' ) {
		return $application->Url(
			array(
				'route'     => $route,
				'script'    => 'admin',
				'params'    => $params,
				'fragment'  => $fragment,
				'separator' => $separator,
			)
		);
	}

	public function isDemoMode() {
		return $this->Filter( 'core_demo_mode', defined( 'DRTS_DEMO_MODE' ) && DRTS_DEMO_MODE );
	}
}
