<?php

class MailsterPlaceholder {

	private $content;
	private $styles = array();
	private $keeptag;
	private $keeptags;
	private $placeholder         = array();
	private $post_types          = array();
	private $rss                 = array();
	private $rss_timestamp       = null;
	private $campaignID          = null;
	private $subscriberID        = null;
	private $index_offset        = 0;
	private $subscriberHash      = null;
	private $progress_conditions = false;
	private $remove_modules      = true;
	private $replace_custom      = true;
	private $social_services;
	private $apply_the_excerpt_filters = true;
	private $last_post_args            = null;
	private $errors                    = array();
	private static $global_rss_tag     = null;

	/**
	 *
	 *
	 * @param unknown $content (optional)
	 */
	public function __construct( $content = '', $deprecated = null ) {

		$this->set_content( $content );
		$this->post_types = mailster( 'helper' )->get_dynamic_post_types();
	}


	public function __destruct() { }



	/**
	 *
	 *
	 * @param unknown $content (optional)
	 */
	public function set_content( $content = '' ) {
		$this->content = $content;
	}


	/**
	 *
	 *
	 * @param unknown $id
	 */
	public function set_campaign( $id ) {
		$this->campaignID = $id;
		$autoresponder    = mailster( 'campaigns' )->meta( $id, 'autoresponder' );
		if ( $autoresponder && isset( $autoresponder['since'] ) ) {
			$timeoffset = mailster( 'helper' )->gmt_offset( true );
			$this->set_last_post_args(
				array(
					'date_query' => array( 'after' => date( 'Y-m-d H:i:s', $autoresponder['since'] + $timeoffset ) ),
				)
			);
		}
	}


	/**
	 *
	 *
	 * @param unknown $id
	 */
	public function set_subscriber( $id ) {
		$this->subscriberID = $id;
	}

	/**
	 *
	 *
	 * @param unknown $id
	 */
	public function set_index_offset( $offset ) {
		$this->index_offset = $offset;
	}

	/**
	 *
	 *
	 * @param unknown $args
	 */
	public function set_last_post_args( $args ) {
		$this->last_post_args = wp_parse_args( $args, $this->last_post_args );
	}


	public function remove_last_post_args() {
		$this->last_post_args = null;
	}


	/**
	 *
	 *
	 * @param unknown $hash
	 */
	public function set_hash( $hash ) {
		$this->subscriberHash = $hash;
	}


	/**
	 *
	 *
	 * @param unknown $bool (optional)
	 */
	public function replace_custom_tags( $bool = true ) {
		$this->replace_custom = $bool;
	}


	/**
	 *
	 *
	 * @param unknown $removeunused         (optional)
	 * @param unknown $placeholders         (optional)
	 * @param unknown $relative_to_absolute (optional)
	 * @return unknown
	 */
	public function get_content( $removeunused = true, $placeholders = array(), $relative_to_absolute = false ) {
		return $this->do_placeholder( $removeunused, $placeholders, $relative_to_absolute );
	}

	public function has_content( $check_for_modules = false ) {
		$html = trim( $this->get_content( false ) );
		if ( $check_for_modules ) {
			return preg_match( '/<module[^>]*?data-tag="(.*?)"/', $html );
		}
		return ! empty( $html );
	}

	public function error( $error, $html_tag = true ) {
		if ( ! is_wp_error( $error ) ) {
			$error = new WP_Error( 'placeholder_error', (string) $error );
		}
		$this->errors[] = $error;

		// output error only on the preview screen
		if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {

			$message = $error->get_error_message();
			return $html_tag ? '<span style="background-color:#e74e2b;color:#fff;padding:2px;font-size:smaller;">' . esc_html( $message ) . '</span>' : esc_html( $message );
		}
		return '';
	}

	public function has_error() {
		return ! empty( $this->errors );
	}

	public function get_error_messages() {
		$errors = array();
		foreach ( $this->errors as $error ) {
			if ( is_wp_error( $error ) ) {
				$errors[] = $error->get_error_message();
			} else {
				$errors[] = $error;
			}
		}

		return $errors;
	}

	public function clear_placeholder() {
		$this->placeholder = array();
	}


	/**
	 *
	 *
	 * @param unknown $do (optional)
	 */
	public function excerpt_filters( $do = true ) {
		$this->apply_the_excerpt_filters = $do;
	}


	/**
	 *
	 *
	 * @param unknown $campaign_id (optional)
	 * @param unknown $args        (optional)
	 * @return unknown
	 */
	public function add_defaults( $campaign_id = null, $args = array() ) {

		$time = explode( '|', date( 'Y|m|d|H|i|s', current_time( 'timestamp' ) ) );

		$defaults = array(
			'email'   => '<a href="mailto:{emailaddress}">{emailaddress}</a>',
			'year'    => $time[0],
			'month'   => $time[1],
			'day'     => $time[2],
			'hour'    => $time[3],
			'minute'  => $time[4],
			'second'  => $time[5],
			'unsub'   => '<a href="{unsublink}">{unsublinktext}</a>',
			'profile' => '<a href="{profilelink}">{profilelinktext}</a>',
			'lang'    => substr( get_bloginfo( 'language' ), 0, 2 ),
			'dir'     => is_rtl() ? 'rtl' : 'ltr',

		);

		if ( $campaign_id ) {
			$meta = mailster( 'campaigns' )->meta( $campaign_id );
			if ( ! $meta ) {
				$meta = mailster( 'campaigns' )->meta_defaults();
			}

			$defaults = wp_parse_args(
				array(
					'preheader'      => $meta['preheader'],
					'subject'        => $meta['subject'],
					'webversion'     => '<a href="{webversionlink}">{webversionlinktext}</a>',
					'forward'        => '<a href="{forwardlink}">{forwardlinktext}</a>',
					'webversionlink' => get_permalink( $campaign_id ),
					'lists'          => mailster( 'campaigns' )->get_formated_lists( $campaign_id ),
				),
				$defaults
			);

			if ( ! $meta['webversion'] ) {
				$defaults['webversion']     = '';
				$defaults['webversionlink'] = '';
			}

			$this->share_service( get_permalink( $campaign_id ), get_the_title( $campaign_id ) );
		}

		$args = wp_parse_args( $args, $defaults );

		$this->add( mailster_option( 'tags' ) );
		$this->add( mailster_option( 'custom_tags' ) );

		$this->add( apply_filters( 'mailster_placeholder_defaults', $args, $campaign_id ) );
	}


	/**
	 *
	 *
	 * @param unknown $campaign_id (optional)
	 * @param unknown $args        (optional)
	 * @return unknown
	 */
	public function add_custom( $campaign_id = null, $args = array() ) {

		$unsubscribelink = mailster()->get_unsubscribe_link( $campaign_id, $this->subscriberHash );
		$forwardlink     = mailster()->get_forward_link( $campaign_id );
		$profilelink     = mailster()->get_profile_link( $campaign_id, $this->subscriberHash );

		$defaults = array(
			'webversionlinktext' => mailster_text( 'webversion' ),
			'unsublinktext'      => mailster_text( 'unsubscribelink' ),
			'forwardlinktext'    => mailster_text( 'forward' ),
			'profilelinktext'    => mailster_text( 'profile' ),
			'unsublink'          => $unsubscribelink,
			'forwardlink'        => $forwardlink,
			'profilelink'        => $profilelink,
		);

		$args = wp_parse_args( $args, $defaults );

		$this->add( apply_filters( 'mailster_placeholder_custom', $args, $campaign_id, $this->subscriberID ) );
	}


	/**
	 *
	 *
	 * @param unknown $placeholder (optional)
	 * @param unknown $brackets    (optional)
	 * @return unknown
	 */
	public function add( $placeholder = array(), $brackets = true ) {
		if ( empty( $placeholder ) ) {
			return false;
		}

		foreach ( $placeholder as $key => $value ) {
			( $brackets )
				? $this->placeholder[ '{' . $key . '}' ] = $value
				: $this->placeholder[ $key ]             = $value;
		}
	}


	/**
	 *
	 *
	 * @param unknown $bool (optional)
	 */
	public function do_conditions( $bool = true ) {
		$this->progress_conditions = (bool) $bool;
	}

	/**
	 *
	 *
	 * @param unknown $bool (optional)
	 */
	public function do_remove_modules( $bool = true ) {
		$this->remove_modules = (bool) $bool;
	}


	/**
	 *
	 *
	 * @param unknown $removeunused         (optional)
	 * @param unknown $placeholders         (optional)
	 * @param unknown $relative_to_absolute (optional)
	 * @return unknown
	 */
	public function do_placeholder( $removeunused = true, $placeholders = array(), $relative_to_absolute = false ) {

		// nothing to replace for sure
		if ( empty( $this->content ) || false === strpos( $this->content, '{' ) ) {
			return $this->content;
		}

		$this->add( $placeholders );

		$this->rss();
		$this->conditions();
		$this->remove_modules();

		// as long there are tags in the content.
		while ( false !== strpos( $this->content, '{' ) ) {

			$initial_content = $this->content;
			// temporary remove style blocks if exists
			if ( strpos( $this->content, '<style' ) !== false && preg_match_all( '#(<style(>|[^<]+?>)([^<]+)<\/style>)#', $this->content, $styles ) ) {
				foreach ( $styles[0] as $style ) {
					$this->content  = str_replace( $style, '<!--Mailster:styleblock' . count( $this->styles ) . '-->', $this->content );
					$this->styles[] = $style;
				}
			}

			$this->rss( false ); // once more without modules
			$this->replace_images( $relative_to_absolute );
			$this->replace_dynamic( $relative_to_absolute );
			$this->replace_static( $removeunused );

			// if content hasn't changed in this loop => break;
			if ( $initial_content == $this->content ) {
				break;
			}
		}

		if ( ! empty( $this->keeptag ) ) {
			foreach ( $this->keeptag as $i => $keeptag ) {
				$this->content = str_replace( '<!--Mailster:keeptag' . $i . '-->', $keeptag, $this->content );
			}
		}

		if ( ! empty( $this->styles ) ) {
			foreach ( $this->styles as $i => $style ) {
				$this->content = str_replace( '<!--Mailster:styleblock' . $i . '-->', $style, $this->content );
			}
		}

		/**
		 * Filters the replaced content
		 *
		 * @param string $content
		 */
		return apply_filters( 'mailster_do_placeholder', $this->content );
	}


	/**
	 *
	 *
	 * @param unknown $url
	 * @param unknown $title (optional)
	 */
	private function share_service( $url, $title = '' ) {

		$placeholders = array();

		$social = array( 'twitter', 'facebook', 'google', 'linkedin' );

		$social = implode( '|', apply_filters( 'mailster_share_services', $social ) );

		if ( $count = preg_match_all( '#\{(share:(' . $social . ') ?([^}]+)?)\}#i', $this->content, $hits ) ) {

			for ( $i = 0; $i < $count; $i++ ) {

				$service = $hits[2][ $i ];

				$url = ! empty( $hits[3][ $i ] ) ? $hits[3][ $i ] : $url;

				$placeholders[ $hits[1][ $i ] ] = $this->get_social_service( $service, $url, $title );

			}
		}

		$this->add( $placeholders );
	}


	/**
	 *
	 *
	 * @param unknown $service
	 * @param unknown $url
	 * @param unknown $title    (optional)
	 * @param unknown $fallback (optional)
	 * @return unknown
	 */
	private function get_social_service( $service, $url, $title = '', $fallback = '' ) {

		// bit caching
		if ( ! $this->social_services ) {
			$this->social_services = mailster( 'helper' )->social_services();
		}

		if ( ! isset( $this->social_services[ $service ] ) ) {
			return $fallback;
		}

		$_url = str_replace(
			array( '%title', '%url' ),
			array(
				rawurlencode( $title ),
				rawurlencode( $url ),
			),
			$this->social_services[ $service ]['url']
		);

		$content = '<img alt="' . esc_attr( sprintf( esc_html__( 'Share this on %s', 'mailster' ), $this->social_services[ $service ]['name'] ) ) . '" src="' . MAILSTER_URI . 'assets/img/share/share_' . $service . '.png" style="display:inline;display:inline !important;" />';

		$content = apply_filters( 'mailster_share_button_' . $service, $content );

		return '<a href="' . $_url . '" class="social">' . $content . '</a>' . "\n";
	}


	/**
	 *
	 *
	 * @return unknown
	 */
	public function rss_since( $timestamp = null ) {
		if ( is_null( $timestamp ) ) {
			$timestamp = time();
		}

		$this->rss_timestamp = $timestamp;
	}


	/**
	 *
	 *
	 * @return unknown
	 */
	private function rss( $do_modules = true ) {

		if ( $do_modules ) {
			if ( preg_match_all( '#<module[^>]*?data-rss="(.*?)".*?</module>#ms', $this->content, $modules ) ) {

				foreach ( $modules[0] as $i => $html ) {

					$search   = $modules[0][ $i ];
					$feed_url = $modules[1][ $i ];

					// $last_feed_timestamp = mailster( 'helper' )->new_feed_since( $this->rss_timestamp, $feed_url );
					$feeds = mailster( 'helper' )->get_feed_since( $this->rss_timestamp, $feed_url );

					if ( ! empty( $feeds ) ) {
						$replace        = '';
						$id             = md5( $feed_url );
						$temp_post_type = 'mailster_rss_' . $id;

						if ( ! isset( $this->rss[ $id ] ) ) {
							$this->rss[ 'mailster_get_last_post_' . $temp_post_type ] = $feed_url;
							add_filter( 'mailster_get_last_post_' . $temp_post_type, array( &$this, 'rss_replace' ), 5, 6 );
							if ( $i === 0 ) {
								self::$global_rss_tag = $temp_post_type;
							}
						}

						$this->post_types[] = $temp_post_type;
						$replace            = str_replace( '{rss', '{' . $temp_post_type, $search );
						$replace            = str_replace( 'mailster_image_placeholder&amp;tag=rss', 'mailster_image_placeholder&amp;tag=' . $temp_post_type, $replace );
						$replace            = str_replace( 'mailster_image_placeholder&tag=rss', 'mailster_image_placeholder&tag=' . $temp_post_type, $replace );

					} else {
						$replace = '';
					}

					$this->content = str_replace( $search, $replace, $this->content );
				}
			}
		} else {
			// replace all tags outside the scope
			if ( self::$global_rss_tag ) {
				$this->content = str_replace( '{rss', '{' . self::$global_rss_tag, $this->content );
				$this->content = str_replace( 'mailster_image_placeholder&amp;tag=rss', 'mailster_image_placeholder&amp;tag=' . self::$global_rss_tag, $this->content );
				$this->content = str_replace( 'mailster_image_placeholder&tag=rss', 'mailster_image_placeholder&tag=' . self::$global_rss_tag, $this->content );
			}
		}
	}


	/**
	 *
	 *
	 * @return unknown
	 */
	public function rss_replace( $return, $args, $offset, $term_ids, $campaign_id, $subscriber_id ) {

		$current = current_filter();

		if ( ! isset( $this->rss[ $current ] ) ) {
			return $return;
		}

		$feed_url = $this->rss[ $current ];
		return mailster( 'helper' )->feed( $feed_url, $offset );
	}


	/**
	 *
	 *
	 * @return unknown
	 */
	private function remove_modules() {

		if ( ! $this->remove_modules ) {
			return;
		}

		if ( preg_match_all( '#<module[^>]*?data-tag="{(([a-z0-9_-]+):(-|~)?([\d]+)(;([0-9;,-]+))?)\}"(.*?)".*?</module>#ms', $this->content, $modules ) ) {

			foreach ( $modules[0] as $i => $html ) {

				$search         = $modules[0][ $i ];
				$tag            = $modules[1][ $i ];
				$post_type      = $modules[2][ $i ];
				$post_or_offset = $modules[4][ $i ];
				$type           = $modules[3][ $i ];

				if ( empty( $type ) ) {
					$post = get_post( $post_or_offset );
				} else {
					$term_ids = ! empty( $modules[6][ $i ] ) ? explode( ';', trim( $modules[6][ $i ] ) ) : array();
					if ( '~' == $type ) {
						$post = mailster()->get_random_post( $post_or_offset, $post_type, $term_ids, $this->last_post_args, $this->campaignID, $this->subscriberID );
					} else {
						$post = mailster()->get_last_post( $post_or_offset - 1 + $this->index_offset, $post_type, $term_ids, $this->last_post_args, $this->campaignID, $this->subscriberID );
					}
				}

				if ( ! $post || is_wp_error( $post ) ) {
					$this->content = str_replace( $search, '', $this->content );
				}
			}
		}
	}


	/**
	 *
	 *
	 * @return unknown
	 */
	public function conditions() {

		if ( ! $this->progress_conditions ) {
			return;
		}

		if ( preg_match_all( '#<(module|single|multi)[^>]*?condition="([a-z0-9-_]+)([=!GLTE\^$]+)(.*?)".*?</(\\1)>#ms', $this->content, $conditions ) ) {

			$subscriber = $this->subscriberID ? mailster( 'subscribers' )->get( $this->subscriberID, true, true ) : false;

			foreach ( $conditions[0] as $i => $html ) {
				$key      = $conditions[2][ $i ];
				$operator = $conditions[3][ $i ];
				$value    = $conditions[4][ $i ];

				if ( $operator == '=' && isset( $subscriber->{$key} ) && $subscriber->{$key} == $value ) {
					continue;
				}

				if ( $operator == '!=' && ( ! isset( $subscriber->{$key} ) || $subscriber->{$key} != $value ) ) {
					continue;
				}

				if ( $operator == '^' && isset( $subscriber->{$key} ) && false !== ( strrpos( $subscriber->{$key}, $value, -strlen( $subscriber->{$key} ) ) ) ) {
					continue;
				}

				if ( $operator == '$' && isset( $subscriber->{$key} ) && false !== ( ( $t = strlen( $subscriber->{$key} ) - strlen( $value ) ) >= 0 && strpos( $subscriber->{$key}, $value, $t ) ) ) {
					continue;
				}

				if ( $operator == 'GT' && isset( $subscriber->{$key} ) && $subscriber->{$key} > $value ) {
					continue;
				}

				if ( $operator == 'GTE' && isset( $subscriber->{$key} ) && $subscriber->{$key} >= $value ) {
					continue;
				}

				if ( $operator == 'LT' && isset( $subscriber->{$key} ) && $subscriber->{$key} < $value ) {
					continue;
				}

				if ( $operator == 'LTE' && isset( $subscriber->{$key} ) && $subscriber->{$key} <= $value ) {
					continue;
				}

				$this->content = str_replace( $html, '', $this->content );
			}
		}

		if ( preg_match_all( '#<if field="([a-z0-9-_]+)" operator="([a-z_]+)+" value="(.*?)">(.*?)</if>#s', $this->content, $if_conditions ) ) {

			$subscriber = $this->subscriberID ? mailster( 'subscribers' )->get( $this->subscriberID, true, true ) : false;

			foreach ( $if_conditions[0] as $i => $ifhtml ) {

				// if condition passed
				if ( $this->check_condition( $subscriber, $if_conditions[1][ $i ], $if_conditions[2][ $i ], $if_conditions[3][ $i ] ) ) {

					$html          = $if_conditions[4][ $i ];
					$html          = preg_replace( '#<elseif(.*?)<\/elseif>#s', '', $if_conditions[4][ $i ] );
					$html          = preg_replace( '#<else(.*?)<\/else>#s', '', $html );
					$this->content = str_replace( $ifhtml, $html, $this->content );

				} else {

					// elseif exists
					if ( preg_match_all( '#<elseif field="([a-z0-9-_]+)" operator="([a-z_]+)+" value="(.*?)">(.*?)</elseif>#s', $ifhtml, $elseif_conditions ) ) {

						foreach ( $elseif_conditions[0] as $j => $elseifhtml ) {

							// elseif condition passed
							if ( $this->check_condition( $subscriber, $elseif_conditions[1][ $j ], $elseif_conditions[2][ $j ], $elseif_conditions[3][ $j ] ) ) {

								$this->content = str_replace( $ifhtml, $elseif_conditions[4][ $j ], $this->content );

								break;

							} elseif ( $j == count( $elseif_conditions[0] ) - 1 ) {
									// no elseif passes
								if ( preg_match( '#<else>(.*?)</else>#s', $ifhtml, $else_conditions ) ) {

									$this->content = str_replace( $ifhtml, $else_conditions[1], $this->content );

									break;

								}
							}
						}

						// no else if but else
					} elseif ( preg_match( '#<else>(.*?)</else>#s', $ifhtml, $else_conditions ) ) {

						$this->content = str_replace( $ifhtml, $else_conditions[1], $this->content );

						// only if statement but didn't pass
					} else {

						$this->content = str_replace( $ifhtml, '', $this->content );

					}
				}
			}
		}

		return $this->content;
	}


	/**
	 *
	 *
	 * @param unknown $subscriber
	 * @param unknown $field
	 * @param unknown $operator
	 * @param unknown $value
	 * @return unknown
	 */
	private function check_condition( $subscriber, $field, $operator, $value ) {

		// return only true if operator is is_not
		if ( ! isset( $subscriber->{$field} ) ) {
			return $operator == 'is_not' || $operator == 'not_pattern';
		}

		switch ( $operator ) {
			case 'is':
				return $subscriber->{$field} == $value;
			case 'is_not':
				return $subscriber->{$field} != $value;
			case 'begin_with':
				return false !== ( strrpos( $subscriber->{$key}, $value, - strlen( $subscriber->{$key} ) ) );
			case 'end_with':
				return false !== ( ( $t = strlen( $subscriber->{$key} ) - strlen( $value ) ) >= 0 && strpos( $subscriber->{$key}, $value, $t ) );
			case 'is_greater':
				return $subscriber->{$key} > $value;
			case 'is_greater_equal':
				return $subscriber->{$key} >= $value;
			case 'is_smaller':
				return $subscriber->{$key} < $value;
			case 'is_smaller_equal':
				return $subscriber->{$key} <= $value;
			case 'pattern':
				return preg_match( '#' . preg_quote( $subscriber->{$key} ) . '#', $value );
			case 'not_pattern':
				return ! preg_match( '#' . preg_quote( $subscriber->{$key} ) . '#', $value );
		}

		return false;
	}


	/**
	 *
	 *
	 * @param unknown $relative_to_absolute (optional)
	 */
	private function replace_images( $relative_to_absolute = false ) {

		// placeholder images
		if ( $count = preg_match_all( '#<(img|td|th|v:fill)([^>]*)(src|background)="([^"]+)"([^>]*)>#', $this->content, $hits ) ) {

			for ( $i = 0; $i < $count; $i++ ) {

				$search = $hits[0][ $i ];

				// check if string is still there
				if ( $i && false === strrpos( $this->content, $search ) ) {
					continue;
				}

				$imagestring = str_replace( '&amp;', '&', $hits[4][ $i ] );
				// not a placeholder image
				if ( strpos( $imagestring, 'action=mailster_image_placeholder' ) === false ) {
					continue;
				}

				$tag         = $hits[1][ $i ];
				$pre_stuff   = str_replace( '&amp;', '&', $hits[2][ $i ] );
				$attribute   = $hits[3][ $i ];
				$querystring = wp_parse_url( $imagestring, PHP_URL_QUERY );
				$post_stuff  = str_replace( '&amp;', '&', $hits[5][ $i ] );
				$is_img_tag  = 'img' == $tag;
				$cache_key   = md5( $search );

				wp_parse_str( $querystring, $query );

				if ( isset( $query['tag'] ) ) {

					$replace_to = mailster_cache_get( $cache_key );

					if ( false === $replace_to ) {

						$parts     = explode( ':', trim( $query['tag'] ) );
						$factor    = isset( $query['f'] ) && $is_img_tag ? (int) $query['f'] : 1;
						$width     = isset( $query['w'] ) ? (int) $query['w'] * $factor : null;
						$height    = isset( $query['h'] ) ? (int) $query['h'] * $factor : null;
						$crop      = isset( $query['c'] ) && $height ? (bool) ( $query['c'] ) : false;
						$original  = isset( $query['o'] ) ? (bool) ( $query['o'] ) : false;
						$post_type = str_replace( '_image', '', $parts[0] );
						$is_post   = $post_type != $parts[0];
						$is_random = null;
						$org_src   = false;
						$is_author = false;

						// not on RSS
						if ( $relative_to_absolute && 0 === strpos( $post_type, 'mailster_rss_' ) ) {
							$relative_to_absolute = false;
						}
						// tag is [post_type]_author_image
						if ( false !== strpos( $post_type, '_author' ) ) {
							$is_author = true;
							$post_type = str_replace( '_author', '', $post_type );
						}
						if ( $is_post ) {
							// cropping requires height
							if ( ! $crop ) {
								$height = null;
							}
							$extra                 = explode( '|', $parts[1] );
							$term_ids              = explode( ';', $extra[0] );
							$fallback_id           = isset( $extra[1] ) ? (int) $extra[1] : mailster_option( 'fallback_image' );
							$post_id_or_identifier = array_shift( $term_ids );
							$is_random             = 0 === strpos( $post_id_or_identifier, '~' );

							if ( $is_random ) {

								$post = mailster()->get_random_post( substr( $post_id_or_identifier, 1 ), $post_type, $term_ids, $this->last_post_args, $this->campaignID, $this->subscriberID );

							} elseif ( $post_id_or_identifier < 0 ) {

								$post = mailster()->get_last_post( abs( $post_id_or_identifier ) - 1 + $this->index_offset, $post_type, $term_ids, $this->last_post_args, $this->campaignID, $this->subscriberID );

							} elseif ( $post_id_or_identifier > 0 ) {

								if ( $relative_to_absolute ) {
									continue;
								}
								$post = get_post( $post_id_or_identifier );

							}
						} else {

							$fallback_id = mailster_option( 'fallback_image' );
							$post        = null;
							$thumb_id    = null;
							$src         = apply_filters( 'mailster_image_placeholder', $query['tag'], $width, $height, $crop, $original, $this->campaignID, $this->subscriberID );
							if ( $src && $src != $query['tag'] ) {
								if ( ! is_array( $src ) ) {
									$src = array( $src, $width, $height );
								}
								$org_src = $src;
							} else {
								$org_src = array( '{' . $query['tag'] . '}', $width, $height );
							}
						}

						if ( ! $relative_to_absolute ) {

							if ( ! empty( $post ) ) {

								if ( is_numeric( $post->ID ) ) {

									// tag is [post_type]_author_image
									if ( $is_author ) {
										$org_src = array( get_avatar_url( $post->post_author, array( 'width' => $width ) ), 0, 0, false );
									} else {
										if ( 'attachment' == $post->post_type ) {
											$thumb_id = $post->ID;
										} else {
											$thumb_id = get_post_thumbnail_id( $post->ID );
										}
										$org_src = wp_get_attachment_image_src( $thumb_id, 'full' );
									}
								}

								if ( empty( $org_src ) ) {
									if ( is_numeric( $post->post_image ) ) {
										$org_src = wp_get_attachment_image_src( $post->post_image, 'full' );
									} elseif ( empty( $post->post_image ) ) {

										// get image from sites meta tags
										if ( $src = mailster( 'helper' )->get_meta_tags_from_url( $post->post_permalink, array( 'mailster:image', 'thumbnail', 'image', 'og:image', 'twitter:image' ) ) ) {
											$org_src = array( $src, 0, 0, false );
										}
									} else {
										$org_src = array( $post->post_image, 0, 0, false );
									}
								}
							}

							if ( empty( $org_src ) && $fallback_id ) {
								$thumb_id = $fallback_id;

								$org_src = wp_get_attachment_image_src( $thumb_id, 'full' );

							}

							if ( ! empty( $org_src ) ) {

								if ( $org_src[1] && $org_src[2] ) {
									$asp    = $org_src[1] / $org_src[2];
									$height = $height ? $height : round( ( $width / $asp ) / $factor );
									$img    = mailster( 'helper' )->create_image( $thumb_id, $org_src[0], $width, $height * $factor, $crop, $original );
								} else {
									$img = array( 'url' => $org_src[0] );
								}

								if ( $is_img_tag ) {
									// set new height
									$post_stuff = preg_replace( '# height="(\d+)"#i', $height ? ' height="' . $height . '"' : '', $post_stuff );
									$pre_stuff  = preg_replace( '# height="(\d+)"#i', $height ? ' height="' . $height . '"' : '', $pre_stuff );

									$replace_to = '<img' . $pre_stuff . 'src="' . $img['url'] . '" ' . $post_stuff . '>';
								} else {
									$pre_stuff  = str_replace( $imagestring, $img['url'], $pre_stuff );
									$post_stuff = str_replace( $imagestring, $img['url'], $post_stuff );
									$replace_to = '<' . $tag . $pre_stuff . 'background="' . $img['url'] . '" ' . $post_stuff . '>';
								}
							} elseif ( $is_img_tag ) {

									$replace_to = '';
							} else {
								$pre_stuff  = str_replace( $imagestring, '', $pre_stuff );
								$post_stuff = str_replace( $imagestring, '', $post_stuff );
								$replace_to = '<' . $tag . $pre_stuff . 'background="" ' . $post_stuff . '>';
							}
						} else {
							// not if no ID is provided (custom dynamic post types)
							if ( ! $post->ID ) {
								continue;
							}
							if ( $post && ! $is_random ) {
								$replace_to = str_replace( 'tag=' . $query['tag'], 'tag=' . $post_type . '_image:' . $post->ID, $search );
							}
						}

						// cache it for faster processing
						mailster_cache_set( $cache_key, $replace_to );
					}

					if ( false !== $replace_to ) {
						$replace_to = apply_filters( 'mailster_replace_image', $replace_to, $search, $this->campaignID, $this->subscriberID );
						if ( is_wp_error( $replace_to ) ) {
							$this->error( $replace_to );
							continue;
						}

						$this->content = str_replace( $search, $replace_to, $this->content );
					}
				}
			}
		}
	}


	/**
	 *
	 *
	 * @param unknown $relative_to_absolute (optional)
	 */
	private function replace_dynamic( $relative_to_absolute = false ) {

		$pts = implode( '|', $this->post_types );

		// all dynamic post type tags
		if ( $count = preg_match_all( '#\{(!)?((' . $pts . ')_([^}]+):(-|~)?([\d]+)(;([0-9;,-]+))?)\}#i', $this->content, $hits ) ) {

			for ( $i = 0; $i < $count; $i++ ) {

				$search         = $hits[0][ $i ];
				$encode         = ! empty( $hits[1][ $i ] );
				$post_or_offset = $hits[6][ $i ];
				$post_type      = $hits[3][ $i ];
				$type           = $hits[5][ $i ];
				$term_ids       = ! empty( $hits[8][ $i ] ) ? explode( ';', trim( $hits[8][ $i ] ) ) : array();
				$is_random      = '~' == $type;

				// not on RSS
				if ( $relative_to_absolute && 0 === strpos( $post_type, 'mailster_rss_' ) ) {
					$relative_to_absolute = false;
				}

				if ( empty( $type ) || $is_random ) {

					$post_id = $post_or_offset;

					if ( $relative_to_absolute ) {
						continue;
					}

					if ( $is_random ) {
						$post = mailster()->get_random_post( $post_or_offset, $post_type, $term_ids, $this->last_post_args, $this->campaignID, $this->subscriberID );
					} else {
						$post = get_post( $post_or_offset );
					}

					$post = apply_filters( 'mailster_get_post_' . $post_type, $post, $post_id );

					if ( ! $post ) {
						continue;
					}

					$post->post_excerpt = trim( $post->post_excerpt );

					if ( empty( $post->post_excerpt ) && preg_match( '/<!--more(.*?)?-->/', $post->post_content, $matches ) ) {
						$content            = explode( $matches[0], $post->post_content, 2 );
						$post->post_excerpt = trim( $content[0] );
						$post->post_excerpt = mailster_remove_block_comments( $post->post_excerpt );
					}

					if ( empty( $post->post_excerpt ) ) {
						$post->post_excerpt = mailster( 'helper' )->get_excerpt( $post->post_content );
					}

					if ( $this->apply_the_excerpt_filters ) {
						if ( $length = apply_filters( 'mailster_excerpt_length', null ) ) {
							$post->post_excerpt = wp_trim_words( $post->post_excerpt, $length );
						}
						$post->post_excerpt = apply_filters( 'the_excerpt', $post->post_excerpt );
					}
				} else {

					$post_offset = $post_or_offset - 1;
					$term_ids    = ! empty( $hits[8][ $i ] ) ? explode( ';', trim( $hits[8][ $i ] ) ) : array();

					$post = mailster()->get_last_post( $post_or_offset - 1 + $this->index_offset, $post_type, $term_ids, $this->last_post_args, $this->campaignID, $this->subscriberID );

				}

				if ( $relative_to_absolute && $post ) {

					// not if no ID is provided (custom dynamic post types)
					if ( ! $post->ID ) {
						continue;
					}

					if ( $encode ) {
						$replace_to = '{!' . $post_type . '_' . $hits[4][ $i ] . ':' . $post->ID . '}';
					} else {
						$replace_to = '{' . $post_type . '_' . $hits[4][ $i ] . ':' . $post->ID . '}';
					}
				} elseif ( $post ) {

					$what = $hits[4][ $i ];

					$replace_to = $this->get_replace( $post, $what );
					if ( is_wp_error( $replace_to ) ) {
						$replace_to = $this->error( $replace_to, ! $encode );
					}

					if ( is_null( $replace_to ) ) {
						continue;
					}

					if ( $encode ) {
						$replace_to = rawurlencode( $replace_to );
					}
				} else {
					$replace_to = '';
				}

				$this->content = str_replace( $search, $replace_to, $this->content );

			}
		}

		if ( ! $relative_to_absolute ) {
			if ( $count = preg_match_all( '#\{(!)?(tweet:([^}|]+)\|?([^}]+)?)\}#i', $this->content, $hits ) ) {

				for ( $i = 0; $i < $count; $i++ ) {
					$search = $hits[0][ $i ];
					$encode = ! empty( $hits[1][ $i ] );
					$tweet  = $this->get_last_tweet( $hits[3][ $i ], $hits[4][ $i ] );
					if ( $encode ) {
						$tweet = rawurlencode( $tweet );
					}
					$this->content = str_replace( $search, $tweet, $this->content );
				}
			}
		}
	}


	/**
	 *
	 *
	 */
	private function replace_static( $removeunused = true ) {

		global $mailster_tags;

		if ( ! did_action( 'mailster_add_tag' ) ) {
			do_action( 'mailster_add_tag' );
		}

		$keep = $this->get_keep_tags();

		// replace static
		if ( $count = preg_match_all( '#\{(!)?([a-z0-9-_]+):?([^\}|]+)?\|?([^\}]+)?\}#i', $this->content, $hits_fallback ) ) {

			for ( $i = 0; $i < $count; $i++ ) {

				$search = $hits_fallback[0][ $i ];
				$encode = ! empty( $hits_fallback[1][ $i ] );

				// check if string is still there
				if ( $i && false === strrpos( $this->content, $search ) ) {
					continue;
				}

				$tag      = $hits_fallback[2][ $i ];
				$option   = $hits_fallback[3][ $i ];
				$fallback = $hits_fallback[4][ $i ];
				$replace  = '';

				// tag is in placeholders
				if ( isset( $this->placeholder[ $search ] ) ) {
					$replace = $this->placeholder[ $search ];

					if ( $replace && '{preheader}' === $search ) {

						/**
						 * Adds invisible characters after the preheader text also known as "preview text hack"
						 * Read more here: https://www.litmus.com/blog?p=4367
						 * Updated in Sept. 2023 to use the new invisible characters
						 *
						 * defaults:
						 * `true`
						 *
						 * @param boolean $default default values
						 */
						$preview_text_fix = apply_filters( 'mailster_preview_text_fix', true );

						if ( $preview_text_fix ) {
							$preview_text_count = 200 - strlen( $replace );
							// PHP throws an error if $count = 0, preventing the campaign from being sent. This check patch this.
							if ( $preview_text_count > 0 ) {
								$replace .= str_repeat( ' &#847; &zwnj; &nbsp; &#8199; &shy;', $preview_text_count );
							}
						}
					}

					// tag is a custom tag
				} elseif ( isset( $this->placeholder[ '{' . $tag . '}' ] ) ) {
					$replace = $this->placeholder[ '{' . $tag . '}' ];

					// tag is a custom tag
				} elseif ( isset( $mailster_tags[ $tag ] ) && $this->replace_custom ) {
					$replace = call_user_func_array( $mailster_tags[ $tag ], array( $option, $fallback, $this->campaignID, $this->subscriberID ) );
					// prevent infinity loops if replace contains it's own tag
					if ( false !== strpos( $replace, '{' . $tag ) ) {
						$replace = str_replace( array( '{', '}' ), array( '!', '!' ), $replace );
					}

					// tag should be kept
				} elseif ( in_array( $tag, $keep ) ) {
					if ( $fallback ) {
						$replace = $fallback;
					} else {
						$this->keeptag[ $i ] = $search;
						$replace             = '<!--Mailster:keeptag' . $i . '-->';
					}

					// keep unused
				} elseif ( ! $removeunused ) {
					continue;
				}

				// add fallback if set
				if ( '' == $replace && $fallback ) {
					$replace = $fallback;
				}
				$replace_to = apply_filters( 'mailster_replace_' . $tag, $replace, $option, $fallback, $this->campaignID, $this->subscriberID );

				if ( is_wp_error( $replace_to ) ) {
					$replace_to = $this->error( $replace_to, ! $encode );
				}

				if ( $encode ) {
					$replace_to = rawurlencode( $replace_to );
				}

				$this->content = str_replace( $search, $replace_to, $this->content );
			}
		}
	}

	public function get_replace( $post, $what ) {

		if ( is_wp_error( $post ) ) {
			return $post;
		}

		$extra        = null;
		$replace_to   = null;
		$author       = null;
		$post_type    = $post->post_type;
		$post_content = $post->post_content;

		// author related tags
		if ( 0 === strpos( $what, 'author' ) ) {
			$author = get_user_by( 'id', $post->post_author );
			$extra  = $author;

			// post meta related tags (e.g meta[meta_key])
		} elseif ( 0 === strpos( $what, 'meta[' ) ) {
			preg_match( '#meta\[(.*)\]#i', $what, $metakey );
			if ( ! isset( $metakey[1] ) ) {
				return null;
			}

			$metakey   = trim( $metakey[1] );
			$metavalue = get_post_meta( $post->ID, $metakey, true );
			if ( is_null( $metavalue ) ) {
				return null;
			}

			$what  = 'meta';
			$extra = $metakey;

			// categories related tags (e.g categories[taxonomy])
		} elseif ( 0 === strpos( $what, 'categories' ) || 0 === strpos( $what, 'category' ) ) {

			// if set in the post object use this
			if ( is_string( $post->post_category ) ) {
				$extra = is_string( $post->post_category ) ? array( $post->post_category ) : $post->post_category;
			} else {
				preg_match( '#(category|categories)(_strip)?\[(.*)\]#i', $what, $matches );
				// check taxonomy type
				if ( isset( $matches[2] ) ) {
					$taxonomy = trim( $matches[3] );
					$what     = $matches[1] . $matches[2];
				} else {
					$taxonomy = 'category';
				}

				// get all terms of the taxonomy
				$terms = get_the_term_list( $post->ID, $taxonomy, '', ', ' );

				// no terms or error
				if ( ! $terms || is_wp_error( $terms ) ) {
					return null;
				}

				// convert to array
				$extra = explode( ', ', $terms );

				// only first
				if ( 0 === strpos( $what, 'category' ) ) {
					// get all terms of the taxonomy
					$extra = array( $extra[0] );
				}
			}
			// convert to string
			$terms = implode( ', ', $extra );

			// with format (e.g date[format])
		} elseif ( false !== strpos( $what, '[' ) ) {
			preg_match( '#(.*)\[(.*)\]#i', $what, $withformat );
			if ( ! isset( $withformat[1] ) ) {
				return null;
			}
			$what  = trim( $withformat[1] );
			$extra = trim( $withformat[2] );
		}

		switch ( $what ) {
			case 'id':
				$replace_to = $post->ID;
				break;
			case 'title':
				$replace_to = str_replace( array( '"', "'" ), array( '&quot;', '&#039;' ), $post->post_title );
				break;
			case 'button':
				$replace_to = esc_html__( 'Read More', 'mailster' );
				break;
			case 'link':
				if ( isset( $post->post_link ) ) {
					$replace_to = $post->post_link;
				}
			case 'permalink':
				if ( ! $replace_to && $post->ID ) {
					$replace_to = get_permalink( $post->ID );
				}
				if ( ! $replace_to ) {
					$replace_to = $post->post_link;
				}
				if ( ! $replace_to ) {
					$replace_to = $post->post_permalink;
				}
				break;
			case 'shortlink':
				if ( $post->ID ) {
					$replace_to = wp_get_shortlink( $post->ID );
				} else {
					$replace_to = $post->post_permalink;
				}
				break;
			case 'author':
			case 'author_strip':
				if ( ! $author ) {
					$replace_to = $post->post_author;
					break;
				}
				if ( $author->data->user_url && $what != 'author_strip' ) {
					$replace_to = '<a href="' . $author->data->user_url . '">' . $author->data->display_name . '</a>';
				} else {
					$replace_to = $author->data->display_name;
				}
				break;
			case 'author_name':
				$replace_to = $author ? $author->data->display_name : $post->post_author;
				break;
			case 'author_nicename':
				$replace_to = $author ? $author->data->user_nicename : $post->post_author;
				break;
			case 'author_email':
				$replace_to = $author ? $author->data->user_email : '';
				break;
			case 'author_url':
				$replace_to = $author ? $author->data->user_url : '';
				break;
			case 'author_info':
				$replace_to = $author ? get_the_author_meta( 'description', $author->ID ) : '';
				break;
			case 'author_image':
				$replace_to = $author ? get_avatar_url( $author->ID ) : '';
				break;
			case 'date':
			case 'date_gmt':
			case 'modified':
			case 'modified_gmt':
				$replace_to = date_i18n( $extra ? $extra : mailster( 'helper' )->dateformat(), strtotime( $post->{'post_' . $what} ) );
				break;
			case 'time':
				$what = 'date';
			case 'time_gmt':
				$what = isset( $what ) ? $what : 'date_gmt';
			case 'modified_time':
				$what = isset( $what ) ? $what : 'modified';
			case 'modified_time_gmt':
				$what       = isset( $what ) ? $what : 'modified_gmt';
				$replace_to = date_i18n( $extra ? $extra : mailster( 'helper' )->timeformat(), strtotime( $post->{'post_' . $what} ) );
				break;
			case 'excerpt':
				if ( ! empty( $post->{'post_excerpt'} ) ) {
					$replace_to = wpautop( $post->{'post_excerpt'} );
				} else {
					$replace_to = mailster( 'helper' )->get_excerpt( $post_content );
				}
				break;
			case 'content':
				$replace_to = $post_content;
				if ( function_exists( 'do_blocks' ) && has_blocks( $replace_to ) ) {
					$replace_to = do_blocks( $replace_to );
				}
				$replace_to = wpautop( $replace_to );
				break;
			case 'meta':
				$replace_to = maybe_unserialize( $metavalue );
				break;
			case 'category':
			case 'categories':
				$replace_to = $terms;
				break;
			case 'category_strip':
			case 'categories_strip':
				$replace_to = strip_tags( $terms );
				break;
			case 'twitter':
			case 'facebook':
			case 'google':
			case 'linkedin':
				$replace_to = $this->get_social_service( $what, get_permalink( $post->ID ), get_the_title( $post->ID ) );
				break;
			case 'image':
				$replace_to = '[' . ( sprintf( esc_html__( 'use the tag %s as url in the editbar', 'mailster' ), '"' . $hits[1][ $i ] . '"' ) ) . ']';
				break;
			default:
				if ( isset( $post->{'post_' . $what} ) && $post->{'post_' . $what} ) {
					$replace_to = $post->{'post_' . $what};
				} elseif ( isset( $post->{$what} ) && $post->{$what} ) {
					$replace_to = $post->{$what};
				} else {
					$replace_to = '';
				}
		}

		return apply_filters( 'mailster_replace_' . $post_type . '_' . $what, $replace_to, $post, $extra, $this->campaignID, $this->subscriberID );
	}


	/**
	 *
	 *
	 * @param unknown $username
	 * @param unknown $fallback (optional)
	 * @return unknown
	 */
	private function get_last_tweet( $username, $fallback = '' ) {

		if ( false === ( $tweet = get_transient( 'mailster_tweet_' . $username ) ) ) {

			$token           = mailster_option( 'twitter_token' );
			$token_secret    = mailster_option( 'twitter_token_secret' );
			$consumer_key    = mailster_option( 'twitter_consumer_key' );
			$consumer_secret = mailster_option( 'twitter_consumer_secret' );

			if ( ! $token || ! $token_secret || ! $consumer_key || ! $consumer_secret ) {

				return esc_html__( 'Please enter your Twitter application credentials on the settings page', 'mailster' );

			}

			require_once MAILSTER_DIR . 'classes/libs/twitter.class.php';

			$twitter = new TwitterApiClass( $token, $token_secret, $consumer_key, $consumer_secret );

			if ( is_numeric( $username ) ) {
				$method = 'statuses/show/' . $username;

				$args = array();
			} else {
				$method = 'statuses/user_timeline';

				$args = array(
					'screen_name'      => $username,
					'count'            => 1,
					'include_rts'      => false,
					'exclude_replies'  => true,
					'include_entities' => true,
				);
			}

			$response = $twitter->query( $method, $args );

			if ( is_wp_error( $response ) ) {
				return $fallback;
			}

			$data = $response;

			if ( isset( $data->errors ) ) {
				return $fallback;
			}

			if ( isset( $data->error ) ) {
				return $fallback;
			}

			$tweet = ( is_array( $data ) ) ? $data[0] : $data;

			if ( ! isset( $tweet->text ) ) {
				return $fallback;
			}

			if ( $tweet->entities->hashtags ) {
				foreach ( $tweet->entities->hashtags as $hashtag ) {
					$tweet->text = str_replace( '#' . $hashtag->text, '#<a href="https://twitter.com/search/%23' . $hashtag->text . '">' . $hashtag->text . '</a>', $tweet->text );

				}
			}
			if ( $tweet->entities->urls ) {
				foreach ( $tweet->entities->urls as $url ) {
					$tweet->text = str_replace( $url->url, '<a href="' . $url->url . '">' . $url->display_url . '</a>', $tweet->text );

				}
			}

			$tweet->text = preg_replace( '/(^|\s)@(\w+)/', '\1@<a href="https://twitter.com/\2">\2</a>', $tweet->text );

			set_transient( 'mailster_tweet_' . $username, $tweet, 60 * mailster_option( 'tweet_cache_time' ) );
		}

		return $tweet->text;
	}


	private function get_keep_tags() {
		if ( is_null( $this->keeptags ) ) {
			$this->keeptags = array_unique( (array) apply_filters( 'mailster_keep_tags', array() ) );
		}
		return $this->keeptags;
	}
}
