<?php

namespace App\Classes\Sock;

use Illuminate\Support\Facades\Log;

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 array $handler;				// The class and method that will handle our connection

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

		$this->_init();
	}

	/**
	 * Bind to our Socket
	 *
	 * @throws SocketException
	 */
	private function _bindSocket(): void
	{
		if (socket_bind($this->server,$this->bind,$this->port) === FALSE)
			throw new SocketException(SocketException::CANT_BIND_SOCKET,socket_strerror(socket_last_error($this->server)));
	}

	/**
	 * 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');

		$this->server = socket_create(AF_INET,SOCK_STREAM,SOL_TCP);

		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);
	}

	/**
	 * Setup Socket and Bind
	 *
	 * @throws SocketException
	 */
	private function _init(): void
	{
		$this->_createSocket();
		$this->_bindSocket();
	}

	/**
	 * 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 (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));

		$this->loop();
		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()
	{
		while (TRUE) {
			if (($accept = socket_accept($this->server)) === FALSE)
				throw new SocketException(SocketException::CANT_ACCEPT,socket_strerror(socket_last_error($this->server)));

			$this->handler[0]->{$this->handler[1]}(new SocketClient($accept));
		}
	}

	/**
	 * Set our connection handler Class and Method
	 *
	 * @param array $handler
	 */
	public function setConnectionHandler(array $handler): void
	{
		$this->handler = $handler;
	}
}