<?php

namespace App\Classes\Protocol;

use Illuminate\Support\Facades\Log;
use League\Flysystem\UnreadableFileException;

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
{
	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)) {
			$this->session(self::SESSION_ZMODEM,$client);
			$this->client->close();
			Log::info(sprintf('%s: = End - Connection closed [%s]',__METHOD__,$client->getAddress()));
		}

		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:   ! Strange cancap [%d]',__METHOD__,$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
	}

	/**
	 * 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: + Start [%d]',__METHOD__,$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:   = ZFIN after INIT, empty batch',__METHOD__));
				$this->ls_zdonereceiver();

				return self::LSZ_OK;

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

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

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

				return self::LSZ_ERROR;
		}

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

					return self::LSZ_OK;

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

						$frame = self::ZSKIP;

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

								break;

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

								break;

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

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

										break;

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

										break;

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

										break;

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

										return self::LSZ_ERROR;
								}

								break;
						}
					}

					break;

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

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

					return self::LSZ_ERROR;

				default:
					Log::error(sprintf('%s:   ? Something strange [%d]',__METHOD__,$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:   ? Something strange [%d]',__METHOD__,$rc));

					if ($rc < 0)
						return $rc;

					$retransmit = 1;
			}

		} while ($trys < 10);

		Log::error(sprintf('%s:   ? Something strange or timeeout [%d]',__METHOD__,$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: + Start [%d]',__METHOD__,$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',
			__METHOD__,
			$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: = End [%x]',__METHOD__,$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: + Start',__METHOD__));

		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]',__METHOD__,$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 $c): void
	{
		$hexdigitslower = "0123456789abcdef";

		$this->client->buffer_add(substr($hexdigitslower,($c&0xf0)>>4,1));
		$this->client->buffer_add(chr($this->ls_txLastSent = ord(substr($hexdigitslower,($c&0x0f),1))));
	}

	/**
	 * 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: + Start',__METHOD__));

		$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:   - XON/XOFF, skip it',__METHOD__));
					break;

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

					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:   ? Something strange [%d]',__METHOD__,$rc));

					if ($rc < 0)
						return $rc;

					$retransmit = 1;
			}

		} while ($trys < 10);

		Log::error(sprintf('%s:   ? Something strange [%d]',__METHOD__,$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: + Start',__METHOD__));

		$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:   - ZRINIT',__METHOD__));

					$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:   - ZRINIT OK - effproto [%08x], winsize [%d]',__METHOD__,$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:   - ZRINIT OK - block sizes Max [%d] Current [%d]',__METHOD__,$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:   - CHALLENGE',__METHOD__));

					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:   - ZFIN [%d]',__METHOD__,$zfins));

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

					break;

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

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

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

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

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

		} while ($trys < 10);

		Log::error(sprintf('%s:   ? Something strange or timeout [%d]',__METHOD__,$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 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('+ Start',['m'=>__METHOD__,'d'=>$data]);

		$got = 0;							/* Bytes total got */
		$incrc = self::LSZ_INIT_CRC32;		/* Calculated CRC */
		$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('  - got [%x] (%c)',$c,($c<31 ? 32 : $c)),['m'=>__METHOD__,'c'=>serialize($c)]);

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

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

				$incrc = $this->CRC32_UPDATE($c,$incrc);

			} else {
				switch ($c) {
					case self::LSZ_CRCE:
					case self::LSZ_CRCG:
					case self::LSZ_CRCQ:
					case self::LSZ_CRCW:
						$rcvdata = 0;
						$frametype = ($c&0xff);
						$incrc = $this->CRC32_UPDATE($c,$incrc);

						break;

					default:
						return self::LSZ_BADCRC;
				}
			}
		}

		/* We finish loop by error in ls_readzdle() */
		if ($rcvdata) {
			Log::error(sprintf('%s:   ? Something strange or timeout [%d]',__METHOD__,$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 = $this->CRC32_FINISH($incrc);

		if ($this->DEBUG)
			Log::debug(sprintf('CRC%d got %08x - calculated %08x',32,$incrc,$crc),['m'=>__METHOD__,'crc'=>$crc,'test_crc32'=>sprintf('%08x',$this->CRC32($data))]);

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

		$len = $got;

		if ($this->DEBUG)
			Log::debug('= End',['m'=>__METHOD__,'frametype'=>$frametype]);

		return $frametype;
	}

	/**
	 * Receive one file
	 *
	 * @param int $pos
	 * @return int
	 * @throws \Exception
	 */
	private function ls_zrecvfile(int $pos): int
	{
		Log::debug('+ Start',['m'=>__METHOD__,'pos'=>$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:   - ZCRC%s, [%d] bytes at [%d]',__METHOD__,($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:   - ZCRC%s, [%d] bytes at [%d]',__METHOD__,($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:   ? Something strange [%d]',__METHOD__,$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:   - Want ZDATA/ZEOF at [%d]',__METHOD__,$rxpos));

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

				if ($newpos != $rxpos) {
					Log::error(sprintf('%s:   - Bad new position [%d] in [%d]',__METHOD__,$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:   - ZEOF',__METHOD__));

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

						return self::LSZ_OK;
					}

					Log::debug(sprintf('%s:   - ZDATA',__METHOD__));
					$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: + Start - Frame [%d], First [%d]',__METHOD__,$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:   - ZRINIT',__METHOD__));

				$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:   - ZRQINIT',__METHOD__));
					/* We will trust in first ZFIN after ZRQINIT */
					$first = 1;

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

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

					break;

				/* He want to set some options */
				case self::ZSINIT:
					Log::debug(sprintf('%s:   - ZSINIT',__METHOD__));
					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:   - ZFILE',__METHOD__));

					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:   ! File info is corrupted [%s]',__METHOD__,$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:   - ZFIN [%d], first [%d]',__METHOD__,$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:   - ZABORT',__METHOD__));
					return self::ZABORT;

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

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

					break;

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

					if ($rc < 0)
						return $rc;

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

		} while ($trys < 10);

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

	private function ls_zrecvhdr(array &$hdr,int $timeout): int
	{
		if ($this->DEBUG)
			Log::debug(sprintf('%s: + Start with [%d] timeout',__METHOD__,$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:   - Init State',__METHOD__));

			$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:   - hasData - readmode [%d] - Garbage [%d]',__METHOD__,$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:   - %s [%x] (%c)',__METHOD__,$readmode,$c,$c));

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

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

			if ($this->DEBUG)
				Log::debug(sprintf('%s:   = %x',__METHOD__,$c),['state'=>$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:   - [%02x] (%d)',__METHOD__,$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:   - [%02x] (%d|%d)',__METHOD__,$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:    - was CRC32, got CRC16 binary header',__METHOD__));

							$crc &= 0xffff;

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

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

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

						if ($this->DEBUG)
							Log::debug(sprintf('%s:   - CRC%d got %08x - calculated %08x',__METHOD__,(($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:   ? rhCR, ignoring any remaining chars [%d]',__METHOD__,$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:   ? rhCR, got XON without CR/LF [%d]',__METHOD__,$c));

						return $frametype;

						default:
							Log::error(sprintf('%s:   ? Something strange [%d]',__METHOD__,$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:   ? Something strange [%d]',__METHOD__,$c));
							return self::LSZ_BADCRC;
					}

					break;

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

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

					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:   ! rhXON unexpcted [%d]',__METHOD__,$c));
							return self::LSZ_BADCRC;
					}

					break;
			}
		}

		Log::error(sprintf('%s:   ? Something strange or timeout [%d]',__METHOD__,$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:   - got BADCRC',__METHOD__));
					$this->ls_zsendhhdr(self::ZNAK,$this->ls_storelong(0));

					return 1;

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

				default:
					Log::error(sprintf('%s:   ? Something strange [%d]',__METHOD__,$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:   ? Something strange [%d]',__METHOD__,$rc));

					if ($rc < 0)
						return $rc;

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

		} while (++$trys < 10);

		Log::error(sprintf('%s:   ? Something strange or timeout [%d]',__METHOD__,$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: + Start, newpos [%d]',__METHOD__,$newpos));

		if ($newpos == $this->ls_txLastRepos) {
			if (++$this->ls_txReposCount > 10) {
				Log::error(sprintf('%s:   ! ZRPOS to [%ld] limit reached',__METHOD__,$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',__METHOD__,$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
	{
		Log::debug(sprintf('%s: + Start [%d] (%d)',__METHOD__,strlen($data),$frame));

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

			$crc = self::LSZ_INIT_CRC32;

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

			$this->client->buffer_add(chr(self::ZDLE).chr($frame));
			$crc = $this->CRC32_UPDATE($frame,$crc);

			/*
			*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 = $this->CRC32_FINISH($crc);
			$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:   - CRC16',__METHOD__));

			$crc = self::LSZ_INIT_CRC16;

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

			$this->client->buffer_add(chr(self::ZDLE).chr($frame));
			$crc = $this->CRC16USD_UPDATE($frame,$crc);

			/*
			*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 &= 0xffff;
			$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: + Start',__METHOD__));

		$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: + Start [%s]',__METHOD__,$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:   - ZRPOS to [%d]',__METHOD__,$send->filepos));
				break;

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

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

				return self::LSZ_ERROR;

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

				Log::debug(sprintf('%s:   - Strange answer on ZFILE:  %d',__METHOD__,$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:   - send ZDATA at [%d]',__METHOD__,$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: Read error'));

				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:   - ZSKIP',__METHOD__));

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

					/* Ok, position ACK */
					case self::ZACK:
						$this->ls_txLastACK = $this->ls_fetchlong($this->ls_rxHdr);
						Log::debug(sprintf('%s:   - ZACK',__METHOD__),['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',__METHOD__));

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

						Log::debug(sprintf('%s:   - ZRPOS [%d]',__METHOD__,$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:   ? Something strange [%d]',__METHOD__,$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:   - ZACK after EOF',__METHOD__));
							$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:   ? Something strange after ZEOF [%d]',__METHOD__,$rc));

							if ($rc < 0)
								return $rc;

							$trys++;
					}

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

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

					return self::LSZ_ERROR;
				}
			}
		}

		Log::error(sprintf('%s:   ? Something strange or timeout [%d]',__METHOD__,$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: + Start [%s]',__METHOD__,$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]',__METHOD__,$rc));

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

		} while ($trys < 10);

		Log::error(sprintf('%s:   ? Something strange or timeout [%d]',__METHOD__,$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: + Start',__METHOD__));

		/* 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);
		$crc = $this->CRC16USD_UPDATE($frametype,self::LSZ_INIT_CRC16);

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

		$crc = ($crc&0xffff);
		$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: + Start [%s]',__METHOD__,$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]',__METHOD__,$rc));

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

		} while ($trys < 10);

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