<?

// оболочка для функций fopen, fwrite... Чтобы можно было полностью протестировать программу
class File {
	
	public $fh; // иногда используем извне
	private $obj;
	private $useClass; // для работы либо проведения тестирования. 'Testing_file' или 'File_func_wrapper';
	public $last_error='';
	public $fpath = '';

	function __construct($fpath, $mode, $is_test) {
		GLOBAL $log, $OP;

		$this->useClass = $is_test ? 'File_func_test' : 'File_func_wrap';
		$this->obj = new $this->useClass($OP);
		$this->is_test = $is_test;
		$this->fpath = $fpath;
		$this->mode = $mode;
	}
	
	// открытие в виде отдельного метода, потому что конструктор не может вернуть ошибку, если файл не найден итд
	// $popytka нужна только для тестирования
	function open($popytka=0) {
		GLOBAL $log, $thread, $Disc_info;

		// ломаем метод ради скорости
		for($x=0; $x<5; $x++) {
			$GLOBALS[DS_error] = false;
			$this->fh = fopen($this->fpath, $this->mode);
			$GLOBALS[DS_error] = true;
			
			// либо коннект прошел, либо нет но с диск не отвалился
			if($this->fh OR $Disc_info->isReady('cur'))
				break;
			
			$thread->disk_ready_wait();
		}

		// // для отладки
		// if(preg_match('#(2|5)$#', $this->fpath)) {
		// 	if($popytka == 1)
		// 		return false;
		// }

		return $this->fh;

		// // если открыт файл на чтение, проверяем, существует ли он
		// if(in_array($this->mode, array('r', 'r+', 'rb', 'rb+')))
		// 	if(!$this->obj->is_exists($this->fpath)) {
		// 		$this->last_error = 'no_exists';
		// 		$log->line('файл не найден');
		// 		return false;
		// 	}
		
		// //$log->line("$fpath, $mode");
		// if( ! $this->fh = $this->obj->fopen($this->fpath, $this->mode)) {
		// 	$log->line('fopen не сработал');
		// 	$this->last_error = 'no_open';
		// 	return false;
		// }

		// return true;
	}

	// все сообщения выдаем через эту функцию
	// $method нужно указывать если используется промежуточная функция, такая как is_open_file
	private function _error($str, $method='') {
		GLOBAL $log;
		
		if(!$method) {
			$stack = debug_backtrace();
			$method = isset($stack[0]['func'])
							? $stack[0]['func']
							: '';
		}
		$log->line("File::{$method}(): $str");
	}
	
	function set_write_buffer($size_bt) {
		return stream_set_write_buffer($this->fh, $size_bt);

		//return $this->obj->set_write_buffer($this->fh, $size_bt);
	}

	private function is_open_file() {
		if(!$this->fh) {
			$stack = debug_backtrace();
			$method = $stack[0]['func'];
			$this->_error('файл не открыт', $method);
			return false;
		}
		return true;
	}
	
	function write(&$data_str, &$write_time) {
		GLOBAL $Disc_info, $thread, $log;

		// нещадно ломаем код чтобы работал быстрее
		for($x=0; $x<5; $x++) {

			// if($x == 0) {
			// 	$log->line("засыпаем, выдергивай флешку");
			// 	$thread->send_info(true);
			// 	sleep(15);
			// }

			$GLOBALS[DS_error] = false;

			$time_start = microtime(true);
			$len = fwrite($this->fh, &$data_str);
			$write_time =  microtime(true) - $time_start ;
			
			$GLOBALS[DS_error] = true;

			// то при его подключении все равно придется заново файл открыть
			// если записали сколько требовалось, либо возникла ошибка, но диск при этом подключен
			if($len === strlen($data_str) OR $Disc_info->isReady('cur'))
				break;
			
			// ждем диск, закрывам файл, открываем и на следующей итерации цикла пробуем записать еще раз.
			// потому что если диск отвалился в процессе записи, данные наверняка повредились или же записались частично. И если мы будет дописывать, то файл получится совсем не того размера который нам надо
			$thread->disk_ready_wait();
			$this->close();
			$log->line("Открываем файл заново и пробуем перезаписать: {$this->fpath}");
			$this->open();
		}


		return $len;

		// if(!$this->is_open_file('write')) return false;
		// if(!strlen($data_str)) {$this->_error('write', 'передана пустая строка для записи'); return false;}
		
		// //$log->line("запись " . strlen($data_str));
		
		// return $this->obj->fwrite($this->fh, $data_str, $write_time);
	}
	
	function read(&$res, $len, &$time) {
		if(!$this->is_open_file('read')) return false;
		if($len < 1) {$this->_error('read', 'длина строки должна быть больше нуля'); return false;}
		
		return $this->obj->fread($this->fh, $res, $len, $time);
	}
	
	
	// ЭТО ОПТИМИЗИРОВАННАЯ ФУНКЦИЯ
	// она специально заточенна под быстрое чтение и не возвращает фактические данные.
	// Режим посекторного чтения. Если файл быстрый, прочитает и вернет содержимое. Если медленный, вернет false. В обоих случаях будет указана скорость чтения и -1 если не удалось прочитать один из секторов.
	// return {status=ok|timeout|read_error|low_sect_speed, data, speed, sect_timeout} sect_timeout - сектор на котором закончилось время
	// из min_speed (MB) и file_size (KB) определяем какой у нас есть лимит времени
	// sect_size (KB) - размер сектора который читаем за раз
	function read_by_sect($min_speed, $min_sect_speed, $file_size, $sect_size) {
		GLOBAL $log, $Gen_str, $GOP;
		STATIC $sample_sect_str;

		if(!isset($sample_sect_str)) {
			$sample_sect_str = str_repeat($GOP['file_content'], $sect_size * 1024);
			usleep(500*1000); // чтобы нагрузка с процессора спала. Если она будет 100%, скорость может посчитаться неправильно
		}
		
		// максимально время на чтение всего файла
		//$log->line("$file_size / ($min_speed *1024)");
		$max_all_time = $file_size / ($min_speed*1024);
		if($this->is_test)
			$log->line("max_time_file:  $max_all_time");
		
		$sect_size_bytes = $sect_size * 1024;
		$sectMax = floor($file_size / $sect_size);
		$all_time = 0;
		
		//$log->line("\$sectMax $sectMax ");
		
		for($sectI = 1; $sectI <= $sectMax; $sectI++ ) {
			// читаем сектор
			// в тестовом методе read(), usleep(и аналог с наносекундами) не может заснуть точно на указанное время, поэтому 
			// read должен сам возвращать свое время
			//usleep(100*1000);
			$res = '';
			$this->read($res, $sect_size_bytes, $read_time);
			if($res === false) {
				if($this->is_test) $log->line('read_error');
				return array('status'=>'read_error', 'speed'=>-1,  'comment'=>'');
			}
			
			$sect_time = $read_time;
			$sect_speed = ($sect_size/1024) / $sect_time; // скорость текущего сектора в MB

			// проверяем корректность данных
			//if(strcmp($sample_sect_str, $res) !== 0)
			if($sample_sect_str !== $res)
				return array('status'=>'bad_file', 'speed'=>-1,  'comment'=>'');
			
			//$all_mas[] = $res;
			$all_time += $sect_time; 

			if($this->is_test)
				$log->line("speed: $sect_speed    time: $sect_time");
			
			if($sect_speed < $min_sect_speed) {
				$result_speed = ($sectI * $sect_size / 1024) / $all_time; 

				if($this->is_test)
					$log->line('low_sect_speed');

				$log->line("Недостаточная секторная скорость. Общая скорость: " . $result_speed. ", сект: $sect_speed");
				return array('status'=>'low_sect_speed', 'speed'=>$result_speed, 'comment'=>"$sectI из $sectMax");
			}
			
			// если время превышено, выходим
			if($all_time > $max_all_time) {
				$result_speed =  ($sectI * $sect_size / 1024) / $all_time;
				if($this->is_test) $log->line('full_timeout');
				$log->line('Общее время превышено. Общая скорость: ' . $result_speed);
				return array('status'=>'timeout', 'speed'=>$result_speed, 'comment'=>"$sectI из $sectMax");
			}
		}
		
		// если все произошло так быстро, что время не удалось задетектить
		// такое бывает когда данные читаются из кеша
		if($all_time == 0) 
			$all_time = 0.000001;

		//$all_str = implode($all_mas);
		// все ок
		$result_speed = ($file_size/1024) / $all_time;
		return array('status'=>'ok', 'speed'=>$result_speed,  'comment'=>'');
	}
	
	function close() {
		$GLOBALS[DS_error] = false;
		fflush($this->fh);
		$res = fclose($this->fh);
		$GLOBALS[DS_error] = true;
		return $res;
		//return $this->obj->fclose($this->fh);
	}
	
	function flush() {
		return fflush($this->fh);
		//return $this->obj->fflush($this->fh);
	}
}

?>