<?php

/*
 * This file is part of MailSo.
 *
 * (c) 2014 Usenko Timur
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace MailSo\Log;

/**
 * @category MailSo
 * @package Log
 */
class Logger extends \SplFixedArray
{
	private bool $bUsed = false;

	private int $iLevel = \LOG_WARNING;

	private array $aSecretWords = [];

	private bool $bShowSecrets = false;

	private static $SIGNALS = [
		'SIGABRT',
		'SIGALRM',
		'SIGBABY',
		'SIGBUS',
		'SIGCHLD',
		'SIGCLD',
//		'SIGCONT',
		'SIGFPE',
		'SIGHUP',
		'SIGILL',
//		'SIGINT',
		'SIGIO',
		'SIGIOT',
//		'SIGKILL',
		'SIGPIPE',
		'SIGPOLL',
		'SIGPROF',
		'SIGPWR',
		'SIGQUIT',
		'SIGSEGV',
		'SIGSTKFLT',
//		'SIGSTOP',
		'SIGSYS',
		'SIGTERM',
		'SIGTRAP',
		'SIGTSTP',
		'SIGTTIN',
		'SIGTTOU',
		'SIGURG',
		'SIGUSR1',
		'SIGUSR2',
		'SIGVTALRM',
		'SIGWINCH',
		'SIGXCPU',
		'SIGXFSZ'
	];

	function __construct(bool $bMainLogger = false)
	{
		parent::__construct();

		if ($bMainLogger) {
			\set_error_handler([$this, '__phpErrorHandler']);
			\set_exception_handler([$this, '__phpExceptionHandler']);
			\register_shutdown_function([$this, '__loggerShutDown']);

			if (\is_callable('pcntl_signal')) {
				\pcntl_async_signals(true);
				foreach (static::$SIGNALS as $SIGNAL) {
					\defined($SIGNAL) && \pcntl_signal(\constant($SIGNAL), [$this, 'signalHandler']);
				}
			}
		}
	}

	/**
	 * @staticvar string $sCache;
	 */
	public static function Guid() : string
	{
		static $sCache = null;
		if (null === $sCache) {
			$sCache = \substr(\MailSo\Base\Utils::Sha1Rand(), -8);
		}

		return $sCache;
	}

	public function append($oDriver) : void
	{
		if ($oDriver) {
			$this->setSize(1);
			$this[0] = $oDriver;
		}
	}

	public function IsEnabled() : bool
	{
		return 0 < $this->count();
	}

	public function AddSecret(
		#[\SensitiveParameter]
		string $sWord
	) : void
	{
//		$this->bShowSecrets && $this->Write("AddSecret '{$sWord}'", \LOG_INFO);
		$sWord = \trim($sWord);
		if (\strlen($sWord)) {
			$this->aSecretWords[] = $sWord;
			$this->aSecretWords = \array_unique($this->aSecretWords);
			\usort($this->aSecretWords, fn($a,$b) => \strlen($b) - \strlen($a));
		}
	}

	public function SetShowSecrets(bool $bShow) : self
	{
		$this->bShowSecrets = $bShow;
		return $this;
	}

	public function ShowSecrets() : bool
	{
		return $this->bShowSecrets;
	}

	public function SetLevel(int $iLevel) : self
	{
		$this->iLevel = $iLevel;
		return $this;
	}

	const PHP_TYPES = array(
		\E_ERROR => \LOG_ERR,
		\E_WARNING => \LOG_WARNING,
		\E_PARSE => \LOG_CRIT,
		\E_NOTICE => \LOG_NOTICE,
		\E_CORE_ERROR => \LOG_ERR,
		\E_CORE_WARNING => \LOG_WARNING,
		\E_COMPILE_ERROR => \LOG_ERR,
		\E_COMPILE_WARNING => \LOG_WARNING,
		\E_USER_ERROR => \LOG_ERR,
		\E_USER_WARNING => \LOG_WARNING,
		\E_USER_NOTICE => \LOG_NOTICE,
		\E_STRICT => \LOG_CRIT,
		\E_RECOVERABLE_ERROR => \LOG_ERR,
		\E_DEPRECATED => \LOG_INFO,
		\E_USER_DEPRECATED => \LOG_INFO
	);

	const PHP_TYPE_POSTFIX = array(
		\E_ERROR => '',
		\E_WARNING => '',
		\E_PARSE => '-PARSE',
		\E_NOTICE => '',
		\E_CORE_ERROR => '-CORE',
		\E_CORE_WARNING => '-CORE',
		\E_COMPILE_ERROR => '-COMPILE',
		\E_COMPILE_WARNING => '-COMPILE',
		\E_USER_ERROR => '-USER',
		\E_USER_WARNING => '-USER',
		\E_USER_NOTICE => '-USER',
		\E_STRICT => '-STRICT',
		\E_RECOVERABLE_ERROR => '-RECOVERABLE',
		\E_DEPRECATED => '-DEPRECATED',
		\E_USER_DEPRECATED => '-USER_DEPRECATED'
	);

	public function __phpErrorHandler(int $iErrNo, string $sErrStr, string $sErrFile, int $iErrLine) : bool
	{
		if (\error_reporting() & $iErrNo) {
			$this->Write(
				"{$sErrStr} {$sErrFile} [line:{$iErrLine}, code:{$iErrNo}]",
				static::PHP_TYPES[$iErrNo],
				'PHP' . static::PHP_TYPE_POSTFIX[$iErrNo]
			);
		}
		/* forward to standard PHP error handler */
		return false;
	}

	/**
	 * Called by PHP when an Exception is uncaught
	 */
	public function __phpExceptionHandler(\Throwable $oException): void
	{
		$this->Write('Uncaught exception: ' . $oException, \LOG_CRIT);
		\error_log('Uncaught exception: ' . $oException);
	}

	public function __loggerShutDown() : void
	{
		if ($this->bUsed) {
			$error = \error_get_last();
			$error && $this->Write('Last error: '.\json_encode($error));
			$this->Write('Memory peak usage: '.\MailSo\Base\Utils::FormatFileSize(\memory_get_peak_usage(true), 2));
			$this->Write('Time delta: '.(\microtime(true) - $_SERVER['REQUEST_TIME_FLOAT']));
		}
	}

	public function signalHandler($signo, /*?array*/$siginfo = null)
	{
		if (\SIGTERM == $signo) {
			exit;
		}
		foreach (static::$SIGNALS as $SIGNAL) {
			if (\defined($SIGNAL) && \constant($SIGNAL) == $signo) {
				$this->Write("Caught {$SIGNAL} ".($siginfo ? \json_encode($siginfo) : ''), \LOG_CRIT, 'PHP');
				break;
			}
		}
	}

	public function Write(string $sDesc, int $iType = \LOG_INFO, string $sName = '', bool $bDiplayCrLf = false) : bool
	{
		if ($this->iLevel < $iType) {
			return true;
		}

		$this->bUsed = true;

		if (!$this->bShowSecrets && $this->aSecretWords) {
			$sDesc = \str_replace($this->aSecretWords, '*******', $sDesc);
		}

		$iResult = 1;

		foreach ($this as /* @var $oLogger \MailSo\Log\Driver */ $oLogger)
		{
			$iResult = $oLogger->Write($sDesc, $iType, $sName, $bDiplayCrLf);
		}

		return (bool) $iResult;
	}

	/**
	 * @param mixed $mValue
	 */
	public function WriteDump($mValue, int $iType = \LOG_INFO, string $sName = '') : bool
	{
		return $this->Write(\print_r($mValue, true), $iType, $sName);
	}

	private $aExceptions = [];

	public function WriteException(\Throwable $oException, int $iType = \LOG_NOTICE, string $sName = '') : void
	{
		if (!\in_array($oException, $this->aExceptions)) {
			$this->Write((string) $oException, $iType, $sName);
			$this->aExceptions[] = $oException;
		}
	}

	public function WriteExceptionShort(\Throwable $oException, int $iType = \LOG_NOTICE, string $sName = '') : void
	{
		if (!\in_array($oException, $this->aExceptions)) {
			$this->Write($oException->getMessage(), $iType, $sName);
			$this->aExceptions[] = $oException;
		}
	}
}
