<?php
/**
 * FANCIFY framework action and filters class
 *
 * @package SplendorStudio\Framework\API\Actions
 *
 * @since   1.0.0
 */
class Fancify_Lite_Actions extends Fancify_Lite_Model {
	
	/**
	 * construct class
	 *
	 * @since    1.0.0
	 */
	public function __construct() {
		$this->register_hook_callbacks();
	}

	/**
	 * Register necessary staff
	 *
	 * @since    1.0.0
	 */
	public function register_hook_callbacks() {
		//Initialize action globals
		global $fancify_lite_registered_actions;

		if ( ! isset( $fancify_lite_registered_actions ) ) {
			$fancify_lite_registered_actions = array(
				'added'    => array(),
				'modified' => array(),
				'removed'  => array(),
				'replaced' => array(),
			);
		}
	}

	/**
	 * Hooks a callback (function or method) to a specific action event.
	 *
	 * @since 1.0.0
	 * @return bool
	 */
	protected static function add_action( $id, $hook, $callback, $priority = 10, $args = 1 ) {
		$action = array(
			'hook'     => $hook,
			'callback' => $callback,
			'priority' => $priority,
			'args'     => $args,
		);

		$replaced_action = self::get_action( $id, 'replaced' );

		// If the ID is set to be "replaced", then replace that(those) parameter(s).
		if ( ! empty( $replaced_action ) ) {
			$action = array_merge( $action, $replaced_action );
		}

		$action = self::set_action( $id, $action, 'added', true );

		// If the ID is set to be "removed", then bail out.
		if ( self::get_action( $id, 'removed' ) ) {
			return false;
		}

		$modified_action = self::get_action( $id, 'modified' );

		// If the ID is set to be "modified", then modify that(those) parameter(s).
		if ( ! empty( $modified_action ) ) {
			$action = array_merge( $action, $modified_action );
		}

		return add_action( $action['hook'], $action['callback'], $action['priority'], $action['args'] );
	}

	/**
	 * Set {@see add_action()} using the callback argument as the action ID.
	 *
	 * @since 1.0.0
	 * @return bool
	 */
	public static function add_smart_action( $hook, $callback, $priority = 10, $args = 1 ) {
		return self::add_action( $callback, $hook, $callback, $priority, $args );
	}

	/**
	 * Modify one or more of the arguments for the given action, i.e. referenced by its FANCIFY's ID.
	 *
	 * @since 1.0.0
	 * @return bool
	 */
	protected static function modify_action( $id, $hook = null, $callback = null, $priority = null, $args = null ) {
		$action = self::build_action_array( $hook, $callback, $priority, $args );

		// If no changes were passed in, there's nothing to modify. Bail out.
		if ( empty( $action ) ) {
			return false;
		}

		$current_action = self::get_current_action( $id );

		// If the action is registered, let's remove it.
		if ( ! empty( $current_action ) ) {
			remove_action( $current_action['hook'], $current_action['callback'], $current_action['priority'] );
		}

		// Merge the modified parameters and register with FANCIFYs.
		$action = self::merge_action( $id, $action, 'modified' );

		// If there is no action to modify, bail out.
		if ( empty( $current_action ) ) {
			return false;
		}

		// Overwrite the modified parameters.
		$action = array_merge( $current_action, $action );

		return add_action( $action['hook'], $action['callback'], $action['priority'], $action['args'] );
	}

	/**
	 * Modify one or more of the arguments for the given action, i.e. referenced by its FANCIFY's ID.
	 *
	 * @since 1.0.0
	 * @return bool
	 */
	public static function modify_action_hook( $id, $hook ) {
		if ( empty( $hook ) || ! is_string( $hook ) ) {
			return false;
		}

		return self::modify_action( $id, $hook );
	}

	/**
	 * Modify the callback of the given action.
	 *
	 * @since 1.0.0
	 * @return bool
	 */
	public static function modify_action_callback( $id, $callback ) {
		if ( empty( $callback ) ) {
			return false;
		}

		return self::modify_action( $id, null, $callback );
	}

	/**
	 * Modify the priority of the given action.
	 *
	 * @since 1.0.0
	 * @return bool
	 */
	public static function modify_action_priority( $id, $priority ) {
		return self::modify_action( $id, null, null, $priority );
	}

	/**
	 * Modify the number of arguments of the given action.
	 *
	 * @since 1.0.0
	 * @return bool
	 */
	public static function modify_action_arguments( $id, $number_of_args ) {
		return self::modify_action( $id, null, null, null, $number_of_args );
	}

	/**
	 * Replace one or more of the arguments for the given action.
	 *
	 * @since 1.0.0
	 * @return bool
	 */
	protected static function replace_action( $id, $hook = null, $callback = null, $priority = null, $args = null ) {
		$action = self::build_action_array( $hook, $callback, $priority, $args );

		// If no changes were passed in, there's nothing to modify. Bail out.
		if ( empty( $action ) ) {
			return false;
		}

		// Set and get the latest "replaced" action.
		$action = self::merge_action( $id, $action, 'replaced' );

		// Modify the action.
		$is_modified = self::modify_action( $id, $hook, $callback, $priority, $args );

		// If there's a current action, merge it with the replaced one; else, it will be replaced when the original is added.
		if ( $is_modified ) {
			self::merge_action( $id, $action, 'added' );
		}

		return $is_modified;
	}

	/**
	 * Replace the action's event name (hook) for the given action, i.e. referenced by its FANCIFY's ID.
	 *
	 * @since 1.0.0
	 * @return bool
	 */
	public static function replace_action_hook( $id, $hook ) {
		if ( empty( $hook ) || ! is_string( $hook ) ) {
			return false;
		}

		return self::replace_action( $id, $hook );
	}

	/**
	 * Replace the callback of the given action, i.e. referenced by its FANCIFY's ID.
	 *
	 * @since 1.0.0
	 * @return bool
	 */
	public static function replace_action_callback( $id, $callback ) {
		if ( empty( $callback ) ) {
			return false;
		}

		return self::replace_action( $id, null, $callback );
	}

	/**
	 * Replace the priority of the given action, i.e. referenced by its FANCIFY's ID.
	 *
	 * @since 1.0.0
	 * @return bool
	 */
	public static function replace_action_priority( $id, $priority ) {
		return self::replace_action( $id, null, null, $priority );
	}

	/**
	 * Replace the number of arguments of the given action, i.e. referenced by its FANCIFY's ID.
	 *
	 * @return bool
	 */
	public static function replace_action_arguments( $id, $args ) {
		return self::replace_action( $id, null, null, null, $args );
	}

	/**
	 * Remove an action.
	 *
	 * @since 1.0.0
	 * @return bool
	 */
	public static function remove_action( $id ) {
		$action = self::get_current_action( $id );

		// When there is a current action, remove it.
		if ( ! empty( $action ) ) {
			remove_action( $action['hook'], $action['callback'], $action['priority'] );
		} else {
			// If the action is not registered yet, set it to a default configuration.
			$action = array(
				'hook'     => null,
				'callback' => null,
				'priority' => null,
				'args'     => null,
			);
		}

		// Store as "removed".
		return self::set_action( $id, $action, 'removed' );
	}

	/**
	 * Reset an action.
	 *
	 * @since 1.0.0
	 * @return bool
	 */
	public static function reset_action( $id ) {
		self::unset_action( $id, 'modified' );
		self::unset_action( $id, 'removed' );

		$action = self::get_action( $id, 'added' );

		// If there is no "added" action, bail out.
		if ( empty( $action ) ) {
			return false;
		}

		$current = self::get_current_action( $id );

		// If there's no current action, return the "added" action.
		if ( empty( $current ) ) {
			return $action;
		}

		remove_action( $current['hook'], $current['callback'], $current['priority'] );
		add_action( $action['hook'], $action['callback'], $action['priority'], $action['args'] );

		return $action;
	}

	/**
	 * Get the action's configuration for the given ID and status. Returns `false` if the action is not registered with
	 * FANCIFYs.
	 *
	 * @since 1.0.0
	 * @return array|bool
	 */
	public static function get_action( $id, $status ) {
		global $fancify_lite_registered_actions;

		$registered_actions = fancify_lite_get( $status, $fancify_lite_registered_actions );

		// If the status is empty, return false, as no actions are registered.
		if ( empty( $registered_actions ) ) {
			return false;
		}

		$id     = self::unique_action_id( $id );
		$action = fancify_lite_get( $id, $registered_actions );

		// If the action is empty, return false.
		if ( empty( $action ) ) {
			return false;
		}

		return $action;
	}

	/**
	 * Store the action's configuration for the given ID and status.
	 *
	 * @since 1.0.0
	 * @return array|mixed
	 */
	public static function set_action( $id, $action, $status, $overwrite = false ) {
		$id = self::unique_action_id( $id );

		// If not overwriting, return the registered action (if it's registered).
		if ( ! $overwrite ) {
			$registered_action = self::get_action( $id, $status );

			if ( ! empty( $registered_action ) ) {
				return $registered_action;
			}
		}

		if ( ! empty( $action ) || 'removed' === $status ) {
			global $fancify_lite_registered_actions;
			$fancify_lite_registered_actions[ $status ][ $id ] = $action;
		}

		return $action;
	}

	/**
	 * Unset the action's configuration for the given ID and status. Returns `false` if there are is no action
	 * registered with FANCIFYs actions for the given ID and status. Else, returns true when complete.
	 *
	 * @since 1.0.0
	 * @return bool
	 */
	public static function unset_action( $id, $status ) {
		$id = self::unique_action_id( $id );

		// Bail out if the ID is not registered for the given status.
		if ( false === self::get_action( $id, $status ) ) {
			return false;
		}

		global $fancify_lite_registered_actions;
		unset( $fancify_lite_registered_actions[ $status ][ $id ] );

		return true;
	}

	/**
	 * Merge the action's configuration and then store it for the given ID and status.
	 *
	 * @since 1.0.0
	 * @return array
	 */
	public static function merge_action( $id, array $action, $status ) {
		$id                = self::unique_action_id( $id );
		$registered_action = self::get_action( $id, $status );

		// If the action's configuration is already registered with FANCIFYs, merge the new configuration with it.
		if ( ! empty( $registered_action ) ) {
			$action = array_merge( $registered_action, $action );
		}

		// Now store/register it.
		return self::set_action( $id, $action, $status, true );
	}

	/**
	 * Get the current action, meaning get from the "added" and/or "modified" statuses.
	 *
	 * @since 1.0.0
	 * @return array|bool
	 */
	protected static function get_current_action( $id ) {
		// Bail out if the action is "removed".
		if ( self::get_action( $id, 'removed' ) ) {
			return false;
		}

		$added = self::get_action( $id, 'added' );

		// If there is no "added" action registered, bail out.
		if ( empty( $added ) ) {
			return false;
		}

		$modified = self::get_action( $id, 'modified' );

		// If the action is set to be modified, merge the changes and return the action.
		if ( ! empty( $modified ) ) {
			return array_merge( $added, $modified );
		}

		return $added;
	}

	/**
	 * Build the action's array for only the valid given arguments.
	 *
	 * @since 1.0.0
	 * @return array
	 */
	public static function build_action_array( $hook = null, $callback = null, $priority = null, $args = null ) {
		$action = array();

		if ( ! empty( $hook ) ) {
			$action['hook'] = $hook;
		}

		if ( ! empty( $callback ) ) {
			$action['callback'] = $callback;
		}

		foreach ( array( 'priority', 'args' ) as $arg_name ) {
			$arg = ${$arg_name};

			if ( is_numeric( $arg ) ) {
				$action[ $arg_name ] = (int) $arg;
			}
		}

		return $action;
	}

	/**
	 * Render action which can therefore be stored in a variable.
	 *
	 * @since 1.0.0
	 * @return bool|null|string
	 */
	public static function render_action( $hook ) {
		$args = func_get_args();

		// Return simple action if no sub-hook(s) is(are) set.
		if ( ! preg_match_all( '#\[(.*?)\]#', $args[0], $sub_hooks ) ) {
			return self::when_has_action_do_render( $args );
		}

		$output          = null;
		$prefix          = current( explode( '[', $args[0] ) );
		$variable_prefix = $prefix;
		$suffix          = preg_replace( '/^.*\]\s*/', '', $args[0] );

		// Base hook.
		$args[0] = $prefix . $suffix;

		// If the base hook is registered, render it.
		self::when_has_action_do_render( $args, $output );

		foreach ( (array) $sub_hooks[0] as $index => $sub_hook ) {
			$variable_prefix .= $sub_hook;

			$levels = array( $prefix . $sub_hook . $suffix );

			// Cascade sub-hooks.
			if ( $index > 0 ) {
				$levels[] = $variable_prefix . $suffix;
			}

			// Apply sub-hooks.
			foreach ( $levels as $level ) {
				$args[0] = $level;

				// If the level is registered, render it.
				self::when_has_action_do_render( $args, $output );

				// Apply filter without square brackets for backwards compatibility.
				$args[0] = preg_replace( '#(\[|\])#', '', $args[0] );

				// If the backwards compatible $args[0] is registered, render it.
				self::when_has_action_do_render( $args, $output );
			}
		}

		return $output;
	}

	/**
	 * Render all hooked action callbacks by firing {@see do_action()}.  The output is captured in the buffer and then
	 * returned.
	 *
	 * @since 1.0.0
	 * @return string|bool
	 */
	public static function when_has_action_do_render( array $args, &$output = '' ) {
		if ( ! has_action( $args[0] ) ) {
			return false;
		}

		ob_start();
		call_user_func_array( 'do_action', $args );
		$output .= ob_get_clean();

		return $output;
	}

	/**
	 * Make sure the action ID is unique.
	 *
	 * @since 1.0.0
	 * @return array|string
	 */
	public static function unique_action_id( $callback ) {
		if ( is_string( $callback ) ) {
			return $callback;
		}

		if ( is_object( $callback ) ) {
			$callback = array( $callback, '' );
		} else {
			$callback = (array) $callback;
		}

		// Treat object.
		if ( is_object( $callback[0] ) ) {

			if ( function_exists( 'spl_object_hash' ) ) {
				return spl_object_hash( $callback[0] ) . $callback[1];
			}

			return get_class( $callback[0] ) . $callback[1];
		}

		// Treat static method.
		if ( is_string( $callback[0] ) ) {
			return $callback[0] . '::' . $callback[1];
		}

		return md5( $callback );
	}
}
