#!/usr/bin/env php
<?php

ini_set('error_reporting', E_ALL);

function td(...$a) { foreach ($a as $v) var_dump($v); die('td()'); }
function tp(...$a) { foreach ($a as $v) var_dump($v); echo 'tp()'; if (count($a)===1) return $a[0]; }

class Config
{
	public $read_quant = 16 * 1024;
	public $total_bytes = null;
	public $terminal_nr_columns;

	function totalBytesConfigFromInputs($hin)
	{
		$a = fstat($hin);
		if ($a['size'] > 0)
			$this->total_bytes = $a['size'];
	}

	function doEnvironment()
	{
			# doesn't seem to work?
			# the var exists in bash, but doesn't get exported to processes - why?
		$n = getenv('COLUMNS');
		if (!empty($n))
			$this->terminal_nr_columns = (int)$n;
	}

	function doTerminalDetection()
	{
		$tput_n = (int)exec('tput cols');
		if (!empty($tput_n))
			$this->terminal_nr_columns = $tput_n;
	}

	protected
	function interpretBytesSpecifier(string $str) : int
	{
		$n = $m = null;
		sscanf($str, '%d%s', $n, $m);

		switch ($m) {
		case 'T':
		case 't':
			$n *= 1024;
		case 'G':
		case 'g':
			$n *= 1024;
		case 'M':
		case 'm':
			$n *= 1024;
		case 'K':
		case 'k':
			$n *= 1024;
		case null:
			return $n;
		default:
			die_with_error(3, sprintf('unsupported bytes multiplier "%s"', $m)); }
	}

	function doGetopt(array $argv)
	{
		$a = getopt('s:q:v', [ 'size:' ]);

		foreach ($a as $opt => $v) {
			switch ($opt) {
			case 's':
			case 'size':
				$this->total_bytes = $this->interpretBytesSpecifier($v);
				break;
			default:
				die_with_error(3, sprintf('unsupported option: "%s"', $opt)); } }
	}
}

class State
{
	public $time_start_sec;	# a float from microtime()
	public $processed_bytes;

	function __construct()
	{
		$this->time_start_sec = $this->timeCurrentSec();
		$this->processed_bytes = 0;
	}

	function timeCurrentSec() : float
	{
		return microtime($get_as_float = true);
	}

	function timeRunningSec() : float
	{
		return $this->timeCurrentSec() - $this->time_start_sec;
	}
}

function die_with_error(int $code, string $str)
{
	fputs(STDERR, $str .PHP_EOL);
	die($code);
}

function terminal_output(string $str)
{
	$hinfo = STDERR;
	fputs($hinfo, $str);
}

function terminal_enqueue_flush(string $str = null, bool $do_flush = true) : int
{
	static $last_line_len = 0;
	static $buffer;

	if ($str !== null)
		$buffer .= $str;

	if ($do_flush) {
		$last_line_len = strlen($buffer);
		terminal_output($buffer);
		$buffer = ''; }

	return $last_line_len;
}

function terminal_last_line_len() : int
{
	return terminal_enqueue_flush(null, false);
}

function terminal_enqueue(string $str)
{
	terminal_enqueue_flush($str, $do_flush = false);
}

function terminal_flush_line()
{
	terminal_enqueue_flush(null, $do_flush = true);
}

function progress_num(int $pos, Config $Config, State $State) : string
{
	$R = function(int $n = null) : ?int { if ($n === null) return $n; return $n / 1024; };
	$F2 = function(int $n = null) : string { if ($n === null) return '?'; return sprintf('%dk', $n); };
	return sprintf("%s / %s", $F2($R($State->processed_bytes)), $F2($R($Config->total_bytes)));
}

function progress_bar(int $pos, Config $Config, State $State) : string
{
	$endpA_marker = '[';
	$endpB_marker = ']';
	$vesegment_maker = '=';
	$vpsegment_marker = '>';
	$bsegment_marker = ' ';

	if ($Config->total_bytes === null) {
		$v = 1.0;
		$perc_msg = '??%'; }
	else {
		$v = 1.0 * $State->processed_bytes / $Config->total_bytes;
		$perc_msg = sprintf(' % 5.1f%%', $v * 100); }

	$perc_msg_width = strlen($perc_msg);
	$fudge = ' ';
	$fudge_width = strlen($fudge);

	$bar_width = $Config->terminal_nr_columns - $pos - $perc_msg_width - $fudge_width;
	$endp_markers_width = strlen($endpA_marker .$endpB_marker);
	$vpsegment_marker_width = strlen($vpsegment_marker);
	$total_segments = $bar_width - $endp_markers_width;
	$total_segments = max(0, $total_segments);
	$total_segments = min($total_segments, $bar_width);

	$vsegments = floor($total_segments * $v);
	$vpsegments = ($vsegments > 0);
	$vesegments = $vsegments - ($vpsegments * $vpsegment_marker_width);
	$bsegments = $total_segments - $vsegments;

	return $endpA_marker .str_repeat($vesegment_maker, $vesegments) .str_repeat($vpsegment_marker, $vpsegments)
		.str_repeat($bsegment_marker, $bsegments) .$endpB_marker
		.$perc_msg;
}

function running_time(int $pos, Config $Config, State $State) : string
{
		# force integer division
	$s = (int)round($State->timeRunningSec());
	$m = $s / 60;
	$s = $s % 60;
	$h = $m / 60;
	$m = $m % 60;

	return sprintf('%02d:%02d:%02d', $h, $m, $s);
}

function pick_iec_unit(float $bs) : array
{
	$u = [
		1 => 'KiB',
		2 => 'MiB',
		3 => 'GiB',
		4 => 'TiB',
		5 => 'PiB', ];

	$se = intval(floor(log($bs, 1024)));
	if (array_key_exists($se, $u))
		return [ $u[$se], 1.0 * $bs / (1024 ** $se) ];
	else
		return [ 'B', $bs ];
}

function overall_bandwidth_info(int $pos, Config $Config, State $State) : string
{
	$bw = $State->processed_bytes / $State->timeRunningSec();
	[ $unit, $bs ] = pick_iec_unit($bw);

	return sprintf('[ %.3g %s/s ]', $bs, $unit);
}

function show_progress(Config $Config, State $State) {
	$pos = 0;
	$XPOS = function(string $str) use(&$pos) : string
	{
		$pos += strlen($str);
		return $str;
	};

	if (1)
		terminal_op_clear_line();
	else
		terminal_output("\n");

	terminal_enqueue($XPOS(progress_num($pos, $Config, $State)));
	terminal_enqueue($XPOS($separator = ' '));
	terminal_enqueue($XPOS(running_time($pos, $Config, $State)));
	terminal_enqueue($XPOS($separator = ' '));
		# i think the original uses momentary bandwidth...?
	terminal_enqueue($XPOS(overall_bandwidth_info($pos, $Config, $State)));
	terminal_enqueue($XPOS($separator = ' '));
	terminal_enqueue($XPOS(progress_bar($pos, $Config, $State)));
	terminal_flush_line();
}

function adjust_quant(Config $Config, State $State)
{
	$a = [ $Config->read_quant ];
	if ($State->processed_bytes > 128 * 1024)
		$a[] = 128 * 1024;
#	if ($State->processed_bytes > 1024*1024)
#		$a[] = 1024 * 1024;
	$Config->read_quant = max($a);
}

function terminal_is_lineaddr() : bool
{
	static $ret = null;

	if ($ret === null) {
		switch (getenv('TERM')) {
		default:
		case 'dumb':
			$ret = false;
			break;
		case 'linux':
		case 'xterm':
			$ret = true; } }

	return $ret;
}

function terminal_op_clear_line()
{
	if (terminal_is_lineaddr())
		terminal_lineaddr_op_clear_line();
	else
		terminal_dumb_op_clear_line();
}

function terminal_dumb_op_clear_line()
{
	$cnt = terminal_last_line_len();
	$rubout = "\x08";
	terminal_output(str_repeat($rubout, $cnt));
}

function terminal_lineaddr_op_clear_line()
{
	$str = "\r";
	terminal_output($str);
}

$hin = STDIN;
$hout = STDOUT;

$Config = new Config();
$Config->totalBytesConfigFromInputs($hin);
$Config->doTerminalDetection();
$Config->doEnvironment();
$Config->doGetopt($argv);

$State = new State();

$continue_copy = true;
while ($continue_copy) {
	if (feof($hin)) {
		$continue_copy = false;
		break; }
	if (feof($hout))
		die_with_error(1, 'Output write error');

	$data = fread($hin, $Config->read_quant);
	if ($data === false)
		die_with_error(2, 'Input read error');

	$v = fwrite($hout, $data);
	if ($v === false)
		die_with_error(1, 'Output write error');

	$State->processed_bytes += strlen($data);
	show_progress($Config, $State);
	adjust_quant($Config, $State);
}

terminal_output("\n");
