<?php

namespace App\Classes\FTN;

use Carbon\Carbon;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\File\File;

use App\Classes\FTN as FTNBase;
use App\Models\Software;
use App\Traits\GetNode;

class Packet extends FTNBase
{
	//use GetNode;
	private const LOGKEY = 'PKT';

	private const HEADER_LEN		= 0x3a;
	private const VERSION_OFFSET	= 0x12;
	private const BLOCKSIZE			= 1024;
	private const PACKED_MSG_HEADER_LEN = 0x22;

	public File $file;					// Packet filename
	public Collection $messages;		// Messages in the Packet
	private array $header;				// Packet Header

	// V2 Packet Header (2/2e/2+)
	private const v2header = [
		'onode'			=> [0x00,'v',2],	// Originating Node
		'dnode'			=> [0x02,'v',2],	// Destination Node
		'y'				=> [0x04,'v',2],	// Year
		'm'				=> [0x06,'v',2],	// Month
		'd'				=> [0x08,'v',2],	// Day
		'H'				=> [0x0a,'v',2],	// Hour
		'M'				=> [0x0c,'v',2],	// Minute
		'S'				=> [0x0e,'v',2],	// Second
		'baud'			=> [0x10,'v',2],	// Baud
		'pktver'		=> [0x12,'v',2],	// Packet Version
		'onet'			=> [0x14,'v',2],	// Originating Net	(0xffff when origPoint !=0 2+)
		'dnet'			=> [0x16,'v',2],	// Destination Net
		'prodcode-lo'	=> [0x18,'C',1],
		'prodrev-maj'	=> [0x19,'C',1],	// Product Version Major (serialNum 2)
		'password'		=> [0x1a,'Z8',8],	// Packet Password
		'qozone'		=> [0x22,'v',2],
		'qdzone'		=> [0x24,'v',2],
		'filler'		=> [0x26,'v',2],	// Reserved	(auxnet 2+ - contains Orignet if Origin is a point) fsc-0048.001
		'capvalid'		=> [0x28,'n',2],	// fsc-0039.004 (Not used 2) (copy of 0x2c)
		'prodcode-hi'	=> [0x2a,'C',1],	// (Not used 2)
		'prodrev-min'	=> [0x2b,'C',1],	// (Not used 2)
		'capword'		=> [0x2c,'v',2],	// fsc-0039.001 (Not used 2)
		'ozone'			=> [0x2e,'v',2],	// Originating Zone (Not used 2)
		'dzone'			=> [0x30,'v',2],	// Destination Zone (Not used 2)
		'opoint'		=> [0x32,'v',2],	// Originating Point (Not used 2)
		'dpoint'		=> [0x34,'v',2],	// Destination Point (Not used 2)
		'proddata'		=> [0x36,'A4',4],	// ProdData (Not used 2)		// FSC-39/FSC-48
	];

	public function __construct(File $file)
	{
		$this->messages = collect();

		if ($file) {
			$this->file = $file;
			$this->open($file);
		}
	}

	/**
	 * @throws \Exception
	 */
	public function __get($key)
	{
		switch ($key) {
			// From Addresses
			case 'fz': return Arr::get($this->header,'ozone');
			case 'fn': return Arr::get($this->header,'onet');
			case 'ff': return Arr::get($this->header,'onode');
			case 'fp': return Arr::get($this->header,'opoint');
			case 'fd': return Arr::get($this->header,'odomain');

			// To Addresses
			case 'tz': return Arr::get($this->header,'dzone');
			case 'tn': return Arr::get($this->header,'dnet');
			case 'tf': return Arr::get($this->header,'dnode');
			case 'tp': return Arr::get($this->header,'dpoint');
			case 'td': return Arr::get($this->header,'ddomain');

			case 'date':
				return Carbon::create(
					Arr::get($this->header,'y'),
					Arr::get($this->header,'m')+1,
					Arr::get($this->header,'d'),
					Arr::get($this->header,'H'),
					Arr::get($this->header,'M'),
					Arr::get($this->header,'S')
				);

			case 'capability':
				return Arr::get($this->header,'capword') == Arr::get($this->header,'capvalid') ? sprintf('%016b',Arr::get($this->header,'capword')) : 'FTS-1';

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

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

			case 'software':
				$code = Arr::get($this->header,'prodcode-hi')<<8|Arr::get($this->header,'prodcode-lo');
				Software::unguard();
				$o = Software::singleOrNew(['code'=>$code,'type'=>Software::SOFTWARE_TOSSER]);
				Software::reguard();

				return $o;

			case 'software_ver':
				return sprintf('%d.%d',Arr::get($this->header,'prodrev-maj'),Arr::get($this->header,'prodrev-min'));

			// Packet Type
			case 'type':
				if ((Arr::get($this->header,'onet') == 0xffff) && (Arr::get($this->header,'opoint') != 0) && Arr::get($this->header,'filler'))
					return '2+';
				elseif (Arr::get($this->header,'prodrev-maj') && ! Arr::get($this->header,'capword'))
					return '2';
				else
					return '2e';

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

	// @note - messages in this object have the same next destination
	// @todo To rework
	/*
	public function __toString(): string
	{
		// @todo - is this appropriate to set here
		$this->date = now();
		$this->pktsrc = (string)$this->get_node(ftn_address_split('10:1/5.0'),TRUE);

		// @todo
		if ($this->messages->first()->type == 'echomail')
			$this->pktdst = (string)$this->messages->first()->fqfa->uplink;
		else
			$this->pktdst = (string)$this->messages->first()->fqda->uplink;

		$this->software['prodcode-lo'] = 0x00;
		$this->software['prodcode-hi'] = 0xde;
		$this->software['rev-maj'] = 0x00;
		$this->software['rev-min'] = 0x01;

		// Type 2+ Packet
		$this->cap['valid'] = 0x0100;
		$this->cap['word'] = 0x0001;
		$this->pktver = 0x0002;

		$return = $this->createHeader();

		foreach ($this->messages as $o)
			$return .= "\02\00".(string)$o;

		$return .= "\00\00";

		return $return;
	}
	*/

	/**
	 * Create our message packet header
	 * @todo To rework
	 */
	/*
	private function createHeader(): string
	{
		try {
			$a = pack(join('',collect($this->pack1)->pluck(1)->toArray()),
				$this->ff,
				$this->tf,
				$this->date->year,
				$this->date->month,
				$this->date->day,
				$this->date->hour,
				$this->date->minute,
				$this->date->second,
				$this->baud,
				$this->pktver,
				$this->fn,                       // @todo if point, this needs to be 0xff
				$this->tn,
				$this->software['prodcode-lo'],  // @todo change to this software
				$this->software['rev-maj']       // @todo change to this software
				);

			$b = pack(join('',collect($this->pack2)->pluck(1)->toArray()),
				0x0000,                          // @note: Type 2 packet this is $this->sz,
				0x0000,                          // @note: Type 2 packet this is $this->dz,
				0x0000,                          // Filler $this->>sn if message to point.
				$this->cap['valid'],             // @todo to check
				$this->software['prodcode-hi'],  // @todo change to this software
				$this->software['rev-min'],      // @todo change to this software
				$this->cap['word'],              // @todo to check
				$this->fz,
				$this->tz,
				$this->fp,                       // @note: point address, type 2+ packets
				$this->tp                        // @note: point address, type 2+ packets
			);

			return $a.pack('a8',strtoupper($this->password)).$b."mbse"; // @todo change to this software

		} catch (\Exception $e) {
			return $e->getMessage();
		}
	}

	public function addMessage(FTNMessage $o)
	{
		// @todo Check that this message is for the same uplink.
		$this->messages->push($o);
	}
	*/

	/**
	 * Open a packet file
	 *
	 * @param string $file
	 * @throws InvalidPacketException
	 */
	private function open(string $file)
	{
		Log::debug(sprintf('%s:Opening Packet [%s]',self::LOGKEY,$file));

		$f = fopen($file,'r');
		$fstat = fstat($f);

		// PKT Header
		$header = fread($f,self::HEADER_LEN);
		Log::debug(sprintf("%s:\n%s",self::LOGKEY,hex_dump($header)));

		// Could not read header
		if (strlen($header) != self::HEADER_LEN)
			throw new InvalidPacketException(sprintf('Length of header [%d] too short'.strlen($header)));

		// Not a type 2 packet
		$version = Arr::get(unpack('vv',substr($header,self::VERSION_OFFSET)),'v');
		if ($version != 2)
			throw new InvalidPacketException('Not a type 2 packet: '.$version);

		$this->header = unpack($this->unpackheader(self::v2header),$header);

		$x = fread($f,2);

		// End of Packet?
		if (strlen($x) == 2 and $x == "\00\00")
			return;

		// Messages start with 02H 00H
		if (strlen($x) == 2 AND $x != "\02\00")
			throw new InvalidPacketException('Not a valid packet: '.bin2hex($x));

		// No message attached
		else if (! strlen($x))
			throw new InvalidPacketException('No message in packet: '.bin2hex($x));

		$buf_ptr = 0;
		$message = '';
		$readbuf = '';

		while ($buf_ptr || (! feof($f) && ($readbuf=fread($f,self::BLOCKSIZE)))) {
			// A message header is atleast 0x22 chars long
			if (strlen($readbuf) < self::PACKED_MSG_HEADER_LEN) {
				$message .= $readbuf;
				$buf_ptr = 0;

				continue;

			} elseif (strlen($message) < self::PACKED_MSG_HEADER_LEN) {
				$addchars = self::PACKED_MSG_HEADER_LEN-strlen($message);
				$message .= substr($readbuf,$buf_ptr,$addchars);
				$buf_ptr += $addchars;
			}

			// If we didnt find a packet end, perhaps there are no more
			if (($end=strpos($readbuf,"\x00\x02\x00",$buf_ptr)) === FALSE)
				$end = strpos($readbuf,"\x00\x00\x00",$buf_ptr);

			// See if we have found the end of the packet, if not read more.
			if ($end === FALSE && (ftell($f) < $fstat['size'])) {
				$message .= substr($readbuf,$buf_ptr);
				$buf_ptr = 0;

				continue;

			} else {
				$message .= substr($readbuf,$buf_ptr,$end-$buf_ptr);
				$buf_ptr += $end-$buf_ptr+3;

				if ($buf_ptr >= strlen($readbuf))
					$buf_ptr = 0;
			}

			// Look for the next message
			$this->messages->push(new Message($message));
			$message = '';
		}
	}
}