<?php

/**
 * @copyright   Copyright (C) 2010-2024 Combodo SAS
 * @license     http://opensource.org/licenses/AGPL-3.0
 */

namespace Combodo\iTop\Service\Module;

use MetaModel;
use ReflectionClass;
use ReflectionMethod;
use utils;

class ModuleService
{
	/** @var ModuleService */
	private static $oInstance;

	private function __construct()
	{
	}

	public static function GetInstance(): ModuleService
	{
		if (!isset(static::$oInstance)) {
			static::$oInstance = new ModuleService();
		}

		return static::$oInstance;
	}

	/**
	 * Get a "signature" of the method of an extension in the form of: "[module-name] class::method()"
	 *
	 * @param object|string $object Object or class
	 * @param string $sMethod
	 *
	 * @return string
	 * @throws \ReflectionException
	 */
	public function GetModuleMethodSignature($object, string $sMethod): string
	{
		$sSignature = '';
		$oReflectionMethod = new ReflectionMethod($object, $sMethod);
		$oReflectionClass = $oReflectionMethod->getDeclaringClass();
		$sExtension = $this->GetModuleNameFromObject($oReflectionClass->getName());
		if (strlen($sExtension) !== 0) {
			$sSignature .= '['.$sExtension.'] ';
		} else {
			$sSignature .= '[core] ';
		}
		$sSignature .= $oReflectionClass->getShortName().'::'.$sMethod.'()';

		return $sSignature;
	}

	/**
	 * Get the module name from an object or class
	 *
	 * @param object|string $object
	 *
	 * @return string
	 * @throws \ReflectionException
	 */
	public function GetModuleNameFromObject($object): string
	{
		$oReflectionClass = new ReflectionClass($object);
		$sPath = str_replace('\\', '/', $oReflectionClass->getFileName());
		$sPattern = str_replace('\\', '/', '@'.APPROOT.'env-'.utils::GetCurrentEnvironment()).'/(?<ext>.+)/@U';
		if (preg_match($sPattern, $sPath, $aMatches) !== false) {
			if (isset($aMatches['ext'])) {
				return $aMatches['ext'];
			}
		}

		return '';
	}

	/**
	 * **Warning** : returned result can be invalid as we're using backtrace to find the module dir name
	 *
	 * @param int $iCallDepth The depth of the module in the callstack. Zero when called directly from within the module
	 *
	 * @return string the relative (to MODULESROOT) path of the root directory of the module containing the file where the call to
	 *     this function is made
	 *     or an empty string if no such module is found (or not called within a module file)
	 *
	 * @uses \debug_backtrace()
	 */
	public function GetCurrentModuleDir(int $iCallDepth): string
	{
		$sCurrentModuleDir = '';
		$aCallStack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
		$sCallerFile = realpath($aCallStack[$iCallDepth]['file']);

		foreach (GetModulesInfo() as $sModuleName => $aInfo) {
			if ($aInfo['root_dir'] !== '') {
				$sRootDir = realpath(APPROOT.$aInfo['root_dir']);

				if (substr($sCallerFile, 0, strlen($sRootDir)) === $sRootDir) {
					$sCurrentModuleDir = basename($sRootDir);
					break;
				}
			}
		}
		return $sCurrentModuleDir;
	}

	/**
	 * **Warning** : as this method uses {@see GetCurrentModuleDir} it produces hazardous results.
	 * You should better uses directly {@see GetAbsoluteUrlModulesRoot} and add the module dir name yourself ! See N°4573
	 *
	 * @return string the base URL for all files in the current module from which this method is called
	 * or an empty string if no such module is found (or not called within a module file)
	 * @throws \Exception
	 *
	 * @uses GetCurrentModuleDir
	 */
	public function GetCurrentModuleUrl(int $iCallDepth = 0): string
	{
		$sDir = $this->GetCurrentModuleDir(1 + $iCallDepth);
		if ($sDir !== '') {
			return utils::GetAbsoluteUrlModulesRoot().'/'.$sDir;
		}
		return '';
	}

	/**
	 * @param string $sProperty The name of the property to retrieve
	 * @param mixed $defaultValue
	 *
	 * @return mixed the value of a given setting for the current module
	 */
	public function GetCurrentModuleSetting(string $sProperty, $defaultValue = null)
	{
		$sModuleName = $this->GetCurrentModuleName(1);
		return MetaModel::GetModuleSetting($sModuleName, $sProperty, $defaultValue);
	}

	/**
	 * @param string $sModuleName
	 *
	 * @return string|NULL compiled version of a given module, as it was seen by the compiler
	 */
	public function GetCompiledModuleVersion(string $sModuleName): ?string
	{
		$aModulesInfo = GetModulesInfo();
		if (array_key_exists($sModuleName, $aModulesInfo)) {
			return $aModulesInfo[$sModuleName]['version'];
		}
		return null;
	}

	/**
	 * Returns the name of the module containing the file where the call to this function is made
	 * or an empty string if no such module is found (or not called within a module file)
	 *
	 * @param int $iCallDepth The depth of the module in the callstack. Zero when called directly from within the module
	 *
	 * @return string
	 */
	public function GetCurrentModuleName(int $iCallDepth = 0): string
	{
		$sCurrentModuleName = '';
		$aCallStack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
		$sCallerFile = realpath($aCallStack[$iCallDepth]['file']);

		return $this->GetModuleNameFromPath($sCallerFile);
	}

	private function GetModuleNameFromPath($sPath)
	{
		foreach (GetModulesInfo() as $sModuleName => $aInfo) {
			if ($aInfo['root_dir'] !== '') {
				$sRootDir = realpath(APPROOT.$aInfo['root_dir']);
				if (substr($sPath, 0, strlen($sRootDir)) === $sRootDir) {

					return $sModuleName;
				}
			}
		}

		return '';
	}

	/**
	 * Get the extension code from the call stack.
	 * Scan the call stack until a module is found.
	 *
	 * @param int $iLevelsToIgnore
	 *
	 * @return string module name
	 */
	public function GetModuleNameFromCallStack(int $iLevelsToIgnore = 0): string
	{
		$aCallStack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
		$aCallStack = array_slice($aCallStack, $iLevelsToIgnore);

		foreach ($aCallStack as $aCallInfo) {
			$sFile = realpath(empty($aCallInfo['file']) ? '' : $aCallInfo['file']);

			$sModuleName = $this->GetModuleNameFromPath($sFile);
			if (strlen($sModuleName) > 0) {
				return $sModuleName;
			}
		}

		return '';
	}

}
