<?php

namespace CommonsMetadata;

use CommonsMetadata\Hooks\SkinAfterBottomScriptsHandler;
use File;
use FormatMetadata;
use LocalRepo;
use MediaWiki\Content\Content;
use MediaWiki\Content\Hook\ContentAlterParserOutputHook;
use MediaWiki\Context\DerivativeContext;
use MediaWiki\Context\IContextSource;
use MediaWiki\Hook\GetExtendedMetadataHook;
use MediaWiki\Hook\SkinAfterBottomScriptsHook;
use MediaWiki\Hook\ValidateExtendedMetadataCacheHook;
use MediaWiki\MediaWikiServices;
use MediaWiki\Parser\ParserOutput;
use MediaWiki\Title\Title;
use Skin;
use Wikimedia\Bcp47Code\Bcp47Code;

/**
 * Hook handler
 */
class HookHandler implements
	GetExtendedMetadataHook,
	ValidateExtendedMetadataCacheHook,
	ContentAlterParserOutputHook,
	SkinAfterBottomScriptsHook
{
	/**
	 * Metadata version. When getting metadata of a remote file via the API, sometimes
	 * we get the data generated by a CommonsMetadata extension installed at the remote,
	 * as well. We use this version number to keep track of whether that data is different
	 * from what would be generated here.
	 * @var float
	 */
	private const VERSION = 1.2;

	/**
	 * Hook handler for extended metadata
	 *
	 * @param array &$combinedMeta Metadata so far
	 * @param File $file The file object in question
	 * @param IContextSource $context Context. Used to select language
	 * @param bool $singleLang Get only target language, or all translations
	 * @param int &$maxCache How many seconds to cache the result
	 */
	public function onGetExtendedMetadata(
		&$combinedMeta, $file, $context, $singleLang, &$maxCache
	) {
		global $wgCommonsMetadataForceRecalculate;

		if (
			isset( $combinedMeta['CommonsMetadataExtension']['value'] )
			&& $combinedMeta['CommonsMetadataExtension']['value'] == self::VERSION
			&& !$wgCommonsMetadataForceRecalculate
		) {
			// This is a file from a remote API repo, and CommonsMetadata is installed on
			// the remote as well, and generates the same metadata format. We have nothing to do.
			return;
		} else {
			$combinedMeta['CommonsMetadataExtension'] = [
				'value' => self::VERSION,
				'source' => 'extension',
			];
		}

		$lang = $context->getLanguage();

		$templateParser = new TemplateParser();
		$templateParser->setMultiLanguage( !$singleLang );
		$fallbacks = MediaWikiServices::getInstance()->getLanguageFallback()->getAll( $lang->getCode() );
		array_unshift( $fallbacks, $lang->getCode() );
		$templateParser->setPriorityLanguages( $fallbacks );
		$templateParser->setArtistCreditSeparator( $context->msg( 'commonsmetadata-artistcredit-separator' )->text() );

		$dataCollector = new DataCollector();
		$dataCollector->setLanguage( $lang );
		$dataCollector->setMultiLang( !$singleLang );
		$dataCollector->setTemplateParser( $templateParser );
		$dataCollector->setLicenseParser( new LicenseParser() );

		$dataCollector->collect( $combinedMeta, $file );

		if ( !$file->getDescriptionTouched() ) {
			// Not all files provide the last update time of the description.
			// If that's the case, just cache blindly for a shorter period.
			$maxCache = 60 * 60 * 12;
		}
	}

	/**
	 * Hook to check if cache is stale
	 *
	 * @param string $timestamp Timestamp of when cache taken
	 * @param File $file The file metadata is for
	 * @return bool Is metadata still valid
	 */
	public function onValidateExtendedMetadataCache( $timestamp, $file ) {
		return // use cached value if...
			// we don't know when the file was last updated
			!$file->getDescriptionTouched()
			// or last update was before we cached it
			|| wfTimestamp( TS_UNIX, $file->getDescriptionTouched() )
				<= wfTimestamp( TS_UNIX, $timestamp );
	}

	/**
	 * Check HTML output of a file page to see if it has all the basic metadata, and
	 * add tracking categories if it does not.
	 * @param Content $content
	 * @param Title $title
	 * @param ParserOutput $parserOutput
	 */
	public function onContentAlterParserOutput(
		$content, $title, $parserOutput
	) {
		global $wgCommonsMetadataSetTrackingCategories;

		if (
			!$wgCommonsMetadataSetTrackingCategories
			|| !$title->inNamespace( NS_FILE )
			|| !$parserOutput->hasText()
			|| $content->getModel() !== CONTENT_MODEL_WIKITEXT
		) {
			return;
		}

		/*
		 * We also need to check if the file can be found. This is pretty straightforward, except
		 * for when a file gets moved: the old & new file details are cached, and cache is purged
		 * later on, in a DeferredUpdate.
		 * We could just `$repo->findFile( $title, [ 'ignoreRedirect' => true, 'latest' => true ] )`
		 * to force it to always check the database, but apart from file moves, the data in cache
		 * (if any) is usually just fine.
		 * Instead, we'll:
		 * * first test if `$title->isRedirect()`, to weed out the old (now renamed) title
		 * * attempt to fetch from cache, which should usually be fine
		 * * then fallback to DB, for files that have just been renamed
		 */
		$services = MediaWikiServices::getInstance();
		$trackingCategories = $services->getTrackingCategories();
		$repo = $services->getRepoGroup()->getLocalRepo();
		if ( $title->isRedirect() ) {
			return;
		}
		$file = $repo->findFile( $title, [ 'ignoreRedirect' => true ] );
		if ( $file === false ) {
			$file = $repo->findFile( $title, [ 'ignoreRedirect' => true, 'latest' => true ] );
			if ( $file === false ) {
				return;
			}
		}

		$langCode = $parserOutput->getLanguage() ?? $services->getContentLanguage();
		$dataCollector = self::getDataCollector( $langCode, true );

		$categoryKeys = $dataCollector->verifyAttributionMetadata( $parserOutput, $file );
		foreach ( $categoryKeys as $key ) {
			$trackingCategories->addTrackingCategory(
				$parserOutput,
				'commonsmetadata-trackingcategory-' . $key,
				$title
			);
		}
	}

	/**
	 * @param Bcp47Code $langCode
	 * @param bool $singleLang
	 * @return DataCollector
	 */
	private static function getDataCollector( Bcp47Code $langCode, $singleLang ) {
		$templateParser = new TemplateParser();
		$templateParser->setMultiLanguage( !$singleLang );
		$fallbacks = MediaWikiServices::getInstance()->getLanguageFallback()->getAll( $langCode->toBcp47Code() );
		array_unshift( $fallbacks, $langCode->toBcp47Code() );
		$templateParser->setPriorityLanguages( $fallbacks );
		$templateParser->setArtistCreditSeparator( wfMessage( 'commonsmetadata-artistcredit-separator' )->text() );

		$lang = MediaWikiServices::getInstance()->getLanguageFactory()->getLanguage( $langCode );

		$dataCollector = new DataCollector();
		$dataCollector->setLanguage( $lang );
		$dataCollector->setMultiLang( !$singleLang );
		$dataCollector->setTemplateParser( $templateParser );
		$dataCollector->setLicenseParser( new LicenseParser() );

		return $dataCollector;
	}

	/**
	 * Injects an inline JSON-LD script schema with image license info.
	 *
	 * See https://phabricator.wikimedia.org/T254039. This only adds the script
	 * to File pages.
	 *
	 * @param Skin $skin
	 * @param string &$html
	 */
	public function onSkinAfterBottomScripts( $skin, &$html ) {
		$title = $skin->getOutput()->getTitle();
		$isFilePage = $title->inNamespace( NS_FILE );

		if (
			!$title ||
			!$title->exists() ||
			!$isFilePage
		) {
			return;
		}

		$localRepo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo();

		// Get and prepare FormatMetadata object.
		$format = new FormatMetadata;
		$context = new DerivativeContext( $format->getContext() );
		// Language doesn't matter so just use en to improve performance.
		$format->setSingleLanguage( true );
		$context->setLanguage( 'en' );
		$format->setContext( $context );

		// Get URL for public domain page from config.
		$config = MediaWikiServices::getInstance()->getConfigFactory()->makeConfig( 'CommonsMetadata' );
		$publicDomainPageUrl = $config->get( 'CommonsMetadataPublicDomainPageUrl' );

		$handler = new SkinAfterBottomScriptsHandler( $format, $publicDomainPageUrl );

		$html .= $this->doSkinAfterBottomScripts(
			$localRepo,
			$handler,
			$title
		);
	}

	/**
	 * Get schema script html (or empty string).
	 *
	 * @param LocalRepo $localRepo
	 * @param SkinAfterBottomScriptsHandler $handler
	 * @param Title $title
	 * @return string
	 */
	public function doSkinAfterBottomScripts(
		LocalRepo $localRepo,
		SkinAfterBottomScriptsHandler $handler,
		Title $title
	) {
		$file = $localRepo->newFile( $title );
		return $handler->getSchemaElement( $title, $file );
	}
}
