<?php
/**
 * <pre>
 * Invision Power Services
 * IP.Board v3.4.6
 * Archive: Writer
 * By Matt Mecham
 * </pre>
 *
 * @author 		$Author: ips_terabyte $
 * @copyright	(c) 2010 Invision Power Services, Inc.
 *
 * @package		IP.Board
 *
 * @since		17th February 2010
 * @version		$Revision: 8644 $
 */

class classes_archive_writer
{
	/**#@+
	 * Registry objects
	 *
	 * @access	protected
	 * @var		object
	 */
	protected $registry;
	protected $DB;
	protected $settings;
	protected $request;
	protected $lang;
	protected $member;
	protected $memberData;
	protected $cache;
	protected $caches;
	/**#@-*/
	
	/**
	 * Set current app
	 */
	private $_app = '';
	
	/**
	 * Archive 'do' allowed fields
	 */
	private $_archiveOnFields = array( 'state'		=> 'state',
									   'pinned'		=> 'pinned',
									   'approved'	=> 'approved',
									   'poll'		=> 'poll_state',
									   'post'		=> 'posts',
									   'view'		=> 'views',
									   'rating'		=> 'topic_rating_total',
									   'forum'		=> 'forum_id',
									   'member'		=> 'starter_id',
									   'lastpost' 	=> 'last_post' );
	
	/**
	 * Archive 'skip' allowed fields
	 */
	private $_archiveSkipFields = array( 'post'		=> 'posts',
									     'view'		=> 'views',
									     'rating'	=> 'topic_rating_total',
									     'forum'	=> 'forum_id',
									     'member'	=> 'starter_id',
									     'lastpost' => 'last_post');
	
	/**
	 * Constructor
	 *
	 */
	public function __construct()
	{
		/* Make registry objects */
		$this->registry		=  ipsRegistry::instance();
		$this->DB			=  $this->registry->DB();
		$this->settings		=& $this->registry->fetchSettings();
		$this->request		=& $this->registry->fetchRequest();
		$this->lang			=  $this->registry->getClass('class_localization');
		$this->member		=  $this->registry->member();
		$this->memberData	=& $this->registry->member()->fetchMemberData();
		$this->cache		=  $this->registry->cache();
		$this->caches		=& $this->registry->cache()->fetchCaches();
		
		/* Check for class_forums */
		if ( ipsRegistry::isClassLoaded('class_forums') !== TRUE )
		{
			$classToLoad = IPSLib::loadLibrary( IPSLib::getAppDir( 'forums' ) . "/sources/classes/forums/class_forums.php", 'class_forums', 'forums' );
			$this->registry->setClass( 'class_forums', new $classToLoad( $this->registry ) );
			$this->registry->class_forums->forumsInit();
		}
		
		/* Language class */
		$this->registry->getClass('class_localization')->loadLanguageFile( array( 'public_global' ), 'core' );
		
		/* Fetch engine class */
		$this->settings['archive_engine'] = ( $this->settings['archive_engine'] ) ? $this->settings['archive_engine'] : 'sql';
		
		/* Load up archive class */
		$classToLoad = IPSLib::loadLibrary( IPS_ROOT_PATH . 'sources/classes/archive/writer/' . $this->settings['archive_engine'] . '.php', 'classes_archive_writer_' . $this->settings['archive_engine'] );
		$this->engine = new $classToLoad();
		
		if ( $this->engine->test() !== true )
		{
			$this->error( $this->lang->words['archive_fail_engine'] );
		}
	}

	/**
	 * It's magic!
	 * @param string $method
	 * @param array $arguments
	 */
	public function __call( $method, $arguments )
	{
		switch ( $method )
		{
			case 'testConnection':
				return $this->engine->test();
			break;
			case 'createTable':
				return $this->engine->createTable();
			break;
			case 'flush':
				return $this->engine->flush();
			break;
		}
	}
	
	/**
	 * Updates posts
	 *
	 * @param	$what		Array of fields with vals to update
	 * @param	$where		String of what to update
	 */
	public function update( array $what, $where )
	{
		return $this->engine->update( $what, $where );
	}
	
	/**
	 * Set the current application
	 * @param string $app
	 */
	public function setApp( $app )
	{
		$this->_app = $app;
	}
	
	/**
	 * Get the application
	 * @return string
	 */
	public function getApp()
	{
		return $this->_app;
	}
	
	/**
	 * Return archive on fields
	 * @return array
	 */
	public function getArchiveOnFields()
	{
		return array_keys( $this->_archiveOnFields );
	}
	
	/**
	 * Return archive skip fields
	 * @return array
	 */
	public function getArchiveSkipFields()
	{
		return array_keys( $this->_archiveSkipFields );
	}
	
	/**
	 * Write topics to the archive
	 * @param	array	INTS
	 */
	public function write( $pids=array() )
	{
		if ( ! count( $pids ) )
		{
			return null;
		}
		
		$topics          = array();
		$forumIds        = array();
		$completedTopics = array();
		$workingTopics   = array();
		$maxes			 = array();
		
		/* Load the attachments stuff */
		$classToLoad  = IPSLib::loadLibrary( IPSLib::getAppDir( 'core' ) . '/sources/classes/attach/class_attach.php', 'class_attach' );
		$class_attach = new $classToLoad( $this->registry );
		$class_attach->type = 'post';
		$class_attach->init();
		
		/* Fetch data based on PIDs */
		$this->DB->build( array( 'select' => 'p.*',
								 'from'   => array( 'posts' => 'p' ),
								 'where'  => 'p.pid IN (' . implode( ',', $pids ) . ')',
								 'order'  => 'p.topic_id ASC, p.pid ASC',
								 'add_join' => array( array( 'select' => 't.*',
															 'from'   => array( 'topics' => 't' ),
															 'where'  => 'p.topic_id=t.tid' ) ) ) );
		
		$o = $this->DB->execute();
		
		while( $post = $this->DB->fetch( $o ) )
		{
			/* Write it */
			$this->engine->set( $post );
			
			/* Collect some data */
			if ( ! in_array( $post['tid'], $topics ) )
			{
				$post['_pids']          = 1;
				$post['_lowPid']		= $post['pid'];
				$post['_maxPid']		= $post['pid'];
				$post['_topicCount']    = ( intval( $post['posts'] ) + intval( $post['topic_deleted_posts'] ) + intval( $post['topic_queuedposts'] ) + 1 );
				$topics[ $post['tid'] ] = $post;
			}
			else
			{
				$topics[ $post['tid'] ]['_maxPid'] = $post['pid'];
				$topics[ $post['tid'] ]['_pids']++;
			}
				
			/* Collect fids */
			$forums[ $post['forum_id'] ] = $post['forum_id'];
		}
		
		/* Got any topics? */
		if ( ! count( $topics ) )
		{
			return;
		}
		
		/* Get the maxes */
		$this->DB->build( array( 'select' => 'topic_id, MAX(pid) as i',
								 'from'   => 'posts',
								 'where'  => 'topic_id IN (' . implode( ',', array_keys( $topics ) ) . ')',
								 'group'  => 'topic_id' ) );
		
		$o = $this->DB->execute();
		
		while( $t = $this->DB->fetch( $o ) )
		{
			$maxes[ $t['topic_id'] ] = intval( $t['i'] );
		}
		
		/* Figure out how many topics have been completed */
		foreach( $topics as $tid => $data )
		{
			/* First pid matches topic first pid, so we have the first post in the topic
			 * but have we got all the posts in this topic?
			 */
			if ( $maxes[ $tid ] == $data['_maxPid'] )
			{
				$completedTopics[] = $data['tid'];
			}
			else
			{
				$workingTopics[] = $data['tid'];
			}
		}
		
		/* Update working */
		if ( count( $workingTopics ) )
		{
			$this->DB->update( 'topics', array( 'topic_archive_status' => $this->registry->class_forums->fetchTopicArchiveFlag( 'working' ) ),'tid IN(' . implode( ',', $workingTopics ) . ')' );
		}
		
		/* Update completed */
		if ( count( $completedTopics ) )
		{
			$this->DB->update( 'topics', array( 'topic_archive_status' => $this->registry->class_forums->fetchTopicArchiveFlag( 'archived' ) ), 'tid IN(' . implode( ',', $completedTopics ) . ')' );
			
			/* Archive attachments */
			$class_attach->bulkArchive( $completedTopics, 'attach_parent_id' );
			
			/* Remove posts */
			$this->DB->delete( 'posts', 'topic_id IN(' . implode( ',', $completedTopics ) . ')' );		
		}
		
		/* Do forums */
		foreach( $forums as $id )
		{
			$this->registry->class_forums->forumRebuild( $id );
		}
		
		/* Log it */
		$this->log( $pids );
	}
	
	/**
	 * Process a batch. Genius.
	 * $options is an array of keys and values:
	 * process - INT - number of items to process in this batch
	 * @return int count of topics archived
	 */
	public function processBatch( $options=array() )
	{
		/* Archiver running? */
		if ( ! $this->settings['archive_on'] )
		{
			return false;
		}
		
		/* Init vars */
		$query           = $this->getArchiveWhereQuery();
		$topics          = array();
		$forums          = array();
		$pids            = array();
		
		/* Fix up options */
		$options['process'] = ( is_numeric( $options['process'] ) && $options['process'] > 0 ) ? $options['process'] : 250;
		
		if ( $query === null )
		{
			return null;
		}
		
		/* First see if we have a topic in progress */
		$working = $this->DB->buildAndFetch( array( 'select' => '*',
													'from'   => 'topics',
													'where'  => 'topic_archive_status=' . $this->registry->class_forums->fetchTopicArchiveFlag( 'working' ),
													'order'  => 'tid ASC',
													'limit'  => array( 0, 1 ) ) );
		
		/* Got one? */
		if ( $working['tid'] )
		{
			$doneSoFar = $this->engine->getDoneSoFarByTid( $working['tid'] );
			
			$postTable = $this->DB->buildAndFetch( array( 'select' => 'COUNT(*) as count, MAX(pid) as max',
														  'from'   => 'posts',
														  'where'  => 'pid > ' . intval( $doneSoFar['max'] ) . ' AND topic_id=' . $working['tid'] ) );
			
			/* Mark topic complete if there's no posts left - This should not strictly be necessary but an old bug can leave topics in limbo */
			if ( !$postTable['count'] )
			{
				$this->DB->update( 'topics', array( 'topic_archive_status' => $this->registry->class_forums->fetchTopicArchiveFlag( 'archived' ) ), 'tid=' . $working['tid'] );
			
				/* Archive any attachments */
				$classToLoad  = IPSLib::loadLibrary( IPSLib::getAppDir( 'core' ) . '/sources/classes/attach/class_attach.php', 'class_attach' );
				$class_attach = new $classToLoad( $this->registry );
				$class_attach->type = 'post';
				$class_attach->init();
			
				$class_attach->bulkArchive( array( $working['tid'] ), 'attach_parent_id' );
			
				/* Remove posts */
				$this->DB->delete( 'posts', 'topic_id =' . $working['tid'] );
			}			
			
			/* Select remaining Pids */
			$this->DB->build( array( 'select' => 'pid',
									 'from'   => 'posts',
									 'where'  => 'pid > ' . intval( $doneSoFar['max'] ) . ' AND topic_id=' . $working['tid'],
									 'order'  => 'pid ASC',
									 'limit'  => array( 0, $options['process'] ) ) );
			
			$o = $this->DB->execute();
			
			while( $post = $this->DB->fetch( $o ) )
			{
				$pids[ $post['pid'] ] = $post['pid'];
			}
			
			/* Update process for below */
			$options['process'] -= count( $pids );
		}
		
		/* Carry on? */
		if ( $options['process'] > 0 )
		{
			/* Collect topic IDs first */
			$this->DB->build( array( 'select' => '*',
									 'from'   => 'topics',
									 'where'  => $query . ' AND (posts + topic_deleted_posts + topic_queuedposts ) >= 0',
									 /**
									  * We can't check for posts > 0 because if the topic has only the first post it will skip them
									  * 
									  * @link	http://community.---.com/tracker/issue-37131-archive-posts-missing-old-topics-with-post-count-0/
									  */
									 //'where'  => $query . ' AND (posts + topic_deleted_posts + topic_queuedposts ) > 0',
									 'order'  => 'tid ASC',
									 'limit'  => array( 0, 250 ) ) );
			
			$o = $this->DB->execute();
			
			$counter = 0;
			
			while( $topic = $this->DB->fetch( $o ) )
			{
				$topics[] = $topic['tid'];
				
				$counter += ( $topic['posts'] + $topic['topic_deleted_posts'] + $topic['topic_queuedposts'] ) + 1;
				
				if ( $counter >= $options['process'] )
				{
					break;
				}
			}
			
			/* Got anything? */
			if ( count( $topics ) )
			{	
				/* Now fetch the PIDS */
				$this->DB->build( array( 'select' => 'pid',
										 'from'   => 'posts',
										 'where'  => 'topic_id IN (' . implode( ',', $topics ) . ')',
										 'order'  => 'topic_id ASC, pid ASC',
										 'limit'  => array( 0, $options['process'] ) ) );
				
				$o = $this->DB->execute();
				
				while( $post = $this->DB->fetch( $o ) )
				{
					$pids[ $post['pid'] ] = $post['pid'];
				}
			}
		}
		
		/* Process */
		$this->write( $pids );
		
		return count( $pids );
	}
	
	/**
	 * Give me a key and I'll give you a DB field you lucky beasts
	 * @param string $key
	 */
	public function getDbFieldFromKey( $key )
	{
		return ( isset( $this->_archiveOnFields[ $key ] ) ) ? $this->_archiveOnFields[ $key ] : null;
	}
	
	/**
	 * Get a count of how many topics the current settings would archive off
	 * if performed in one go
	 * @param array $rules [Optional]
	 * @return array	array( count, total, percentage )
	 */
	public function getArchivePossibleCount( $rules=array() )
	{
		$query  = $this->getArchiveWhereQuery( $rules, false, true );	

		$return = array( 'count' => null, 'percentage' => 0, 'total' => 0 );
		 
		if ( $query !== null )
		{
			$total = $this->DB->buildAndFetch( array( 'select' => 'count(*) as count',
													  'from'   => 'topics',
			 										  'where'  => $query  . ' AND state != \'link\'' ) );
			
			$all   = $this->DB->buildAndFetch( array( 'select' => 'count(*) as count',
												 	  'from'   => 'topics' ) );
			
			$return['count'] = intval( $total['count'] );
			$return['total'] = intval( $all['count'] );
			
			if ( $return['count'] && $return['total'] )
			{
				$return['percentage'] = round( ( $return['count'] / $return['total'] ) * 100, 2 );
			}
		}
		
		return $return;
	}
	
	/**
	 * Returns the WHERE portion of a query string required to fetch topics to archive
	 * @param array $rules [Optional]
	 */
	public function getArchiveWhereQuery( $rules=array(), $onlyGetUnarchived=true, $excludeExcluded=false )
	{
		$where    = ( $onlyGetUnarchived ) ? array( 'topic_archive_status=' . $this->registry->class_forums->fetchTopicArchiveFlag( 'not' ) ) : array();
		$whereNot = ( $excludeExcluded ) ? array( 'topic_archive_status=' . $this->registry->class_forums->fetchTopicArchiveFlag( 'exclude' ) ) : array();
		
		/* If no input, fetch from deebee */
		if ( ! count( $rules ) )
		{
			$rules = $this->getRulesFromDb();
		}
		
		/* Hello mum! */
		if ( is_array( $rules ) && count( $rules ) )
		{
			if ( is_array( $rules['archive'] ) )
			{
				foreach( $rules['archive'] as $key => $data )
				{
					$result = $this->_buildQueryBit( $key, $data, true );
					
					if ( $result !== null )
					{
						$where[] = $result;
					}
				}
			}
			
			if ( is_array( $rules['skip'] ) )
			{
				foreach( $rules['skip'] as $key => $data )
				{
					$result = $this->_buildQueryBit( $key, $data, false );
					
					if ( $result !== null )
					{
						$where[] = $result;
					}
				}
			}
		}
		
		foreach ( $whereNot as $w )
		{
			$where[] = "!({$w})";
		}
		
		if ( count( $where ) )
		{
			/* Only fetch non-deleted topics */
			$where[] = $this->registry->class_forums->fetchTopicHiddenQuery( array( 'hidden', 'visible' ) );
			
			return implode( ' AND ', $where );
		}
		else
		{
			return null;
		}
	}
	
	/**
	 * Fetch the rules form the database
	 */
	public function getRulesFromDb()
	{
		$rules = array();
		
		$this->DB->build( array( 'select' => '*',
								 'from'   => 'core_archive_rules',
								 'where'  => 'archive_app=\'' . $this->DB->addSlashes( $this->getApp() ) . '\'' ) );
		
		$this->DB->execute();
		
		while( $row = $this->DB->fetch() )
		{
			$whut = ( $row['archive_skip'] ) ? 'skip' : 'archive';
			
			$rules[ $whut ][ $row['archive_field'] ] = array( 'value' => $row['archive_value'],
															  'text'  => $row['archive_text'],
															  'unit'  => $row['archive_unit'] );
		}
		
		return $rules;
	}
	
	/**
	 * Take a post and returned archive friendly array
	 * @param array $post
	 * @return array
	 */
	public function nativeToArchiveFields( $post )
	{
		$archive = array( 'archive_id'           	 => intval( $post['pid'] ),
						  'archive_author_id'    	 => intval( $post['author_id'] ),
						  'archive_author_name'  	 => $post['author_name'],
					      'archive_ip_address'   	 => $post['ip_address'],
						  'archive_content_date' 	 => intval( $post['post_date'] ),
						  'archive_content'		 	 => $post['post'],
						  'archive_queued'	 	     => $post['queued'],
						  'archive_topic_id'     	 => intval( $post['topic_id'] ),
						  'archive_is_first'     	 => intval( $post['new_topic'] ),
						  'archive_bwoptions'    	 => $post['post_bwoptions'],
						  'archive_added'		 	 => IPS_UNIX_TIME_NOW,
						  'archive_attach_key'   	 => $post['post_key'],
						  'archive_html_mode'   	 => $post['post_htmlstate'],
						  'archive_show_signature'   => $post['use_sig'],
						  'archive_show_emoticons'   => $post['use_emo'],
						  'archive_show_edited_by'   => $post['append_edit'],
						  'archive_edit_time'   	 => intval( $post['edit_time'] ),
						  'archive_edit_name'   	 => $post['edit_name'],
						  'archive_edit_reason'      => $post['post_edit_reason'],
						  'archive_forum_id'		 => intval( $post['forum_id'] ) );
		
		return $archive;
						
	}
	
	/**
	 * Logs archive action
	 * @param array $ids
	 */
	public function log( $ids )
	{
		$this->DB->insert( 'core_archive_log', array( 'archlog_date'       => IPS_UNIX_TIME_NOW,
													  'archlog_app'        => $this->getApp(),
													  'archlog_ids'        => serialize( $ids ),
													  'archlog_is_restore' => 0,
													  'archlog_count'      => count( $ids ) ) );	
	}
	
	/**
	 * Logs archive action
	 * @param array $ids
	 * @param boolean $isRestore
	 */
	public function error( $msg )
	{
		$this->DB->insert( 'core_archive_log', array( 'archlog_date'       => IPS_UNIX_TIME_NOW,
													  'archlog_app'        => $this->getApp(),
													  'archlog_msg'        => $msg,
													  'archlog_is_error'   => 1 ) );	
	}
	
	/**
	 * Build a WHERE segment
	 * @param string $key
	 * @param data $data
	 * @param boolean $archiveOn Archive = true, skip = false
	 */
	protected function _buildQueryBit( $key, $data, $archiveOn )
	{
		$strOp = ( $archiveOn ) ? '='  : '!=';
		$inOp  = ( $archiveOn ) ? 'IN' : 'NOT IN';
		
		switch( $key )
		{
			case 'state':
				if ( $data['value'] != '' && $data['value'] != '-' )
				{
					return $this->getDbFieldFromKey( $key ) . $strOp . "'" . $data['value'] . "'"; 
				}
 			break;
			case 'pinned':
			case 'approved':
			case 'poll':
				if ( $data['value'] != '' && $data['value'] != '-' )
				{
					return $this->getDbFieldFromKey( $key ) . $strOp . intval( $data['value'] ); 
				}
			break;
			case 'post':
			case 'view':
			case 'rating':
				if ( is_numeric( $data['text'] ) && $data['text'] > 0 )
				{
					return $this->getDbFieldFromKey( $key ) . $data['value'] . intval( $data['text'] ); 
				}
			break;
			case 'lastpost':
				if ( is_numeric( $data['text'] ) && $data['text'] > 0 )
				{
					switch( $data['unit'] )
					{
						case 'd':
							$seconds = 86400;
						break;
						case 'm':
							$seconds = 2592000;
						break;
						case 'y':
							$seconds = 31536000;
						break;
					}
					
					$time = IPS_UNIX_TIME_NOW - ( $seconds * intval( $data['text'] ) );
					
					/* Switch around for skipping */
					$data['value'] = ( $archiveOn ) ? $data['value'] : ( $data['value'] == '>' ? '<' : '>' );
					
					return $this->getDbFieldFromKey( $key ) . ' ' . $data['value'] . ' ' . $time;
				}
			break;
			case 'forum':
			case 'member':
				if ( $data['text'] )
				{
					if ( IPSLib::isSerialized( $data['text'] ) )
					{
						$ids = unserialize( $data['text'] );
					}
					else
					{
						$ids = array();
					}
					
					$inOp = ( $data['value'] == '+' ) ? 'IN' : 'NOT IN';
					
					if ( is_array( $ids ) && count( $ids ) )
					{
						return $this->getDbFieldFromKey( $key ) . ' ' . $inOp . '(' . implode(',', $ids ) . ')';
					}
				}
			break;
		}
		
		return null;
	}
}
