<?php

namespace App\Classes\File;

use Exception;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use League\Flysystem\UnreadableFileEncountered;

use App\Models\Address;

/**
 * Object representing the files we are sending
 *
 * @property-read resource fd
 * @property-read int file_mtime
 * @property-read int file_size
 * @property-read string file_name
 * @property-read int mail_size
 * @property-read int total_count
 * @property-read int total_sent
 * @property-read int total_sent_bytes
 */
final class Send extends Item
{
	private const LOGKEY = 'IS-';

	private Collection $list;
	private ?Item $sending;
	private Collection $packets;

	private mixed $f;				// File descriptor
	private int $start;				// Time we started sending
	private int $file_pos;			// Current read pointer

	public function __construct()
	{
		// Initialise our variables
		$this->list = collect();
		$this->packets = collect();
		$this->sending = NULL;
		$this->file_pos = 0;
		$this->f = NULL;
	}

	public function __get($key)
	{
		switch ($key) {
			case 'fd':
				return is_resource($this->f) ?: $this->f;

			case 'file_count':
				return $this->list
					->filter(function($item) { return $item->isType(self::IS_FILE); })
					->count();

			case 'file_size':
				return $this->list
					->filter(function($item) { return $item->isType(self::IS_FILE); })
					->sum(function($item) { return $item->file_size; });

			case 'filepos':
				return $this->file_pos;

			case 'mail_count':
				return $this->list
					->filter(function($item) { return $item->isType(self::IS_ARC|self::IS_PKT); })
					->count()
					+ $this->packets->count();

			case 'mail_size':
				return $this->list
					->filter(function($item) { return $item->isType(self::IS_ARC|self::IS_PKT); })
					->sum(function($item) { return $item->file_size; })
					+ $this->packets->sum(function($item) { return $item->file_size; });

			case 'sendas':
				return $this->sending ? $this->sending->{$key} : NULL;

			case 'name':
			case 'mtime':
			case 'size':
				return $this->sending ? $this->sending->{'file_'.$key} : NULL;

			case 'total_sent':
				return $this->list
					->filter(function($item) { return ($item->action & self::I_SEND) && $item->sent === TRUE; })
					->count()
					+ $this->packets
						->filter(function($item) { return ($item->action & self::I_SEND) && $item->sent === TRUE; })
						->count();

			case 'total_sent_bytes':
				return $this->list
					->filter(function($item) { return ($item->action & self::I_SEND) && $item->sent === TRUE; })
					->sum(function($item) { return $item->file_size; })
					+ $this->packets
						->filter(function($item) { return ($item->action & self::I_SEND) && $item->sent === TRUE; })
						->sum(function($item) { return $item->file_size; });

			case 'total_count':
				return $this->list
					->filter(function($item) { return ($item->action & self::I_SEND) && $item->sent === FALSE; })
					->count()
					+ $this->packets
						->filter(function($item) { return ($item->action & self::I_SEND) && $item->sent === FALSE; })
						->count();

			case 'total_size':
				return $this->list
					->sum(function($item) { return $item->file_size; })
					+ $this->packets->sum(function($item) { return $item->file_size; });

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

	/**
	 * Add a file to the list of files to send
	 *
	 * @param string $file
	 * @throws Exception
	 * @todo Catch if we add the same file twice
	 */
	public function add(string $file): void
	{
		Log::debug(sprintf('%s:+ add [%s]',self::LOGKEY,$file));

		try {
			$this->list->push(new Item($file,self::I_SEND));

		} catch (FileNotFoundException) {
			Log::error(sprintf('%s:! Item [%s] doesnt exist',self::LOGKEY,$file));
			return;

		} catch (UnreadableFileEncountered) {
			Log::error(sprintf('%s:! Item [%s] cannot be read',self::LOGKEY,$file));
			return;

		// Uncaught, rethrow the error
		} catch (Exception $e) {
			throw new Exception($e->getMessage());
		}
	}

	/**
	 * Close the file descriptor of the file we are sending
	 *
	 * @param bool $successful
	 * @throws Exception
	 */
	public function close(bool $successful): void
	{
		if (! $this->f)
			throw new Exception('No file to close');

		if ($successful) {
			$this->sending->sent = TRUE;
			$end = time()-$this->start;
			Log::debug(sprintf('%s: - Closing [%s], sent in [%d]',self::LOGKEY,$this->sending->file_name,$end));
		}

		if (! $this->sending instanceof Mail)
			fclose($this->f);

		$this->sending = NULL;
		$this->file_pos = 0;
		$this->f = NULL;
	}

	/**
	 * Check if we are at the end of the file
	 *
	 * @return bool
	 */
	public function feof(): bool
	{
		return ($this->sending instanceof Mail) ? ($this->file_pos == $this->size) : feof($this->f);
	}

	/**
	 * Open a file for sending
	 *
	 * @return bool
	 * @throws Exception
	 */
	public function open(): bool
	{
		Log::debug(sprintf('%s:+ open',self::LOGKEY));

		// If we have mail, we'll send that first
		if ($this->sending = $this->packets
			->filter(function($item) { return ($item->action & self::I_SEND) && $item->sent === FALSE; })
			->first())
		{
			$this->file_pos = 0;
			$this->start = time();
			$this->f = TRUE;

			return TRUE;
		}

		$this->sending = $this->list
			->filter(function($item) { return ($item->action & self::I_SEND) && $item->sent === FALSE; })
			->first();

		if (! $this->sending)
			throw new Exception('No files to open');

		$this->file_pos = 0;
		$this->start = time();

		$this->f = fopen($this->sending->file_name,'rb');
		if (! $this->f) {
			Log::error(sprintf('%s:! Unable to open file [%s] for reading',self::LOGKEY,$this->sending->file_name));
			return FALSE;
		}

		Log::info(sprintf('%s:= open - File [%s] opened with size [%d]',self::LOGKEY,$this->sending->file_name,$this->sending->file_size));
		return TRUE;
	}

	/**
	 * Add our mail to the send queue
	 *
	 * @param Address $ao
	 * @throws Exception
	 * @todo We need to make this into a transaction, incase the transfer fails.
	 */
	public function mail(Address $ao): void
	{
		// If the node is marked as hold - dont send any mail.
		if ($ao->system->hold) {
			Log::info(sprintf('%s: - System [%d] mail is marked as hold - not checking for mail.',self::LOGKEY,$ao->system_id));
			return;
		}

		// Netmail
		if ($x=$ao->getNetmail()) {
			Log::debug(sprintf('%s: - Netmail(s) added for sending to [%s]',self::LOGKEY,$ao->ftn));

			$this->packets->push(new Mail($x,self::I_SEND));
		}

		// Echomail
		if ($x=$ao->getEchomail()) {
			Log::debug(sprintf('%s: - Echomail(s) added for sending to [%s]',self::LOGKEY,$ao->ftn));

			$this->packets->push(new Mail($x,self::I_SEND));
		}
	}

	/**
	 * Read bytes of the sending file
	 *
	 * @param int $length
	 * @return string|null
	 * @throws UnreadableFileEncountered
	 * @throws Exception
	 */
	public function read(int $length): ?string
	{
		if (! $this->f)
			throw new Exception('No file open for read');

		// We are sending mail
		if ($this->sending instanceof Mail) {
			$data = $this->sending->read($this->file_pos,$length);
		} else {
			$data = fread($this->f,$length);
		}

		$this->file_pos += strlen($data);
		Log::debug(sprintf('%s: - Read [%d] bytes, file pos now [%d]',self::LOGKEY,strlen($data),$this->file_pos));

		if ($data === FALSE)
			throw new UnreadableFileEncountered('Error reading file: '.$this->sending->file_name);

		return $data;
	}

	/**
	 * Seek to a specific position of our file
	 *
	 * @param int $pos
	 * @return bool
	 * @throws Exception
	 */
	public function seek(int $pos): bool
	{
		if (! $this->f)
			throw new Exception('No file open for seek');

		if ($this->sending instanceof Mail) {
			$pos = ($pos < $this->size) ? $pos : $this->size;
			$rc = TRUE;

		} else {
			$rc = (fseek($this->f,$pos,SEEK_SET) === 0);
		}

		if ($rc)
			$this->file_pos = $pos;

		Log::debug(sprintf('%s:= Seeked to [%d]',self::LOGKEY,$this->file_pos));

		return $rc;
	}
}