<?php

namespace App\Classes\Protocol;

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

use App\Classes\Protocol as BaseProtocol;
use App\Classes\Sock\SocketClient;
use App\Classes\Sock\SocketException;
use App\Models\{Address,Setup};
use App\Interfaces\CRC as CRCInterface;
use App\Interfaces\Zmodem as ZmodemInterface;
use App\Traits\CRC as CRCTrait;

// http://ftsc.org/docs/fsc-0056.001
// http://ftsc.org/docs/fsc-0088.001

final class EMSI extends BaseProtocol implements CRCInterface,ZmodemInterface
{
	private const LOGKEY = 'PE-';

	use CRCTrait;

	private const EMSI_BEG			= '**EMSI_';
	private const EMSI_ARGUS1		= '-PZT8AF6-';
	private const EMSI_DAT			= self::EMSI_BEG.'DAT';
	private const EMSI_REQ			= self::EMSI_BEG.'REQA77E';
	private const EMSI_INQ			= self::EMSI_BEG.'INQC816';
	private const EMSI_ACK			= self::EMSI_BEG.'ACKA490';
	private const EMSI_NAK			= self::EMSI_BEG.'NAKEEC3';
	private const EMSI_HBT			= self::EMSI_BEG.'HBTEAEE';

	private const CR				= "\r";
	private const NL				= "\n";
	private const DEL				= "\x08";

	private const EMSI_BUF			= 8192;
	private const TMP_LEN			= 1024;

	private const SM_INBOUND		= 0;
	private const SM_OUTBOUND		= 1;

	private const EMSI_HSTIMEOUT	= 60;						/* Handshake timeout */
	private const EMSI_SEQ_LEN		= 14;
	private const EMSI_LOG_IN		= 0;
	private const EMSI_LOG_OUT		= 1;

	/* FREQs flags */
	private const FR_NOTHANDLED		= (-1);
	private const FR_NOTAVAILABLE	= 0;
	private const FR_AVAILABLE		= 1;

	private const EMSI_RESEND_TO	= 5;

	// Our session status
	private int $session;

	// Protocols we support in order
	// @todo This should be a config item
	private array $protocols = [
		//'4'=>self::P_HYDRA4,
		//'8'=>self::P_HYDRA8,
		//'6'=>self::P_HYDRA16,
		//'H'=>self::P_HYDRA,
		//'J'=>self::P_JANUS,
		//'D'=>self::P_DIRZAP,
		//'Z'=>self::P_ZEDZAP,
		'1'=>self::P_ZMODEM,
	];

	/**
	 * Incoming EMSI session
	 *
	 * @param SocketClient $client
	 * @return int|null
	 * @throws SocketException
	 * @throws Exception
	 */
	public function onConnect(SocketClient $client): ?int
	{
		// If our parent returns a PID, we've forked
		if (! parent::onConnect($client)) {
			Log::withContext(['pid'=>getmypid()]);

			$this->session(self::SESSION_AUTO,$client,(new Address));
			$this->client->close();

			Log::info(sprintf('%s:= onConnect - Connection closed [%s]',self::LOGKEY,$client->address_remote));
			exit(0);
		}

		return NULL;
	}

	/**
	 * Send our welcome banner
	 *
	 * @throws Exception
	 */
	private function emsi_banner(): void
	{
		Log::debug(sprintf('%s:+ emsi_banner',self::LOGKEY));

		$banner = 'This is a mail only system - unless you are a mailer, you should disconnect :)';
		$this->client->buffer_add(self::EMSI_REQ.str_repeat(self::DEL,strlen(self::EMSI_REQ)).$banner.self::CR);
		$this->client->buffer_flush(5);
	}

	/**
	 * Create the EMSI_DAT
	 *
	 * @return string
	 */
	private function emsi_makedat(): string
	{
		$makedata = sprintf('%s0000',self::EMSI_DAT);

		/*
		 * Link Codes
		 *
		 * Link codes is a string of flags that specify desired connect conditions. These codes are separated by commas.
		 * New codes may be added with prior approval from the author of this document.
		 *
		 * Calling system options:
         * PUA        Pickup mail for all presented addresses.
         * PUP        Pickup mail for primary address only.
         * NPU        No mail pickup desired.
		 *
		 * Answering system options:
         * HAT        Hold ALL traffic.
         * HXT        Hold compressed mail traffic.
         * HRQ        Hold file requests (not processed at this time).
		 */
		$link_codes = $this->originate ? ['8N1','PUA'] : ['8N1'];

		/*
		 * Compatibility codes
		 *
		 * The calling system must list supported protocols first and descending order of preference (the most desirable
		 * protocol should be listed first). The answering system should only present one protocol and it should be the
		 * first item in the compatibility_codes field.
		 *
		 *  Protocols
        -----------------------------------------------------------------
        DZA*    DirectZAP               (Zmodem variant, reduced escape set).
		TZA		DirectZap               (TrapDoor DirectZap varient)
        ZAP     ZedZap                  (Zmodem variant, upe 8K blocks).
        ZMO**   Zmodem w/1,024 packets  (Wazoo ZedZip)
        JAN     Janus bi-directional.
        KER     Kermit.
		HYD		Hydra bi-directional    (link flags define parameters)
		SLK		SeaLink					(no TYSNC, No MDM7, No TeLink)
		CHT		Chat?

        Other codes
        -----------------------------------------------------------------
        NCP     No compatible protocols (failure).
        NRQ     No file requests accepted by this system. (IE: capability not implemented)
		FRQ		Node accepts and will process file rquests.
        ARC     ARCmail 0.60-capable, as defined by the FTSC.
        XMA     Supports other forms of compressed mail.
        FNC     Filename conversion. This indicates that any transmitted
                files must follow the MS-DOS restrictions of an eight
                character file name followed by a three character
                extension; eg. FILENAME.EXT
		DFB		Indicates  that  the  system  presenting  is  capabable of fall-back to
    			FTS1/WAZOO  negotiation  in the case of failure of EMSI handshake or no
    			common  protocol.

		Link Session options:
		-----------------------------------------------------------------
		RMA		Indicates that the presenting site is able to send and process multiple
    			file  requests. If both sites present this flag, the caller will send
    			any REQ files found for each AKA presented by the answering system.
    			The answering system will process each received REQ.
		PMO     PickUp Mail (ARCmail and Packets) ONLY
	 	NFE     No TIC'S, associated files or files attachs desired
		NXP     No compressed mail pickup desired
		NRQ     File requests not accepted by caller
                This  flag is presented if file request processing
                is disabled TEMPORARILY for any reason
		 */

		// @todo We need to evaluate what the remote presents
		$compat_codes = $this->originate ? ['ZMO','ARC','XMA'] : ['ZMO'];

		// Site address, password and compatibility
		$makedata .= sprintf('{EMSI}{%s}{%s}{%s}{%s}',
			$this->our_addresses()->pluck('ftn')->join(' '),
			$this->node->password == '-' ? '' : $this->node->password,
			join(',',$link_codes),
			join(',',$compat_codes),
		);

		// Mailer Details
		$makedata .= sprintf('{%s}{%s}{%s}{%s}',
			Setup::product_id(),
			config('app.name'),
			$this->setup->version,
			'#000000'					// Serial Numbers
		);

		// System Details
		$makedata .= sprintf('{IDENT}{[%s][%s][%s][-Unpublished-][38400][%s]}',
			$this->setup->system_name,
			$this->setup->location,
			$this->setup->sysop,
			'XA'						// Nodelist Flags
		);

		$makedata .= sprintf('{TRAF}{%lX %lX}',$this->send->mail_size,$this->send->file_size);

		// @todo Not sure what MOH is for
		//$makedata .= sprintf('{MOH#}{[%lX]}',0);

		$makedata .= sprintf('{TRX#}{[%lX]}',Carbon::now()->timestamp);

		$makedata .= sprintf('{TZUTC}{[%+03d%02d]}',10,0);

		// @todo Not sure what OHFR is for
		//$makedata .= sprintf('{OHFR}{%s}','Never Never');

		/* Calculate emsi length */
		$makedata = preg_replace('/0000/',sprintf('%04X',strlen($makedata)-14),$makedata,1);

		/* EMSI crc16 */
		$makedata .= sprintf('%04X',crc16(substr($makedata,2)));

		return $makedata;
	}

	/**
	 * Parse the EMSI dat string and return chunks
	 *
	 * @param string $str
	 * @param int $x
	 * @param string $needle
	 * @return string
	 */
	private function emsi_dat_parse(string $str,int &$x,string $needle='}'): string
	{
		$y = $x;
		$t = strpos($str,$needle,$x);
		$x = $t+2;

		return substr($str,$y,$t-$y);
	}

	/**
	 * Parse the received EMSI_DAT for remote system details
	 *
	 * @param string $str
	 * @return int
	 * @throws Exception
	 */
	private function emsi_parsedat(string $str): int
	{
		if ($this->DEBUG)
			Log::debug(sprintf('%s:+ emsi_parsedat',self::LOGKEY));

		$l = 0;

		if (! ($str=strstr($str,self::EMSI_DAT))) {
			Log::error(sprintf('%s:! No EMSI_DAT signature found?',self::LOGKEY));

			return 0;
		}

		// Get our EMSI_DAT length
		sscanf(substr($str,10),"%04X",$l);

		/* Bad EMSI length */
		if ($l != ($x=strlen($str)-18)) {
			Log::error(sprintf('%s:! Bad EMSI_DAT length: [%u], should be: [%u]!',self::LOGKEY,$l,$x));

			return 0;
		}

		// Check the CRC16 checksum
		sscanf(substr($str,strlen($str)-4),"%04X",$l);

		/* Bad EMSI CRC */
		if ($l != ($x = crc16(substr($str,2,strlen($str)-6)))) {
			Log::error(sprintf('%s:! Bad EMSI_DAT CRC: [%04X], should be: [%04X]!',self::LOGKEY,$l,$x));

			return 0;
		}

		/* No EMSI ident */
		if (strncmp(substr($str,14),"{EMSI}",6)) {
			Log::error(sprintf('%s:! No EMSI fingerprint?',self::LOGKEY));

			return 0;
		}

		/* {AKAs} */
		$x = 21;
		foreach (explode(' ',$this->emsi_dat_parse($str,$x)) as $rem_aka) {
			Log::debug(sprintf('%s: - Parsing AKA [%s]',self::LOGKEY,$rem_aka));

			try {
				if (! ($o = Address::findFTN($rem_aka))) {
					Log::debug(sprintf('%s: ? AKA is UNKNOWN [%s]',self::LOGKEY,$rem_aka));
					continue;
				}

			} catch (Exception) {
				Log::error(sprintf('%s: ! AKA is INVALID [%s]',self::LOGKEY,$rem_aka));

				continue;
			}

			// Check if the remote has our AKA
			if ($this->setup->system->addresses->pluck('ftn')->search($o->ftn) !== FALSE) {
				Log::error(sprintf('%s: ! AKA is OURS [%s]',self::LOGKEY,$o->ftn));

				continue;
			}

			// @todo lock nodes
			Log::info(sprintf('%s: - Remote AKA [%s]',self::LOGKEY,$o->ftn));
			$this->node->ftn = $o;
		}

		if ($this->originate AND ! $this->node->originate_check()) {
			Log::error(sprintf('%s: ! We didnt get who we called?',self::LOGKEY));

			return self::S_FAILURE|self::S_ADDTRY;
		}

		// By definition, if we are in the DB, we are nodelisted
		if ($this->node->aka_num)
			$this->node->optionSet(self::O_LST);

		/* Password */
		$p = $this->emsi_dat_parse($str,$x);
		if ($this->originate) {
			$c = ($p === $this->node->password);

		} else {
			$c = $this->node->auth($p);
		}

		if (! $c) {
			Log::info(sprintf('%s: - Remote has password [%s] on us, and we expect [%s]',self::LOGKEY,$p,$this->node->password));

			if ($p || $this->node->password)
				$this->node->optionSet(self::O_BAD);

		} else {
			$this->node->optionSet(self::O_PWD);
			Log::info(sprintf('%s: - Remote Authed [%d] AKAs',self::LOGKEY,$c));
		}

		/* Link codes */
		Log::notice(sprintf('%s: - Remote Link Codes [%s]',self::LOGKEY,$this->emsi_dat_parse($str,$x)));

		/* Compatibility codes */
		$codes = $this->emsi_dat_parse($str,$x);
		if ($codes)
			foreach (explode(',',$codes) as $code) {
				switch ($code) {
					case 'ARC':
						Log::debug(sprintf('%s: = Node accepts ARC mail bundle (ARC)',self::LOGKEY));
						break;

					case 'NRQ':
						Log::debug(sprintf('%s: = No file requests accepted by this system (NRQ)',self::LOGKEY));
						$this->node->optionSet(self::O_NRQ);
						break;

					case 'XMA':
						Log::debug(sprintf('%s: = Node supports other forms of compressed mail (XMA)',self::LOGKEY));
						break;

					case 'ZAP':
						Log::debug(sprintf('%s: = Remote wants ZEDZAP',self::LOGKEY));
						$this->node->optionSet(self::P_ZEDZAP);
						break;

					case 'ZMO':
						Log::debug(sprintf('%s: = Remote wants ZMODEM',self::LOGKEY));
						$this->node->optionSet(self::P_ZMODEM);
						break;

					default:
						Log::info(sprintf('%s: = Ignoring unknown option: [%s] ',self::LOGKEY,$code));
				}
			}

		/* Mailer code */
		Log::notice(sprintf('%s: - Remote Mailer Code [%s]',self::LOGKEY,$this->emsi_dat_parse($str,$x))); // hex

		/* Mailer name */
		Log::notice(sprintf('%s: - Remote Mailer [%s]',self::LOGKEY,$this->emsi_dat_parse($str,$x)));

		/* Mailer version */
		Log::notice(sprintf('%s: - Remote Mailer Version [%s]',self::LOGKEY,$this->emsi_dat_parse($str,$x)));

		/* Mailer serial number */
		Log::notice(sprintf('%s: - Remote Mailer Serial Number [%s]',self::LOGKEY,$this->emsi_dat_parse($str,$x)));

		while ($t=strpos($str,'}',$x)) {
			$p = substr($str,$x,$t-$x);
			$t++;			// End of this field

			switch ($p) {
				// {IDENT}{[]}
				case 'IDENT':
					/* System name */
					$x = $t+2;
					Log::notice(sprintf('%s: - Remote System [%s]',self::LOGKEY,$this->emsi_dat_parse($str,$x,']')));

					/* System location */
					Log::notice(sprintf('%s: - Remote Location [%s]',self::LOGKEY,$this->emsi_dat_parse($str,$x,']')));

					/* Operator name */
					Log::notice(sprintf('%s: - Remote Operator [%s]',self::LOGKEY,$this->emsi_dat_parse($str,$x,']')));

					/* Phone */
					Log::notice(sprintf('%s: - Remote Phone Number [%s]',self::LOGKEY,$this->emsi_dat_parse($str,$x,']')));

					/* Baud rate */
					$this->client->speed = $this->emsi_dat_parse($str,$x,']');

					/* Flags */
					Log::notice(sprintf('%s: - Remote Flags [%s]',self::LOGKEY,$this->emsi_dat_parse($str,$x,']')));
					$x++;

					break;

				// {TRAF}{}
				case 'TRAF':
					$x = $t+1;
					Log::notice(sprintf('%s: - Remote TRAF [%s]',self::LOGKEY,$this->emsi_dat_parse($str,$x)));

					break;

				// {OHFR}{}
				case 'OHFR':
					$x = $t+1;
					Log::notice(sprintf('%s: - Remote OHFR [%s]',self::LOGKEY,$this->emsi_dat_parse($str,$x)));

					break;

				// {MOH#}{[]}
				case 'MOH#':
					$x = $t+2;
					Log::notice(sprintf('%s: - Remote MOH# [%s]',self::LOGKEY,$this->emsi_dat_parse($str,$x,']')));
					$x++;

					break;

				// {TRX#}{[]}
				case 'TRX#':
					$x = $t+2;
					Log::notice(sprintf('%s: - Remote TRX [%s]',self::LOGKEY,$this->emsi_dat_parse($str,$x,']')));
					$x++;

					break;

				// {TZUTC}{[]}
				case 'TZUTC':
					$x = $t+2;
					Log::notice(sprintf('%s: - Remote TZUTC [%s]',self::LOGKEY,$this->emsi_dat_parse($str,$x,']')));
					$x++;

					break;

				default:
					$x = $t+1;
					Log::notice(sprintf('%s: - Remote UNKNOWN [%s] (%s)',self::LOGKEY,$this->emsi_dat_parse($str,$x),$p));
			}
		}

		return 1;
	}

	/**
	 * STEP 2A, RECEIVE EMSI HANDSHAKE
	 *
	 * @throws SocketException
	 * @throws Exception
	 */
	private function emsi_recv(int $mode): int
	{
		Log::debug(sprintf('%s:+ emsi_recv',self::LOGKEY));

		Log::debug(sprintf('%s: - STEP 1',self::LOGKEY));
		/*
		 * Step 1
		 * +-+------------------------------------------------------------------+
		 * :1: Tries=0, T1=20 seconds, T2=60 seconds							:
		 * +-+------------------------------------------------------------------+
		 */

		$p = '';
		$tries = 0;
		$t1 = $this->client->timer_set(20);
		$t2 = $this->client->timer_set(self::EMSI_HSTIMEOUT);

		do {
		step2:
			Log::debug(sprintf('%s: - STEP 2',self::LOGKEY));
			/* Step 2
			+-+------------------------------------------------------------------+
			:2: Increment Tries													 :
			: :																	 :
			: : Tries>6?		  Terminate, and report failure.				 :
			: +------------------------------------------------------------------+
			: : Are we answering system?	  Transmit EMSI_REQ, go to step 3.   :
			: +------------------------------------------------------------------+
			: : Tries>1?					  Transmit EMSI_NAK, go to step 3.   :
			: +------------------------------------------------------------------+
			: : Go to step 4.													 :
			+-+------------------------------------------------------------------+
			*/

			if (++$tries > 6)
				return self::TIMEOUT;

			if ($mode == self::SM_INBOUND) {
				$this->client->buffer_add(self::EMSI_REQ.self::CR);

			} elseif ($tries > 1) {
				$this->client->buffer_add(self::EMSI_NAK.self::CR);

			} else {
				goto step4;
			}

			$this->client->buffer_flush(5);

		step3:
			Log::debug(sprintf('%s: - STEP 3',self::LOGKEY));
			/* Step 3
			 * +-+------------------------------------------------------------------+
			 * :3: T1=20 seconds													:
			 * +-+------------------------------------------------------------------+
			 */

			$t1 = $this->client->timer_set(20);

		step4:
			Log::debug(sprintf('%s: - STEP 4',self::LOGKEY));
			/* Step 4
			+-+------------------------------------------------------------------+
			:4: Wait for EMSI sequence until EMSI_HBT or EMSI_DAT or any of the  :
			: : timers have expired.											 :
			: :																	 :
			: : If T2 has expired, terminate call and report failure.			 :
			: +------------------------------------------------------------------+
			: : If T1 has expired, go to step 2.								 :
			: +------------------------------------------------------------------+
			: : If EMSI_HBT received, go to step 3.								 :
			: +------------------------------------------------------------------+
			: : If EMSI_DAT received, go to step 5.								 :
			: +------------------------------------------------------------------+
			: : Go to step 4.													 :
			+-+------------------------------------------------------------------+
			*/

			$got = 0;

			while (TRUE) {
				$ch = $this->client->read_ch(max( 1,min($this->client->timer_rest($t1),$this->client->timer_rest($t2))));
				///Log::debug(sprintf('%s:    - Got [%x]{%d} (%c)',__METHOD__,$ch,$ch,$ch));

				if (($ch != self::TIMEOUT) && ($ch < 0))
					return $ch;

				if ($this->client->timer_expired($t2))
					return self::TIMEOUT;

				/* goto step2; */
				if ($this->client->timer_expired($t1))
					break;

				if ($ch == self::TIMEOUT)
					continue;

				if (! $got) {
					if ($ch == ord('*'))
						$got = 1;
					else
						continue;
				}

				if (($ch == ord(self::CR)) || ($ch == ord(self::NL))) {
					if (! strncmp($p,self::EMSI_HBT,self::EMSI_SEQ_LEN)) {
						Log::debug(sprintf('%s: - Received EMSI_HBT',self::LOGKEY));

						goto step3;
					}

					if (! strncmp($p,self::EMSI_DAT,10)) {
						Log::debug(sprintf('%s:- Received EMSI_DAT',self::LOGKEY));

						Log::debug(sprintf('%s: - STEP 5',self::LOGKEY));
						/* Step 5
						+-+------------------------------------------------------------------+
						:5: Receive EMSI_DAT packet											 :
						: +------------------------------------------------------------------+
						: : Packet received OK?				Transmit EMSI_ACK twice, and	 :
						: :									go to step 6.					 :
						: +------------------------------------------------------------------+
						: : Go to step 2.													 :
						+-+------------------------------------------------------------------+
						*/

						$ch = $this->emsi_parsedat($p);
						if ($ch) {
							$this->client->buffer_add(self::EMSI_ACK.self::CR);
							$this->client->buffer_add(self::EMSI_ACK.self::CR);
							$this->client->buffer_flush(5);

							Log::debug(sprintf('%s: - STEP 6',self::LOGKEY));
							/* Step 6
							+-+------------------------------------------------------------------+
							:6: Received EMSI_DAT packet OK, exit.								 :
							+-+------------------------------------------------------------------+
							*/

							return self::OK;

						} else {
							Log::debug(sprintf('%s: - EMSI_DAT didnt parse',self::LOGKEY));

							goto step2;
						}
					}

					$p = '';// Clear our EMSI dat since the return is the end of a transmission and its not what we want.
					goto step4;

				} else {
					if (strlen($p) >= self::EMSI_BUF) {
						Log::warning(sprintf('%s: ! EMSI_DAT packet too long.',self::LOGKEY));

						$rew = strstr($p,self::EMSI_BEG,TRUE);
						if ($rew && $rew != $p) {
							Log::notice(sprintf('%s: - Got EMSI_DAT at offset [%d].',self::LOGKEY,strlen($rew)));

							$p = substr($p,strlen($rew));
						}
					}

					if ($ch > 31 && $ch <= 255)
						$p .= chr($ch);
				}
			}

		} while(! $this->client->timer_expired($t2));

		return self::TIMEOUT;
	}

	/**
	 * STEP 2B, TRANSMIT EMSI HANDSHAKE
	 *
	 * @throws SocketException
	 * @throws Exception
	 */
	private function emsi_send(): int
	{
		Log::debug(sprintf('%s:+ emsi_send',self::LOGKEY));

		Log::debug(sprintf('%s: - STEP 1',self::LOGKEY));
		/* Step 1
		+-+------------------------------------------------------------------+
		:1: Tries=0, T1=60 seconds					   :
		+-+------------------------------------------------------------------+
		*/

		$p = '';
		$tries = 0;
		$t1 = $this->client->timer_set(self::EMSI_HSTIMEOUT);

	step2:
		Log::debug(sprintf('%s: - STEP 2',self::LOGKEY));
		/* Step 2
		+-+------------------------------------------------------------------+
		:2: Transmit EMSI_DAT packet and increment Tries		 :
		: :								  :
		: +------------------------------------------------------------------+
		: : Tries>6?						Terminate, and report failure.   :
		: +------------------------------------------------------------------+
		: : Go to step 3.													:
		+-+------------------------------------------------------------------+
		*/
		if (++$tries > 6)
			return self::TIMEOUT;

		$this->client->buffer_add($this->emsi_makedat().self::CR);
		$this->client->buffer_flush(5);

		/* Step 3
		+-+------------------------------------------------------------------+
		:3: T2=20 seconds													:
		+-+------------------------------------------------------------------+
		*/
		Log::debug(sprintf('%s: - STEP 3',self::LOGKEY));
		$t2 = $this->client->timer_set(20);

		/* Step 4
		+-+------------------------------------------------------------------+
		:4: Wait for EMSI sequence until T1 has expired			  :
		: :								  :
		: : If T1 has expired, terminate call and report failure.			:
		: +------------------------------------------------------------------+
		: : If T2 has expired, go to step 2.				 :
		: +------------------------------------------------------------------+
		: : If EMSI_REQ received, go to step 4.				  :
		: +------------------------------------------------------------------+
		: : If EMSI_ACK received, go to step 5.				  :
		: +------------------------------------------------------------------+
		: : If any other sequence received, go to step 2.					:
		+-+------------------------------------------------------------------+
		*/
		Log::debug(sprintf('%s: - STEP 4',self::LOGKEY));
		while(! $this->client->timer_expired($t1)) {
			$ch = $this->client->read_ch(max( 1,min($this->client->timer_rest($t1),$this->client->timer_rest($t2))));
			// Log::debug(sprintf('%s:   - Got (%x) {%03d} (%c)',__METHOD__,$ch,$ch,$ch));

			if (($ch != self::TIMEOUT) && ($ch < 0))
				return $ch;

			if ($this->client->timer_expired($t2))
				goto step2;

			if ($this->client->timer_expired($t1))
				return self::TIMEOUT;

			if ($ch == self::TIMEOUT)
				continue;

			$ch &= 0x7f;

			if (($ch == ord(self::CR)) || ($ch == ord(self::NL))) {
				if (! $p)
					continue;

				if (! strncmp($p,self::EMSI_DAT,10)) {
					Log::warning(sprintf('%s: - Got unexpected EMSI_DAT - Argus?',self::LOGKEY));
					$this->client->buffer_add(self::EMSI_ACK);
					$this->client->buffer_add(self::EMSI_ACK);
					$this->client->buffer_flush(1);

					$t2 = $this->client->timer_set($this->client->timer_rest($t2) >> 2);

				} else if (! strncmp($p,self::EMSI_REQ,self::EMSI_SEQ_LEN)) {
					Log::notice(sprintf('%s: - Got EMSI_REQ - skipping...',self::LOGKEY));

				} else if (! strncmp($p,self::EMSI_ACK,self::EMSI_SEQ_LEN)) {
					Log::debug(sprintf('%s: - Got EMSI_ACK',self::LOGKEY));

					Log::debug(sprintf('%s: - STEP 5',self::LOGKEY));
					/* Step 5
					+-+------------------------------------------------------------------+
					:5: Received EMSI_ACK, exit.										 :
					+-+------------------------------------------------------------------+
					*/

					return self::OK;
				}

				$p = '';
				continue;
			}

			/* Put new symbol in buffer */
			if ($ch > 31) {
				if (strlen($p) < self::TMP_LEN) {
					$p .= chr($ch);

				} else {
					Log::warning(sprintf('%s: ! EMSI packet too long',self::LOGKEY));
				}
			}
		} /* goto step4; */

		return self::TIMEOUT;
	}

	private function is_freq_available(): int
	{
		return self::FR_NOTAVAILABLE; // @todo
		/*
		if (! cfgs(self::CFG_EXTRP ) && ! cfgs(self::CFG_SRIFRP)) {
			return self::FR_NOTHANDLED;
		}

		return ((cfgs(self::CFG_EXTRP) || cfgs(self::CFG_SRIFRP)) && checktimegaps(cfgs(self::CFG_FREQTIME)));
		*/
	}

	/**
	 * STEP 1, EMSI INIT
	 *
	 * @throws SocketException
	 * @throws Exception
	 */
	protected function protocol_init(): int
	{
		if ($this->DEBUG)
			Log::debug(sprintf('%s:+ protocol_init',self::LOGKEY));

		$got = 0;
		$tries = 0;
		$p = '';

		if ($this->originate) {
			$gotreq = 0;

			// Send a character to get a response from the remote
			$t1 = $this->client->timer_set(self::EMSI_HSTIMEOUT);
			do {
				$this->client->send(ord(self::CR),1);

			} while (! $this->client->hasData(1) && ! $this->client->timer_expired($t1));

			if ($this->client->timer_expired($t1))
				return self::TIMEOUT;

			$t2 = $this->client->timer_set(self::EMSI_RESEND_TO);
			while (TRUE) {
				$ch = $this->client->read_ch(max( 1,min($this->client->timer_rest($t1),$this->client->timer_rest($t2))));
				if ($this->DEBUG)
					Log::debug(sprintf('%s: - Got [%x] (%c)',self::LOGKEY,$ch,$ch));

				if (($ch != self::TIMEOUT) && ($ch < 0))
					return $ch;

				if ($this->client->timer_expired($t1))
					return self::TIMEOUT;

				if ($this->client->timer_expired($t2)) {
					if ($this->setup->do_prevent && $tries == 0) {
						$this->setup->do_prevent = 0;

						$this->client->buffer_add(self::EMSI_INQ.self::CR);
						$this->client->buffer_flush(5);

					} else {
						if (++$tries > 10)
							return self::TIMEOUT;

						Log::debug(sprintf('%s: - Sending EMSI_INQ (Try %d of 10)...',self::LOGKEY,$tries));
						$this->client->buffer_add(self::EMSI_INQ.self::CR);
					}

					$t2 = $this->client->timer_set(self::EMSI_RESEND_TO);
					continue;
				}

				if ($ch == self::TIMEOUT)
					continue;

				$ch &= 0x7f;

				if (($ch == ord(self::CR)) || ($ch == ord(self::NL))) {
					if (strstr($p,self::EMSI_REQ)) {
						Log::info(sprintf('%s: - Got EMSI_REQ',self::LOGKEY));
						if ($gotreq++)
							return self::OK;

						$this->client->buffer_add(self::EMSI_INQ.self::CR);
						$this->client->buffer_flush(5);

					} elseif ($p && strstr($p,self::EMSI_BEG) && strstr($p,self::EMSI_ARGUS1)) {
						Log::info(sprintf('%s: - Got Intro [%s]',self::LOGKEY,$p));
					}

					continue;
				}

				if ($ch > 31)
					$p .= chr($ch);

				if (strlen($p) >= self::EMSI_BUF)
					return self::ERROR;
			}
		}

		$this->client->rx_purge();
		$this->client->buffer_clear();
		$this->emsi_banner();

		$t1 = $this->client->timer_set(self::EMSI_HSTIMEOUT);
		$t2 = $this->client->timer_set(self::EMSI_RESEND_TO);

		while (! $this->client->timer_expired($t1)) {
			$ch = $this->client->read_ch(max( 1,min($this->client->timer_rest($t1),$this->client->timer_rest($t2))));
			if ($this->DEBUG)
				Log::debug(sprintf('%s: - Got [%x] (%c)',self::LOGKEY,$ch,$ch));

			if (($ch != self::TIMEOUT) && ($ch < 0))
				return $ch;

			if ($this->client->timer_expired($t1))
				return self::TIMEOUT;

			if (($ch == self::TIMEOUT) && $this->client->timer_expired($t2)) {
				if (! $got) {
					$this->emsi_banner();
					$t2 = $this->client->timer_set(self::EMSI_RESEND_TO);

				} else {
					$t2 = $t1;
				}

				continue;
			}

			$ch &= 0x7f;

			if ((! $got) && ($ch == ord('*')))
				$got = 1;

			if ($got && (($ch == ord(self::CR)) || ($ch == ord(self::NL)))) {
				$got = 0;

				if (strstr($p, self::EMSI_INQ)) {
					Log::info(sprintf('%s: - Got EMSI_REQ',self::LOGKEY));

					return self::OK;
				}

			} else {
				if ($got && ($ch > 31))
					$p .= chr($ch);

				if (strlen($p) >= self::EMSI_BUF)
					return self::ERROR;
			}
		}

		return self::TIMEOUT;
	}

	/**
	 * Setup our EMSI session
	 *
	 * @return int
	 * @throws Exception
	 */
	protected function protocol_session(): int
	{
		Log::debug(sprintf('%s:+ protocol_session',self::LOGKEY));

		$was_req = 0;
		$got_req = 0;

		// Outbound session
		if ($this->originate) {
			$this->optionSet(self::O_PUA);
			//$emsi_lo |= ($this->is_freq_available() <= self::FR_NOTAVAILABLE ) ? self::O_NRQ : $emsi_lo;

			if ($this->emsi_send() < 0)
				return (self::S_REDIAL|self::S_ADDTRY);

			$rc = $this->emsi_recv(self::SM_OUTBOUND);

			if ($rc < 0)
				return (self::S_REDIAL|self::S_ADDTRY);

			Log::info(sprintf('%s: - Starting outbound EMSI session to [%s]',self::LOGKEY,$this->client->address_remote));

		// Inbound session
		} else {
			$rc = $this->emsi_recv(self::SM_INBOUND);

			if ($rc < 0) {
				Log::error(sprintf('%s:! Unable to establish EMSI session',self::LOGKEY));

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

			Log::info(sprintf('%s: - Starting inbound EMSI session from [%s]',self::LOGKEY,$this->client->address_remote));

			if ($this->node->aka_authed) {
				$xproto = $this->is_freq_available();

				if ($xproto == self::FR_NOTHANDLED || $xproto == self::FR_NOTAVAILABLE)
					$this->node->optionSet(self::O_HRQ);
			}

			foreach ($this->protocols as $p => $key) {
				if ($this->node->optionGet($key)) {
					Log::debug(sprintf('%s: - Remote supports [%s] (%x)',self::LOGKEY,$p,$key));
					$this->optionSet($key);
				}
			}

			// Disable chat
			//$this->node->optionClear(self::MO_CHAT);

			if (! $this->protocols)
				$this->optionSet(self::P_NCP);

			if ($this->emsi_send() < 0)
				return (self::S_REDIAL|self::S_ADDTRY);
		}

		// @todo Lock Node AKAs

		Log::info(sprintf('%s: - We have %lu%s mail, %lu%s files',self::LOGKEY,$this->send->mail_size,'b',$this->send->file_size,'b'));

		$proto = $this->originate ? $this->node->optionGet(self::P_MASK) : $this->optionGet(self::P_MASK);

		switch ($proto) {
			case self::P_NONE:
			case self::P_NCP:
				Log::error(sprintf('%s:! No compatible protocols',self::LOGKEY));

				return self::S_FAILURE;

			case self::P_ZMODEM:
				$t = 'ZModem-1k';
				break;

			case self::P_ZEDZAP:
				$t = 'ZedZap';
				break;

			case self::P_DIRZAP:
				$t = 'DirZap';
				break;

			case self::P_HYDRA4:
				$t = 'Hydra-4k';
				break;

			case self::P_HYDRA8:
				$t = 'Hydra-8k';
				break;

			case self::P_HYDRA16:
				$t = 'Hydra-16k';
				break;

			case self::P_HYDRA:
				$t = 'Hydra';
				break;

			case self::P_JANUS:
				$t = 'Janus';
				break;

			default:
				Log::error(sprintf('%s: ? Unknown Protocol [%s]',self::LOGKEY,$proto));
				$t = 'Unknown';
		}

		$xproto = ($this->optionGet(self::O_RH1) && ($this->node->optionGet(self::O_RH1)));
		$x = (substr($t,1,1) == 'H' && $xproto ) ? 'x' : '';

		Log::info(sprintf('%s: = Using [%s]',self::LOGKEY,$t));

		Log::debug(sprintf('%s: = Options: %s%s%s%s%s%s%s%s%s%s%s',
			self::LOGKEY,$x,$t,
			($this->node->optionGet(self::O_LST))		? '/LST' : '',
			($this->node->optionGet(self::O_PWD))		? '/PWD' : '',
			($this->node->optionGet(self::O_HXT))		? '/MO': '',
			($this->node->optionGet(self::O_HAT))		? '/HAT' : '',
			($this->node->optionGet(self::O_HRQ))		? '/HRQ' : '',
			($this->node->optionGet(self::O_NRQ))		? '/NRQ' : '',
			($this->node->optionGet(self::O_FNC))		? '/FNC' : '',
			($this->node->optionGet(self::O_BAD))		? '/BAD' : '',
			($this->node->optionGet(self::MO_CHAT))	? '/CHT' : ''
		));

		//chatinit($this->rnode->opt & self::MO_CHAT ? proto : -1 );

		switch ($proto) {
			case self::P_ZEDZAP:
			case self::P_DIRZAP:
			case self::P_ZMODEM:
				$this->client->cps = 1;

				$xproto = ($proto&self::P_ZEDZAP) ? self::CZ_ZEDZAP : (($proto&self::P_DIRZAP) ? self::CZ_DIRZAP : self::CZ_ZEDZIP);

				if ($this->originate) {
					$rc = $this->wazoosend($xproto);

					if (! $rc)
						$rc = $this->wazoorecv($xproto);

					if ($got_req && ! $rc)
						$rc = $this->wazoosend($xproto);

				} else {
					$rc = $this->wazoorecv($xproto|0x0100);

					if ($rc)
						return self::S_REDIAL;

					$rc = $this->wazoosend($xproto);

					if ($was_req)
						$rc = $this->wazoorecv($xproto);
				}

				break;

			case self::P_HYDRA:
			case self::P_HYDRA4:
			case self::P_HYDRA8:
			case self::P_HYDRA16:
				switch ($proto) {
					case self::P_HYDRA:
						$rc = 1;
						break;

					case self::P_HYDRA4:
						$rc = 2;
						break;

					case self::P_HYDRA8:
						$rc = 4;
						break;

					case self::P_HYDRA16:
						$rc = 8;
						break;

					default:
						$rc = 1;
				}

				//$rc = hydra($this->originate,$rc,$xproto);
				break;

			case self::P_JANUS:
				//$rc = janus();
				break;

			default:
				return self::S_OK;
		}

		return $rc ? self::S_REDIAL : self::S_OK;
	}

	/**
	 * Receive a file with a transfer protocol
	 *
	 * @param int $zap
	 * @return bool
	 */
	private function wazoorecv(int $zap): bool
	{
		Log::debug(sprintf('%s:+ wazoorecv',self::LOGKEY));

		// @todo If the node is not defined in the DB node->address is NULL. Need to figure out how to handle those nodes.
		$rc = (new Zmodem)->zmodem_receive($this->client,$zap,$this->recv,$this->node->address);

		return ($rc == self::RCDO || $rc == self::ERROR);
	}

	/**
	 * Possibly receive something from the remote
	 *
	 * @param int $zap
	 * @return bool
	 * @throws Exception
	 */
	private function wazoosend(int $zap): bool
	{
		Log::debug(sprintf('%s:+ wazoosend [%d]',self::LOGKEY,$zap));

		// See if there is anything to add to the outbound
		// Add our mail to the queue if we have authenticated
		if ($this->node->aka_authed)
			foreach ($this->node->aka_remote_authed as $ao) {
				$this->send->mail($ao);
			}

		$z = new Zmodem;

		if (! $z->zmodem_sendinit($this->client,$zap) && $this->send->total_count)
			$z->zmodem_sendfile($this->send);

		Log::debug(sprintf('%s:- Finished sending',self::LOGKEY));
		return ($z->zmodem_senddone()<0);
	}
}