<?php
/**
 * This file implements the Info dots renderer plugin for b2evolution
 *
 * Info dots formatting, like [infodot:1234:40:60:20ex]text of the info dot additional info text including <a href="/">link text</a>[enddot]
 *
 * b2evolution - {@link http://b2evolution.net/}
 * Released under GNU GPL License - {@link http://b2evolution.net/about/gnu-gpl-license}
 * @copyright (c)2003-2020 by Francois Planque - {@link http://fplanque.com/}
 *
 * @package plugins
 */
if( !defined('EVO_MAIN_INIT') ) die( 'Please, do not access this page directly.' );


/**
 * @package plugins
 */
class infodots_plugin extends Plugin
{
	var $code = 'b2evoDot';
	var $name = 'Info dots renderer';
	var $priority = 95;
	var $version = '7.2.3';
	var $group = 'rendering';
	var $short_desc;
	var $long_desc;
	var $help_topic = 'infodots-plugin';
	var $number_of_installs = 1;

	/*
	 * Internal vars
	 */
	var $search_text;
	var $replace_func;
	var $dots = NULL;
	var $object_ID = 0;
	var $loaded_objects = NULL;
	var $dot_numbers = NULL;

	/**
	 * Init
	 */
	function PluginInit( & $params )
	{
		$this->short_desc = T_('Info dots formatting e-g [infodot:1234:40:60:20ex]html text[enddot]');
		$this->long_desc = T_('This plugin allows to render info dots over images by using the syntax [infodot:1234:40:60:20ex]html text[enddot] for example');
	}


	/**
	 * Define here default custom settings that are to be made available
	 *     in the backoffice for collections, private messages and newsletters.
	 *
	 * @param array Associative array of parameters.
	 * @return array See {@link Plugin::get_custom_setting_definitions()}.
	 */
	function get_custom_setting_definitions( & $params )
	{
		return array(
			'coll_min_width' => array(
					'label' => T_('Min width'),
					'type' => 'integer',
					'size' => 4,
					'defaultvalue' => 400,
					'note' => T_('Enter the minimum pixel width an image must have for dots to be displayed.')
				),
		);
	}


	/**
	 * Define here default collection/blog settings that are to be made available in the backoffice.
	 *
	 * @return array See {@link Plugin::GetDefaultSettings()}.
	 */
	function get_coll_setting_definitions( & $params )
	{
		$default_params = array_merge( $params, array(
				'default_comment_rendering' => 'never',
				'default_post_rendering' => 'opt-out'
			) );

		return parent::get_coll_setting_definitions( $default_params );
	}


	/**
	 * Include JS/CSS files in HTML head
	 */
	function init_html_head()
	{
		global $Collection, $Blog;

		if( ! isset( $Blog ) || (
		    $this->get_coll_setting( 'coll_apply_rendering', $Blog ) == 'never' &&
		    $this->get_coll_setting( 'coll_apply_comment_rendering', $Blog ) == 'never' ) )
		{	// Don't load css/js files when plugin is not enabled
			return;
		}

		$this->require_css_async( 'infodots.css', false, 'footerlines' );

		// Bubbletip
		require_js_defer( '#jquery#', 'blog', false, '#', 'footerlines' );
		require_js_defer( 'customized:jquery/bubbletip/js/jquery.bubbletip.min.js', 'blog', false, '#', 'footerlines' );
		require_css_async( 'customized:jquery/bubbletip/css/jquery.bubbletip.css', 'blog', NULL, NULL, '#', false, 'footerlines' );
		$this->require_js_defer( 'infodots.init.js', false, 'footerlines' );
	}


	/**
	 * Event handler: Called at the beginning of the skin's HTML HEAD section.
	 *
	 * Use this to add any HTML HEAD lines (like CSS styles or links to resource files (CSS, JavaScript, ..)).
	 *
	 * @param array Associative array of parameters
	 */
	function SkinBeginHtmlHead( & $params )
	{
		$this->init_html_head();
	}


	/**
	 * Event handler: Called when ending the admin html head section.
	 *
	 * @param array Associative array of parameters
	 * @return boolean did we do something?
	 */
	function AdminEndHtmlHead( & $params )
	{
		$this->init_html_head();
	}


	/**
	 * Perform rendering
	 *
	 * Renders code:
	 * from [infodot:1234:40:60:20ex]html text[enddot]
	 * to <div class="infodots_info" id="infodot_1234_1" data-xy="40:60" style="width:20ex:>html text</div>
	 * This html <div> is hidden by an inline CSS style="display: none;". 
	 * This plugin event is called only once to render content.
	 * This only generates the content divs. The red dots themselves are added later to the evo_image_block divs, in the "RenderItemAttachment" event.
	 * 
	 * This event also collects all dots in the array $this->dots.
	 *
	 * @see Plugin::RenderItemAsHtml()
	 */
	function RenderItemAsHtml( & $params )
	{
		if( empty( $params['Item'] ) )
		{	// This plugin can works only with items:
			return false;
		}

		$params['data'] = $this->render_infodot_captions( 'itm_'.$params['Item']->ID, $params['data'] );

		return true;
	}


	/**
	 * Do the same as for HTML.
	 *
	 * @see RenderItemAsHtml()
	 */
	function RenderItemAsXml( & $params )
	{
		$this->RenderItemAsHtml( $params );
	}


	/**
	 * Perform rendering of Message content
	 *
	 * NOTE: Use default coll settings of comments as messages settings
	 *
	 * @see Plugin::RenderMessageAsHtml()
	 */
	function RenderMessageAsHtml( & $params )
	{
		// This plugin cannot works with messages:
		return true;
	}


	/**
	 * Perform rendering of Email content
	 *
	 * NOTE: Use default coll settings of comments as messages settings
	 *
	 * @see Plugin::RenderEmailAsHtml()
	 */
	function RenderEmailAsHtml( & $params )
	{
		// This plugin cannot works with emails:
		return true;
	}


	/**
	 * Render comments if required
	 *
	 * Renders code:
	 * from [infodot:1234:40:60:20ex]html text[enddot]
	 * to <div class="infodots_info" id="infodot_1234_1" data-xy="40:60" style="width:20ex:>html text</div>
	 * This html <div> is hidden by inline CSS style="display: none;". 
	 * This plugin event is called only once to render content.
	 * This only generates the content divs. The red dots themselves are added later to the evo_image_block divs, in the "RenderCommentAttachment" event.
	 * 
	 * This event also collects all dots in the array $this->dots.
	 *
	 * @see Plugin::FilterCommentContent()
	 */
	function FilterCommentContent( & $params )
	{
		$Comment = & $params['Comment'];

		if( in_array( $this->code, $Comment->get_renderers_validated() ) )
		{	// If apply_comment_rendering is set to render:
			$params['data'] = $this->render_infodot_captions( 'cmt_'.$Comment->ID, $params['data'] );
		}
	}


	/**
	 * Render infodots from like [infodot:1234:40:60:20ex]html text[enddot]
	 *    to <div class="infodots_info" id="infodot_1234_1" data-xy="40:60" style="display:none">html text</div>
	 *
	 * @param string Object ID: for example, 'itm_123', 'cmt_456'.
	 * @param string Source content
	 * @return string Rendered content
	 */
	function render_infodot_captions( $object_ID, $content )
	{
		$this->dot_numbers = NULL;
		$this->object_ID = $object_ID;

		$content = replace_outside_code_tags( '#((<br />|<p>)\r?\n?)?\[infodot:(\d+):(-?\d+[pxecm%]*):(-?\d+[pxecm%]*)(:\d+[pxecm%]*)?\](.+?)\[enddot\](\r?\n?(<br />|</p>))?#is',
				array( $this, 'load_infodot_from_source' ),
				$content, 'replace_content_callback' );

		$this->loaded_objects[ $this->object_ID ] = 1;

		return $content;
	}


	/**
	 * Callback function to load a dot from NOT rendered content
	 *
	 * @param array Matches
	 * @param boolean TRUE is used only to load dot without returning of tooltip template
	 * @return string Empty string to don't display the dot template in content, It is printed out before image tag
	 */
	function load_infodot_from_source( $matches, $only_load_dot = false )
	{
		$link_ID = intval( $matches[3] );

		if( empty( $link_ID ) || empty( $matches ) || empty( $this->object_ID ) )
		{	// Skip this incorrect match
			return;
		}

		$LinkCache = & get_LinkCache();
		$Link = & $LinkCache->get_by_ID( $link_ID, false, false );
		if( ! $Link )
		{	// Inform about invalid Link ID
			return '<div style="color:#F00"><b>'.T_('Invalid Link ID').' - '.$matches[0].'</b></div>';
		}

		if( $this->dot_numbers === NULL )
		{	// Init dot numbers array first time
			$this->dot_numbers = array();
		}

		if( ! isset( $this->dot_numbers[ $link_ID ] ) )
		{	// Start to calculate number of the dots for current Link object
			$this->dot_numbers[ $link_ID ] = 1;
		}

		if( ! isset( $this->loaded_objects[ $this->object_ID ] ) )
		{	// Load dots only once
			if( $this->dots === NULL )
			{	// Init dots array first time
				$this->dots = array();
			}

			if( ! isset( $this->dots[ $link_ID ] ) )
			{	// Init sub array for each Link
				$this->dots[ $link_ID ] = array();
			}

			// Add dot
			$this->dots[ $link_ID ][] = array(
					'x' => $matches[4].( strlen( intval( $matches[4] ) ) == strlen( $matches[4] ) ? 'px' : '' ), // Left
					'y' => $matches[5].( strlen( intval( $matches[5] ) ) == strlen( $matches[5] ) ? 'px' : '' ), // Top
				);
		}

		if( $only_load_dot )
		{	// Exit here to don't execute a code below
			return;
		}

		// We inline style="display:none" to make sure this doesn't display on screen BEFORE the CSS files are defer-loaded:
		$tooltip_styles = array( 'display:none' );

		$dot_num = $this->dot_numbers[ $link_ID ];
		if( ! empty( $matches[6] ) )
		{	// Set css style for width:
			$tooltip_width = substr( $matches[6], 1 );
			$tooltip_width = ( strlen( intval( $tooltip_width ) ) == strlen( $tooltip_width ) ? $tooltip_width.'px' : $tooltip_width );
			$tooltip_styles[] = 'width:'.$tooltip_width;
		}
		$dot_xy = ' data-xy="'.$this->dots[ $link_ID ][ $dot_num - 1 ]['x'].':'.$this->dots[ $link_ID ][ $dot_num - 1 ]['y'].'"';

		$this->dot_numbers[ $link_ID ]++;

		// Print this element that will be used for tooltip of the dot
		return '<div class="infodots_info" id="infodot_'.$link_ID.'_'.$dot_num.'"'.$dot_xy
				.' style="'.implode( ';', $tooltip_styles ).'">' 
				.balance_tags( $matches[7] )
			.'</div>'."\n";
	}


	/**
	 * Callback function to load a dot from the rendered content
	 *
	 * @param array Matches
	 */
	function load_infodot_from_rendered_content( $matches )
	{
		// Load a dot from the rendered content
		$this->load_infodot_from_source( array(
				0 => $matches[0],
				3 => $matches[1],
				4 => $matches[4],
				5 => $matches[5]
			), true );
	}


	/**
	 * Render the dots before <img> tag
	 *
	 * Renders code:
	 * from the before collected array $this->dots
	 * OR from the prerendered content <div class="infodots_info" id="infodot_1234_1" data-xy="40:60">html text</div>
	 * to <div class="infodots_dot" rel="infodot_1234_1" style="left:40px;top:60px"></div>
	 *
	 * @param array Associative array of parameters. $params['File'] - attachment, $params['data'] - output
	 * @param string Content of the Item/Comment
	 */
	function render_infodots( & $params, $content )
	{
		if( empty( $params['File'] ) || empty( $params['Link'] ) )
		{	// Check input data
			return;
		}

		$File = $params['File'];
		$Link = $params['Link'];

		if( ! $File->is_image() )
		{	// This plugin works only with image files
			return;
		}

		if( ( $LinkOwner = & $Link->get_LinkOwner() ) === false || ( $Collection = $Blog = & $LinkOwner->get_Blog() ) === false )
		{	// Couldn't get Blog object
			return;
		}

		global $thumbnail_sizes;
		$thumbnail_width = isset( $thumbnail_sizes[ $params['image_size'] ] ) ? $thumbnail_sizes[ $params['image_size'] ][1] : 0;

		if( $File->get_image_size( 'width' ) < $this->get_coll_setting( 'coll_min_width', $Blog ) ||
		    $thumbnail_width < $this->get_coll_setting( 'coll_min_width', $Blog ) )
		{	// Don't draw a dot on image if width is less than setting value
			return;
		}

		if( ! isset( $this->loaded_objects[ $this->object_ID ] ) )
		{	// Load the info dots if they were not loaded before:
			replace_outside_code_tags( '#<div class="infodots_info" id="infodot_(\d+)_(\d+)" (data-)?xy="(-?\d+[pxecm%]*):(-?\d+[pxecm%]*)"[^>]*>(.+?)</div>#is', 
				array( $this, 'load_infodot_from_rendered_content' ), $content, 'replace_content_callback' );
			$this->loaded_objects[ $this->object_ID ] = 1;
		}

		if( empty( $this->dots[ $Link->ID ] ) )
		{	// No dots for this Link
			return;
		}

		$before_image = '<div class="infodots_image">'."\n";
		foreach( $this->dots[ $Link->ID ] as $d => $dot )
		{	// Init html element for each dot
			$before_image .= '<div class="infodots_dot" rel="infodot_'.$Link->ID.'_'.( $d + 1 ).'" style="left:'.$dot['x'].';top:'.$dot['y'].'"></div>'."\n";
		}

		// Append info dots html to current image tag
		$params['before_image'] = $params['before_image'].$before_image;
		$params['after_image'] = '</div>'.$params['after_image'];
	}


	/**
	 * Event handler: Called prepare params before rendering attachments of item contents.
	 *
	 * This will render the red dots in the same <div> as the image, which is important for using relative positioning based on the image coordinates.
	 *
	 * Renders the red dots before each image by getting them from array $this->dots if it was initialized during RenderItemAsHtml().
	 * If RenderItemAsHtml() was not called (which happens if content was already pre-rendered) then the array $this->dots is built by preg_match from prerendered content 
	 * from hidden div <div class="infodots_info" id="infodot_1234_1" data-xy="40:60" style="width:20ex:>html text</div>
	 *
	 * The dots are rendered as <div class="infodots_image"> <div class="infodots_dot" rel="infodot_1234_1" style="left:40px;top:60px"></div> </div> before attached image.
	 *
	 * @param array Associative array of parameters. $params['File'] - attachment, $params['data'] - output
	 * @param boolean TRUE - when render in comments
	 * @return boolean true if plugin rendered this attachment
	 */
	function PrepareForRenderItemAttachment( & $params )
	{
		if( empty( $params['Item'] ) || empty( $params['Link'] ) )
		{	// Wrong input data, Exit here:
			return false;
		}

		$Item = & $params['Item'];

		$this->dot_numbers = NULL;
		$this->object_ID = 'itm_'.$Item->ID;

		// Render dots:
		$this->render_infodots( $params, $Item->get_prerendered_content( 'htmlbody' ) );

		return true;
	}


	/**
	 * Event handler: Called prepare params before rendering attachments of comment content.
	 *
	 * See sister function RenderItemAttachment() above for more details.
	 *
	 * @param array Associative array of parameters. $params['File'] - attachment, $params['data'] - output
	 * @return boolean true if plugin rendered this attachment
	 */
	function PrepareForRenderCommentAttachment( & $params )
	{
		if( empty( $params['Comment'] ) || empty( $params['Link'] ) )
		{	// Wrong input data, Exit here:
			return false;
		}

		$Comment = & $params['Comment'];

		$this->dot_numbers = NULL;
		$this->object_ID = 'cmt_'.$Comment->ID;

		// Render dots:
		$this->render_infodots( $params, $Comment->get_prerendered_content( 'htmlbody' ) );

		return true;
	}
}

?>
