<?php
/**
 * Base class for item value handlers.
 *
 * @package Ealab\Deployer
 */

namespace Ealab\Deployer\Tracking\ItemValueHandler;

use Dakwamine\Component\ComponentBasedObject;
use Dakwamine\Component\RootDependencyDefinition;
use Ealab\Deployer\Exception\WpDeployException;
use Ealab\Deployer\Tracking\FieldConfig;
use Ealab\Deployer\Tracking\ItemValueHandler\ValueProcessor\ValueProcessorBase;
use Ealab\Deployer\Tracking\ItemValueHandler\ValueProcessor\ValueProcessorManager;
use Ealab\Deployer\Tracking\TrackableItem;
use Ealab\Deployer\Utility\Database;

/**
 * Base class for item value handlers.
 *
 * An item value handler holds a value and transforms it for export/import.
 * It informs the containing trackable item of the dependencies it may have.
 */
abstract class ItemValueHandlerBase extends ComponentBasedObject {

	use PlaceholderableTrait;

	/**
	 * The value comes from the database.
	 */
	const ORIGIN__DATABASE = 100;

	/**
	 * The value comes from the import file.
	 */
	const ORIGIN__IMPORT_FILE = 200;

	/**
	 * Transforms the value for export.
	 *
	 * The value must come from the database.
	 */
	const OPERATION__EXPORT = 0;

	/**
	 * Transforms the value for import, for the ensure ID stage on import.
	 *
	 * The value must come from the import file.
	 */
	const OPERATION__IMPORT__PRESAVE = 1;

	/**
	 * Transforms the value for import, for the final import stage.
	 *
	 * The value must come from the import file.
	 */
	const OPERATION__IMPORT__FINALIZE = 2;

	/**
	 * Trackable items that are required by the item holding this value_handler.
	 *
	 * This is useful for processors that are aware of dependencies (local ID references nested in the values, etc).
	 * Call addItemDependency() to add a dependency.
	 *
	 * @var array<TrackableItem>
	 */
	protected array $additional_trackable_item_dependencies = array();

	/**
	 * The result of the last operation.
	 *
	 * When the result is null, the value is considered as not set.
	 * When it is an array, it has the following keys:
	 * - "operation": The operation that was performed.
	 * - "value": The value after the operation.
	 *
	 * @var array<string,mixed>|null
	 */
	protected ?array $cached_result = null;

	/**
	 * The unprefixed full field name of this instance.
	 *
	 * This is the "<unprefixed_table_name>.<field_name>" assigned by the container.
	 * Please note that <unprefixed_table_name> is not DB prefixed.
	 *
	 * @var string
	 */
	protected string $full_field_name;

	/**
	 * The full handler ID of this instance.
	 *
	 * A full handler ID is a composite of the handler ID and a sub-handler ID, separated by a colon.
	 *
	 * @var string
	 */
	protected string $full_handler_id;

	/**
	 * The item holder. Changing this value after init is highly discouraged.
	 *
	 * @var TrackableItem
	 */
	protected TrackableItem $holder_item;

	/**
	 * The origin of the value. This is one of the ItemValueHandlerBase::ORIGIN__* constants.
	 *
	 * @var int
	 */
	protected int $origin;

	/**
	 * The value to handle.
	 *
	 * @var mixed
	 */
	protected $value = null;

	/**
	 * Value processor manager.
	 *
	 * @var ValueProcessorManager|null
	 */
	protected ?ValueProcessorManager $value_processor_manager = null;

	/**
	 * Call this when dependencies have been found and must be collected by the processDependencies method.
	 *
	 * @param TrackableItem $item The item to add as a dependency (requirement).
	 *
	 * @throws WpDeployException Could not get the item UUID.
	 */
	public function addItemDependency( TrackableItem $item ): void {
		$this->additional_trackable_item_dependencies[ $item->getFullItemId() ] = $item;
	}

	/**
	 * Extracts the base ID from a full handler ID.
	 *
	 * @param string $full_handler_id The full handler ID.
	 *
	 * @return string The base ID.
	 *
	 * @throws WpDeployException If the full handler ID is invalid.
	 */
	public static function getBaseIdFromFullId( string $full_handler_id ): string {
		$parts = explode( ':', $full_handler_id, 2 );

		if ( false === $parts || count( $parts ) === 0 ) {
			throw new WpDeployException( 'Invalid full handler ID: ' . $full_handler_id );
		}

		return $parts[0];
	}

	/**
	 * {@inheritDoc}
	 */
	public function getDependencyDefinitions(): array {
		$definitions   = parent::getDependencyDefinitions();
		$definitions[] = new RootDependencyDefinition( ValueProcessorManager::class, $this->value_processor_manager );

		return $definitions;
	}

	/**
	 * Gets the form build for this item value handler, if any.
	 *
	 * @return array|null The form build. Returns null if a form is unneeded.
	 */
	public static function getFormBuild(): ?array {
		return null;
	}

	/**
	 * Gets the full handler ID.
	 *
	 * This is the handler ID which is used in the tracking metadata tables to give a second level of identification
	 * for the value handlers. For example, the handler ID for the reference item value handler is "reference"
	 * but the full handler ID is "reference:options".
	 *
	 * @return string The full handler id, in the form "handler_id" or "handler_id:sub_handler_id".
	 *
	 * @throws WpDeployException If the full handler ID cannot be determined.
	 */
	public function getFullHandlerId(): string {
		if ( empty( $this->full_handler_id ) ) {
			throw new WpDeployException( 'No full handler ID set.' );
		}

		return $this->full_handler_id;
	}

	/**
	 * Gets the handler ID.
	 *
	 * @return string The handler ID.
	 */
	abstract public static function getHandlerBaseId(): string;

	/**
	 * Gets the item holder.
	 *
	 * @return TrackableItem The item holder.
	 */
	public function getHolder(): TrackableItem {
		return $this->holder_item;
	}

	/**
	 * Alias of getHandlerBaseId for the PlaceholderableTrait.
	 *
	 * @return string The same value as getHandlerBaseId.
	 */
	public static function getId(): string {
		return static::getHandlerBaseId();
	}

	/**
	 * The public label of the handler.
	 *
	 * @return string The label.
	 */
	abstract public static function getLabel(): string;

	/**
	 * Gets the short field name for this field.
	 *
	 * Use with caution to prevent name clashes.
	 *
	 * @return string The short field name.
	 */
	public function getShortFieldName(): string {
		$delimiter_position = strpos( $this->full_field_name, '.' );

		if ( false === $delimiter_position ) {
			// Already a short field_name, or it's an invalid name.
			return $this->full_field_name;
		}

		// Return everything after the delimiter.
		return substr( $this->full_field_name, $delimiter_position + 1 );
	}

	/**
	 * Gets the table and column names on which this handler operates.
	 *
	 * @param string $table_name The table name.
	 * @param string $column_name The column name.
	 * @param bool   $with_prefix Whether to include the DB prefix in the table name.
	 *
	 * @throws WpDeployException If the table and column names cannot be determined.
	 */
	public function getTableAndColumnNames( string &$table_name, string &$column_name, bool $with_prefix = true ): void {
		// The table name is determined by the full field name.
		$parts = explode( '.', $this->full_field_name, 2 );

		if ( count( $parts ) !== 2 ) {
			// This should never happen.
			throw new WpDeployException( 'Invalid full field name: ' . $this->full_field_name );
		}

		list ( $table_name, $column_name ) = $parts;

		if ( $with_prefix ) {
			$table_name = Database::getTrackingDatabaseHandle()->prefix . $table_name;
		}
	}

	/**
	 * Gets the unprocessed value to export/import.
	 *
	 * This is the value before it gets processed by processValue().
	 * Classes extending ItemValueHandlerBase should override this method
	 * to provide the unprocessed value instead of overriding getValue().
	 *
	 * @param int           $operation The operation for which to get the value.
	 *     See ItemValueHandlerBase::OPERATION__* constants.
	 * @param callable|null $progress_callback The callable to call on export/import events.
	 *
	 * @return mixed The unprocessed value specifically crafted for export/import by this class.
	 *
	 * @throws WpDeployException If the value could not be retrieved.
	 */
	protected function getUnprocessedValue( int $operation, ?callable $progress_callback = null ) {
		// This is the default implementation, which simply returns the value.
		return $this->value;
	}

	/**
	 * Gets the processed value to export/import.
	 *
	 * @param int           $operation The operation for which to get the value.
	 *     See ItemValueHandlerBase::OPERATION__* constants.
	 * @param callable|null $progress_callback The callable called on export/import events.
	 *
	 * @return mixed The data to export/import.
	 *
	 * @throws WpDeployException If the value could not be retrieved.
	 */
	public function getValue( int $operation, ?callable $progress_callback = null ) {
		return $this->processValue(
			$this->getUnprocessedValue( $operation, $progress_callback ),
			$operation,
			$progress_callback
		);
	}

	/**
	 * Inits this handler instance.
	 *
	 * @param TrackableItem $holder_item The holder of this value handler.
	 * @param string        $full_field_name The unprefixed full field name.
	 * @param mixed         $value The value(s) to init the handler with.
	 * @param int           $origin The origin of the value. See ItemValueHandlerBase::ORIGIN__* constants.
	 * @param FieldConfig   $field_config The field config. It contains info about the full handler ID and processors.
	 *
	 * @throws WpDeployException If the handler ID is invalid, or any other error occurs.
	 */
	public function init( TrackableItem $holder_item, string $full_field_name, $value, int $origin, FieldConfig $field_config ): void {
		$this->holder_item     = $holder_item;
		$this->full_field_name = $full_field_name;
		$this->origin          = $origin;
		$this->full_handler_id = $field_config->getFullValueHandlerId();

		// A null value is allowed.
		$this->value = $value;

		$index = 0;

		foreach ( $field_config->getProcessorConfig() as $processor_options ) {
			// The order in the config is the order of execution.
			$processor_options['order'] = ++$index;

			$this->value_processor_manager->appendProcessorInstanceTo( $processor_options, $this );
		}
	}

	/**
	 * Normalizes the form submitted data.
	 *
	 * @param array|null $to_normalize The data to normalize.
	 *
	 * @return array|null The normalized data.
	 */
	public static function normalizeFormSubmittedData( ?array $to_normalize = null ): ?array {
		if ( ! is_array( $to_normalize ) || empty( $to_normalize['value_handler'] ) ) {
			return null;
		}

		// The default implementation only transfers the "value_handler" key.
		return array(
			'value_handler' => $to_normalize['value_handler'],
		);
	}

	/**
	 * Processes the dependencies of this handler.
	 *
	 * This is where an ItemValueHandler can inform the containing TrackableItem of the dependencies it has.
	 *
	 * @return array<string,TrackableItem> The dependencies found.
	 *
	 * @throws WpDeployException If a dependency could not be processed.
	 */
	public function processDependencies(): array {
		// Get the value for export; this will trigger the nested processing of the dependencies.
		// Note: the processValue method is cached on subsequent calls with the same operation,
		// so it's safe to call it multiple times.
		$this->getValue( static::OPERATION__EXPORT );

		foreach ( $this->additional_trackable_item_dependencies as $collected_depends_on ) {
			$this->getHolder()->addItemDependency( $collected_depends_on );
		}

		return $this->additional_trackable_item_dependencies;
	}

	/**
	 * Processes the value using the ValueProcessor instances appended to this item.
	 *
	 * @param mixed         $value The value to process.
	 * @param int           $operation The operation for which to process the value.
	 *   See ItemValueHandlerBase::OPERATION__* constants.
	 * @param callable|null $progress_callback The callable called on export/import events.
	 *
	 * @return mixed The processed value.
	 *
	 * @throws WpDeployException If the value could not be processed.
	 */
	protected function processValue( $value, int $operation, ?callable $progress_callback = null ) {
		if ( ( $this->cached_result['operation'] ?? null ) === $operation ) {
			// The operation has already been processed before.
			return $this->cached_result['value'];
		}

		$value_processors = $this->getComponentsByClassName( ValueProcessorBase::class );

		// Sort by order value.
		usort(
			$value_processors,
			function ( ValueProcessorBase $a, ValueProcessorBase $b ) {
				return $a->getOrder() <=> $b->getOrder();
			}
		);

		if ( static::OPERATION__IMPORT__FINALIZE === $operation || static::OPERATION__IMPORT__PRESAVE === $operation ) {
			// Reverse the order of processors for import.
			// This is so that the processors are executed in the reverse order of export:
			// - export : A -> B -> C,
			// - import : C -> B -> A.
			$value_processors = array_reverse( $value_processors );
		}

		foreach ( $value_processors as $processor ) {
			/** @var ValueProcessorBase $processor */
			$value = $processor->process( $value, $operation, $progress_callback );
		}

		$this->cached_result = array(
			'operation' => $operation,
			'value'     => $value,
		);

		return $value;
	}
}
