<?php

/*
 *
 *  ____            _        _   __  __ _                  __  __ ____
 * |  _ \ ___   ___| | _____| |_|  \/  (_)_ __   ___      |  \/  |  _ \
 * | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
 * |  __/ (_) | (__|   <  __/ |_| |  | | | | | |  __/_____| |  | |  __/
 * |_|   \___/ \___|_|\_\___|\__|_|  |_|_|_| |_|\___|     |_|  |_|_|
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * @author PocketMine Team
 * @link http://www.pocketmine.net/
 *
 *
 */

declare(strict_types=1);

namespace pocketmine\server_phar_stub;

use function clearstatcache;
use function copy;
use function fclose;
use function fflush;
use function flock;
use function fopen;
use function fwrite;
use function getmypid;
use function hrtime;
use function is_dir;
use function is_file;
use function mkdir;
use function number_format;
use function str_replace;
use function stream_get_contents;
use function sys_get_temp_dir;
use function tempnam;
use function unlink;
use const DIRECTORY_SEPARATOR;
use const LOCK_EX;
use const LOCK_NB;
use const LOCK_UN;

/**
 * Finds the appropriate tmp directory to store the decompressed phar cache, accounting for potential file name
 * collisions.
 */
function preparePharCacheDirectory() : string{
	clearstatcache();

	$i = 0;
	do{
		$tmpPath = sys_get_temp_dir() . '/PocketMine-MP-phar-cache.' . $i;
		$i++;
	}while(is_file($tmpPath));
	if(!@mkdir($tmpPath) && !is_dir($tmpPath)){
		throw new \RuntimeException("Failed to create temporary directory $tmpPath. Please ensure the disk has enough space and that the current user has permission to write to this location.");
	}

	return $tmpPath;
}

/**
 * Deletes caches left behind by previous server instances.
 * This ensures that the tmp directory doesn't get flooded by servers crashing in restart loops.
 */
function cleanupPharCache(string $tmpPath) : void{
	clearstatcache();

	/** @var string[] $matches */
	foreach(new \RegexIterator(
		new \FilesystemIterator(
			$tmpPath,
			\FilesystemIterator::CURRENT_AS_PATHNAME | \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::UNIX_PATHS
		),
		'/(.+)\.lock$/',
		\RegexIterator::GET_MATCH
	) as $matches){
		$lockFilePath = $matches[0];
		$baseTmpPath = $matches[1];

		$file = @fopen($lockFilePath, "rb");
		if($file === false){
			//another process probably deleted the lock file already
			continue;
		}

		if(flock($file, LOCK_EX | LOCK_NB)){
			//this tmpfile is no longer in use
			flock($file, LOCK_UN);
			fclose($file);

			unlink($lockFilePath);
			unlink($baseTmpPath . ".tar");
			unlink($baseTmpPath);
			echo "Deleted stale phar cache at $baseTmpPath\n";
		}else{
			$pid = stream_get_contents($file);
			fclose($file);

			echo "Phar cache at $baseTmpPath is still in use by PID $pid\n";
		}
	}
}

function convertPharToTar(string $tmpName, string $pharPath) : string{
	$tmpPharPath = $tmpName . ".phar";
	copy($pharPath, $tmpPharPath);

	$phar = new \Phar($tmpPharPath);
	//phar requires phar.readonly=0, and zip doesn't support disabling compression - tar is the only viable option
	//we don't need phar anyway since we don't need to directly execute the file, only require files from inside it
	$phar->convertToData(\Phar::TAR, \Phar::NONE);
	unset($phar);
	\Phar::unlinkArchive($tmpPharPath);

	return $tmpName . ".tar";
}

/**
 * Locks a phar tmp cache to prevent it from being deleted by other server instances.
 * This code looks similar to Filesystem::createLockFile(), but we can't use that because it's inside the compressed
 * phar.
 */
function lockPharCache(string $lockFilePath) : void{
	//this static variable will keep the file(s) locked until the process ends
	static $lockFiles = [];

	$lockFile = fopen($lockFilePath, "wb");
	if($lockFile === false){
		throw new \RuntimeException("Failed to open temporary file");
	}
	flock($lockFile, LOCK_EX); //this tells other server instances not to delete this cache file
	fwrite($lockFile, (string) getmypid()); //maybe useful for debugging
	fflush($lockFile);
	$lockFiles[$lockFilePath] = $lockFile;
}

/**
 * Prepares a decompressed .tar of PocketMine-MP.phar in the system temp directory for loading code from.
 *
 * @return string path to the temporary decompressed phar (actually a .tar)
 */
function preparePharCache(string $tmpPath, string $pharPath) : string{
	clearstatcache();

	$tmpName = tempnam($tmpPath, "PMMP");
	if($tmpName === false){
		throw new \RuntimeException("Failed to create temporary file");
	}

	lockPharCache($tmpName . ".lock");
	return convertPharToTar($tmpName, $pharPath);
}

$tmpDir = preparePharCacheDirectory();
cleanupPharCache($tmpDir);
echo "Preparing PocketMine-MP.phar decompressed cache...\n";
$start = hrtime(true);
$cacheName = preparePharCache($tmpDir, __FILE__);
echo "Cache ready at $cacheName in " . number_format((hrtime(true) - $start) / 1e9, 2) . "s\n";

require 'phar://' . str_replace(DIRECTORY_SEPARATOR, '/', $cacheName) . '/src/PocketMine.php';