<?php

namespace App\Classes\File;

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

use App\Models\Address;

/**
 * Object representing the files we are sending
 *
 * @property-read resource fd
 * @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 string $comp_data;

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

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

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

			case 'files_size':
				return $this->list
					->filter(function($item) { return $item->isType(self::IS_FILE|self::IS_TIC); })
					->sum(function($item) { return $item->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->size; })
					+ $this->packets->sum(function($item) { return $item->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->size; })
					+ $this->packets
						->filter(function($item) { return ($item->action & self::I_SEND) && $item->sent === TRUE; })
						->sum(function($item) { return $item->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->size; })
					+ $this->packets->sum(function($item) { return $item->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());
		}
	}

	/*
	private function compress(string $comp_mode): void
	{
		switch ($comp_mode) {
			case 'BZ2':
				$this->comp_data = bzcompress($buf);
				break;

			case 'GZ':
				$this->comp_data = gzcompress($buf);
				break;
		}
	}
	*/

	/**
	 * 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->name,$end));
		}

		// @todo This should be done better isType == file?
		if ((! $this->sending instanceof Mail) && (! $this->sending->isType(self::IS_TIC)))
			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->sending->isType(self::IS_TIC)))
			? ($this->file_pos === $this->size)
			: feof($this->f);
	}

	/**
	 * Add our mail to the send queue
	 *
	 * @param Address $ao
	 * @return bool
	 * @throws Exception
	 * @todo We need to make this into a transaction, incase the transfer fails.
	 */
	public function files(Address $ao): bool
	{
		$file = FALSE;

		// If the node is marked as hold - dont send any files.
		if ($ao->system->hold) {
			Log::info(sprintf('%s: - System [%d] is marked as hold - not checking for files.',self::LOGKEY,$ao->system_id));

			return FALSE;
		}

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

			// Add Files
			foreach ($x as $fo) {
				$this->list->push(new Item($fo,self::I_SEND));
				$this->list->push(new Tic($ao,$fo,self::I_SEND));
			}

			$file = TRUE;
		}

		return $file;
	}

	/**
	 * Open a file for sending
	 *
	 * @param string $compress
	 * @return bool
	 * @throws Exception
	 */
	public function open(string $compress=''): 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;

			/*
			if ($compress)
				$this->comp_data = $this->compdata($compress);
			*/

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

		// If sending->file is a string, then we dont need to actually open anything
		if ($this->sending->isType(self::IS_TIC)) {
			$this->f = TRUE;
			return TRUE;
		}

		// If sending file is a File::class, then our file is s3
		if (! $this->sending->name && $this->sending->filemodel) {
			$this->f = Storage::readStream($this->sending->filemodel->full_storage_path);
			return TRUE;

		} else {
			$this->f = fopen($this->sending->name,'rb');

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

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

	/**
	 * Add our mail to the send queue
	 *
	 * @param Address $ao
	 * @return bool
	 * @throws Exception
	 * @todo We need to make this into a transaction, incase the transfer fails.
	 */
	public function mail(Address $ao): bool
	{
		$mail = FALSE;

		// If the node is marked as hold - dont send any mail.
		if ($ao->system->hold) {
			Log::info(sprintf('%s: - System [%d] is marked as hold - not checking for mail.',self::LOGKEY,$ao->system_id));

			return FALSE;
		}

		// 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));
			$mail = TRUE;
		}

		// 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));
			$mail = TRUE;
		}

		return $mail;
	}

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

		// We are sending a tic file
		} else if ($this->sending->isType(self::IS_TIC)) {
			$data = $this->sending->read($this->file_pos,$length);

		} else {
			$data = fread($this->f,$length);
		}

		$this->file_pos += strlen($data);

		if ($this->DEBUG)
			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->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) || $this->sending->isType(self::IS_TIC)) {
			$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;
	}
}