/**
	 * Get tag dropdown select
	 * @param int $selected, selected level
	 * @return string
	 */
	public static function getTagMenu( $selected = '' ) {
		$s  = "<label for='wpRatingTag'>" . wfMsgHtml('readerfeedback-tagfilter') . "</label>&#160;";
		$s .= Xml::openElement( 'select', array('name' => 'ratingtag', 'id' => 'wpRatingTag') );
		foreach( ReaderFeedback::getFeedbackTags() as $tag => $weight ) {
			$s .= Xml::option( wfMsg( "readerfeedback-$tag" ), $tag, $selected===$tag );
		}
		$s .= Xml::closeElement('select')."\n";
		return $s;
	}
	protected function showPageList() {
		global $wgOut;
		$tags = ReaderFeedback::getFeedbackTags();
		$pager = new RatedPagesPager( $this, array(), $this->namespace, $this->tag, $this->tier );
		if( isset($tags[$this->tag]) && $pager->getNumRows() ) {
			$wgOut->addHTML( wfMsgExt('ratedpages-list', array('parse') ) );
			$wgOut->addHTML( $pager->getNavigationBar() );
			$wgOut->addHTML( $pager->getBody() );
			$wgOut->addHTML( $pager->getNavigationBar() );
		} elseif( $this->tag ) { // must select first...
			$wgOut->addHTML( wfMsgExt('ratedpages-none', array('parse') ) );
		}
	}
   	/**
	* Get a table of the vote totals for a page
	* @param Title $page
	* @param int $period, number of days back
	* @param array $add, optional vote to add on (used to visually avoid lag)
	* @param string $cache, optional param to not use cache
	* @return string HTML table
	*/	
	public static function getVoteAggregates(
		$page, $period, $add = array(), $cache = 'useCache'
	) {
		global $wgLang, $wgMemc;
		$votes = null;
		$now = time();
		$key = wfMemcKey( 'feedback', 'ratingtally', $page->getArticleId(), $period );
		// Check cache
		if( $cache == 'useCache' ) {
			$set = $wgMemc->get($key);
			// Cutoff is at the 24 hour mark due to the way the aggregate 
			// schema groups ratings by date for graphs.
			$cache_cutoff = $now - ($now % 86400);
			if( is_array($set) && count($set) == 2 ) {
				list($val,$time) = $set;
				$touched = wfTimestamp( TS_UNIX, RatingHistory::getTouched($page) );
				if( $time > $cache_cutoff && $time > $touched ) {
					$votes = $val;
				}
			}
		}
		// Do query, cache miss
		if( !isset($votes) ) {
			// Set cutoff time for period
			$dbr = wfGetDB( DB_SLAVE );
			$cutoff_unixtime = $now - ($period * 24 * 3600);
			// Use integral number of days to be consistent with graphs
			$cutoff_unixtime = $cutoff_unixtime - ($cutoff_unixtime % 86400);
			$cutoff = $dbr->addQuotes( $dbr->timestamp( $cutoff_unixtime ) );
			// Get the first revision possibly voted on in the range
			$firstRevTS = $dbr->selectField( 'revision',
				'rev_timestamp',
				array( 'rev_page' => $page->getArticleId(), "rev_timestamp <= $cutoff" ),
				__METHOD__,
				array( 'ORDER BY' => 'rev_timestamp DESC' )
			);
			// Find average, median...
			$res = $dbr->select( array( 'revision', 'reader_feedback' ),
				array( 'rfb_ratings' ),
				array( 'rev_page' => $page->getArticleId(),
					"rev_id = rfb_rev_id",
					"rfb_timestamp >= $cutoff",
					// Trigger INDEX usage
					"rev_timestamp >= ".$dbr->addQuotes($firstRevTS) ),
				__METHOD__,
				array( 'USE INDEX' => array('revision' => 'page_timestamp') )
			);
			$votes = array();
			foreach( ReaderFeedback::getFeedbackTags() as $tag => $w ) {
				$votes[$tag] = array( 0 => 0, 1 => 0, 2 => 0, 3 => 0, 4 => 0 );
			}
			// Read votes and tally the numbers
			foreach ( $res as $row ) {
				$dims = ReaderFeedback::expandRatings( $row->rfb_ratings );
				foreach( $dims as $tag => $val ) {
					if( isset($votes[$tag]) && isset($votes[$tag][$val]) ) {
						$votes[$tag][$val]++;
					}
				}
			}
			// Tack on $add for display (used to avoid cache/lag)
			foreach( $add as $tag => $val ) {
				if( isset($votes[$tag]) && isset($votes[$tag][$val]) ) {
					$votes[$tag][$val]++;
				}
			}
			$wgMemc->set( $key, array( $votes, $now ), 24*3600 );
		}
		// Output multi-column list
		$html = "<table class='rfb-reader_feedback_table' cellspacing='0'><tr>";
		foreach( ReaderFeedback::getFeedbackTags() as $tag => $w ) {
			// Get tag average...
			$dist = isset($votes[$tag]) ? $votes[$tag] : array();
			$count = array_sum($dist);
			if( $count ) {
				$ave = ($dist[0] + 2*$dist[1] + 3*$dist[2] + 4*$dist[3] + 5*$dist[4])/$count;
				$ave = round($ave,1);
			} else {
				$ave = '-'; // DIV by zero
			}
			$html .= '<td align="center"><b>'.wfMsgHtml("readerfeedback-$tag").'</b>&#160;&#160;'.
				'<sup>('.wfMsgHtml('ratinghistory-ave',$wgLang->formatNum($ave)).')</sup></td>';
		}
		$html .= '</tr><tr>';
		foreach( $votes as $dist ) {
			$html .= '<td><table>';
			$html .= '<tr><th align="left">'.wfMsgHtml('ratinghistory-table-rating').'</th>';
			for( $i = 1; $i <= 5; $i++ ) {
				$html .= "<td align='center' class='rfb-rating-option-".($i-1)."'>$i</td>";
			}
			$html .= '</tr><tr>';
			$html .= '<th align="left">'.wfMsgHtml("ratinghistory-table-votes").'</th>';
			$html .= '<td align="center">'.$dist[0].'</td>';
			$html .= '<td align="center">'.$dist[1].'</td>';
			$html .= '<td align="center">'.$dist[2].'</td>';
			$html .= '<td align="center">'.$dist[3].'</td>';
			$html .= '<td align="center">'.$dist[4].'</td>';
			$html .= "</tr></table></td>\n";
		}
		$html .= '</tr></table>';
		return $html;
	}
	 /**
	 * Adds a brief feedback form to a page.
	 * @param OutputPage $out
	 * @param bool $top, should this form always go on top?
	  * @param Title $title
	 */
	protected static function addQuickFeedback( &$data, $top = false, $title ) {
		global $wgOut, $wgUser, $wgFeedbackTags;
		# Are there any reader input tags?
		if( empty($wgFeedbackTags) ) {
			return false;
		}
		# Revision being displayed
		$id = $wgOut->getRevisionId();
		# Load required messages
		$reviewTitle = SpecialPage::getTitleFor( 'ReaderFeedback' );
		$action = $reviewTitle->getLocalUrl( 'action=submit' );
		$form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $action,
			'id' => 'mw-feedbackform' ) );
		$form .= Xml::openElement( 'fieldset', array('class' => 'feedback_reviewform noprint') );
		$form .= "<legend><strong>" . wfMsgHtml( 'readerfeedback' ) . "</strong></legend>\n";
		# Avoid clutter
		if( !$wgUser->isAllowed('review') ) {
			$form .= wfMsgExt( 'readerfeedback-text', array('parse') );
		}
		$form .= Xml::openElement( 'span', array('id' => 'mw-feedbackselects') );
		# Loop through all different flag types
		foreach( ReaderFeedback::getFeedbackTags() as $quality => $levels ) {
			$selected = ( isset($flags[$quality]) && $flags[$quality] > 0 ) ? $flags[$quality] : -1;
			$form .= "<b>" . Xml::label( wfMsgHtml("readerfeedback-$quality"), "wp$quality" ) . ":</b>";
			$attribs = array( 'name' => "wp$quality", 'id' => "wp$quality",
				'onchange' => "updateFeedbackForm()" );
			$form .= '&#160;' . Xml::openElement( 'select', $attribs );
			$levels = array_reverse($levels,true);
			foreach( $levels as $i => $name ) {
				$optionClass = array( 'class' => "rfb-rating-option-$i" );
				$form .= Xml::option( wfMsg("readerfeedback-level-$i"), $i, ($i == $selected), $optionClass ) ."\n";
			}
			$form .= Xml::option( wfMsg("readerfeedback-level-none"), -1, (-1 == $selected) ) ."\n";
			$form .= Xml::closeElement( 'select' )."\n";
		}
		$form .= Xml::closeElement( 'span' );
		$form .= Xml::submitButton( wfMsg('readerfeedback-submit'),
			array('id' => 'submitfeedback','accesskey' => wfMsg('readerfeedback-ak-review'),
			'title' => wfMsg('readerfeedback-tt-review').' ['.wfMsg('readerfeedback-ak-review').']' )
		);
		# Hidden params
		$form .= Html::hidden( 'title', $reviewTitle->getPrefixedText() ) . "\n";
		$form .= Html::hidden( 'target', $title->getPrefixedDBKey() ) . "\n";
		$form .= Html::hidden( 'oldid', $id ) . "\n";
		$form .= Html::hidden( 'validatedParams', ReaderFeedbackPage::validationKey( $id, $wgUser->getId() ) );
		$form .= Html::hidden( 'action', 'submit') . "\n";
		$form .= Html::hidden( 'wpEditToken', $wgUser->editToken() ) . "\n";
		# Honeypot input
		$form .= Xml::input( 'commentary', 12, '', array('style' => 'display:none;') ) . "\n";
		$form .= Xml::closeElement( 'fieldset' );
		$form .= Xml::closeElement( 'form' );
		if( $top ) {
			$wgOut->prependHTML( $form );
		} else {
			$data .= $form;
		}
		return true;
	}
	public static function AjaxReview( /*$args...*/ ) {
		global $wgUser;
		$args = func_get_args();
		// Basic permission check
		if( $wgUser->isAllowed( 'feedback' ) ) {
			if( $wgUser->isBlocked() ) {
				return '<err#><h2>' . wfMsgHtml('blockedtitle') . '</h2>' .
					wfMsg('badaccess-group0');
			}
		} else {
			return '<err#><strong>' . wfMsg('badaccess-group0') . '</<strong>';
		}
		if( wfReadOnly() ) {
			return '<err#><strong>' . wfMsg('formerror') . '</<strong>';
		}
		$tags = ReaderFeedback::getFeedbackTags();
		// Make review interface object
		$form = new ReaderFeedbackPage();
		$form->dims = array();
		$unsureCount = 0;
		$bot = false;
		// Each ajax url argument is of the form param|val.
		// This means that there is no ugly order dependance.
		foreach( $args as $arg ) {
			$set = explode('|',$arg,2);
			if( count($set) != 2 ) {
				return '<err#>' . wfMsg('formerror');
			}
			list($par,$val) = $set;
			switch( $par )
			{
				case "target":
					$form->page = Title::newFromURL( $val );
					if( is_null($form->page) || !ReaderFeedback::isPageRateable( $form->page ) ) {
						return '<err#>' . wfMsg('formerror');
					}
					break;
				case "oldid":
					$form->oldid = intval( $val );
					if( !$form->oldid ) {
						return '<err#>' . wfMsg('formerror');
					}
					break;
				case "validatedParams":
					$form->validatedParams = $val;
					break;
				case "wpEditToken":
					if( !$wgUser->matchEditToken( $val ) ) {
						return '<err#>' . wfMsg('formerror');
					}
					break;
				case "commentary": // honeypot value
					if( $val )
						$bot = true;
					break;
				default:
					$p = preg_replace( '/^wp/', '', $par ); // kill any "wp" prefix
					if( array_key_exists( $p, $tags ) ) {
						$form->dims[$p] = intval($val);
						if( $form->dims[$p] === null ) { // nothing sent at all :(
							return '<err#>' . wfMsg('formerror'); // bad range
						} elseif( $form->dims[$p] === -1 ) {
							$unsureCount++;
						}
					}
					break;
			}
		}
		// Missing params?
		if( count($form->dims) != count($tags) || $unsureCount >= count($form->dims) ) {
			return '<err#>' . wfMsg('formerror');
		}
		// Doesn't match up?
		if( $form->validatedParams != self::validationKey( $form->oldid, $wgUser->getId() ) ) {
			return '<err#>' . wfMsg('formerror');
		}
		$rhist = SpecialPage::getTitleFor( 'RatingHistory' );
		$graphLink = $rhist->getFullUrl( 'target='.$form->page->getPrefixedUrl() );
		$talk = $form->page->getTalkPage();

		$tallyTable = ReaderFeedback::getVoteAggregates( $form->page, 31, $form->dims );
		
		$dbw = wfGetDB( DB_MASTER );
		$dbw->begin();
		if( $bot ) {
			$ok = self::REVIEW_ERROR; // don't submit for mindless drones
		} else {
			$ok = $form->submit();
		}
		$dbw->commit();
		switch( $ok ) {
			case self::REVIEW_OK:
				return '<suc#>' .
					"<div class='plainlinks'>" .
						wfMsgExt( 'readerfeedback-success', array('parseinline'), 
							$form->page->getPrefixedText(), $graphLink,
							$talk->getFullUrl( 'action=edit&section=new' ) ) .
						'<h4>'.wfMsgHtml('ratinghistory-table')."</h4>\n$tallyTable" .
					"</div>";
			case self::REVIEW_DUP:
				return '<err#>' .
					"<div class='plainlinks'>" .
						wfMsgExt( 'readerfeedback-voted', array('parseinline'), 
							$form->page->getPrefixedText(), $graphLink,
							$talk->getFullUrl( 'action=edit&section=new' ) ) .
					"</div>";
			default:
				return '<err#>' .
					"<div class='plainlinks'>" .
						wfMsgExt( 'readerfeedback-error', array('parseinline'), 
							$form->page->getPrefixedText(), $graphLink,
							$talk->getFullUrl( 'action=edit&section=new' ) ) .
					"</div>";
		}
	}
	public function purgePage() {
		global $wgUploadDirectory;
		foreach( ReaderFeedback::getFeedbackTags() as $tag => $weight ) {
			$dir = "{$wgUploadDirectory}/graphs/".$this->page->getArticleId()."/{$tag}/";
			if( is_dir( $dir ) ) {
				$handle = opendir( $dir );
				if( $handle ) {
					while( false !== ( $file = readdir($handle) ) ) {
						@unlink("$dir/$file");
					}
					closedir( $handle );
				}
			}
			@rmdir( $dir );
		}
		return true;
	}