<?php

namespace MediaWiki\Extension\TemplateData;

use MediaWiki\MainConfigNames;
use MediaWiki\MediaWikiServices;
use MediaWiki\Status\Status;
use stdClass;
use Wikimedia\Rdbms\IReadableDatabase;

/**
 * Represents the information about a template,
 * coming from the JSON blob in the <templatedata> tags
 * on wiki pages.
 * @license GPL-2.0-or-later
 */
class TemplateDataBlob {

	protected string $json;
	protected Status $status;

	/**
	 * Parse and validate passed JSON and create a blob handling
	 * instance.
	 * Accepts and handles user-provided data.
	 *
	 * @param IReadableDatabase $db
	 * @param string $json
	 * @return self
	 */
	public static function newFromJSON( IReadableDatabase $db, string $json ): self {
		$lang = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::LanguageCode );
		if ( $db->getType() === 'mysql' ) {
			$tdb = new TemplateDataCompressedBlob( $json, $lang );
		} else {
			$tdb = new self( $json, $lang );
		}
		return $tdb;
	}

	/**
	 * Parse and validate passed JSON (possibly gzip-compressed) and create a blob handling
	 * instance.
	 *
	 * @param IReadableDatabase $db
	 * @param string $json
	 * @return self
	 */
	public static function newFromDatabase( IReadableDatabase $db, string $json ): self {
		// Handle GZIP compression. \037\213 is the header for GZIP files.
		if ( substr( $json, 0, 2 ) === "\037\213" ) {
			$json = gzdecode( $json );
		}
		return self::newFromJSON( $db, $json );
	}

	protected function __construct( string $json, string $lang ) {
		$deprecatedTypes = array_keys( TemplateDataNormalizer::DEPRECATED_PARAMETER_TYPES );
		$validator = new TemplateDataValidator( $deprecatedTypes );
		$this->status = $validator->validate( json_decode( $json ) );

		// If data is invalid, replace with the minimal valid blob.
		// This is to make sure that, if something forgets to check the status first,
		// we don't end up with invalid data in the database.
		$value = $this->status->getValue() ?? (object)[ 'params' => (object)[] ];

		$normalizer = new TemplateDataNormalizer( $lang );
		$normalizer->normalize( $value );

		// Don't bother storing the decoded object, it will always be cloned anyway
		$this->json = json_encode( $value );
	}

	/**
	 * Get a single localized string from an InterfaceText object.
	 *
	 * Uses the preferred language passed to this function, or one of its fallbacks,
	 * or the site content language, or its fallbacks.
	 *
	 * @param stdClass $text An InterfaceText object
	 * @param string $langCode Preferred language
	 * @return null|string Text value from the InterfaceText object or null if no suitable
	 *  match was found
	 */
	private function getInterfaceTextInLanguage( stdClass $text, string $langCode ): ?string {
		if ( isset( $text->$langCode ) ) {
			return $text->$langCode;
		}

		[ $userlangs, $sitelangs ] = MediaWikiServices::getInstance()->getLanguageFallback()
			->getAllIncludingSiteLanguage( $langCode );

		foreach ( $userlangs as $lang ) {
			if ( isset( $text->$lang ) ) {
				return $text->$lang;
			}
		}

		foreach ( $sitelangs as $lang ) {
			if ( isset( $text->$lang ) ) {
				return $text->$lang;
			}
		}

		// If none of the languages are found fallback to null. Alternatively we could fallback to
		// reset( $text ) which will return whatever key there is, but we should't give the user a
		// "random" language with no context (e.g. could be RTL/Hebrew for an LTR/Japanese user).
		return null;
	}

	public function getStatus(): Status {
		return $this->status;
	}

	/**
	 * @return stdClass
	 */
	public function getData() {
		// Return deep clone so callers can't modify data. Needed for getDataInLanguage().
		return json_decode( $this->json );
	}

	/**
	 * Get data with all InterfaceText objects resolved to a single string to the
	 * appropriate language.
	 *
	 * @param string $langCode Preferred language
	 * @return stdClass
	 */
	public function getDataInLanguage( string $langCode ): stdClass {
		$data = $this->getData();

		// Root.description
		if ( $data->description !== null ) {
			$data->description = $this->getInterfaceTextInLanguage( $data->description, $langCode );
		}

		foreach ( $data->params as $param ) {
			// Param.label
			if ( $param->label !== null ) {
				$param->label = $this->getInterfaceTextInLanguage( $param->label, $langCode );
			}

			// Param.description
			if ( $param->description !== null ) {
				$param->description = $this->getInterfaceTextInLanguage( $param->description, $langCode );
			}

			// Param.default
			if ( $param->default !== null ) {
				$param->default = $this->getInterfaceTextInLanguage( $param->default, $langCode );
			}

			// Param.example
			if ( $param->example !== null ) {
				$param->example = $this->getInterfaceTextInLanguage( $param->example, $langCode );
			}
		}

		foreach ( $data->sets as $setObj ) {
			$label = $this->getInterfaceTextInLanguage( $setObj->label, $langCode );
			if ( $label === null ) {
				// Contrary to other InterfaceTexts, set label is not optional. If we're here it
				// means the template data from the wiki doesn't contain either the user language,
				// site language or any of its fallbacks. Wikis should fix data that is in this
				// condition (TODO: Disallow during saving?). For now, fallback to whatever we can
				// get that does exist in the text object.
				$arr = (array)$setObj->label;
				$label = reset( $arr );
			}

			$setObj->label = $label;
		}

		return $data;
	}

	/**
	 * @return string JSON
	 */
	public function getJSONForDatabase(): string {
		return $this->json;
	}

}
