<?php namespace Intellex\Curly;

use Intellex\Curly\Content\Type;
use Intellex\Curly\Enum\CurlOptions;
use Intellex\Debugger\Debugger;

/**
 * A single request via the cURL.
 */
class Curl {

	/** @var Method Method The HTTP method to use */
	private $method = null;

	/** @var URL The target URL. */
	private $url;

	/** @var array The list of HTTP header to be sent, as a key => value pair. */
	private $headers = [];

	/** var @mixed The data to send. */
	private $data = null;

	/** @var Type The Content-Type to use. */
	private $type = 'application/x-www-form-urlencoded';

	/** @var array The list of overrides to use, where key is a curl option and value is its value. */
	private $overrides = [];

	/** @var string The custom user agent to use. */
	private $agent = null;

	/** @var object The credentials for the HTTP authentication, or null if no authentication is needed. */
	private $authentication = null;

	/** @var string[]|null The request log of all set curl options. Null if curl was not executed. */
	private $log = null;

	/**
	 * Curl constructor.
	 *
	 * @param string $url The full path to the target URL.
	 */
	public function __construct($url) {
		$this->url = new URL($url);
	}

	/**
	 * Set the method used.
	 *
	 * @param string $method The HTTP method to use, see Method.
	 *
	 * @return $this
	 */
	public function setMethod($method) {
		$this->method = $method;
		return $this;
	}

	/**
	 * Set the data.
	 *
	 * @param mixed  $data   The data to send.
	 * @param string $type   The content type to use.
	 * @param string $method The HTTP method to use, see Method.
	 *
	 * @return $this
	 */
	public function setData($data, $type = 'application/x-www-form-urlencoded', $method = null) {
		$this->data = $data;
		$this->type = Type::init($type);

		// Set content type
		$this->addHeader('Content-Type', $type);

		// Default method to post
		if ($this->method === null) {
			$this->setMethod($method ? $method : ($data ? Method::POST : Method::GET));
		}

		return $this;
	}

	/**
	 * Set the headers.
	 *
	 * @param string[] $headers The associative array of the headers and their value.
	 *
	 * @return Curl $this.
	 */
	public function setHeaders($headers = []) {
		foreach ((array) $headers as $header => $value) {
			$this->addHeader($header, $value);
		}

		return $this;
	}

	/**
	 * Set the HTTP authentication.
	 *
	 * @param string $user     The user for the authentication.
	 * @param string $password The password for the authentication, as plain text.
	 */
	public function setAuthentication($user, $password) {
		$this->authentication = [ 'user' => $user, 'password' => $password ];
	}

	/**
	 * Add a header.
	 * Previous values will be overwritten.
	 *
	 * @param string $header The name of the header.
	 * @param string $value  The value of the header.
	 *
	 * @return Curl $this.
	 */
	public function addHeader($header, $value = '') {
		$this->headers[$this->sanitizeHeader($header)] = $value;
		return $this;
	}

	/**
	 * Set the user agent.
	 *
	 * @param  string $agent The user agent to use.
	 *
	 * @return Curl $this.
	 */
	public function setUserAgent($agent) {
		$this->agent = $agent;
		return $this;
	}

	/**
	 * Sanitize the name of the header.
	 *
	 * @param string $header The name of the header to sanitize.
	 *
	 * @return string The sanitized header, as lowercase, minus-separated.
	 */
	private function sanitizeHeader($header) {
		return strtolower(preg_replace('~[\s_]+~', '', $header));
	}

	/**
	 * Override a cURL option.
	 *
	 * @param array $options An array specifying which options to set and their values. The keys
	 *                       should be valid curl_setopt() constants or their integer equivalents.
	 */
	public function override($options) {
		$this->overrides = array_merge($this->overrides, $options);
	}

	/**
	 * Create and execute the cURL.
	 *
	 * @return Response The response from the remote.
	 */
	public function execute() {

		// Initialize the type
		if (is_string($this->type)) {
			$this->type = Type::init($this->type);
		}

		// Convert data
		if ($this->data !== null) {
			$this->data = $this->type->format($this->data);
		}

		// Initialize
		$ch = curl_init();
		static::curl_setopt($ch, CURLOPT_URL, $this->url->toString());
		if ($this->method) {
			static::curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $this->method);
		}

		// Additional options
		static::curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
		static::curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
		static::curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
		static::curl_setopt($ch, CURLOPT_HEADER, 1);

		// HTTP authentication
		if ($this->authentication) {
			static::curl_setopt($ch, CURLOPT_USERPWD, "{$this->authentication['user']}:{$this->authentication['password']}");
		}

		// User agent
		if ($this->agent) {
			static::curl_setopt($ch, CURLOPT_USERAGENT, $this->agent);
		}

		// Handle "100 Continue" header responses 
		$this->headers['Expect'] = '';

		// Set the headers
		$headers = [];
		foreach ($this->headers as $header => $value) {
			$headers[] = "{$header}: {$value}";
		}
		if (!empty($headers)) {
			static::curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
		}

		// Additional data
		if ($this->data !== null) {
			static::curl_setopt($ch, CURLOPT_POSTFIELDS, $this->data);
		}

		// Apply the overrides
		foreach ($this->overrides as $option => $value) {
			static::curl_setopt($ch, $option, $value);
		}

		// Execute and get the full info
		$data = curl_exec($ch);
		$response = new Response($data, curl_getinfo($ch));

		// Return the data
		curl_close($ch);
		return $response;
	}

	/**
	 * Set the curl option and write it to the request log.
	 *
	 * @param curl  $ch     The curl handle to set on.
	 * @param int   $option The option to set.
	 * @param mixed $value  The value to set.
	 */
	private function curl_setopt($ch, $option, $value) {

		// Initialize log
		if (!$this->log) {
			$this->log = [];
		}

		// Write to log
		$label = 'CURLOPT_' . CurlOptions::getName($option) . ' (' . $option . ')';
		$this->log[] = trim($label . ' => ' . Debugger::getReadableValue($value));

		// Set the curl option
		curl_setopt($ch, $option, $value);
	}

	/** @var string[]|null The request log of all set curl options. Null if curl was not executed. */
	public function getLog() {
		return $this->log;
	}

}
