<?php

namespace JamesHeinrich\GetID3\Write;

use JamesHeinrich\GetID3\GetID3;
use JamesHeinrich\GetID3\Utils;

/////////////////////////////////////////////////////////////////
/// getID3() by James Heinrich <info@getid3.org>               //
//  available at https://github.com/JamesHeinrich/getID3       //
//            or https://www.getid3.org                        //
//            or http://getid3.sourceforge.net                 //
//  see readme.txt for more details                            //
/////////////////////////////////////////////////////////////////
//                                                             //
// write.real.php                                              //
// module for writing RealAudio/RealVideo tags                 //
//                                                            ///
/////////////////////////////////////////////////////////////////

class Real
{
	/**
	 * @var string
	 */
	public $filename;

	/**
	 * @var array
	 */
	public $tag_data          = array();

	/**
	 * Read buffer size in bytes.
	 *
	 * @var int
	 */
	public $fread_buffer_size = 32768;

	/**
	 * Any non-critical errors will be stored here.
	 *
	 * @var array
	 */
	public $warnings          = array();

	/**
	 * Any critical errors will be stored here.
	 *
	 * @var array
	 */
	public $errors            = array();

	/**
	 * Minimum length of CONT tag in bytes.
	 *
	 * @var int
	 */
	public $paddedlength      = 512;

	/**
	 * @return bool
	 */
	public function WriteReal() {
		// File MUST be writeable - CHMOD(646) at least
		if (Utils::isWritable($this->filename) && is_file($this->filename) && ($fp_source = fopen($this->filename, 'r+b'))) {

			// Initialize getID3 engine
			$getID3 = new GetID3;
			$OldThisFileInfo = $getID3->analyze($this->filename);
			if (empty($OldThisFileInfo['real']['chunks']) && !empty($OldThisFileInfo['real']['old_ra_header'])) {
				$this->errors[] = 'Cannot write Real tags on old-style file format';
				fclose($fp_source);
				return false;
			}

			if (empty($OldThisFileInfo['real']['chunks'])) {
				$this->errors[] = 'Cannot write Real tags because cannot find DATA chunk in file';
				fclose($fp_source);
				return false;
			}
			$oldChunkInfo = array();
			foreach ($OldThisFileInfo['real']['chunks'] as $chunknumber => $chunkarray) {
				$oldChunkInfo[$chunkarray['name']] = $chunkarray;
			}
			if (!empty($oldChunkInfo['CONT']['length'])) {
				$this->paddedlength = max($oldChunkInfo['CONT']['length'], $this->paddedlength);
			}

			$new_CONT_tag_data = $this->GenerateCONTchunk();
			$new_PROP_tag_data = $this->GeneratePROPchunk($OldThisFileInfo['real']['chunks'], $new_CONT_tag_data);
			$new__RMF_tag_data = $this->GenerateRMFchunk($OldThisFileInfo['real']['chunks']);

			if (isset($oldChunkInfo['.RMF']['length']) && ($oldChunkInfo['.RMF']['length'] == strlen($new__RMF_tag_data))) {
				fseek($fp_source, $oldChunkInfo['.RMF']['offset']);
				fwrite($fp_source, $new__RMF_tag_data);
			} else {
				$this->errors[] = 'new .RMF tag ('.strlen($new__RMF_tag_data).' bytes) different length than old .RMF tag ('.$oldChunkInfo['.RMF']['length'].' bytes)';
				fclose($fp_source);
				return false;
			}

			if (isset($oldChunkInfo['PROP']['length']) && ($oldChunkInfo['PROP']['length'] == strlen($new_PROP_tag_data))) {
				fseek($fp_source, $oldChunkInfo['PROP']['offset']);
				fwrite($fp_source, $new_PROP_tag_data);
			} else {
				$this->errors[] = 'new PROP tag ('.strlen($new_PROP_tag_data).' bytes) different length than old PROP tag ('.$oldChunkInfo['PROP']['length'].' bytes)';
				fclose($fp_source);
				return false;
			}

			if (isset($oldChunkInfo['CONT']['length']) && ($oldChunkInfo['CONT']['length'] == strlen($new_CONT_tag_data))) {

				// new data length is same as old data length - just overwrite
				fseek($fp_source, $oldChunkInfo['CONT']['offset']);
				fwrite($fp_source, $new_CONT_tag_data);
				fclose($fp_source);
				return true;

			} else {

				if (empty($oldChunkInfo['CONT'])) {
					// no existing CONT chunk
					$BeforeOffset = $oldChunkInfo['DATA']['offset'];
					$AfterOffset  = $oldChunkInfo['DATA']['offset'];
				} else {
					// new data is longer than old data
					$BeforeOffset = $oldChunkInfo['CONT']['offset'];
					$AfterOffset  = $oldChunkInfo['CONT']['offset'] + $oldChunkInfo['CONT']['length'];
				}
				if ($tempfilename = tempnam(Utils::getTempDirectory(), 'getID3')) {
					if (Utils::isWritable($tempfilename) && is_file($tempfilename) && ($fp_temp = fopen($tempfilename, 'wb'))) {

						rewind($fp_source);
						fwrite($fp_temp, fread($fp_source, $BeforeOffset));
						fwrite($fp_temp, $new_CONT_tag_data);
						fseek($fp_source, $AfterOffset);
						while ($buffer = fread($fp_source, $this->fread_buffer_size)) {
							fwrite($fp_temp, $buffer, strlen($buffer));
						}
						fclose($fp_temp);

						if (copy($tempfilename, $this->filename)) {
							unlink($tempfilename);
							fclose($fp_source);
							return true;
						}
						unlink($tempfilename);
						$this->errors[] = 'FAILED: copy('.$tempfilename.', '.$this->filename.')';

					} else {
						$this->errors[] = 'Could not fopen("'.$tempfilename.'", "wb")';
					}
				}
				fclose($fp_source);
				return false;

			}

		}
		$this->errors[] = 'Could not fopen("'.$this->filename.'", "r+b")';
		return false;
	}

	/**
	 * @param array $chunks
	 *
	 * @return string
	 */
	public function GenerateRMFchunk(&$chunks) {
		$oldCONTexists = false;
		$chunkNameKeys = array();
		foreach ($chunks as $key => $chunk) {
			$chunkNameKeys[$chunk['name']] = $key;
			if ($chunk['name'] == 'CONT') {
				$oldCONTexists = true;
			}
		}
		$newHeadersCount = $chunks[$chunkNameKeys['.RMF']]['headers_count'] + ($oldCONTexists ? 0 : 1);

		$RMFchunk  = "\x00\x00"; // object version
		$RMFchunk .= Utils::BigEndian2String($chunks[$chunkNameKeys['.RMF']]['file_version'], 4);
		$RMFchunk .= Utils::BigEndian2String($newHeadersCount,                                4);

		$RMFchunk  = '.RMF'.Utils::BigEndian2String(strlen($RMFchunk) + 8, 4).$RMFchunk; // .RMF chunk identifier + chunk length
		return $RMFchunk;
	}

	/**
	 * @param array  $chunks
	 * @param string $new_CONT_tag_data
	 *
	 * @return string
	 */
	public function GeneratePROPchunk(&$chunks, &$new_CONT_tag_data) {
		$old_CONT_length = 0;
		$old_DATA_offset = 0;
		$old_INDX_offset = 0;
		$chunkNameKeys = array();
		foreach ($chunks as $key => $chunk) {
			$chunkNameKeys[$chunk['name']] = $key;
			if ($chunk['name'] == 'CONT') {
				$old_CONT_length = $chunk['length'];
			} elseif ($chunk['name'] == 'DATA') {
				if (!$old_DATA_offset) {
					$old_DATA_offset = $chunk['offset'];
				}
			} elseif ($chunk['name'] == 'INDX') {
				if (!$old_INDX_offset) {
					$old_INDX_offset = $chunk['offset'];
				}
			}
		}
		$CONTdelta = strlen($new_CONT_tag_data) - $old_CONT_length;

		$PROPchunk  = "\x00\x00"; // object version
		$PROPchunk .= Utils::BigEndian2String($chunks[$chunkNameKeys['PROP']]['max_bit_rate'],    4);
		$PROPchunk .= Utils::BigEndian2String($chunks[$chunkNameKeys['PROP']]['avg_bit_rate'],    4);
		$PROPchunk .= Utils::BigEndian2String($chunks[$chunkNameKeys['PROP']]['max_packet_size'], 4);
		$PROPchunk .= Utils::BigEndian2String($chunks[$chunkNameKeys['PROP']]['avg_packet_size'], 4);
		$PROPchunk .= Utils::BigEndian2String($chunks[$chunkNameKeys['PROP']]['num_packets'],     4);
		$PROPchunk .= Utils::BigEndian2String($chunks[$chunkNameKeys['PROP']]['duration'],        4);
		$PROPchunk .= Utils::BigEndian2String($chunks[$chunkNameKeys['PROP']]['preroll'],         4);
		$PROPchunk .= Utils::BigEndian2String(max(0, $old_INDX_offset + $CONTdelta),              4);
		$PROPchunk .= Utils::BigEndian2String(max(0, $old_DATA_offset + $CONTdelta),              4);
		$PROPchunk .= Utils::BigEndian2String($chunks[$chunkNameKeys['PROP']]['num_streams'],     2);
		$PROPchunk .= Utils::BigEndian2String($chunks[$chunkNameKeys['PROP']]['flags_raw'],       2);

		$PROPchunk  = 'PROP'.Utils::BigEndian2String(strlen($PROPchunk) + 8, 4).$PROPchunk; // PROP chunk identifier + chunk length
		return $PROPchunk;
	}

	/**
	 * @return string
	 */
	public function GenerateCONTchunk() {
		foreach ($this->tag_data as $key => $value) {
			// limit each value to 0xFFFF bytes
			$this->tag_data[$key] = substr($value, 0, 65535);
		}

		$CONTchunk  = "\x00\x00"; // object version

		$CONTchunk .= Utils::BigEndian2String((!empty($this->tag_data['title'])     ? strlen($this->tag_data['title'])     : 0), 2);
		$CONTchunk .= (!empty($this->tag_data['title'])     ? strlen($this->tag_data['title'])     : '');

		$CONTchunk .= Utils::BigEndian2String((!empty($this->tag_data['artist'])    ? strlen($this->tag_data['artist'])    : 0), 2);
		$CONTchunk .= (!empty($this->tag_data['artist'])    ? strlen($this->tag_data['artist'])    : '');

		$CONTchunk .= Utils::BigEndian2String((!empty($this->tag_data['copyright']) ? strlen($this->tag_data['copyright']) : 0), 2);
		$CONTchunk .= (!empty($this->tag_data['copyright']) ? strlen($this->tag_data['copyright']) : '');

		$CONTchunk .= Utils::BigEndian2String((!empty($this->tag_data['comment'])   ? strlen($this->tag_data['comment'])   : 0), 2);
		$CONTchunk .= (!empty($this->tag_data['comment'])   ? strlen($this->tag_data['comment'])   : '');

		if ($this->paddedlength > (strlen($CONTchunk) + 8)) {
			$CONTchunk .= str_repeat("\x00", $this->paddedlength - strlen($CONTchunk) - 8);
		}

		$CONTchunk  = 'CONT'.Utils::BigEndian2String(strlen($CONTchunk) + 8, 4).$CONTchunk; // CONT chunk identifier + chunk length

		return $CONTchunk;
	}

	/**
	 * @return bool
	 */
	public function RemoveReal() {
		// File MUST be writeable - CHMOD(646) at least
		if (Utils::isWritable($this->filename) && is_file($this->filename) && ($fp_source = fopen($this->filename, 'r+b'))) {

			// Initialize getID3 engine
			$getID3 = new GetID3;
			$OldThisFileInfo = $getID3->analyze($this->filename);
			if (empty($OldThisFileInfo['real']['chunks']) && !empty($OldThisFileInfo['real']['old_ra_header'])) {
				$this->errors[] = 'Cannot remove Real tags from old-style file format';
				fclose($fp_source);
				return false;
			}

			if (empty($OldThisFileInfo['real']['chunks'])) {
				$this->errors[] = 'Cannot remove Real tags because cannot find DATA chunk in file';
				fclose($fp_source);
				return false;
			}
			$oldChunkInfo = array();
			foreach ($OldThisFileInfo['real']['chunks'] as $chunknumber => $chunkarray) {
				$oldChunkInfo[$chunkarray['name']] = $chunkarray;
			}

			if (empty($oldChunkInfo['CONT'])) {
				// no existing CONT chunk
				fclose($fp_source);
				return true;
			}

			$BeforeOffset = $oldChunkInfo['CONT']['offset'];
			$AfterOffset  = $oldChunkInfo['CONT']['offset'] + $oldChunkInfo['CONT']['length'];
			if ($tempfilename = tempnam(Utils::getTempDirectory(), 'getID3')) {
				if (Utils::isWritable($tempfilename) && is_file($tempfilename) && ($fp_temp = fopen($tempfilename, 'wb'))) {

					rewind($fp_source);
					fwrite($fp_temp, fread($fp_source, $BeforeOffset));
					fseek($fp_source, $AfterOffset);
					while ($buffer = fread($fp_source, $this->fread_buffer_size)) {
						fwrite($fp_temp, $buffer, strlen($buffer));
					}
					fclose($fp_temp);

					if (copy($tempfilename, $this->filename)) {
						unlink($tempfilename);
						fclose($fp_source);
						return true;
					}
					unlink($tempfilename);
					$this->errors[] = 'FAILED: copy('.$tempfilename.', '.$this->filename.')';

				} else {
					$this->errors[] = 'Could not fopen("'.$tempfilename.'", "wb")';
				}
			}
			fclose($fp_source);
			return false;
		}
		$this->errors[] = 'Could not fopen("'.$this->filename.'", "r+b")';
		return false;
	}

}
