<?php

namespace WPDesk\HttpClient\Curl;

use WPDesk\HttpClient\HttpClient;
use WPDesk\HttpClient\HttpClientResponse;
use WPDesk\HttpClient\HttpClientRequestException;

class CurlClient implements HttpClient {
	/** @var resource */
	private $curlResource;
	/** @var  string|null|boolean */
	private $rawResponse;
	/** @var int */
	private $httpResponseCode;
	/** @var array */
	private $headers = array();

	/**
	 * @param string $url
	 * @param string $body
	 * @param array  $headers
	 * @param int    $timeOut
	 *
	 * @return HttpClientResponse
	 * @throws HttpClientRequestException
	 */
	public function get( $url, $body, array $headers, $timeOut ) {
		return $this->send( $url, 'GET', $body, $headers, $timeOut );
	}

	/**
	 * @param string $url
	 * @param string $method
	 * @param string $body
	 * @param array  $headers
	 * @param int    $timeOut
	 *
	 * @return HttpClientResponse
	 * @throws HttpClientRequestException
	 */
	public function send( $url, $method, $body, array $headers, $timeOut ) {
		$this->initResource();
		try {
			$this->prepareConnection( $url, $method, $body, $headers, $timeOut );
			$this->sendRequest();
			$this->throwExceptionIfError();
			$this->closeConnection();

			return $this->prepareResponse();
		} catch ( \Exception $e ) {
			$this->closeConnection();
			if ( $e instanceof HttpClientRequestException ) {
				throw $e;
			}
			throw CurlExceptionFactory::createDefaultException( $e->getMessage(), $e->getCode(), $e );
		}
	}

	private function initResource() {
		$this->curlResource = \curl_init();
	}

	/**
	 * Opens a new curl connection.
	 *
	 * @param string $url     The endpoint to send the request to.
	 * @param string $method  The request method.
	 * @param string $body    The body of the request.
	 * @param array  $headers The request headers.
	 * @param int    $timeOut The timeout in seconds for the request.
	 */
	private function prepareConnection( $url, $method, $body, array $headers, $timeOut ) {

		$options = [
			\CURLOPT_CUSTOMREQUEST  => $method,
			\CURLOPT_HTTPHEADER     => $this->compileRequestHeaders( $headers ),
			\CURLOPT_URL            => $url,
			\CURLOPT_CONNECTTIMEOUT => 25,
			\CURLOPT_TIMEOUT        => $timeOut,
			\CURLOPT_RETURNTRANSFER => \true,
			// Return response as string
			\CURLOPT_HEADER         => \false,
			// Enable header processing
			\CURLOPT_SSL_VERIFYHOST => 2,
			\CURLOPT_SSL_VERIFYPEER => \true,
			\CURLOPT_HEADERFUNCTION => function ( $curl, $header ) {
				$len             = strlen( $header );
				$this->headers[] = trim( $header );

				return $len;
			}
		];
		if ( ! empty( $body ) ) {
			$options[ \CURLOPT_POSTFIELDS ] = $body;
		}
		\curl_setopt_array( $this->curlResource, $options );
	}

	/**
	 * Compiles the request headers into a curl-friendly format.
	 *
	 * @param array $headers The request headers.
	 *
	 * @return array
	 */
	private function compileRequestHeaders( array $headers ) {
		$return = [];
		foreach ( $headers as $key => $value ) {
			$return[] = $key . ': ' . $value;
		}

		return $return;
	}

	/**
	 * Send the request and get the raw response from curl
	 */
	private function sendRequest() {
		$this->rawResponse      = \curl_exec( $this->curlResource );
		$this->httpResponseCode = $this->getHttpResponseCode();
	}

	/** @return int */
	private function getHttpResponseCode() {
		return \intval( \curl_getinfo( $this->curlResource, \CURLINFO_HTTP_CODE ) );
	}

	private function throwExceptionIfError() {
		$errorNumber = \curl_errno( $this->curlResource );
		if ( $errorNumber === 0 ) {
			return;
		}
		$errorMessage = \curl_error( $this->curlResource );
		throw CurlExceptionFactory::createCurlException( $errorMessage, $errorNumber );
	}

	/**
	 * Closes an existing curl connection
	 */
	private function closeConnection() {
		\curl_close( $this->curlResource );
		$this->curlResource = null;
	}

	private function prepareResponse() {
		list( $rawHeaders, $rawBody ) = $this->extractResponseHeadersAndBody();

		return new HttpClientResponse( $rawHeaders, $rawBody, $this->httpResponseCode );
	}

	/**
	 * Extracts the headers and the body into a two-part array
	 *
	 * @return array
	 */
	private function extractResponseHeadersAndBody() {
		$rawBody    = $this->rawResponse;
		$rawHeaders = implode( "\r\n", $this->headers );

		return [ $rawHeaders, $rawBody ];
	}

	/**
	 * @param string $url
	 * @param string $body
	 * @param array  $headers
	 * @param int    $timeOut
	 *
	 * @return HttpClientResponse
	 * @throws HttpClientRequestException
	 */
	public function post( $url, $body, array $headers, $timeOut ) {
		return $this->send( $url, 'POST', $body, $headers, $timeOut );
	}

	/**
	 * @param string $url
	 * @param string $body
	 * @param array  $headers
	 * @param int    $timeOut
	 *
	 * @return HttpClientResponse
	 * @throws HttpClientRequestException
	 */
	public function delete( $url, $body, array $headers, $timeOut ) {
		return $this->send( $url, 'DELETE', $body, $headers, $timeOut );
	}

	/**
	 * @param string $url
	 * @param string $body
	 * @param array  $headers
	 * @param int    $timeOut
	 *
	 * @return HttpClientResponse
	 * @throws HttpClientRequestException
	 */
	public function put( $url, $body, array $headers, $timeOut ) {
		return $this->send( $url, 'PUT', $body, $headers, $timeOut );
	}
}