<?php

namespace App\Classes;

use Illuminate\Support\Arr;

use App\Exceptions\InvalidFidoPacketException;
use App\Traits\GetNode;

/**
 * Class FTNMessage
 * NOTE: FTN Echomail Messages are ZONE agnostic.
 *
 * @package App\Classes
 */
class FTNMessage extends FTN
{
	use GetNode;

	private $src = NULL;       // SRC N/F from packet
	private $dst = NULL;       // DST N/F from packet

	private $flags = NULL;     // Flags from packet
	private $cost = 0;         // Cost from packet

	// @todo need to validate these string lengths when creating packet.
	private $from = NULL;       // FTS-0001.016 From Name: upto 36 chars null terminated
	private $to = NULL;         // FTS-0001.016 To Name: upto 36 chars null terminated
	private $subject = NULL;    // FTS-0001.016 Subject: upto 72 chars null terminated
	private $date = NULL;       // FTS-0001.016 Date: upto 20 chars null terminated

	private $message = NULL;    // The actual message content
	private $echoarea = NULL;   // FTS-0004.001
	private $intl = NULL;
	private $msgid = NULL;
	private $reply = NULL;      // Message thread reply source
	private $origin = NULL;     // FTS-0004.001

	private $kludge = [];       // Hold kludge items
	private $path = [];         // FTS-0004.001
	private $seenby = [];       // FTS-0004.001
	private $via = [];
	private $_other = [];
	private $unknown = [];

	// We auto create these values - they are used to create packets.
	private $_fqfa = NULL;      // Fully qualified fidonet source where packet originated
	private $_fqda = NULL;      // Fully qualified fidonet destination address (Netmail)

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

	// Flags for messages
	const FLAG_PRIVATE       =                0b1;
	const FLAG_CRASH         =               0b10;
	const FLAG_RECD          =              0b100;
	const FLAG_SENT          =             0b1000;
	const FLAG_FILEATTACH    =            0b10000;
	const FLAG_INTRANSIT     =           0b100000;
	const FLAG_ORPHAN        =          0b1000000;
	const FLAG_KILLSENT      =         0b10000000;
	const FLAG_LOCAL         =        0b100000000;
	const FLAG_HOLD          =       0b1000000000;
	const FLAG_UNUSED_10     =      0b10000000000;
	const FLAG_FREQ          =     0b100000000000;
	const FLAG_RETRECEIPT    =    0b1000000000000;
	const FLAG_ISRETRECEIPT  =   0b10000000000000;
	const FLAG_AUDITREQ      =  0b100000000000000;
	const FLAG_FILEUPDATEREQ = 0b1000000000000000;

	// FTS-0001.016 Message header 12 bytes
	//   node, net, flags, cost
	private $struct = [
		'onode'=>[0x00,'v',2],
		'dnode'=>[0x02,'v',2],
		'onet'=>[0x04,'v',2],
		'dnet'=>[0x06,'v',2],
		'flags'=>[0x08,'v',2],
		'cost'=>[0x0a,'v',2],
		];

	public function __construct(string $header=NULL)
	{
		// Initialise vars
		$this->kludge = collect();  // The message kludge lines
		$this->path = collect();    // The message PATH lines
		$this->seenby = collect();  // The message SEEN-BY lines
		$this->via = collect();     // The path the message has gone using Via lines
		$this->_other = collect();  // Temporarily hold attributes we dont process yet.
		$this->unknown = collect(); // Temporarily hold attributes we have no logic for.

		if ($header)
			$this->parseheader($header);
	}

	public function __get($k)
	{
		switch ($k)
		{
			case 'fz': return ftn_address_split($this->_fqfa,'z');
			case 'fn': return ftn_address_split($this->_fqfa,'n');
			case 'ff': return ftn_address_split($this->_fqfa,'f');
			case 'fp': return ftn_address_split($this->_fqfa,'p');

			case 'fqfa': return $this->_fqfa;
			case 'fqda': return $this->_fqda;

			// Echomails dont have a fully qualified from address
			case 'tz': return ftn_address_split($this->_fqda,'z');
			case 'tn': return ftn_address_split($this->_fqda,'n');
			case 'tf': return ftn_address_split($this->_fqda,'f');
			case 'tp': return ftn_address_split($this->_fqda,'p');

			case 'tearline':
				return '--- FTNHub';

			case 'type':
				if ($this->echoarea)
					return 'echomail';

				if ($this->intl)
					return 'netmail';

				return NULL;

			default:
				return isset($this->{$k}) ? $this->{$k} : NULL;
		}
	}

	public function __set($k,$v)
	{
		switch ($k)
		{
			case 'fqfa':
			case 'fqda':
				$this->{'_'.$k} = $this->get_node(ftn_address_split($v),TRUE);

				if ($this->_fqfa AND $this->_fqda)
					$this->intl = sprintf('%s %s',$this->_fqda,$this->_fqfa);

			case 'origin':
				if (! $this->_fqfa)
					throw new \Exception('Must set from address before origin');

				$this->origin = sprintf(' * Origin: %s (%s)',$v,$this->_fqfa);
				break;

			default:
				$this->{$k} = $v;
		}
	}

	/**
	 * Export an FTN message, ready for sending.
	 *
	 * @return string
	 */
	public function __toString(): string
	{
		// if (f->net == 65535) { /* Point packet - Get Net from auxNet */
		$return = '';

		$return .= pack(join('',collect($this->struct)->pluck(1)->toArray()),
			$this->ff,
			$this->tf,
			$this->fn,
			$this->tn,
			$this->flags,
			$this->cost
		);

		// @todo use pack for this.
		$return .= $this->date->format('d M y  H:i:s')."\00";
		$return .= $this->to."\00";
		$return .= $this->from."\00";
		$return .= $this->subject."\00";

		if ($this->type == 'echomail')
			$return .= "AREA:".$this->echoarea."\r";

		// Add some kludges
		$return .= "\01MSGID ".$this->_fqfa." 1"."\r";

		foreach ($this->_kludge as $k=>$v)
		{
			if ($x=$this->kludge->get($k))
				$return .= chr(1).$v.$x."\r";
		}

		$return .= $this->message."\r";
		$return .= $this->tearline."\r";
		$return .= $this->origin."\r";

		switch ($this->type)
		{
			case 'echomail':
				break;

			case 'netmail':
				foreach ($this->via as $k=>$v)
					$return .= "\01Via: ".$v."\r";

				// @todo Set product name/version as var
				$return .= sprintf('%sVia: %s @%s.UTC %s %i.%i',
					chr(1),
					'10:0/0',
					now('UTC')->format('Ymd.His'),
					'FTNHub',
					1,1)."\r";

				break;
		}

		$return .= "\00";

		return $return;
	}

	/**
	 * Return an array of flag descriptions
	 *
	 * @return array
	 *
	 * 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(int $flags): array
	{
		return [
			'private'=>$this->isFlagSet($flags,self::FLAG_PRIVATE),
			'crash'=>$this->isFlagSet($flags,self::FLAG_CRASH),
			'recd'=>$this->isFlagSet($flags,self::FLAG_RECD),
			'sent'=>$this->isFlagSet($flags,self::FLAG_SENT),
			'killsent'=>$this->isFlagSet($flags,self::FLAG_KILLSENT),
			'local'=>$this->isFlagSet($flags,self::FLAG_LOCAL),
		];
	}

	private function isFlagSet($value,$flag)
	{
		return (($value & $flag) == $flag);
	}

	/**
	 * Parse the head of an FTN message
	 *
	 * @param string $header
	 */
	public function parseheader(string $header)
	{
		$result = unpack($this->unpackheader($this->struct),$header);

		// For Echomail this is the packet src.
		$this->psn = Arr::get($result,'onet');
		$this->psf = Arr::get($result,'onode');

		$this->src = sprintf('%s/%s',
			$this->psn,
			$this->psf
		);

		// For Echomail this is the packet dst.
		$this->pdn = Arr::get($result,'dnet');
		$this->pdf = Arr::get($result,'dnode');

		$this->dst = sprintf('%s/%s',
			$this->pdn,
			$this->pdf
		);

		$this->flags = Arr::get($result,'flags');
		$this->cost = Arr::get($result,'cost');
	}

	public function parsemessage(string $message)
	{
		// Remove DOS \n\r
		$message = preg_replace("/\n\r/","\r",$message);

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

		foreach ($result as $k => $v)
		{
			// Search for \r - if that is the end of the line, then its a kludge
			$x = strpos($v,"\r");

			// If there are more characters, then put the kludge back into the result, so that we process it.
			if ($x != strlen($v)-1)
			{
				/**
				 * Anything after the origin line is also kludge data.
				 */
				if ($y = strpos($v,"\r * Origin: "))
				{
					$this->message .= substr($v,$x+1,$y-$x-1);
					$this->parseorigin(substr($v,$y));

					// If this is netmail, the FQFA will have been set by the INTL line, we can skip the rest of this
					$matches = [];

					// Capture the fully qualified 4D name from the Origin Line - it tells us the ZONE.
					preg_match('/^.*\((.*)\)$/',$this->origin,$matches);

					// Double check we have an address in the origin line
					if (! Arr::get($matches,1))
						throw new InvalidFidoPacketException(sprintf('No address in Origin?',$matches));

					// Double check, our src and origin match
					if (! preg_match('#^[0-9]+:'.$this->src.'#',$matches[1]))
						throw new InvalidFidoPacketException(sprintf('Source address mismatch? [%s,%s]',$this->_fqfa,$matches[1]));

					// If this is netmail, a double check our FQFA matches
					if ($this->type == 'netmail') {
						if ($this->_fqfa != $matches[1])
							throw new InvalidFidoPacketException(sprintf('Source address mismatch? [%s,%s]',$this->_fqfa,$matches[1]));

					// For other types, this is our only way of getting a FQFA
					} else {
						$this->_fqfa = $matches[1];

						// Our FQDA is not available, we'll assume its the same zone as our FQFA
						$this->_fqda = sprintf('%d:%s',ftn_address_split($this->_fqfa,'z'),$this->dst);
					}
				}

				$v = substr($v,0,$x+1);
			}

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

			if ($t)
				continue;

			if ($t = $this->kludge('AREA:',$v))
				$this->echoarea = $t;

		    // From point: <SOH>"FMPT <point number><CR>
			elseif ($t = $this->kludge('FMPT ',$v))
				$this->_other->push($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 ',$v))
			{
				$this->intl = $t;
				list($this->_fqda,$this->_fqfa) = explode(' ',$t);
			}

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

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

			elseif ($t = $this->kludge('REPLY: ',$v))
				$this->reply = $t;

			// To Point: <SOH>TOPT <point number><CR>
			elseif ($t = $this->kludge('TOPT ',$v))
				$this->_other->push($t);

			// Time Zone of the sender.
			elseif ($t = $this->kludge('TZUTC: ',$v))
				$this->tzutc= $t;

			// <SOH>Via <FTN Address> @YYYYMMDD.HHMMSS[.Precise][.Time Zone] <Program Name> <Version> [Serial Number]<CR>
			elseif ($t = $this->kludge('Via ',$v))
				$this->via->push($t);

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

				//dd(['v'=>$v,'t'=>$t]);
			}
		}
	}

	/**
	 * 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();

		foreach ($result as $k => $v) {
			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))
				$this->origin = $t;

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

				//dd(['v'=>$v,'t'=>$t,'message'=>$message]);
			}
		}
	}
}