<?php

namespace okapi\services\caches\map;

use Exception;
use okapi\Okapi;
use okapi\Settings;
use okapi\Cache;
use okapi\Db;
use okapi\OkapiRequest;
use okapi\OkapiHttpResponse;
use okapi\ParamMissing;
use okapi\InvalidParam;
use okapi\BadRequest;
use okapi\DoesNotExist;
use okapi\OkapiInternalRequest;
use okapi\OkapiInternalConsumer;
use okapi\OkapiServiceRunner;
use okapi\OkapiLock;


class TileTree
{
	# Static flags (stored in the database).
	public static $FLAG_STAR = 0x01;
	public static $FLAG_HAS_TRACKABLES = 0x02;
	public static $FLAG_NOT_YET_FOUND = 0x04;

	# Dynamic flags (added at runtime).
	public static $FLAG_FOUND = 0x0100;
	public static $FLAG_OWN = 0x0200;
	public static $FLAG_NEW = 0x0400;
	public static $FLAG_DRAW_CAPTION = 0x0800;

	/**
	 * Return null if not computed, 1 if computed and empty, 2 if computed and not empty.
	 */
	public static function get_tile_status($zoom, $x, $y)
	{
		return Db::select_value("
			select status
			from okapi_tile_status
			where
				z = '".mysql_real_escape_string($zoom)."'
				and x = '".mysql_real_escape_string($x)."'
				and y = '".mysql_real_escape_string($y)."'
		");
	}

	/**
	 * Return MySQL's result set iterator over all caches which are present
	 * in the given result set AND in the given tile.
	 *
	 * Each row is an array of the following format:
	 * list(cache_id, $pixel_x, $pixel_y, status, type, rating, flags, count).
	 *
	 * Note that $pixels can also be negative or >=256 (up to a margin of 32px).
	 * Count is the number of other caches "eclipsed" by this geocache (such
	 * eclipsed geocaches are not included in the result).
	 */
	public static function query_fast($zoom, $x, $y, $set_id)
	{
		# First, we check if the cache-set for this tile was already computed
		# (and if it was, was it empty).

		$status = self::get_tile_status($zoom, $x, $y);
		if ($status === null)  # Not yet computed.
		{
			# Note, that computing the tile does not involve taking any
			# search parameters.

			$status = self::compute_tile($zoom, $x, $y);
		}

		if ($status === 1)  # Computed and empty.
		{
			# This tile was already computed and it is empty.
			return null;
		}

		# If we got here, then the tile is computed and not empty (status 2).

		$tile_upper_x = $x << 8;
		$tile_leftmost_y = $y << 8;

		$zoom_escaped = "'".mysql_real_escape_string($zoom)."'";
		$tile_upper_x_escaped = "'".mysql_real_escape_string($tile_upper_x)."'";
		$tile_leftmost_y_escaped = "'".mysql_real_escape_string($tile_leftmost_y)."'";
		return Db::query("
			select
				otc.cache_id,
				cast(otc.z21x >> (21 - $zoom_escaped) as signed) - $tile_upper_x_escaped as px,
				cast(otc.z21y >> (21 - $zoom_escaped) as signed) - $tile_leftmost_y_escaped as py,
				otc.status, otc.type, otc.rating, otc.flags, count(*)
			from
				okapi_tile_caches otc,
				okapi_search_results osr
			where
				z = $zoom_escaped
				and x = '".mysql_real_escape_string($x)."'
				and y = '".mysql_real_escape_string($y)."'
				and otc.cache_id = osr.cache_id
				and osr.set_id = '".mysql_real_escape_string($set_id)."'
			group by
				z21x >> (3 + (21 - $zoom_escaped)),
				z21y >> (3 + (21 - $zoom_escaped))
			order by
				z21y >> (3 + (21 - $zoom_escaped)),
				z21x >> (3 + (21 - $zoom_escaped))
		");
	}

	/**
	 * Precache the ($zoom, $x, $y) slot in the okapi_tile_caches table.
	 */
	public static function compute_tile($zoom, $x, $y)
	{
		$time_started = microtime(true);

		# Note, that multiple threads may try to compute tiles simulatanously.
		# For low-level tiles, this can be expensive. WRTODO: Think of some
		# appropriate locks.

		$status = self::get_tile_status($zoom, $x, $y);
		if ($status !== null)
			return $status;

		if ($zoom === 0)
		{
			# When computing zoom zero, we don't have a parent to speed up
			# the computation. We need to use the caches table. Note, that
			# zoom level 0 contains *entire world*, so we don't have to use
			# any WHERE condition in the following query.

			# This can be done a little faster (without the use of internal requests),
			# but there is *no need* to - this query is run seldom and is cached.

			$params = array();
			$params['status'] = "Available|Temporarily unavailable|Archived";  # we want them all
			$params['limit'] = "10000000";  # no limit

			$internal_request = new OkapiInternalRequest(new OkapiInternalConsumer(), null, $params);
			$internal_request->skip_limits = true;
			$response = OkapiServiceRunner::call("services/caches/search/all", $internal_request);
			$cache_codes = $response['results'];

			$internal_request = new OkapiInternalRequest(new OkapiInternalConsumer(), null, array(
				'cache_codes' => implode('|', $cache_codes),
				'fields' => 'internal_id|code|name|location|type|status|rating|recommendations|founds|trackables_count'
			));
			$internal_request->skip_limits = true;
			$caches = OkapiServiceRunner::call("services/caches/geocaches", $internal_request);

			foreach ($caches as $cache)
			{
				$row = self::generate_short_row($cache);
				Db::execute("
					replace into okapi_tile_caches (
						z, x, y, cache_id, z21x, z21y, status, type, rating, flags
					) values (
						0, 0, 0,
						'".mysql_real_escape_string($row[0])."',
						'".mysql_real_escape_string($row[1])."',
						'".mysql_real_escape_string($row[2])."',
						'".mysql_real_escape_string($row[3])."',
						'".mysql_real_escape_string($row[4])."',
						".(($row[5] === null) ? "null" : "'".mysql_real_escape_string($row[5])."'").",
						'".mysql_real_escape_string($row[6])."'
					);
				");
			}
			$status = 2;
		}
		else
		{
			# We will use the parent tile to compute the contents of this tile.

			$parent_zoom = $zoom - 1;
			$parent_x = $x >> 1;
			$parent_y = $y >> 1;

			$status = self::get_tile_status($parent_zoom, $parent_x, $parent_y);
			if ($status === null)  # Not computed.
			{
				$time_started = microtime(true);
				$status = self::compute_tile($parent_zoom, $parent_x, $parent_y);
			}

			if ($status === 1)  # Computed and empty.
			{
				# No need to check.
			}
			else  # Computed, not empty.
			{
				$scale = 8 + 21 - $zoom;
				$parentcenter_z21x = (($parent_x << 1) | 1) << $scale;
				$parentcenter_z21y = (($parent_y << 1) | 1) << $scale;
				$margin = 1 << ($scale - 2);
				$left_z21x = (($parent_x << 1) << $scale) - $margin;
				$right_z21x = ((($parent_x + 1) << 1) << $scale) + $margin;
				$top_z21y = (($parent_y << 1) << $scale) - $margin;
				$bottom_z21y = ((($parent_y + 1) << 1) << $scale) + $margin;

				# Choose the right quarter.
				# |1 2|
				# |3 4|

				if ($x & 1)  # 2 or 4
					$left_z21x = $parentcenter_z21x - $margin;
				else  # 1 or 3
					$right_z21x = $parentcenter_z21x + $margin;
				if ($y & 1)  # 3 or 4
					$top_z21y = $parentcenter_z21y - $margin;
				else  # 1 or 2
					$bottom_z21y = $parentcenter_z21y + $margin;

				# Cache the result.

				Db::execute("
					replace into okapi_tile_caches (
						z, x, y, cache_id, z21x, z21y, status, type, rating, flags
					)
					select
						'".mysql_real_escape_string($zoom)."',
						'".mysql_real_escape_string($x)."',
						'".mysql_real_escape_string($y)."',
						cache_id, z21x, z21y, status, type, rating, flags
					from okapi_tile_caches
					where
						z = '".mysql_real_escape_string($parent_zoom)."'
						and x = '".mysql_real_escape_string($parent_x)."'
						and y = '".mysql_real_escape_string($parent_y)."'
						and z21x between $left_z21x and $right_z21x
						and z21y between $top_z21y and $bottom_z21y
				");
				$test = Db::select_value("
					select 1
					from okapi_tile_caches
					where
						z = '".mysql_real_escape_string($zoom)."'
						and x = '".mysql_real_escape_string($x)."'
						and y = '".mysql_real_escape_string($y)."'
					limit 1;
				");
				if ($test)
					$status = 2;
				else
					$status = 1;
			}
		}

		# Mark tile as computed.

		Db::execute("
			replace into okapi_tile_status (z, x, y, status)
			values (
				'".mysql_real_escape_string($zoom)."',
				'".mysql_real_escape_string($x)."',
				'".mysql_real_escape_string($y)."',
				'".mysql_real_escape_string($status)."'
			);
		");

		return $status;
	}

	/**
	 * Convert OKAPI's cache object to a short database row to be inserted
	 * into okapi_tile_caches table. Returns the list of the following attributes:
	 * cache_id, z21x, z21y, status, type, rating, flags (rating might be null!).
	 */
	public static function generate_short_row($cache)
	{
		list($lat, $lon) = explode("|", $cache['location']);
		list($z21x, $z21y) = self::latlon_to_z21xy($lat, $lon);
		$flags = 0;
		if (($cache['founds'] > 6) && (($cache['recommendations'] / $cache['founds']) > 0.3))
			$flags |= self::$FLAG_STAR;
		if ($cache['trackables_count'] > 0)
			$flags |= self::$FLAG_HAS_TRACKABLES;
		if ($cache['founds'] == 0)
			$flags |= self::$FLAG_NOT_YET_FOUND;
		return array($cache['internal_id'], $z21x, $z21y, Okapi::cache_status_name2id($cache['status']),
			Okapi::cache_type_name2id($cache['type']), $cache['rating'], $flags);
	}

	private static function latlon_to_z21xy($lat, $lon)
	{
		$offset = 128 << 21;
		$x = round($offset + ($offset * $lon / 180));
		$y = round($offset - $offset/pi() * log((1 + sin($lat * pi() / 180)) / (1 - sin($lat * pi() / 180))) / 2);
		return array($x, $y);
	}
}