<?php

namespace App\Classes;

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

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

abstract class Protocol
{
	// Enable extra debugging
	protected bool $DEBUG = FALSE;

	private const LOGKEY = 'P--';

	// 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 needs to be 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;

	public function __construct(Setup $o=NULL)
	{
		if ($o && ! $o->system->addresses->count())
			throw new Exception('We dont have any FTN addresses assigned');

		$this->setup = $o;
	}

	/**
	 * @throws Exception
	 */
	public function __get($key)
	{
		switch ($key) {
			case 'ls_SkipGuard':									/* double-skip protection on/off */
			case 'rxOptions':										/* Options from ZRINIT header */
			case 'socket_error':
				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':
			case 'socket_error':
				$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]',self::LOGKEY,$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;
	}

	/**
	 * Our addresses to send to the remote
	 * @return Collection
	 */
	protected function our_addresses(): Collection
	{
		if ($this->setup->optionGet(Setup::O_HIDEAKA)) {
			$addresses = collect();

			foreach (($this->originate ? $this->node->aka_remote_authed : $this->node->aka_remote) as $ao)
				$addresses = $addresses->merge($this->setup->system->match($ao->zone,Address::NODE_ZC|Address::NODE_RC|Address::NODE_NC|Address::NODE_HC|Address::NODE_ACTIVE|Address::NODE_PVT|Address::NODE_POINT));

			$addresses = $addresses->unique();

			Log::debug(sprintf('%s: - Presenting limited AKAs [%s]',self::LOGKEY,$addresses->pluck('ftn')->join(',')));

		} else {
			$addresses = $this->setup->system->addresses;

			Log::debug(sprintf('%s: - Presenting ALL our AKAs [%s]',self::LOGKEY,$addresses->pluck('ftn')->join(',')));
		}

		return $addresses;
	}

	/**
	 * Initialise our Session
	 *
	 * @param int $type
	 * @param SocketClient $client
	 * @param Address|null $o
	 * @return int
	 * @throws Exception
	 */
	public function session(int $type,SocketClient $client,Address $o=NULL): int
	{
		if ($o->exists)
			Log::withContext(['ftn'=>$o->ftn]);

		Log::debug(sprintf('%s:+ Start [%d]',self::LOGKEY,$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) {
			// 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',self::LOGKEY));

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

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

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

				break;

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

				break;

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

				return $this->protocol_session();

			default:
				Log::error(sprintf('%s: ! Unsupported session type [%d]',self::LOGKEY,$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',
			self::LOGKEY,
			$this->node->address ? $this->node->address->ftn : 'Unknown',
			$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;
	}
}