<?php

namespace okapi\services\caches\map;

use Exception;
use okapi\Okapi;
use okapi\Settings;
use okapi\Cache;
use okapi\Db;
use okapi\FileCache; // WRTODO

interface TileRenderer
{
	/**
	 * Return the unique hash of the tile being rendered. This method will be
	 * called only once, prior the render method. You may (but don't have to)
	 * throw an Exception on every subsequent call.
	 */
	public function get_unique_hash();
	
	/** Get the content type of the data returned by the render method. */
	public function get_content_type();
	
	/**
	 * Render the image. This function will be called only once, after calling
	 * get_unique_hash method.
	 */
	public function render();
}

class DefaultTileRenderer implements TileRenderer
{
	/**
	 * Changing this will affect all generated hashes. You should increment it
	 * whenever you alter anything in the drawing algorithm.
	 */
	private static $VERSION = 56;
	
	/**
	 * Should be always true. You may temporarily set it to false, when you're
	 * testing/debugging a new set of static icons.
	 */
	private static $USE_STATIC_IMAGE_CACHE = true;
	
	/**
	 * Should be always true. You may temporarily set it to false, when you're
	 * testing/debugging a new captions renderer.
	 */
	private static $USE_CAPTIONS_CACHE = true;
	
	private $zoom;
	private $rows_ref;
	private $im;
	
	/**
	 * Takes the zoom level and the list of geocache descriptions. Note, that
	 * $rows_ref can be altered after calling render. If you don't want it to,
	 * you should pass a deep copy.
	 */
	public function __construct($zoom, &$rows_ref)
	{
		$this->zoom = $zoom;
		$this->rows_ref = &$rows_ref;
	}
	
	public function get_unique_hash()
	{
		return md5(json_encode(array(
			"DefaultTileRenderer",
			self::$VERSION,
			$this->zoom,
			$this->rows_ref
		)));
	}
	
	public function get_content_type()
	{
		return "image/png";
	}
	
	public function render()
	{
		# Preprocess the rows.
		
		if ($this->zoom >= 5)
			$this->decide_which_get_captions();
		
		# Make a background.
		
		$this->im = imagecreatetruecolor(256, 256);
		imagealphablending($this->im, false);
		if ($this->zoom >= 13) $opacity = 15;
		elseif ($this->zoom <= 12) $opacity = max(0, $this->zoom * 2 - 14);
		$transparent = imagecolorallocatealpha($this->im, 0, 0, 0, 127 - $opacity);
		imagefilledrectangle($this->im, 0, 0, 256, 256, $transparent);
		imagealphablending($this->im, true);
		
		# Draw the caches.
		
		foreach ($this->rows_ref as &$row_ref)
			$this->draw_cache($row_ref);
		
		# Return the result.
		
		ob_start();
		imagesavealpha($this->im, true);
		imagepng($this->im);
		imagedestroy($this->im);
		return ob_get_clean();
	}
	
	private static function get_image($name, $opacity=1, $brightness=0,
		$contrast=0, $r=0, $g=0, $b=0)
	{
		static $locmem_cache = array();

		# Check locmem cache.
		
		$key = "$name/$opacity/$brightness/$contrast/$r/$g/$b";
		if (!isset($locmem_cache[$key]))
		{
			# Miss. Check default cache.  WRTODO: upgrade to normal Cache?
			
			try
			{
				$cache_key = "tilesrc/".Okapi::$revision."/".self::$VERSION."/".$key;
				$gd2_path = self::$USE_STATIC_IMAGE_CACHE
					? FileCache::get_file_path($cache_key) : null;
				if ($gd2_path === null)
					throw new Exception("Not in cache");
				# File cache hit. GD2 files are much faster to read than PNGs.
				# This can throw an Exception (see bug#160).
				$locmem_cache[$key] = imagecreatefromgd2($gd2_path);
			}
			catch (Exception $e)
			{
				# Miss again (or error decoding). Read the image from PNG.
				
				$locmem_cache[$key] = imagecreatefrompng($GLOBALS['rootpath']."okapi/static/tilemap/$name.png");
				
				# Apply all wanted effects.
				
				if ($opacity != 1)
					self::change_opacity($locmem_cache[$key], $opacity);
				if ($contrast != 0)
					imagefilter($locmem_cache[$key], IMG_FILTER_CONTRAST, $contrast);
				if ($brightness != 0)
					imagefilter($locmem_cache[$key], IMG_FILTER_BRIGHTNESS, $brightness);
				if (($r != 0) || ($g != 0) || ($b != 0))
				{
					imagefilter($locmem_cache[$key], IMG_FILTER_GRAYSCALE); 
					imagefilter($locmem_cache[$key], IMG_FILTER_COLORIZE, $r, $g, $b);
				}
				
				# Cache the result.
				
				ob_start();
				imagegd2($locmem_cache[$key]);
				$gd2 = ob_get_clean();
				FileCache::set($cache_key, $gd2);
			}
		}
		return $locmem_cache[$key];
	}

	/**
	 * Extremely slow! Remember to cache the result!
	 */
	private static function change_opacity($im, $ratio)
	{
		imagealphablending($im, false);
		
		$w = imagesx($im);
		$h = imagesy($im);

		for($x = 0; $x < $w; $x++)
		{
			for($y = 0; $y < $h; $y++)
			{
				$color = imagecolorat($im, $x, $y);
				$new_color = ((max(0, floor(127 - ((127 - (($color >> 24) & 0x7f)) * $ratio))) & 0x7f) << 24) | ($color & 0x80ffffff);
				imagesetpixel($im, $x, $y, $new_color);
			}
		}
		
		imagealphablending($im, true);
	}

	private function draw_cache(&$cache_struct)
	{
		$capt = ($cache_struct[6] & TileTree::$FLAG_DRAW_CAPTION);
		if (($this->zoom <= 8) && (!$capt))
			$this->draw_cache_tiny($cache_struct);
		elseif (($this->zoom <= 12) && (!$capt))
			$this->draw_cache_medium($cache_struct);
		else
			$this->draw_cache_large($cache_struct);

		# Put caption (this flag is set only when there is plenty of space around).
		
		if ($cache_struct[6] & TileTree::$FLAG_DRAW_CAPTION)
		{
			$caption = $this->get_caption($cache_struct[0]);
			imagecopy($this->im, $caption, $cache_struct[1] - 32, $cache_struct[2] + 6, 0, 0, 64, 26);
		}

	}


	private function draw_cache_large(&$cache_struct)
	{
		list($cache_id, $px, $py, $status, $type, $rating, $flags, $count) = $cache_struct;
		
		$found = $flags & TileTree::$FLAG_FOUND;
		$own = $flags & TileTree::$FLAG_OWN;
		$new = $flags & TileTree::$FLAG_NEW;
		
		# Prepare vars.
		
		if ($own) {
			$key = 'large_outer_own';
			$a = 1; $br = 0; $c = 0;
			$r = 0; $g = 0; $b = 0;
		} elseif ($found) {
			$key = 'large_outer_found';
			$a = ($flags & TileTree::$FLAG_DRAW_CAPTION) ? .7 : .35;
			$br = 40; $c = 20;
			//$a = 0.5; $br = 0; $c = 0;
			$r = 0; $g = 0; $b = 0;
		} elseif ($new) {
			$key = 'large_outer_new';
			$a = 1; $br = 0; $c = 0;
			$r = 0; $g = 0; $b = 0;
		} else {
			$key = 'large_outer';
			$a = 1; $br = 0; $c = 0;
			$r = 0; $g = 0; $b = 0;
		}
		
		# Put the outer marker (indicates the found/new/own status).
		
		$outer_marker = self::get_image($key, $a);
		
		$width = 40;
		$height = 32;
		$center_x = 12;
		$center_y = 26;
		$markercenter_x = 12;
		$markercenter_y = 12;
		
		if ($count > 1)
			imagecopy($this->im, $outer_marker, $px - $center_x + 3, $py - $center_y - 2, 0, 0, $width, $height);
		imagecopy($this->im, $outer_marker, $px - $center_x, $py - $center_y, 0, 0, $width, $height);
		
		# Put the inner marker (indicates the type).
		
		$inner_marker = self::get_image("large_inner_".self::get_type_suffix(
			$type, true), $a, $br, $c, $r, $g, $b);
		imagecopy($this->im, $inner_marker, $px - 7, $py - 22, 0, 0, 16, 16);
		
		# If the cache is unavailable, mark it with X. 

		if (($status != 1) && ($count == 1))
		{
			$icon = self::get_image(($status == 2) ? "status_unavailable"
				: "status_archived", $a);
			imagecopy($this->im, $icon, $px - 1, $py - $center_y - 4, 0, 0, 16, 16);
		}
		
		# Put the rating smile. :)
		
		if ($status == 1)
		{
			if ($rating >= 4.2)
			{
				if ($flags & TileTree::$FLAG_STAR) {
					$icon = self::get_image("rating_grin", $a, $br, $c, $r, $g, $b);
					imagecopy($this->im, $icon, $px - 7 - 6, $py - $center_y - 8, 0, 0, 16, 16);
					$icon = self::get_image("rating_star", $a, $br, $c, $r, $g, $b);
					imagecopy($this->im, $icon, $px - 7 + 6, $py - $center_y - 8, 0, 0, 16, 16);
				} else {
					$icon = self::get_image("rating_grin", $a, $br, $c, $r, $g, $b);
					imagecopy($this->im, $icon, $px - 7, $py - $center_y - 8, 0, 0, 16, 16);
				}
			}
			# This was commented out because users complained about too many smiles ;)
			// elseif ($rating >= 3.6) {
			//   $icon = self::get_image("rating_smile", $a, $br, $c, $r, $g, $b);
			//   imagecopy($this->im, $icon, $px - 7, $py - $center_y - 8, 0, 0, 16, 16);
			// }
		}
		
		# Mark found caches with V.
		
		if ($found)
		{
			$icon = self::get_image("found", 0.7*$a, $br, $c, $r, $g, $b);
			imagecopy($this->im, $icon, $px - 2, $py - $center_y - 3, 0, 0, 16, 16);
		}
		
	}

	/**
	 * Split lines so that they fit inside the specified width.
	 */
	private static function wordwrap($font, $size, $maxWidth, $text)
	{
		$words = explode(" ", $text);
		$lines = array();
		$line = "";
		$nextBonus = "";
		for ($i=0; ($i<count($words)) || (mb_strlen($nextBonus)>0); $i++) {
			$word = isset($words[$i])?$words[$i]:"";
			if (mb_strlen($nextBonus) > 0)
				$word = $nextBonus." ".$word;
			$nextBonus = "";
			while (true) {
				$bbox = imagettfbbox($size, 0, $font, $line.$word);
				$width = $bbox[2]-$bbox[0];
				if ($width <= $maxWidth) {
					$line .= $word." ";
					continue 2;
				}
				if (mb_strlen($line) > 0) {
					$lines[] = trim($line);
					$line = "";
					continue;
				}
				$nextBonus = $word[mb_strlen($word)-1].$nextBonus;
				$word = mb_substr($word, 0, mb_strlen($word)-1);
				continue;
			}
		}
		if (mb_strlen($line) > 0)
			$lines[] = trim($line);
		return implode("\n", $lines);
	}
	
	/**
	 * Return 64x26 bitmap with the caption (name) for the given geocache.
	 */
	private function get_caption($cache_id)
	{
		# Check cache.
		
		$cache_key = "tilecaption/".self::$VERSION."/".$cache_id;
		$gd2 = self::$USE_CAPTIONS_CACHE ? Cache::get($cache_key) : null;
		if ($gd2 === null)
		{
			# We'll work with 16x bigger image to get smoother interpolation.
			
			$im = imagecreatetruecolor(64*4, 26*4);
			imagealphablending($im, false); 
			$transparent = imagecolorallocatealpha($im, 255, 255, 255, 127);
			imagefilledrectangle($im, 0, 0, 64*4, 26*4, $transparent);
			imagealphablending($im, true);
			
			# Get the name of the cache.
			
			$name = Db::select_value("
				select name
				from caches
				where cache_id = '".mysql_real_escape_string($cache_id)."'
			");
			
			# Split the name into a couple of lines.
			
			//$font = $GLOBALS['rootpath'].'util.sec/bt.ttf';
			$font = $GLOBALS['rootpath'].'okapi/static/tilemap/tahoma.ttf';
			$size = 25;
			$lines = explode("\n", self::wordwrap($font, $size, 64*4 - 6*2, $name));
			
			# For each line, compute its (x, y) so that the text is centered.
			
			$y = 0;
			$positions = array();
			foreach ($lines as $line)
			{
				$bbox = imagettfbbox($size, 0, $font, $line);
				$width = $bbox[2]-$bbox[0];
				$x = 128 - ($width >> 1);
				$positions[] = array($x, $y);
				$y += 36;
			}
			$drawer = function($x, $y, $color) use (&$lines, &$positions, &$im, &$size, &$font)
			{
				$len = count($lines);
				for ($i=0; $i<$len; $i++)
				{
					$line = $lines[$i];
					list($offset_x, $offset_y) = $positions[$i];
					imagettftext($im, $size, 0, $offset_x + $x, $offset_y + $y, $color, $font, $line);
				}
			};
			
			# Draw an outline.
			
			$outline_color = imagecolorallocatealpha($im, 255, 255, 255, 80);
			for ($x=0; $x<=12; $x+=3)
				for ($y=$size-3; $y<=$size+9; $y+=3)
					$drawer($x, $y, $outline_color);
			
			# Add a slight shadow effect (on top of the outline).
			
			$drawer(9, $size + 3, imagecolorallocatealpha($im, 0, 0, 0, 110));
			
			# Draw the caption.
			
			$drawer(6, $size + 3, imagecolorallocatealpha($im, 150, 0, 0, 40));

			# Resample.
			
			imagealphablending($im, false); 
			$small = imagecreatetruecolor(64, 26);
			imagealphablending($small, false);
			imagecopyresampled($small, $im, 0, 0, 0, 0, 64, 26, 64*4, 26*4);
			
			# Cache it!
			
			ob_start();
			imagegd2($small);
			$gd2 = ob_get_clean();
			Cache::set_scored($cache_key, $gd2);
		}
		
		return imagecreatefromstring($gd2);
	}

	private function draw_cache_medium(&$cache_struct)
	{
		list($cache_id, $px, $py, $status, $type, $rating, $flags, $count) = $cache_struct;
		
		$found = $flags & TileTree::$FLAG_FOUND;
		$own = $flags & TileTree::$FLAG_OWN;
		$new = $flags & TileTree::$FLAG_NEW;
		if ($found && (!($flags & TileTree::$FLAG_DRAW_CAPTION)))
			$a = .35;
		else
			$a = 1;
		
		# Put the marker (indicates the type).
		
		$marker = self::get_image("medium_".self::get_type_suffix($type, false), $a);
		$width = 14;
		$height = 14;
		$center_x = 7;
		$center_y = 8;
		$markercenter_x = 7;
		$markercenter_y = 8;

		if ($count > 1)
		{
			imagecopy($this->im, $marker, $px - $center_x + 3, $py - $center_y - 2, 0, 0, $width, $height);
			imagecopy($this->im, $marker, $px - $center_x, $py - $center_y, 0, 0, $width, $height);
		}
		elseif ($status == 1)  # don't put the marker for unavailable caches (X only)
		{
			imagecopy($this->im, $marker, $px - $center_x, $py - $center_y, 0, 0, $width, $height);
		}

		# If the cache is unavailable, mark it with X. 

		if (($status != 1) && ($count == 1))
		{
			$icon = self::get_image(($status == 2) ? "status_unavailable"
				: "status_archived");
			imagecopy($this->im, $icon, $px - ($center_x - $markercenter_x) - 6,
				$py - ($center_y - $markercenter_y) - 8, 0, 0, 16, 16);
		}
		
		# Put small versions of rating icons.
		
		if ($status == 1)
		{
			if ($rating >= 4.2)
			{
				if ($flags & TileTree::$FLAG_STAR) {
					$icon = self::get_image("rating_grin_small", max(0.6, $a));
					imagecopy($this->im, $icon, $px - 5, $py - $center_y - 1, 0, 0, 6, 6);
					$icon = self::get_image("rating_star_small", max(0.6, $a));
					imagecopy($this->im, $icon, $px - 2, $py - $center_y - 3, 0, 0, 10, 10);
				} else {
					$icon = self::get_image("rating_grin_small", max(0.6, $a));
					imagecopy($this->im, $icon, $px - 3, $py - $center_y - 1, 0, 0, 6, 6);
				}
			}
		}
		
		if ($own)
		{
			# Mark own caches with additional overlay.
			
			$overlay = self::get_image("medium_overlay_own");
			imagecopy($this->im, $overlay, $px - $center_x, $py - $center_y, 0, 0, $width, $height);
		}
		elseif ($found)
		{
			# Mark found caches with V.
			
			$icon = self::get_image("found", 0.7*$a);
			imagecopy($this->im, $icon, $px - ($center_x - $markercenter_x) - 7,
				$py - ($center_y - $markercenter_y) - 9, 0, 0, 16, 16);
		}
		elseif ($new)
		{
			# Mark new caches with additional overlay.
			
			$overlay = self::get_image("medium_overlay_new");
			imagecopy($this->im, $overlay, $px - $center_x, $py - $center_y, 0, 0, $width, $height);
		}
	}

	private static function get_type_suffix($type, $extended_set)
	{
		switch ($type) {
			case 2: return 'traditional';
			case 3: return 'multi';
			case 6: return 'event';
			case 7: return 'quiz';
			case 4: return 'virtual';
			case 1: return 'unknown';
		}
		if ($extended_set)
		{
			switch ($type) {
				case 10: return 'own';
				case 8: return 'moving';
				case 5: return 'webcam';
			}
		}
		return 'other';
	}
	
	private function draw_cache_tiny(&$cache_struct)
	{
		list($cache_id, $px, $py, $status, $type, $rating, $flags, $count) = $cache_struct;
		
		$found = $flags & TileTree::$FLAG_FOUND;
		$own = $flags & TileTree::$FLAG_OWN;
		$new = $flags & TileTree::$FLAG_NEW;
		
		$marker = self::get_image("tiny_".self::get_type_suffix($type, false));
		$width = 10;
		$height = 10;
		$center_x = 5;
		$center_y = 6;
		$markercenter_x = 5;
		$markercenter_y = 6;
		
		# Put the marker. If cache covers more caches, then put two markers instead of one.
		
		if ($count > 1)
		{
			imagecopy($this->im, $marker, $px - $center_x + 3, $py - $center_y - 2, 0, 0, $width, $height);
			imagecopy($this->im, $marker, $px - $center_x, $py - $center_y, 0, 0, $width, $height);
		}
		elseif ($status == 1)
		{
			imagecopy($this->im, $marker, $px - $center_x, $py - $center_y, 0, 0, $width, $height);
		}

		# If the cache is unavailable, mark it with X. 

		if (($status != 1) && ($count == 1))
		{
			$icon = self::get_image(($status == 2) ? "status_unavailable"
				: "status_archived");
			imagecopy($this->im, $icon, $px - ($center_x - $markercenter_x) - 6,
				$py - ($center_y - $markercenter_y) - 8, 0, 0, 16, 16);
		}
	}

	/**
	 * Examine all rows and decide which of them will get captions.
	 * Mark selected rows with TileTree::$FLAG_DRAW_CAPTION.
	 *
	 * Note: Calling this will alter the rows!
	 */
	private function decide_which_get_captions()
	{
		# We will split the tile (along with its margins) into 12x12 squares.
		# A single geocache placed in square (x, y) gets the caption only
		# when there are no other geocaches in any of the adjacent squares.
		# This is efficient and yields acceptable results.
		
		$matrix = array();
		for ($i=0; $i<12; $i++)
			$matrix[] = array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
		
		foreach ($this->rows_ref as &$row_ref)
		{
			$mx = ($row_ref[1] + 64) >> 5;
			$my = ($row_ref[2] + 64) >> 5;
			if (($mx >= 12) || ($my >= 12)) continue;
			if (($matrix[$mx][$my] === 0) && ($row_ref[7] == 1))  # 7 is count
				$matrix[$mx][$my] = $row_ref[0];  # 0 is cache_id
			else
				$matrix[$mx][$my] = -1;
		}
		$selected_cache_ids = array();
		for ($mx=1; $mx<11; $mx++)
		{
			for ($my=1; $my<11; $my++)
			{
				if ($matrix[$mx][$my] > 0)  # cache_id
				{
					# Check all adjacent squares.
					
					if (   ($matrix[$mx-1][$my-1] === 0)
						&& ($matrix[$mx-1][$my  ] === 0)
						&& ($matrix[$mx-1][$my+1] === 0)
						&& ($matrix[$mx  ][$my-1] === 0)
						&& ($matrix[$mx  ][$my+1] === 0)
						&& ($matrix[$mx+1][$my-1] === 0)
						&& ($matrix[$mx+1][$my  ] === 0)
						&& ($matrix[$mx+1][$my+1] === 0)
					)
						$selected_cache_ids[] = $matrix[$mx][$my];
				}
			}
		}
		
		foreach ($this->rows_ref as &$row_ref)
			if (in_array($row_ref[0], $selected_cache_ids))
				$row_ref[6] |= TileTree::$FLAG_DRAW_CAPTION;
	}

}