<?php

namespace App\Classes\Protocol;

use Illuminate\Support\Facades\Log;

use App\Classes\Protocol;
use App\Classes\Protocol\Zmodem as ZmodemClass;
use App\Classes\File\{Receive,Send};
use App\Classes\Sock\{SocketClient,SocketException};
use App\Interfaces\CRC as CRCInterface;
use App\Interfaces\Zmodem as ZmodemInterface;
use App\Models\Address;
use App\Traits\CRC as CRCTrait;

final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
{
	private const LOGKEY = 'Z--';

	use CRCTrait;

	/* ZModem constants */
	private const LSZ_MAXHLEN		= 16;			/* Max header information length NEVER CHANGE */
	private const LSZ_MAXATTNLEN	= 32;			/* Max length of Attn string */
	private const LSZ_MAXFRAME		= self::ZSTDERR;
	private const LSZ_WINDOW		= 1024;
	private const LSZ_BLOCKSIZE		= 1024;

	/* Special characters */
	private const ZPAD				= 0x2a; //*		/* 052 Padding character begins frames */
	private const ZDLE				= 0x18;			/* Ctrl-X ZmodemReceive escape - `ala BISYNC DLE */
	private const ZDLEE				= (self::ZDLE^0x40);	/* Escaped ZDLE as transmitted */
	private const ZDEL				= 0x7f;			/* DEL character */
	private const ZBIN				= 0x41; //A		/* Binary frame indicator (CRC-16) */
	private const ZHEX				= 0x42; //B		/* HEX frame indicator */
	private const ZBIN32			= 0x43; //C		/* Binary frame with 32 bit FCS */
	private const ZBINR32			= 0x44; //D		/* RLE packed Binary frame with 32 bit FCS */
	private const ZVBIN				= 0x61; //a		/* Binary frame indicator (CRC-16) and v.header */
	private const ZVHEX				= 0x62; //b		/* HEX frame indicator and v.header */
	private const ZVBIN32			= 0x63; //c		/* Binary frame with 32 bit FCS and v.header */
	private const ZVBINR32			= 0x64; //d		/* RLE packed Binary frame with 32 bit FCS and v.header */
	private const ZRESC				= 0x7e;			/* RLE flag/escape character */

	private const ZRQINIT			= 0;			/* Request receive init */
	private const ZRINIT			= 1;			/* Receive init */
	private const ZSINIT			= 2;			/* Send init sequence (optional) */
	private const ZACK				= 3;			/* ACK to above */
	private const ZFILE				= 4;			/* File name from sender */
	public const ZSKIP				= 5;			/* To sender: skip this file */
	private const ZNAK				= 6;			/* Last packet was garbled */
	private const ZABORT			= 7;			/* Abort batch transfers */
	private const ZFIN				= 8;			/* Finish session */
	private const ZRPOS				= 9;			/* Resume data trans at this position */
	private const ZDATA				= 10;			/* Data packet(s) follow */
	private const ZEOF				= 11;			/* End of file */
	public const ZFERR				= 12;			/* Fatal Read or Write error Detected (we use for refuse -- suspend) */
	private const ZCRC				= 13;			/* Request for file CRC and response */
	private const ZCHALLENGE		= 14;			/* Receiver's Challenge */
	private const ZCOMPL			= 15;			/* Request is complete */
	private const ZCAN				= 16;			/* Other end canned session with CAN*5 */
	private const ZFREECNT			= 17;			/* Request for free bytes on filesystem (OK, we always send unlimited) */
	private const ZCOMMAND			= 18;			/* Command from sending program (NOT SUPPORTED!) */
	private const ZSTDERR			= 19;			/* Output to standard error, data follows (NOT SUPPORTED!) */

	private const ZCRCE				= 0x68; //h		/* CRC next, frame ends, header packet follows */
	private const ZCRCG				= 0x69; //i		/* CRC next, frame continues nonstop */
	private const ZCRCQ				= 0x6a; //j		/* CRC next, frame continues, ZACK expected */
	private const ZCRCW				= 0x6b; //k		/* CRC next, ZACK expected, end of frame */
	private const ZRUB0				= 0x6c; //l		/* Translate to rubout 0177 */
	private const ZRUB1				= 0x6d; //m		/* Translate to rubout 0377 */

	/* Return codes -- pseudo-characters */
	private const LSZ_OK			= self::OK;			/* 0 */
	private const LSZ_TIMEOUT		= self::TIMEOUT;	/* -2 */
	private const LSZ_RCDO			= self::RCDO;		/* -3 */
	private const LSZ_CAN			= self::GCOUNT;		/* -4 */
	private const LSZ_ERROR			= self::ERROR;		/* -5 */
	private const LSZ_NOHEADER		= -6;
	private const LSZ_BADCRC		= -7;
	private const LSZ_XONXOFF		= -8;
	private const LSZ_CRCE			= (self::ZCRCE|0x0100);
	private const LSZ_CRCG			= (self::ZCRCG|0x0100);
	private const LSZ_CRCQ			= (self::ZCRCQ|0x0100);
	private const LSZ_CRCW			= (self::ZCRCW|0x0100);

	/* Dirty hack */
	private const LSZ_TRUSTZFINS	= 3;				/* We trust only in MANY ZFINs during initialization */

	/* Byte positions within header array */
	private const LSZ_F0			= 3;				/* First flags byte */
	private const LSZ_F1			= 2;
	private const LSZ_F2			= 1;
	private const LSZ_F3			= 0;
	private const LSZ_P0			= 0;				/* Low order 8 bits of position */
	private const LSZ_P1			= 1;
	private const LSZ_P2			= 2;
	private const LSZ_P3			= 3;				/* High order 8 bits of file position */

	/* different protocol variations */
	private const LSZ_OPTZEDZAP		= 0x00000001;		/* We could big blocks, ZModem variant (ZedZap) */
	private const LSZ_OPTDIRZAP		= 0x00000002;		/* We escape nothing, ZModem variant (DirZap) */
	private const LSZ_OPTESCAPEALL	= 0x00000004;		/* We must escape ALL contorlo characters */
	private const LSZ_OPTCRC32		= 0x00000008;		/* We must send CRC 32 */
	private const LSZ_OPTVHDR		= 0x00000010;		/* We must send variable headers */
	private const LSZ_OPTRLE		= 0x00000020;		/* We must send RLEd data (NOT SUPPORTED)! */
	private const LSZ_OPTESC8		= 0x00000040;		/* We must escape all 8-bit data (NOT SUPPORTED!) */
	private const LSZ_OPTSKIPGUARD	= 0x00000080;		/* We use double-skip guard */
	private const LSZ_OPTFIRSTBATCH = 0x00000100;		/* It is first batch -- trust in first ZFIN */

	/* Peer's capabilities from ZRINIT header */
	private const LSZ_RXCANDUPLEX	= 0x0001;			/* Receiver can send and receive true FDX */
	private const LSZ_RXCANOVIO		= 0x0002;			/* Receiver can receive data during disk I/O */
	private const LSZ_RXCANBRK		= 0x0004;			/* Receiver can send a break signal */
	private const LSZ_RXCANRLE		= 0x0008;			/* Receiver can decode RLE */
	private const LSZ_RXCANLZW		= 0x0010;			/* Receiver can uncompress LZW */
	private const LSZ_RXCANFC32		= 0x0020;			/* Receiver can	use 32 bit Frame Check */
	private const LSZ_RXWNTESCCTL	= 0x0040;			/* Receiver expects ctl	chars to be escaped */
	private const LSZ_RXWNTESC8		= 0x0080;			/* Receiver expects 8th	bit to be escaped */
	private const LSZ_RXCANVHDR		= 0x0100;			/* Receiver can variable headers */

	/* Conversion options (ZF0 in ZFILE frame) */
	private const LSZ_CONVBIN		= 1;				/* Binary transfer - inhibit conversion */
	private const LSZ_CONVCNL		= 2;				/* Convert NL to local end of line convention */
	private const LSZ_CONVRECOV		= 3;				/* Resume interrupted file transfer -- binary */

	/* Read Mode */
	private const rm8BIT			= 0;
	private const rm7BIT			= 1;
	private const rmZDLE			= 2;
	private const rmHEX				= 3;

	/* State */
	private const rhInit			= 0;
	private const rhZPAD			= 1;
	private const rhXON				= 2;
	private const rhLF				= 3;
	private const rhCR				= 4;
	private const rhCRC				= 5;
	private const rhBYTE			= 6;
	private const rhFrameType		= 7;
	private const rhZHEX			= 8;
	private const rhZBIN			= 9;
	private const rhZBIN32			= 10;
	private const rhZVHEX			= 11;
	private const rhZVBIN			= 12;
	private const rhZVBIN32			= 13;
	private const rhZDLE			= 14;

	private const sfSlidingWindow	= 0;				/* Window requested, could do fullduplex (use stream with ZCRCQ and sliding window) */
	private const sfStream			= 1;				/* No window requested (use ZCRCG) */
	private const sfBuffered		= 2;				/* Window requested, couldn't use fullduplex (use frames ZCRCG + ZCRCW) */

	private int $ls_GotZDLE			= 0;				/* We seen DLE as last character */
	private int $ls_GotHexNibble	= 0;				/* We seen one hex digit as last character */
	private int $ls_Protocol;							/* Plain/ZedZap/DirZap and other options */
	private int $ls_CANCount		= 0;				/* Count of CANs to go */
	private int $ls_SerialNum		= 0;				/* Serial number of file -- for Double-skip protection */

	private int $ls_DataTimeout;						/* Timeout for data blocks */
	private int $ls_HeaderTimeout;						/* Timeout for headers */
	private int $ls_MaxBlockSize	= 8192;				/* Maximum block size */

	/* Special table to FAST calculate header type */
	/*				CRC32,VAR,RLE */
	private const HEADER_TYPE = [[[self::ZBIN,-1],[self::ZVBIN,-1]],[[self::ZBIN32,self::ZBINR32],[self::ZVBIN32,self::ZVBINR32]]];

	/* Variables to control sender */
	private array $ls_rxHdr			= [];				/* Receiver header */
	private int $ls_rxCould			= 0;				/* Receiver could fullduplex/streamed IO (from ZRINIT) */

	private int $ls_txWinSize		= 0;				/* Receiver Window/Buffer size (0 for streaming) */
	private int $ls_txCurBlockSize;						/* Current block size */
	private int $ls_txLastSent;							/* Last sent character -- for escaping */
	private int $ls_txLastACK		= 0;				/* Last ACKed byte */
	private int $ls_txLastRepos		= 0;				/* Last requested byte */
	private int $ls_txReposCount	= 0;				/* Count of REPOSes on one position */
	private int $ls_txGoodBlocks	= 0;				/* Good blocks sent */

	private const NUL				= 0x00;
	private const ACK				= 0x06;
	private const BEL				= 0x07;
	private const BS				= 0x08;
	private const HT				= 0x09;
	private const LF				= 0x0a;
	private const VT				= 0x0b;
	private const CR				= 0x0d;
	private const DLE				= 0x10;
	private const XON				= 0x11;	//(81&037); 'Q'
	private const XOFF				= 0x13; //(83&037); 'S'
	private const CAN				= 0x18;
	private const ESC				= 0x1b;

	private const RX_SKIP			= 1;
	private const RX_SUSPEND		= 2;

	private string $rxbuf			= '';
	private string $txbuf			= '';

	/**
	 * @param SocketClient $client
	 * @return 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(self::SESSION_ZMODEM,$client);
			$this->client->close();

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

		return NULL;
	}

	/**
	 * Initialise our session
	 */
	private function init(SocketClient $client,int $canzap): void
	{
		$this->client = $client;

		/* Set all options to requested state -- this may be alerted by other side in ZSINIT */
		$this->ls_Protocol = self::LSZ_OPTCRC32|self::LSZ_OPTSKIPGUARD;

		switch ($canzap&0xff) {
			case 2:
				$this->ls_Protocol |= self::LSZ_OPTDIRZAP;
				/* FALL THROUGH */

			case 1:
				$this->ls_Protocol |= self::LSZ_OPTZEDZAP;
				/* FALL THROUGH */

			case 0:
				break;

			default:
				Log::error(sprintf('%s: ! init Strange cancap [%d]',self::LOGKEY,$canzap));
		}

		/* Maximum block size -- by protocol, may be reduced by window size later */
		$this->ls_MaxBlockSize = ($this->ls_Protocol&self::LSZ_OPTZEDZAP) ? 8192 : 1024;

		/* Calculate timeouts */
		/* Timeout for header waiting, if no data sent -- 3*TransferTime or 10 seconds */
		$this->ls_HeaderTimeout = (self::LSZ_MAXHLEN * 30) / $this->client->speed;
		$this->ls_HeaderTimeout = ($this->ls_HeaderTimeout > 10) ? $this->ls_HeaderTimeout : 10;

		/* Timeout for data packet (3*TransferTime or 60 seconds) */
		$this->ls_DataTimeout = ($this->ls_MaxBlockSize * 30) / $this->client->speed;
		$this->ls_DataTimeout = ($this->ls_DataTimeout > 60) ? $this->ls_DataTimeout : 60;

		$this->ls_SkipGuard = ($this->ls_Protocol&self::LSZ_OPTSKIPGUARD) ? 1 : 0;
	}

	public function protocol_init(): int
	{
		// Not used
		return 0;
	}

	/**
	 * Setup our ZMODEM session
	 *
	 * @return int
	 * @throws \Exception
	 */
	public function protocol_session(): int
	{
		$proto = $this->originate ? $this->node->optionGet(self::P_MASK) : $this->optionGet(self::P_MASK);

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

			$rc = $this->zmodem_senddone();

		} else {
			$rc = $this->zmodem_receive($this->client,$proto,$this->recv,$this->node->address);
		}

		return $rc;
	}

	/**
	 * Receive files via Zmodem
	 *
	 * @param SocketClient $client
	 * @param Receive $recv
	 * @param int $canzap
	 * @return int
	 */
	public function zmodem_receive(SocketClient $client,int $canzap,Receive $recv,Address $ao): int
	{
		Log::debug(sprintf('%s:+ zmodem_receive [%d]',self::LOGKEY,$canzap));

		$opts = $this->init($client,$canzap);

		$this->ls_txWinSize = self::LSZ_WINDOW;
		$this->recv = $recv;

		if ($canzap&0x0100)
			$opts |= self::LSZ_OPTFIRSTBATCH;

		switch ($rc=$this->ls_zrecvfinfo(self::ZRINIT,($this->ls_Protocol&self::LSZ_OPTFIRSTBATCH) ? 1 : 0)) {
			case self::ZFIN:
				Log::debug(sprintf('%s:= zmodem_receive ZFIN after INIT, empty batch',self::LOGKEY));
				$this->ls_zdonereceiver();

				return self::LSZ_OK;

			case self::ZFILE:
				Log::debug(sprintf('%s: = zmodem_receive ZFILE after INIT',self::LOGKEY));
				break;

			default:
				Log::error(sprintf('%s:! zmodem_receive Something strange after init [%d]',self::LOGKEY,$rc));

				$this->ls_zabort();
				$this->ls_zdonereceiver();

				return self::LSZ_ERROR;
		}

		while (TRUE) {
			switch ($rc) {
				case self::ZFIN:
					Log::debug(sprintf('%s:= zmodem_receive ZFIN',self::LOGKEY));
					$this->ls_zdonereceiver();

					return self::LSZ_OK;

				case self::ZFILE:
					if (! $this->recv->to_get) {
						Log::error(sprintf('%s: ! zmodem_receive No files to get?',self::LOGKEY));

						$frame = self::ZSKIP;

					} else {
						switch ($this->recv->open($ao)) {
							case self::FOP_SKIP:
								Log::info(sprintf('%s: = zmodem_receive Skip this file [%s]',self::LOGKEY,$this->recv->name));
								$frame = self::ZSKIP;

								break;

							case self::FOP_SUSPEND:
								Log::info(sprintf('%s: = zmodem_receive Suspend this file [%s]',self::LOGKEY,$this->recv->name));
								$frame = self::ZFERR;

								break;

							case self::FOP_CONT:
							case self::FOP_OK:
								Log::info(sprintf('%s: = zmodem_receive Receving [%s] from [%d]',self::LOGKEY,$this->recv->name,$this->recv->filepos));
								$frame = self::ZRINIT;

								switch (($rc=$this->ls_zrecvfile($recv->filepos))) {
									case self::ZFERR:
										Log::debug(sprintf('%s: = zmodem_receive ZFERR',self::LOGKEY));
										$this->recv->close();
										$frame = self::ZFERR;

										break;

									case self::ZSKIP:
										Log::debug(sprintf('%s: = zmodem_receive ZSKIP',self::LOGKEY));
										$this->recv->close();
										$frame = self::ZSKIP;

										break;

									case self::LSZ_OK:
										Log::debug(sprintf('%s: = zmodem_receive OK',self::LOGKEY));
										$this->recv->close();

										break;

									default:
										Log::error(sprintf('%s:! zmodem_receive OTHER [%d]',self::LOGKEY,$rc));
										$this->recv->close();

										return self::LSZ_ERROR;
								}

								break;
						}
					}

					break;

				case self::ZABORT:
					Log::debug(sprintf('%s:= zmodem_receive ZABORT',self::LOGKEY));

					$this->ls_zabort();
					$this->ls_zdonereceiver();

					return self::LSZ_ERROR;

				default:
					Log::error(sprintf('%s:? zmodem_receive Something strange [%d]',self::LOGKEY,$rc));

					$this->ls_zabort();
					$this->ls_zdonereceiver();

					return self::LSZ_ERROR;
			}

			$rc = $this->ls_zrecvfinfo($frame,1);
		}

		return self::LSZ_OK;
	}

	/**
	 * Done sender -- good way
	 *
	 * @return int
	 * @throws \Exception
	 */
	public function zmodem_senddone():int
	{
		$trys = 0;
		$retransmit = 1;
		$this->txbuf = '';

		do {
			if ($retransmit) {
				if (($rc=$this->ls_zsendhhdr(self::ZFIN,$this->ls_storelong(0))) < 0)
					return $rc;

				$trys++;
				$retransmit = 0;
			}

			switch (($rc=$this->ls_zrecvhdr($this->ls_rxHdr,$this->ls_HeaderTimeout))) {
				/* Ok, GOOD */
				case self::ZFIN:
					$this->client->buffer_add('OO');
					$this->client->buffer_flush(5);

					return self::LSZ_OK;

				case self::ZNAK:
				case self::LSZ_TIMEOUT:
					$retransmit = 1;

					break;

				default:
					Log::error(sprintf('%s: ? zmodem_senddone Something strange [%d]',self::LOGKEY,$rc));

					if ($rc < 0)
						return $rc;

					$retransmit = 1;
			}

		} while ($trys < 10);

		Log::error(sprintf('%s:? zmodem_senddone Something strange or timeeout [%d]',self::LOGKEY,$rc));
		return $rc;
	}

	/**
	 * Init sending -- wrapper
	 *
	 * @param SocketClient $client
	 * @param int $canzap
	 * @return int
	 */
	public function zmodem_sendinit(SocketClient $client,int $canzap): int
	{
		Log::debug(sprintf('%s:+ zmodem_sendinit [%d]',self::LOGKEY,$canzap));

		$this->init($client,$canzap);

		if (($rc=$this->ls_zinitsender(self::LSZ_WINDOW,'')) < 0)
			return $rc;

		Log::debug(sprintf('%s: - ZMODEM Link Options %d/%d, %s%s%s%s',
			self::LOGKEY,
			$this->ls_MaxBlockSize,
			$this->ls_txWinSize,
			($this->ls_Protocol&self::LSZ_OPTCRC32)		? 'CRC32' : 'CRC16',
			($this->ls_rxCould&self::LSZ_RXCANDUPLEX)	? ',DUPLEX' : '',
			($this->ls_Protocol&self::LSZ_OPTVHDR)		? ',VHEADER' : '',
			($this->ls_Protocol&self::LSZ_OPTESCAPEALL)	? ',ESCALL' : ''
		));

		Log::debug(sprintf('%s:= zmodem_sendinit [%x]',self::LOGKEY,$rc));
		return $rc;
	}

	/**
	 * Send a file with the Zmodem Protocol
	 *
	 * @param Send $send
	 * @return int
	 */
	public function zmodem_sendfile(Send $send): int
	{
		Log::debug(sprintf('%s:+ zmodem_sendfile',self::LOGKEY));

		while ($send->total_count && $send->open()) {
			try {
				$rc = $this->ls_zsendfile($send,$this->ls_SerialNum++,$send->total_count,$send->total_size);

				switch ($rc) {
					case self::LSZ_OK:
						$send->close(TRUE);
						break;

					case self::ZSKIP:
					case self::ZFERR:
						$send->close(FALSE);
						break;

					default:
						$send->close(FALSE);
						$this->ls_zabort();
						break;
				}

				return $rc;

			} catch (\Exception $e) {
				Log::error(sprintf('%s: ! Error [%s]',self::LOGKEY,$e->getMessage()));
			}
		}

		return self::OK;
	}

	/**
	 * Fetch long integer (4 bytes) from buffer, as it must be stored in header
	 *
	 * @param array $buf
	 * @return int
	 */
	private function ls_fetchlong(array $buf): int
	{
		$l = $buf[self::LSZ_P3];
		$l<<=8;
		$l |= $buf[self::LSZ_P2];
		$l<<=8;
		$l |= $buf[self::LSZ_P1];
		$l<<=8;
		$l |= $buf[self::LSZ_P0];

		return $l;
	}

	/**
	 * Return 7bit character, strip XON/XOFF if not DirZap, with timeout
	 *
	 * @param int $timeout
	 * @return int
	 * @throws SocketException
	 */
	private function ls_read7bit(int $timeout): int
	{
		$this->ls_CANCount = 0;

		do {
			if (($c = $this->client->read_ch($timeout)) < 0)
				return $c;

		} while ((! ($this->ls_Protocol&self::LSZ_OPTDIRZAP)) && (($c == self::XON) || ($c == self::XOFF)));

		if ($c == self::CAN) {
			if (++$this->ls_CANCount == 5)
				return self::LSZ_CAN;

		} else {
			$this->ls_CANCount = 0;
		}

		if (ord($c) == 0)
			return self::LSZ_ERROR;

		return $c&0x7f;
	}

	/**
	 * Read one character, check for five CANs
	 *
	 * @param int $timeout
	 * @return int
	 */
	private function ls_readcanned(int $timeout): int
	{
		if (($ch = $this->client->read_ch($timeout)) < 0)
			return $ch;

		if ($ch == self::CAN) {
			if (++$this->ls_CANCount == 5)
				return self::LSZ_CAN;

		} else {
			$this->ls_CANCount = 0;
		}

		return $ch&0xff;
	}

	/**
	 * Read character as two hex digit
	 *
	 * @param int $timeout
	 * @return int
	 */
	private function ls_readhex(int $timeout): int
	{
		static $c = 0;

		if (! $this->ls_GotHexNibble) {
			if (($c = $this->ls_readhexnibble($timeout)) < 0)
				return $c;

			$c <<= 4;
		}

		if (($c2 = $this->ls_readhexnibble($timeout)) >= 0) {
			$this->ls_GotHexNibble = 0;

			return ($c|$c2);

		} else {
			$this->ls_GotHexNibble = 1;

			return $c2;
		}
	}

	/**
	 * Read one hex character
	 *
	 * @param int $timeout
	 * @return int
	 */
	private function ls_readhexnibble(int $timeout): int
	{
		if (($c = $this->ls_readcanned($timeout)) < 0)
			return $c;

		if (chr($c) >= '0' && chr($c) <= '9') {
			return $c-ord('0');

		} elseif (chr($c) >= 'a' && chr($c) <= 'f') {
			return $c-ord('a')+10;

		} else {
			/* will be CRC error */
			return 0;
		}
	}

	/**
	 * Return 8bit character, strip <DLE>
	 *
	 * @param int $timeout
	 * @return int
	 */
	private function ls_readzdle(int $timeout): int
	{
		$r = 0;

		/* There was no ZDLE in stream, try to read one */
		if (! $this->ls_GotZDLE) {
			do {
				if (($c = $this->ls_readcanned($timeout)) < 0)
					return $c;

				/* Check for unescaped XON/XOFF */
				if (! ($this->ls_Protocol&self::LSZ_OPTDIRZAP)) {
					switch ($c) {
						case self::XON:
						case self::XON|0x80:
						case self::XOFF:
						case self::XOFF|0x80:
							$c = self::LSZ_XONXOFF;
					}
				}

				if ($c == self::ZDLE)
					$this->ls_GotZDLE = 1;

				elseif ($c != self::LSZ_XONXOFF)
					return $c&0xff;

			} while ($c == self::LSZ_XONXOFF);
		}

		/* We will be here only in case of DLE */
		/* We have data */
		if (($c = $this->ls_readcanned($timeout)) >= 0) {
			$this->ls_GotZDLE = 0;

			switch ($c) {
				case self::ZCRCE:
				case self::ZCRCG:
				case self::ZCRCW:
				case self::ZCRCQ:
					$r = ($c|0x100);
					break;
			}

			/*
			*chat*
			if ($r && isset($rnode) && $rnode->opt&self::MO_CHAT) {
				do {
					$rr = $this->ls_readcanned($timeout);
					// @todo to implement
					$this->z_devrecv_c($rr,0);
				} while ($rr);

				$this->z_devsend_c(0);
			}
			*/

			if ($r)
				return $r;

			switch ($c) {
				case self::ZRUB0:
					return self::ZDEL;

				case self::ZRUB1:
					return (self::ZDEL|0x80);

				default:
					if (($c&0x60) != 0x40)
						return self::LSZ_BADCRC;

					return ($c^0x40)&0xff;
			}
		}

		return $c;
	}

	/**
	 * Send one char with escaping
	 *
	 * @param int $c
	 * @throws \Exception
	 */
	private function ls_sendchar(int $c): void
	{
		$c &= 0xff;

		/* We are Direct ZedZap -- escape only <DLE> */
		if ($this->ls_Protocol&self::LSZ_OPTDIRZAP) {
			$esc = (self::ZDLE == $c);

		/* We are normal ZModem (may be ZedZap) */
		} else {
			/* Receiver want to escape ALL */
			if (($this->ls_Protocol&self::LSZ_OPTESCAPEALL) && (($c&0x60) == 0)) {
				$esc = 1;

			} else {
				switch ($c) {
					case self::XON:
					case (self::XON|0x80):
					case self::XOFF:
					case (self::XOFF|0x80):
					case self::DLE:
					case (self::DLE|0x80):
					case self::ZDLE:
						$esc = 1;
						break;

					default:
						$esc = ((($this->ls_txLastSent&0x7f) == ord('@')) && (($c&0x7f) == self::CR));
						break;
				}
			}
		}

		if ($esc) {
			$this->client->buffer_add(chr(self::ZDLE));
			$c ^= 0x40;
		}

		$this->client->buffer_add(chr($this->ls_txLastSent = $c));
	}

	/**
	 * Send one char as two hex digits
	 *
	 * @param int $i
	 * @throws \Exception
	 */
	private function ls_sendhex(int $i): void
	{
		$str = hexstr($i);
		$this->ls_txLastSent = ord(substr($str,-1));

		$this->client->buffer_add($str);
	}

	/**
	 * Store long integer (4 bytes) in buffer, as it must be stored in header
	 *
	 * @param int $l
	 * @return array
	 */
	private function ls_storelong(int $l): array
	{
		$buf[self::LSZ_P0] = ($l)&0xff;
		$buf[self::LSZ_P1] = ($l>>8)&0xff;
		$buf[self::LSZ_P2] = ($l>>16)&0xff;
		$buf[self::LSZ_P3] = ($l>>24)&0xff;

		return $buf;
	}

	/**
	 * Abort the session
	 *
	 * @throws \Exception
	 */
	private function ls_zabort(): void
	{
		$this->client->buffer_flush($this->ls_DataTimeout);
		$this->client->buffer_add(chr(self::XON));

		for ($i=0;$i<8;$i++)
			$this->client->buffer_add(chr(self::CAN));

		$this->client->buffer_flush($this->ls_DataTimeout);
	}

	/**
	 * Finished receiving
	 *
	 * @return int
	 * @throws SocketException
	 */
	private function ls_zdonereceiver(): int
	{
		Log::debug(sprintf('%s:+ ls_zdonereceiver',self::LOGKEY));

		$trys = 0;
		$retransmit = 1;
		$this->rxbuf = '';

		do {
			if ($retransmit) {
				if (($rc=$this->ls_zsendhhdr(self::ZFIN,$this->ls_storelong(0))) < 0)
					return $rc;

				$retransmit = 0;
				$trys++;
			}

			switch ($rc=$this->client->read_ch($this->ls_HeaderTimeout)) {
				/* Ok, GOOD */
				case ord('O'):
					$rc = $this->client->read_ch(0);
					return self::LSZ_OK;

				case self::XON:
				case self::XOFF:
				case self::XON|0x80:
				case self::XOFF|0x80:
					if ($this->DEBUG)
						Log::debug(sprintf('%s: - ls_zdonereceiver XON/XOFF, skip it',self::LOGKEY));
					break;

				case self::ZPAD:
					if ($this->DEBUG)
						Log::debug(sprintf('%s: - ls_zdonereceiver ZPAD',self::LOGKEY));

					if (($rc=$this->ls_zrecvhdr($this->ls_rxHdr,$this->ls_HeaderTimeout)) < 0)
						return $rc;

					if (self::ZFIN != $rc)
						return self::LSZ_OK;

					$retransmit = 1;

					break;

				default:
					Log::error(sprintf('%s: ? ls_zdonereceiver Something strange [%d]',self::LOGKEY,$rc));

					if ($rc < 0)
						return $rc;

					$retransmit = 1;
			}

		} while ($trys < 10);

		Log::error(sprintf('%s:? ls_zdonereceiver Something strange [%d]',self::LOGKEY,$rc));
		return $rc;
	}

	/**
	 * Init sender, preapre to send files (initialize timeouts too!)
	 *
	 * @param int $protocol
	 * @param int $window
	 * @return int
	 */
	private function ls_zinitsender(int $window,string $attstr)
	{
		Log::debug(sprintf('%s:+ ls_zinitsender',self::LOGKEY));

		$trys = 0;

		/* Options from ZRINIT header */
		$this->rxOptions = 0;

		/* ZFIN counter -- we will trust only MAY of them on this stage */
		$zfins = 0;

		$this->ls_SerialNum = 1;

		/* Why we need to send this? Old, good times... */
		$this->client->send("rz\r",5);
		$retransmit = 1;

		do {
			if ($retransmit) {
				/* Send first ZRQINIT (do we need it?) */
				if (($rc=$this->ls_zsendhhdr(self::ZRQINIT,$this->ls_storelong(0))) < 0)
					return $rc;

				$retransmit = 0;
				$trys++;
			}

			switch ($rc=$this->ls_zrecvhdr($this->ls_rxHdr,$this->ls_HeaderTimeout)) {
				/* Ok, We got RINIT! */
				case self::ZRINIT:
					Log::debug(sprintf('%s: - ls_zinitsender ZRINIT',self::LOGKEY));

					$this->rxOptions= (($this->ls_rxHdr[self::LSZ_F1]&0xff)<<8)|($this->ls_rxHdr[self::LSZ_F0]&0xff);

					/* What receiver could (in hardware) -- Duplex, sim. I/O, send break signals */
					$this->ls_rxCould = ($this->rxOptions&(self::LSZ_RXCANDUPLEX|self::LSZ_RXCANOVIO|self::LSZ_RXCANBRK));

					/* Strip RLE from ls_Protocol, if peer could not use RLE (WE COULD NOT RLE in ANY CASE!) */
					if (! ($this->rxOptions&self::LSZ_RXCANRLE))
						$this->ls_Protocol &= (~self::LSZ_OPTRLE);

					/* Strip CRC32 from $this->ls_Protocol, if peer could not use CRC32 */
					if (! ($this->rxOptions&self::LSZ_RXCANFC32))
						$this->ls_Protocol &= (~self::LSZ_OPTCRC32);

					/* Set EscapeAll if peer want it */
					if ($this->rxOptions&self::LSZ_RXWNTESCCTL)
						$this->ls_Protocol |= self::LSZ_OPTESCAPEALL;

					/* Strip VHeaders from $this->ls_Protocol, if peer could not use VHDR */
					if (! ($this->rxOptions&self::LSZ_RXCANVHDR))
						$this->ls_Protocol &= (~self::LSZ_OPTVHDR);

					/* Ok, options are ready */
					/* Fetch window size */
					$this->ls_txWinSize = (($this->ls_rxHdr[self::LSZ_P1]&0xff)<<8)|($this->ls_rxHdr[self::LSZ_P0]&0xff);

					/* Override empty or big window by our window (if not emty too) */
					if ($window && (! $this->ls_txWinSize || ($this->ls_txWinSize > $window)))
						$this->ls_txWinSize = $window;

					Log::debug(sprintf('%s: - ls_zinitsender ZRINIT OK - effproto [%08x], winsize [%d]',self::LOGKEY,$this->ls_Protocol,$this->ls_txWinSize));

					/* Ok, now we could calculate real max frame size and initial block size */
					if ($this->ls_txWinSize && $this->ls_MaxBlockSize>$this->ls_txWinSize) {
						for ($this->ls_MaxBlockSize=1;$this->ls_MaxBlockSize<$this->ls_txWinSize;$this->ls_MaxBlockSize<<=1) {};

						/*ls_MaxBlockSize >>= 1;*/
						if ($this->ls_MaxBlockSize<32)
							$this->ls_txWinSize = $this->ls_MaxBlockSize=32;
					}

					if ($this->client->speed < 2400)
						$this->ls_txCurBlockSize = 256;

					elseif ($this->client->speed >= 2400 && $this->client->speed < 4800)
						$this->ls_txCurBlockSize = 512;

					else
						$this->ls_txCurBlockSize = 1024;

					if ($this->ls_Protocol&self::LSZ_OPTZEDZAP) {
						if ($this->client->speed >= 7200 && $this->client->speed < 9600)
							$this->ls_txCurBlockSize = 2048;

						elseif ($this->client->speed >= 9600 && $this->client->speed < 14400)
							$this->ls_txCurBlockSize = 4096;

						elseif ($this->client->speed >= 14400)
							$this->ls_txCurBlockSize = 8192;
					}

					if ($this->ls_txCurBlockSize>$this->ls_MaxBlockSize)
						$this->ls_txCurBlockSize=$this->ls_MaxBlockSize;

					Log::debug(sprintf('%s: - ls_zinitsender ZRINIT OK - block sizes Max [%d] Current [%d]',self::LOGKEY,$this->ls_MaxBlockSize,$this->ls_txCurBlockSize));

					/* Send ZSINIT, if we need it */
					if ($attstr || (! ($this->rxOptions&self::LSZ_RXWNTESCCTL) && ($this->ls_Protocol&self::LSZ_OPTESCAPEALL)))
						return $this->ls_zsendsinit($attstr);

					else
						return self::LSZ_OK;

				/* Return number to peer, he is paranoid */
				case self::ZCHALLENGE:
					Log::debug(sprintf('%s: - ls_zinitsender CHALLENGE',self::LOGKEY));

					if (($rc=$this->ls_zsendhhdr(ZACK,$this->ls_rxHdr)) < 0)
						return $rc;

					break;

				/* Send ZRQINIT again */
				case self::ZNAK:
				case self::LSZ_TIMEOUT:
					$retransmit = 1;

					break;

				/* ZFIN from previous session? Or may be real one? */
				case self::ZFIN:
					Log::debug(sprintf('%s: - ls_zinitsender ZFIN [%d]',self::LOGKEY,$zfins));

					if (++$zfins == self::LSZ_TRUSTZFINS)
						return self::LSZ_ERROR;

					break;

				/* Please, resend */
				case self::LSZ_BADCRC:
					Log::debug(sprintf('%s: - ls_zinitsender LSZ_BADCRC',self::LOGKEY));

				/* We don't support it! */
				case self::ZCOMMAND:
					Log::debug(sprintf('%s: - ls_zinitsender ZCOMMAND',self::LOGKEY));
					$this->ls_zsendhhdr(ZNAK,$this->ls_storelong(0));

				/* Abort this session -- we trust in ABORT! */
				case self::ZABORT:
					Log::debug(sprintf('%s:- ls_zinitsender ZABORT',self::LOGKEY));
					return self::LSZ_ERROR;

				default:
					Log::error(sprintf('%s: ? ls_zinitsender Something strange [%d]',self::LOGKEY,$rc));

					if ($rc < 0)
						return $rc;
			}

		} while ($trys < 10);

		Log::error(sprintf('%s:? ls_zinitsender Something strange or timeout [%d]',self::LOGKEY,$rc));
		return $rc;
	}

	private function ls_zrecvdata(&$data,&$len,$timeout,$crc32) {
		return $crc32 ? $this->ls_zrecvdata32($data,$len,$timeout) : $this->ls_zrecvdata16($data,$len,$timeout);
	}

	/**
	 * Receive data subframe with CRC16, return frame type or error (may be -- timeout)
	 *
	 * @return mixed
	 */
	private function ls_zrecvdata16(string &$data,int &$len,int $timeout): int
	{
		if ($this->DEBUG)
			Log::debug(sprintf('%s:+ ls_zrecvdata16',self::LOGKEY),['d'=>$data]);

		$got = 0;							/* Bytes total got */
		$crc = 0;							/* Received CRC */
		$frametype = self::LSZ_ERROR;		/* Type of frame - ZCRC(G|W|Q|E) */
		$rcvdata = 1;						/* Data is being received NOW (not CRC) */

		while ($rcvdata && (($c = $this->ls_readzdle($timeout)) >= 0)) {
			if ($this->DEBUG)
				Log::debug(sprintf('%s: - ls_zrecvdata16 got [%x] (%c)',self::LOGKEY,$c,($c<31 ? 32 : $c)),['c'=>serialize($c)]);

			if ($c < 256) {
				$data .= chr($c&0xff);

				if (++$got > $this->ls_MaxBlockSize)
					return self::LSZ_BADCRC;

			} else {
				switch($c) {
					case self::LSZ_CRCE:
					case self::LSZ_CRCG:
					case self::LSZ_CRCQ:
					case self::LSZ_CRCW:
						$rcvdata = 0;
						$frametype = ($c & 0xff);

						break;

					default:
						return self::LSZ_BADCRC;
				}
			}
		}

		/* We finish loop by error in ls_readzdle() */
		if ($rcvdata) {
			Log::error(sprintf('%s:? ls_zrecvdata16 Something strange or timeout [%d]',self::LOGKEY,$rc));

			return $c;
		}

		/* Loops ar unrolled */
		if (($c = $this->ls_readzdle($timeout)) < 0)
			return $c;

		$crc = $c;
		if (($c = $this->ls_readzdle($timeout)) < 0)
			return $c;

		$crc <<= 8;
		$crc |= $c;

		$incrc = crc16($data.chr($frametype));

		if ($this->DEBUG)
			Log::debug(sprintf('%s: - ls_zrecvdata16 CRC%d got %08x - calculated %08x',self::LOGKEY,16,$incrc,$crc));

		if ($incrc != $crc)
			return self::LSZ_BADCRC;

		$len = $got;

		if ($this->DEBUG)
			Log::debug(sprintf('%s:= ls_zrecvdata16 - frametype [%d]',self::LOGKEY,$frametype));

		return $frametype;
	}

	/**
	 * Receive data subframe with CRC32, return frame type or error (may be -- timeout)
	 *
	 * @param string $data
	 * @param int $len
	 * @param int $timeout
	 * @return int
	 */
	private function ls_zrecvdata32(string &$data,int &$len,int $timeout): int
	{
		if ($this->DEBUG)
			Log::debug(sprintf('%s:+ ls_zrecvdata32',self::LOGKEY),['d'=>$data]);

		$got = 0;							/* Bytes total got */
		$crc = 0;							/* Received CRC */
		$frametype = self::LSZ_ERROR;		/* Type of frame - ZCRC(G|W|Q|E) */
		$rcvdata = 1;						/* Data is being received NOW (not CRC) */

		while ($rcvdata && (($c=$this->ls_readzdle($timeout)) >= 0)) {
			if ($this->DEBUG)
				Log::debug(sprintf('%s: - ls_zrecvdata32 got [%x] (%c)',self::LOGKEY,$c,($c<31 ? 32 : $c)),['c'=>serialize($c)]);

			if ($c < 256) {
				$data .= chr($c&0xff);

				if (++$got > $this->ls_MaxBlockSize)
					return self::LSZ_BADCRC;

			} else {
				switch ($c) {
					case self::LSZ_CRCE:
					case self::LSZ_CRCG:
					case self::LSZ_CRCQ:
					case self::LSZ_CRCW:
						$rcvdata = 0;
						$frametype = ($c&0xff);

						break;

					default:
						return self::LSZ_BADCRC;
				}
			}
		}

		/* We finish loop by error in ls_readzdle() */
		if ($rcvdata) {
			Log::error(sprintf('%s:? ls_zrecvdata32 Something strange or timeout [%d]',self::LOGKEY,$rc));

			return $c;
		}

		/* Loops ar unrolled */
		if (($c = $this->ls_readzdle($timeout)) < 0)
			return $c;

		$crc |= ($c << 0x00);
		if (($c = $this->ls_readzdle($timeout)) < 0)
			return $c;

		$crc |= ($c << 0x08);
		if (($c = $this->ls_readzdle($timeout)) < 0)
			return $c;

		$crc |= ($c << 0x10);
		if (($c = $this->ls_readzdle($timeout)) < 0)
			return $c;

		$crc |= ($c << 0x18);

		$incrc = crc32($data.chr($frametype));
		if ($this->DEBUG)
			Log::debug(sprintf('%s: - ls_zrecvdata32 CRC%d got %08x - calculated %08x',self::LOGKEY,32,$incrc,$crc));

		if ($incrc != $crc)
			return self::LSZ_BADCRC;

		$len = $got;

		if ($this->DEBUG)
			Log::debug(sprintf('%s:= ls_zrecvdata32 - frametype [%d]',self::LOGKEY,$frametype));

		return $frametype;
	}

	/**
	 * Receive one file
	 *
	 * @param int $pos
	 * @return int
	 * @throws \Exception
	 */
	private function ls_zrecvfile(int $pos): int
	{
		Log::debug(sprintf('%s:+ ls_zrecvfile - pos [%d]',self::LOGKEY,$pos));

		$needzdata = 1;
		$len = 0;
		$rxpos = $pos;
		$rxstatus = 0;
		$this->rxbuf = '';

		if (($rc=$this->ls_zsendhhdr(self::ZRPOS,$this->ls_storelong($rxpos))) < 0)
			return $rc;

		do {
			if (! $needzdata) {
				switch (($rc=$this->ls_zrecvdata($this->rxbuf,$len,$this->ls_DataTimeout,$this->ls_Protocol&self::LSZ_OPTCRC32))) {
					case self::ZCRCE:
						$needzdata = 1;

					case self::ZCRCG:
						Log::debug(sprintf('%s: - ls_zrecvfile ZCRC%s, [%d] bytes at [%d]',self::LOGKEY,($rc==self::ZCRCE ? 'E' : 'G'),$len,$rxpos));

						$rxpos += $len;

						if ($len != $this->recv->write($this->rxbuf))
							return self::ZFERR;

						$this->rxbuf = '';

						break;

					case self::ZCRCW:
						$needzdata = 1;

					case self::ZCRCQ:
						Log::debug(sprintf('%s: - ls_zrecvfile ZCRC%s, [%d] bytes at [%d]',self::LOGKEY,($rc==self::ZCRCW ? 'W' : 'Q'),$len,$rxpos));

						$rxpos += $len;

						if ($len != $this->recv->write($this->rxbuf))
							return self::ZFERR;

						$this->rxbuf = '';

						$this->ls_zsendhhdr(self::ZACK,$this->ls_storelong($rxpos));

						break;

					case self::LSZ_BADCRC:
					case self::LSZ_TIMEOUT:
						if ($this->ls_rxAttnStr) {
							$this->client->buffer_add($this->ls_rxAttnStr);
							$this->client->buffer_flush(5);
						}

						$this->client->rx_purge();
						$this->ls_zsendhhdr(self::ZRPOS,$this->ls_storelong($rxpos));
						$needzdata = 1;

						break;

					default:
						Log::error(sprintf('%s: ? ls_zrecvfile Something strange [%d]',self::LOGKEY,$rc));

						if ($rc < 0)
							return $rc;

						if ($this->ls_rxAttnStr) {
							$this->client->buffer_add($this->ls_rxAttnStr);
							$this->client->buffer_flush(5);
						}

						$this->client->rx_purge();
						$this->ls_zsendhhdr(self::ZRPOS,$this->ls_storelong($rxpos));
						$needzdata = 1;
				}

			/* We need new position -- ZDATA (and may be ZEOF) */
			} else {
				Log::debug(sprintf('%s: - ls_zrecvfile Want ZDATA/ZEOF at [%d]',self::LOGKEY,$rxpos));

				if (($rc=$this->ls_zrecvnewpos($rxpos,$newpos)) < 0)
					return $rc;

				if ($newpos != $rxpos) {
					Log::error(sprintf('%s: - ls_zrecvfile Bad new position [%d] in [%d]',self::LOGKEY,$newpos,$rc));

					if ($this->ls_rxAttnStr) {
						$this->client->buffer_add($this->ls_rxAttnStr);
						$this->client->buffer_flush(5);
					}

					$this->client->rx_purge();

					if (($rc=$this->ls_zsendhhdr(self::ZRPOS,$this->ls_storelong($rxpos))) < 0)
						return $rc;

				} else {
					if ($rc == self::ZEOF) {
						Log::debug(sprintf('%s: - ls_zrecvfile ZEOF',self::LOGKEY));

						if (($rc=$this->ls_zsendhhdr(self::ZRINIT,$this->ls_storelong(0))) < 0)
							return $rc;

						return self::LSZ_OK;
					}

					Log::debug(sprintf('%s: - ls_zrecvfile ZDATA',self::LOGKEY));
					$needzdata = 0;
				}
			}

			if ($rxstatus)
				return ($rxstatus==self::RX_SKIP) ? self::ZSKIP : self::ZFERR;

		} while (TRUE);

		return self::LSZ_OK;
	}

	/**
	 * @param int $frame
	 * @param int $first
	 * @return int
	 * @throws \Exception
	 */
	private function ls_zrecvfinfo(int $frame,int $first): int
	{
		Log::debug(sprintf('%s:+ ls_zrecvfinfo - Frame [%d], First [%d]',self::LOGKEY,$frame,$first));

		$trys = 0;
		$retransmit = ($frame != self::ZRINIT);
		$len = 0;
		$rc = 0;

		/* ZFIN counter -- we will trust only MAY of them on first stage */
		$zfins = 0;

		$win = $this->ls_txWinSize;
		$flags = (self::LSZ_RXCANDUPLEX|self::LSZ_RXCANOVIO);

		if ($this->ls_Protocol&self::LSZ_OPTCRC32)
			$flags |= self::LSZ_RXCANFC32;

		if ($this->ls_Protocol&self::LSZ_OPTESCAPEALL)
			$flags |= self::LSZ_RXWNTESCCTL;

		if ($this->ls_Protocol&self::LSZ_OPTESC8)
			$flags |= self::LSZ_RXWNTESC8;

		do {
			if ($retransmit) {
				if ($frame != self::ZRINIT) {
					if (($rc=$this->ls_zsendhhdr($frame,$this->ls_storelong($this->ls_SerialNum))) < 0)
						return $rc;
				}

				Log::debug(sprintf('%s: - ls_zrecvfinfo ZRINIT',self::LOGKEY));

				$txHdr = [];
				$txHdr[self::LSZ_P0] = ($win&0xff);
				$txHdr[self::LSZ_P1] = (($win>>8)&0xff);
				$txHdr[self::LSZ_F1] = ($this->ls_Protocol&self::LSZ_OPTVHDR) ? self::LSZ_RXCANVHDR : 0;
				$txHdr[self::LSZ_F0] = $flags;

				if (($rc=$this->ls_zsendhhdr(self::ZRINIT,$txHdr)) < 0)
					return $rc;

				$retransmit = 0;
				$trys++;
			}

			switch (($rc=$this->ls_zrecvhdr($this->ls_rxHdr,$this->ls_HeaderTimeout))) {
				/* Send ZRINIT again */
				case self::ZRQINIT:
					Log::debug(sprintf('%s: - ls_zrecvfinfo ZRQINIT',self::LOGKEY));
					/* We will trust in first ZFIN after ZRQINIT */
					$first = 1;

				case self::ZNAK:
					Log::debug(sprintf('%s: - ls_zrecvfinfo ZNAK',self::LOGKEY));

				case self::LSZ_TIMEOUT:
					Log::debug(sprintf('%s: - ls_zrecvfinfo LSZ_TIMEOUT',self::LOGKEY));
					$retransmit = 1;

					break;

				/* He want to set some options */
				case self::ZSINIT:
					Log::debug(sprintf('%s: - ls_zrecvfinfo ZSINIT',self::LOGKEY));
					if (($rc=$this->ls_zrecvcrcw($this->rxbuf,$len)) < 0)
						return $rc;

					/* We will trust in first ZFIN after ZSINIT */
					$first = 0;

					/* Everything is OK */
					if (! $rc) {
						$this->ls_zsendhhdr(self::ZACK,$this->ls_storelong(1));

						$this->ls_rxAttnStr = $this->rxbuf;

						if ($this->ls_rxHdr[self::LSZ_F0]&self::LSZ_TXWNTESCCTL)
							$this->ls_Protocol |= self::LSZ_OPTESCAPEALL;

						if ($this->ls_rxHdr[self::LSZ_F0]&self::LSZ_TXWNTESC8)
							$this->ls_Protocol |= self::LSZ_OPTESC8;

					/* We could not receive ZCRCW subframe, but error is not fatal */
					} else {
						$trys++;
					}

					break;

				/* Ok, File started! */
				case self::ZFILE:
					Log::debug(sprintf('%s: - ls_zrecvfinfo ZFILE',self::LOGKEY));

					if (($rc=$this->ls_zrecvcrcw($this->rxbuf,$len)) < 0)
						return $rc;

					/* Everything is OK, decode frame */
					if (! $rc) {
						$file = [];
						$file['name'] = substr($this->rxbuf,0,$x=strpos($this->rxbuf,chr(0x00)));

						if (sscanf(substr($this->rxbuf,$x+1),
							'%ld %lo %o %o %ld %ld',
							$file['size'],
								$file['mtime'],
								$len,				// @todo What is $len?
								$ls_SerialNum,		// @todo Do we use this?
								$filesleft,			// @todo Should track this
								$bytesleft) < 2)
						{
							Log::error(sprintf('%s: ! ls_zrecvfinfo File info is corrupted [%s]',self::LOGKEY,$this->rxbuf));
							$filesleft = -1;

						} else {
							$this->recv->new($file);
						}

						return self::ZFILE;

					/* We could not receive ZCRCW subframe, but error is not fatal */
					} else {
						$trys++;
					}

					break;

				/* ZFIN from previous session? Or may be real one? */
				case self::ZFIN:
					Log::debug(sprintf('%s: - ls_zrecvfinfo ZFIN [%d], first [%d]',self::LOGKEY,$zfins,$first));

					if ($first || (++$zfins == self::LSZ_TRUSTZFINS))
						return self::ZFIN;

					break;

				/* Abort this session -- we trust in ABORT! */
				case self::ZABORT:
					Log::debug(sprintf('%s:- ls_zrecvfinfo ZABORT',self::LOGKEY));
					return self::ZABORT;

				case self::LSZ_BADCRC:
					Log::debug(sprintf('%s: - ls_zrecvfinfo BADCRC',self::LOGKEY));

					$this->ls_zsendhhdr(self::ZNAK,$this->ls_storelong(0));
					$retransmit = 1;

					break;

				default:
					Log::error(sprintf('%s: ? ls_zrecvfinfo Something strange [%d]',self::LOGKEY,$rc));

					if ($rc < 0)
						return $rc;

					$this->ls_zsendhhdr(self::ZNAK,$this->ls_storelong(0));
			}

		} while ($trys < 10);

		Log::error(sprintf('%s:? ls_zrecvfinfo Something strange or timeout',self::LOGKEY));
		return self::LSZ_TIMEOUT;
	}

	private function ls_zrecvhdr(array &$hdr,int $timeout): int
	{
		if ($this->DEBUG)
			Log::debug(sprintf('%s:+ ls_zrecvhdr with [%d] timeout',self::LOGKEY,$timeout));

		$state = self::rhInit;
		$readmode = self::rm7BIT;

		static $frametype = self::LSZ_ERROR;	/* Frame type */
		static $crcl = 2;						/* Length of CRC (CRC16 is default) */
		static $crcgot = 0;						/* Number of CRC bytes already got */
		static $incrc = 0;						/* Calculated CRC */
		static $crc = 0;						/* Received CRC */
		static $len = 4;						/* Length of header (4 is default) */
		static $got = 0;						/* Number of header bytes already got */
		static $garbage = 0;					/* Count of garbage characters */
		$c = -1;

		if ($state == self::rhInit) {
			if ($this->DEBUG)
				Log::debug(sprintf('%s: - ls_zrecvhdr Init State',self::LOGKEY));

			$frametype = self::LSZ_ERROR;
			$crc = 0;
			$crcl = 2;
			$crcgot = 0;
			$incrc = self::LSZ_INIT_CRC16;
			$len = 4;
			$got = 0;
			$readmode = self::rm7BIT;
		}

		while ($rc = $this->client->hasData($timeout)) {
			if ($this->DEBUG)
				Log::debug(sprintf('%s: - ls_zrecvhdr hasData - readmode [%d] - Garbage [%d]',self::LOGKEY,$readmode,$garbage));

			switch ($readmode) {
				case self::rm8BIT:
					$c = $this->ls_readcanned($timeout);
					break;
				case self::rm7BIT:
					$c = $this->ls_read7bit($timeout);
					break;
				case self::rmZDLE:
					$c = $this->ls_readzdle($timeout);
					break;
				case self::rmHEX:
					$c = $this->ls_readhex($timeout);
					break;
			}

			if ($this->DEBUG)
				Log::debug(sprintf('%s: - ls_zrecvhdr %s [%x] (%c)',self::LOGKEY,$readmode,$c,$c));

			/* Here is error */
			if ($c < 0)
				return $c;

			/* Strip high bits */
			$c &= 0xff;

			if ($this->DEBUG)
				Log::debug(sprintf('%s: = ls_zrecvhdr %x (%d)',self::LOGKEY,$c,$state));

			switch ($state) {
				case self::rhInit:
					if ($c == self::ZPAD)
						$state = self::rhZPAD;
					else
						$garbage++;

					break;

				case self::rhZPAD:
					switch ($c) {
						case self::ZPAD:
							break;

						case self::ZDLE:
							$state = self::rhZDLE;
							break;

						default:
							$garbage++;
							$state = self::rhInit;
							break;
					}

					break;

				case self::rhZDLE:
					switch ($c) {
						case self::ZBIN:
							$state = self::rhZBIN;
							$readmode = self::rmZDLE;
							break;

						case self::ZHEX:
							$state = self::rhZHEX;
							$readmode = self::rmHEX;
							break;

						case self::ZBIN32:
							$state = self::rhZBIN32;
							$readmode = self::rmZDLE;
							break;

						case self::ZVBIN:
							$state = self::rhZVBIN;
							$readmode = self::rmZDLE;
							break;

						case self::ZVHEX:
							$state = self::rhZVHEX;
							$readmode = self::rmHEX;
							break;

						case self::ZVBIN32:
							$state = self::rhZVBIN32;
							$readmode = self::rmZDLE;
							break;

						default:
							$garbage++;
							$state = self::rhInit;
							$readmode = self::rm7BIT;
					}

					break;

				case self::rhZVBIN32:
					$crcl = 4;
					/* Fall throught */

				case self::rhZVBIN:
				case self::rhZVHEX:
					if ($c > self::LSZ_MAXHLEN) {
						$state = self::rhInit;

						return self::LSZ_BADCRC;
					}

					$len = $c;
					$state = self::rhFrameType;

					break;

				case self::rhZBIN32:
					$crcl = 4;
					/* Fall throught */

				case self::rhZBIN:
				case self::rhZHEX:
					$len = 4;

				case self::rhFrameType:
					if (($c < 0) || ($c > self::LSZ_MAXFRAME)) {
						$state = self::rhInit;

						return self::LSZ_BADCRC;
					}

					$frametype = $c;
					$incrc = ($crcl == 2) ? $this->CRC16USD_UPDATE($c,self::LSZ_INIT_CRC16) : $this->CRC32_UPDATE($c,self::LSZ_INIT_CRC32);
					$state = self::rhBYTE;

					break;

				case self::rhBYTE:
					if ($this->DEBUG)
						Log::debug(sprintf('%s: - ls_zrecvhdr [%02x] (%d)',self::LOGKEY,$c,$got));

					$hdr[$got] = $c;

					if ($len == ++$got)
						$state = self::rhCRC;

					$incrc = ($crcl == 2) ? $this->CRC16USD_UPDATE($c,$incrc) : $this->CRC32_UPDATE($c,$incrc);

					break;

				case self::rhCRC:
					if ($this->DEBUG)
						Log::debug(sprintf('%s: - ls_zrecvhdr [%02x] (%d|%d)',self::LOGKEY,$c,$crcgot+1,$got));

					if ($crcl == 2) {
						$crc <<= 8;
						$crc |= $c;

					} else {
						$crc |= ($c << ($crcgot*8));
					}

					/* Crc finished */
					if ($crcl == ++$crcgot) {
						$state = self::rhInit;
						$garbage = 0;

						if ($crcl == 2) {
							if (($this->ls_Protocol&self::LSZ_OPTCRC32) && ($readmode != self::rmHEX))
								Log::error(sprintf('%s: - ls_zrecvhdr was CRC32, got CRC16 binary header',self::LOGKEY));

							$crc &= 0xffff;

							if ($readmode != self::rmHEX)
								$this->ls_Protocol &= (~self::LSZ_OPTCRC32);

						} else {
							if (! ($this->ls_Protocol&self::LSZ_OPTCRC32))
								Log::error(sprintf('%s: - ls_zrecvhdr was CRC16, got CRC32 binary header',self::LOGKEY));

							$incrc = $this->CRC32_FINISH($incrc);
							$this->ls_Protocol |= self::LSZ_OPTCRC32;
						}

						if ($this->DEBUG)
							Log::debug(sprintf('%s: - ls_zrecvhdr CRC%d got %08x - calculated %08x',self::LOGKEY,(($crcl==2) ? 16 : 32),$incrc,$crc));

						if ($incrc != $crc)
							return self::LSZ_BADCRC;

						/* We need to read <CR><LF> after HEX header */
						if ($readmode == self::rmHEX) {
							$state = self::rhCR;
							$readmode = self::rm8BIT;

						} else {
							return $frametype;
						}
					}

					break;

				case self::rhCR:
					$state = self::rhInit;

					switch ($c) {
						/* we need LF after <CR> */
						case self::CR:
						case self::CR|0x80:
							if ($this->DEBUG)
								Log::debug(sprintf('%s:? ls_zrecvhdr rhCR, ignoring any remaining chars [%d]',self::LOGKEY,$c));

						/* At this point, most implementations ignore checking for the remaining chars */
						return $frametype;

						/* Ok, UNIX-like EOL */
						case self::LF:
						case self::LF|0x80:
							$state = self::rhXON;
							break;

						case self::XON:
						case self::XON|0x80:
							if ($this->DEBUG)
								Log::debug(sprintf('%s:? ls_zrecvhdr rhCR, got XON without CR/LF [%d]',self::LOGKEY,$c));

						return $frametype;

						default:
							Log::error(sprintf('%s:? ls_zrecvhdr Something strange [%d]',self::LOGKEY,$c));
							return self::LSZ_BADCRC;
					}

					break;

				case self::rhLF:
					$state = self::rhInit;

					switch ($c) {
						case self::LF:
						case self::LF|0x80:
							$state = self::rhXON;
							break;

						default:
							Log::error(sprintf('%s:? ls_zrecvhdr Something strange [%d]',self::LOGKEY,$c));
							return self::LSZ_BADCRC;
					}

					break;

				case self::rhXON:
					$state = self::rhInit;

					if ($this->DEBUG)
						Log::debug(sprintf('%s: - ls_zrecvhdr rhXON',self::LOGKEY));

					switch ($c) {
						case self::ZPAD:
						case self::ZPAD|0x80:
							$state = self::rhZPAD;
							$got = 0;
							$crcgot = 0;
							break;

						case self::XON:
						case self::XON|0x80:
							return $frametype;

						default:
							Log::error(sprintf('%s:! ls_zrecvhdr rhXON unexpcted [%d]',self::LOGKEY,$c));
							return self::LSZ_BADCRC;
					}

					break;
			}
		}

		Log::error(sprintf('%s:? ls_zrecvhdr Something strange or timeout [%d]',self::LOGKEY,$rc));
		return $rc;
	}

	/**
	 * Internal function -- receive ZCRCW frame in 10 trys, send ZNAK/ZACK
	 *
	 * @param string $buf
	 * @param int $len
	 * @return int
	 */
	private function ls_zrecvcrcw(string &$buf,int $len): int
	{
		$trys = 0;

		do {
			switch (($rc=$this->ls_zrecvdata($buf,$len,$this->ls_DataTimeout,$this->ls_Protocol&self::LSZ_OPTCRC32))) {
				/* Ok, here it is */
				case self::ZCRCW:
					return self::LSZ_OK;

				case self::LSZ_BADCRC:
					Log::debug(sprintf('%s: - ls_zrecvcrcw got BADCRC',self::LOGKEY));
					$this->ls_zsendhhdr(self::ZNAK,$this->ls_storelong(0));

					return 1;

				case self::LSZ_TIMEOUT:
					Log::debug(sprintf('%s: - ls_zrecvcrcw got TIMEOUT',self::LOGKEY));
					break;

				default:
					Log::error(sprintf('%s: ? ls_zrecvcrcw Something strange [%d]',self::LOGKEY,$rc));

					if ($rc < 0)
						return $rc;

					$this->ls_zsendhhdr(self::ZNAK,$this->ls_storelong(0));

					return 1;
			}

		} while (++$trys < 10);

		$this->ls_zsendhhdr(self::ZNAK,$this->ls_storelong(0));
		return 1;
	}

	/**
	 * Receive ZDATA/ZEOF frame, do 10 trys, return position
	 *
	 * @param $oldpos
	 * @param $pos
	 * @return int
	 */
	private function ls_zrecvnewpos($oldpos,&$pos): int
	{
		$rc = 0;
		$trys = 0;

		do {
			switch (($rc=$this->ls_zrecvhdr($this->ls_rxHdr,$this->ls_HeaderTimeout))) {
				case self::ZDATA:
				case self::ZEOF:
					$pos = $this->ls_fetchlong($this->ls_rxHdr);

					return $rc;

				case self::ZNAK:
					if (($rc=$this->ls_zsendhhdr(self::ZRPOS,$this->ls_storelong($oldpos))) < 0)
						return $rc;

					break;

				case self::LSZ_TIMEOUT:
					break;

				case self::LSZ_BADCRC:
					$this->ls_zsendhhdr(self::ZNAK,$this->ls_storelong(0));

					break;

				default:
					Log::error(sprintf('%s: ? ls_zrecvnewpos Something strange [%d]',self::LOGKEY,$rc));

					if ($rc < 0)
						return $rc;

					$this->ls_zsendhhdr(self::ZNAK,$this->ls_storelong(0));
			}

		} while (++$trys < 10);

		Log::error(sprintf('%s:? ls_zrecvnewpos Something strange or timeout [%d]',self::LOGKEY,$rc));
		return self::LSZ_TIMEOUT;
	}

	/**
	 * Internal function to process ZRPOS
	 *
	 * @param Send $send
	 * @param int $newpos
	 * @return int
	 * @throws \Exception
	 */
	private function ls_zrpos(Send $send,int $newpos): int
	{
		Log::debug(sprintf('%s:+ ls_zrpos, newpos [%d]',self::LOGKEY,$newpos));

		if ($newpos == $this->ls_txLastRepos) {
			if (++$this->ls_txReposCount > 10) {
				Log::error(sprintf('%s:! ZRPOS to [%ld] limit reached',self::LOGKEY,$newpos));

				return self::LSZ_ERROR;
			}

		} else {
			$this->ls_txReposCount = 0;
			$this->ls_txLastRepos = $newpos;
		}

		/* Drop window */
		$this->ls_txLastACK = $newpos;

		if (! $send->seek($newpos)) {
			Log::error(sprintf('%s:! ZRPOS to [%ld] seek error',self::LOGKEY,$send->filepos));

			return self::LSZ_ERROR;
		}

		if ($this->ls_txCurBlockSize > 32)
			$this->ls_txCurBlockSize >>= 1;

		$this->ls_txGoodBlocks = 0;
		return self::LSZ_OK;
	}

	/**
	 * Send data block, with CRC16 and framing
	 *
	 * @param string $data
	 * @param int $frame
	 * @return int
	 * @throws \Exception
	 */
	private function ls_zsenddata(string $data,int $frame): int
	{
		if ($this->DEBUG)
			Log::debug(sprintf('%s:+ ls_zsenddata [%d] (%d)',self::LOGKEY,strlen($data),$frame));

		if ($this->ls_Protocol&self::LSZ_OPTCRC32) {
			if ($this->DEBUG)
				Log::debug(sprintf('%s: - ls_zsenddata CRC32',self::LOGKEY));

			for ($n=0;$n<strlen($data);$n++)
				$this->ls_sendchar(ord($data[$n]));

			$this->client->buffer_add(chr(self::ZDLE).chr($frame));

			/*
			*chat*
			if (FALSE AND $rnode->opt&self::MO_CHAT) {
				if ($frame==self::ZCRCG||$frame==self::ZCRCW) {
					z_devsend_c(1);
				}
				$this->client->buffer_add(chr(0));
			}
			*/

			$crc = crc32($data.chr($frame));
			$this->ls_sendchar($crc&0xff);
			$crc >>= 8;
			$this->ls_sendchar($crc&0xff);
			$crc >>= 8;
			$this->ls_sendchar($crc&0xff);
			$crc >>= 8;
			$this->ls_sendchar($crc&0xff);
			$crc >>= 8;

		} else {
			if ($this->DEBUG)
				Log::debug(sprintf('%s: - ls_zsenddata CRC16',self::LOGKEY));

			for ($n=0;$n<strlen($data);$n++) {
				$this->ls_sendchar(ord($data[$n]));
			}

			$this->client->buffer_add(chr(self::ZDLE).chr($frame));

			/*
			*chat*
			if (FALSE AND $rnode->opt&self::MO_CHAT) {
				if ($frame==self::ZCRCG||$frame==self::ZCRCW) {
					z_devsend_c(1);
				}
				$this->client->buffer_add(chr(0));
			}
			*/

			$crc = crc16($data.chr($frame));
			$this->ls_sendchar($crc >> 8);
			$this->ls_sendchar($crc&0xff);
		}

		if (! ($this->ls_Protocol&self::LSZ_OPTDIRZAP) && self::ZCRCW == $frame)
			$this->client->buffer_add(chr(self::XON));

		return $this->client->buffer_flush($this->ls_DataTimeout);
	}

	/**
	 * Send binary header. Use proper CRC, send var. len. if could
	 *
	 * @param int $frametype
	 * @param array $hdr
	 * @return int
	 * @throws \Exception
	 */
	private function ls_zsendbhdr(int $frametype,array $hdr): int
	{
		Log::debug(sprintf('%s:+ ls_zsendbhdr',self::LOGKEY));

		$crc = $this->LSZ_INIT_CRC();

		/* First, calculate packet header byte */
		if (($type = self::HEADER_TYPE
			[($this->ls_Protocol&self::LSZ_OPTCRC32)==self::LSZ_OPTCRC32]
			[($this->ls_Protocol&self::LSZ_OPTVHDR)==self::LSZ_OPTVHDR]
			[($this->ls_Protocol&self::LSZ_OPTRLE)==self::LSZ_OPTRLE]) < 0)
		{
			return self::LSZ_ERROR;
		}

		/* Send *<DLE> and packet type */
		$this->client->buffer_add(chr(self::ZPAD).chr(self::ZDLE).chr($type));

		/* Send length of header, if needed */
		if ($this->ls_Protocol&self::LSZ_OPTVHDR)
			$this->ls_sendchar(count($hdr));

		/* Send type of frame */
		$this->ls_sendchar($frametype);

		$crc = $this->LSZ_UPDATE_CRC($frametype,$crc);

		/* Send whole header */
		for ($n=0; $n<count($hdr); $n++) {
			$this->ls_sendchar($hdr[$n]);
			$crc = $this->LSZ_UPDATE_CRC($hdr[$n],$crc);
		}

		$crc = $this->LSZ_FINISH_CRC($crc);

		if ($this->ls_Protocol&self::LSZ_OPTCRC32) {
			for ($n=0;$n<4;$n++) {
				$this->ls_sendchar($crc&0xff);
				$crc >>= 8;
			}

		} else {
			$crc &= 0xffff;
			$this->ls_sendchar($crc >> 8);
			$this->ls_sendchar($crc&0xff);
		}

		/* Clean buffer, do real send */
		return $this->client->buffer_flush($this->ls_HeaderTimeout);
	}

	/**
	 * Send one file to peer
	 *
	 * @param Send $send
	 * @param int $sernum
	 * @param int $fileleft
	 * @param int $bytesleft
	 * @return int
	 * @throws \Exception
	 */
	private function ls_zsendfile(Send $send,int $sernum,int $fileleft,int $bytesleft): int
	{
		Log::debug(sprintf('%s:+ ls_zsendfile [%s]',self::LOGKEY,$send->name));

		$trys = 0;
		$needack = 0;

		switch (($rc = $this->ls_zsendfinfo($send,$sernum,$send->filepos,$fileleft,$bytesleft))) {
			/* Ok, It's OK! */
			case self::ZRPOS:
				Log::debug(sprintf('%s: - ls_zsendfile ZRPOS to [%d]',self::LOGKEY,$send->filepos));
				break;

			/* Skip it */
			case self::ZSKIP:
			/* Suspend it */
			case self::ZFERR:
				// @todo Mark the file as skipped
				Log::debug(sprintf('%s: - ls_zsendfile ZSKIP/ZFERR',self::LOGKEY));
				return $rc;

			case self::ZABORT:
			/* Session is aborted */
			case self::ZFIN:
				Log::debug(sprintf('%s: - ls_zsendfile ZABORT/ZFIN',self::LOGKEY));
				$this->ls_zsendhhdr(self::ZFIN,$this->ls_storelong(0));

				return self::LSZ_ERROR;

			default:
				if ($rc < 0)
					return $rc;

				Log::debug(sprintf('%s:- ls_zsendfile Strange answer on ZFILE [%d]',self::LOGKEY,$rc));

				return self::LSZ_ERROR;
		}

		/* Send file data */
		if ($this->ls_txWinSize)
			$mode = ($this->ls_rxCould&self::LSZ_RXCANDUPLEX) ? self::sfSlidingWindow : self::sfBuffered;
		else
			$mode = self::sfStream;

		$frame = self::ZCRCW;

		while (! $send->feof()) {
			/* We need to send ZDATA if previous frame was ZCRCW
			Also, frame will be ZCRCW, if it is after RPOS */
			if ($frame == self::ZCRCW) {
				Log::debug(sprintf('%s: - ls_zsendfile send ZDATA at [%d]',self::LOGKEY,$send->filepos));

				if (($rc=$this->ls_zsendbhdr(self::ZDATA,$this->ls_storelong($send->filepos))) < 0)
					return $rc;
			}

			/* Send frame of data */
			try {
				$txbuf = $send->read($this->ls_txCurBlockSize);

			} catch (\Exception $e) {
				Log::error(sprintf('%s:! ls_zsendfile Read error',self::LOGKEY));

				return self::LSZ_ERROR;
			}

			/* Select sub-frame type */
			/* This is last sub-frame -- EOF */
			if (strlen($txbuf) < $this->ls_txCurBlockSize) {
				$frame = ($mode == self::sfStream) ? self::ZCRCE : self::ZCRCW;

			/* This is not-last sub-frame */
			} else {
				switch ($mode) {
					/* Simple sub-frame */
					case self::sfStream:
						$frame = self::ZCRCG;
						break;

					/* Simple sub-frame, but with SlWin */
					case self::sfSlidingWindow:
						$frame = self::ZCRCQ;
						break;

					case self::sfBuffered:
						if (($send->filepos + strlen($txbuf)) > $this->ls_txLastACK + $this->ls_txWinSize) {
							$frame = self::ZCRCW;	/* Last sub-frame in buffer */

						} else {
							$frame = self::ZCRCG;	/* Simple sub-frame */
						}

						break;
				}
			}

			if (($rc=$this->ls_zsenddata($txbuf,$frame)) < 0)
				return $rc;

			/* Ok, now wait for ACKs if here is window, or sample for RPOSes */
			$trys = 0;

			do {
				$needack = (self::ZCRCW == $frame) || ($this->ls_txWinSize && ($send->filepos > $this->ls_txLastACK + $this->ls_txWinSize));

				switch (($rc=$this->ls_zrecvhdr($this->ls_rxHdr,$needack ? $this->ls_HeaderTimeout : 0))) {	// @todo set timeout to 5 for debugging wtih POP
					/* They don't need this file */
					case self::ZSKIP:
						Log::debug(sprintf('%s: - ls_zsendfile ZSKIP',self::LOGKEY));

					/* Problems occured -- suspend file */
					case self::ZFERR:
						Log::debug(sprintf('%s: - ls_zsendfile ZFERR',self::LOGKEY));
						return $rc;

					/* Ok, position ACK */
					case self::ZACK:
						$this->ls_txLastACK = $this->ls_fetchlong($this->ls_rxHdr);

						if ($this->DEBUG)
							Log::debug(sprintf('%s: - ls_zsendfile ZACK',self::LOGKEY),['ls_rxHdr'=>$this->ls_rxHdr,'ls_txLastACK'=>$this->ls_txLastACK,'ls_rxHdr'=>$this->ls_rxHdr]);

						break;

					/* Repos */
					case self::ZRPOS:
						Log::debug(sprintf('%s: - ZRPOS',self::LOGKEY));

						if (($rc=$this->ls_zrpos($send,$this->ls_fetchlong($this->ls_rxHdr))) < 0)
							return $rc;

						Log::debug(sprintf('%s: - ZRPOS [%d]',self::LOGKEY,$rc));

						/* Force to retransmit ZDATA */
						$frame = self::ZCRCW;

						break;

					/* Abort transfer */
					case self::ZABORT:
					/* Strange? Ok, abort too */
					case self::ZFIN:
					/* Abort too */
					case self::ZCAN:
					case self::LSZ_CAN:
						$this->ls_zsendhhdr(self::ZFIN,$this->ls_storelong(0));
						/* Fall through */

					case self::LSZ_RCDO:
					case self::LSZ_ERROR:
						return self::LSZ_ERROR;

					case self::LSZ_TIMEOUT:   /* Ok! */
						break;

					default:
						Log::error(sprintf('%s: ? ls_zsendfile Something strange [%d]',self::LOGKEY,$rc));

						if ($rc < 0)
							return $rc;
				}

			} while (
				/* Here is window, and we send more than window without ACK*/
				/* Frame was ZCRCW and here is no ACK for it */
				/* trys less than 10 */
				(($this->ls_txWinSize && ($send->filepos>($this->ls_txLastACK+$this->ls_txWinSize)))
					|| ((self::ZCRCW == $frame) && ($send->filepos>$this->ls_txLastACK)))
					&& ++$trys < 10);

			if ($trys >= 10)
				return self::LSZ_ERROR;

			/* Ok, increase block, if here is MANY good blocks was sent */
			if (++$this->ls_txGoodBlocks > 32) {
				$this->ls_txCurBlockSize <<= 1;

				if ($this->ls_txCurBlockSize > $this->ls_MaxBlockSize)
					$this->ls_txCurBlockSize = $this->ls_MaxBlockSize;

				$this->ls_txGoodBlocks = 0;
			}

			/* Ok, if here is EOF, send it and wait for ZRINIT or ZRPOS */
			/* We do it here, because we coulde receive ZRPOS as answer */
			if ($send->feof()) {
				if (($rc=$this->ls_zsendhhdr(self::ZEOF,$this->ls_storelong($send->filepos))) < 0)
					return $rc;

				$trys = 0;

				do {
					switch (($rc=$this->ls_zrecvhdr($this->ls_rxHdr,$this->ls_HeaderTimeout))) {
						/* They don't need this file */
						case self::ZSKIP:
						/* Problems occured -- suspend file */
						case self::ZFERR:
							return $rc;

						/* Repos */
						case self::ZRPOS:
							if (($rc=$this->ls_zrpos($send,$this->ls_fetchlong($this->ls_rxHdr))) < 0)
								return $rc;

							/* Force to retransmit ZDATA */
							$frame = self::ZCRCW;

							break;

						/* OK! */
						case self::ZRINIT:
							return self::LSZ_OK;

						/* ACK for data -- it lost! */
						case self::ZACK:
							Log::debug(sprintf('%s: - ls_zsendfile ZACK after EOF',self::LOGKEY));
							$this->ls_txLastACK = $this->ls_fetchlong($this->ls_rxHdr);

							break;

						/* Abort transfer */
						case self::ZABORT:
						/* Strange? Ok, abort too */
						case self::ZFIN:
						/* Abort too */
						case self::ZCAN:
							return self::LSZ_ERROR;

						/* Ok, here is no header */
						case self::LSZ_TIMEOUT:
							$trys++;

							break;

						default:
							Log::error(sprintf('%s: ? ls_zsendfile Something strange after ZEOF [%d]',self::LOGKEY,$rc));

							if ($rc < 0)
								return $rc;

							$trys++;
					}

				} while ($send->feof() && $trys < 10);

				if ($send->feof()) {
					Log::error(sprintf('%s:! ls_zsendfile To many tries waiting for ZEOF ACK',self::LOGKEY));

					return self::LSZ_ERROR;
				}
			}
		}

		Log::error(sprintf('%s:? ls_zsendfile Something strange or timeout [%d]',self::LOGKEY,$rc));
		return $rc;
	}

	/**
	 * Send file information to peer, get start position from them.
	 * Return packet type -- ZRPOS, ZSKIP, ZFERR, ZABORT or ZFIN (may be any error, too)
	 *
	 * @param int $sernum
	 * @param int $pos
	 * @return int
	 */
	private function ls_zsendfinfo(Send $send,int $sernum,int $pos,int $fileleft,int $bytesleft): int
	{
		Log::debug(sprintf('%s:+ ls_zsendfinfo [%s]',self::LOGKEY,$send->name));

		$trys = 0;
		$retransmit = 1;
		$crc = self::LSZ_INIT_CRC32;
		$buf = '';

		$this->client->buffer_clear();
		$buf = $send->sendas.chr(0);

		$buf .= sprintf('%ld %lo %o %o %ld %ld',
			$send->size,
			$send->mtime,
			0,
			$sernum,
			$fileleft,
			$bytesleft
		);

		do {
			if ($retransmit) {
				$txHdr = [];
				$txHdr[self::LSZ_F0] = (self::LSZ_CONVBIN|self::LSZ_CONVRECOV);
				$txHdr[self::LSZ_F1] = 0; /* No managment */
				$txHdr[self::LSZ_F2] = 0; /* No compression/encryption */
				$txHdr[self::LSZ_F3] = 0; /* No sparse files or variable headers */

				if (($rc=$this->ls_zsendbhdr(self::ZFILE,$txHdr)) < 0)
					return $rc;

				if (($rc=$this->ls_zsenddata($buf,self::ZCRCW)) < 0)
					return $rc;

				$retransmit = 0;
				$trys++;
			}

			switch ($rc=$this->ls_zrecvhdr($this->ls_rxHdr,$this->ls_HeaderTimeout)) {
				/* Ok, he want our file */
				case self::ZRPOS:
					$pos = $this->ls_fetchlong($this->ls_rxHdr);

					return self::ZRPOS;

				/* Skip */
				case self::ZSKIP:
				/* Refuse */
				case self::ZFERR:
					/* Check for double-skip protection */
					$sn = $this->ls_fetchlong($this->ls_rxHdr);

					/* Here is skip protection */
					if ($this->ls_SkipGuard && $sn && ($sn == $sernum-1)) {
						if (($rc=$this->ls_zsendhhdr(self::ZNAK,$this->ls_storelong(0))) < 0)
							return $rc;

						/* We don't need to skip this file */
						break;

					} elseif ($sn != $sernum) {
						$this->ls_SkipGuard = 0;
					}
					/* Fall through */

				/* Abort this session */
				case self::ZABORT:
				/* Finish this session */
				case self::ZFIN:
					return $rc;

				/* Send CRC to peer */
				case self::ZCRC:
					$len = $this->ls_fetchlong($this->ls_rxHdr);
					if (! $len)
						$len = $send->size;

					$cnt = 0;
					$send->seek(0);

					while (($cnt++ < $len) && (($c=$send->read(1)) > 0))
						$crc = $this->CRC32_UPDATE($c,$crc);

					$crc = $this->CRC32_FINISH($crc);
					if (($rc=$this->ls_zsendhhdr(self::ZCRC,$this->ls_storelong($crc))) < 0)
						return $rc;

					break;

				case self::ZRINIT:
					break;

				case self::ZNAK:
				case self::LSZ_TIMEOUT:
					$retransmit = 1;

					break;

				case self::LSZ_BADCRC:
					if (($rc=$this->ls_zsendhhdr(self::ZNAK,$this->ls_storelong(0))) < 0)
						return $rc;

					break;

				default:
					Log::error(sprintf('%s: ? Something strange [%d]',self::LOGKEY,$rc));

					if ($rc < 0)
						return $rc;
			}

		} while ($trys < 10);

		Log::error(sprintf('%s:? Something strange or timeout [%d]',self::LOGKEY,$rc));
		return $rc;
	}

	/* Send HEX header. Use CRC16, send var. len. if could */
	private function ls_zsendhhdr(int $frametype,array $hdr): int
	{
		if ($this->DEBUG)
			Log::debug(sprintf('%s:+ ls_zsendhhdr',self::LOGKEY));

		/* Send **<DLE> */
		$this->client->buffer_add(chr(self::ZPAD).chr(self::ZPAD).chr(self::ZDLE));

		/* Send header type */
		if ($this->ls_Protocol&self::LSZ_OPTVHDR) {
			$this->client->buffer_add(chr(self::ZVHEX));
			$this->ls_sendhex(count($hdr));

		} else {
			$this->client->buffer_add(chr(self::ZHEX));
		}

		$this->ls_sendhex($frametype);

		/* Send whole header */
		for ($n=0;$n<count($hdr);$n++) {
			$this->ls_sendhex($hdr[$n]);
		}

		$crc = crc16(chr($frametype).join('',array_map(function($item) { return chr($item); },$hdr)));
		$this->ls_sendhex($crc >> 8);
		$this->ls_sendhex($crc&0xff);
		$this->client->buffer_add(chr(self::CR));
		$this->client->buffer_add(chr(self::LF|0x80));

		if ($frametype != self::ZACK && $frametype != self::ZFIN)
			$this->client->buffer_add(chr(self::XON));

		/* Clean buffer, do real send */
		return $this->client->buffer_flush($this->ls_HeaderTimeout);
	}

	/**
	 * Send ZSINIT and wait for ZACK, skip ZRINIT, ZCOMMAND, answer on ZCHALLENGE
	 *
	 * @param string $attstr
	 * @return int
	 * @throws \Exception
	 */
	private function ls_zsendsinit(string $attstr): int
	{
		Log::debug(sprintf('%s:+ ls_zsendsinit [%s]',self::LOGKEY,$attrstr));

		$trys = 0;
		$retransmit = 1;

		if ($attstr) {
			if (strlen($attstr) > self::LSZ_MAXATTNLEN-1)
				$attstr = substr($attrstr,LSZ_MAXATTNLEN);

			$this->txbuf = $attrstr;

		} else {
			$this->txbuf = '';
		}

		do {
			if (retransmit) {
				/* We don't support ESC8, so don't ask for it in any case */
				$txHdr = [];
				$txHdr[self::LSZ_F0] = ($this->ls_Protocol&self::LSZ_OPTESCAPEALL) ? self::LSZ_TXWNTESCCTL : 0;
				$txHdr[self::LSZ_F1] = $txHdr[self::LSZ_F2] = $txHdr[self::LSZ_F3] = 0;

				if (($rc=$this->ls_zsendbhdr(self::ZSINIT,$txHdr)) < 0)
					return $rc;

				if ($rc=$this->ls_zsenddata($this->txbuf,self::ZCRCW))
					return $rc;

				$retransmit = 0;
				$trys++;
			}

			switch (($rc=$this->ls_zrecvhdr($this->ls_rxHdr,$this->ls_HeaderTimeout))) {
				/* Skip it */
				case self::ZRINIT:
					break;

				/* Ok */
				case self::ZACK:
					return self::LSZ_OK;

				/* Return number to peer, he is paranoid */
				case self::ZCHALLENGE:
					if (($rc=$this->ls_zsendhhdr(self::ZACK,$this->ls_rxHdr)) < 0)
						return $rc;

					break;

				case self::LSZ_BADCRC:
				case self::ZCOMMAND:
					if (($rc=$this->ls_zsendhhdr(self::ZNAK,$this->ls_storelong(0))) < 0)
						return $rc;

					break;

				/* Retransmit */
				case ZNAK:
				case LSZ_TIMEOUT:
					$retransmit = 1;

					break;

				default:
					Log::error(sprintf('%s: ? Something strange [%d]',self::LOGKEY,$rc));

					if ($rc < 0)
						return $rc;
			}

		} while ($trys < 10);

		Log::error(sprintf('%s:? Something strange or timeout [%d]',self::LOGKEY,$rc));
		return $rc;
	}
}