<?php
namespace Engine\Mvc\Response\Components;

/**
 * The file response
 * @author Oana
 */
class File
{
	/**
	 * Internal data
	 * @var string
	 */
	protected $filePath = "";

	protected $options = [
		'deleteAfterSend' => false, 	// automatically remove the file after sending it to the browser
		'debug' => false, 				// enable debug logging
		'ignoreUserAbort' => false, 	// ensure script continues execution even if client disconnects (recommended for large files)
		'downloadMethod' => 'readfile', 	// method to use for downloading: 'readfile' or 'fread:50kbps'
	];

	/**
	 * Create the body response
	 * @param array $body
	 */
	public function __construct($filePath)
	{
		if ($filePath && !\Engine\File::check($filePath)) {
			throw new \Exception(sprintf("File %s does not exist", $filePath));
		}

		$this->filePath = ($filePath?:"");
	}

	/**
	 * Append to the internal data
	 * @param $filePath
	 */
	public function append($filePath)
	{
		throw new \Exception("You cannot append data on a file reponse");
	}

	/**
	 * Set the internal data
	 * @param string $body
	 */
	public function set($filePath)
	{
		if ($filePath && !\Engine\File::check($filePath)) {
			throw new \Exception(sprintf("File %s does not exist", $filePath));
		}

		$this->filePath = $filePath;
	}

	/**
	 * Get the internal data
	 * @return \SplFileInfo
	 */
	public function get()
	{
		return new \SplFileInfo($this->filePath);
	}

	public function setOption($key, $value)
	{
		$this->options[$key] = $value;
		return $this;
	}

	/**
	 * Check if any internal content has been set
	 * @return boolean
	 */
	public function hasContent()
	{
		return \Engine\File::check($this->filePath);
	}

	/**
	 * Return the body as a string
	 * @return string
	 */
	public function asString()
	{
		// Store the previous ignore_user_abort setting if we need to modify it
		$previousIgnoreUserAbort = null;
		
		try {
			if ($this->options['debug']) {
				$memBefore = memory_get_usage(true);
				$connStatus = $this->getConnectionStatusText(connection_status());
				\Log::getInstance()->info(sprintf("[%s] Starting file download: %s, Memory: %d, Connection: %s", __METHOD__, $this->filePath, $memBefore, $connStatus));
			}
			
			// Set ignore_user_abort to true if the option is enabled
			if ($this->options['ignoreUserAbort']) {
				$previousIgnoreUserAbort = ignore_user_abort();
				ignore_user_abort(true);
				
				if ($this->options['debug']) {
					\Log::getInstance()->info(sprintf("[%s] Enabled ignore_user_abort for file: %s", __METHOD__, $this->filePath));
				}
			}
			
			// Choose download method based on the downloadMethod option
			switch ($this->options['downloadMethod']) {
				case 'fread:50kbps':
					$fp = fopen($this->filePath, 'rb');
					if ($fp === FALSE) {
						throw new \Exception(sprintf("Cannot open file %s", $this->filePath));
					}
					
					while (!feof($fp)) {
						$chunk = fread($fp, 50 * 1024); // 50KB chunks
						echo $chunk;
						flush();
						sleep(1); // 1 second delay
					}
					
					fclose($fp);
					break;
					
				case 'readfile':
				default:
					if (readfile($this->filePath) === FALSE) {
						if ($this->options['debug']) {
							\Log::getInstance()->error(sprintf("[%s] Failed to read file: %s", __METHOD__, $this->filePath));
						}
						throw new \Exception(sprintf("Cannot read data from file %s", $this->filePath));
					}
					break;
			}
			
			if ($this->options['debug']) {
				$memAfter = memory_get_usage(true);
				$connStatus = $this->getConnectionStatusText(connection_status());
				\Log::getInstance()->info(sprintf("[%s] File successfully sent: %s, Memory: %d (Diff: %d), Connection: %s", 
					__METHOD__, $this->filePath, $memAfter, ($memAfter - $memBefore), $connStatus));
			}

			if ($this->options['deleteAfterSend']) {
				if ($this->options['debug']) {
					\Log::getInstance()->info(sprintf("[%s] Attempting to delete file: %s", __METHOD__, $this->filePath));
				}
				\Engine\File::delete($this->filePath);
				if ($this->options['debug']) {
					\Log::getInstance()->info(sprintf("[%s] File deleted: %s", __METHOD__, $this->filePath));
				}
			}

			return '';
		} catch (\Exception $e) {
			if ($this->options['debug']) {
				\Log::getInstance()->error(sprintf("[%s] Exception in asString: %s", __METHOD__, $e->getMessage()));
			}
			throw $e;
		} finally {
			// Restore the previous ignore_user_abort setting if we modified it
			if ($this->options['ignoreUserAbort'] && $previousIgnoreUserAbort !== null) {
				ignore_user_abort($previousIgnoreUserAbort);
				
				if ($this->options['debug']) {
					\Log::getInstance()->info(sprintf("[%s] Restored ignore_user_abort to previous value for file: %s", __METHOD__, $this->filePath));
				}
			}
		}
	}

	/**
	 * Delete the file if deleteAfterSend is set. 
	 * This is used as a fallback mechanism in case the file is not deleted in asString()
	 * @return void 
	 */
	public function __destruct()
	{
		if ($this->options['deleteAfterSend'] && \Engine\File::check($this->filePath)) {
			if ($this->options['debug']) {
				$memUsage = memory_get_usage(true);
				$connStatus = $this->getConnectionStatusText(connection_status());
				\Log::getInstance()->info(sprintf("[%s] Destructor: Deleting file: %s, Memory: %d, Connection: %s", 
					__METHOD__, $this->filePath, $memUsage, $connStatus));
			}
			\Engine\File::delete($this->filePath);
		}
	}
	


	/**
	 * Get a text representation of the connection status bitfield
	 * @param int $status Connection status bitfield
	 * @return string Text representation of the connection status
	 */
	protected function getConnectionStatusText($status)
	{
		switch ($status) {
			case CONNECTION_NORMAL:
				return 'normal';
			case CONNECTION_ABORTED:
				return 'aborted';
			case CONNECTION_TIMEOUT:
				return 'timeout';
			case CONNECTION_ABORTED | CONNECTION_TIMEOUT:
				return 'aborted, timeout';
			default:
				return 'unknown (' . $status . ')';
		}
	}

}
