<?php
namespace App\Cron\Mutex;

/**
 * Use flock() calls to put a lock on a file
 * @author Lucian Sirbu
 */
class FLock implements AbstractLock
{
	/**
	 * Lock file resource
	 * @var resource
	 */
	private $handle = null;

	/**
	 * Lock file path
	 * @var string
	 */
	private $file = null;

	public function __construct($id)
	{
		$CFG = \Engine\Loader_Cfg::getInstance()->getGlobal();
		$this->setFile(sprintf("%s%s%s.lock", APP_REAL_PATH, $CFG['paths']['temporary'], $id));
	}

	/**
	 * Set the file to be locked/unlocked
	 * @return \App\Cron\Mutex\FLock
	 */
	protected function setFile($file)
	{
		$this->file = $file;
		return $this;
	}

	/**
	 * Lock and block waiting
	 * {@inheritDoc}
	 * @see \App\Cron\Mutex\AbstractLock::lock()
	 */
	public function lock()
	{
		if (!file_exists($this->file)) {
			file_put_contents($this->file, '');
			chmod($this->file, 0777);
		}

		$this->handle = fopen($this->file, 'wb');
		if ($this->handle) {
			flock($this->handle, LOCK_EX);	// shared lock - read-only
		}

		return $this;
	}

	/**
	 * Unlock, if previously locked
	 * {@inheritDoc}
	 * @see \App\Cron\Mutex\AbstractLock::unlock()
	 */
	public function unlock()
	{
		if ($this->handle) {
			flock($this->handle, LOCK_UN);
			fclose($this->handle);
		}

		return $this;
	}

	/**
	 * Check if a lock is in effect
	 * {@inheritDoc}
	 * @see \App\Cron\Mutex\AbstractLock::isLocked()
	 */
	public function isLocked()
	{
		if (!file_exists($this->file)) {
			file_put_contents($this->file, '');
			chmod($this->file, 0777);
			return false;
		}

		$handle = fopen($this->file, 'wb');
		if ($handle) {
			$ret = !flock($handle, LOCK_EX | LOCK_NB);
			fclose($handle);
			return $ret;
		}

		return true;
	}

	/**
	 * Check if a lock is in effect.
	 * When doing checks in parallel (FluX-es/Cronjobs) then one of the callers might lock the other one and thus cause a false-positive
	 * In order to avoid this, we keep re-checking the lock until it's gone or the timeout passes
	 * @return bool
	 */
	public function isLockedWithTimeout($timeoutMin=0.0, $timeoutMax=0.5, $loopsMin=1, $loopsMax=PHP_INT_MAX)
	{
		$keepWaiting = true;
		$loopStart = microtime(true);
		$locked = true;
		$loops = 0;
		do {
			if ($keepWaiting && !$this->isLocked()) {
				$locked = false;
			}

			// internal timeout expired
			if ($timeoutMax!==null && $keepWaiting && (microtime(true) - $loopStart)>=$timeoutMax) {
				$keepWaiting = false;
			}

			// internal loop limit expired
			if ($loopsMax!==null && $keepWaiting && $loops>=$loopsMax) {
				$keepWaiting = false;
			}

			// minimum limits checks
			if ($loopsMin!==null && !$keepWaiting && $loops<$loopsMin) {
				$keepWaiting = true;
			}
			if ($timeoutMin!==null && !$keepWaiting && (microtime(true) - $loopStart)<$timeoutMin) {
				$keepWaiting = true;
			}

			if ($keepWaiting && $locked) {
				usleep(1);
			}
			$loops++;
		} while ($keepWaiting && $locked);

		return $locked;
	}

	/**
	 * {@inheritDoc}
	 * @see \App\Cron\Mutex\AbstractLock::rawGetIsLockedReasons()
	 */
	public function rawGetIsLockedReasons()
	{
		return [];
	}

	/**
	 * {@inheritDoc}
	 * @see \App\Cron\Mutex\AbstractLock::wasAquired()
	 */
	public function wasAquired()
	{
		return (bool)$this->handle;
	}

	/**
	 * {@inheritDoc}
	 * @see \App\Cron\Mutex\AbstractLock::lockWithTimeout()
	 */
	public function lockWithTimeout($timeout)
	{
		$keepWaiting = true;
		$loopStart = microtime(true);
		$locked = false;
		do {
			// the lock is unlocked, lock now
			if ($keepWaiting && !$this->isLocked()) {
				$keepWaiting = false;
				$locked = true;
				$this->lock();
			}

			// internal timeout expired
			if ($keepWaiting && (microtime(true) - $loopStart)>=$timeout) {
				$keepWaiting = false;
			}

			if ($keepWaiting) {
				usleep(1);
			}
		} while ($keepWaiting);

		return $locked;
	}

	/**
	 * Physically removes the lockfile
	 * !!! USE CAREFULLY !!!
	 * @return bool
	 */
	public function removeLock()
	{
		unlink($this->file);
		return $this;
	}

}
