<?php

namespace wpforo\classes;

// Exit if accessed directly
if( ! defined( 'ABSPATH' ) ) exit;

class Topics {
	static $cache = [ 'topics' => [], 'tags' => [], 'item' => [], 'topic' => [], 'tag' => [], 'forum_slug' => [] ];
	
	function __construct() {
		$this->init_hooks();
	}
	
	private function init_hooks() {
		add_action( 'wpforo_after_add_post', [ $this, 'after_add_post' ], 10, 2 );
		add_action( 'wpforo_after_delete_post', [ $this, 'after_delete_post' ] );
		add_action( 'wpforo_post_status_update', [ $this, 'after_post_status_update' ] );
		add_action( 'wpforo_after_delete_user', [ $this, 'after_delete_user' ], 11, 2 );
	}
	
	public function get_cache( $var ) {
		if( isset( self::$cache[ $var ] ) ) return self::$cache[ $var ];
		
		return [];
	}
	
	public function reset() {
		self::$cache = [ 'topics' => [], 'tags' => [], 'item' => [], 'topic' => [], 'tag' => [], 'forum_slug' => [] ];
	}

	/**
	 * Get appropriate error message for flood protection
	 *
	 * @param string $reason The flood reason code
	 * @return string Error message
	 */
	private function get_flood_error_message( $reason ) {
		switch( $reason ) {
			case 'temp_ban':
				$remaining = WPF()->perm->get_flood_ban_remaining();
				$minutes = ceil( $remaining / 60 );
				return sprintf(
					__( 'You have been temporarily blocked for posting too quickly. Please wait %d minute(s) before trying again.', 'wpforo' ),
					$minutes
				);
			case 'per_minute':
				return __( 'You are posting too quickly. Please wait a moment before creating another topic.', 'wpforo' );
			case 'per_hour':
				return __( 'You have reached the hourly posting limit. Please wait before creating more topics.', 'wpforo' );
			case 'ip_per_hour':
				return __( 'Too many posts from your location. Please wait before trying again.', 'wpforo' );
			case 'interval':
			default:
				return __( 'You are posting too quickly. Slow down.', 'wpforo' );
		}
	}

	public function edit( $args = [] ) {
		if( empty( $args ) && empty( $_REQUEST['thread'] ) ) return false;
		if( ! isset( $args['topicid'] ) && isset( $_GET['id'] ) ) $args['topicid'] = intval( $_GET['id'] );
		if( empty( $args ) && ! empty( $_REQUEST['thread'] ) ) $args = $_REQUEST['thread'];
		if( isset( $args['name'] ) ) {
			$args['name'] = strip_tags( (string) $args['name'] );
		}
		if( isset( $args['email'] ) ) {
			$args['email'] = sanitize_email( $args['email'] );
		}
		
		
		if( ! $topic = $this->get_topic( $args['topicid'] ) ) {
			WPF()->notice->add( 'Topic not found.', 'error' );
			
			return false;
		}
		
		if( ! $forum = WPF()->forum->get_forum( $topic['forumid'] ) ) {
			WPF()->notice->add( 'Forum not found.', 'error' );
			
			return false;
		}
		
		do_action( 'wpforo_start_edit_topic', $args, $forum );
		
		if( ! is_user_logged_in() ) {
			if( ! isset( $topic['email'] ) || ! $topic['email'] ) {
				WPF()->notice->add( 'Permission denied', 'error' );
				
				return false;
			} elseif( ! wpforo_current_guest( $topic['email'] ) ) {
				WPF()->notice->add( 'You are not allowed to edit this post', 'error' );
				
				return false;
			}
		}
		
		$args['status'] = $topic['status'];
		$args['userid'] = $topic['userid'];
		
		$args = apply_filters( 'wpforo_edit_topic_data_filter', $args, $forum );
		if( empty( $args ) ) return false;
		
		if( $min = wpforo_setting( 'posting', 'topic_body_min_length' ) ) {
			if( wpfkey( $args, 'body' ) && (int) $min > wpforo_length( $args['body'] ) ) {
				WPF()->notice->add( 'The content is too short', 'error' );
				
				return false;
			}
		}
		
		// Security: extract only expected keys to prevent variable injection (e.g., $topic/$forum override)
		$topicid = isset( $args['topicid'] ) ? $args['topicid'] : null;
		$title   = isset( $args['title'] ) ? $args['title'] : null;
		$type    = isset( $args['type'] ) ? $args['type'] : null;
		$status  = isset( $args['status'] ) ? $args['status'] : null;
		$private = isset( $args['private'] ) ? $args['private'] : null;
		$name    = isset( $args['name'] ) ? $args['name'] : null;
		$email   = isset( $args['email'] ) ? $args['email'] : null;
		$body    = isset( $args['body'] ) ? $args['body'] : null;
		$tags    = isset( $args['tags'] ) ? $args['tags'] : null;

		if( isset( $topicid ) ) $topicid = intval( $topicid );
		if( isset( $title ) ) $title = sanitize_text_field( trim( (string) $title ) );
		if( isset( $type ) ) $type = intval( $type );
		if( isset( $status ) ) $status = intval( $status );
		if( isset( $private ) ) $private = intval( $private );
		if( isset( $name ) ) $name = strip_tags( trim( (string) $name ) );
		if( isset( $email ) ) $email = strip_tags( trim( (string) $email ) );
		if( isset( $body ) ) $body = wpforo_kses( trim( (string) $body ) );
		if( isset( $tags ) ) {
			if( isset( $topic['forumid'] ) && wpforo_is_module_enabled( 'tags' ) && WPF()->perm->forum_can(
					'tag',
					$topic['forumid']
				) ) {
				$tags = $this->sanitize_tags( $tags, false, true );
			} else {
				$tags = '';
			}
		}
		
		
		if( ! isset( $topicid ) ) {
			WPF()->notice->add( 'Topic edit error', 'error' );
			
			return false;
		}
		
		if( isset( $body ) ) $body = preg_replace( '#</pre>[\r\n\t\s\0]*<pre>#isu', "\r\n", (string) $body );
		
		$diff = time() - strtotime( $topic['created'] . ' GMT' );
		if( ! ( WPF()->perm->forum_can( 'et', $topic['forumid'] ) || ( WPF()->current_userid == $topic['userid'] && WPF()->perm->forum_can( 'eot', $topic['forumid'] ) ) ) ) {
			WPF()->notice->add( 'You have no permission to edit this topic', 'error' );
			
			return false;
		}
		
		if( ! WPF()->perm->forum_can( 'et', $topic['forumid'] ) && wpforo_setting(
			                                                           'posting',
			                                                           'edit_own_topic_durr'
		                                                           ) !== 0 && $diff > wpforo_setting(
				'posting',
				'edit_own_topic_durr'
			) ) {
			WPF()->notice->add( 'The time to edit this topic is expired', 'error' );
			
			return false;
		}
		
		$title      = ( isset( $title ) ? stripslashes( $title ) : stripslashes( (string) $topic['title'] ) );
		$type       = ( isset( $type ) ? $type : intval( $topic['type'] ) );
		$status     = ( isset( $status ) ? $status : intval( $topic['status'] ) );
		$private    = ( isset( $private ) ? $private : intval( $topic['private'] ) );
		// Force private for ticket_forum type forums (all topics must be private)
		if( wpfval( $forum, 'type' ) === 'ticket_forum' ) {
			$private = 1;
		}
		$has_attach = ( isset( $body ) ? ( strpos(
			                                   (string) $body,
			                                   '[attach]'
		                                   ) !== false ? 1 : 0 ) : $topic['has_attach'] );
		$name       = ( isset( $name ) ? stripslashes( $name ) : stripslashes( (string) $topic['name'] ) );
		$email      = ( isset( $email ) ? stripslashes( $email ) : stripslashes( (string) $topic['email'] ) );
		$tags       = ( isset( $tags ) ? $tags : '' );
		
		$t_update = WPF()->db->update(
			WPF()->tables->topics,
			[
				'title'      => $title,
				'type'       => $type,
				'status'     => $status,
				'private'    => $private,
				'has_attach' => $has_attach,
				'name'       => $name,
				'email'      => $email,
				'tags'       => $tags,
			],
			[ 'topicid' => $topicid ],
			[ '%s', '%d', '%d', '%d', '%d', '%s', '%s', '%s' ], [ '%d' ]
		);
		
		if( isset( $topic['first_postid'] ) ) {
			if( ! $post = WPF()->post->get_post( $topic['first_postid'] ) ) {
				WPF()->notice->add( 'Topic first post data not found.', 'error' );
				
				return false;
			}
		} else {
			WPF()->notice->add( 'Topic first post not found.', 'error' );
			
			return false;
		}
		
		$body = ( ! $min || ( isset( $body ) && $body ) ) ? stripslashes( (string) $body ) : stripslashes(
			(string) $post['body']
		);
		
		$p_update = WPF()->db->update(
			WPF()->tables->posts,
			[
				'title'    => $title,
				'body'     => $body,
				'modified' => current_time(
					'mysql',
					1
				),
				'status'   => $status,
				'private'  => $private,
				'name'     => $name,
				'email'    => $email,
			],
			[ 'postid' => intval( $topic['first_postid'] ) ],
			[ '%s', '%s', '%s', '%d', '%d', '%s', '%s' ], [ '%d' ]
		);
		
		if( $t_update !== false && $p_update !== false ) {
			
			if( isset( $tags ) ) $this->edit_tags( $tags, $topic );
			
			$a = [
				'userid'       => $topic['userid'],
				'forumid'      => $topic['forumid'],
				'topicid'      => $topicid,
				'postid'       => $topic['first_postid'],
				'first_postid' => $topic['first_postid'],
				'title'        => $title,
				'body'         => $body,
				'status'       => $status,
				'private'      => $private,
				'name'         => $name,
				'email'        => $email,
				'type'         => $type,
				'has_attach'   => $has_attach,
				'tags'         => $tags,
			];
			do_action( 'wpforo_after_edit_topic', $a, $args, $forum );
			
			wpforo_clean_cache( 'topic-first-post', $topicid, $topic );
			WPF()->notice->add( 'Topic successfully updated', 'success' );
			
			return $topicid;
		}
		
		WPF()->notice->add( 'Topic edit error', 'error' );
		
		return false;
	}
	
	public function get_topic( $args = [], $protect = true ) {
		return wpforo_ram_get( [ $this, '_get_topic' ], $args, $protect );
	}
	
	public function add( $args = [] ) {
		
		if( empty( $args ) && empty( $_REQUEST['thread'] ) ) return false;
		if( empty( $args ) && ! empty( $_REQUEST['thread'] ) ) $args = $_REQUEST['thread'];
		if( $min = wpforo_setting( 'posting', 'topic_body_min_length' ) ) {
			if( wpfkey( $args, 'body' ) && (int) $min > wpforo_length( $args['body'] ) ) {
				WPF()->notice->add( 'The content is too short. It must be at least %1$d characters long.', 'error', $min );
				
				return false;
			}
		}
		$args['name']  = ( isset( $args['name'] ) ? strip_tags( (string) $args['name'] ) : '' );
		$args['email'] = ( isset( $args['email'] ) ? sanitize_email( $args['email'] ) : '' );
		
		if( ! isset( $args['forumid'] ) || ! $args['forumid'] = intval( $args['forumid'] ) ) {
			WPF()->notice->add( 'Add Topic error: No forum selected', 'error' );
			
			return false;
		}
		
		if( ! $forum = WPF()->forum->get_forum( $args['forumid'] ) ) {
			WPF()->notice->add( 'Add Topic error: No forum selected', 'error' );
			
			return false;
		}
		
		if( ! WPF()->perm->forum_can( 'ct', $args['forumid'] ) ) {
			WPF()->notice->add( 'You don\'t have permission to create topic into this forum', 'error' );
			
			return false;
		}
		
		// Skip flood check for AI-generated content (created by AI Tasks)
		if ( empty( $args['is_ai_generated'] ) ) {
			$flood_reason = '';
			$flood_result = WPF()->perm->can_post_now( $flood_reason );
			if( $flood_result === false ) {
				$message = $this->get_flood_error_message( $flood_reason );
				WPF()->notice->add( $message, 'error' );
				return false;
			}
			// If flood action is 'unapprove', force the topic to be unapproved
			if( $flood_result === 'unapprove' ) {
				$args['status'] = 1;
				$args['_flood_reason'] = $flood_reason; // Pass flood reason for moderation logging
			}
		}
		
		if( ! isset( $args['title'] ) || ! $args['title'] = wpforo_text(
				$args['title'],
				250,
				false,
				true,
				false,
				false,
				false
			) ) {
			WPF()->notice->add( 'Please insert required fields!', 'error' );
			
			return false;
		}
		
		if( ! is_user_logged_in() ) {
			if( $args['name'] && $args['email'] ) {
				WPF()->member->set_guest_cookies( $args );
			} else {
				$uqid = uniqid();
				if( ! trim( (string) $args['name'] ) ) $args['name'] = wpforo_phrase( 'Anonymous', false );
				if( ! trim( (string) $args['email'] ) ) $args['email'] = "anonymous_$uqid@example.com";
			}
		}
		
		do_action( 'wpforo_start_add_topic', $args, $forum );
		
		$root_exists  = wpforo_root_exist();
		$args['body'] = preg_replace( '#</pre>[\r\n\t\s\0]*<pre>#isu', "\r\n", (string) $args['body'] );
		$args['slug'] = ( isset( $args['slug'] ) && $args['slug'] ) ? sanitize_title(
			$args['slug']
		) : ( ( isset( $args['title'] ) ) ? sanitize_title( $args['title'] ) : md5( time() ) );
		if( ! trim( (string) $args['slug'] ) ) $args['slug'] = md5( time() );
		$args['slug']    = $this->unique_slug( $args['slug'] );
		$args['created'] = ( isset( $args['created'] ) ? sanitize_text_field( $args['created'] ) : current_time(
			'mysql',
			1
		) );
		$args['userid']  = ( isset( $args['userid'] ) && wpforo_current_user_is( 'moderator' ) ? intval( $args['userid'] ) : WPF()->current_userid );
		$args['name']    = ( isset( $args['name'] ) ? $args['name'] : '' );
		$args['email']   = ( isset( $args['email'] ) ? $args['email'] : '' );
		$args['tags']    = ( isset( $args['tags'] ) ? $args['tags'] : '' );
		
		$args = apply_filters( 'wpforo_add_topic_data_filter', $args, $forum );
		
		if( empty( $args ) ) return false;

		// Security: extract only expected keys to prevent variable injection
		$title    = isset( $args['title'] ) ? $args['title'] : null;
		$slug     = isset( $args['slug'] ) ? $args['slug'] : null;
		$created  = isset( $args['created'] ) ? $args['created'] : null;
		$userid   = isset( $args['userid'] ) ? $args['userid'] : null;
		$name     = isset( $args['name'] ) ? $args['name'] : null;
		$email    = isset( $args['email'] ) ? $args['email'] : null;
		$body     = isset( $args['body'] ) ? $args['body'] : null;
		$tags     = isset( $args['tags'] ) ? $args['tags'] : null;
		$type     = isset( $args['type'] ) ? $args['type'] : null;
		$status   = isset( $args['status'] ) ? $args['status'] : null;
		$private  = isset( $args['private'] ) ? $args['private'] : null;
		$meta_key  = isset( $args['meta_key'] ) ? $args['meta_key'] : null;
		$meta_desc = isset( $args['meta_desc'] ) ? $args['meta_desc'] : null;
		$views     = isset( $args['views'] ) ? $args['views'] : null;
		$has_attach = isset( $args['has_attach'] ) ? $args['has_attach'] : null;

		if( isset( $title ) ) $title = sanitize_text_field( trim( (string) $title ) );
		if( isset( $created ) ) $created = sanitize_text_field( $created );
		if( isset( $userid ) ) $userid = intval( $userid );
		$type    = ( isset( $type ) && $type ? 1 : 0 );
		$status  = ( isset( $status ) && $status ? 1 : 0 );
		$private = ( isset( $private ) && $private ? 1 : 0 );
		// Force private for ticket_forum type forums (all topics must be private)
		if( wpfval( $forum, 'type' ) === 'ticket_forum' ) {
			$private = 1;
		}
		if( isset( $meta_key ) ) $meta_key = sanitize_text_field( $meta_key );
		if( isset( $meta_desc ) ) $meta_desc = sanitize_text_field( $meta_desc );
		if( isset( $name ) ) $name = strip_tags( trim( (string) $name ) );
		if( isset( $email ) ) $email = strip_tags( trim( (string) $email ) );
		if( isset( $body ) ) $body = wpforo_kses( trim( (string) $body ) );
		if( isset( $tags ) ) {
			if( wpforo_is_module_enabled( 'tags' ) && WPF()->perm->forum_can( 'tag', $forum['forumid'] ) ) {
				$tags = $this->sanitize_tags( $tags, false, true );
			} else {
				$tags = '';
			}
		}
		$views      = ( isset( $views ) ? intval( $views ) : 0 );
		$meta_key   = ( isset( $meta_key ) ? $meta_key : '' );
		$meta_desc  = ( isset( $meta_desc ) ? $meta_desc : '' );
		$has_attach = ( isset( $has_attach ) && $has_attach ) ? 1 : ( ( strpos(
			                                                                (string) $body,
			                                                                '[attach]'
		                                                                ) !== false ) ? 1 : 0 );
		$layout     = WPF()->forum->get_layout( $forum );
		$posts      = ( $layout == 3 ) ? 0 : 1;
		do_action( 'wpforo_before_add_topic', $args );
		
		if( WPF()->db->insert(
			WPF()->tables->topics,
			[
				'title'      => stripslashes(
					$title
				),
				'slug'       => $slug,
				'forumid'    => $forum['forumid'],
				'userid'     => $userid,
				'type'       => $type,
				'status'     => $status,
				'private'    => $private,
				'created'    => $created,
				'modified'   => $created,
				'last_post'  => 0,
				'views'      => $views,
				'posts'      => $posts,
				'meta_key'   => $meta_key,
				'meta_desc'  => $meta_desc,
				'has_attach' => $has_attach,
				'name'       => $name,
				'email'      => $email,
				'tags'       => $tags,
			],
			[
				'%s',
				'%s',
				'%d',
				'%d',
				'%d',
				'%d',
				'%d',
				'%s',
				'%s',
				'%d',
				'%d',
				'%d',
				'%s',
				'%s',
				'%d',
				'%s',
				'%s',
				'%s',
			]
		) ) {
			$topicid = WPF()->db->insert_id;
			$fields  = [
				'forumid'       => $forum['forumid'],
				'topicid'       => $topicid,
				'userid'        => $userid,
				'title'         => stripslashes( $title ),
				'body'          => stripslashes( (string) $body ),
				'created'       => $created,
				'modified'      => $created,
				'is_first_post' => 1,
				'status'        => $status,
				'private'       => $private,
				'name'          => $name,
				'email'         => $email,
				'root'          => - 1,
			];
			
			$values = [ '%d', '%d', '%d', '%s', '%s', '%s', '%s', '%d', '%d', '%d', '%s', '%s', '%d' ];
			
			if( ! $root_exists ) {
				unset( $fields['root'] );
				unset( $fields[13] );
			}
			
			if( WPF()->db->insert(
				WPF()->tables->posts,
				$fields,
				$values
			) ) {
				$first_postid = WPF()->db->insert_id;
				if( false !== WPF()->db->update(
						WPF()->tables->topics,
						[ 'first_postid' => $first_postid, 'last_post' => $first_postid ],
						[ 'topicid' => $topicid ],
						[ '%d', '%d' ], [ '%d' ]
					) ) {
					$args['topicid']      = $topicid;
					$args['first_postid'] = $first_postid;
					$args['type']         = $type;
					$args['status']       = $status;
					$args['private']      = $private;
					$args['url']          = $args['topicurl'] = $this->get_url( $topicid );
					if( $tags && ! $status && ! $private ) $this->add_tags( $tags );
					
					$topic = apply_filters( 'wpforo_after_add_topic_filter', $args, $forum );
					do_action( 'wpforo_after_add_topic', $topic, $forum );
					
					wpforo_clean_cache( 'topic', $topicid, $topic );
					if( $status ) {
						WPF()->notice->add( 'Your topic successfully added and awaiting moderation', 'success' );
					} else {
						WPF()->notice->add( 'Your topic successfully added', 'success' );
					}
					
					return $topicid;
				}
			}
			
		}
		
		WPF()->notice->add( 'Topic add error', 'error' );
		
		return false;
	}
	
	#################################################################################
	
	private function unique_slug( $slug ) {
		$new_slug = wpforo_text( $slug, 250, false );
		$i        = 2;
		while( ! WPF()->can_use_this_slug( $new_slug ) || WPF()->db->get_var(
				"SELECT `topicid` FROM " . WPF()->tables->topics . " WHERE `slug` = '" . esc_sql( $new_slug ) . "'"
			) ) {
			$new_slug = wpforo_text( $slug, 250, false ) . '-' . $i;
			$i ++;
		}
		
		return $new_slug;
	}
	
	#################################################################################
	
	public function sanitize_tags( $tags, $array = false, $limit = false ) {
		if( $tags ) {
			if( ! is_array( $tags ) ) {
				$tag_separator = ( strpos( $tags, '،' ) !== false ) ? '،' : ',';
				$tag_separator = apply_filters( 'wpforo_tag_separator', $tag_separator );
				$tags          = wp_unslash( $tags );
				//Replace other tag separators to regular comma to store in the database
				$tags = str_replace( $tag_separator, ',', $tags );
				$tags = explode( $tag_separator, $tags );
			}
			if( wpforo_setting( 'tags', 'lowercase' ) ) {
				if( function_exists( 'mb_strtolower' ) ) {
					$tags = array_map( 'mb_strtolower', $tags );
				} else {
					$tags = array_map( 'strtolower', $tags );
				}
			}
			$length    = wpforo_setting( 'tags', 'length' );
			$mb_substr = function_exists( 'mb_substr' );
			foreach( $tags as $key => $tag ) {
				if( $mb_substr ) {
					$tags[ $key ] = mb_substr( (string) $tag, 0, $length );
				} else {
					$tags[ $key ] = substr( (string) $tag, 0, $length );
				}
			}
			$tags = array_map( 'trim', $tags );
			$tags = array_map( 'sanitize_text_field', $tags );
			$tags = array_filter( $tags );
			$tags = array_unique( $tags );
			if( $limit ) {
				$tags = array_slice( $tags, 0, wpforo_setting( 'tags', 'max_per_topic' ) );
			}
			if( $array ) {
				return $tags;
			} else {
				// We keep using "," in the database even if the separator is different
				return implode( ',', $tags );
			}
		}
		
		if( $array ) {
			return [];
		} else {
			return '';
		}
	}
	
	/**
	 * @param $topic
	 * @param $forum
	 * @param $_cache
	 *
	 * @return string
	 */
	public function get_url( $topic, $forum = [], $_cache = true ) {
		return (string) wpforo_ram_get( [ $this, '_get_url' ], $topic, $forum, $_cache );
	}
	
	public function add_tags( $tags ) {
		if( $tags ) {
			$tags = $this->sanitize_tags( $tags, true );
			if( ! empty( $tags ) ) {
				$tags = array_slice( $tags, 0, wpforo_setting( 'tags', 'max_per_topic' ) );
				foreach( $tags as $tag ) {
					$count = (int) WPF()->db->get_var(
						"SELECT `count` FROM `" . WPF()->tables->tags . "` WHERE `tag` = '" . esc_sql( $tag ) . "'"
					);
					if( $count ) {
						WPF()->db->update(
							WPF()->tables->tags,
							[ 'count' => $count + 1 ],
							[ 'tag' => $tag ],
							[ '%d' ], [ '%s' ]
						);
					} else {
						$this->add_tag( $tag, 1 );
					}
					wpforo_clean_cache( 'tag', $tag );
				}
				wpforo_clean_cache( 'tag' );
			}
		}
	}
	
	public function add_tag( $tag, $count = 0 ) {
		$tagid = 0;
		if( $tag ) {
			$tag = $this->sanitize_tag( $tag );
			if( ! WPF()->db->get_var(
				"SELECT `tagid` FROM `" . WPF()->tables->tags . "` WHERE `tag` = '" . esc_sql( $tag ) . "'"
			) ) {
				$tagid = WPF()->db->insert(
					WPF()->tables->tags,
					[ 'tag' => $tag, 'prefix' => 0, 'count' => intval( $count ) ], [ '%s', '%d', '%d' ]
				);
			}
		}
		
		return $tagid;
	}
	
	public function sanitize_tag( $tag ) {
		$tag     = trim( (string) $tag );
		$tag     = wp_unslash( $tag );
		$tag     = sanitize_text_field( $tag );
		$length  = wpforo_setting( 'tags', 'length' );
		$tag     = ( function_exists( 'mb_substr' ) ) ? mb_substr( (string) $tag, 0, $length ) : substr(
			(string) $tag,
			0,
			$length
		);
		$lowcase = wpforo_setting( 'tags', 'lowercase' );
		if( $lowcase ) {
			$tag = ( function_exists( 'mb_strtolower' ) ) ? mb_strtolower( (string) $tag ) : strtolower(
				(string) $tag
			);
		}
		
		return $tag;
	}
	
	public function edit_tags( $tags, $topic = [] ) {
		$old_tags = ( wpfval( $topic, 'tags' ) ) ? $this->sanitize_tags( $topic['tags'], true ) : false;
		if( $tags ) {
			$tags = $this->sanitize_tags( $tags, true );
			$tags = array_slice( $tags, 0, wpforo_setting( 'tags', 'max_per_topic' ) );
			if( ! empty( $tags ) ) {
				if( wpfval( $topic, 'topicid' ) ) {
					if( ! empty( $old_tags ) ) {
						foreach( $old_tags as $old_tag ) {
							if( ! in_array( $old_tag, $tags ) ) {
								$this->remove_tags( $old_tag );
							}
						}
					}
				}
				if( ! wpfval( $topic, 'status' ) && ! wpfval( $topic, 'private' ) ) {
					foreach( $tags as $tag ) {
						$count = (int) WPF()->db->get_var(
							"SELECT `count` FROM `" . WPF()->tables->tags . "` WHERE `tag` = '" . esc_sql( $tag ) . "'"
						);
						if( ! $count ) {
							WPF()->db->insert(
								WPF()->tables->tags,
								[ 'tag' => $tag, 'prefix' => 0, 'count' => 1 ], [ '%s', '%d', '%d' ]
							);
						} elseif( empty( $old_tags ) || ! in_array( $tag, $old_tags ) ) {
							WPF()->db->update(
								WPF()->tables->tags,
								[ 'count' => ( $count + 1 ) ],
								[ 'tag' => $tag ],
								[ '%d' ], [ '%s' ]
							);
						}
						wpforo_clean_cache( 'tag', $tag );
					}
				}
			}
		} else {
			if( ! empty( $old_tags ) && wpfval( $topic, 'topicid' ) ) {
				WPF()->db->update(
					WPF()->tables->topics,
					[ 'tags' => '' ],
					[ 'topicid' => $topic['topicid'] ],
					[ '%s' ], [ '%d' ]
				);
				$this->remove_tags( $old_tags );
			}
		}
		wpforo_clean_cache( 'tag' );
	}
	
	public function remove_tags( $tags ) {
		if( $tags ) {
			$tags = $this->sanitize_tags( $tags, true );
			foreach( $tags as $tag ) {
				$count = (int) WPF()->db->get_var(
					"SELECT `count` FROM `" . WPF()->tables->tags . "` WHERE `tag` = '" . esc_sql( $tag ) . "'"
				);
				if( $count === 1 ) {
					WPF()->db->query(
						"DELETE FROM `" . WPF()->tables->tags . "` WHERE `tag` = '" . esc_sql( $tag ) . "'"
					);
				} else {
					WPF()->db->update(
						WPF()->tables->tags,
						[ 'count' => ( $count - 1 ) ],
						[ 'tag' => $tag ],
						[ '%d' ], [ '%s' ]
					);
				}
				wpforo_clean_cache( 'tag', $tag );
			}
		}
		wpforo_clean_cache( 'tag' );
	}
	
	/**
	 * array get_topic(array or id(num))
	 *
	 * Returns array from defined and default arguments.
	 *
	 * @param mixed $args defined arguments array for returning
	 * @param bool $protect
	 *
	 * @return    array
	 * @since 1.0.0
	 *
	 */
	
	public function _get_topic( $args = [], $protect = true ) {
		if( ! $args ) return [];
		
		if( is_array( $args ) ) {
			$default = [
				'topicid' => null,
				'slug'    => '',
			];
		} elseif( wpforo_is_id( $args ) ) {
			$default = [
				'topicid' => $args,
				'slug'    => '',
			];
		} else {
			$default = [
				'topicid' => null,
				'slug'    => $args,
			];
		}
		
		$args = wpforo_parse_args( $args, $default );
		
		$sql    = "SELECT * FROM `" . WPF()->tables->topics . "`";
		$wheres = [];
		if( $topicid = wpforo_bigintval( $args['topicid'] ) ) $wheres[] = "`topicid` = " . $topicid;
		if( $args['slug'] ) $wheres[] = "`slug` = '" . esc_sql( $args['slug'] ) . "'";
		if( ! empty( $wheres ) ) $sql .= " WHERE " . implode( " AND ", $wheres );
		
		$topic = (array) WPF()->db->get_row( $sql, ARRAY_A );
		
		if( $protect ) {
			if( isset( $topic['forumid'] ) && $topic['forumid'] && ! WPF()->perm->forum_can(
					'vf',
					$topic['forumid']
				) ) {
				return [];
			}
			if( isset( $topic['private'] ) && $topic['private'] && ! wpforo_is_owner(
					$topic['userid'],
					$topic['email']
				) ) {
				if( isset( $topic['forumid'] ) && $topic['forumid'] && ! WPF()->perm->forum_can(
						'vp',
						$topic['forumid']
					) ) {
					return [];
				}
			}
			if( isset( $topic['status'] ) && $topic['status'] && ! wpforo_is_owner(
					$topic['userid'],
					$topic['email']
				) ) {
				if( isset( $topic['forumid'] ) && $topic['forumid'] && ! WPF()->perm->forum_can(
						'au',
						$topic['forumid']
					) ) {
					WPF()->current_object['status'] = 'unapproved';
					
					return [];
				}
			}
		}
		
		if( $topic ) {
			$topic['url']       = $this->get_url( $topic, [], false );
			$topic['full_url']  = $this->get_full_url( $topic, [], false );
			$topic['short_url'] = $this->get_short_url( $topic );
		}
		
		return apply_filters( 'wpforo_get_topic', $topic );
	}
	
	public function get_full_url( $topic, $forum = [], $_cache = true ): string {
		if( ! is_array( $topic ) ) $topic = $this->get_topic( $topic, false );
		if( $topic ) {
			$cache = WPF()->cache->on( 'url' );
			// $_cache is used to stop caching on merge and move actions,
			// otherwise the merging and splitting go to redirection loop
			if( $_cache && $cache && wpfval( $topic, 'topicid' ) ) {
				$topic_url = (string) WPF()->cache->get_item( $topic['topicid'], 'url', 'topic' );
				if( $topic_url ) return $topic_url;
			}
			if( is_array( $forum ) && ! empty( $forum ) ) {
				$forum_slug = $forum['slug'];
			} else {
				$forum_slug = '';
				if( wpfval( $topic, 'forumid' ) ) {
					$forum_slug = $this->get_forumslug( $topic['forumid'] );
				} elseif( wpfval( $topic, 'topicid' ) ) {
					$forum_slug = $this->get_forumslug_byid( $topic['topicid'] );
				}
			}
			if( wpfval( $topic, 'slug' ) ) {
				$topic_url = wpforo_home_url( $forum_slug . '/' . $topic['slug'] );
				if( $cache ) {
					WPF()->cache->create(
						'item',
						[ 'topic_' . intval( $topic['topicid'] ) => $topic_url ],
						'url'
					);
				}
				
				return $topic_url;
			}
		}
		
		return wpforo_home_url();
	}
	
	function get_forumslug( $forumid ) {
		$slug = WPF()->db->get_var(
			"SELECT `slug` FROM " . WPF()->tables->forums . " WHERE `forumid` = " . intval( $forumid )
		);
		if( $slug ) return $slug;
		
		return 0;
	}
	
	function get_forumslug_byid( $topicid ) {
		$slug = WPF()->db->get_var(
			"SELECT `slug` FROM " . WPF()->tables->forums . " WHERE `forumid` =(SELECT forumid FROM `" . WPF()->tables->topics . "` WHERE `topicid` =" . intval( $topicid ) . ")"
		);
		if( $slug ) return $slug;
		
		return 0;
	}
	
	public function get_short_url( $arg ) {
		if( wpforo_is_id( $arg ) ) {
			$topicid = $arg;
		} elseif( wpfkey( $arg, 'topicid' ) ) {
			$topicid = $arg['topicid'];
		} else {
			$topicid = 0;
		}
		
		if( $topicid = wpforo_bigintval( $topicid ) ) {
			return wpforo_home_url(
				'/' . wpforo_settings_get_slug( 'topicid' ) . '/' . $topicid . '/'
			);
		}
		
		return wpforo_home_url();
	}
	
	/**
	 * Search in your chosen column and return array with needles
	 *
	 * @param string $needle
	 *
	 * @param array $fields can be ( slug, title, body )
	 *
	 * @return array with  matches
	 * @since 1.0.0
	 *
	 */
	function search( $needle = '', $fields = [ 'title', 'body' ] ) {
		if( $needle !== '' ) {
			$needle = stripslashes( (string) $needle );
			$fields = (array) $fields;
			
			$topicids = [];
			foreach( $fields as $field ) {
				$matches = [];
				if( $field === 'body' ) {
					// Search the needle as a whole phrase in body to find closer result
					$matches = WPF()->db->get_col(
						"SELECT `topicid` FROM " . WPF()->tables->posts . " WHERE `" . esc_sql(
							$field
						) . "` LIKE '%" . esc_sql( sanitize_text_field( $needle ) ) . "%'"
					);
				} else {
					// Search all words of the needle independently in title and other fields
					// If the search phrase is "es lorem du ipsum dolor", then SQL  becomes
					// SELECT * FROM `wp_wpforo_topics` WHERE `title` REGEXP 'lorem|ipsum|dolor'
					$words = array_filter(
						explode( ' ', sanitize_text_field( $needle ) ),
						function( $word ) { return mb_strlen( (string) $word ) > 2; }
					);
					if( $words ) {
						if( apply_filters( 'wpforo_suggested_topics_search_fields_method_regexp', false ) ) {
							$words           = array_map(
								function( $word ) {
									return str_replace( [ '"', "'" ],
									                    [ '\"', "\'" ],
									                    preg_quote( preg_quote( $word ) ) );
								},
								$words
							);
							$search_mode_sql = '`' . esc_sql( $field ) . '` REGEXP "' . implode( '|', $words ) . '"';
						} else {
							$wheres          = array_map( function( $word ) use ( $field ) {
								return sprintf(
									'`%1$s` LIKE \'%%%2$s%%\'',
									esc_sql( $field ),
									esc_sql( $word )
								);
							}, $words );
							$search_mode_sql = implode( ' OR ', $wheres );
						}
						
						$sql     = "SELECT `topicid` FROM " . WPF()->tables->topics . " WHERE " . $search_mode_sql;
						$matches = WPF()->db->get_col( $sql );
					}
					
				}
				$topicids = array_merge( $topicids, $matches );
			}
			
			return array_unique( array_map( 'wpforo_bigintval', $topicids ) );
		}
		
		return [];
	}
	
	function get_sum_answer( $forumids ) {
		$sum = WPF()->db->get_var(
			"SELECT SUM(`answers`) FROM `" . WPF()->tables->topics . "` WHERE `forumid` IN(" . implode(
				', ',
				array_map( 'intval', $forumids )
			) . ")"
		);
		if( $sum ) return $sum;
		
		return 0;
	}
	
	function is_sticky( $topicid ) {
		if( $topicid ) {
			if( WPF()->cache->on() ) {
				$is_sticky = wpforo_topic( $topicid, 'type' );
			} else {
				$sql       = "SELECT `type` FROM " . WPF()->tables->topics . " WHERE `topicid` = %d";
				$is_sticky = WPF()->db->get_var( WPF()->db->prepare( $sql, $topicid ) );
			}
			
			return (bool) $is_sticky;
		}
		
		return false;
	}
	
	function is_private( $topicid ) {
		if( $topicid ) {
			if( WPF()->cache->on() ) {
				$private = wpforo_topic( $topicid, 'private' );
			} else {
				$sql     = "SELECT `private` FROM " . WPF()->tables->topics . " WHERE `topicid` = %d";
				$private = WPF()->db->get_var( WPF()->db->prepare( $sql, $topicid ) );
			}
			
			return (bool) $private;
		}
		
		return false;
	}
	
	function is_unapproved( $topicid ) {
		if( WPF()->cache->on() ) {
			$status = wpforo_topic( $topicid, 'status' );
		} else {
			$status = WPF()->db->get_var(
				"SELECT `status` FROM " . WPF()->tables->topics . " WHERE `topicid` = " . intval( $topicid )
			);
		}
		if( $status == 1 ) return true;
		
		return false;
	}
	
	function is_closed( $topicid ) {
		if( $topicid ) {
			if( WPF()->cache->on() ) {
				$closed = wpforo_topic( $topicid, 'closed' );
			} else {
				$sql    = "SELECT `closed` FROM " . WPF()->tables->topics . " WHERE `topicid` = %d";
				$closed = WPF()->db->get_var( WPF()->db->prepare( $sql, $topicid ) );
			}
			
			return (bool) $closed;
		}
		
		return false;
	}
	
	function is_solved( $topicid ) {
		if( $topicid ) {
			$sql    = "SELECT `solved` FROM " . WPF()->tables->topics . " WHERE `topicid` = %d";
			$solved = WPF()->db->get_var( WPF()->db->prepare( $sql, $topicid ) );
			
			return (bool) $solved;
		}
		
		return false;
	}
	
	public function close( $topicid ) {
		$topicid = wpforo_bigintval( $topicid );
		$sql     = "UPDATE " . WPF()->tables->topics . " SET closed = 1 WHERE topicid = %d";
		WPF()->db->query( WPF()->db->prepare( $sql, $topicid ) );
		wpforo_clean_cache( 'topic-first-post', $topicid );
	}
	
	public function open( $topicid ) {
		$topicid = wpforo_bigintval( $topicid );
		$sql     = "UPDATE " . WPF()->tables->topics . " SET closed = 0 WHERE topicid = %d";
		WPF()->db->query( WPF()->db->prepare( $sql, $topicid ) );
		wpforo_clean_cache( 'topic-first-post', $topicid );
	}
	
	/**
	 * Move topic to another forum
	 *
	 * @param int $topicid
	 * @param int $forumid
	 *
	 * @return int|false $topicid on success, otherwise false
	 * @since 1.0.0
	 *
	 */
	function move( $topicid, $forumid ) {
		$topic = $this->get_topic( $topicid );
		if( WPF()->db->query(
			"UPDATE `" . WPF()->tables->topics . "` SET `forumid` = " . intval(
				$forumid
			) . " WHERE `topicid` = " . intval( $topicid )
		) ) {
			WPF()->db->query(
				"UPDATE `" . WPF()->tables->posts . "` SET `forumid` = " . intval(
					$forumid
				) . " WHERE `topicid` = " . intval( $topicid )
			);
			
			do_action( 'wpforo_after_move_topic', $topic, $forumid );
			
			wpforo_clean_cache( 'topic', $topicid, $topic );
			WPF()->notice->add( 'Done!', 'success' );
			
			return $topicid;
		}
		
		WPF()->notice->add( 'Topic Move Error', 'error' );
		
		return false;
	}
	
	public function split( $args, $to_target_title = 0 ) {
		if( ! $args ) return false;
		
		$args['name']  = ( isset( $args['name'] ) ? strip_tags( (string) $args['name'] ) : '' );
		$args['email'] = ( isset( $args['email'] ) ? sanitize_email( $args['email'] ) : '' );
		
		if( ! isset( $args['forumid'] ) || ! $args['forumid'] = intval( $args['forumid'] ) ) {
			WPF()->notice->add( 'Please select a target forum', 'error' );
			
			return false;
		}
		
		if( ! isset( $args['title'] ) || ! $args['title'] = trim( strip_tags( (string) $args['title'] ) ) ) {
			WPF()->notice->add( 'Please insert required fields', 'error' );
			
			return false;
		}
		
		if( empty( $args['postids'] ) ) {
			WPF()->notice->add( 'Please select at least one post to split', 'error' );
			
			return false;
		}
		
		$args['postids'] = array_values( $args['postids'] );
		
		if( $fpost = WPF()->post->get_post( $args['postids'][0] ) ) {
			$args['title'] = wpforo_text( $args['title'], 250, false );
			$args['slug']  = ( isset( $args['slug'] ) && $args['slug'] ) ? sanitize_title(
				$args['slug']
			) : ( ( isset( $args['title'] ) ) ? sanitize_title( $args['title'] ) : md5( time() ) );
			if( ! trim( $args['slug'] ) ) $args['slug'] = md5( time() );
			$args['slug'] = $this->unique_slug( $args['slug'] );
			
			
			$args['body']    = $fpost['body'];
			$args['created'] = $fpost['created'];
			$args['userid']  = $fpost['userid'];
			$args['name']    = $fpost['name'];
			$args['email']   = $fpost['email'];

			// Security: extract only expected keys to prevent variable injection
			$forumid = isset( $args['forumid'] ) ? $args['forumid'] : null;
			$title   = isset( $args['title'] ) ? $args['title'] : null;
			$slug    = isset( $args['slug'] ) ? $args['slug'] : null;
			$created = isset( $args['created'] ) ? $args['created'] : null;
			$userid  = isset( $args['userid'] ) ? $args['userid'] : null;
			$body    = isset( $args['body'] ) ? $args['body'] : null;
			$name    = isset( $args['name'] ) ? $args['name'] : null;
			$email   = isset( $args['email'] ) ? $args['email'] : null;
			$type    = isset( $args['type'] ) ? $args['type'] : null;
			$status  = isset( $args['status'] ) ? $args['status'] : null;
			$private = isset( $args['private'] ) ? $args['private'] : null;
			$has_attach = isset( $args['has_attach'] ) ? $args['has_attach'] : null;

			if( isset( $forumid ) ) $forumid = intval( $forumid );
			if( isset( $title ) ) $title = sanitize_text_field( trim( (string) $title ) );
			if( isset( $slug ) ) $slug = sanitize_title( $slug );
			if( isset( $created ) ) $created = sanitize_text_field( $created );
			if( isset( $userid ) ) $userid = intval( $userid );
			$type    = ( isset( $type ) && $type ? 1 : 0 );
			$status  = ( isset( $status ) && $status ? 1 : 0 );
			$private = ( isset( $private ) && $private ? 1 : 0 );
			if( isset( $name ) ) $name = strip_tags( trim( (string) $name ) );
			if( isset( $email ) ) $email = strip_tags( trim( (string) $email ) );
			$has_attach = ( isset( $has_attach ) && $has_attach ) ? 1 : ( ( strpos(
				                                                                (string) $body,
				                                                                '[attach]'
			                                                                ) !== false ) ? 1 : 0 );
			
			if( WPF()->db->insert(
				WPF()->tables->topics,
				[
					'title'      => stripslashes(
						$title
					),
					'slug'       => $slug,
					'forumid'    => $forumid,
					'userid'     => $userid,
					'type'       => $type,
					'status'     => $status,
					'private'    => $private,
					'created'    => $created,
					'modified'   => $created,
					'last_post'  => 0,
					'views'      => 0,
					'posts'      => 1,
					'has_attach' => $has_attach,
					'name'       => $name,
					'email'      => $email,
				],
				[
					'%s',
					'%s',
					'%d',
					'%d',
					'%d',
					'%d',
					'%d',
					'%s',
					'%s',
					'%d',
					'%d',
					'%d',
					'%d',
					'%s',
					'%s',
				]
			) ) {
				$args['topicid'] = $topicid = WPF()->db->insert_id;
				
				if( $this->merge( $args, WPF()->current_object['topic'], $args['postids'], $to_target_title ) ) {
					WPF()->notice->clear();
					WPF()->notice->add( 'Done!', 'success' );
					
					return $topicid;
				}
			}
		}
		
		WPF()->notice->add( 'Topic splitting error', 'error' );
		
		return false;
	}
	
	/**
	 * merge topic with target topic
	 *
	 * @param array $target target topic array
	 * @param array $current current topic array
	 * @param array $postids current topic postids array
	 * @param int $to_target_title Update post titles with target topic title
	 * @param int $append Update post dates and append to end
	 *
	 * @return bool true|false true on success, otherwise false
	 */
	public function merge( $target, $current = [], $postids = [], $to_target_title = 0, $append = 0 ) {
		
		if( ! $current ) $current = WPF()->current_object['topic'];
		
		$sql = "UPDATE `" . WPF()->tables->posts . "` SET `topicid` = %d, `forumid` = %d, `private` = %d,`is_first_post` = 0";
		$sql = WPF()->db->prepare( $sql, $target['topicid'], $target['forumid'], (int) wpfval( $target, 'private' ) );
		
		if( $append ) {
			$sql .= ", `modified` = %s, `created` = %s";
			$sql = WPF()->db->prepare( $sql, current_time( 'mysql', 1 ), current_time( 'mysql', 1 ) );
		}
		
		if( $to_target_title ) {
			$layout = WPF()->forum->get_layout( $target['forumid'] );
			$phrase = ( $layout == 3 ? wpforo_phrase( 'Answer to', false ) : wpforo_phrase( 'RE', false ) );
			$title  = $phrase . ': ' . $target['title'];
			$sql    .= ", `title` = %s";
			$sql    = WPF()->db->prepare( $sql, $title );
		}
		
		$sql .= " WHERE `topicid` = %d";
		$sql = WPF()->db->prepare( $sql, $current['topicid'] );
		
		if( $postids ) {
			$postids = (array) $postids;
			$postids = array_map( 'wpforo_bigintval', $postids );
			
			$sql .= " AND `postid` IN(" . implode( ',', $postids ) . ")";
		}
		
		do_action( 'wpforo_before_merge_topic', $target, $current, $postids, $to_target_title, $append );
		
		$db_resp = WPF()->db->query( $sql );
		
		if( $db_resp !== false ) {
			$sql = "SELECT COUNT(*) FROM `" . WPF()->tables->posts . "` WHERE `topicid` = %d";
			$sql = WPF()->db->prepare( $sql, $current['topicid'] );
			if( ! WPF()->db->get_var( $sql ) ) {
				$this->delete( $current['topicid'], true, false );
			} else {
				$this->rebuild_first_last( $current );
				$this->rebuild_stats( $current );
				$this->rebuild_threads( $current );
				wpforo_clean_cache( 'topic', $current['topicid'], $current );
			}
			
			$this->rebuild_first_last( $target );
			$this->rebuild_stats( $target );
			$this->rebuild_threads( $target );
			
			do_action( 'wpforo_after_merge_topic', $target, $current, $postids, $to_target_title, $append );
			
			WPF()->notice->clear();
			WPF()->notice->add( 'Done!', 'success' );
			
			wpforo_clean_cache();
			
			return true;
		}
		
		WPF()->notice->add( 'Data merging error', 'error' );
		
		return false;
	}
	
	/**
	 * Delete topic from DB
	 *
	 * Returns true if successfully deleted or false.
	 *
	 * @param int $topicid
	 * @param bool $delete_cache
	 *
	 * @return    bool
	 * @since 1.0.0
	 *
	 */
	function delete( $topicid = 0, $delete_cache = true, $check_permissions = true ) {
		$topicid = intval( $topicid );
		if( ! $topicid && isset( $_REQUEST['id'] ) ) $topicid = intval( $_REQUEST['id'] );
		
		if( ! $topic = $this->get_topic( $topicid ) ) return true;
		
		do_action( 'wpforo_before_delete_topic', $topic );
		
		if( $check_permissions ) {
			$diff = time() - strtotime( $topic['created'] . ' GMT' );
			if( ! ( WPF()->perm->forum_can( 'dt', $topic['forumid'] ) || ( WPF()->current_userid == $topic['userid'] && WPF()->perm->forum_can(
						'dot',
						$topic['forumid']
					) ) ) ) {
				WPF()->notice->add( 'You don\'t have permission to delete topic from this forum.', 'error' );
				
				return false;
			}
			
			if( ! WPF()->perm->forum_can( 'dt', $topic['forumid'] ) && wpforo_setting(
				                                                           'posting',
				                                                           'delete_own_topic_durr'
			                                                           ) !== 0 && $diff > wpforo_setting(
					'posting',
					'delete_own_topic_durr'
				) ) {
				WPF()->notice->add( 'The time to delete this topic is expired.', 'error' );
				
				return false;
			}
		}
		
		// START delete topic posts include first post
		if( $postids = WPF()->db->get_col(
			WPF()->db->prepare(
				"SELECT `postid` FROM `" . WPF()->tables->posts . "` WHERE `topicid` = %d ORDER BY `is_first_post`",
				$topicid
			)
		) ) {
			$exclude = [];
			foreach( $postids as $postid ) {
				if( $postid == $topic['first_postid'] ) {
					return WPF()->post->delete( $postid, false, false, $exclude, false );
				} else {
					WPF()->post->delete( $postid, false, false, $exclude, false );
				}
			}
		}
		// END delete topic posts include first post
		
		if( WPF()->db->delete( WPF()->tables->topics, [ 'topicid' => $topicid ], [ '%d' ] ) ) {
			if( wpfval( $topic, 'tags' ) ) $this->remove_tags( $topic['tags'] );
			
			do_action( 'wpforo_after_delete_topic', $topic );
			
			if( $delete_cache ) wpforo_clean_cache( 'topic', $topicid, $topic );
			WPF()->notice->add( 'This topic successfully deleted', 'success' );
			
			return true;
		}
		
		WPF()->notice->add( 'Topics delete error', 'error' );
		
		return false;
	}
	
	public function rebuild_first_last( $topic ) {
		if( ! $topic ) return false;
		if( wpforo_is_id( $topic ) ) $topic = $this->get_topic( $topic );
		if( ! is_array( $topic ) || ! $topic ) return false;
		
		$sql = "SELECT `postid` FROM `" . WPF()->tables->posts . "` WHERE `topicid` = %d ORDER BY `is_first_post` DESC, `created` ASC, `postid` ASC LIMIT 1";
		if( $first_postid = WPF()->db->get_var( WPF()->db->prepare( $sql, $topic['topicid'] ) ) ) {
			$sql = "UPDATE `" . WPF()->tables->posts . "` SET `is_first_post` = 1 WHERE `postid` = %d";
			WPF()->db->query( WPF()->db->prepare( $sql, $first_postid ) );
			
			do_action( 'wpforo_after_is_first_post_update', $first_postid, 1 );
		} else {
			$first_postid = 0;
		}
		
		$sql = "SELECT `postid`, `created`
			FROM `" . WPF()->tables->posts . "`
			WHERE `topicid` = %d
			ORDER BY `is_first_post` ASC, `created` DESC, `postid` DESC LIMIT 1";
		if( ! $last_post = WPF()->db->get_row( WPF()->db->prepare( $sql, $topic['topicid'] ), ARRAY_A ) ) {
			$last_post = [ 'postid' => 0, 'created' => $topic['modified'] ];
		}
		
		if( $r = WPF()->db->update(
			WPF()->tables->topics,
			[
				'first_postid' => $first_postid,
				'last_post'    => $last_post['postid'],
				'modified'     => $last_post['created'],
			],
			[ 'topicid' => $topic['topicid'] ],
			[ '%d', '%d', '%s' ], [ '%d' ]
		) ) {
			wpforo_clean_cache( 'topic-first-post' );
		}
		
		return $r !== false;
	}
	
	public function rebuild_stats( $topic ) {
		if( ! $topic ) return false;
		if( wpforo_is_id( $topic ) ) $topic = $this->get_topic( $topic );
		if( ! is_array( $topic ) || ! $topic ) return false;
		
		$posts = WPF()->post->get_count( [ 'topicid' => $topic['topicid'], 'status' => 0 ] );
		
		$data        = [ 'posts' => $posts, 'answers' => 0 ];
		$data_format = [ '%d', '%d' ];
		
		$layout = WPF()->forum->get_layout( $topic['forumid'] );
		if( $layout === 3 ) {
			$data['answers'] = WPF()->post->get_count(
				[ 'topicid' => $topic['topicid'], 'status' => 0, 'parentid' => 0, 'is_first_post' => 0 ]
			);
			$data['posts']   = $posts - 1;
		}
		
		if( $r = WPF()->db->update(
			WPF()->tables->topics,
			$data,
			[ 'topicid' => $topic['topicid'] ],
			$data_format,
			[ '%d' ]
		) ) {
			wpforo_clean_cache( 'topic-first-post', $topic['topicid'], $topic );
		}
		
		return $r !== false;
	}
	
	function get_count( $args = [] ) {
		$sql = "SELECT SQL_NO_CACHE COUNT(*) FROM `" . WPF()->tables->topics . "`";
		if( ! empty( $args ) ) {
			$wheres = [];
			foreach( $args as $key => $value ) $wheres[] = "`$key` = '" . esc_sql( $value ) . "'";
			if( $wheres ) $sql .= " WHERE " . implode( ' AND ', $wheres );
		}
		
		return WPF()->db->get_var( $sql );
	}
	
	function rebuild_threads( $topicid, $root = null ) {
		if( ! is_null( $root ) && $root <= 0 ) return;
		if( is_array( $topicid ) && wpfval( $topicid, 'topicid' ) ) $topicid = $topicid['topicid'];
		if( $topicid ) {
			$posts = WPF()->db->get_results(
				"SELECT * FROM `" . WPF()->tables->posts . "` WHERE `topicid` = " . intval( $topicid ),
				ARRAY_A
			);
			if( ! empty( $posts ) ) {
				$threads = [];
				foreach( $posts as $post ) {
					$threads[ $post['postid'] ] = [ 'postid' => $post['postid'], 'parentid' => $post['parentid'] ];
				}
				unset( $posts );
				foreach( $threads as $item ) {
					if( ! is_null( $root ) && $root != $item['postid'] ) continue;
					if( ! $item['parentid'] ) {
						$thread = WPF()->post->build_thread_data( $item['postid'], $threads );
						if( wpfval( $thread, 'children' ) ) {
							$children = implode( ',', $thread['children'] );
							WPF()->db->query(
								"UPDATE `" . WPF()->tables->posts . "` SET `root` = 0 WHERE `postid` = " . intval(
									$item['postid']
								)
							);
							WPF()->db->query(
								"UPDATE `" . WPF()->tables->posts . "` SET `root` = " . intval(
									$item['postid']
								) . " WHERE `postid` IN(" . esc_sql( $children ) . ")"
							);
						}
					} elseif( ! wpfkey( $threads, $item['parentid'] ) ) {
						WPF()->db->query(
							"UPDATE `" . WPF()->tables->posts . "` SET `root` = 0, `parentid` = 0 WHERE `postid` = " . intval(
								$item['postid']
							)
						);
					}
				}
			}
		}
	}
	
	/**
	 * @param $topic
	 * @param $forum
	 * @param $_cache
	 *
	 * @return string
	 * @deprecated since 2.0.10 instead this method use _get_url()
	 *
	 */
	public function _get_topic_url( $topic, $forum = [], $_cache = true ) {
		return $this->_get_url( $topic, $forum, $_cache );
	}
	
	/**
	 * @param $topic
	 * @param $forum
	 * @param $_cache
	 *
	 * @return string
	 */
	public function _get_url( $topic, $forum = [], $_cache = true ) {
		if( wpforo_setting( 'board', 'url_structure' ) === 'short' ) return $this->get_short_url( $topic );
		
		return $this->get_full_url( $topic, $forum, $_cache );
	}
	
	/**
	 * @param $topic
	 * @param $forum
	 * @param $_cache
	 *
	 * @return string
	 * @deprecated since 2.0.10 instead this method use get_url()
	 *
	 */
	public function get_topic_url( $topic, $forum = [], $_cache = true ) {
		return $this->get_url( $topic, $forum, $_cache );
	}
	
	public function set_status( $topicid, $status ) {
		$topicid = wpforo_bigintval( $topicid );
		$status  = intval( $status );
		if( ! $topic = $this->get_topic( $topicid, false ) ) return false;
		
		if( $r = WPF()->db->update(
			WPF()->tables->topics,
			[ 'status' => $status ],
			[ 'topicid' => $topicid ],
			[ '%d' ], [ '%d' ]
		) ) {
			if( $status ) {
				do_action( 'wpforo_topic_unapprove', $topic );
				if( wpfval( $topic, 'tags' ) ) $this->remove_tags( $topic['tags'] );
			} else {
				do_action( 'wpforo_topic_approve', $topic );
				if( wpfval( $topic, 'tags' ) ) $this->add_tags( $topic['tags'] );
			}
			
			do_action( 'wpforo_topic_status_update', $topic, $status );
			wpforo_clean_cache( 'topic-first-post', $topicid );
		}
		
		if( $r !== false ) {
			WPF()->notice->add( 'Done!', 'success' );
			
			return true;
		}
		
		WPF()->notice->add( 'Status changing error', 'error' );
		
		return false;
	}
	
	public function wprivate( $topicid, $private ) {
		WPF()->db->update(
			WPF()->tables->topics,
			[ 'private' => $private ],
			[ 'topicid' => $topicid ],
			[ '%d' ], [ '%d' ]
		);
		
		WPF()->db->update(
			WPF()->tables->posts,
			[ 'private' => $private ],
			[ 'topicid' => $topicid ],
			[ '%d' ], [ '%d' ]
		);
		
		do_action( 'wpforo_topic_private_update', $topicid, $private );
		wpforo_clean_cache();
		
		return true;
	}
	
	public function delete_attachments( $topicid ) {
		$args  = [ 'topicid' => $topicid ];
		$posts = WPF()->post->get_posts( $args );
		if( ! empty( $posts ) ) {
			foreach( $posts as $post ) {
				WPF()->post->delete_attachments( $post['postid'] );
			}
		}
	}
	
	function rebuild_forum_threads( $forumid = 0 ) {
		if( ! $forumid ) {
			$args   = [ 'layout' => 4 ];
			$forums = WPF()->forum->get_forums( $args );
			if( ! empty( $forums ) ) {
				foreach( $forums as $forum ) {
					$args   = [ 'forumid' => $forum['forumid'] ];
					$topics = $this->get_topics( $args );
					if( ! empty( $topics ) ) {
						foreach( $topics as $topic ) {
							$this->rebuild_threads( $topic['topicid'] );
						}
					}
				}
			}
		} else {
			$args   = [ 'forumid' => $forumid ];
			$topics = $this->get_topics( $args );
			if( ! empty( $topics ) ) {
				foreach( $topics as $topic ) {
					$this->rebuild_threads( $topic['topicid'] );
				}
			}
		}
	}
	
	/**
	 * array get_topic(array or id(num))
	 * Returns merged arguments array from defined and default arguments.
	 *
	 * @return array where count is topic count and other numeric arrays with topic
	 * @since 1.0.0
	 *
	 */
	function get_topics( $args = [], &$items_count = 0, $count = true ) {
		$cache = WPF()->cache->on( 'topic' );
		
		$default = [
			'include'      => [],        // array( 2, 10, 25 )
			'exclude'      => [],        // array( 2, 10, 25 )
			'forumids'     => [],
			'forumid'      => null,
			'userid'       => null,            // user id in DB
			'type'         => null,            //0, 1, etc . . .
			'solved'       => null,
			'closed'       => null,
			'status'       => null,            //0, 1, etc . . .
			'private'      => null,            //0, 1, etc . . .''
			'pollid'       => null,
			'orderby'      => 'type, topicid',    // type, topicid, modified, created
			'order'        => 'DESC',        // ASC DESC
			'offset'       => null,        // this use when you give row_count
			'row_count'    => null,        // 4 or 1 ...
			'permgroup'    => null,        //Checks permissions based on attribute value not on current user usergroup
			'read'         => null,       //true / false
			'prefix'       => null,       //23 / 23,24,50
			'where'        => null,
			'access_filter' => true,      //false to skip forum permission filtering (admin backend ops)
		];
		
		$args = wpforo_parse_args( $args, $default );

		// Security: extract only expected keys instead of extract()
		$include   = $args['include'];
		$exclude   = $args['exclude'];
		$forumids  = $args['forumids'];
		$forumid   = $args['forumid'];
		$userid    = $args['userid'];
		$type      = $args['type'];
		$solved    = $args['solved'];
		$closed    = $args['closed'];
		$status    = $args['status'];
		$private   = $args['private'];
		$pollid    = $args['pollid'];
		$orderby   = $args['orderby'];
		$order     = $args['order'];
		$offset    = $args['offset'];
		$row_count = $args['row_count'];
		$permgroup = $args['permgroup'];
		$read      = $args['read'];
		$prefix        = $args['prefix'];
		$where         = $args['where'];
		$access_filter = $args['access_filter'];

		if( $row_count === 0 ) return [];

		$include  = wpforo_parse_args( $include );
		$exclude  = wpforo_parse_args( $exclude );
		$forumids = wpforo_parse_args( $forumids );
		
		$guest  = [];
		$wheres = [];
		
		if( ! is_null( $prefix ) ) {
			$prefixes = explode( ',', $prefix );
			if( ! empty( $prefixes ) ) {
				foreach( $prefixes as $prefixid ) {
					$wheres[] = " FIND_IN_SET('" . intval( $prefixid ) . "', `prefix`) ";
				}
			}
		}
		
		if( ! is_null( $read ) ) {
			$last_read_postid = WPF()->log->get_all_read( 'post' );
			if( $read ) {
				if( $last_read_postid ) {
					$wheres[] = "`last_post` <= " . intval( $last_read_postid );
				}
				$include_read = WPF()->log->get_read();
				$include      = array_merge( $include, $include_read );
			} else {
				if( $last_read_postid ) {
					$wheres[] = "`last_post` > " . intval( $last_read_postid );
				}
				$exclude_read = WPF()->log->get_read();
				$exclude      = array_merge( $exclude, $exclude_read );
			}
		}
		
		if( ! empty( $include ) ) $wheres[] = "`topicid` IN(" . implode( ', ', array_map( 'intval', $include ) ) . ")";
		if( ! empty( $exclude ) ) {
			$wheres[] = "`topicid` NOT IN(" . implode(
					', ',
					array_map( 'intval', $exclude )
				) . ")";
		}
		if( ! empty( $forumids ) ) {
			$wheres[] = "`forumid` IN(" . implode(
					', ',
					array_map( 'intval', $forumids )
				) . ")";
		}
		if( ! is_null( $forumid ) ) $wheres[] = "`forumid` = " . intval( $forumid );
		if( ! is_null( $userid ) ) $wheres[] = "`userid` = " . wpforo_bigintval( $userid );
		if( ! is_null( $solved ) ) $wheres[] = "`solved` = " . intval( $solved );
		if( ! is_null( $closed ) ) $wheres[] = "`closed` = " . intval( $closed );
		if( ! is_null( $type ) ) $wheres[] = "`type` = " . intval( $type );
		if( ! is_null( $where ) ) $wheres[] = $where;
		
		if( ! is_user_logged_in() ) $guest = WPF()->member->get_guest_cookies();
		
		if( empty( $forumids ) ) {
			if( isset( $forumid ) && ! WPF()->perm->forum_can( 'vf', $forumid, $permgroup ) ) {
				return [];
			}
		}
		
		if( isset( $forumid ) && $forumid ) {
			if( WPF()->perm->forum_can( 'vp', $forumid, $permgroup ) ) {
				if( ! is_null( $private ) ) $wheres[] = " `private` = " . intval( $private );
			} elseif( isset( WPF()->current_userid ) && WPF()->current_userid ) {
				$wheres[] = " ( `private` = 0 OR (`private` = 1 AND `userid` = " . WPF()->current_userid . ") )";
			} elseif( wpfval( $guest, 'email' ) ) {
				$wheres[] = " ( `private` = 0 OR (`private` = 1 AND `email` = '" . sanitize_email(
						$guest['email']
					) . "') )";
			} else {
				$wheres[] = " `private` = 0";
			}
		} else {
			if( ! is_null( $private ) ) $wheres[] = " `private` = " . intval( $private );
		}
		
		if( isset( $forumid ) && $forumid ) {
			if( WPF()->perm->forum_can( 'au', $forumid, $permgroup ) ) {
				if( ! is_null( $status ) ) $wheres[] = " `status` = " . intval( $status );
			} elseif( isset( WPF()->current_userid ) && WPF()->current_userid ) {
				$wheres[] = " ( `status` = 0 OR (`status` = 1 AND `userid` = " . WPF()->current_userid . ") )";
			} elseif( wpfval( $guest, 'email' ) ) {
				$wheres[] = " ( `status` = 0 OR (`status` = 1 AND `email` = '" . sanitize_email(
						$guest['email']
					) . "') )";
			} else {
				$wheres[] = " `status` = 0";
			}
		} else {
			if( ! is_null( $status ) ) $wheres[] = " `status` = " . intval( $status );
		}
		
		if( function_exists( 'WPF_POLL' ) ) {
			if( ! is_null( $pollid ) ) $wheres[] = " `pollid` <> 0";
		}
		
		$wheres = apply_filters( 'wpforo_get_topics_sql_wheres', $wheres );
		
		$sql = "SELECT * FROM `" . WPF()->tables->topics . "`";
		if( ! empty( $wheres ) ) {
			$sql .= " WHERE " . implode( " AND ", $wheres );
		}
		
		if( $count ) {
			$item_count_sql = preg_replace( '#SELECT.+?FROM#isu', 'SELECT count(*) FROM', $sql );
			if( $item_count_sql ) $items_count = WPF()->db->get_var( $item_count_sql );
		}
		
		if( $order === 'RAND' ) {
			$sql .= " ORDER BY RAND()";
		} else {
			$sql .= " ORDER BY " . str_replace( ',', ' ' . esc_sql( $order ) . ',', esc_sql( $orderby ) ) . " " . esc_sql(
					$order
				);
		}
		
		if( ! is_null( $row_count ) ) {
			if( ! is_null( $offset ) ) {
				$sql .= esc_sql( " LIMIT $offset,$row_count" );
			} else {
				$sql .= esc_sql( " LIMIT $row_count" );
			}
		}
		
		if( $cache ) {
			$object_key   = md5( $sql . WPF()->current_user_groupid );
			$object_cache = WPF()->cache->get( $object_key );
			if( ! empty( $object_cache ) ) {
				if( ! empty( $object_cache['items'] ) ) {
					$do_access_filter = $access_filter && apply_filters( 'wpforo_topic_access_filter_cache', true );
					if( $do_access_filter ) {
						$object_cache['items'] = $this->access_filter(
							$object_cache['items'],
							[
								'groupids' => ( array_filter(
									array_map( 'intval', (array) $permgroup )
								) ?: null ),
							]
						);
					}
					return apply_filters( 'wpforo_get_topics', $object_cache['items'] );
				}
			}
		}
		
		$topics = WPF()->db->get_results( $sql, ARRAY_A );
		
		if( $cache && isset( $object_key ) && ! empty( $topics ) ) {
			self::$cache['topics'][ $object_key ]['items']       = $topics;
			self::$cache['topics'][ $object_key ]['items_count'] = $items_count;
		}
		
		if( $access_filter && ( ! empty( $forumids ) || ! $forumid ) ) {
			$topics = $this->access_filter(
				$topics,
				[
					'groupids' => ( array_filter(
						array_map( 'intval', (array) $permgroup )
					) ?: null ),
				]
			);
		}

		return apply_filters( 'wpforo_get_topics', $topics );
	}
	
	function access_filter( $topics, $user = null ) {
		if( ! empty( $topics ) ) {
			foreach( $topics as $key => $topic ) {
				if( ! $this->view_access( $topic, $user ) ) unset( $topics[ $key ] );
			}
		}
		
		return $topics;
	}
	
	function view_access( $topic, $user = null ) {
		$groupids = wpfval( $user, 'groupids' );
		if( ! WPF()->perm->forum_can( 'vf', $topic['forumid'], $groupids ) ) {
			return apply_filters(
				'wpforo_topic_view_access',
				false,
				$topic,
				$user
			);
		}
		if( ! WPF()->perm->forum_can( 'vt', $topic['forumid'], $groupids ) ) {
			return apply_filters(
				'wpforo_topic_view_access',
				false,
				$topic,
				$user
			);
		}
		if( ! wpforo_is_users_same( wpforo_member( $topic ), $user ) && ( ( (int) wpfval( $topic, 'private' ) && ! WPF()->perm->forum_can( 'vp', $topic['forumid'], $groupids ) ) || ( (int) wpfval(
						$topic,
						'status'
					) && ! WPF()->perm->forum_can( 'au', $topic['forumid'], $groupids ) ) ) ) {
			return apply_filters( 'wpforo_topic_view_access', false, $topic, $user );
		}
		
		return apply_filters( 'wpforo_topic_view_access', true, $topic, $user );
	}
	
	public function members( $topicid, $limit = 0 ) {
		if( ! $topicid ) return [];
		$members = [];
		$args    = [
			'topicid'    => $topicid,
			'orderby'    => 'created',
			'order'      => 'ASC',
			'private'    => 0,
			'status'     => 0,
			'cache_type' => 'args',
		];
		$posts   = WPF()->post->get_posts( $args );
		foreach( $posts as $post ) {
			if( wpfval( $post, 'userid' ) ) {
				$members[ $post['userid'] ] = wpforo_member( $post['userid'] );
				if( $limit && count( $members ) >= $limit ) break;
			}
		}
		
		return array_filter( $members );
	}
	
	public function can_answer( $topicid ) {
		if( ! $topicid ) return false;
		$topic = wpforo_topic( $topicid );
		if( wpfval( $topic, 'topicid' ) ) {
			if( wpfval( $topic, 'userid' ) ) {
				if( ! WPF()->perm->forum_can( 'aot', $topic['forumid'] ) && WPF()->current_userid == $topic['userid'] ) {
					return false;
				}
			} else {
				$guest = WPF()->member->get_guest_cookies();
				if( wpfval( $topic, 'email' ) && wpfval( $guest, 'email' ) ) {
					if( ! WPF()->perm->forum_can( 'aot', $topic['forumid'] ) && $topic['email'] == $guest['email'] ) {
						return false;
					}
				}
			}
		}
		
		return true;
	}
	
	/**
	 * @param $topicid
	 *
	 * @return bool
	 */
	public function has_is_answer_post( $topicid ) {
		if( ! $topicid ) return false;
		$sql = "SELECT EXISTS ( SELECT * FROM `" . WPF()->tables->posts . "` WHERE `is_answer` = 1 AND `topicid` = %d) AS is_exists";
		$sql = WPF()->db->prepare( $sql, $topicid );
		
		return (bool) WPF()->db->get_var( $sql );
	}
	
	public function get_postids( $topicid ) {
		$sql = "SELECT `postid`
			FROM `" . WPF()->tables->posts . "`
			WHERE `topicid` = %d";
		$sql = WPF()->db->prepare( $sql, $topicid );
		
		return array_map( 'wpforo_bigintval', WPF()->db->get_col( $sql ) );
	}
	
	public function edit_tag( $tag, $old_tag = '', $disconnect = false ) {
		
		$tag_id   = wpfval( $tag, 'tagid' );
		$tag_name = wpfval( $tag, 'tag' );
		$tag_name = $this->sanitize_tag( $tag_name );
		
		if( false !== WPF()->db->update(
				WPF()->tables->tags,
				[ 'tag' => $tag_name ],
				[ 'tagid' => intval( $tag_id ) ],
				[ '%s' ], [ '%d' ]
			) ) {
			if( $old_tag ) {
				if( $disconnect ) {
					$this->remove_tag_from_topics( $old_tag );
					WPF()->db->update( WPF()->tables->tags, [ 'count' => 0 ], [ 'tag' => $tag_name ], [ '%d' ], [ '%s' ] );
				} else {
					if( $old_tag != $tag_name ) {
						$this->update_tag_in_topics( $tag_name, $old_tag );
					}
					$count = WPF()->db->get_var(
						"SELECT COUNT(*) FROM `" . WPF()->tables->topics . "` WHERE FIND_IN_SET('" . esc_sql(
							$tag_name
						) . "', tags)"
					);
					WPF()->db->update(
						WPF()->tables->tags,
						[ 'count' => intval( $count ) ],
						[ 'tag' => $tag_name ],
						[ '%d' ], [ '%s' ]
					);
				}
			}
			wpforo_clean_cache( 'tag', $tag_name );
			
			return true;
		}
		
		return false;
	}
	
	public function remove_tag_from_topics( $tag ) {
		if( $tag ) {
			WPF()->db->query(
				"UPDATE `" . WPF()->tables->topics . "`
                                            SET tags = TRIM(BOTH ',' FROM REPLACE(CONCAT(',', tags, ','), '," . esc_sql(
					$tag
				) . ",', ','))
                                                    WHERE FIND_IN_SET('" . esc_sql( $tag ) . "', tags)"
			);
		}
	}
	
	public function update_tag_in_topics( $new_tag, $old_tag ) {
		if( $new_tag && $old_tag ) {
			
			WPF()->db->query(
				"UPDATE `" . WPF()->tables->topics . "`
                                    SET tags = '" . esc_sql( $new_tag ) . "'
                                        WHERE tags LIKE '" . esc_sql( $old_tag ) . "'"
			);
			
			WPF()->db->query(
				"UPDATE `" . WPF()->tables->topics . "`
                                    SET tags = REPLACE(tags, '," . esc_sql( $old_tag ) . ",', '," . esc_sql(
					$new_tag
				) . ",')
                                           WHERE tags LIKE '%," . esc_sql( $old_tag ) . ",%'"
			);
			
			$topics = WPF()->db->get_results(
				"SELECT `topicid`, `tags`
                                                        FROM `" . WPF()->tables->topics . "`
                                                            WHERE FIND_IN_SET('" . esc_sql( $old_tag ) . "', tags)",
				ARRAY_A
			);
			if( ! empty( $topics ) ) {
				foreach( $topics as $topic ) {
					$tags = explode( ',', $topic['tags'] );
					if( ! empty( $tags ) ) {
						foreach( $tags as $key => $tag ) {
							if( $tag === $old_tag ) $tags[ $key ] = $new_tag;
						}
						$tags = implode( ',', $tags );
						WPF()->db->update(
							WPF()->tables->topics,
							[ 'tags' => $tags ],
							[ 'topicid' => $topic['topicid'] ],
							[ '%s' ], [ '%d' ]
						);
					}
				}
			}
		}
	}
	
	public function remove_tag( $tag ) {
		if( $tag ) {
			$this->remove_tag_from_topics( $tag );
			$sql_delete_tag = "DELETE FROM `" . WPF()->tables->tags . "` WHERE `tag` = '" . esc_sql( $tag ) . "'";
			WPF()->db->query( $sql_delete_tag );
			wpforo_clean_cache( 'tag', $tag );
		}
	}
	
	public function get_tag( $args = [] ) {
		if( $args ) {
			$default = [
				'tagid' => null,
				'tag'   => null,
			];
			if( wpforo_is_id( $args ) ) {
				$args = [ 'tagid' => $args ];
			} elseif( is_string( $args ) ) {
				$args = [ 'tag' => $args ];
			}
			
			$args   = wpforo_parse_args( $args, $default );
			$sql    = "SELECT * FROM `" . WPF()->tables->tags . "`";
			$wheres = [];
			if( $args['tagid'] ) $wheres[] = "`tagid` = " . intval( $args['tagid'] );
			if( $args['tag'] ) $wheres[] = "`tag` = '" . esc_sql( $args['tag'] ) . "'";
			if( $wheres ) {
				$sql .= " WHERE " . implode( " AND ", $wheres );
				
				if( WPF()->ram_cache->exists( $sql ) ) {
					$tag = WPF()->ram_cache->get( $sql );
				} else {
					$tag = WPF()->db->get_row( $sql, ARRAY_A );
					WPF()->ram_cache->set( $sql, $tag );
				}
				
				return $tag;
			}
		}
		
		return [];
	}
	
	public function get_tags( $args = [], &$items_count = 0 ) {
		$cache   = WPF()->cache->on( 'tag' );
		$default = [
			'tag'       => '',
			'prefix'    => 0,
			'count'     => null,
			'orderby'   => 'count',
			'order'     => 'DESC',
			'offset'    => null,
			'row_count' => null,
		];
		
		$args     = wpforo_parse_args( $args, $default );
		$sql      = "SELECT * FROM `" . WPF()->tables->tags . "`";
		$wheres   = [];
		$wheres[] = " `prefix` = " . intval( $args['prefix'] );
		if( ! empty( $wheres ) ) $sql .= " WHERE " . implode( " AND ", $wheres );
		$item_count_sql = preg_replace( '#SELECT.+?FROM#isu', 'SELECT count(*) FROM', $sql );
		if( $item_count_sql ) $items_count = WPF()->db->get_var( $item_count_sql );
		
		$sql .= " ORDER BY `" . esc_sql( $args['orderby'] ) . "` " . esc_sql( $args['order'] );
		
		if( $args['row_count'] != null ) {
			if( $args['offset'] != null ) {
				$sql .= " LIMIT " . intval( $args['offset'] ) . ',' . intval( $args['row_count'] );
			} else {
				$sql .= " LIMIT " . intval( $args['row_count'] );
			}
		}
		
		if( $cache ) {
			$object_key   = md5( $sql . WPF()->current_user_groupid );
			$object_cache = WPF()->cache->get( $object_key, 'loop', 'tag' );
			if( ! empty( $object_cache ) ) {
				$items_count = $object_cache['items_count'];
				
				return $object_cache['items'];
			}
		}
		
		$tags = WPF()->db->get_results( $sql, ARRAY_A );
		$tags = apply_filters( 'wpforo_get_tags', $tags );
		
		if( $cache && isset( $object_key ) && ! empty( $tags ) ) {
			self::$cache['tags'][ $object_key ]['items']       = $tags;
			self::$cache['tags'][ $object_key ]['items_count'] = $items_count;
			WPF()->cache->create( 'loop', self::$cache, 'tag' );
		}
		
		return $tags;
	}
	
	public function after_add_post( $post, $topic ) {
		if( ! intval( $post['status'] ) ) {
			$this->rebuild_first_last( $topic );
			$this->rebuild_stats( $topic );
		}
	}
	
	public function after_delete_post( $post ) {
		if( ! intval( $post['status'] ) ) {
			$this->rebuild_first_last( $post['topicid'] );
			$this->rebuild_stats( $post['topicid'] );
		}
	}
	
	public function after_post_status_update( $post ) {
		if( $topic = $this->get_topic( $post['topicid'] ) ) {
			$this->rebuild_first_last( $topic );
			$this->rebuild_stats( $topic );
		}
	}
	
	public function after_delete_user( $userid, $reassign ) {
		if( $boardids = WPF()->board->get_active_boardids() ) {
			if( is_null( $reassign ) ) {
				
				foreach( $boardids as $boardid ) {
					WPF()->change_board( $boardid );
					if( $topicids = WPF()->db->get_col(
						WPF()->db->prepare(
							"SELECT `topicid` FROM `" . WPF()->tables->topics . "` WHERE userid = %d",
							$userid
						)
					) ) {
						foreach( $topicids as $topicid ) $this->delete( $topicid, false, false );
					}
				}
				
			} else {
				if( $reassign = wpforo_bigintval( $reassign ) ) {
					$data   = [ 'userid' => $reassign ];
					$format = [ '%d' ];
				} else {
					$data   = [
						'userid' => 0,
						'name'   => wpforo_phrase( 'Anonymous', false ) . " " . $userid,
						'email'  => "anonymous_$userid@example.com",
					];
					$format = [ '%d', '%s', '%s' ];
				}
				
				foreach( $boardids as $boardid ) {
					WPF()->change_board( $boardid );
					WPF()->db->update( WPF()->tables->topics, $data, [ 'userid' => $userid ], $format, [ '%d' ] );
				}
				
			}
		}
	}
}
