<?php
namespace SabaiApps\Directories\Component;

use SabaiApps\Framework\Application\IEventListener;
use SabaiApps\Directories\Application;

/**
 * AbstractComponent provides the base class for all application components.
 * Handles configuration, event handling, schema management, installation, upgrade, and uninstallation.
 */
abstract class AbstractComponent implements IEventListener {

	protected $_name, $_config, $_system = false;

	/** @var Application $_application */
	protected $_application;

	private static $_models = array();

	/**
	 * Constructor.
	 *
	 * @param string      $name Component name.
	 * @param Application $application Application instance.
	 */
	public function __construct( $name, Application $application ) {
		$this->_name        = $name;
		$this->_application = $application;
	}

	/**
	 * Initializes the component with configuration.
	 *
	 * @param array $config
	 */
	final public function init( array $config ) {
		$this->_config = $config;
		$this->_init();
	}

	/**
	 * Internal initialization logic for the component.
	 * Override this method in subclasses for custom initialization.
	 */
	protected function _init() {}

	/**
	 * Handles an event by calling the corresponding handler method if available.
	 *
	 * @param string $eventType
	 * @param array  $eventArgs
	 * @return mixed
	 */
	public function handleEvent( $eventType, array $eventArgs ) {
		if ( ! is_callable( array( $this, 'on' . $eventType ) ) ) {
			return;
		}

		return @call_user_func_array( array( $this, 'on' . $eventType ), $eventArgs );
	}

	/**
	 * Returns the component name as a string.
	 *
	 * @return string
	 */
	final public function __toString() {
		return $this->_name;
	}

	/**
	 * Gets the application instance.
	 *
	 * @return Application
	 */
	final public function getApplication() {
		return $this->_application;
	}

	/**
	 * Gets the component name.
	 *
	 * @return string
	 */
	final public function getName() {
		return $this->_name;
	}

	/**
	 * Gets the component version.
	 *
	 * @return string
	 */
	final public function getVersion() {
		$class = get_class( $this );
		return $class::VERSION;
	}

	/**
	 * Gets the package name for the component.
	 *
	 * @return string
	 */
	final public function getPackage() {
		$class = get_class( $this );
		return $class::PACKAGE;
	}

	/**
	 * Gets the model or a specific repository for this component.
	 *
	 * @param string|null $modelName
	 * @return Model|object
	 */
	final public function getModel( $modelName = null ) {
		if ( ! isset( self::$_models[ $this->_name ] ) ) {
			self::$_models[ $this->_name ] = new Model( $this );
		}

		return isset( $modelName ) ? self::$_models[ $this->_name ]->getRepository( $modelName ) : self::$_models[ $this->_name ];
	}

	/**
	 * Gets the configuration or a specific config value.
	 *
	 * @param string|null $name
	 * @return mixed
	 */
	final public function getConfig( $name = null ) {
		if ( 0 === $num_args = func_num_args() ) {
			return $this->_config;
		}
		if ( $num_args === 1 ) {
			return isset( $this->_config[ $name ] ) ? $this->_config[ $name ] : null;
		}

		$args   = func_get_args();
		$config = $this->_config;
		foreach ( $args as $arg ) {
			if ( ! isset( $config[ $arg ] ) ) {
				return null;
			}

			$config = $config[ $arg ];
		}

		return $config;
	}

	/**
	 * Gets the default configuration for the component.
	 * Override this method to provide default config values.
	 *
	 * @return array
	 */
	public function getDefaultConfig() {
		// Override this to provide default configuration parameters
		return array();
	}

	/**
	 * Installs the plugin and updates the database schema if available.
	 *
	 * @return $this
	 */
	public function install() {
		if ( $schema = $this->_hasSchema() ) {
			$this->_application->getPlatform()->updateDatabase( $schema );
		}
		$this->_createVarDirs();

		return $this;
	}

	/**
	 * Uninstalls the plugin and optionally removes data.
	 *
	 * @param bool $removeData
	 * @throws RuntimeException
	 * @return $this
	 */
	public function uninstall( $removeData = false ) {
		if ( $removeData
			&& ( $latest_schema = $this->_hasSchema() )
		) {
			if ( false === $schema_old = $this->_getOlderSchemaList( $this->getVersion() ) ) {
				throw new RuntimeException( 'Failed fetching schema files.' );
			}

			if ( ! empty( $schema_old ) ) {
				// get the last schema file
				$previous_schema = array_pop( $schema_old );
			} else {
				$previous_schema = $latest_schema;
			}
			$this->_application->getPlatform()->updateDatabase( null, $previous_schema );
		}

		return $this;
	}

	/**
	 * Upgrades the plugin and updates the database schema if needed.
	 *
	 * @param object               $current Current version entity.
	 * @param string               $newVersion New version string.
	 * @param System\Progress|null $progress Optional progress object.
	 * @throws \RuntimeException
	 * @return $this
	 */
	public function upgrade( $current, $newVersion, ?System\Progress $progress = null ) {
		$this->_application->logDebug(
			sprintf(
				'Upgrading component %s from version %s to %s',
				$this->_name,
				$current->version,
				$newVersion
			)
		);
		if ( $this->_hasSchema() ) {
			if ( ( false === $schema_old = $this->_getOlderSchemaList( $current->version ) )
				|| ( false === $schema_new = $this->_getNewerSchemaList( $current->version, $newVersion ) )
			) {
				throw new \RuntimeException( 'Failed fetching schema files.' );
			}

			if ( ! empty( $schema_new ) ) {
				$this->_application->logDebug(
					sprintf(
						'Updating database schema of component %s from version %s to %s',
						$this->_name,
						$current->version,
						$newVersion
					)
				);
				$sorter = function ( $a, $b ) {
					return version_compare( $a, $b, '<' ) ? -1 : 1;
				};
				usort( $schema_new, $sorter ); // sort from old to new
				$new_schema      = array_pop( $schema_new );
				$previous_schema = null;
				if ( ! empty( $schema_old ) ) {
					usort( $schema_old, $sorter ); // sort from old to new
					// get the last schema file
					$previous_schema = array_pop( $schema_old );
				}
				$this->_application->getPlatform()->updateDatabase( $new_schema, $previous_schema );
				if ( isset( $progress ) ) {
					$progress->set(
						sprintf(
							'Updated database schema of ' . $this->_name . ' from %s to %s.',
							isset( $previous_schema ) ? basename( $previous_schema ) : 'none',
							basename( $new_schema )
						)
					);
				}
			}
		}

		$this->_createVarDirs();

		return $this;
	}

	/**
	 * Creates the variable data directories for the component if needed.
	 */
	protected function _createVarDirs() {
		// Create data directory
		if ( $dir = $this->hasVarDir() ) {
			if ( ! is_dir( $path = $this->getVarDir() ) ) {
				@mkdir( $path, 0755, true );
			}
			if ( is_array( $dir ) ) {
				foreach ( $dir as $_dir ) {
					if ( ! is_dir( $path = $this->getVarDir( $_dir ) ) ) {
						@mkdir( $path, 0755 );
					}
				}
			}
		}
	}

	/**
	 * Gets a list of all schema files for this component.
	 *
	 * @return array|false
	 */
	private function _getSchemaList() {
		$list       = array();
		$schema_dir = $this->_application->getComponentPath( $this->_name ) . '/schema';
		if ( false === $files = glob( $schema_dir . '/*.php', GLOB_NOSORT ) ) {
			return false; // return false on error
		}
		foreach ( $files as $file ) {
			if ( preg_match( '/^\d+(?:\.\d+)*(?:\-[a-zA-Z]+\.*\d*)?\.php$/', basename( $file ) ) ) {
				$file_version          = basename( $file, '.php' );
				$list[ $file_version ] = $file;
			}
		}

		return $list;
	}

	/**
	 * Gets a list of schema files older than or equal to the given version.
	 *
	 * @param string $version
	 * @return array|false
	 */
	private function _getOlderSchemaList( $version ) {
		if ( ! $list = $this->_getSchemaList() ) {
			return $list;
		}

		return array_intersect_key(
			$list,
			array_flip(
				array_filter(
					array_flip( $list ),
					function ( $v ) use ( $version ) {
						return version_compare( $v, $version, '<=' ); }
				)
			)
		);
	}

	/**
	 * Gets a list of schema files newer than the given version and up to maxVersion.
	 *
	 * @param string $version
	 * @param string $maxVersion
	 * @return array|false
	 */
	private function _getNewerSchemaList( $version, $maxVersion ) {
		if ( ! $list = $this->_getSchemaList() ) {
			return $list;
		}

		return array_intersect_key(
			$list,
			array_flip(
				array_filter(
					array_flip( $list ),
					function ( $v ) use ( $version, $maxVersion ) {
						return version_compare( $v, $version, '>' ) && version_compare( $v, $maxVersion, '<=' ); }
				)
			)
		);
	}

	/**
	 * Checks if the component has a schema file.
	 *
	 * @return string|false
	 */
	protected function _hasSchema() {
		$schema_path = $this->_application->getComponentPath( $this->_name ) . '/schema/latest.php';

		return file_exists( $schema_path ) ? $schema_path : false;
	}

	/**
	 * Gets the variable data directory for the component.
	 *
	 * @param string|null $subdir
	 * @return string
	 */
	final public function getVarDir( $subdir = null ) {
		$var_dir = $this->_application->getPath( $this->_application->getPlatform()->getVarDir() ) . '/' . $this->_name;

		return isset( $subdir ) ? $var_dir . '/' . $subdir : $var_dir;
	}

	/**
	 * Checks if the component has a variable data directory.
	 *
	 * @return bool|array
	 */
	public function hasVarDir() {
		return false;
	}

	/**
	 * Checks if the component is installable.
	 *
	 * @return bool
	 */
	public function isInstallable() {
		return true;
	}

	/**
	 * Checks if the component is upgradeable to a new version.
	 *
	 * @param string $currentVersion
	 * @param string $newVersion
	 * @return bool
	 */
	public function isUpgradeable( $currentVersion, $newVersion ) {
		return version_compare( $currentVersion, $newVersion, '<' );
	}

	/**
	 * Checks if a slug exists for the given name and language.
	 *
	 * @param string      $name
	 * @param string|null $lang
	 * @return bool
	 */
	public function hasSlug( $name, $lang = null ) {
		return $this->_application->getPlatform()->hasSlug( $this->_name, strtolower( $name ), $lang );
	}

	/**
	 * Gets the slug for the given name and language.
	 *
	 * @param string      $name
	 * @param string|null $lang
	 * @return string
	 */
	public function getSlug( $name, $lang = null ) {
		return $this->_application->getPlatform()->getSlug( $this->_name, strtolower( $name ), $lang );
	}

	/**
	 * Gets the title for the given name and language.
	 *
	 * @param string      $name
	 * @param string|null $lang
	 * @return string
	 */
	public function getTitle( $name, $lang = null ) {
		return $this->_application->getPlatform()->getTitle( $this->_name, strtolower( $name ), $lang );
	}
}
