<?php
/**
 * Per-request resource budget.
 *
 * @package LocalGuardScanner
 */

namespace LocalGuardScanner\Application\Scan;

/**
 * Stops work before the hosting provider's hard limits.
 */
final class ResourceBudget {
	/** @var float */
	private $started_at;
	/** @var float */
	private $seconds;
	/** @var int */
	private $memory_ceiling;
	/** @var callable */
	private $clock;
	/** @var callable */
	private $memory_reader;

	/**
	 * @param float         $seconds       Maximum elapsed seconds.
	 * @param int           $memory_ceiling Absolute memory ceiling in bytes.
	 * @param callable|null $clock         Monotonic clock.
	 * @param callable|null $memory_reader Current memory reader.
	 */
	public function __construct( $seconds, $memory_ceiling, $clock = null, $memory_reader = null ) {
		$this->seconds        = max( 0.05, (float) $seconds );
		$this->memory_ceiling = max( 1, (int) $memory_ceiling );
		$this->clock          = null !== $clock ? $clock : static function () {
			return microtime( true );
		};
		$this->memory_reader  = null !== $memory_reader ? $memory_reader : static function () {
			return memory_get_usage( true );
		};
		$this->started_at     = (float) call_user_func( $this->clock );
	}

	/**
	 * Build a conservative default from the PHP environment.
	 *
	 * @param int $memory_limit_bytes Parsed PHP memory limit.
	 * @return self
	 */
	public static function conservative( $memory_limit_bytes ) {
		$current    = memory_get_usage( true );
		$remaining  = max( 1, (int) $memory_limit_bytes - $current );
		$additional = min( 32 * 1024 * 1024, (int) floor( $remaining * 0.6 ) );
		return new self( 8.0, $current + max( 1, $additional ) );
	}

	/**
	 * Whether another unit of work may start.
	 *
	 * @return bool
	 */
	public function can_continue() {
		$elapsed = (float) call_user_func( $this->clock ) - $this->started_at;
		$memory  = (int) call_user_func( $this->memory_reader );
		return $elapsed < $this->seconds && $memory < $this->memory_ceiling;
	}

	/** @return float */
	public function elapsed() {
		return max( 0.0, (float) call_user_func( $this->clock ) - $this->started_at );
	}
}
