<?php
/**
 * Special page which uses a ChangesList to show query results.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * 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, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 * http://www.gnu.org/copyleft/gpl.html
 *
 * @file
 * @ingroup SpecialPage
 */

/**
 * Special page which uses a ChangesList to show query results.
 * @todo Way too many public functions, most of them should be protected
 *
 * @ingroup SpecialPage
 */
abstract class ChangesListSpecialPage extends SpecialPage {
	/** @var string */
	protected $rcSubpage;

	/** @var FormOptions */
	protected $rcOptions;

	/** @var array */
	protected $customFilters;

	/**
	 * Main execution point
	 *
	 * @param string $subpage
	 */
	public function execute( $subpage ) {
		$this->rcSubpage = $subpage;

		$this->setHeaders();
		$this->outputHeader();
		$this->addModules();

		$rows = $this->getRows();
		$opts = $this->getOptions();
		if ( $rows === false ) {
			if ( !$this->including() ) {
				$this->doHeader( $opts, 0 );
				$this->getOutput()->setStatusCode( 404 );
			}

			return;
		}

		$batch = new LinkBatch;
		foreach ( $rows as $row ) {
			$batch->add( NS_USER, $row->rc_user_text );
			$batch->add( NS_USER_TALK, $row->rc_user_text );
			$batch->add( $row->rc_namespace, $row->rc_title );
			if ( $row->rc_source === RecentChange::SRC_LOG ) {
				$formatter = LogFormatter::newFromRow( $row );
				foreach ( $formatter->getPreloadTitles() as $title ) {
					$batch->addObj( $title );
				}
			}
		}
		$batch->execute();

		$this->webOutput( $rows, $opts );

		$rows->free();
	}

	/**
	 * Get the database result for this special page instance. Used by ApiFeedRecentChanges.
	 *
	 * @return bool|ResultWrapper Result or false
	 */
	public function getRows() {
		$opts = $this->getOptions();
		$conds = $this->buildMainQueryConds( $opts );

		return $this->doMainQuery( $conds, $opts );
	}

	/**
	 * Get the current FormOptions for this request
	 *
	 * @return FormOptions
	 */
	public function getOptions() {
		if ( $this->rcOptions === null ) {
			$this->rcOptions = $this->setup( $this->rcSubpage );
		}

		return $this->rcOptions;
	}

	/**
	 * Create a FormOptions object with options as specified by the user
	 *
	 * @param array $parameters
	 *
	 * @return FormOptions
	 */
	public function setup( $parameters ) {
		$opts = $this->getDefaultOptions();
		foreach ( $this->getCustomFilters() as $key => $params ) {
			$opts->add( $key, $params['default'] );
		}

		$opts = $this->fetchOptionsFromRequest( $opts );

		// Give precedence to subpage syntax
		if ( $parameters !== null ) {
			$this->parseParameters( $parameters, $opts );
		}

		$this->validateOptions( $opts );

		return $opts;
	}

	/**
	 * Get a FormOptions object containing the default options. By default returns some basic options,
	 * you might want to not call parent method and discard them, or to override default values.
	 *
	 * @return FormOptions
	 */
	public function getDefaultOptions() {
		$opts = new FormOptions();

		$opts->add( 'hideminor', false );
		$opts->add( 'hidebots', false );
		$opts->add( 'hideanons', false );
		$opts->add( 'hideliu', false );
		$opts->add( 'hidepatrolled', false );
		$opts->add( 'hidemyself', false );

		$opts->add( 'namespace', '', FormOptions::INTNULL );
		$opts->add( 'invert', false );
		$opts->add( 'associated', false );

		return $opts;
	}

	/**
	 * Get custom show/hide filters
	 *
	 * @return array Map of filter URL param names to properties (msg/default)
	 */
	protected function getCustomFilters() {
		if ( $this->customFilters === null ) {
			$this->customFilters = array();
			Hooks::run( 'ChangesListSpecialPageFilters', array( $this, &$this->customFilters ) );
		}

		return $this->customFilters;
	}

	/**
	 * Fetch values for a FormOptions object from the WebRequest associated with this instance.
	 *
	 * Intended for subclassing, e.g. to add a backwards-compatibility layer.
	 *
	 * @param FormOptions $opts
	 * @return FormOptions
	 */
	protected function fetchOptionsFromRequest( $opts ) {
		$opts->fetchValuesFromRequest( $this->getRequest() );

		return $opts;
	}

	/**
	 * Process $par and put options found in $opts. Used when including the page.
	 *
	 * @param string $par
	 * @param FormOptions $opts
	 */
	public function parseParameters( $par, FormOptions $opts ) {
		// nothing by default
	}

	/**
	 * Validate a FormOptions object generated by getDefaultOptions() with values already populated.
	 *
	 * @param FormOptions $opts
	 */
	public function validateOptions( FormOptions $opts ) {
		// nothing by default
	}

	/**
	 * Return an array of conditions depending of options set in $opts
	 *
	 * @param FormOptions $opts
	 * @return array
	 */
	public function buildMainQueryConds( FormOptions $opts ) {
		$dbr = $this->getDB();
		$user = $this->getUser();
		$conds = array();

		// It makes no sense to hide both anons and logged-in users. When this occurs, try a guess on
		// what the user meant and either show only bots or force anons to be shown.
		$botsonly = false;
		$hideanons = $opts['hideanons'];
		if ( $opts['hideanons'] && $opts['hideliu'] ) {
			if ( $opts['hidebots'] ) {
				$hideanons = false;
			} else {
				$botsonly = true;
			}
		}

		// Toggles
		if ( $opts['hideminor'] ) {
			$conds['rc_minor'] = 0;
		}
		if ( $opts['hidebots'] ) {
			$conds['rc_bot'] = 0;
		}
		if ( $user->useRCPatrol() && $opts['hidepatrolled'] ) {
			$conds['rc_patrolled'] = 0;
		}
		if ( $botsonly ) {
			$conds['rc_bot'] = 1;
		} else {
			if ( $opts['hideliu'] ) {
				$conds[] = 'rc_user = 0';
			}
			if ( $hideanons ) {
				$conds[] = 'rc_user != 0';
			}
		}
		if ( $opts['hidemyself'] ) {
			if ( $user->getId() ) {
				$conds[] = 'rc_user != ' . $dbr->addQuotes( $user->getId() );
			} else {
				$conds[] = 'rc_user_text != ' . $dbr->addQuotes( $user->getName() );
			}
		}

		// Namespace filtering
		if ( $opts['namespace'] !== '' ) {
			$selectedNS = $dbr->addQuotes( $opts['namespace'] );
			$operator = $opts['invert'] ? '!=' : '=';
			$boolean = $opts['invert'] ? 'AND' : 'OR';

			// Namespace association (bug 2429)
			if ( !$opts['associated'] ) {
				$condition = "rc_namespace $operator $selectedNS";
			} else {
				// Also add the associated namespace
				$associatedNS = $dbr->addQuotes(
					MWNamespace::getAssociated( $opts['namespace'] )
				);
				$condition = "(rc_namespace $operator $selectedNS "
					. $boolean
					. " rc_namespace $operator $associatedNS)";
			}

			$conds[] = $condition;
		}

		return $conds;
	}

	/**
	 * Process the query
	 *
	 * @param array $conds
	 * @param FormOptions $opts
	 * @return bool|ResultWrapper Result or false
	 */
	public function doMainQuery( $conds, $opts ) {
		$tables = array( 'recentchanges' );
		$fields = RecentChange::selectFields();
		$query_options = array();
		$join_conds = array();

		ChangeTags::modifyDisplayQuery(
			$tables,
			$fields,
			$conds,
			$join_conds,
			$query_options,
			''
		);

		if ( !$this->runMainQueryHook( $tables, $fields, $conds, $query_options, $join_conds,
			$opts )
		) {
			return false;
		}

		$dbr = $this->getDB();

		return $dbr->select(
			$tables,
			$fields,
			$conds,
			__METHOD__,
			$query_options,
			$join_conds
		);
	}

	protected function runMainQueryHook( &$tables, &$fields, &$conds,
		&$query_options, &$join_conds, $opts
	) {
		return Hooks::run(
			'ChangesListSpecialPageQuery',
			array( $this->getName(), &$tables, &$fields, &$conds, &$query_options, &$join_conds, $opts )
		);
	}

	/**
	 * Return a IDatabase object for reading
	 *
	 * @return IDatabase
	 */
	protected function getDB() {
		return wfGetDB( DB_SLAVE );
	}

	/**
	 * Send output to the OutputPage object, only called if not used feeds
	 *
	 * @param ResultWrapper $rows Database rows
	 * @param FormOptions $opts
	 */
	public function webOutput( $rows, $opts ) {
		if ( !$this->including() ) {
			$this->outputFeedLinks();
			$this->doHeader( $opts, $rows->numRows() );
		}

		$this->outputChangesList( $rows, $opts );
	}

	/**
	 * Output feed links.
	 */
	public function outputFeedLinks() {
		// nothing by default
	}

	/**
	 * Build and output the actual changes list.
	 *
	 * @param array $rows Database rows
	 * @param FormOptions $opts
	 */
	abstract public function outputChangesList( $rows, $opts );

	/**
	 * Set the text to be displayed above the changes
	 *
	 * @param FormOptions $opts
	 * @param int $numRows Number of rows in the result to show after this header
	 */
	public function doHeader( $opts, $numRows ) {
		$this->setTopText( $opts );

		// @todo Lots of stuff should be done here.

		$this->setBottomText( $opts );
	}

	/**
	 * Send the text to be displayed before the options. Should use $this->getOutput()->addWikiText()
	 * or similar methods to print the text.
	 *
	 * @param FormOptions $opts
	 */
	function setTopText( FormOptions $opts ) {
		// nothing by default
	}

	/**
	 * Send the text to be displayed after the options. Should use $this->getOutput()->addWikiText()
	 * or similar methods to print the text.
	 *
	 * @param FormOptions $opts
	 */
	function setBottomText( FormOptions $opts ) {
		// nothing by default
	}

	/**
	 * Get options to be displayed in a form
	 * @todo This should handle options returned by getDefaultOptions().
	 * @todo Not called by anything, should be called by something… doHeader() maybe?
	 *
	 * @param FormOptions $opts
	 * @return array
	 */
	function getExtraOptions( $opts ) {
		return array();
	}

	/**
	 * Return the legend displayed within the fieldset
	 * @todo This should not be static, then we can drop the parameter
	 * @todo Not called by anything, should be called by doHeader()
	 *
	 * @param IContextSource $context The object available as $this in non-static functions
	 * @return string
	 */
	public static function makeLegend( IContextSource $context ) {
		$user = $context->getUser();
		# The legend showing what the letters and stuff mean
		$legend = Html::openElement( 'dl' ) . "\n";
		# Iterates through them and gets the messages for both letter and tooltip
		$legendItems = $context->getConfig()->get( 'RecentChangesFlags' );
		if ( !( $user->useRCPatrol() || $user->useNPPatrol() ) ) {
			unset( $legendItems['unpatrolled'] );
		}
		foreach ( $legendItems as $key => $item ) { # generate items of the legend
			$label = isset( $item['legend'] ) ? $item['legend'] : $item['title'];
			$letter = $item['letter'];
			$cssClass = isset( $item['class'] ) ? $item['class'] : $key;

			$legend .= Html::element( 'dt',
				array( 'class' => $cssClass ), $context->msg( $letter )->text()
			) . "\n" .
			Html::rawElement( 'dd', array(),
				$context->msg( $label )->parse()
			) . "\n";
		}
		# (+-123)
		$legend .= Html::rawElement( 'dt',
			array( 'class' => 'mw-plusminus-pos' ),
			$context->msg( 'recentchanges-legend-plusminus' )->parse()
		) . "\n";
		$legend .= Html::element(
			'dd',
			array( 'class' => 'mw-changeslist-legend-plusminus' ),
			$context->msg( 'recentchanges-label-plusminus' )->text()
		) . "\n";
		$legend .= Html::closeElement( 'dl' ) . "\n";

		# Collapsibility
		$legend =
			'<div class="mw-changeslist-legend">' .
				$context->msg( 'recentchanges-legend-heading' )->parse() .
				'<div class="mw-collapsible-content">' . $legend . '</div>' .
			'</div>';

		return $legend;
	}

	/**
	 * Add page-specific modules.
	 */
	protected function addModules() {
		$out = $this->getOutput();
		// Styles and behavior for the legend box (see makeLegend())
		$out->addModuleStyles( 'mediawiki.special.changeslist.legend' );
		$out->addModules( 'mediawiki.special.changeslist.legend.js' );
	}

	protected function getGroupName() {
		return 'changes';
	}
}
