<?php
/**
 * @package The_SEO_Framework\API
 */

namespace {
	defined( 'THE_SEO_FRAMEWORK_PRESENT' ) or die;
}

/**
 * The SEO Framework plugin
 * Copyright (C) 2018 - 2022 Sybre Waaijer, CyberWire B.V. (https://cyberwire.nl/)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3 as published
 * by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

namespace {
	/**
	 * Returns the class from cache.
	 *
	 * This is the recommended way of calling the class, if needed.
	 * Call this after action 'plugins_loaded' priority 5 otherwise you'll cause
	 * unforeseen issues.
	 *
	 * @since 4.2.0
	 * @see `the_seo_framework()` alias.
	 * @api
	 *
	 * @return null|object The plugin class object.
	 */
	function tsf() {
		return The_SEO_Framework\_init_tsf();
	}

	/**
	 * Returns the class from cache.
	 *
	 * This is the recommended way of calling the class, if needed.
	 * Call this after action 'plugins_loaded' priority 5 otherwise you'll cause
	 * unforeseen issues.
	 *
	 * @since 2.2.5
	 * @see `tsf()` alias.
	 * @api
	 *
	 * @return null|object The plugin class object.
	 */
	function the_seo_framework() {
		return The_SEO_Framework\_init_tsf();
	}

	/**
	 * Returns the database version of TSF.
	 *
	 * @since 3.1.0
	 * @since 3.1.2 Now casts to string.
	 * @api
	 *
	 * @return string The database version. '0' if version isn't found.
	 */
	function the_seo_framework_db_version() {
		return (string) get_option( 'the_seo_framework_upgraded_db_version', '0' );
	}

	/**
	 * Returns the facade class name from cache.
	 *
	 * @since 2.7.0
	 * @since 2.8.0 Added `did_action()` check.
	 * @since 4.2.0 Removed memoization.
	 * @api
	 *
	 * @return string|bool The SEO Framework class name. False if The SEO Framework isn't loaded (yet).
	 */
	function the_seo_framework_class() {

		// did_action() returns true for current action match, too.
		if ( ! did_action( 'plugins_loaded' ) )
			return false;

		return get_class( tsf() );
	}
}

namespace The_SEO_Framework {
	/**
	 * Determines if the method or function has already run.
	 *
	 * @since 4.2.3
	 * @api
	 * @todo make $caller optional and use debug_backtrace()?
	 *
	 * @param string $caller The method or function that calls this.
	 * @return bool True if already called, false otherwise.
	 */
	function has_run( $caller ) {
		static $ran = [];
		return $ran[ $caller ] ?? ! ( $ran[ $caller ] = true );
	}

	/**
	 * Stores and returns memoized values for the caller.
	 *
	 * This method is not forward-compatible with PHP: It expects values it doesn't want populated,
	 * instead of filtering what's actually useful for memoization. For example, it expects `file`
	 * and `line` from debug_backtrace() -- those are expected to be dynamic from the caller, and
	 * we set them to `0` to prevent a few opcode calls, rather than telling which array indexes
	 * we want exactly. The chance this failing in a future update is slim, for all useful data of
	 * the callee is given already via debug_backtrace().
	 * We also populate the `args` value "manually" for it's faster than using debug_backtrace()'s
	 * `DEBUG_BACKTRACE_PROVIDE_OBJECT` option.
	 *
	 * We should keep a tap on debug_backtrace changes. Hopefully, they allow us to ignore
	 * more than just args.
	 *
	 * This method does not memoize the object via debug_backtrace. This means that the
	 * objects will have values memoized cross-instantiations.
	 *
	 * Example usage:
	 * ```
	 * function expensive_call( $arg ) {
	 *     print( "expensive $arg!" );
	 *     return $arg * 2;
	 * }
	 * function my_function( $arg ) {
	 *    return memo( null, $arg );
	 *        ?? memo( expensive_call( $arg ), $arg );
	 * }
	 * my_function( 1 ); // prints "expensive 1!", returns 2.
	 * my_function( 1 ); // returns 2.
	 * my_function( 2 ); // prints "expensive 2!", returns 4.
	 *
	 * function test() {
	 *     return memo() ?? memo( expensive_call( 42 ) );
	 * }
	 * test(); // prints "expensive 42", returns 84.
	 * test(); // returns 84.
	 * ```
	 *
	 * @since 4.2.0
	 * @see umemo() -- sacrifices cleanliness for performance.
	 * @see fmemo() -- sacrifices everything for readability.
	 * @api
	 *
	 * @param mixed $value_to_set The value to set.
	 * @param mixed ...$args      Extra arguments, that are used to differentiaty callbacks.
	 *                            Arguments may not contain \Closure()s.
	 * @return mixed : {
	 *    mixed The cached value if set and $value_to_set is null.
	 *       null When no value has been set.
	 *       If $value_to_set is set, the new value.
	 * }
	 */
	function memo( $value_to_set = null, ...$args ) {

		static $memo = [];

		// phpcs:ignore, WordPress.PHP.DiscouragedPHPFunctions -- No objects inserted, nor ever unserialized.
		$hash = serialize(
			[
				'args' => $args,
				'file' => 0,
				'line' => 0,
			]
			// phpcs:ignore, WordPress.PHP.DevelopmentFunctions -- This is the only efficient way.
			+ debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS, 2 )[1]
		);

		if ( isset( $value_to_set ) )
			return $memo[ $hash ] = $value_to_set;

		return $memo[ $hash ] ?? null;
	}

	/**
	 * Stores and returns memoized values for the caller.
	 * This is 10 times faster than memo(), but requires from you a $key.
	 *
	 * We're talking milliseconds over thousands of iterations, though.
	 *
	 * Example usage:
	 * ```
	 * function expensive_call( $arg ) {
	 *     print( "expensive $arg!" );
	 *     return $arg * 2;
	 * }
	 * function my_function( $arg ) {
	 *    return umemo( __METHOD__, null, $arg );
	 *        ?? umemo( __METHOD__, expensive_call( $arg ), $arg );
	 * }
	 * my_function( 1 ); // prints "expensive 1!", returns 2.
	 * my_function( 1 ); // returns 2.
	 * my_function( 2 ); // prints "expensive 2!", returns 4.
	 * ```
	 *
	 * @since 4.2.0
	 * @see memo() -- sacrifices performance for cleanliness.
	 * @see fmemo() -- sacrifices everything for readability.
	 * @api
	 *
	 * @param string $key          The key you want to use to memoize. It's best to use the method name.
	 *                             You can share a unique key between various functions.
	 * @param mixed  $value_to_set The value to set.
	 * @param mixed  ...$args      Extra arguments, that are used to differentiate callbacks.
	 *                             Arguments may not contain \Closure()s.
	 * @return mixed : {
	 *    mixed The cached value if set and $value_to_set is null.
	 *       null When no value has been set.
	 *       If $value_to_set is set, the new value.
	 * }
	 */
	function umemo( $key, $value_to_set = null, ...$args ) {

		static $memo = [];

		// phpcs:ignore, WordPress.PHP.DiscouragedPHPFunctions -- No objects are inserted, nor is this ever unserialized.
		$hash = serialize( [ $key, $args ] );

		if ( isset( $value_to_set ) )
			return $memo[ $hash ] = $value_to_set;

		return $memo[ $hash ] ?? null;
	}

	/**
	 * Stores and returns memoized values for the Closure caller. This helps wrap
	 * a whole function inside a single memoization call.
	 *
	 * This method does not memoize the object via debug_backtrace. This means that the
	 * objects will have values memoized cross-instantiations.
	 *
	 * Example usage, PHP7.4+:
	 * ```
	 * function my_function( $arg ) { return fmemo( fn() => print( $arg ) + 5 ); }
	 * my_function( 1 ); // prints '1', returns 6.
	 * my_function( 1 ); // does not print, returns 6.
	 * ```
	 * Arrow functions are neat with this for they automatically register only necessary arguments to fmemo().
	 * This way, callers of my_function() won't bust the cache by sending unregistered superfluous arguments.
	 *
	 * ```
	 * function printer() { print( 69 ); }
	 * function print_once() { fmemo( 'printer' ); }
	 * print_once(); // 69
	 * print_once(); // *cricket noises*
	 * ```
	 *
	 * @since 4.2.0
	 * @see memo() -- sacrifices performance for cleanliness.
	 * @see umemo() -- sacrifices cleanliness for performance.
	 * @ignore We couldn't find a use for this... yet. Probably once we support only PHP7.4+
	 * @api
	 *
	 * @param \Closure $fn The Closure or function to memoize.
	 * @return mixed : {
	 *    mixed The cached value if set and $value_to_set is null.
	 *       null When no value has been set.
	 *       If $value_to_set is set, the new value.
	 * }
	 */
	function fmemo( $fn ) {

		static $memo = [];

		// phpcs:ignore, WordPress.PHP.DiscouragedPHPFunctions -- This is never unserialized.
		$hash = serialize(
			[
				'file' => '',
				'line' => 0,
			]
			// phpcs:ignore, WordPress.PHP.DevelopmentFunctions -- This is the only efficient way.
			+ debug_backtrace( 0, 2 )[1]
		);

		// Normally, I try to avoid NOTs for they add (tiny) overhead. Here, I chose readability over performance.
		if ( ! isset( $memo[ $hash ] ) ) {
			// Store the result of the function. If that's null/void, store hash.
			$memo[ $hash ] = \call_user_func( $fn ) ?? $hash;
		}

		return $memo[ $hash ] === $hash ? null : $memo[ $hash ];
	}
}
