<?php

namespace App\Classes\Protocol;

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

use App\Classes\Protocol as BaseProtocol;
use App\Classes\Sock\Exception\SocketException;
use App\Classes\Sock\SocketClient;
use App\Exceptions\InvalidFTNException;
use App\Interfaces\CRC as CRCInterface;
use App\Interfaces\Zmodem as ZmodemInterface;
use App\Models\{Address,Mailer,Setup};
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;

	/* CONSTS */

	public const PORT			= 60179;
	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			= "\x0d";	// \r
	private const NL			= "\x0a";	// \n;
	private const DEL			= "\x08";

	/* FEATURES */

	/** Ignore NRQ */
	public const F_IGNORE_NRQ	= 1<<0;
	/** Send an immediate EMSI_INQ on connect */
	public const F_DO_PREVENT	= 1<<1;

	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;

	protected const MO_CHAT			= 4;

	// 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
	 */
	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(Mailer::where('name','EMSI')->singleOrFail(),$client,(new Address));
			$this->client->close();
			exit(0);
		}

		return NULL;
	}

	/**
	 * Send our welcome banner
	 *
	 * @throws \Exception
	 */
	private function emsi_banner(): void
	{
		Log::debug(sprintf('%s:+ Showing 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
	 * @throws \Exception
	 */
	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][%s][%d][%s]}',
			$this->setup->system->name,
			$this->setup->system->location,
			$this->setup->system->sysop,
			$this->setup->system->phone ?: '-Unpublished-',
			self::TCP_SPEED,
			'XA'						// Nodelist Flags
		);

		// TRAF - netmail & files size (bytes)
		$makedata .= sprintf('{TRAF}{%lX %lX}',$this->send->mail_size,$this->send->files_size);

		// MOH# - Mail On Hold - bytes waiting
		$makedata .= sprintf('{MOH#}{[%lX]}',$this->send->total_size);

		// EMD5 - MD5 unique string

		// Transaction Number (Time in local time)
		$makedata .= sprintf('{TRX#}{[%lX]}',Carbon::now()->timestamp+Carbon::now($this->node->node_timezone)->offset);

		// FREQ Time - NRQ (No Requests if this is not defined)
		$makedata .= sprintf('{OHFR}{Always! CM}');
		$makedata .= sprintf('{TZUTC}{[%+05d]}',-10*60);

		// @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 (static::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,TRUE))) {
					Log::debug(sprintf('%s: ? AKA is UNKNOWN [%s]',self::LOGKEY,$rem_aka));

					$this->node->ftn_other = $rem_aka;
					continue;

				// If we only present limited AKAs dont validate password against akas outside of the domains we present
				} elseif (is_null(our_address($o))) {
					Log::alert(sprintf('%s:/ AKA domain [%s] is not in our domain(s) [%s] - ignoring',self::LOGKEY,$o->zone->domain->name,our_address()->pluck('zone.domain.name')->unique()->join(',')));

					$this->node->ftn_other = $rem_aka;
					continue;

				} elseif (! $o->active) {
					Log::alert(sprintf('%s:/ AKA is not active [%s] - ignoring',self::LOGKEY,$rem_aka));

					$this->node->ftn_other = $rem_aka;
					continue;

				} else {
					Log::info(sprintf('%s:- Got AKA [%s]',self::LOGKEY,$rem_aka));
				}

			} catch (InvalidFTNException $e) {
				Log::error(sprintf('%s:! AKA is INVALID [%s] (%s), ignoring',self::LOGKEY,$rem_aka,$e->getMessage()));

				continue;

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

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

			// 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 receive handshake',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::error(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 transmit Handshake',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),['p'=>$p]);

				} 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 (static::DEBUG)
			Log::debug(sprintf('%s:+ Starting EMSI Protocol INIT',self::LOGKEY));

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

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

			if ($this->setup->optionGet(EMSI::F_DO_PREVENT,'emsi_options'))
				$this->capSet(EMSI::F_DO_PREVENT,self::O_YES);

			// 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 (static::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->capGet(EMSI::F_DO_PREVENT,self::O_YES) && $tries === 0) {
						$this->capSet(EMSI::F_DO_PREVENT,self::O_NO);

						$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->tx_purge();

		if ($this->down) {
			Log::info(sprintf('%s:! System down for maintenance',self::LOGKEY));
			$this->client->buffer_add(self::EMSI_NAK.'Sorry down for maintenance, call back again after a few minutes'.self::CR.self::CR);
			$this->client->buffer_flush(5);
			return -1;
		}

		$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 (static::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(bool $force_queue=FALSE): int
	{
		// @todo introduce emsi_init() to perform the job of protocol_init. Only needs to be done when we originate a session

		Log::debug(sprintf('%s:+ Starting EMSI Protocol SESSION',self::LOGKEY));
		$this->force_queue = $force_queue;

		$was_req = 0;
		$got_req = 0;

		// Outbound session
		if ($this->originate) {
			Log::debug(sprintf('%s:- Outbound session',self::LOGKEY));
			$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 {
			Log::debug(sprintf('%s:- Inbound session',self::LOGKEY));
			$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->files_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:+ Start WAZOO Receive',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,$this->force_queue);

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

		$z = NULL;

		// 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) {
				if (! $ao->validated) {
					Log::alert(sprintf('%s:! Address [%s] is not validated, so we wont bundle mail for it',self::LOGKEY,$ao->ftn));
					continue;
				}

				// Send mail
				while ($this->send->mail($ao)) {
					$z = new Zmodem;

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

				// Send files
				while ($this->send->files($ao)) {
					$z = new Zmodem;

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

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