<?php

use Wikimedia\TestingAccessWrapper;

/**
 * A test-only LocalisationCache that caches all data in memory for test speed.
 */
class TestLocalisationCache extends LocalisationCache {

	/**
	 * A cache of the parsed data for tests. Services are reset between every test, which forces
	 * localization to be recached between every test, which is unreasonably slow. As an
	 * optimization, we cache our data in a static member for tests.
	 *
	 * @var array
	 */
	private static $testingCache = [];

	private $selfAccess;

	public function __construct() {
		parent::__construct( ...func_get_args() );
		$this->selfAccess = TestingAccessWrapper::newFromObject( $this );
	}

	/**
	 * Recurse through the given array and replace every object by a scalar value that can be
	 * serialized as JSON to use as a hash key.
	 *
	 * @param array $arr
	 * @return array
	 */
	private static function hashiblifyArray( array $arr ): array {
		foreach ( $arr as $key => $val ) {
			if ( is_array( $val ) ) {
				$arr[$key] = self::hashiblifyArray( $val );
			} elseif ( is_object( $val ) ) {
				// spl_object_hash() may return duplicate values if an object is destroyed and a new
				// one gets its hash and happens to be registered in the same hook in the same
				// location. This seems unlikely, but let's be safe and maintain a reference so it
				// can't happen. (In practice, there are probably no objects in the hooks at all.)
				static $objects = [];
				if ( !in_array( $val, $objects, true ) ) {
					$objects[] = $val;
				}
				$arr[$key] = spl_object_hash( $val );
			}
		}
		return $arr;
	}

	public function recache( $code ) {
		// Test run performance is killed if we have to regenerate l10n for every test
		$cacheKey = sha1( json_encode( [
			$code,
			$this->selfAccess->options->get( 'ExtensionMessagesFiles' ),
			$this->selfAccess->options->get( 'MessagesDirs' ),
			// json_encode doesn't handle objects well
			self::hashiblifyArray( Hooks::getHandlers( 'LocalisationCacheRecacheFallback' ) ),
			self::hashiblifyArray( Hooks::getHandlers( 'LocalisationCacheRecache' ) ),
		] ) );
		if ( isset( self::$testingCache[$cacheKey] ) ) {
			$this->data[$code] = self::$testingCache[$cacheKey];
			foreach ( self::$testingCache[$cacheKey] as $key => $item ) {
				$loadedItems = $this->selfAccess->loadedItems;
				$loadedItems[$code][$key] = true;
				$this->selfAccess->loadedItems = $loadedItems;
			}
			return;
		}

		parent::recache( $code );

		if ( count( self::$testingCache ) > 4 ) {
			// Don't store more than a few $data's, they can add up to a lot of memory if
			// they're kept around for the whole test duration
			array_pop( self::$testingCache );
		}
		// Put the new one in front
		self::$testingCache = array_merge( [ $cacheKey => $this->data[$code] ], self::$testingCache );
	}
}
