<?php

/**
 * <pre>
 * Invision Power Services
 * IP.Board v3.4.6
 * Tagging: Abstract class
 * Matt Mecham
 * Last Updated: $Date: 2013-05-16 21:35:15 -0400 (Thu, 16 May 2013) $
 * </pre>
 *
 * @author 		$Author: bfarber $
 * @copyright	(c) 2001 - 2011 Invision Power Services, Inc.
 * @license		http://www.invisionpower.com/company/standards.php#license
 * @package		IP.Board
 * @link		http://www.invisionpower.com
 * @since		24 Feb 2011
 * @version		$Revision: 12257 $
 *
 */

if ( ! defined( 'IN_IPB' ) )
{
	print "<h1>Incorrect access</h1>You cannot access this file directly. If you have recently upgraded, make sure you upgraded all the relevant files.";
	exit();
}

abstract class classes_tag_abstract
{
	/**#@+
	 * Registry objects
	 *
	 * @access	protected
	 * @var		object
	 */	
	protected $registry;
	protected $DB;
	protected $settings;
	protected $request;
	protected $lang;
	protected $member;
	protected $memberData;
	protected $cache;
	protected $caches;
	/**#@-*/
	
	protected $app  = '';
	protected $area = '';
	protected $errorMsg = '';
	
	/**
	 * Init
	 *
	 * @return	@e void
	 */
	public function init()
	{
		
	}

	/**
	 * @return the $errorMsg
	 */
	public function getErrorMsg()
	{
		return $this->errorMsg;
	}

	/**
	 * @return the $errorMsg
	 */
	public function getFormattedError()
	{
		if( $this->errorMsg )
		{
			switch( $this->errorMsg )
			{
				case 'no_good_tags':
					return sprintf( $this->lang->words['no_good_tags'], $this->_getMinLen(), $this->_getMaxLen() );
				break;
				case 'too_few_tags':
					return sprintf( $this->lang->words['too_few_tags'], $this->_getMinTags() );
				break;
				
				case 'too_many_tags':
					return sprintf( $this->lang->words['too_many_tags'], $this->_getMaxTags() );
				break;
				
				default:
					return $this->lang->words[ $this->errorMsg ] ? $this->lang->words[ $this->errorMsg ] : $this->errorMsg;
				break;
			}
		}
		
		return '';
	}

	/**
	 * @param field_type $errorMsg
	 */
	public function setErrorMsg( $errorMsg )
	{
		$this->errorMsg = $errorMsg;
	}

	/**
	 * @return the $app
	 */
	public function getApp()
	{
		return $this->app;
	}

	/**
	 * @return the $area
	 */
	public function getArea()
	{
		return $this->area;
	}

	/**
	 * @return the search section
	 */
	public function getSearchSection()
	{
		return '';
	}

	/**
	 * @param field_type $app
	 */
	public function setApp( $app )
	{
		$this->app = $app;
	}

	/**
	 * @param field_type $area
	 */
	public function setArea( $area )
	{
		$this->area = $area;
	}
		
	/**
	 * @return boolean
	 */
	public function isEnabled()
	{
		return $this->_isEnabled();
	}
	
	/**
	 * Check to make sure our tags are ok
	 * @param	mixed	Array of tag text or string (tag, tag, tag)
	 * @param  array $where array (contains any of these: meta_id, meta_visible, meta_parent_id, member_id, permission_string)
	 * @param	string	mask to check (add / replace)
	 * 
	 */
	public function checkAdd( $tags, $where, $mask='add' )
	{
		$where = $this->_cleanWhere( $where );
		
		/* Quick check */
		if ( $this->can( $mask, $where ) !== true )
		{
			$this->setErrorMsg( 'no_permission' );
			return false;
		}
		
		/* Clean my tags please */
		$tags = $this->_cleanTags( $tags );
		
		if ( $tags === false )
		{
			if ( $this->settings['tags_min'] and $this->settings['tags_min_req'] )
			{
				$this->setErrorMsg('too_few_tags');
			}
		
			/* Error message is populated at this point */
			return false;
		}

		return true;
	}
	
	/**
	 * Add tags to the dee bee
	 * @param	mixed	Array of tag text or string (tag, tag, tag)
	 * @param  array $where array (contains any of these: meta_id, meta_visible, meta_parent_id, member_id, permission_string)
	 * @param	string	mask to check (add / replace)
	 * 
	 */
	public function add( $tags, $where, $mask='add' )
	{
		$where       = $this->_cleanWhere( $where );
		$memberId    = ( isset( $where['member_id'] ) ) ? intval( $where['member_id'] ) : $this->memberData['member_id'];
		$trackPrefix = false;
		$c           = 0;

		/* Quick check */
		if ( $this->can( $mask, $where ) !== true )
		{
			$this->setErrorMsg( 'no_permission' );
			return false;
		}
		
		/* If we're adding, check that some don't already exist (see bug report 34329) */
		if ( $mask == 'add' )
		{
			$this->deleteByMetaId( $where['meta_id'] );
		}
		
		/* Clean my tags please */
		$tags = $this->_cleanTags( $tags );

		if ( $tags === false )
		{
			/* Error message is populated at this point */
			return false;
		}
		
		/* Ok, we need some data please */
		$where['meta_parent_id']    = isset( $where['meta_parent_id'] )    ? $where['meta_parent_id']    : $this->getParentId( $where );
		$where['permission_string'] = isset( $where['permission_string'] ) ? $where['permission_string'] : $this->getPermissionData( $where );
		$where['meta_visible']      = isset( $where['meta_visible'] )      ? $where['meta_visible']      : $this->getIsVisible( $where );
				
		/* Prefixes */
		if ( ! empty( $_REQUEST[ $this->_getFieldId( $where ) . '_prefix' ] ) )
		{
			if ( $this->_prefixesEnabled( $where ) )
			{
				$trackPrefix = true;
			}
		}
		
		/* Update permission row */
		$this->_setStoredPermissionData( array( 'meta_id'           => $where['meta_id'],
												'meta_parent_id'	=> $where['meta_parent_id'],
												'permission_string' => $where['permission_string'],
												'meta_visible'		=> $where['meta_visible'] ) );
		
		/* Add tags */
		foreach( $tags as $tag )
		{
			$this->DB->insert( 'core_tags', array( 'tag_aai_lookup'     => $this->_getKey( array( 'meta_id'        => $where['meta_id'] ) ),
												   'tag_aap_lookup'     => $this->_getKey( array( 'meta_parent_id' => $where['meta_parent_id'] ) ),
							   					   'tag_meta_app'       => $this->getApp(),
												   'tag_meta_area'      => $this->getArea(),
												   'tag_meta_id'        => intval( $where['meta_id'] ),
												   'tag_meta_parent_id' => $where['meta_parent_id'],
												   'tag_member_id'      => $memberId,
												   'tag_added'			=> IPS_UNIX_TIME_NOW,
												   'tag_prefix'			=> ( ! $c && $trackPrefix ) ? 1 : 0,
												   'tag_text'			=> $this->DB->addSlashes( $tag ) ) );
			$c++;
		}
		
		/* Rebuild cache */
		$this->_addCache( $where );
	}
	
	/**
	 * Reset prefix ID for meta_id by earliest prefix
	 * @param	int		Meta ID
	 * @param	boolean	Recache tags
	 */
	public function resetPrefix( $metaId, $recache=true )
	{
		$tags 		= $this->getRawTagsByMetaId( $metaId );
		$sortedMate = array();
		
		foreach( $tags as $id => $tag )
		{
			$sortedMate[ $tag['tag_added'] . '.' . $tag['tag_id'] ] = $id;
		}
		
		ksort( $sortedMate );
		
		$tagId = array_shift( $sortedMate );
		
		if ( $tagId )
		{
			$this->DB->update( 'core_tags', array( 'tag_prefix' => 0 ), 'tag_meta_app=\'' . $this->getApp() . '\' AND tag_meta_area=\'' . $this->getArea() . '\' AND tag_meta_id=' . intval( $metaId ) );
			$this->DB->update( 'core_tags', array( 'tag_prefix' => 1 ), 'tag_meta_app=\'' . $this->getApp() . '\' AND tag_meta_area=\'' . $this->getArea() . '\' AND tag_id=' . intval( $tagId ) );
		}
		
		if ( $recache )
		{
			$this->rebuildCache( array( 'meta_id' => $metaId ) );
		}
	}
	
	/**
	 * Add tags to the dee bee
	 * @param	mixed	Array of tag text or string (tag, tag, tag)
	 * @param  array $where array (contains any of these: meta_id, meta_visible, meta_parent_id, member_id, permission_string)
	 * 
	 */
	public function replace( $tags, $where )
	{
		$where = $this->_cleanWhere( $where );
		
		/* Can we edit first of all? */
		$can = $this->can( 'edit', $where );
		
		if ( $can )
		{
			/* Remove */
			if ( ! empty( $where['meta_id'] ) )
			{
				$this->deleteByMetaId( $where['meta_id'] );
			}
			
			return $this->add( $tags, $where, 'edit' );
		}
		else
		{
			return $can;
		}
	}
	
	/**
	 * Deletes tags dot dot dot, sorry, I mean...
	 * 
	 * @param	array		$metaIds	Array of meta ids to delete
	 * @return	@e void
	 */
	public function deleteByMetaId( $metaIds )
	{
		if ( empty( $metaIds ) )
		{
			return false;
		}
		
		/* Basic */
		if ( ! is_array( $metaIds ) )
		{
			$metaIds = array( $metaIds );
		}
		
		/* Fetch keys */
		foreach( $metaIds as $id )
		{
			$keys[] = $this->_getKey( array( 'meta_id' => $id ) );
		}
		
		/* Delete tags */
		$this->DB->delete( 'core_tags', 'tag_aai_lookup IN (\'' . implode( "','", $keys ) . '\')' );
		
		/* Delete perms */
		$this->DB->delete( 'core_tags_perms', 'tag_perm_aai_lookup IN (\'' . implode( "','", $keys ) . '\')' );
		
		/* Delete cache */
		$this->DB->delete( 'core_tags_cache', 'tag_cache_key IN (\'' . implode( "','", $keys ) . '\')' );
	}
	
	/**
	 * Basic permission check
	 * @param	string	$what (add/remove/edit/create) [ add = add new tags to items, create = create unique tags ]
	 * @param	array	$where data
	 */
	public function can( $what, $where )
	{
		/* Tagging disabled? */
		if ( ! $this->settings['tags_enabled'] )
		{
			return false;
		}
		
		/* Admins are god */
		if ( $this->memberData['g_is_supmod'] )
		{
			return true;
		}
		
		/* Member disabled */
		if ( $this->memberData['bw_disable_tagging'] )
		{
			return false;
		}
		
		/* Group disabled */
		if ( $this->memberData['gbw_disable_tagging'] )
		{
			return false;
		}
		
		if ( $what == 'prefix' )
		{
			/* Member disabled */
			if ( $this->memberData['bw_disable_prefixes'] )
			{
				return false;
			}
			
			/* Group disabled */
			if ( $this->memberData['gbw_disable_prefixes'] )
			{
				return false;
			}
		}
		
		return null;
	}
	
	/**
	 * Fetches tags from the debbie, I mean db. Avoiding the cache
	 * @param int 		meta ID
	 * @param string	Tag to match (foo matches foo, foobar, foobaz, etc)
	 */
	public function getRawTagsByMetaId( $metaId, $text='' )
	{
		$tags  = array();
		$where = ( ! empty( $text ) ) ? ' AND tag_text LIKE \'' . $this->DB->addSlashes( $text ) . '%\'' : '';
		
		$this->DB->build( array( 'select' => '*',
								 'from'   => 'core_tags',
								 'where'  => 'tag_aai_lookup=\'' . $this->_getKey( array( 'meta_id' => $metaId ) ) . '\'' . $where,
								 'order'  => 'tag_id' ) );
		
		$this->DB->execute();
		
		while( $row = $this->DB->fetch() )
		{
			$tags[ $row['tag_id'] ] = $row;
		}
		
		return $tags;
	}
	
	/**
	 * Fetches tags from the debbie, I mean db. Avoiding the cache
	 * @param int meta ID
	 * @param string	Tag to match (foo matches foo, foobar, foobaz, etc)
	 */
	public function getRawTagsByParentId( $parentId, $text='' )
	{
		$tags  = array();
		$where = ( ! empty( $text ) ) ? ' AND tag_text LIKE \'' . $this->DB->addSlashes( $text ) . '%\'' : '';
		
		$this->DB->build( array( 'select' => '*',
								 'from'   => 'core_tags',
								 'where'  => 'tag_aap_lookup=\'' . $this->_getKey( array( 'meta_parent_id' => $parentId ) ) . '\'' . $where ) );
		
		$this->DB->execute();
		
		while( $row = $this->DB->fetch() )
		{
			$tags[ $row['tag_id'] ] = $row;
		}
		
		return $tags;
	}
	
	/**
	 * Get tags main interface
	 * 
	 * @param		array	$where
	 * @return		array	Formatted data
	 */
	public function getTagsByMetaId( $where )
	{
		/* get from cache */
		$tags = $this->_getCache( $where );

		return $this->_formatCachedData( $tags );
	}
	
	/**
	 * Get tags by cache key interface
	 *
	 * @param		array	$where
	 * @return		array	Formatted data
	 */
	public function getTagsByCacheKey( $key )
	{
		/* get from cache */
		$tags = $this->_getCache( array( 'tag_cache_key' => $key ) );
	
		return $this->_formatCachedData( $tags );
	}
	
	/**
	 * Format tags for display
	 * @param array $tagData array( 'tag', 'tag', 'tag' )
	 * @param string $tag_aai_lookup (tag_cache_key)
	 * @return string
	 */
	public function formatTagsForDisplay( $tagData, $tag_aai_lookup='' )
	{
		$array		= array();
		$string		= array();
		$truncated	= array();
		$linked		= array();
		$diff		= 0;
		$text		= '';
		$len		= 0;
		
		if ( ! is_array( $tagData ) )
		{
			return null;
		}
		
		/* System enabled? */
		if ( ! $this->_isEnabled() )
		{
			return null;
		}
		
		foreach( $tagData as $t )
		{
			$array[]	= $this->registry->output->getTemplate( $this->skin() )->tagEntry( $t, false, $this->getApp(), $this->getSearchSection() );
			$string[]	= $t;
			
			$len += ( IPSText::mbstrlen( $t ) ) + 2;
			
			if ( $len <= $this->settings['tags_max_truncated_len'] )
			{
				$truncated[]	= $t;
				$linked[]		= $this->registry->output->getTemplate( $this->skin() )->tagEntry( $t, true, $this->getApp(), $this->getSearchSection() );
			}
			
			if ( count( $array ) > count( $truncated ) )
			{
				$diff = count( $array ) - count( $truncated );
				$text = ' <span hovercard-ref=\'tagsPopUp\' hovercard-id=\'' . $tag_aai_lookup . '\' class=\'_hovertrigger clickable\'>' . sprintf( $this->lang->words['tag_trunc_has_more'], $diff ) . '</span>';
			}
		}
		
		return array( 'array'				=> $array,
					  'parsed'				=> implode( ', ', $array ),
					  'parsedWithoutComma'	=> implode( ' ' , $array ),
					  'truncatedNoText'		=> implode( ', ', $truncated ),
					  'truncated'			=> implode( ', ', $truncated ) . $text,
					  'truncatedCount'		=> count( $truncated ),
					  'truncatedWithLinks'	=> implode( ', ', $linked ) . $text,
					  'totalCount'			=> count( $array ),
					  'string'				=> implode( ', ', $string ) );
	}
	
	/**
	 * DEFAULT: returns nothing and should be defined in your own class
	 * @param 	array	Where Data
	 * @return	int		Id of parent if one exists or 0
	 */
	public function getParentId( $where )
	{
		return 0;
	}
	
	/**
	 * DEFAULT: returns nothing and should be defined in your own class
	 * @param 	array	Where Data
	 * @return	string	Comma delimiter or *
	 */
	public function getPermissionData( $where )
	{
		return '*';
	}
	
	/**
	 * DEFAULT: returns true and should be defined in your own class
	 * @param 	array	Where Data
	 * @return	int		If meta item is visible (not unapproved, etc)
	 */
	public function getIsVisible( $where )
	{
		return 1;
	}
	
	/**
	 * Enter description here ...
	 * @param array $array (meta_id_field => x.field_id );
	 */
	public function getCacheJoin( $array )
	{
		/* Tags disabled? */
		if ( ! $this->_isEnabled() )
		{
			return null;
		}
		
		$p = ( empty( $array['tag_table_prefix'] ) ) ? 'xxx' : $array['tag_table_prefix'];
		
		return array( 'select' => $p.'.*',
					  'from'   => array( 'core_tags_cache' => $p ),
					  'type'   => 'left',
					  'where'  => $p.'.tag_cache_key=' . $this->DB->buildMd5Statement( $this->DB->buildConcat( array( array( $this->getApp(), 'string' ),  array( ';', 'string'), array( $this->getArea(), 'string' ), array( ';',  'string' ), array( $array['meta_id_field'] ) ) ) ) );
	}
	
	/**
	 * Format joined data for display
	 * 
	 * @param		array	$tags
	 * @return		array	Formatted data
	 */
	public function formatCacheJoinData( $tags )
	{
		return $this->_formatCachedData( $tags );
	}
	
	/**
	 * Moves tags from one meta ID to another (merging, etc) ...
	 * @param array $oldMetaId array
	 * @param int   $newMetaId
	 */
	public function moveTagsByMetaId( $oldMetaIds, $newMetaId )
	{
		$keys      = array();
		$newKey    = $this->_getKey( array( 'meta_id' => $newMetaId ) );
		$raw       = array();
		$hasPrefix = false;
		
		/* Basic */
		if ( ! is_array( $oldMetaIds ) )
		{
			$oldMetaIds = array( $oldMetaIds );
		}
		
		/* Fetch keys */
		foreach( $oldMetaIds as $id )
		{
			$raw[ $id ] = $this->getRawTagsByMetaId( $id );
			$keys[]     = $this->_getKey( array( 'meta_id' => $id ) );
			$this->_deleteStoredPermissionDataByMetaId( $id );
			$this->_deleteCache( array( 'meta_id' => $id ) );
		}
		
		/* Figure out if we have a prefix or not */
		if ( count( $raw ) )
		{
			foreach( $raw as $_id => $array )
			{
				foreach( $raw[ $_id ] as $id => $tag )
				{
					if ( $tag['tag_prefix'] )
					{
						$hasPrefix = true;
					}
				}
			}
		}
		
		/* Update tags */
		$this->DB->update( 'core_tags', array( 'tag_meta_id'    => $newMetaId,
											   'tag_aai_lookup' => $newKey ), 'tag_aai_lookup IN (\'' . implode( "','", $keys ) . '\')' );
		
		/* Update permissions */
		$parent      = $this->getParentId( array( 'meta_id' => $newMetaId ) );
		$permissions = $this->getPermissionData( array( 'meta_id' => $newMetaId ) );
		$isVisible   = $this->getIsVisible( array( 'meta_id' => $newMetaId ) );
		
		/* Update permission row */
		$this->_setStoredPermissionData( array( 'meta_id'           => $newMetaId,
												'meta_parent_id'	=> $parent,
												'permission_string' => $permissions,
												'meta_visible'		=> $isVisible ) );
		
		/* Get new tags to remove dupes */
		$tags = $this->getTagsByMetaId( $newMetaId );
		$this->registry->tags->replace( $tags['tags'], array( 'meta_id' => $newMetaId ) ); 
		
		/* Reset prefix ID */
		if ( $hasPrefix )
		{
			$this->resetPrefix( $newMetaId );
		}
		
		/* Rebuild cache */
		$this->_addCache( array( 'meta_id' => $newMetaId, 'meta_parent_id' => $parent ) );
	}
	
	/**
	 * Moves tags from one parent to another ...
	 * @param array $where
	 * @param int   $newParentId
	 */
	public function moveTagsByParentId( $oldParentId, $newParentId )
	{
		/* Update parent */
		$permissions  = $this->getPermissionData( array( 'meta_parent_id' => $newParentId ) );
		$parentKeyOld = $this->_getKey( array( 'meta_parent_id' => $oldParentId ) );
		$parentKeyNew = $this->_getKey( array( 'meta_parent_id' => $newParentId ) );
		
		/* Update perms  */
		$this->DB->update( 'core_tags_perms', array( 'tag_perm_aap_lookup' => $parentKeyNew, 'tag_perm_text' => $permissions ), 'tag_perm_aap_lookup=\'' . $parentKeyOld . '\'' );
		
		/* Update tags */
		$this->DB->update( 'core_tags', array( 'tag_meta_parent_id' => $newParentId ), 'tag_meta_parent_id=' . intval( $oldParentId ) );
	}
	
	/**
	 * Moves tags to another parent, by meta id
	 * @param array $oldMetaId array
	 * @param int   $newParentId
	 */
	public function moveTagsToParentId( $oldMetaIds, $newParentId )
	{
		$keys   = array();
		
		/* Basic */
		if ( ! is_array( $oldMetaIds ) )
		{
			$oldMetaIds = array( $oldMetaIds );
		}
		
		/* Fetch keys */
		foreach( $oldMetaIds as $id )
		{
			$keys[] = $this->_getKey( array( 'meta_id' => $id ) );
			$this->_deleteCache( array( 'meta_id' => $id ) );
		}
				
		/* Update tags */
		$this->DB->update( 'core_tags', array( 'tag_meta_parent_id' => $newParentId ), 'tag_aai_lookup IN (\'' . implode( "','", $keys ) . '\')' );
		
		/* Update parent */
		$permissions  = $this->getPermissionData( array( 'meta_parent_id' => $newParentId ) );
		$parentKeyNew = $this->_getKey( array( 'meta_parent_id' => $newParentId ) );
		
		/* Update perms  */
		$this->DB->update( 'core_tags_perms', array( 'tag_perm_aap_lookup' => $parentKeyNew, 'tag_perm_text' => $permissions ), 'tag_perm_aai_lookup IN (\'' . implode( "','", $keys ) . '\')' );

		/* Update caches */
		foreach( $oldMetaIds as $id )
		{
			$this->_addCache( array( 'meta_id' => $id, 'meta_parent_id' => $newParentId ) );
		}
	}
	
	/**
	 * Updates all permissions by parent Id
	 * @param int $parentId
	 */
	public function updatePermssionsByParentId( $parentId )
	{
		$parentKey    = $this->_getKey( array( 'meta_parent_id' => $parentId ) );
		$permissions  = $this->getPermissionData( array( 'meta_parent_id' => $parentId ) );
		
		/* Update perms  */
		$this->DB->update( 'core_tags_perms', array( 'tag_perm_text' => $permissions ), 'tag_perm_aap_lookup=\'' . $parentKey . '\'' );
	}
	
	/**
	 * Updates visibility by parent ID
	 * @param int $parentId
	 * @param int	
	 */
	public function updateVisibilityByParentId( $parentId, $visible=1 )
	{
		$parentKey = $this->_getKey( array( 'meta_parent_id' => $parentId ) );
		
		/* Update perms  */
		$this->DB->update( 'core_tags_perms', array( 'tag_perm_visible' => intval($visible) ), 'tag_perm_aap_lookup=\'' . $parentKey . '\'' );
	}
	
	/**
	 * Updates visibility by meta ID
	 * @param array $metaIds
	 * @param int	
	 */
	public function updateVisibilityByMetaId( $metaIds, $visible=1 )
	{
		$keys = array();
		
		/* Basic */
		if ( ! is_array( $metaIds ) )
		{
			$metaIds = array( $metaIds );
		}
		
		/* Fetch keys */
		foreach( $metaIds as $id )
		{
			$keys[] = $this->_getKey( array( 'meta_id' => $id ) );
		}
		
		/* Update perms  */
		$this->DB->update( 'core_tags_perms', array( 'tag_perm_visible' => intval($visible) ), "tag_perm_aai_lookup IN ('" . implode( "','", $keys ) . "')" );
	}
	
	/**
	 * Render something
	 * 
	 * @param	string	view to show
	 * @param	array	Where data to show
	 */
	public function render( $what, $where )
	{
		/* System enabled? */
		if ( ! $this->_isEnabled() )
		{
			return '';
		}
		
		$where   = $this->_cleanWhere( $where );
		
		$tags    = array( 'array' => array(), 'string' => '' );
		$options = array( 'isOpenSystem'    => $this->_isOpenSystem(),
						  'isEnabled'	    => $this->_isEnabled(),
						  'minLen' 		    => $this->_getMinLen(),
						  'maxLen'		    => $this->_getMaxLen(),
						  'minTags'		    => $this->_getMinTags(),
						  'maxTags'		    => $this->_getMaxTags(),
						  'predefinedTags'  => $this->_getPreDefinedTags( $where ),
						  'fieldId'			=> $this->_getFieldId( $where ),
						  'prefixesEnabled' => $this->_prefixesEnabled( $where ),
						  'meta_app'		=> $this->getApp(),
						  'meta_area'		=> $this->getArea(),
						  'meta_parent_id'	=> intval( $where['meta_parent_id'] ) );
		switch( $what )
		{
			case 'entryBox':
				/* set up some basic data */
				if ( ! empty( $where['meta_id'] ) AND empty( $where['existing_tags'] ) )
				{
					/* fetch current tags if available */
					$tags = $this->getTagsByMetaId( $where );
				}
				else if ( ! empty( $where['existing_tags'] ) )
				{
					$where['existing_tags'] = $this->_cleanTags( implode( ',', $where['existing_tags'] ), false );
					
					$tags	= array( 'tags'		 => $where['existing_tags'],
									 'prefix'	 => '',
									 'formatted' => $this->formatTagsForDisplay( $where['existing_tags'], $this->_getKey( array( 'meta_id' => $where['meta_id'] ) ) ) );
				}

				return $this->registry->output->getTemplate( $this->skin() )->tagTextEntryBox( $tags, $options, $where );
			break;
		}
	}
	
	/**
	 *  Rebuild cache
	 *  @param		array	$where
	 *  @return		void
	 */
	public function rebuildCache( $where )
	{
		$this->_deleteCache( $where );
		$this->_addCache( $where );
	}
	
	/**
	 * Search for tags
	 * @param mixed $tags	Array or string
	 * @param array $options	Array( 'meta_id' (array), 'meta_parent_id' (array), 'olderThan' (int), 'youngerThan' (int), 'limit' (int), 'sortKey' (string) 'sortOrder' (string) )
	 */
	public function search( $tags, $options )
	{
		/* Fetch search class */
		require_once( IPS_ROOT_PATH . 'sources/classes/tags/search/bootstrap.php');/*noLibHook*/
		$search = classes_tags_search_bootstrap::init( $this->getApp(), $this->getArea() );
		
		/* fix up tags */
		$tags = $this->_cleanTags( $tags, FALSE );
		
		if ( $tags === false )
		{
			/* Error message is populated at this point */
			return false;
		}
		
		$metaIds = $search->run( $tags, $options );
		
		return $metaIds;
	}
	
	/**
	 * DEFAULT: Should define this in your own class if you want it differenly displayed
	 * @return	string	Template group
	 */
	protected function skin()
	{
		return 'global_other';
	}
	
	/**
	 * Formats cached data
	 * @param 	array $tags
	 * @return	array
	 */
	protected function _formatCachedData( $tags )
	{
		if ( ! is_array( $tags ) OR ! count( $tags ) OR empty( $tags['tag_cache_text'] ) )
		{
			return null;
		}
		
		/* Unserialise */
		if ( ! IPSLib::isSerialized( $tags['tag_cache_text'] ) )
		{
			return null;
		}
		
		$tagData = unserialize( $tags['tag_cache_text'] );
		
		$tagData['formatted']           = array();
		
		if ( is_array($tagData['tags']) AND count($tagData['tags']) )
		{
			$tagData['formatted']           = $this->formatTagsForDisplay( $tagData['tags'], $tags['tag_cache_key'] );
			$tagData['formatted']['prefix']	= false;
		}
		else
		{
			return null;
		}
		
		if( ! empty( $tagData['prefix'] ) )
		{
			$tagData['formatted']['prefix'] = $this->registry->output->getTemplate( $this->skin() )->tagPrefix( $tagData['prefix'], $this->getApp(), $this->getSearchSection() );
		}
		
		return $tagData;
	}
	
	/**
	 *  Rebuild cache
	 *  @param		array	$where
	 *  @return		void
	 */
	protected function _addCache( $where )
	{
		$where = $this->_cleanWhere( $where );
		
		if ( empty( $where['meta_id'] ) )
		{
			trigger_error( "Meta ID missing", E_USER_ERROR );
		}
		
		$key  = $this->_getKey( $where );
		$tags = $this->getRawTagsByMetaId( $where['meta_id'] );
		$text = array( 'tags' => array(), 'prefix' => '' );
		
		foreach( $tags as $id => $tag )
		{
			$text['tags'][] = $tag['tag_text'];
			
			if ( $tag['tag_prefix'] )
			{
				$text['prefix'] = $tag['tag_text'];
			}
		}
		
		$update = array( 'tag_cache_key'  => $key,
						 'tag_cache_text' => serialize( $text ),
						 'tag_cache_date' => IPS_UNIX_TIME_NOW
						);
		
		$this->DB->replace( 'core_tags_cache', $update, array( 'tag_cache_key' ) );
		
		return $update;
	}
	
	/**
	 *  Rebuild cache
	 *  @param		array	$where
	 *  @return		void
	 */
	protected function _deleteCache( $where )
	{
		$where = $this->_cleanWhere( $where );
		
		if ( empty( $where['meta_id'] ) )
		{
			trigger_error( "Meta ID missing", E_USER_ERROR );
		}
		
		$key  = $this->_getKey( $where );
		
		$this->DB->delete( 'core_tags_cache', 'tag_cache_key=\'' . $key . '\'' );
	}	
	
	/**
	 *  Rebuild cache
	 *  @param		array	$where
	 *  @return		void
	 */
	protected function _getCache( $where )
	{
		$where = $this->_cleanWhere( $where );
		
		if ( ! empty( $where['cache_key'] ) )
		{
			$key = $where['cache_key'];
		}
		else
		{
			if ( empty( $where['meta_id'] ) )
			{
				trigger_error( "Meta ID missing", E_USER_ERROR );
			}
			
			$key	= $this->_getKey( $where );
		}
		
		$cache	= $this->DB->buildAndFetch( array( 'select' => '*',
												   'from'   => 'core_tags_cache',
												   'where'  => 'tag_cache_key=\'' . $key . '\'' ) );
												   
		if ( ! $cache['tag_cache_key'] )
		{
			$cache = $this->_addCache( $where );
		}
		
		return $cache;
	}	
	
	/**
	 * Sets a permission row
	 * @param array $data
	 */
	protected function _setStoredPermissionData( $data )
	{
		$this->DB->replace( 'core_tags_perms', array( 'tag_perm_aai_lookup' => $this->_getKey( array( 'meta_id' => $data['meta_id'] ) ),
													  'tag_perm_aap_lookup' => $this->_getKey( array( 'meta_parent_id' => $data['meta_parent_id'] ) ),
													  'tag_perm_text'		=> trim( IPSText::cleanPermString( $data['permission_string'] ) ),
													  'tag_perm_visible'    => intval( $data['meta_visible'] ) ), array( 'tag_perm_aai_lookup' ) );
		
	}
																 
	/**
	 * Return a permission row for a TID
	 * @param int $metaId
	 * @return array
	 */
	protected function _getStoredPermissionDataByMetaId( $metaId )
	{
		$permRow = $this->DB->build( array( 'select'   => '*',
								 			'from'     => 'core_tags_perms',
							     			'where'    => 'tag_perm_aai_lookup=\'' . $this->_getKey( array( 'meta_id' => $metaId ) ) . '\'' ) );
		return $permRow;
	}
	
	/**
	 * Delete a permission row for a TID
	 * @param int $metaId
	 * @return array
	 */
	protected function _deleteStoredPermissionDataByMetaId( $metaId )
	{
		$this->DB->delete( 'core_tags_perms', 'tag_perm_aai_lookup=\'' . $this->_getKey( array( 'meta_id' => $metaId ) ) . '\'' );
	}
	
	/**
	 * Get text field name (future expansion)
	 * @param 	array	Where Data
	 * @return 	Booyaleean
	 */
	protected function _getFieldId( $where )
	{
		return 'ipsTags';	
	}
	
	/**
	 * Get maximum tags allowed
	 * 
	 * @return 	Booyaleean
	 */
	protected function _getMaxTags()
	{
		return intval( $this->settings['tags_max'] );	
	}
	
	/**
	 * Get minimum tags allowed
	 * 
	 * @return 	Booyaleean
	 */
	protected function _getMinTags()
	{
		return intval( $this->settings['tags_min'] );	
	}
	
	/**
	 * Get maximum tag length allowed
	 * 
	 * @return 	Booyaleean
	 */
	protected function _getMaxLen()
	{
		return intval( $this->settings['tags_len_max'] );	
	}
	
	/**
	 * Get minimum tag length allowed
	 * 
	 * @return 	Booyaleean
	 */
	protected function _getMinLen()
	{
		return intval( $this->settings['tags_len_min'] );	
	}
	
	/**
	 * Must clean words before they are added
	 * 
	 * @return 	Booyaleean
	 */
	protected function _mustCleanWords()
	{
		return ( $this->settings['tags_clean'] ) ? true : false;	
	}
	
	/**
	 * Can set an item as a topic prefix
	 * @param 	array	Where Data
	 * @return 	Booyaleean
	 */
	protected function _prefixesEnabled( $where )
	{
		switch( $this->settings['tags_can_prefix'] )
		{
			case 0:
				return false;
			break;
			case 1:
				return ( $this->memberData['g_is_supmod'] ) ? true : false;
			break;
			case 2:
				return $this->can( 'prefix', $where );
			break;
		}	
	}
	
	/**
	 * Is this an open system
	 * Open is where anyone can create tags, closed is where they are pre-defined
	 * 
	 * @return 	Booyaleean
	 */
	protected function _isOpenSystem()
	{
		return ( $this->settings['tags_open_system'] ) ? true : false;	
	}
	
	/**
	 * Force all tags to lowercase?
	 * 
	 * @return 	Booyaleean
	 */
	protected function _forceLower()
	{
		return ( $this->settings['tags_force_lower'] ) ? true : false;	
	}
	
	/**
	 * Is tagging enabled?
	 * 
	 * @return 	Booyaleean
	 */
	protected function _isEnabled()
	{
		return ( $this->settings['tags_enabled'] ) ? true : false;	
	}
	
	/**
	 * Fetch a list of pre-defined tags
	 * 
	 * @param 	array	Where Data
	 * @return	Array of pre-defined tags or null
	 */
	protected function _getPreDefinedTags( $where=array() )
	{
		if ( $this->settings['tags_predefined'] )
		{
			$this->settings['tags_predefined'] = str_replace( array( "<br>", "<br />", "\r", "," ), "\n", $this->settings['tags_predefined'] );
			$array = explode( "\n", $this->settings['tags_predefined'] );
			$final = array();
			
			foreach( $array as $a )
			{
				$final[] = trim( $a );
			}
			
			return $final;
		}
		else
		{
			return null;
		}
	}
	
	/**
	 * Strip/remove HTML ...
	 * @param string $tag
	 */
	private function _stripHtml( $tag )
	{
		return str_replace( array( '&#62;', '&#60;', '&amp;', '&quot;', '&#39;', '<', '>', '"', "'" ), '', $tag );
	}
	
	/**
	 * Build a key
	 */
	private function _getKey( $where )
	{
		$where = $this->_cleanWhere( $where );
		
		if ( isset( $where['meta_id'] ) )
		{
			return md5( $this->getApp() . ';' . $this->getArea() . ';' . intval( $where['meta_id'] ) );
		}
		else if ( isset( $where['meta_parent_id'] ) )
		{
			return md5( $this->getApp() . ':' . $this->getArea() . ':' . intval( $where['meta_parent_id'] ) );
		}
	}
	
	/**
	 * Cleans incoming tags
	 * @param	String or Array		Comma delim string or array of tags
	 * @param	Bool				If TRUE, will check minimum and maximum amounts of tags - not necessary for searching
	 * @return	Array				Array of cleaned tags
	 */
	private function _cleanTags( $tags, $checkForMinimumAndMaximum=TRUE )
	{
		/* Sort out tags */
		if ( ! is_array( $tags ) )
		{
			if ( strstr( $tags, ',' ) )
			{
				$_tags = explode( ',', IPSText::cleanPermString( $tags ) );
				$tags  = array();
				
				foreach( $_tags as $t )
				{
					if ( $t )
					{
						$tags[] = $this->_stripHtml( trim( ( $this->_forceLower() ? IPSText::mbstrtolower( $t ) : $t ) ) );
					}
				}
			}
			else
			{
				if ( ! strlen( $tags ) )
				{
					return false;
				}
				
				$tags = array( $this->_stripHtml( ( $this->_forceLower() ? IPSText::mbstrtolower( $tags ) : $tags ) ) );
			}
		}
				
		/* So.. got tags to parse? */
		if ( count( $tags ) )
		{
			/* Make sure they are all unique */
			$tags = array_unique( $tags );
			
			/* Check for min/max string length */
			if ( $checkForMinimumAndMaximum and ( $this->_getMaxLen() OR $this->_getMinLen() ) )
			{
				$_tags = $tags;
				$tags  = array();
				
				foreach( $_tags as $tag )
				{
					if ( $this->_getMaxLen() )
					{
						if ( IPSText::mbstrlen( $tag ) > $this->_getMaxLen() )
						{
							continue;
						}
					}
					
					if ( $this->_getMinLen() )
					{
						if ( IPSText::mbstrlen( $tag ) < $this->_getMinLen() )
						{
							continue;
						}
					}
					
					$tags[] = $tag;
				}
			}
			
			/* removes any bad words */
			$badwords = $this->cache->getCache('badwords');
	
			if ( $this->_mustCleanWords() AND ( is_array( $badwords ) && count( $badwords ) ) )
			{
				$_tags = $tags;
				$tags  = array();
				
				foreach( $_tags as $tag )
				{
					$_bad	= false;
					
					foreach( $badwords as $badword )
					{
						if( strtolower($tag) == strtolower($badword['type']) )
						{
							$_bad	= true;
							break;
						}
					}
					
					if( !$_bad )
					{
						$tags[] = $tag;
					}
				}
			}
		}
				
		/* Now, do we have a sufficient number of tags? */
		if ( $checkForMinimumAndMaximum && $this->_getMaxTags() && count( $tags ) > $this->_getMaxTags() )
		{
			$this->setErrorMsg('too_many_tags');
			return false;
		}
		
		/* Perhaps not enough? */
		if ( $checkForMinimumAndMaximum && $this->_getMinTags() && count( $tags ) < $this->_getMinTags() )
		{
			$this->setErrorMsg('too_few_tags');
			return false;
		}
		
		/* Generic catch all in case min/max tags aren't set up. */
		if ( ! count( $tags ) )
		{
			$this->setErrorMsg('no_good_tags');
			return false;
		}
		
		/* Phew. */
		return $tags;
	}
	
	/**
	 * Clean where stuffs
	 * Ensures that DB naming conventions aren't used
	 * @param	Array	Dirty where
	 * @return	Array	Clean where
	 */
	private function _cleanWhere( $where )
	{
		$clean = array( 'tag_meta_app', 'tag_meta_area', 'tag_meta_id', 'tag_meta_parent_id', 'tag_member_id', 'tag_aai_lookup', 'tag_cache_key' );
		
		if ( is_numeric( $where ) )
		{
			$where = array( 'meta_id' => $where );
		}
		
		if ( is_array( $where ) )
		{
			foreach( $where as $k => $v )
			{
				if ( in_array( $k, $clean ) )
				{
					unset( $where[ $k ] );
					$where[ str_replace( 'tag_', '', $k ) ] = $v;
				}
			}
		}
		
		if ( empty( $where['meta_app'] ) )
		{
			$where['meta_app'] = $this->getApp();
		}
		
		if ( empty( $where['meta_area'] ) )
		{
			$where['meta_area'] = $this->getArea();
		}
		
		return $where;
	}
	
}