<?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\thread;

use pmmp\thread\Thread as NativeThread;
use pmmp\thread\ThreadSafeArray;
use pocketmine\crash\CrashDump;
use pocketmine\errorhandler\ErrorToExceptionHandler;
use pocketmine\Server;
use function error_get_last;
use function error_reporting;
use function implode;
use function register_shutdown_function;
use function set_exception_handler;

trait CommonThreadPartsTrait{
	/**
	 * @var ThreadSafeArray|ThreadSafeClassLoader[]|null
	 * @phpstan-var ThreadSafeArray<int, ThreadSafeClassLoader>|null
	 */
	private ?ThreadSafeArray $classLoaders = null;
	protected ?string $composerAutoloaderPath = null;

	protected bool $isKilled = false;

	private ?ThreadCrashInfo $crashInfo = null;

	/**
	 * @return ThreadSafeClassLoader[]
	 */
	public function getClassLoaders() : ?array{
		return $this->classLoaders !== null ? (array) $this->classLoaders : null;
	}

	/**
	 * @param ThreadSafeClassLoader[] $autoloaders
	 */
	public function setClassLoaders(?array $autoloaders = null) : void{
		$this->composerAutoloaderPath = \pocketmine\COMPOSER_AUTOLOADER_PATH;

		if($autoloaders === null){
			$autoloaders = [Server::getInstance()->getLoader()];
		}

		if($this->classLoaders === null){
			$loaders = $this->classLoaders = new ThreadSafeArray();
		}else{
			$loaders = $this->classLoaders;
			foreach($this->classLoaders as $k => $autoloader){
				unset($this->classLoaders[$k]);
			}
		}
		foreach($autoloaders as $autoloader){
			$loaders[] = $autoloader;
		}
	}

	/**
	 * Registers the class loaders for this thread.
	 *
	 * @internal
	 */
	public function registerClassLoaders() : void{
		if($this->composerAutoloaderPath !== null){
			require $this->composerAutoloaderPath;
		}
		$autoloaders = $this->classLoaders;
		if($autoloaders !== null){
			foreach($autoloaders as $autoloader){
				/** @var ThreadSafeClassLoader $autoloader */
				$autoloader->register(false);
			}
		}
	}

	public function getCrashInfo() : ?ThreadCrashInfo{
		//TODO: Joining a crashed worker might be a bit sus, but we need to make sure the thread's shutdown
		//handler has run before we try to collect the crash info. As of 6.1.1, pmmpthread sets isTerminated=true
		//*before* the shutdown handler is invoked, so we might land here before the crash info has been set.
		//In the future this should probably be fixed by running the shutdown handlers before setting isTerminated,
		//but this workaround should be good enough for now.
		//WARNING: Do not call this inside a synchronized block on this thread's context. Because the shutdown handler
		//runs in a synchronized block, this will result in a deadlock.
		if($this->isTerminated() && !$this->isJoined()){
			$this->join();
		}
		return $this->crashInfo;
	}

	public function start(int $options = NativeThread::INHERIT_NONE) : bool{
		ThreadManager::getInstance()->add($this);

		if($this->getClassLoaders() === null){
			$this->setClassLoaders();
		}
		return parent::start($options);
	}

	final public function run() : void{
		error_reporting(-1);
		$this->registerClassLoaders();
		//set this after the autoloader is registered
		ErrorToExceptionHandler::set();

		//this permits adding extra functionality to the exception and shutdown handlers via overriding
		set_exception_handler($this->onUncaughtException(...));
		register_shutdown_function($this->onShutdown(...));

		$this->onRun();
		$this->isKilled = true;
	}

	/**
	 * Stops the thread using the best way possible. Try to stop it yourself before calling this.
	 */
	public function quit() : void{
		$this->isKilled = true;

		if(!$this->isJoined()){
			$this->notify();
			$this->join();
		}

		ThreadManager::getInstance()->remove($this);
	}

	/**
	 * Called by set_exception_handler() when an uncaught exception is thrown.
	 */
	protected function onUncaughtException(\Throwable $e) : void{
		$this->synchronized(function() use ($e) : void{
			$this->crashInfo = ThreadCrashInfo::fromThrowable($e, $this->getThreadName());
			\GlobalLogger::get()->logException($e);
		});
	}

	/**
	 * Called by register_shutdown_function() when the thread shuts down. This may be because of a benign shutdown, or
	 * because of a fatal error. Use isKilled to determine which.
	 */
	protected function onShutdown() : void{
		$this->synchronized(function() : void{
			if($this->isTerminated() && $this->crashInfo === null){
				$last = error_get_last();
				if($last !== null && ($last["type"] & CrashDump::FATAL_ERROR_MASK) !== 0){
					//fatal error
					$crashInfo = ThreadCrashInfo::fromLastErrorInfo($last, $this->getThreadName());
				}else{
					//probably misused exit()
					$crashInfo = ThreadCrashInfo::fromThrowable(new \RuntimeException("Thread crashed without an error - perhaps exit() was called?"), $this->getThreadName());
				}
				$this->crashInfo = $crashInfo;

				$lines = [];
				//mimic exception printed format
				$lines[] = "Fatal error: " . $crashInfo->makePrettyMessage();
				$lines[] = "--- Stack trace ---";
				foreach($crashInfo->getTrace() as $frame){
					$lines[] = "  " . $frame->getPrintableFrame();
				}
				$lines[] = "--- End of fatal error information ---";
				\GlobalLogger::get()->critical(implode("\n", $lines));
			}
		});
	}

	/**
	 * Runs code on the thread.
	 */
	abstract protected function onRun() : void;

	public function getThreadName() : string{
		return (new \ReflectionClass($this))->getShortName();
	}
}