<?php

namespace App\Classes\Sock;

use Illuminate\Support\Facades\Log;

use App\Classes\Sock\Exception\{HAproxyException,SocketException};

final class SocketServer {
	private const LOGKEY = 'SS-';

	private $server;					// Our server resource
	private string $bind;				// The IP address to bind to
	private int $port;					// The Port to bind to
	private int $backlog = 5;			// Number of incoming connections queued

	private int $type;					// Socket type

	private array $handler;				// The class and method that will handle our connection

	public function __construct(int $port,string $bind='0.0.0.0',int $type=SOCK_STREAM)
	{
		$this->bind = $bind;
		$this->port = $port;
		$this->type = $type;

		$this->createSocket();

		if (socket_bind($this->server,$this->bind,$this->port) === FALSE)
			throw new SocketException(SocketException::CANT_BIND_SOCKET,socket_strerror(socket_last_error($this->server)));
	}

	public function __get($key)
	{
		switch ($key) {
			case 'handler':
				return $this->handler;
			default:
				throw new \Exception('Unknown key: '.$key);
		}
	}

	public function __set($key,$value)
	{
		switch ($key) {
			case 'handler':
				return $this->handler = $value;
			default:
				throw new \Exception('Unknown key: '.$key);
		}
	}

	/**
	 * Create our Socket
	 *
	 * @throws SocketException
	 */
	private function createSocket(): void
	{
		/**
		 * Check dependencies
		 */
		if (! extension_loaded('sockets'))
			throw new SocketException(SocketException::CANT_ACCEPT,'Missing sockets extension');

		if (! extension_loaded('pcntl'))
			throw new SocketException(SocketException::CANT_ACCEPT,'Missing pcntl extension');

		switch ($this->type) {
			case SOCK_STREAM:
				$this->server = socket_create(AF_INET|AF_INET6,$this->type,SOL_TCP);
				break;

			case SOCK_DGRAM:
				$this->server = socket_create(AF_INET|AF_INET6,$this->type,SOL_UDP);
				break;

			default:
				throw new \Exception('Unknown socket_type:'.$this->type);
		}

		if ($this->server === FALSE)
			throw new SocketException(SocketException::CANT_CREATE_SOCKET,socket_strerror(socket_last_error()));

		socket_set_option($this->server,SOL_SOCKET,SO_REUSEADDR,1);
	}

	/**
	 * Our main loop where we listen for connections
	 *
	 * @throws SocketException
	 */
	public function listen()
	{
		if (! $this->handler)
			throw new SocketException(SocketException::CANT_LISTEN,'Handler not set.');

		if (in_array($this->type,[SOCK_STREAM,SOCK_SEQPACKET]))
			if (socket_listen($this->server,$this->backlog) === FALSE)
				throw new SocketException(SocketException::CANT_LISTEN,socket_strerror(socket_last_error($this->server)));

		Log::info(sprintf('%s:- Listening on [%s:%d]',self::LOGKEY,$this->bind,$this->port));

		switch ($this->type) {
			case SOCK_STREAM:
				$this->loop_tcp();
				break;

			case SOCK_DGRAM:
				$this->loop_udp();
				break;
		}

		socket_close($this->server);

		Log::info(sprintf('%s:= Closed [%s:%d]',self::LOGKEY,$this->bind,$this->port));
	}

	/**
	 * Manage and execute incoming connections
	 *
	 * @throws SocketException
	 */
	private function loop_tcp(): void
	{
		while (TRUE) {
			if (($accept = socket_accept($this->server)) === FALSE)
				throw new SocketException(SocketException::CANT_ACCEPT,socket_strerror(socket_last_error($this->server)));

			Log::debug(sprintf('%s:* TCP Loop Start',self::LOGKEY));

			try {
				$r = new SocketClient($accept);

			} catch (HAproxyException $e) {
				Log::notice(sprintf('%s:! HAPROXY Exception [%s]',self::LOGKEY,$e->getMessage()));
				socket_close($accept);
				continue;

			} catch (\Exception $e) {
				Log::notice(sprintf('%s:! Creating Socket client failed? [%s]',self::LOGKEY,$e->getMessage()));
				socket_close($accept);
				continue;
			}

			// If the handler returns a value, then that is the main thread
			if (! $this->handler[0]->{$this->handler[1]}($r)) {
				$r->close();
				exit(0);
			}
		}
	}

	private function loop_udp(): void
	{
		while (TRUE) {
			$r = new SocketClient($this->server);

			if ($r->hasData(30)) {
				if (! ($this->handler[0]->{$this->handler[1]}($r)))
					exit(0);

				// Sleep so our thread has a chance to pick up the data from our connection
				usleep(50000);
			}
		}
	}
}