<?php declare(strict_types=1);

/*
 * This file is part of the Rapsys PackBundle package.
 *
 * (c) Raphaël Gertz <symfony@rapsys.eu>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Rapsys\PackBundle\Util;

use Symfony\Component\Routing\RouterInterface;

/**
 * Manages map
 */
class MapUtil {
	/**
	 * The cycle tile server
	 *
	 * @see https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Tile_servers
	 */
	const cycle = 'http://a.tile.thunderforest.com/cycle/{Z}/{X}/{Y}.png';

	/**
	 * The fill color
	 */
	const fill = '#cff';

	/**
	 * The font size
	 */
	const fontSize = 20;

	/**
	 * The high fill color
	 */
	const highFill = '#c3c3f9';

	/**
	 * The high font size
	 */
	const highFontSize = 30;

	/**
	 * The high radius size
	 */
	const highRadius = 6;

	/**
	 * The high stroke color
	 */
	const highStroke = '#3333c3';

	/**
	 * The high stroke width
	 */
	const highStrokeWidth = 4;

	/**
	 * The map width
	 */
	const width = 640;

	/**
	 * The map height
	 */
	const height = 640;
	
	/**
	 * The osm tile server
	 *
	 * @see https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Tile_servers
	 */
	const osm = 'https://tile.openstreetmap.org/{Z}/{X}/{Y}.png';

	/**
	 * The radius size
	 */
	const radius = 5;

	/**
	 * The stroke color
	 */
	const stroke = '#00c3f9';

	/**
	 * The stroke width
	 */
	const strokeWidth = 2;

	/**
	 * The transport tile server
	 *
	 * @see https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Tile_servers
	 */
	const transport = 'http://a.tile.thunderforest.com/transport/{Z}/{X}/{Y}.png';

	/**
	 * The tile size
	 */
	const tz = 256;

	/**
	 * The map zoom
	 */
	const zoom = 17;

	/**
	 * Creates a new map util
	 *
	 * @param RouterInterface $router The RouterInterface instance
	 * @param SluggerUtil $slugger The SluggerUtil instance
	 */
	function __construct(protected RouterInterface $router, protected SluggerUtil $slugger, protected string $fill = self::fill, protected int $fontSize = self::fontSize, protected string $highFill = self::highFill, protected int $highFontSize = self::highFontSize, protected int $highRadius = self::highRadius, protected string $highStroke = self::highStroke, protected int $highStrokeWidth = self::highStrokeWidth, protected int $radius = self::radius, protected string $stroke = self::stroke, protected int $strokeWidth = self::strokeWidth) {
	}

	/**
	 * Get fill color
	 */
	function getFill() {
		return $this->fill;
	}

	/**
	 * Get font size
	 */
	function getFontSize() {
		return $this->fontSize;
	}

	/**
	 * Get high fill color
	 */
	function getHighFill() {
		return $this->highFill;
	}

	/**
	 * Get high font size
	 */
	function getHighFontSize() {
		return $this->highFontSize;
	}

	/**
	 * Get high radius size
	 */
	function getHighRadius() {
		return $this->highRadius;
	}

	/**
	 * Get high stroke color
	 */
	function getHighStroke() {
		return $this->highStroke;
	}

	/**
	 * Get high stroke width
	 */
	function getHighStrokeWidth() {
		return $this->highStrokeWidth;
	}

	/**
	 * Get radius size
	 */
	function getRadius() {
		return $this->radius;
	}

	/**
	 * Get stroke color
	 */
	function getStroke() {
		return $this->stroke;
	}

	/**
	 * Get stroke width
	 */
	function getStrokeWidth() {
		return $this->strokeWidth;
	}

	/**
	 * Get map data
	 *
	 * @param string $caption The caption
	 * @param int $updated The updated timestamp
	 * @param float $latitude The latitude
	 * @param float $longitude The longitude
	 * @param int $zoom The zoom
	 * @param int $width The width
	 * @param int $height The height
	 * @return array The map data
	 */
	public function getMap(string $caption, int $updated, float $latitude, float $longitude, int $zoom = self::zoom, int $width = self::width, int $height = self::height): array {
		//Set link hash
		$link = $this->slugger->hash([$updated, $latitude, $longitude, $zoom + 1, $width * 2, $height * 2]);

		//Set src hash
		$src = $this->slugger->hash([$updated, $latitude, $longitude, $zoom, $width, $height]);

		//Return array
		return [
			'caption' => $caption,
			'link' => $this->router->generate('rapsyspack_map', ['hash' => $link, 'updated' => $updated, 'latitude' => $latitude, 'longitude' => $longitude, 'zoom' => $zoom + 1, 'width' => $width * 2, 'height' => $height * 2]),
			'src' => $this->router->generate('rapsyspack_map', ['hash' => $src, 'updated' => $updated, 'latitude' => $latitude, 'longitude' => $longitude, 'zoom' => $zoom, 'width' => $width, 'height' => $height]),
			'width' => $width,
			'height' => $height
		];
	}

	/**
	 * Get multi map data
	 *
	 * @param string $caption The caption
	 * @param int $updated The updated timestamp
	 * @param array $coordinates The coordinates array
	 * @param int $width The width
	 * @param int $height The height
	 * @return array The multi map data
	 */
	public function getMultiMap(string $caption, int $updated, array $coordinates, int $width = self::width, int $height = self::height): array {
		//Without coordinates
		if (empty($coordinates)) {
			//Return empty array
			return [];
		}

		//Set latitudes
		$latitudes = array_map(function ($v) { return $v['latitude']; }, $coordinates);

		//Set longitudes
		$longitudes = array_map(function ($v) { return $v['longitude']; }, $coordinates);

		//Set latitude
		$latitude = round((min($latitudes)+max($latitudes))/2, 6);

		//Set longitude
		$longitude = round((min($longitudes)+max($longitudes))/2, 6);

		//Set zoom
		$zoom = $this->getMultiZoom($latitude, $longitude, $coordinates, $width, $height);

		//Set coordinate
		$coordinate = implode('-', array_map(function ($v) { return $v['latitude'].','.$v['longitude']; }, $coordinates));

		//Set coordinate hash
		$hash = $this->slugger->hash($coordinate);

		//Set link hash
		$link = $this->slugger->hash([$updated, $latitude, $longitude, $hash, $zoom + 1, $width * 2, $height * 2]);

		//Set src hash
		$src = $this->slugger->hash([$updated, $latitude, $longitude, $hash, $zoom, $width, $height]);

		//Return array
		return [
			'caption' => $caption,
			'link' => $this->router->generate('rapsyspack_multimap', ['hash' => $link, 'updated' => $updated, 'latitude' => $latitude, 'longitude' => $longitude, 'coordinates' => $coordinate, 'zoom' => $zoom + 1, 'width' => $width * 2, 'height' => $height * 2]),
			'src' => $this->router->generate('rapsyspack_multimap', ['hash' => $src, 'updated' => $updated, 'latitude' => $latitude, 'longitude' => $longitude, 'coordinates' => $coordinate, 'zoom' => $zoom, 'width' => $width, 'height' => $height]),
			'width' => $width,
			'height' => $height
		];
	}

	/**
	 * Get multi zoom
	 *
	 * Compute a zoom to have all coordinates on multi map
	 * Multi map visible only from -($width / 2) until ($width / 2) and from -($height / 2) until ($height / 2)
	 *
	 * @see Wether we need to take in consideration circle radius in coordinates comparisons, likely +/-(radius / self::tz)
	 *
	 * @param float $latitude The latitude
	 * @param float $longitude The longitude
	 * @param array $coordinates The coordinates array
	 * @param int $width The width
	 * @param int $height The height
	 * @param int $zoom The zoom
	 * @return int The zoom
	 */
	public function getMultiZoom(float $latitude, float $longitude, array $coordinates, int $width, int $height, int $zoom = self::zoom): int {
		//Iterate on each zoom
		for ($i = $zoom; $i >= 1; $i--) {
			//Get tile xy
			$centerX = self::longitudeToX($longitude, $i);
			$centerY = self::latitudeToY($latitude, $i);

			//Calculate start xy
			$startX = floor($centerX - $width / 2 / self::tz);
			$startY = floor($centerY - $height / 2 / self::tz);

			//Calculate end xy
			$endX = ceil($centerX + $width / 2 / self::tz);
			$endY = ceil($centerY + $height / 2 / self::tz);

			//Iterate on each coordinates
			foreach($coordinates as $k => $coordinate) {
				//Set dest x
				$destX = self::longitudeToX($coordinate['longitude'], $i);

				//With outside point
				if ($startX >= $destX || $endX <= $destX) {
					//Skip zoom
					continue(2);
				}

				//Set dest y
				$destY = self::latitudeToY($coordinate['latitude'], $i);

				//With outside point
				if ($startY >= $destY || $endY <= $destY) {
					//Skip zoom
					continue(2);
				}
			}

			//Found zoom
			break;
		}

		//Return zoom
		return $i;
	}

	/**
	 * Convert longitude to tile x number
	 *
	 * @see https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Lon..2Flat._to_tile_numbers_5
	 *
	 * @param float $longitude The longitude
	 * @param int $zoom The zoom
	 *
	 * @return float The tile x
	 */
	public static function longitudeToX(float $longitude, int $zoom): float {
		return (($longitude + 180) / 360) * pow(2, $zoom);
	}

	/**
	 * Convert latitude to tile y number
	 *
	 * @see https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Lon..2Flat._to_tile_numbers_5
	 *
	 * @param $latitude The latitude
	 * @param $zoom The zoom
	 *
	 * @return float The tile y
	 */
	public static function latitudeToY(float $latitude, int $zoom): float {
		return (1 - log(tan(deg2rad($latitude)) + 1 / cos(deg2rad($latitude))) / pi()) / 2 * pow(2, $zoom);
	}

	/**
	 * Convert tile x to longitude
	 *
	 * @param float $x The tile x
	 * @param int $zoom The zoom
	 *
	 * @return float The longitude
	 */
	public static function xToLongitude(float $x, int $zoom): float {
		return $x / pow(2, $zoom) * 360.0 - 180.0;
	}

	/**
	 * Convert tile y to latitude
	 *
	 * @param float $y The tile y
	 * @param int $zoom The zoom
	 *
	 * @return float The latitude
	 */
	public static function yToLatitude(float $y, int $zoom): float {
		return rad2deg(atan(sinh(pi() * (1 - 2 * $y / pow(2, $zoom)))));
	}

	/**
	 * Convert decimal latitude to sexagesimal
	 *
	 * @param float $latitude The decimal latitude
	 *
	 * @return string The sexagesimal longitude
	 */
	public static function latitudeToSexagesimal(float $latitude): string {
		//Set degree
		//TODO: see if round or intval is better suited to fix the Deprecated: Implicit conversion from float to int loses precision
		$degree = round($latitude) % 60;

		//Set minute
		$minute = round(($latitude - $degree) * 60) % 60;

		//Set second
		$second = round(($latitude - $degree - $minute / 60) * 3600) % 3600;

		//Return sexagesimal longitude
		return $degree.'°'.$minute.'\''.$second.'"'.($latitude >= 0 ? 'N' : 'S');
	}

	/**
	 * Convert decimal longitude to sexagesimal
	 *
	 * @param float $longitude The decimal longitude
	 *
	 * @return string The sexagesimal longitude
	 */
	public static function longitudeToSexagesimal(float $longitude): string {
		//Set degree
		//TODO: see if round or intval is better suited to fix the Deprecated: Implicit conversion from float to int loses precision
		$degree = round($longitude) % 60;

		//Set minute
		$minute = round(($longitude - $degree) * 60) % 60;

		//Set second
		$second = round(($longitude - $degree - $minute / 60) * 3600) % 3600;

		//Return sexagesimal longitude
		return $degree.'°'.$minute.'\''.$second.'"'.($longitude >= 0 ? 'E' : 'W');
	}
}