<?php namespace Intellex\Curly;

use Intellex\Curly\Exception\ProtocolNotSupportedException;
use Intellex\Curly\Exception\UnparseableURLException;

/**
 * Helper for validating URLs.
 */
class URL {

	/** @var string The original $url. */
	public $original;

	/** @var Protocol The protocol to use, either 'HTTP' or 'HTTPS'. */
	private $protocol;

	/** @var string The hostname of this URL. */
	private $host;

	/** @var string The requested path. */
	private $path;

	/** @var array The GET query parameters. */
	private $query;

	/**
	 * Create a URL from the supplied string.
	 *
	 * @param string $url The string from which to extract the URL.
	 *
	 * @throws UnparseableURLException On not being able to parse the URL.
	 */
	public function __construct($url = null) {
		$this->original = $url;
		if ($url) {

			// Build regexp
			$protocol = '((?<protocol>[a-z]+)://)?';
			$ip = '(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])';
			$domain = '(([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])\.)*([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])';
			$host = "((?<host>{$ip}|{$domain})(/|$))";
			$path = '((?<path>.*)(\?|$))';
			$query = '(?<query>.*)';
			/** @noinspection SpellCheckingInspection */
			$final = "~^\s*{$protocol}{$host}{$path}{$query}$~ Uuix";

			// Parse the URL
			if (!preg_match($final, $url, $match)) {
				throw new Exception\UnparseableURLException($url);
			}

			$this->protocol = new Protocol($match['protocol'] ? $match['protocol'] : 'http');
			$this->host = $match['host'];
			$this->path = $match['path'];
			$this->query = $this->parseGetQuery($match['query']);
		}
	}

	/**
	 * Build a URL.
	 *
	 * @param string|Protocol $protocol The protocol to use, either 'HTTP' or 'HTTPS'.
	 * @param string          $host     The hostname of this URL.
	 * @param string          $path     The requested path.
	 * @param array           $query    The GET query parameters.
	 *
	 * @return URL The created instance of the URL.
	 * @throws ProtocolNotSupportedException On protocol not recognized or not supported.
	 */
	public static function create($protocol, $host, $path, $query = []) {

		// Handle protocol
		if (is_string($protocol)) {
			$protocol = new Protocol($protocol);
		}

		// Divide path
		preg_match('~^/?(?<path>.*?)(\?(?<query>.+))?$~', $path, $match);
		$path = $match['path'];
		if ($query === [] && key_exists('query', $match)) {
			$query = static::parseGetQuery($match['query']);
		}

		// Set up
		$url = new URL();
		$url->setProtocol($protocol);
		$url->setHost($host);
		$url->setPath($path);
		$url->setQuery($query);
		$url->original = $url->toString();

		return $url;
	}

	/**
	 * Check if the URL is a secure.
	 *
	 * @return    boolean    True if the used protocol is HTTPS, false otherwise.
	 */
	public function isSecure() {
		return $this->protocol->isSecure();
	}

	/**
	 * Parse the GET parameters into an associative array.
	 *
	 * @param string $query The GET query to parse.
	 *
	 * @return array The parsed values.
	 */
	public static function parseGetQuery($query) {
		$query = preg_replace('~^[\?\s]~', '', trim($query, "\r\n\t &"));

		// Integrated method
		if (function_exists('parse_str')) {
			parse_str($query, $parsed);
			return $parsed;
		}

		// Sanitize end split
		$result = [];
		if (!empty($query)) {
			$query = explode('&', $query);

			// Parse
			foreach ($query as $param) {

				// Extract key and value
				if (strstr($param, '=') === false) {
					$param .= '=';
				}
				list($key, $value) = explode('=', $param, 2);
				$key = urldecode($key);
				$value = urldecode($value);

				// Sub keys
				$keyPath = [ $key ];
				if (preg_match('~(?<key>.+)(?<subs>(\[.*\]+)+$)~ Uui', $key, $match)) {

					// Redefine the key path
					$keyPath = array_merge([ $match['key'] ], explode('][', substr($match['subs'], 1, -1)));

					// Clear the empty add-ons
					foreach ($keyPath as $i => $step) {
						if ($step === '') {
							unset($keyPath[$i]);
						}
					}
				}

				// Drill down
				$pointer =& $result;
				foreach ($keyPath as $key) {

					// If not existent
					if (!key_exists($key, $result)) {
						$pointer =& $pointer[$key];

						// Existing array
					} else if (is_array($pointer[$key])) {
						$pointer =& $pointer[$key];

						// First duplicate
					} else {
						$pointer[$key] = [
							$pointer[$key],
							null
						];

						$pointer =& $pointer[$key][1];
					}
				}
				$pointer = $value;
			}
		}

		return $result;
	}

	/**
	 * Properly format the GET query, based on the current $this->query values.
	 *
	 * @param null   $query  The Get query to format.
	 * @param string $format The sprintf format to use.
	 *
	 * @return string The properly formatted GET query.
	 */
	public function formatGetQuery($query = null, $format = '%s') {

		// Initialize
		$result = '';
		if ($query === null) {
			$query = $this->query;
		}

		// Build
		foreach ($query as $key => $value) {

			// Separator
			if ($result) {
				$result .= '&';
			}

			// Simple kay and value
			if (is_null($value) || !is_array($value)) {
				$result .= sprintf($format, urlencode($key)) . '=' . urlencode($value);

				// Drill down to array
			} else {
				$result .= $this->formatGetQuery($value, sprintf($format, urlencode($key)) . '[%s]');
			}
		}

		return $result;
	}

	/**
	 * Get the URL as a string.
	 *
	 * @return string    The URL built from the supplied data.
	 */
	public function toString() {
		$query = $this->formatGetQuery();

		return
			($this->getProtocol()->toString() . '://') .
			($this->host) .
			($this->path ? '/' . $this->path : null) .
			($query ? '?' . $query : null);
	}

	/** @return string The original $url. */
	public function getOriginal() {
		return $this->original;
	}

	/** @param Protocol $protocol The protocol to use, either 'HTTP' or 'HTTPS'. */
	public function setProtocol(Protocol $protocol) {
		$this->protocol = new Protocol($protocol);
	}

	/** @return Protocol The protocol to use, either 'HTTP' or 'HTTPS'. */
	public function getProtocol() {
		return $this->protocol;
	}

	/** @param string $host The hostname of this URL. */
	public function setHost($host) {
		$this->host = $host;
	}

	/** @return string The hostname of this URL. */
	public function getHost() {
		return $this->host;
	}

	/** @param string $path The requested path. */
	public function setPath($path) {
		$this->path = $path;
	}

	/** @return string The requested path. */
	public function getPath() {
		return $this->path;
	}

	/** @param array $query The GET query parameters. */
	public function setQuery($query) {
		$this->query = $query;
	}

	/** @return array The GET query parameters. */
	public function getQuery() {
		return $this->query;
	}

}
