<?php
	
/*
	Question2Answer (c) Gideon Greenspan

	http://www.question2answer.org/

	
	File: qa-include/qa-app-recalc.php
	Version: See define()s at top of qa-include/qa-base.php
	Description: Managing database recalculations (clean-up operations) and status messages


	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.

	More about this license: http://www.question2answer.org/license.php
*/
	
/*
	A full list of redundant (non-normal) information in the database that can be recalculated:
	
	Recalculated in doreindexcontent:
	================================
	^titlewords (all): index of words in titles of posts
	^contentwords (all): index of words in content of posts
	^tagwords (all): index of words in tags of posts (a tag can contain multiple words)
	^posttags (all): index tags of posts
	^words (all): list of words used for indexes
	^options (title=cache_*): cached values for various things (e.g. counting questions)
	
	Recalculated in dorecountposts:
	==============================
	^posts (upvotes, downvotes, netvotes, hotness, acount, amaxvotes, flagcount): number of votes, hotness, answers, answer votes, flags
	
	Recalculated in dorecalcpoints:
	===============================
	^userpoints (all except bonus): points calculation for all users
	^options (title=cache_userpointscount):
	
	Recalculated in dorecalccategories:
	===================================
	^posts (categoryid): assign to answers and comments based on their antecedent question
	^posts (catidpath1, catidpath2, catidpath3): hierarchical path to category ids (requires QA_CATEGORY_DEPTH=4)
	^categories (qcount): number of (visible) questions in each category
	^categories (backpath): full (backwards) path of slugs to that category
	
	Recalculated in dorebuildupdates:
	=================================
	^sharedevents (all): per-entity event streams (see big comment in qa-db-favorites.php)
	^userevents (all): per-subscriber event streams
	
	[but these are not entirely redundant since they can contain historical information no longer in ^posts]
*/

	if (!defined('QA_VERSION')) { // don't allow this page to be requested directly from browser
		header('Location: ../');
		exit;
	}

	require_once QA_INCLUDE_DIR.'qa-db-recalc.php';
	require_once QA_INCLUDE_DIR.'qa-db-post-create.php';
	require_once QA_INCLUDE_DIR.'qa-db-points.php';
	require_once QA_INCLUDE_DIR.'qa-db-selects.php';
	require_once QA_INCLUDE_DIR.'qa-db-admin.php';
	require_once QA_INCLUDE_DIR.'qa-db-users.php';
	require_once QA_INCLUDE_DIR.'qa-app-options.php';
	require_once QA_INCLUDE_DIR.'qa-app-post-create.php';
	require_once QA_INCLUDE_DIR.'qa-app-post-update.php';


	function qa_recalc_perform_step(&$state)
/*
	Advance the recalculation operation represented by $state by a single step.
	$state can also be the name of a recalculation operation on its own.
*/
	{
		$continue=false;
		
		@list($operation, $length, $next, $done)=explode(',', $state);
		
		switch ($operation) {
			case 'doreindexcontent':
				qa_recalc_transition($state, 'doreindexcontent_pagereindex');
				break;
				
			case 'doreindexcontent_pagereindex':
				$pages=qa_db_pages_get_for_reindexing($next, 10);
				
				if (count($pages)) {
					require_once QA_INCLUDE_DIR.'qa-app-format.php';
					
					$lastpageid=max(array_keys($pages));
					
					foreach ($pages as $pageid => $page)
						if (!($page['flags'] & QA_PAGE_FLAGS_EXTERNAL)) {
							$searchmodules=qa_load_modules_with('search', 'unindex_page');
							foreach ($searchmodules as $searchmodule)
								$searchmodule->unindex_page($pageid);
								
							$searchmodules=qa_load_modules_with('search', 'index_page');
							if (count($searchmodules)) {
								$indextext=qa_viewer_text($page['content'], 'html');

								foreach ($searchmodules as $searchmodule)
									$searchmodule->index_page($pageid, $page['tags'], $page['heading'], $page['content'], 'html', $indextext);
							}
						}
						
					$next=1+$lastpageid;
					$done+=count($pages);
					$continue=true;
				
				} else
					qa_recalc_transition($state, 'doreindexcontent_postcount');
				break;
			
			case 'doreindexcontent_postcount':
				qa_db_qcount_update();
				qa_db_acount_update();
				qa_db_ccount_update();

				qa_recalc_transition($state, 'doreindexcontent_postreindex');
				break;
				
			case 'doreindexcontent_postreindex':
				$posts=qa_db_posts_get_for_reindexing($next, 10);
				
				if (count($posts)) {
					require_once QA_INCLUDE_DIR.'qa-app-format.php';

					$lastpostid=max(array_keys($posts));
					
					qa_db_prepare_for_reindexing($next, $lastpostid);
					qa_suspend_update_counts();
		
					foreach ($posts as $postid => $post) {
						qa_post_unindex($postid);
						qa_post_index($postid, $post['type'], $post['questionid'], $post['parentid'], $post['title'], $post['content'],
							$post['format'], qa_viewer_text($post['content'], $post['format']), $post['tags'], $post['categoryid']);
					}
					
					$next=1+$lastpostid;
					$done+=count($posts);
					$continue=true;

				} else {
					qa_db_truncate_indexes($next);
					qa_recalc_transition($state, 'doreindexposts_wordcount');
				}
				break;
				
			case 'doreindexposts_wordcount':
				$wordids=qa_db_words_prepare_for_recounting($next, 1000);
				
				if (count($wordids)) {
					$lastwordid=max($wordids);
					
					qa_db_words_recount($next, $lastwordid);
					
					$next=1+$lastwordid;
					$done+=count($wordids);
					$continue=true;
			
				} else {
					qa_db_tagcount_update(); // this is quick so just do it here
					qa_recalc_transition($state, 'doreindexposts_complete');
				}
				break;
				
			case 'dorecountposts':
				qa_recalc_transition($state, 'dorecountposts_postcount');
				break;
				
			case 'dorecountposts_postcount':
				qa_db_qcount_update();
				qa_db_acount_update();
				qa_db_ccount_update();
				qa_db_unaqcount_update();
				qa_db_unselqcount_update();

				qa_recalc_transition($state, 'dorecountposts_votecount');
				break;
				
			case 'dorecountposts_votecount':
				$postids=qa_db_posts_get_for_recounting($next, 1000);
				
				if (count($postids)) {
					$lastpostid=max($postids);
					
					qa_db_posts_votes_recount($next, $lastpostid);
					
					$next=1+$lastpostid;
					$done+=count($postids);
					$continue=true;

				} else
					qa_recalc_transition($state, 'dorecountposts_acount');
				break;
				
			case 'dorecountposts_acount':
				$postids=qa_db_posts_get_for_recounting($next, 1000);
				
				if (count($postids)) {
					$lastpostid=max($postids);
					
					qa_db_posts_answers_recount($next, $lastpostid);
					
					$next=1+$lastpostid;
					$done+=count($postids);
					$continue=true;

				} else {
					qa_db_unupaqcount_update();
					qa_recalc_transition($state, 'dorecountposts_complete');
				}
				break;
			
			case 'dorecalcpoints':
				qa_recalc_transition($state, 'dorecalcpoints_usercount');
				break;
				
			case 'dorecalcpoints_usercount':
				qa_db_userpointscount_update(); // for progress update - not necessarily accurate
				qa_db_uapprovecount_update(); // needs to be somewhere and this is the most appropriate place
				qa_recalc_transition($state, 'dorecalcpoints_recalc');
				break;
				
			case 'dorecalcpoints_recalc':
				$userids=qa_db_users_get_for_recalc_points($next, 10);
				
				if (count($userids)) {
					$lastuserid=max($userids);
					
					qa_db_users_recalc_points($next, $lastuserid);
					
					$next=1+$lastuserid;
					$done+=count($userids);
					$continue=true;
				
				} else {
					qa_db_truncate_userpoints($next);
					qa_db_userpointscount_update(); // quick so just do it here
					qa_recalc_transition($state, 'dorecalcpoints_complete');
				}
				break;
				
			case 'dorefillevents':
				qa_recalc_transition($state, 'dorefillevents_qcount');
				break;
				
			case 'dorefillevents_qcount':
				qa_db_qcount_update();
				qa_recalc_transition($state, 'dorefillevents_refill');
				break;
				
			case 'dorefillevents_refill':
				$questionids=qa_db_qs_get_for_event_refilling($next, 1);
				
				if (count($questionids)) {
					require_once QA_INCLUDE_DIR.'qa-app-events.php';
					require_once QA_INCLUDE_DIR.'qa-app-updates.php';
					require_once QA_INCLUDE_DIR.'qa-util-sort.php';
					
					$lastquestionid=max($questionids);
					
					foreach ($questionids as $questionid) {

					//	Retrieve all posts relating to this question

						list($question, $childposts, $achildposts)=qa_db_select_with_pending(
							qa_db_full_post_selectspec(null, $questionid),
							qa_db_full_child_posts_selectspec(null, $questionid),
							qa_db_full_a_child_posts_selectspec(null, $questionid)
						);
						
					//	Merge all posts while preserving keys as postids
						
						$posts=array($questionid => $question);

						foreach ($childposts as $postid => $post)
							$posts[$postid]=$post;

						foreach ($achildposts as $postid => $post)
							$posts[$postid]=$post;
							
					//	Creation and editing of each post
							
						foreach ($posts as $postid => $post) {
							$followonq=($post['basetype']=='Q') && ($postid!=$questionid);
							
							if ($followonq)
								$updatetype=QA_UPDATE_FOLLOWS;
							elseif ( ($post['basetype']=='C') && (@$posts[$post['parentid']]['basetype']=='Q') )
								$updatetype=QA_UPDATE_C_FOR_Q;
							elseif ( ($post['basetype']=='C') && (@$posts[$post['parentid']]['basetype']=='A') )
								$updatetype=QA_UPDATE_C_FOR_A;
							else
								$updatetype=null;
							
							qa_create_event_for_q_user($questionid, $postid, $updatetype, $post['userid'], @$posts[$post['parentid']]['userid'], $post['created']);
							
							if (isset($post['updated']) && !$followonq)
								qa_create_event_for_q_user($questionid, $postid, $post['updatetype'], $post['lastuserid'], $post['userid'], $post['updated']);
						}
						
					//	Tags and categories of question

						qa_create_event_for_tags($question['tags'], $questionid, null, $question['userid'], $question['created']);
						qa_create_event_for_category($question['categoryid'], $questionid, null, $question['userid'], $question['created']);
						
					//	Collect comment threads
						
						$parentidcomments=array();
						
						foreach ($posts as $postid => $post)
							if ($post['basetype']=='C')
								$parentidcomments[$post['parentid']][$postid]=$post;
					
					//	For each comment thread, notify all previous comment authors of each comment in the thread (could get slow)

						foreach ($parentidcomments as $parentid => $comments) {
							$keyuserids=array();
							
							qa_sort_by($comments, 'created');
							
							foreach ($comments as $comment) {
								foreach ($keyuserids as $keyuserid => $dummy)
									if ( ($keyuserid != $comment['userid']) && ($keyuserid != @$posts[$parentid]['userid']) )
										qa_db_event_create_not_entity($keyuserid, $questionid, $comment['postid'], QA_UPDATE_FOLLOWS, $comment['userid'], $comment['created']);

								if (isset($comment['userid']))
									$keyuserids[$comment['userid']]=true;
							}
						}
					}
					
					$next=1+$lastquestionid;
					$done+=count($questionids);
					$continue=true;

				} else
					qa_recalc_transition($state, 'dorefillevents_complete');
				break;
			
			case 'dorecalccategories':
				qa_recalc_transition($state, 'dorecalccategories_postcount');
				break;
			
			case 'dorecalccategories_postcount':
				qa_db_acount_update();
				qa_db_ccount_update();
				
				qa_recalc_transition($state, 'dorecalccategories_postupdate');
				break;
				
			case 'dorecalccategories_postupdate':
				$postids=qa_db_posts_get_for_recategorizing($next, 100);
				
				if (count($postids)) {
					$lastpostid=max($postids);
					
					qa_db_posts_recalc_categoryid($next, $lastpostid);
					qa_db_posts_calc_category_path($next, $lastpostid);
					
					$next=1+$lastpostid;
					$done+=count($postids);
					$continue=true;
				
				} else {
					qa_recalc_transition($state, 'dorecalccategories_recount');
				}
				break;
			
			case 'dorecalccategories_recount':
				$categoryids=qa_db_categories_get_for_recalcs($next, 10);
				
				if (count($categoryids)) {
					$lastcategoryid=max($categoryids);
					
					foreach ($categoryids as $categoryid)
						qa_db_ifcategory_qcount_update($categoryid);
					
					$next=1+$lastcategoryid;
					$done+=count($categoryids);
					$continue=true;
				
				} else {
					qa_recalc_transition($state, 'dorecalccategories_backpaths');
				}
				break;
				
			case 'dorecalccategories_backpaths':
				$categoryids=qa_db_categories_get_for_recalcs($next, 10);

				if (count($categoryids)) {
					$lastcategoryid=max($categoryids);
					
					qa_db_categories_recalc_backpaths($next, $lastcategoryid);
					
					$next=1+$lastcategoryid;
					$done+=count($categoryids);
					$continue=true;
				
				} else {
					qa_recalc_transition($state, 'dorecalccategories_complete');
				}
				break;
				
			case 'dodeletehidden':
				qa_recalc_transition($state, 'dodeletehidden_comments');
				break;
				
			case 'dodeletehidden_comments':
				$posts=qa_db_posts_get_for_deleting('C', $next, 1);
				
				if (count($posts)) {
					require_once QA_INCLUDE_DIR.'qa-app-posts.php';
					
					$postid=$posts[0];

					qa_post_delete($postid);
					
					$next=1+$postid;
					$done++;
					$continue=true;
				
				} else
					qa_recalc_transition($state, 'dodeletehidden_answers');
				break;
			
			case 'dodeletehidden_answers':
				$posts=qa_db_posts_get_for_deleting('A', $next, 1);
				
				if (count($posts)) {
					require_once QA_INCLUDE_DIR.'qa-app-posts.php';
					
					$postid=$posts[0];
					
					qa_post_delete($postid);
					
					$next=1+$postid;
					$done++;
					$continue=true;
				
				} else
					qa_recalc_transition($state, 'dodeletehidden_questions');
				break;

			case 'dodeletehidden_questions':
				$posts=qa_db_posts_get_for_deleting('Q', $next, 1);
				
				if (count($posts)) {
					require_once QA_INCLUDE_DIR.'qa-app-posts.php';
					
					$postid=$posts[0];
					
					qa_post_delete($postid);
					
					$next=1+$postid;
					$done++;
					$continue=true;
				
				} else
					qa_recalc_transition($state, 'dodeletehidden_complete');
				break;
				
			case 'doblobstodisk':
				qa_recalc_transition($state, 'doblobstodisk_move');
				break;
				
			case 'doblobstodisk_move':
				$blob=qa_db_get_next_blob_in_db($next);
				
				if (isset($blob)) {
					require_once QA_INCLUDE_DIR.'qa-app-blobs.php';
					require_once QA_INCLUDE_DIR.'qa-db-blobs.php';
					
					if (qa_write_blob_file($blob['blobid'], $blob['content'], $blob['format']))
						qa_db_blob_set_content($blob['blobid'], null);
						
					$next=1+$blob['blobid'];
					$done++;
					$continue=true;
				
				} else
					qa_recalc_transition($state, 'doblobstodisk_complete');
				break;
				
			case 'doblobstodb':
				qa_recalc_transition($state, 'doblobstodb_move');
				break;
			
			case 'doblobstodb_move':
				$blob=qa_db_get_next_blob_on_disk($next);
				
				if (isset($blob)) {
					require_once QA_INCLUDE_DIR.'qa-app-blobs.php';
					require_once QA_INCLUDE_DIR.'qa-db-blobs.php';
					
					$content=qa_read_blob_file($blob['blobid'], $blob['format']);
					qa_db_blob_set_content($blob['blobid'], $content);
					qa_delete_blob_file($blob['blobid'], $blob['format']);
						
					$next=1+$blob['blobid'];
					$done++;
					$continue=true;
				
				} else
					qa_recalc_transition($state, 'doblobstodb_complete');
				break;

			default:
				$state='';
				break;
		}
		
		if ($continue)
			$state=$operation.','.$length.','.$next.','.$done;
		
		return $continue && ($done<$length);
	}
	

	function qa_recalc_transition(&$state, $operation)
/*
	Change the $state to represent the beginning of a new $operation
*/
	{
		$state=$operation.','.qa_recalc_stage_length($operation).',0,0';
	}

		
	function qa_recalc_stage_length($operation)
/*
	Return how many steps there will be in recalculation $operation
*/
	{
		switch ($operation) {
			case 'doreindexcontent_pagereindex':
				$length=qa_db_count_pages();
				break;
			
			case 'doreindexcontent_postreindex':
				$length=qa_opt('cache_qcount')+qa_opt('cache_acount')+qa_opt('cache_ccount');
				break;
			
			case 'doreindexposts_wordcount':
				$length=qa_db_count_words();
				break;
				
			case 'dorecalcpoints_recalc':
				$length=qa_opt('cache_userpointscount');
				break;
				
			case 'dorecountposts_votecount':
			case 'dorecountposts_acount':
			case 'dorecalccategories_postupdate':
				$length=qa_db_count_posts();
				break;
				
			case 'dorefillevents_refill':
				$length=qa_opt('cache_qcount')+qa_db_count_posts('Q_HIDDEN');
				break;
			
			case 'dorecalccategories_recount':
			case 'dorecalccategories_backpaths':
				$length=qa_db_count_categories();
				break;
			
			case 'dodeletehidden_comments':
				$length=count(qa_db_posts_get_for_deleting('C'));
				break;
				
			case 'dodeletehidden_answers':
				$length=count(qa_db_posts_get_for_deleting('A'));
				break;
				
			case 'dodeletehidden_questions':
				$length=count(qa_db_posts_get_for_deleting('Q'));
				break;
				
			case 'doblobstodisk_move':
				$length=qa_db_count_blobs_in_db();
				break;
			
			case 'doblobstodb_move':
				$length=qa_db_count_blobs_on_disk();
				break;
			
			default:
				$length=0;
				break;
		}
		
		return $length;
	}

	
	function qa_recalc_get_message($state)
/*
	Return a string which gives a user-viewable version of $state
*/
	{
		@list($operation, $length, $next, $done)=explode(',', $state);
		
		$done=(int)$done;
		$length=(int)$length;
		
		switch ($operation) {
			case 'doreindexcontent_postcount':
			case 'dorecountposts_postcount':
			case 'dorecalccategories_postcount':
			case 'dorefillevents_qcount':
				$message=qa_lang('admin/recalc_posts_count');
				break;
				
			case 'doreindexcontent_pagereindex':
				$message=strtr(qa_lang('admin/reindex_pages_reindexed'), array(
					'^1' => number_format($done),
					'^2' => number_format($length)
				));
				break;

			case 'doreindexcontent_postreindex':
				$message=strtr(qa_lang('admin/reindex_posts_reindexed'), array(
					'^1' => number_format($done),
					'^2' => number_format($length)
				));
				break;
				
			case 'doreindexposts_wordcount':
				$message=strtr(qa_lang('admin/reindex_posts_wordcounted'), array(
					'^1' => number_format($done),
					'^2' => number_format($length)
				));
				break;
				
			case 'dorecountposts_votecount':
				$message=strtr(qa_lang('admin/recount_posts_votes_recounted'), array(
					'^1' => number_format($done),
					'^2' => number_format($length)
				));
				break;
				
			case 'dorecountposts_acount':
				$message=strtr(qa_lang('admin/recount_posts_as_recounted'), array(
					'^1' => number_format($done),
					'^2' => number_format($length)
				));
				break;
				
			case 'doreindexposts_complete':
				$message=qa_lang('admin/reindex_posts_complete');
				break;
				
			case 'dorecountposts_complete':
				$message=qa_lang('admin/recount_posts_complete');
				break;
				
			case 'dorecalcpoints_usercount':
				$message=qa_lang('admin/recalc_points_usercount');
				break;
				
			case 'dorecalcpoints_recalc':
				$message=strtr(qa_lang('admin/recalc_points_recalced'), array(
					'^1' => number_format($done),
					'^2' => number_format($length)
				));
				break;
				
			case 'dorecalcpoints_complete':
				$message=qa_lang('admin/recalc_points_complete');
				break;
				
			case 'dorefillevents_refill':
				$message=strtr(qa_lang('admin/refill_events_refilled'), array(
					'^1' => number_format($done),
					'^2' => number_format($length)
				));
				break;
				
			case 'dorefillevents_complete':
				$message=qa_lang('admin/refill_events_complete');
				break;
				
			case 'dorecalccategories_postupdate':
				$message=strtr(qa_lang('admin/recalc_categories_updated'), array(
					'^1' => number_format($done),
					'^2' => number_format($length)
				));
				break;
				
			case 'dorecalccategories_recount':
				$message=strtr(qa_lang('admin/recalc_categories_recounting'), array(
					'^1' => number_format($done),
					'^2' => number_format($length)
				));
				break;
				
			case 'dorecalccategories_backpaths':
				$message=strtr(qa_lang('admin/recalc_categories_backpaths'), array(
					'^1' => number_format($done),
					'^2' => number_format($length)
				));
				break;
				
			case 'dorecalccategories_complete':
				$message=qa_lang('admin/recalc_categories_complete');
				break;
				
			case 'dodeletehidden_comments':
				$message=strtr(qa_lang('admin/hidden_comments_deleted'), array(
					'^1' => number_format($done),
					'^2' => number_format($length)
				));
				break;
				
			case 'dodeletehidden_answers':
				$message=strtr(qa_lang('admin/hidden_answers_deleted'), array(
					'^1' => number_format($done),
					'^2' => number_format($length)
				));
				break;
				
			case 'dodeletehidden_questions':
				$message=strtr(qa_lang('admin/hidden_questions_deleted'), array(
					'^1' => number_format($done),
					'^2' => number_format($length)
				));
				break;

			case 'dodeletehidden_complete':
				$message=qa_lang('admin/delete_hidden_complete');
				break;
				
			case 'doblobstodisk_move':
			case 'doblobstodb_move':
				$message=strtr(qa_lang('admin/blobs_move_moved'), array(
					'^1' => number_format($done),
					'^2' => number_format($length)
				));
				break;
				
			case 'doblobstodisk_complete':
			case 'doblobstodb_complete':
				$message=qa_lang('admin/blobs_move_complete');
				break;
			
			default:
				$message='';
				break;
		}
		
		return $message;
	}


/*
	Omit PHP closing tag to help avoid accidental output
*/