<?php
/**
 * @copyright © 2022 ExacTI Technology Solutions. All rights reserved.
 * GPLv3 General License.
 * https://exacti.com.br
 * Phacil PHP Framework - https://github.com/exacti/phacil-framework
 * @author Bruno O. Notario <bruno@exacti.com.br>
 */

namespace Phacil\Framework;

use Phacil\Framework\Controller;
use Phacil\Framework\Request;
use Phacil\Framework\ReflectionMethod;

/**
 * Create a simple and faster REST API controller.
 * 
 * @package Phacil\Framework
 * @since 2.0.0
 * @abstract
 * @api
 */
abstract class RESTful extends Controller {

	/**
	 * The output content type
	 * 
	 * @var string
	 */
	public $contentType = 'application/json';

	/**
	 * 
	 * @var string
	 */
	public $acceptType = 'application/json';

	/**
	 * 
	 * @var int
	 */
	public $error_code = 404;

	/**
	 * 
	 * @var string
	 */
	public $error_msg = 'This service not have implemented the %s method.';

	/**
	 * 
	 * @var string[]
	 */
	public $HTTPMETHODS = [ 'GET', 'CONNECT', 'HEAD', 'PUT', 'DELETE', 'TRACE', 'POST', 'OPTIONS', 'PATCH'];

	/**
	 * 
	 * @return void 
	 */
	function __construct() {
		parent::__construct();
	}

	/**
	 * 
	 * @return void 
	 * @throws \Phacil\Framework\Exception 
	 */
	function index() {
		$method = (Request::METHOD());

		if (in_array($method, $this->HTTPMETHODS) && is_callable(array($this, $method))) {
			$r = new ReflectionMethod($this, $method);
			$params = [];

			$comment_string = $r->getDocCommentParse();

			$phpDocParams = ($comment_string) ? $comment_string->getParams() : false;
			
			foreach($r->getParameters() as $key => $value) {
				switch (strtoupper( $method)) {
					case 'GET':
						$data = Request::GET($value->getName());
						break;
					
					case 'HEAD':
						$data = Request::HEADER($value->getName());
						break;
					
					case 'POST':
					default:
						try {
							$data = (Request::POST($value->getName())) ?: Request::INPUT($value->getName());
						} catch (\Exception $th) {
							return $this->__callInterrupt($th->getMessage());
						}
						break;
				}

				/**
				 * check if have a sufficiente data for the request
				 */
				if($data === null) {
					if(!$value->isOptional()){
						return $this->__callInterrupt($value->getName(). " is required.");
					}
				}

				if($data !== null && $phpDocParams && isset($phpDocParams['param']) && is_array($phpDocParams['param'])){
					$type = (isset($phpDocParams['param']['$'.$value->getName()])) ? $phpDocParams['param']['$' . $value->getName()]['type']: false;
					if($type){
						if((is_array($type) && !in_array(gettype($data), $type)) || (gettype($data) != $type)){
							$invalidDataType = true;

							if(is_array($type)){
								foreach ($type as $avalType) {
									if(self::__testType( $avalType, $data)) {
										$invalidDataType = false;
										break;
									}
								}
							} else {
								if(self::__testType($type, $data)) {
									$invalidDataType = false;
								}
							}
							
							if($invalidDataType){
								return $this->__callInterrupt($value->getName() . " need to be: ".(is_array($type) ? implode(', ', $type) : $type).". You give: ".gettype($data).".");
							}
							
						} 

					}
				}

				if($data){
					$params[$value->getName()] = $data; 
				};
			};

			try {
				//code...
				call_user_func_array(array($this, $method), $params);
			} catch (\Phacil\Framework\Exception\WebApiException $re) {
				$this->data = is_string($re->getMessage()) ? ['error' => $re->getMessage()] : $re->getMessage();
				$this->response->code($re->getCode());
				$this->out();
			}catch (\Phacil\Framework\Exception\Throwable $th) {
				if(get_class($th) == 'TypeError'){
					new Exception($th->getMessage(), $th->getCode());
					return $this->__callInterrupt($th->getMessage());
				} else {
					throw new Exception($th->getMessage(), $th->getCode());
				}
				
			} catch (Exception $e) {
				throw new Exception($e->getMessage(), $e->getCode());
			}
		} else {
			$this->__callNotFound($method);
		}
	}

	/**
	 * Return true or false for data type
	 * 
	 * @param string $type Type to test
	 * @param string $data Data to test
	 * @return bool 
	 */
	static function __testType($type, $data){ 

		switch ($type) {
			case 'mixed':
				return true;
				break;
				
			case 'int':
			case 'string':
			case 'array':
			case 'integer':
			case 'bool':
			case 'double':
			case 'float':
			case 'long':
			case 'null':
			case 'numeric':
			case 'scalar':
			case 'real':
				return call_user_func("is_" . $type, $data);
				break;
			
			default:
				return false;
				break;
		}

	}

	/**
	 * Not found default method
	 * 
	 * @param string $method 
	 * @param mixed $args 
	 * @return void 
	 * @throws \Phacil\Framework\Exception 
	 */
	protected function __callNotFound($method, $args = null) {
		$this->response->code($this->error_code);
		$this->data['error'] = sprintf($this->error_msg, $method);

		$this->out();
	}

	/**
	 * Interrupt method with a exit.
	 * 
	 * @param string $msg 
	 * @param int $code 
	 * @return void 
	 * @throws \Phacil\Framework\Exception 
	 */
	protected function __callInterrupt($msg, $code = 400) {
		$this->error_msg = $msg;
		$this->error_code = 400;
		$method = 'JSONERROR';
		$this->__callNotFound($method);
		return;
	}

	/**
	 * 
	 * @return void 
	 * @throws \Phacil\Framework\Exception 
	 */
	protected function JSONERROR() {
		$this->out();
	}

	/**
	 * Deafult method to OPTIONS HTTP call.
	 * 
	 * This method list in HTTP header a "Allow" tag with methods implemented and accesibles.
	 * 
	 * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/OPTIONS
	 * 
	 * @return void 
	 * @throws \Phacil\Framework\Exception 
	 */
	protected function OPTIONS(){
		$methods = [];
		foreach ($this->HTTPMETHODS as $method) {
			if (is_callable(array($this, $method)))
				$methods[] = $method;
		}

		$this->response->addHeader('Allow', implode(", ", $methods));
		$this->data['allow'] =  $methods;

		$this->out();
	}

	/**
	 * The default and automated output method. All itens in the $this->data are rendered in JSON format.
	 * 
	 * @param bool $commonChildren (optional)
	 * @return \Phacil\Framework\Response 
	 * @throws Exception 
	 */
	protected function out($commonChildren = true)
	{
		$this->response->addHeader('Content-Type', $this->contentType);
		if($this->acceptType)
			$this->response->addHeader('Accept', $this->acceptType);

		return $this->response->setOutput(JSON::encode($this->data));
	}
}