<?php
/**
 * Autoload Base Class
 *
 * @package errigal
 * @since 1.1.0
 */

namespace Errigal\Autoload;

defined( 'ABSPATH' ) || exit;

require get_template_directory() . '/inc/shared/class-errigal-utility.php';
require get_template_directory() . '/inc/shared/class-errigal-validator.php';

use Errigal\Shared\{
	Errigal_Utility as Utility,
	Errigal_Validator as Validator,
};

/**
 * This class is in charge of providing shared autoloading functions.
 *
 * @since 1.1.0
 */
abstract class Errigal_Autoload_Base {
	/**
	 * Namespace separator.
	 *
	 * @since 1.1.0
	 *
	 * @var string
	 */
	private const NAMESPACE_SEPARATOR = '\\';

	/**
	 * Directory separator.
	 *
	 * @since 1.1.0
	 *
	 * @var string
	 */
	private const DIRECTORY_SEPARATOR = '/';

	/**
	 * Default root class folder.
	 *
	 * @since 1.1.0
	 *
	 * @var string
	 */
	private const DEFAULT_ROOT_CLASS_FOLDER = 'inc';

	/**
	 * Class name suffix: Interface.
	 *
	 * @since 1.2.0
	 *
	 * @var string
	 */
	private const CLASS_NAME_SUFFIX_INTERFACE = 'Interface';

	/**
	 * Class name suffix: Trait.
	 *
	 * @since 1.2.0
	 *
	 * @var string
	 */
	private const CLASS_NAME_SUFFIX_TRAIT = 'Trait';

	/**
	 * File extension: PHP.
	 *
	 * @since 1.2.0
	 *
	 * @var string
	 */
	private const EXTENSION_PHP = '.php';

	/**
	 * File name prefix: Class.
	 *
	 * @since 1.2.0
	 *
	 * @var string
	 */
	private const FILE_NAME_PREFIX_CLASS = 'class-';

	/**
	 * File name prefix: None.
	 *
	 * @since 1.2.0
	 *
	 * @var string
	 */
	private const FILE_NAME_PREFIX_NONE = '';

	/**
	 * Class map.
	 *
	 * @since 1.2.0
	 *
	 * @var string
	 */
	private $class_map;

	/**
	 * Root namespace.
	 *
	 * @since 1.2.0
	 *
	 * @var string
	 */
	private $root_namespace;

	/**
	 * Root path.
	 *
	 * @since 1.2.0
	 *
	 * @var string
	 */
	private $root_path;

	/**
	 * Root class folder.
	 *
	 * @since 1.2.0
	 *
	 * @var string
	 */
	private $root_class_folder;

	/**
	 * Constructor.
	 *
	 * @since 1.1.0
	 */
	public function __construct() {
		if ( ! $this->initialise() ) {
			return;
		}

		spl_autoload_register( array( $this, 'autoload' ) );
	}

	/**
	 * Gets root namespace.
	 *
	 * @since 1.1.0
	 *
	 * @return string
	 */
	abstract protected function get_root_namespace();

	/**
	 * Gets root path for theme.
	 *
	 * @since 1.1.0
	 *
	 * @return string
	 */
	abstract protected function get_root_path();

	/**
	 * Gets class map.
	 *
	 * @since 1.1.0
	 *
	 * @return array
	 */
	abstract protected function get_class_map();

	/**
	 * Autoloads class file.
	 *
	 * @param string $class_name Fully qualified class name.
	 * @return void
	 */
	public function autoload( $class_name ) {
		if (
			! $this->is_theme_class( $class_name ) ||
			! Validator::is_non_empty_string( $this->root_path ) ||
			! Validator::is_non_empty_string( $this->root_class_folder )
		) {
			return;
		}

		$relative_file_path = $this->get_relative_file_path( $class_name );

		if ( ! Validator::is_non_empty_string( $relative_file_path ) ) {
			return;
		}

		$file_path = trailingslashit( $this->root_path ) .
			trailingslashit( $this->root_class_folder ) .
			$relative_file_path;

		if ( ! file_exists( $file_path ) ) {
			return;
		}

		require $file_path;
	}

	/**
	 * Gets root class folder.
	 *
	 * @since 1.1.0
	 *
	 * @return string
	 */
	protected function get_root_class_folder() {
		return self::DEFAULT_ROOT_CLASS_FOLDER;
	}

	/**
	 * Gets relative file path.
	 *
	 * @param string $class_name Fully qualified class name.
	 * @return string
	 */
	protected function get_relative_file_path( $class_name ) {
		if ( ! $this->is_theme_class( $class_name ) ) {
			return '';
		}

		if ( $this->is_class_mapped( $class_name ) ) {
			return $this->class_map[ $class_name ];
		}

		$path_parts = $this->get_path_parts( $class_name );

		if ( ! Validator::is_string_list( $path_parts ) ) {
			return '';
		}

		$file_name = $this->get_file_name( array_pop( $path_parts ) );

		if ( ! Validator::is_non_empty_string( $file_name ) ) {
			return '';
		}

		$file_directory = Utility::join(
			self::DIRECTORY_SEPARATOR,
			...$path_parts
		);

		$file_directory = Validator::is_non_empty_string( $file_directory )
			? trailingslashit( $file_directory )
			: '';

		return strtolower( $file_directory . sanitize_file_name( $file_name ) );
	}

	/**
	 * Initialises autoload.
	 *
	 * @since 1.2.0
	 *
	 * @return bool
	 */
	private function initialise() {
		$this->class_map         = $this->get_class_map();
		$this->root_namespace    = $this->get_root_namespace();
		$this->root_path         = $this->get_root_path();
		$this->root_class_folder = $this->get_root_class_folder();

		return Validator::is_associative_array( $this->class_map ) &&
			Validator::is_non_empty_string( $this->root_namespace ) &&
			Validator::is_non_empty_string( $this->root_path ) &&
			Validator::is_non_empty_string( $this->root_class_folder );
	}

	/**
	 * Determines whether class belongs to theme.
	 *
	 * @since 1.1.0
	 *
	 * @param string $class_name Fully qualified class name.
	 * @return bool
	 */
	private function is_theme_class( $class_name ) {
		if ( ! Validator::is_non_empty_string( $class_name ) ) {
			return false;
		}

		$root_namespace = strtok( $class_name, self::NAMESPACE_SEPARATOR );

		return $this->root_namespace === $root_namespace;
	}

	/**
	 * Determines whether class name is mapped.
	 *
	 * @since 1.2.0
	 *
	 * @param string $class_name Fully qualified class name.
	 * @return bool
	 */
	private function is_class_mapped( $class_name ) {
		$class_map = $this->class_map;

		return $this->is_theme_class( $class_name ) &&
			Validator::has_array_key( $class_map, $class_name ) &&
			Validator::is_non_empty_string( $class_map[ $class_name ] );
	}

	/**
	 * Gets path parts.
	 *
	 * @since 1.1.0
	 *
	 * @param string $class_name Fully qualified class name.
	 * @return string[]
	 */
	private function get_path_parts( $class_name ) {
		if ( ! $this->is_theme_class( $class_name ) ) {
			return array();
		}

		$class_name = $this->remove_root_namespace_prefix( $class_name );
		$class_name = $this->replace_separators( $class_name );

		if ( ! Validator::is_non_empty_string( $class_name ) ) {
			return array();
		}

		return explode( self::DIRECTORY_SEPARATOR, $class_name );
	}

	/**
	 * Removes root namespace prefix.
	 *
	 * @since 1.1.0
	 *
	 * @param string $class_name Fully qualified class name.
	 * @return string
	 */
	private function remove_root_namespace_prefix( $class_name ) {
		if (
			! $this->is_theme_class( $class_name ) ||
			! Validator::is_non_empty_string( $this->root_namespace )
		) {
			return '';
		}

		return str_replace(
			$this->root_namespace . self::NAMESPACE_SEPARATOR,
			Utility::SEPARATOR_EMPTY,
			$class_name
		);
	}

	/**
	 * Replaces separators.
	 *
	 * @since 1.1.0
	 *
	 * @param string $class_name_without_namespace_prefix Fully qualified class name without namespace prefix.
	 * @return string
	 */
	private function replace_separators( $class_name_without_namespace_prefix ) {
		if ( ! Validator::is_non_empty_string( $class_name_without_namespace_prefix ) ) {
			return '';
		}

		return str_replace(
			array(
				self::NAMESPACE_SEPARATOR,
				Utility::SEPARATOR_UNDERSCORE,
			),
			array(
				self::DIRECTORY_SEPARATOR,
				Utility::SEPARATOR_HYPHEN,
			),
			$class_name_without_namespace_prefix
		);
	}

	/**
	 * Gets file name.
	 *
	 * @since 1.2.0
	 *
	 * @param string $class_name Class name.
	 * @return string
	 */
	private function get_file_name( $class_name ) {
		if ( ! Validator::is_non_empty_string( $class_name ) ) {
			return '';
		}

		$file_name_prefix = $this->is_interface( $class_name ) || $this->is_trait( $class_name )
			? self::FILE_NAME_PREFIX_NONE
			: self::FILE_NAME_PREFIX_CLASS;

		return sanitize_file_name( $file_name_prefix . $class_name . self::EXTENSION_PHP );
	}

	/**
	 * Determines whether object is interface.
	 *
	 * @since 1.2.0
	 *
	 * @param string $class_name Class name.
	 * @return bool
	 */
	private function is_interface( $class_name ) {
		return Validator::string_ends_with(
			$class_name,
			self::CLASS_NAME_SUFFIX_INTERFACE
		);
	}

	/**
	 * Determines whether object is trait.
	 *
	 * @since 1.2.0
	 *
	 * @param string $class_name Class name.
	 * @return bool
	 */
	private function is_trait( $class_name ) {
		return Validator::string_ends_with(
			$class_name,
			self::CLASS_NAME_SUFFIX_TRAIT
		);
	}
}
