<?php

namespace App\Classes\FTN;

use Carbon\Carbon;
use Carbon\Exceptions\InvalidFormatException;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Validator as ValidatorResult;

use App\Classes\FTN as FTNBase;
use App\Models\{Address,Domain,Zone};
use App\Rules\{TwoByteInteger,TwoByteIntegerWithZero};
use App\Traits\EncodeUTF8;

/**
 * Class Message
 * Represents the structure of a message in a packet
 * NOTE: FTN Echomail Messages are ZONE agnostic.
 *
 * @package App\Classes
 */
class Message extends FTNBase
{
	use EncodeUTF8;

	private const LOGKEY = 'FM-';

	private const cast_utf8 = [
		'user_to',
		'user_from',
		'subject',
		'message',
		'message_src',
		'origin',
		'tearline',
		'tagline',
		'dump',
	];

	// Single value kludge items
	private array $_kludge = [
		'chrs' => 'CHRS: ',
		'charset' => 'CHARSET: ',
		'codepage' => 'CODEPAGE: ',
		'dbid' => 'DBID: ',
		'pid' => 'PID: ',
		'tid' => 'TID: ',
	];

	// Flags for messages
	/** @var int Private message */
	public const FLAG_PRIVATE		= 1<<0;
	/** @var int Crash priority message (Crash + Hold = Direct) */
	public const FLAG_CRASH			= 1<<1;
	/** @var int Read by addressee */
	public const FLAG_RECD			= 1<<2;
	/** @var int Message has been sent */
	public const FLAG_SENT			= 1<<3;
	/** @var int File attached (filename in subject) */
	public const FLAG_FILEATTACH	= 1<<4;
	/** @var int Message in transit to another destination */
	public const FLAG_INTRANSIT 	= 1<<5;
	/** @var int Unknown destination - node not in nodelist */
	public const FLAG_ORPHAN		= 1<<6;
	/** @var int Kill after mailing */
	public const FLAG_KILLSENT 		= 1<<7;
	/** @var int Message originated here */
	public const FLAG_LOCAL			= 1<<8;
	/** @var int Hold message here to be collected (Crash + Hold = Direct) */
	public const FLAG_HOLD 			= 1<<9;
	/** @var int Reserved for future use by FTS-0001 */
	public const FLAG_UNUSED_10		= 1<<10;
	/** @var int Requesting a file (filename in subject) */
	public const FLAG_FREQ			= 1<<11;
	/** @var int Return Receipt requested */
	public const FLAG_RETRECEIPT	= 1<<12; // (RRQ)
	/** @var int Return Receipt message in response to an RRQ */
	public const FLAG_ISRETRECEIPT	= 1<<13;
	/** @var int Request audit trail */
	public const FLAG_AUDITREQ		= 1<<14; // (ARQ)
	/** @var int Requesting a file update (filename in subject) */
	public const FLAG_FILEUPDATEREQ = 1<<15; // (URQ)
	/**	@var int Echomail has been scanned out */
	public const FLAG_ECHOMAIL		= 1<<16;
	/** @var int Use packet password on the subject line for this message */
	public const FLAG_PKTPASSWD		= 1<<17;

	// FTS-0001.016 Message header 32 bytes node, net, flags, cost, date
	public const HEADER_LEN = 0x20;		// Length of message header
	private const header = [			// Struct of message header
		'onode'	=> [0x00,'v',2],		// Originating Node
		'dnode'	=> [0x02,'v',2],		// Destination Node
		'onet'	=> [0x04,'v',2],		// Originating Net
		'dnet'	=> [0x06,'v',2],		// Destination Net
		'flags'	=> [0x08,'v',2],		// Message Flags
		'cost'	=> [0x0a,'v',2],		// Send Cost
		'date'	=> [0x0c,'a20',20]		// Message Date			FTS-0001.016 Date: upto 20 chars null terminated
	];

	private const USER_FROM_LEN	= 36;	// FTS-0001.016 From Name: upto 36 chars null terminated
	private const USER_TO_LEN	= 36;	// FTS-0001.016 To Name: upto 36 chars null terminated
	private const SUBJECT_LEN	= 71;	// FTS-0001.016 Subject: upto 72 chars null terminated
	private const AREATAG_LEN	= 35;	//

	private ?ValidatorResult $errors = NULL;			// Packet validation
	private array $header;						// Message Header
	private Collection $kludge;					// Hold kludge items

	public string $dump;						// Raw message

	private string $user_from;					// User message is From
	private string $user_to;					// User message is To
	private string $subject;					// Message subject

	private string $msgid;						// MSG ID
	private string $replyid;					// Reply ID
	private string $gateid;						// MSG ID if the message came via gate

	private string $echoarea;					// FTS-0004.001
	private string $intl;						// Netmail details

	private string $message;					// The parsed message content
	private string $message_src;				// The actual message part

	private ?string $tagline;
	private ?string $tearline;
	private string $origin;						// FTS-0004.001

	private int $tzutc;

	private array $point;						// Point the message belongs to (Netmail)
	private array $src;							// Address the message is from
	private array $dst;							// Address the message is to

	private Collection $rescanned;				// Message was created as a result of a rescan
	private Collection $path;					// FTS-0004.001 The message PATH lines
	private Collection $seenby;					// FTS-0004.001 The message SEEN-BY lines
	private Collection $via;					// The path the message has gone using Via lines (Netmail)
	private Collection $unknown;				// Temporarily hold attributes we have no logic for.

	public bool $packed = FALSE;				// Has the message been packed successfully

	// Convert characters into printable chars
	// https://int10h.org/oldschool-pc-fonts/readme/#437_charset
	private const CP437 = [
		0x01 => 0x263a, 0x02 => 0x263b, 0x03 => 0x2665, 0x04 => 0x2666,
		0x05 => 0x2663, 0x06 => 0x2660, 0x07 => 0x2022, 0x08 => 0x25d8,
		0x09 => 0x25cb, 0x0a => 0x2509, 0x0b => 0x2642, 0x0c => 0x2640,
		0x0d => 0x266a, 0x0e => 0x266b, 0x0f => 0x263c,
		0x10 => 0x25ba, 0x11 => 0x25ca, 0x12 => 0x2195, 0x13 => 0x203c,
		0x14 => 0x00b6, 0x15 => 0x00a7, 0x16 => 0x25ac, 0x17 => 0x21a8,
		0x18 => 0x2191, 0x19 => 0x2193, 0x1a => 0x2192, 0x1b => 0x2190,
		0x1c => 0x221f, 0x1d => 0x2194, 0x1e => 0x25bc, 0x1f => 0x25bc,
		0x7f => 0x2302,
		0x80 => 0x00c7, 0x81 => 0x00fc, 0x82 => 0x00e9, 0x83 => 0x00e2,
		0x84 => 0x00e4, 0x85 => 0x00e0, 0x86 => 0x00e5, 0x87 => 0x00e7,
		0x88 => 0x00ea, 0x89 => 0x00eb, 0x8a => 0x00e8, 0x8b => 0x00ef,
		0x8c => 0x00ee, 0x8d => 0x00ec, 0x8e => 0x00c4, 0x8f => 0x00c5,
		0x90 => 0x00c9, 0x91 => 0x00e6, 0x92 => 0x00c6, 0x93 => 0x00f4,
		0x94 => 0x00f6, 0x95 => 0x00f2, 0x96 => 0x00fb, 0x97 => 0x00f9,
		0x98 => 0x00ff, 0x99 => 0x00d6, 0x9a => 0x00dc, 0x9b => 0x00a2,
		0x9c => 0x00a3, 0x9d => 0x00a5, 0x9e => 0x20a7, 0x9f => 0x0192,
		0xa0 => 0x00e1, 0xa1 => 0x00ed, 0xa2 => 0x00f3, 0xa3 => 0x00fa,
		0xa4 => 0x00f1, 0xa5 => 0x00d1, 0xa6 => 0x00aa, 0xa7 => 0x00ba,
		0xa8 => 0x00bf, 0xa9 => 0x2310, 0xaa => 0x00ac, 0xab => 0x00bd,
		0xac => 0x00bc, 0xad => 0x00a1, 0xae => 0x00ab, 0xaf => 0x00bb,
		0xb0 => 0x2591, 0xb1 => 0x2592, 0xb2 => 0x2593, 0xb3 => 0x2502,
		0xb4 => 0x2524, 0xb5 => 0x2561, 0xb6 => 0x2562, 0xb7 => 0x2556,
		0xb8 => 0x2555, 0xb9 => 0x2563, 0xba => 0x2551, 0xbb => 0x2557,
		0xbc => 0x255d, 0xbd => 0x255c, 0xbe => 0x255b, 0xbf => 0x2510,
		0xc0 => 0x2514, 0xc1 => 0x2534, 0xc2 => 0x252c, 0xc3 => 0x251c,
		0xc4 => 0x2500, 0xc5 => 0x253c, 0xc6 => 0x255e, 0xc7 => 0x255f,
		0xc8 => 0x255a, 0xc9 => 0x2554, 0xca => 0x2569, 0xcb => 0x2566,
		0xcc => 0x2560, 0xcd => 0x2550, 0xce => 0x256c, 0xcf => 0x2567,
		0xd0 => 0x2568, 0xd1 => 0x2564, 0xd2 => 0x2565, 0xd3 => 0x2559,
		0xd4 => 0x2558, 0xd5 => 0x2552, 0xd6 => 0x2553, 0xd7 => 0x256b,
		0xd8 => 0x256a, 0xd9 => 0x2518, 0xda => 0x250c, 0xdb => 0x2588,
		0xdc => 0x2584, 0xdd => 0x258c, 0xde => 0x2590, 0xdf => 0x2580,
		0xe0 => 0x03b1, 0xe1 => 0x00df, 0xe2 => 0x0393, 0xe3 => 0x03c0,
		0xe4 => 0x03a3, 0xe5 => 0x03c3, 0xe6 => 0x00b5, 0xe7 => 0x03c4,
		0xe8 => 0x03a6, 0xe9 => 0x0398, 0xea => 0x03a9, 0xeb => 0x03b4,
		0xec => 0x221e, 0xed => 0x03c6, 0xee => 0x03b5, 0xef => 0x2229,
		0xf0 => 0x2261, 0xf1 => 0x00b1, 0xf2 => 0x2265, 0xf3 => 0x2264,
		0xf4 => 0x2320, 0xf5 => 0x2321, 0xf6 => 0x00f7, 0xf7 => 0x2248,
		0xf8 => 0x00b0, 0xf9 => 0x2219, 0xfa => 0x00b7, 0xfb => 0x221a,
		0xfc => 0x207f, 0xfd => 0x00b2, 0xfe => 0x25a0, 0xff => 0x00a0,
	];

	public function __construct(Zone $zone=NULL)
	{
		$this->zone = $zone;

		$this->header = [];

		$this->user_from = '';
		$this->user_to = '';
		$this->subject = '';
		$this->message = '';

		$this->msgid = '';
		$this->gateid = '';
		$this->replyid = '';

		$this->echoarea = '';
		$this->intl = '';

		$this->tearline = NULL;
		$this->tagline = NULL;
		$this->origin = '';

		$this->tzutc = 0;

		$this->src = [];
		$this->dst = [];
		$this->point = [];

		$this->kludge = collect();
		$this->rescanned = collect();
		$this->path = collect();
		$this->seenby = collect();
		$this->via = collect();
		$this->unknown = collect();
	}

	// Fix for a call to pluck('date') (which is resolved via __get()), but it returns false.
	public function __isset($key)
	{
		return (bool)$this->{$key};
	}

	public function __get($key)
	{
		switch ($key) {
			// From Addresses
			case 'fz': return Arr::get($this->src,'z');
			case 'fn': return $this->src ? Arr::get($this->src,'n') : Arr::get($this->header,'onet');
			case 'ff': return $this->src ? Arr::get($this->src,'f') : Arr::get($this->header,'onode');
			case 'fp': return Arr::get($this->point,'src',Arr::get($this->src,'p',Arr::get($this->header,'opoint',0)));
			case 'fd': return Arr::get($this->src,'d');

			case 'fdomain':
				// We'll use the zone's domain if this method class was called with a zone
				if ($this->zone && (($this->zone->domain->name === Arr::get($this->src,'d')) || ! Arr::get($this->src,'d')))
					return $this->zone->domain;

				// If we get the domain from the packet, we'll find it
				if ($x=Arr::get($this->src,'d')) {
					return Domain::where('name',$x)->single();
				}

				return NULL;

			case 'tdomain':
				// We'll use the zone's domain if this method class was called with a zone
				if ($this->zone && (($this->zone->domain->name === Arr::get($this->dst,'d')) || ! Arr::get($this->dst,'d')))
					return $this->zone->domain;

				// If we get the domain from the packet, we'll find it
				if ($x=Arr::get($this->dst,'d')) {
					return Domain::where('name',$x)->single();
				}

				// Otherwise we'll assume the same as the source domain
				return $this->fdomain ?: NULL;

			case 'fzone':
				// Use the zone if this class was called with it.
				if ($this->zone && ($this->fz === $this->zone->zone_id))
					return $this->zone;

				if ($this->fdomain) {
					if (($x=$this->fdomain->zones->search(function($item) { return $item->zone_id === $this->fz; })) !== FALSE)
						return $this->fdomain->zones->get($x);
				}

				// No domain, so we'll use the default zone
				return Zone::where('zone_id',$this->fz)
					->where('default',TRUE)
					->single();

			case 'tzone':
				// Use the zone if this class was called with it.
				if ($this->zone && ($this->tz === $this->zone->zone_id))
					return $this->zone;

				if ($this->tdomain) {
					if (($x=$this->tdomain->zones->search(function($item) { return $item->zone_id === $this->tz; })) !== FALSE)
						return $this->tdomain->zones->get($x);
				}

				// No domain, so we'll use the default zone
				return Zone::where('zone_id',$this->tz)
					->where('default',TRUE)
					->single();

			// To Addresses
			// Echomail doesnt have a zone, so we'll use the source zone
			case 'tz': return Arr::get($this->echoarea ? $this->src : $this->dst,'z');
			case 'tn': return Arr::get($this->header,'dnet');
			case 'tf': return Arr::get($this->header,'dnode');
			case 'tp': return Arr::get($this->point,'dst',Arr::get($this->header,'dpoint',0));

			case 'fftn':
			case 'fftn_o':
			case 'tftn':
			case 'tftn_o':
				return parent::__get($key);

			// For 5D we need to include the domain
			case 'fboss':
				return sprintf('%d:%d/%d',$this->fz,$this->fn,$this->ff).(($x=$this->fdomain) ? '@'.$x->name : '');
			case 'tboss':
				return sprintf('%d:%d/%d',$this->tz,$this->tn,$this->tf).(($x=$this->tdomain) ? '@'.$x->name : '');
			case 'fboss_o':
				return Address::findFTN($this->fboss);
			case 'tboss_o':
				return Address::findFTN($this->tboss);

			case 'date':
				try {
					if (str_contains($x=chop(Arr::get($this->header,$key)),"\x00"))
						throw new \Exception('Date contains null values.');

					return Carbon::createFromFormat('d M y  H:i:s O',
						sprintf('%s %s%04d',$x,($this->tzutc < 0) ? '-' : '+',abs($this->tzutc)));

				} catch (InvalidFormatException|\Exception $e) {
					Log::error(sprintf('%s:! Date doesnt parse [%s] (%s)',self::LOGKEY,$e->getMessage(),Arr::get($this->header,$key)));
					throw new \Exception(sprintf('%s (%s)',$e->getMessage(),hex_dump(Arr::get($this->header,$key))));
				}

			case 'flags':
			case 'cost':
				return Arr::get($this->header,$key);

			case 'tzutc':

			case 'user_to':
			case 'user_from':
			case 'subject':
			case 'echoarea':

			case 'msgid':
			case 'replyid':
			case 'gateid':

			case 'message':
			case 'message_src':

			case 'tearline':
			case 'tagline':
			case 'origin':

			case 'kludge':
			case 'rescanned':
			case 'path':
			case 'seenby':
			case 'unknown':
			case 'via':

			case 'errors':
				return $this->{$key};

			case 'dbid':
				return $this->kludge->get($key);

			default:
				throw new \Exception('Unknown key: '.$key);
		}
	}

	/**
	 * When we serialise this object, we'll need to utf8_encode some values
	 *
	 * @return array
	 */
	public function __serialize(): array
	{
		return $this->encode();
	}

	public function __set($key,$value)
	{
		switch ($key) {
			case 'flags':
			case 'header':
			case 'tzutc':

			case 'user_from':
			case 'user_to':
			case 'subject':

			case 'gateid':
			case 'msgid':
			case 'replyid':

			case 'echoarea':
			case 'intl':

			case 'message':

			case 'kludge':
			case 'tagline':
			case 'tearline':
			case 'origin':
			case 'seenby':
			case 'path':
			case 'via':
				$this->{$key} = $value;
				break;

			default:
				throw new \Exception('Unknown key: '.$key);
		}
	}

	/**
	 * Export an FTN message, ready for sending.
	 *
	 * @return string
	 */
	public function __toString(): string
	{
		$return = pack(collect(self::header)->pluck(1)->join(''),
			$this->ff,	// Originating Node
			$this->tf,			// Destination Node
			$this->fn,			// Originating Net
			$this->tn,			// Destination Net
			$this->flags&~(self::FLAG_INTRANSIT|self::FLAG_LOCAL),	// Turn off our local/intransit bits
			$this->cost,
			$this->date->format('d M y  H:i:s'),
		);

		$return .= $this->user_to."\00";
		$return .= $this->user_from."\00";
		$return .= $this->subject."\00";

		if (! $this->isNetmail())
			$return .= sprintf("AREA:%s\r",strtoupper($this->echoarea));

		// If the message is local, then our kludges are not in the msg itself, we'll add them
		if ($this->isFlagSet(self::FLAG_LOCAL)) {
			if ($this->isNetmail()) {
				$return .= sprintf("\01INTL %s\r",$this->intl);

				if ($this->fp)
					$return .= sprintf("\01FMPT %d\r",$this->fp);
				if ($this->tp)
					$return .= sprintf("\01TOPT %d\r",$this->tp);
			}

			$return .= sprintf("\01TZUTC: %s\r",str_replace('+','',$this->date->getOffsetString('')));

			// Add some kludges
			$return .= sprintf("\01MSGID: %s\r",$this->msgid);

			if ($this->replyid)
				$return .= sprintf("\01REPLY: %s\r",$this->replyid);

			if ($this->gateid)
				$return .= sprintf("\01GATE: %s\r",$this->gateid);

			foreach ($this->_kludge as $k=>$v) {
				if ($x=$this->kludge->get($k))
					$return .= sprintf("\01%s%s\r",$v,$x);
			}

			$return .= $this->message;
			if ($this->tagline)
				$return .= sprintf("... %s\r",$this->tagline);
			if ($this->tearline)
				$return .= sprintf("--- %s\r",$this->tearline);
			if ($this->origin)
				$return .= sprintf(" * Origin: %s\r",$this->origin);

		} else {
			if ($this->isFlagSet(self::FLAG_INTRANSIT) && $this->isNetmail()) {
				$return .= sprintf("\01INTL %s\r",$this->intl);

				if ($this->fp)
					$return .= sprintf("\01FMPT %d\r",$this->fp);
				if ($this->tp)
					$return .= sprintf("\01TOPT %d\r",$this->tp);
			}

			$return .= $this->message;
		}

		if ($this->isNetmail()) {
			foreach ($this->via as $via)
				$return .= sprintf("\01Via %s\r",$via);

		} else {
			// Seenby & PATH - FSC-0068
			$return .= sprintf("SEEN-BY: %s\r",wordwrap(optimize_path($this->seenby)->join(' '),70,"\rSEEN-BY: "));
			$return .= sprintf("\01PATH: %s\r",wordwrap(optimize_path($this->path)->join(' '),73,"\rPATH: "));
		}

		$return .= "\00";

		return $return;
	}

	/**
	 * When we unserialize, we'll restore (utf8_decode) some values
	 *
	 * @param array $values
	 */
	public function __unserialize(array $values): void
	{
		$this->decode($values);
	}

	/**
	 * Parse a message from a packet
	 *
	 * @param string $msg
	 * @param Zone|null $zone
	 * @return Message
	 * @throws \Exception
	 */
	public static function parseMessage(string $msg,Zone $zone=NULL): self
	{
		Log::info(sprintf('%s:= Processing message [%d] bytes from zone [%d]',self::LOGKEY,strlen($msg),$zone?->zone_id));

		$o = new self($zone);
		$o->dump = $msg;

		try {
			$o->header = unpack(self::unpackheader(self::header),substr($msg,0,self::HEADER_LEN));

		} catch (\Exception $e) {
			Log::error(sprintf('%s:! Error bad packet header',self::LOGKEY));
			$validator = Validator::make([
				'header' => substr($msg,0,self::HEADER_LEN),
			],[
				'header' => [function ($attribute,$value,$fail) use ($e) { return $fail($e->getMessage()); }]
			]);

			if ($validator->fails())
				$o->errors = $validator;

			return $o;
		}

		$ptr = 0;

		// To User
		$o->user_to = strstr(substr($msg,self::HEADER_LEN+$ptr),"\x00",TRUE);
		$ptr += strlen($o->user_to)+1;

		// From User
		$o->user_from = strstr(substr($msg,self::HEADER_LEN+$ptr),"\x00",TRUE);
		$ptr += strlen($o->user_from)+1;

		// Subject
		$o->subject = strstr(substr($msg,self::HEADER_LEN+$ptr),"\x00",TRUE);
		$ptr += strlen($o->subject)+1;

		// Check if this is an Echomail
		if (! strncmp(substr($msg,self::HEADER_LEN+$ptr),'AREA:',5)) {
			$o->echoarea = strtoupper(substr($msg,self::HEADER_LEN+$ptr+5,strpos($msg,"\r",self::HEADER_LEN+$ptr+5)-(self::HEADER_LEN+$ptr+5)));
			$ptr += strlen($o->echoarea)+5+1;
		}

		$o->unpackMessage(substr($msg,self::HEADER_LEN+$ptr));

		if (($x=$o->validate())->fails()) {
			Log::debug(sprintf('%s:! Message fails validation (%s@%s->%s@%s)',self::LOGKEY,$o->user_from,$o->fftn,$o->user_to,$o->tftn),['result'=>$x->errors()]);
			//throw new \Exception('Message validation fails:'.join(' ',$x->errors()->all()));
		}

		return $o;
	}

	/**
	 * Translate the string into something printable via the web
	 *
	 * @param string $string
	 * @param array $skip
	 * @return string
	 */
	public static function tr(string $string,array $skip=[0x0a,0x0d]): string
	{
		$tr = [];

		foreach (self::CP437 as $k=>$v) {
			if (in_array($k,$skip))
				continue;

			$tr[chr($k)] = '&#'.$v;
		}

		return strtr($string,$tr);
	}

	/**
	 * If this message doesnt have an AREATAG, then its a netmail.
	 *
	 * @return bool
	 */
	public function isNetmail(): bool
	{
		return ! $this->echoarea;
	}

	/**
	 * Return an array of flag descriptions
	 *
	 * @return Collection
	 *
	 * http://ftsc.org/docs/fsc-0001.000
	 *       AttributeWord   bit       meaning
	 * ---       --------------------
	 * 0  +    Private
	 * 1  + s  Crash
	 * 2       Recd
	 * 3       Sent
	 * 4  +    FileAttached
	 * 5       InTransit
	 * 6       Orphan
	 * 7       KillSent
	 * 8       Local
	 * 9    s  HoldForPickup
	 * 10  +    unused
	 * 11    s  FileRequest
	 * 12  + s  ReturnReceiptRequest
	 * 13  + s  IsReturnReceipt
	 * 14  + s  AuditRequest
	 * 15    s  FileUpdateReq
	 *
	 * s - this bit is supported by SEAdog only
	 * + - this bit is not zeroed before packeting
	 */
	public function flags(): Collection
	{
		return collect([
			'private' => $this->isFlagSet(self::FLAG_PRIVATE),
			'crash' => $this->isFlagSet(self::FLAG_CRASH),
			'recd' => $this->isFlagSet(self::FLAG_RECD),
			'sent' => $this->isFlagSet(self::FLAG_SENT),
			'fileattach' => $this->isFlagSet(self::FLAG_FILEATTACH),
			'intransit' => $this->isFlagSet(self::FLAG_INTRANSIT),
			'orphan' => $this->isFlagSet(self::FLAG_ORPHAN),
			'killsent' => $this->isFlagSet(self::FLAG_KILLSENT),
			'local' => $this->isFlagSet(self::FLAG_LOCAL),
			'hold' => $this->isFlagSet(self::FLAG_HOLD),
			'unused-10' => $this->isFlagSet(self::FLAG_UNUSED_10),
			'filereq' => $this->isFlagSet(self::FLAG_FREQ),
			'receipt-req' => $this->isFlagSet(self::FLAG_RETRECEIPT),
			'receipt' => $this->isFlagSet(self::FLAG_ISRETRECEIPT),
			'audit' => $this->isFlagSet(self::FLAG_AUDITREQ),
			'fileupdate' => $this->isFlagSet(self::FLAG_FILEUPDATEREQ),
		]);
	}

	private function isFlagSet($flag): bool
	{
		return ($this->flags & $flag);
	}

	/**
	 * Process the data after the ORIGIN
	 * There may be kludge lines after the origin - notably SEEN-BY
	 *
	 * @param string $message
	 */
	private function parseOrigin(string $message)
	{
		// Split out each line
		$result = collect(explode("\r",$message))->filter();

		while ($result->count()) {
			$v = $result->shift();

			foreach ($this->_kludge as $a => $b) {
				if ($t = $this->kludge($b,$v)) {
					$this->kludge->put($a,$t);
					break;
				}
			}

			if ($t = $this->kludge('SEEN-BY: ', $v))
				$this->seenby->push($t);

			elseif ($t = $this->kludge('PATH: ', $v))
				$this->path->push($t);

			elseif ($t = $this->kludge(' \* Origin: ',$v)) {
				// Check in case there is a kludge that starts with SOH
				if ($soh=strpos($t,"\x01")) {
					$this->origin = substr($t,0,$soh);
					$result->push(substr($t,$soh+1));

				} else {
					$this->origin = $t;
				}
			}

			// We got unknown Kludge lines in the origin
			else
				$this->unknown->push($v);
		}
	}

	/**
	 * Extract information out of the message text.
	 *
	 * @param string $message
	 * @throws \Exception
	 */
	public function unpackMessage(string $message): void
	{
		// Remove DOS \n\r
		$message = preg_replace("/\n\r/","\r",$message);
		$message = preg_replace("/\r\n/","\r",$message);

		// Split out the <SOH> lines
		$result = collect(explode("\x01",$message))->filter();

		$this->message = '';
		$this->message_src = '';
		$msgpos = 0;
		$haveOrigin = FALSE;

		while ($result->count()) {
			// $kl is our line starting with a \x01 kludge delimiter
			$kl = $result->shift();

			// Search for \r - if that is the end of the line, then its a kludge
			$retpos = strpos($kl,"\r");
			$msgpos += 1+strlen($kl);	// SOH+text
			$t = '';

			// If there is a return in this middle of this line, that means the text message starts after the return,
			// ie: SOH+text\rmessage

			// Just in case we already parsed this as part of our message, we'll continue, since its probably a
			// binary message with a SOH inside it.
			if (strlen($this->message) || ($retpos !== strlen($kl)-1)) {
				// If there was no return, its part of the message, so we need to add back the SOH.
				if ($retpos === FALSE) {
					$this->message .= "\x01".$kl;

					continue;
				}

				// Check if this has the origin line. Anything before is the message, anything after the origin
				// line is also kludge data.
				if ($originpos=strrpos($kl,"\r * Origin: ")) {
					// Anything after the return (from the kludge) is a message.
					if (! $this->message) {
						$this->message .= substr($kl,$retpos+1,$originpos-$retpos-1);

					// But if we are already sourcing a message, then its part of it message.
					} else {
						$this->message .= "\x01".substr($kl,0,$originpos);

						$retpos = 0;
					}

					// See if we have a tagline
					if ($tl=strrpos($kl,"\r... ")) {
						$tlr = strpos(substr($kl,$tl+6),"\r");

						$this->tagline = substr($kl,$tl+5,$tlr);
					}

					// Message is finished, now we are parsing origin data (and more kludges)
					$this->parseOrigin(substr($kl,$originpos+1));
					$haveOrigin = TRUE;

					// Our message source (for resending, is everything up to the end of the origin line.
					$this->message_src = substr($message, 0, $msgpos - (1+strlen($kl)) + $originpos + 12 + strlen($this->origin) + 1);
					$kl = substr($kl,0,$retpos);

				// The message is the rest?
				// Netmails dont generally have an origin line
				} elseif (strlen($kl) > $retpos+1) {
					// We still have some text to process, add it to the list
					if ($haveOrigin && ($retpos+1 < strlen($kl))) {
						$result->push(substr($kl,$retpos+1));

					// If this was the overflow from echomail, then our message_src would be defined, and thus its not part of the message
					} else {
						$this->message .= substr($kl,$retpos+1);
						$this->message_src = substr($message, 0, $msgpos);
					}

					$kl = substr($kl,0,$retpos);
				}

				if (! $kl)
					continue;
			}

			foreach ($this->_kludge as $a => $b)
				if ($t = $this->kludge($b,$kl)) {
					$this->kludge->put($a,$t);
					break;
				}

			// There is more text.
			if ($t)
				continue;

			// From point: <SOH>"FMPT <point number><CR>
			if ($t = $this->kludge('FMPT ',$kl))
				$this->point['src'] = $t;

			/*
			 * The INTL control paragraph shall be used to give information about
			 * the zone numbers of the original sender and the ultimate addressee
			 * of a message.
			 *
			 * <SOH>"INTL "<destination address>" "<origin address><CR>
			 */
			elseif ($t = $this->kludge('INTL ',$kl)) {
				$this->intl = $t;

				// INTL kludge is in Netmail, so we'll do some validation:
				list($dst,$src) = explode(' ',$t);

				$this->src = Address::parseFTN($src);
				if (($this->src['n'] !== $this->fn) || ($this->src['f'] !== $this->ff)) {
					Log::error(sprintf('%s:! INTL src address [%s] doesnt match packet',self::LOGKEY,$src),['src'=>$this->src,'fn'=>$this->fn,'ff'=>$this->ff]);
				}

				$this->dst = Address::parseFTN($dst);
				if (($this->dst['n'] !== $this->tn) || ($this->dst['f'] !== $this->tf)) {
					Log::error(sprintf('%s:! INTL dst address [%s] doesnt match packet',self::LOGKEY,$dst),['dst'=>$this->dst,'tn'=>$this->tn,'tf'=>$this->tf]);
				}
			}

			elseif (($t = $this->kludge('TZUTC: ',$kl)) && is_numeric($t))
				$this->tzutc = $t;

			elseif ($t = $this->kludge('MSGID: ',$kl))
				$this->msgid = $t;

			elseif ($t = $this->kludge('REPLY: ',$kl))
				$this->replyid = $t;

			elseif ($t = $this->kludge('GATE: ',$kl))
				$this->gateid = $t;

			elseif ($t = $this->kludge('PATH: ',$kl))
				$this->path->push($t);

			elseif ($t = $this->kludge('RESCANNED ',$kl))
				$this->rescanned->push($t);

			elseif ($t = $this->kludge('SEEN-BY: ',$kl))
				$this->seenby->push($t);

			// To Point: <SOH>TOPT <point number><CR>
			elseif ($t = $this->kludge('TOPT ',$kl))
				$this->point['dst'] = $t;

			// <SOH>Via <FTN Address> @YYYYMMDD.HHMMSS[.Precise][.Time Zone] <Program Name> <Version> [Serial Number]<CR>
			// @todo The via line is still showing in the main message? https://clrghouz.bbs.dege.au/netmail/view/707
			// @todo Need to make sure that the CRC doesnt include this
			elseif ($t = $this->kludge('Via ',$kl))
				$this->via->push($t);

			// We got a kludge line we dont know about
			else
				$this->unknown->push(chop($kl,"\r"));
		}

		// Work out our zone/point
		// http://ftsc.org/docs/fsc-0068.001
		// MSGID should be the basis of the source, if it cannot be obtained from the origin
		// If the message was gated, we'll use the gateid
		$m = [];
		if ($this->origin && preg_match('#\(([0-9]+:[0-9]+/[0-9]+)?\.?([0-9]+)?@?([A-Za-z-_~]+)?\)$#',$this->origin,$m)) {
			$this->src = Address::parseFTN($m[1].((isset($m[2]) && $m[2] != '') ? '.'.$m[2] : '').(isset($m[3]) ? '@'.$m[3] : ''));

		} elseif (($this->msgid || $this->gateid) && preg_match('#([0-9]+:[0-9]+/[0-9]+)?\.?([0-9]+)?(\.[0-9]+)?@?([A-Za-z-_~]+)?\ +#',$this->gateid ?: $this->msgid,$m)) {
			try {
				$this->src = Address::parseFTN($m[1].((isset($m[2]) && $m[2] != '') ? '.'.$m[2] : '').(isset($m[4]) ? '@'.$m[4] : ''));
			} catch (\Exception $e) {
				Log::error(sprintf('%s:! MSGID [%s] address is invalid [%s]',self::LOGKEY,$this->msgid,$e->getMessage()));
			}

		// Otherwise get it from our zone object and packet header
		} elseif ($this->zone) {
			$this->src = Address::parseFTN(sprintf('%d:%d/%d.%d@%s',$this->zone->zone_id,$this->fn,$this->ff,$this->fp,$this->zone->domain->name));
		}
	}

	/**
	 * Validate details about this message
	 *
	 * @return \Illuminate\Contracts\Validation\Validator
	 */
	public function validate(): ValidatorResult
	{
		// Check lengths
		$validator = Validator::make([
			'user_from' => $this->user_from,
			'user_to' => $this->user_to,
			'subject' => $this->subject,
			'onode'	=> $this->ff,
			'dnode'	=> $this->tf,
			'onet' => $this->fn,
			'dnet' => $this->tn,
			'flags'	=> $this->flags,
			'cost' => $this->cost,
			'echoarea' => $this->echoarea,
			'ozone' => $this->fz,
			'dzone' => $this->tz,
		],[
			'user_from' => 'required|min:1|max:'.self::USER_FROM_LEN,
			'user_to' => 'required|min:1|max:'.self::USER_TO_LEN,
			'subject' => 'present|max:'.self::SUBJECT_LEN,
			'onode' => ['required',new TwoByteIntegerWithZero],
			'dnode' => ['required',new TwoByteIntegerWithZero],
			'onet' => ['required',new TwoByteInteger],
			'dnet' => ['required',new TwoByteInteger],
			'flags' => 'required|numeric',
			'cost' => 'required|numeric',
			'echoarea' => 'nullable|max:'.self::AREATAG_LEN,
			'ozone' => ['required'],
			'dzone' => ['required']
		]);

		$validator->after(function($validator) {
			if ($this->zone->domain->flatten) {
				if (! $this->zone->domain->zones->pluck('zone_id')->contains($this->fz))
					$validator->errors()->add('invalid-zone',sprintf('Message zone [%d] doesnt match any zone in domain for packet zone [%d].',$this->fz,$this->zone->zone_id));

			} else {
				if ($this->zone->zone_id !== $this->fz)
					$validator->errors()->add('invalid-zone',sprintf('Message zone [%d] doesnt match packet zone [%d].',$this->fz,$this->zone->zone_id));
			}

			if (! $this->fboss_o)
				$validator->errors()->add('from',sprintf('Undefined Node [%s] sent message.',$this->fboss));
			if (! $this->tboss_o)
				$validator->errors()->add('to',sprintf('Undefined Node [%s] for destination.',$this->tboss));
		});

		if ($validator->fails())
			$this->errors = $validator;

		return $validator;
	}
}