<?php

namespace App\Classes;

use Exception;
use Illuminate\Support\Facades\Log;

use App\Classes\File\{Receive,Send};
use App\Classes\Sock\SocketClient;
use App\Classes\Sock\SocketException;
use App\Models\Node as NodeModel;
use App\Models\Setup;

abstract class Protocol
{
	// Our product code
	// @todo Move These to a config file
	protected const product_code	= 'AB8D';
	public const setup				= 1;

	// Enable extra debugging
	protected bool $DEBUG = FALSE;

	// Return constants
	protected const OK				= 0;
	protected const EOF				= -1;
	protected const TIMEOUT			= -2;
	protected const RCDO			= -3;
	protected const GCOUNT			= -4;
	protected const ERROR			= -5;

	// Our sessions Types
	public const SESSION_AUTO		= 0;
	public const SESSION_EMSI		= 1;
	public const SESSION_BINKP		= 2;
	public const SESSION_ZMODEM		= 3;

	protected const MAX_PATH		= 1024;

	/* 9 most right bits are zeros */
	private const O_BASE			= 9;						/* First 9 bits are protocol */
	protected const O_NRQ			= (1<<self::O_BASE);		/* 0000 0000 0000 0000 0010 0000 0000 BOTH - No file requests accepted by this system */
	protected const O_HRQ			= (1<<(self::O_BASE+1));	/* 0000 0000 0000 0000 0100 0000 0000 BOTH - Hold file requests (not processed at this time). */
	protected const O_FNC			= (1<<(self::O_BASE+2));	/* 0000 0000 0000 0000 1000 0000 0000 - Filename conversion, transmitted files must be 8.3 */
	protected const O_XMA			= (1<<(self::O_BASE+3));	/* 0000 0000 0000 0001 0000 0000 0000 - Supports other forms of compressed mail */
	protected const O_HAT			= (1<<(self::O_BASE+4));	/* 0000 0000 0000 0010 0000 0000 0000 BOTH - Hold ALL files (Answering System) */
	protected const O_HXT			= (1<<(self::O_BASE+5));	/* 0000 0000 0000 0100 0000 0000 0000 BOTH - Hold Mail traffic */
	protected const O_NPU			= (1<<(self::O_BASE+6));	/* 0000 0000 0000 1000 0000 0000 0000 - No files pickup desired (Calling System) */
	protected const O_PUP			= (1<<(self::O_BASE+7));	/* 0000 0000 0001 0000 0000 0000 0000 - Pickup files for primary address only */
	protected const O_PUA			= (1<<(self::O_BASE+8));	/* 0000 0000 0010 0000 0000 0000 0000 EMSI - Pickup files for all presented addresses */
	protected const O_PWD			= (1<<(self::O_BASE+9));	/* 0000 0000 0100 0000 0000 0000 0000 BINK - Node password validated */
	protected const O_BAD			= (1<<(self::O_BASE+10));	/* 0000 0000 1000 0000 0000 0000 0000 BOTH - NOde invalid password presented */
	protected const O_RH1			= (1<<(self::O_BASE+11));	/* 0000 0001 0000 0000 0000 0000 0000 EMSI - Use RH1 for Hydra (files-after-freqs) */
	protected const O_LST			= (1<<(self::O_BASE+12));	/* 0000 0010 0000 0000 0000 0000 0000 BOTH - Node is nodelisted */
	protected const O_INB			= (1<<(self::O_BASE+13));	/* 0000 0100 0000 0000 0000 0000 0000 BOTH - Inbound session */
	protected const O_TCP			= (1<<(self::O_BASE+14));	/* 0000 1000 0000 0000 0000 0000 0000 BOTH - TCP session */
	protected const O_EII			= (1<<(self::O_BASE+15));	/* 0001 0000 0000 0000 0000 0000 0000 EMSI - Remote understands EMSI-II */

	// Session Status
	protected const S_OK			= 0;
	protected const S_NODIAL		= 1;
	protected const S_REDIAL		= 2;
	protected const S_BUSY			= 3;
	protected const S_FAILURE		= 4;
	protected const S_MASK			= 7;
	protected const S_HOLDR			= 8;
	protected const S_HOLDX			= 16;
	protected const S_HOLDA			= 32;
	protected const S_ADDTRY		= 64;
	protected const S_ANYHOLD		= (self::S_HOLDR|self::S_HOLDX|self::S_HOLDA);

	// File transfer status
	protected const FOP_OK			= 0;
	protected const FOP_CONT		= 1;
	protected const FOP_SKIP		= 2;
	protected const FOP_ERROR		= 3;
	protected const FOP_SUSPEND		= 4;

	protected const MO_CHAT			= 4;

	protected SocketClient $client;									/* Our socket details */
	protected Setup $setup;											/* Our setup */
	protected Node $node;											/* The node we are communicating with */
	protected Send $send;											/* The list of files we are sending */
	protected Receive $recv;										/* The list of files we are receiving */

	private int $options;											/* Our options for a session */
	private int $session;											/* Tracks where we are up to with this session */
	protected bool $originate;										/* Are we originating a connection */
	private array $comms;											/* Our comms details */

	abstract protected function protocol_init(): int;
	abstract protected function protocol_session(): int;

	/**
	 * @throws Exception
	 */
	public function __get($key)
	{
		switch ($key) {
			case 'ls_SkipGuard':									/* double-skip protection on/off */
			case 'rxOptions':										/* Options from ZRINIT header */
				return $this->comms[$key] ?? 0;

			case 'ls_rxAttnStr':
				return $this->comms[$key] ?? '';

			default:
				throw new Exception('Unknown key: '.$key);
		}
	}

	/**
	 * @throws Exception
	 */
	public function __set($key,$value)
	{
		switch ($key) {
			case 'ls_rxAttnStr':
			case 'ls_SkipGuard':
			case 'rxOptions':
				$this->comms[$key] = $value;
				break;

			default:
				throw new Exception('Unknown key: '.$key);
		}
	}

	/**
	 * We got an error, close anything we are have open
	 *
	 * @throws Exception
	 */
	 protected function error_close(): void
	 {
		 if ($this->send->fd)
			 $this->send->close(FALSE);

		 if ($this->recv->fd)
			 $this->recv->close();
	 }

	/**
	 * Incoming Protocol session
	 *
	 * @param SocketClient $client
	 * @return int|null
	 * @throws SocketException
	 */
	public function onConnect(SocketClient $client): ?int
	{
		$pid = pcntl_fork();

		if ($pid == -1)
			throw new SocketException(SocketException::CANT_ACCEPT,'Could not fork process');

		Log::debug(sprintf('%s: = End [%d]',__METHOD__,$pid));

		// Parent return ready for next connection
		return $pid;
	}

	protected function optionClear(int $key): void
	{
		$this->options &= ~$key;
	}

	protected function optionGet(int $key): int
	{
		return ($this->options & $key);
	}

	protected function optionSet(int $key): void
	{
		$this->options |= $key;
	}

	/**
	 * Initialise our Session
	 *
	 * @param int $type
	 * @param SocketClient $client
	 * @param NodeModel|null $o
	 * @return int
	 * @throws Exception
	 */
	public function session(int $type,SocketClient $client,NodeModel $o=NULL): int
	{
		Log::debug(sprintf('%s: + Start [%d]',__METHOD__,$type));

		// This sessions options
		$this->options = 0;
		$this->session = 0;

		// Our files that we are sending/receive
		$this->send = new Send;
		$this->recv = new Receive;

		if ($o) {
			// Our configuration and initialise values
			$this->setup = Setup::findOrFail(self::setup);

			// The node we are communicating with
			$this->node = new Node;

			$this->originate = $o->exists;

			// If we are connecting to a node
			if ($o->exists) {
				$this->node->originate($o);

			} else {
				$this->optionSet(self::O_INB);
			}
		}

		// We are an IP node
		$this->optionSet(self::O_TCP);
		$this->setClient($client);

		switch ($type) {
			/** @noinspection PhpMissingBreakStatementInspection */
			case self::SESSION_AUTO:
				Log::debug(sprintf('%s:   - Trying EMSI',__METHOD__));

				$rc = $this->protocol_init();
				if ($rc < 0) {
					Log::error(sprintf('%s:   ! Unable to start EMSI [%d]',__METHOD__,$rc));

					return self::S_REDIAL | self::S_ADDTRY;
				}

			case self::SESSION_EMSI:
				Log::debug(sprintf('%s:   - Starting EMSI',__METHOD__));
				$rc = $this->protocol_session();

				break;

			case self::SESSION_BINKP:
				Log::debug(sprintf('%s:   - Starting BINKP',__METHOD__));
				$rc = $this->protocol_session();

				break;

			case self::SESSION_ZMODEM:
				Log::debug(sprintf('%s:   - Starting ZMODEM',__METHOD__));
				$this->client->speed = SocketClient::TCP_SPEED;
				$this->originate = FALSE;

				// @todo While Debugging
				$this->send->add('/tmp/aa');

				return $this->protocol_session();

			default:
				Log::error(sprintf('%s:   ! Unsupported session type [%d]',__METHOD__,$type));

				return self::S_REDIAL | self::S_ADDTRY;
		}

		// @todo Unlock outbounds

		// @todo These flags determine when we connect to the remote.
		// If the remote indicated that they dont support file requests (NRQ) or temporarily hold them (HRQ)
		if (($this->node->optionGet(self::O_NRQ) && (! $this->setup->ignore_nrq)) || $this->node->optionGet(self::O_HRQ))
			$rc |= self::S_HOLDR;

		if ($this->optionGet(self::O_HXT))
			$rc |= self::S_HOLDX;

		if ($this->optionGet(self::O_HAT))
			$rc |= self::S_HOLDA;

		Log::info(sprintf('%s:   Total: %s - %d:%02d:%02d online, (%d) %lu%s sent, (%d) %lu%s received - %s',
			__METHOD__,
			$this->node->ftn,
			$this->node->session_time/3600,
			$this->node->session_time%3600/60,
			$this->node->session_time%60,
			$this->send->total_sent,$this->send->total_sent_bytes,'b',
			$this->recv->total_recv,$this->recv->total_recv_bytes,'b',
			(($rc & self::S_MASK) == self::S_OK) ? 'Successful' : 'Failed',
		));

		// @todo Log to history log in the DB.
		//if ($this->node->start_time && $this->setup->cfg('CFG_HISTORY')) {}

		// @todo Optional after session execution event
		// if ($this->node->start_time && $this->setup->cfg('CFG_AFTERSESSION')) {}

		// @todo Optional after session includes mail event
		// if ($this->node->start_time && $this->setup->cfg('CFG_AFTERMAIL')) {}

		return ($rc & ~self::S_ADDTRY);
	}

	/**
	 * Clear a session bit
	 *
	 * @param int $key
	 */
	protected function sessionClear(int $key): void
	{
		$this->session &= ~$key;
	}

	/**
	 * Get a session bit
	 * @param int $key
	 * @return bool
	 */
	protected function sessionGet(int $key): bool
	{
		return ($this->session & $key);
	}

	/**
	 * Set a session bit (with SE_*)
	 *
	 * @param int $key
	 */
	protected function sessionSet(int $key): void
	{
		$this->session |= $key;
	}

	/**
	 * Set our client that we are communicating with
	 *
	 * @param SocketClient $client
	 */
	private function setClient(SocketClient $client): void
	{
		$this->client = $client;
	}
}