/**
	 * @return  string  html representation of question statistics
	 */
	public function displayStats( qp_SpecialPage $page, $pid ) {
		$ctrl = $this->ctrl;
		$current_title = $page->getTitle();
		$output = $this->displayHeader() .
			"<div class=\"qpoll\">\n" . "<table class=\"qdata\">\n" .
			qp_Renderer::displayRow(
				array_map( array( $this, 'categoryentities' ), $ctrl->CategorySpans ),
				array( 'class' => 'spans' ),
				'th',
				array( 'count' => 'colspan', 'name' => 0 )
			) .
			qp_Renderer::displayRow(
				array_map( array( $this, 'categoryentities' ), $ctrl->Categories ),
				array(),
				'th',
				array( 'name' => 0 )
			);
		# multiple choice polls doesn't use real spans, instead, every column is like "span"
		$spansUsed = count( $ctrl->CategorySpans ) > 0 || $ctrl->type == "multipleChoice";
		foreach ( $ctrl->ProposalText as $propkey => &$proposal_text ) {
			if ( isset( $ctrl->Votes[ $propkey ] ) ) {
				if ( $ctrl->Percents === null ) {
					$row = $ctrl->Votes[ $propkey ];
				} else {
					$row = $ctrl->Percents[ $propkey ];
					foreach ( $row as $catkey => &$cell ) {
						# Replace spaces with en spaces
						$formatted_cell = str_replace( " ", "&#8194;", sprintf( '%3d%%', intval( round( 100 * $cell ) ) ) );
						# only percents !=0 are displayed as link
						if ( $cell == 0.0 && $ctrl->question_id !== null ) {
							$cell = array( 0 => $formatted_cell, "style" => "color:gray" );
						} else {
							$cell = array( 0 => $page->qpLink( $current_title, $formatted_cell,
								array( "title" => wfMsgExt( 'qp_votes_count', array( 'parsemag' ), $ctrl->Votes[ $propkey ][ $catkey ] ) ),
								array( "action" => "qpcusers", "id" => $pid, "qid" => $ctrl->question_id, "pid" => $propkey, "cid" => $catkey ) ) );
						}
						if ( $spansUsed ) {
							if ( $ctrl->type == "multipleChoice" ) {
								$cell[ "class" ] = ( ( $catkey & 1 ) === 0 ) ? "spaneven" : "spanodd";
							} else {
								$cell[ "class" ] = ( ( $ctrl->Categories[ $catkey ][ "spanId" ] & 1 ) === 0 ) ? "spaneven" : "spanodd";
							}
						} else {
							$cell[ "class" ] = "stats";
						}
					}
				}
			} else {
				# this proposal has no statistics (no votes)
				$row = array_fill( 0, count( $ctrl->Categories ), '' );
			}
			$row[] = array( 0 => qp_Setup::entities( $proposal_text ), "style" => "text-align:left;" );
			$output .= qp_Renderer::displayRow( $row );
		}
		$output .= "</table>\n" . "</div>\n";
		return $output;
	}
	/**
	 * Add category as select / option list tagarray
	 */
	function addSelect( stdClass $elem, $className ) {
		if ( $elem->options[0] !== '' ) {
			# default element in select/option set always must be an empty option
			array_unshift( $elem->options, '' );
		}
		$html_options = array();
		# prepare the list of selected values
		if ( $elem->attributes['multiple'] !== null ) {
			# new lines are separator for selected multiple options
			$selected_values = explode( qp_Setup::SELECT_MULTIPLE_VALUES_SEPARATOR, $elem->value );
		} else {
			$selected_values = array( $elem->value );
		}
		# generate options list
		foreach ( $elem->options as $option ) {
			$html_option = array(
				'__tag' => 'option',
				'value' => qp_Setup::entities( $option ),
				qp_Setup::specialchars( $option )
			);
			if ( in_array( $option, $selected_values ) ) {
				$html_option['selected'] = 'selected';
			}
			$html_options[] = $html_option;
		}
		$select = array(
			'__tag' => 'select',
			# unique (poll_type,order_id,question,proposal,category) "coordinate" for javascript
			'id' => "{$this->id_prefix}c{$this->catId}",
			'class' => $className,
			'name' => $elem->name,
			$html_options
		);
		# multiple options 'name' attribute should have array hint []
		if ( $elem->attributes['multiple'] !== null ) {
			$select['multiple'] = 'multiple';
			$select['name'] .= '[]';
		}
		# determine visual height of select options list
		if ( ( $size = $elem->attributes['height'] ) !== 0 ) {
			if ( is_int( $size ) ) {
				if ( count( $elem->options ) < $size ) {
					$size = count( $elem->options );
				}
			} else { /* 'auto' */
				$size = count( $elem->options );
			}
			$select['size'] = $size;
		}
		$this->cell[] = $select;
		$this->catId++;
	}
	/**
	 * @return  string  html representation of user vote for Special:Pollresults output
	 */
	function displayUserVote() {
		$ctrl = $this->ctrl;
		$output = $this->displayHeader();
		$output .= "<div class=\"qpoll\">\n" . "<table class=\"qdata\">\n";
		foreach ( $ctrl->ProposalText as $propkey => &$serialized_tokens ) {
			if ( !is_array( $dbtokens = unserialize( $serialized_tokens ) ) ) {
				throw new MWException( 'dbtokens is not an array in ' . __METHOD__ );
			}
			$catId = 0;
			$row = array();
			foreach ( $dbtokens as &$token ) {
				if ( is_string( $token ) ) {
					# add a proposal part
					$row[] = array( '__tag' => 'span', 'class' => 'prop_part', qp_Setup::entities( $token ) );
				} elseif ( is_array( $token ) ) {
					# add a category definition with selected text answer (if any)
					# resulting category view tagarray
					$catview = array(
						'__tag' =>'span',
						'class' => 'cat_part',
						'' // text_answer
					);
					if ( array_key_exists( $propkey, $ctrl->ProposalCategoryId ) &&
						( $id_key = array_search( $catId, $ctrl->ProposalCategoryId[$propkey] ) ) !== false ) {
						if ( ( $text_answer = $ctrl->ProposalCategoryText[$propkey][$id_key] ) === '' ) {
							if ( count( $token ) === 1 ) {
								# indicate selected checkbox / radiobuttn
								$catview[0] = qp_Setup::RESULTS_CHECK_SIGN;
							}
						} else {
							# text answer is not empty;
							# try to extract select multiple, if any
							$text_answer = explode( qp_Setup::SELECT_MULTIPLE_VALUES_SEPARATOR, $text_answer );
							# place unused categories into the value of 'title' attribute
							$titleAttr = '';
							foreach ( $token as &$option ) {
								if ( !in_array( $option, $text_answer ) ) {
									if ( $titleAttr !== '' ) {
										$titleAttr .= ' | ';
									}
									$titleAttr .= qp_Setup::entities( $option );
								}
							}
							if ( count( $text_answer ) > 1 ) {
								# selected multiple values;
								# re-create the view for multiple category parts
								$catview = array();
								foreach ( $text_answer as $key => &$cat_part ) {
									$tag = array(
										'__tag' => 'span',
										'class' => 'cat_part',
										'title' => $titleAttr,
										qp_Setup::specialchars( $cat_part )
									);
									if ( in_array( $cat_part, $token ) ) {
											$tag['class'] .= ( $key % 2 === 0 ) ? ' cat_even' : ' cat_odd';
									} else {
										# add 'cat_unanswered' CSS class only to select multiple values
										$tag['class'] .= ' cat_unanswered';
									}
									if ( $key == 0 ) {
										$tag['class'] .= ' cat_first';
									}
									$catview[] = $tag;
								}
							} else {
								# text input or textarea
								$catview['title'] = $titleAttr;
								# note that count( $text_answer) here cannot be zero, because
								# explode() was performed on non-empty $text_answer
								$catview[0] = qp_Setup::specialchars( array_pop( $text_answer ) );
							}
						}
					} else {
						# many browsers trim the spaces between spans when the text node is empty;
						# use non-breaking space to prevent this
						$catview[0] = '&#160;';
						$catview['class'] .= ' cat_unanswered';
					}
					$row[] = $catview;
					# move to the next category (if any)
					$catId++;
				} else {
					throw new MWException( 'DB token has invalid type (' . gettype( $token ) . ') in ' . __METHOD__ );
				}
			}
			$output .= qp_Renderer::displayRow(
				array( $row ),
				array( 'class' => 'qdatatext' )
			);
		}
		$output .= "</table>\n" . "</div>\n";
		return $output;
	}
	/**
	 * Builds the question with fully parsed headers
	 *
	 * internally, the header is split into
	 *   main header (part inside curly braces) and
	 *   body header (categories and metacategories defitions)
	 *
	 * @param  $header : the text of question "main" header (common question and XML-like attrs)
	 * @param  $body   : the text of question body
	 *                   for tabular questions body begins with header line that defines
	 *                   categories and spans, followed by proposal lines)
	 * @return           question object with parsed headers
	 */
	function parseQuestionHeader( $header, $body ) {
		# parse questions common question and XML attributes
		$question = $this->parseMainHeader( $header );
		if ( $question->getState() != 'error' ) {
			# load previous user choice, when it's available and DB header is compatible with parsed header
			if ( !method_exists( $question, 'parseBody' ) ) {
				$question->setState( 'error', wfMsgHtml( 'qp_error_question_not_implemented', qp_Setup::entities( $question->mType ) ) );
			} elseif ( $body === '' ) {
				$question->setState( 'error', wfMsgHtml( 'qp_error_question_empty_body' ) );
			} else {
				# build $question->raws[]
				$question->splitRawProposals( $body );
				# parse the categories and spans (metacategories)
				$question->parseBodyHeader( $body );
			}
		}
		return $question;
	}
	function formatResult( $result ) {
		global $wgLang, $wgContLang;
		$link = "";
		if ( $result !== null ) {
			$uid = intval( $result->uid );
			$userName = $result->username;
			$userTitle = Title::makeTitleSafe( NS_USER, $userName );
			$user_link = $this->qpLink( $userTitle, $userName );
			$voice_link = $this->qpLink( $this->getTitle(), wfMsg( 'qp_voice_link' . ( $this->inverse ? "_inv" : "" ) ), array(), array( "id" => intval( $this->pid ), "uid" => $uid, "action" => "uvote" ) );
			$text_answer = ( $result->text_answer == '' ) ? '' : '<i>' . qp_Setup::entities( $result->text_answer ) . '</i>';
			$link = wfMsg( 'qp_results_line_qucl', $user_link, $voice_link, $text_answer );
		}
		return $link;
	}