<?php
/**
 * Useful functions
 *
 * @package   MyAAC
 * @author    Slawkens <slawkens@gmail.com>
 * @copyright 2019 MyAAC
 * @link      https://my-aac.org
 */
defined('MYAAC') or die('Direct access not allowed!');

use MyAAC\Cache\Cache;
use MyAAC\CsrfToken;
use MyAAC\Items;
use MyAAC\Models\Config;
use MyAAC\Models\Guild;
use MyAAC\Models\House;
use MyAAC\Models\Pages;
use MyAAC\Models\Player;
use MyAAC\Models\PlayerDeath;
use MyAAC\Models\PlayerKillers;
use MyAAC\News;
use MyAAC\Plugins;
use MyAAC\Settings;
use PHPMailer\PHPMailer\PHPMailer;

function message($message, $type, $return)
{
	if(IS_CLI) {
		if($return) {
			return $message;
		}

		echo $message;
		return true;
	}

	if($return) {
		// for install and admin pages use bootstrap classes
		return '<div class="' . ((defined('MYAAC_INSTALL') || defined('MYAAC_ADMIN')) ? 'alert alert-' : '') . $type . '" style="margin-bottom:10px;">' . $message . '</div>';
	}

	echo '<div class="' . ((defined('MYAAC_INSTALL') || defined('MYAAC_ADMIN')) ? 'alert alert-' : '') . $type . '" style="margin-bottom:10px;">' . $message . '</div>';
	return true;
}
function success($message, $return = false) {
	return message($message, 'success', $return);
}
function warning($message, $return = false) {
	return message($message, 'warning', $return);
}
function note($message, $return = false) {
	return message($message, 'note', $return);
}
function info($message, $return = false) {
	return message($message, 'info', $return);
}
function error($message, $return = false) {
	return message($message, ((defined('MYAAC_INSTALL') || defined('MYAAC_ADMIN')) ? 'danger' : 'error'), $return);
}

function longToIp($ip): string
{
	$exp = explode(".", long2ip($ip));
	return $exp[3].".".$exp[2].".".$exp[1].".".$exp[0];
}

function generateLink($url, $name, $blank = false): string {
	return '<a href="' . $url . '"' . ($blank ? ' target="_blank"' : '') . '>' . $name . '</a>';
}

function getFullLink($page, $name, $blank = false): string {
	return generateLink(getLink($page), $name, $blank);
}

function getLink($page, $action = null): string {
	return BASE_URL . (setting('core.friendly_urls') ? '' : 'index.php/') . $page . ($action ? '/' . $action : '');
}
function internalLayoutLink($page, $action = null): string {
	return getLink($page, $action);
}

function getForumThreadLink($thread_id, $page = NULL): string {
	return BASE_URL . (setting('core.friendly_urls') ? '' : 'index.php/') . 'forum/thread/' . (int)$thread_id . (isset($page) ? '/' . $page : '');
}

function getForumBoardLink($board_id, $page = NULL): string {
	return BASE_URL . (setting('core.friendly_urls') ? '' : 'index.php/') . 'forum/board/' . (int)$board_id . (isset($page) ? '/' . $page : '');
}

function getPlayerLink($name, $generate = true, bool $colored = false): string
{
	if (is_object($name) and $name instanceof OTS_Player) {
		$player = $name;
	}
	else {
		$player = new OTS_Player();

		if(is_numeric($name)) {
			$player->load((int)$name);
		}
		else {
			$player->find($name);
		}
	}

	if (!$player->isLoaded()) {
		return '(error)';
	}

	$name = $player->getName();

	$url = BASE_URL . (setting('core.friendly_urls') ? '' : 'index.php/') . 'characters/' . urlencode($name);

	if ($colored) {
		$name = '<span style="color: ' . ($player->isOnline() ? 'green' : 'red') . ';">' . $name . '</span>';
	}

	if(!$generate) return $url;
	return generateLink($url, $name);
}

function getMonsterLink($name, $generate = true): string
{
	$url = BASE_URL . (setting('core.friendly_urls') ? '' : 'index.php/') . 'monsters?name=' . urlencode($name);

	if(!$generate) return $url;
	return generateLink($url, $name);
}

function getHouseLink($name, $generate = true): string
{
	if(is_numeric($name)) {
		$house = House::find(intval($name), ['name']);
		if ($house) {
			$name = $house->name;
		}
	}

	$url = BASE_URL . (setting('core.friendly_urls') ? '' : 'index.php/') . 'houses?name=' . urlencode($name);

	if(!$generate) return $url;
	return generateLink($url, $name);
}

function getGuildLink($name, $generate = true): string
{
	if(is_numeric($name)) {
		$guild = Guild::find(intval($name), ['name']);
		$name = $guild->name ?? 'Unknown';
	}

	$url = BASE_URL . (setting('core.friendly_urls') ? '' : 'index.php/') . 'guilds/' . urlencode($name);

	if(!$generate) return $url;
	return generateLink($url, $name);
}

function getItemNameById($id) {
	$item = Items::get($id);
	return !empty($item['name']) ? $item['name'] : '';
}

function getItemImage($id, $count = 1)
{
	$tooltip = '';

	$name = getItemNameById($id);
	if(!empty($name)) {
		$tooltip = ' class="item_image" title="' . $name . '"';
	}

	$file_name = $id;
	if($count > 1)
		$file_name .= '-' . $count;

	return '<img src="' . setting('core.item_images_url') . $file_name . setting('core.item_images_extension') . '"' . $tooltip . ' width="32" height="32" border="0" alt="' .$id . '" />';
}

function getItemRarity($chance) {
	if ($chance >= 21) {
		return "common";
	} elseif (between($chance, 8, 21)) {
		return "uncommon";
	} elseif (between($chance, 1.1, 8)) {
		return "semi rare";
	} elseif (between($chance, 0.4, 1.1)) {
		return "rare";
	} elseif (between($chance, 0.8, 0.4)) {
		return "very rare";
	} elseif ($chance <= 0.8) {
		return "extremely rare";
	}
	return '';
}

function getFlagImage($country): string
{
	if(!isset($country[0]))
		return '';

	global $config;
	if(!isset($config['countries']))
		require SYSTEM . 'countries.conf.php';

	if(!isset($config['countries'][$country])) {
		return '';
	}

	return '<img src="images/flags/' . $country . '.gif" title="' . $config['countries'][$country]. '"/>';
}

/**
 * Performs a boolean check on the value.
 *
 * @param mixed $v Variable to check.
 * @return bool Value boolean status.
 */
function getBoolean(mixed $v): bool
{
	if(is_bool($v)) {
		return $v;
	}

	if(is_numeric($v))
		return (int)$v > 0;

	if (is_null($v)) {
		return false;
	}

	$v = strtolower($v);
	return $v === 'yes' || $v === 'true';
}

/**
 * Generates random string.
 *
 * @param int $length Length of the generated string.
 * @param bool $lowCase Should lower case characters be used?
 * @param bool $upCase Should upper case characters be used?
 * @param bool $numeric Should numbers by used too?
 * @param bool $special Should special characters by used?
 * @return string Generated string.
 */
function generateRandomString($length, $lowCase = true, $upCase = false, $numeric = false, $special = false): string
{
	$characters = '';
	if($lowCase)
		$characters .= 'abcdefghijklmnopqrstuxyvwz';

	if($upCase)
		$characters .= 'ABCDEFGHIJKLMNPQRSTUXYVWZ';

	if($numeric)
		$characters .= '123456789';

	if($special)
		$characters .= '+-*#&@!?';

	$characters_length = strlen($characters) - 1;
	if($characters_length <= 0) return '';

	$ret = '';
	for($i = 0; $i < $length; $i++)
		$ret .= $characters[mt_rand(0, $characters_length)];

    return $ret;
}

/**
 * Get forum sections
 *
 * @return array Forum sections.
 */
function getForumBoards()
{
	global $db, $canEdit;
	$sections = $db->query('SELECT `id`, `name`, `description`, `closed`, `guild`, `access`' . ($canEdit ? ', `hide`, `ordering`' : '') . ' FROM `' . TABLE_PREFIX . 'forum_boards` ' . (!$canEdit ? ' WHERE `hide` != 1' : '') .
		' ORDER BY `ordering`;');
	if($sections)
		return $sections->fetchAll();

	return array();
}

// TODO:
// convert forum threads links from just forum/ID
// INTO: forum/thread-name-id, like in XenForo
//function convertForumThreadTitle($title) {
//	return str_replace(' ', '-', strtolower($title));
//}

/**
 * Retrieves data from myaac database config.
 *
 * @param string $name Key.
 * @param string &$value Reference where requested data will be set to.
 * @return bool False if value was not found in table, otherwise true.
 */
function fetchDatabaseConfig($name, &$value)
{
	$config = Config::select('value')->where('name', '=', $name)->first();
	if (!$config) {
		return false;
	}

	$value = $config->value;
	return true;
}

/**
 * Retrieves data from database config.
 *
 * $param string $name Key.
 * @return string Requested data.
 */
function getDatabaseConfig($name)
{
	$value = null;
	fetchDatabaseConfig($name, $value);
	return $value;
}

/**
 * Register a new key pair in myaac database config.
 *
 * @param string $name Key name.
 * @param string $value Data to be associated with key.
 */
function registerDatabaseConfig($name, $value)
{
	Config::create(compact('name', 'value'));
}

/**
 * Updates a value in myaac database config.
 *
 * @param string $name Key name.
 * @param string $value New data.
 */
function updateDatabaseConfig($name, $value)
{
	Config::where('name', '=', $name)->update([
		'value' => $value
	]);
}

/**
 * Encrypt text using method specified in config.lua (encryptionType or passwordType)
 */
function encrypt($str)
{
	global $config;
	if(isset($config['database_salt'])) // otserv
		$str .= $config['database_salt'];

	$encryptionType = $config['database_encryption'];
	if(isset($encryptionType) && strtolower($encryptionType) !== 'plain')
	{
		if($encryptionType === 'vahash')
			return base64_encode(hash('sha256', $str));

		return hash($encryptionType, $str);
	}

	return $str;
}

//delete player with name
function delete_player($name)
{
	// DB::beginTransaction();
	global $capsule;
	$player = Player::where(compact('name'))->first();
	if (!$player) {
		return false;
	}

	return false;
	// global $db;
	// $player = new OTS_Player();
	// $player->find($name);
	// if($player->isLoaded()) {
	// 	try { $db->exec("DELETE FROM player_skills WHERE player_id = '".$player->getId()."';"); } catch(PDOException $error) {}
	// 	try { $db->exec("DELETE FROM guild_invites WHERE player_id = '".$player->getId()."';"); } catch(PDOException $error) {}
	// 	try { $db->exec("DELETE FROM player_items WHERE player_id = '".$player->getId()."';"); } catch(PDOException $error) {}
	// 	try { $db->exec("DELETE FROM player_depotitems WHERE player_id = '".$player->getId()."';"); } catch(PDOException $error) {}
	// 	try { $db->exec("DELETE FROM player_spells WHERE player_id = '".$player->getId()."';"); } catch(PDOException $error) {}
	// 	try { $db->exec("DELETE FROM player_storage WHERE player_id = '".$player->getId()."';"); } catch(PDOException $error) {}
	// 	try { $db->exec("DELETE FROM player_viplist WHERE player_id = '".$player->getId()."';"); } catch(PDOException $error) {}
	// 	try { $db->exec("DELETE FROM player_deaths WHERE player_id = '".$player->getId()."';"); } catch(PDOException $error) {}
	// 	try { $db->exec("DELETE FROM player_deaths WHERE killed_by = '".$player->getId()."';"); } catch(PDOException $error) {}
	// 	$rank = $player->getRank();
	// 	if($rank->isLoaded()) {
	// 		$guild = $rank->getGuild();
	// 		if($guild->getOwner()->getId() == $player->getId()) {
	// 			$rank_list = $guild->getGuildRanksList();
	// 			if(count($rank_list) > 0) {
	// 				$rank_list->orderBy('level');
	// 				foreach($rank_list as $rank_in_guild) {
	// 					$players_with_rank = $rank_in_guild->getPlayersList();
	// 					$players_with_rank->orderBy('name');
	// 					$players_with_rank_number = count($players_with_rank);
	// 					if($players_with_rank_number > 0) {
	// 						foreach($players_with_rank as $player_in_guild) {
	// 							$player_in_guild->setRank();
	// 							$player_in_guild->save();
	// 						}
	// 					}
	// 					$rank_in_guild->delete();
	// 				}
	// 				$guild->delete();
	// 			}
	// 		}
	// 	}
	// 	$player->delete();
	// 	return true;
	// }

	// return false;
}

//delete guild with id
function delete_guild($id)
{
	$guild = new OTS_Guild();
	$guild->load($id);
	if(!$guild->isLoaded())
		return false;

	$rank_list = $guild->getGuildRanksList();
	if(count($rank_list) > 0) {
		$rank_list->orderBy('level');

		global $db;

		$deletedColumn = 'deleted';
		if ($db->hasColumn('players', 'deletion')) {
			$deletedColumn = 'deletion';
		}

		/**
		 * @var OTS_GuildRank $rank_in_guild
		 */
		foreach($rank_list as $rank_in_guild) {
			if($db->hasTable('guild_members'))
				$players_with_rank = $db->query('SELECT `players`.`id` as `id`, `guild_members`.`rank_id` as `rank_id` FROM `players`, `guild_members` WHERE `guild_members`.`rank_id` = ' . $rank_in_guild->getId() . ' AND `players`.`id` = `guild_members`.`player_id` AND `' . $deletedColumn . '` = 0 ORDER BY `name`;');
			else if($db->hasTable('guild_membership'))
				$players_with_rank = $db->query('SELECT `players`.`id` as `id`, `guild_membership`.`rank_id` as `rank_id` FROM `players`, `guild_membership` WHERE `guild_membership`.`rank_id` = ' . $rank_in_guild->getId() . ' AND `players`.`id` = `guild_membership`.`player_id` AND `' . $deletedColumn . '` = 0 ORDER BY `name`;');
			else
				$players_with_rank = $db->query('SELECT `id`, `rank_id` FROM `players` WHERE `rank_id` = ' . $rank_in_guild->getId() . ' AND `' . $deletedColumn . '` = 0;');

			$players_with_rank_number = $players_with_rank->rowCount();
			if($players_with_rank_number > 0) {
				foreach($players_with_rank as $result) {
					$player = new OTS_Player();
					$player->load($result['id']);
					if(!$player->isLoaded())
						continue;

					$player->setRank();
					$player->save();
				}
			}
			$rank_in_guild->delete();
		}
	}

	$guild->delete();
	return true;
}

//################### DISPLAY FUNCTIONS #####################
//return shorter text (news ticker)
function short_text($text, $limit)
{
	if(strlen($text) > $limit)
		return substr($text, 0, strrpos(substr($text, 0, $limit), " ")).'...';

	return $text;
}

function tickers()
{
	global $tickers_content, $featured_article;

	if(PAGE === 'news') {
		if(isset($tickers_content))
			return $tickers_content . $featured_article;
	}

	return '';
}

/**
 * Template place holder
 *
 * Types: head_start, head_end, body_start, body_end, center_top
 *
 */
function template_place_holder($type): string
{
	global $twig, $template_place_holders, $debugBar;
	$ret = '';

	if (isset($debugBar)) {
		$debugBarRenderer = $debugBar->getJavascriptRenderer();
	}

	if(array_key_exists($type, $template_place_holders) && is_array($template_place_holders[$type]))
		$ret = implode($template_place_holders[$type]);

	if($type === 'head_start') {
		$ret .= template_header();
		if (isset($debugBar)) {
			$ret .= $debugBarRenderer->renderHead();
		}
	}
	elseif ($type === 'head_end') {
		$ret .= setting('core.html_head');
	}
	elseif ($type === 'body_start') {
		$ret .= setting('core.html_body');

		$ret .= $twig->render('browsehappy.html.twig');

		if (admin()) {
			global $account_logged;
			$ret .= $twig->render('admin-bar.html.twig', [
				'username' => USE_ACCOUNT_NAME ? $account_logged->getName() : $account_logged->getId()
			]);
		}
	}
	elseif($type === 'body_end') {
		$ret .= setting('core.html_footer');

		$ret .= template_ga_code();
		if (isset($debugBar)) {
			$ret .= $debugBarRenderer->render();
		}
	}

	return $ret;
}

/**
 * Returns <head> content to be used by templates.
 */
function template_header($is_admin = false): string
{
	global $title_full, $twig;
	$charset = setting('core.charset') ?? 'utf-8';

	return $twig->render('templates.header.html.twig',
		[
			'charset' => $charset,
			'title' => $title_full,
			'is_admin' => $is_admin
		]
	);
}

/**
 * Returns footer content to be used by templates.
 */
function template_footer(): string
{
	$footer = [];

	if(admin()) {
		$footer[] = generateLink(ADMIN_URL, 'Admin Panel', true);
	}

	if(setting('core.visitors_counter')) {
		global $visitors;
		$amount = $visitors->getAmountVisitors();
		$footer[] = 'Currently there ' . ($amount > 1 ? 'are' : 'is') . ' ' . $amount . ' visitor' . ($amount > 1 ? 's' : '') . '.';
	}

	if(setting('core.views_counter')) {
		global $views_counter;
		$footer[] = 'Page has been viewed ' . $views_counter . ' times.';
	}

	if(setting('core.footer_load_time')) {
		$footer[] = 'Load time: ' . round(microtime(true) - START_TIME, 4) . ' seconds.';
	}

	$settingFooter = setting('core.footer');
	if(isset($settingFooter[0])) {
		$footer[] = '' . $settingFooter;
	}

	// please respect my work and help spreading the word, thanks!
	$footer[] = base64_decode('UG93ZXJlZCBieSA8YSBocmVmPSJodHRwOi8vbXktYWFjLm9yZyIgdGFyZ2V0PSJfYmxhbmsiPk15QUFDLjwvYT4=');

	global $hooks;
	$hooks->triggerFilter(HOOK_FILTER_THEME_FOOTER, $footer);

	return implode('<br/>', $footer);
}

function template_ga_code()
{
	global $twig;
	if(!isset(setting('core.google_analytics_id')[0]))
		return '';

	return $twig->render('google_analytics.html.twig');
}

function template_form()
{
	global $template_name;

	$templates = Cache::remember('templates', 5 * 60, function() {
		return get_templates();
	});

	$options = '';
	foreach($templates as $value)
		$options .= '<option ' . ($template_name == $value ? 'SELECTED' : '') . '>' . $value . '</option>';

	global $twig;
	return $twig->render('forms.change_template.html.twig', ['options' => $options]);
}

function getStyle($i)
{
	global $config;
	return is_int($i / 2) ? $config['darkborder'] : $config['lightborder'];
}

$vowels = array('e', 'y', 'u', 'i', 'o', 'a');
function getCreatureName($killer, $showStatus = false, $extendedInfo = false)
{
	global $vowels, $ots, $config;
	$str = "";
	$players_rows = '';

	if(is_numeric($killer))
	{
		$player = new OTS_Player();
		$player->load($killer);
		if($player->isLoaded())
		{
			$str .= '<a href="' . getPlayerLink($player->getName(), false) . '">';
			if(!$showStatus)
				return $str.'<b>'.$player->getName().'</b></a>';

			$str .= '<span style="color: '.($player->isOnline() ? 'green' : 'red').'">' . $player->getName() . '</span></b></a>';
			if($extendedInfo) {
				$str .= '<br><small>'.$player->getLevel().' '.$player->getVocationName().'</small>';
			}
			return $str;
		}
	}
	else
	{
		if($killer == "-1")
			$players_rows .= "item or field";
		else
		{
			if(in_array(strtolower($killer[0]), $vowels))
				$players_rows .= "an ";
			else
				$players_rows .= "a ";
			$players_rows .= $killer;
		}
	}

	return $players_rows;
}

/**
 * Find skill name using skill id.
 *
 * @param int $skillId Skill id.
 * @param bool $suffix Should suffix also be added?
 * @return string Skill name or 'unknown' if not found.
 */
function getSkillName($skillId, $suffix = true)
{
	switch($skillId)
	{
		case POT::SKILL_FIST:
		{
			$tmp = 'fist';
			if($suffix)
				$tmp .= ' fighting';

			return $tmp;
		}
		case POT::SKILL_CLUB:
		{
			$tmp = 'club';
			if($suffix)
				$tmp .= ' fighting';

			return $tmp;
		}
		case POT::SKILL_SWORD:
		{
			$tmp = 'sword';
			if($suffix)
				$tmp .= ' fighting';

			return $tmp;
		}
		case POT::SKILL_AXE:
		{
			$tmp = 'axe';
			if($suffix)
				$tmp .= ' fighting';

			return $tmp;
		}
		case POT::SKILL_DIST:
		{
			$tmp = 'distance';
			if($suffix)
				$tmp .= ' fighting';

			return $tmp;
		}
		case POT::SKILL_SHIELD:
			return 'shielding';
		case POT::SKILL_FISH:
			return 'fishing';
		case POT::SKILL__MAGLEVEL:
			return 'magic level';
		case POT::SKILL__LEVEL:
			return 'level';
		default:
			break;
	}

	return 'unknown';
}

/**
 * Performs flag check on the current logged in user.
 * Table in database: accounts, field: website_flags
 */
function hasFlag(int $flag): bool {
	global $logged, $logged_flags;
	return ($logged && ($logged_flags & $flag) == $flag);
}
/**
 * Check if current logged user have got admin flag set.
 */
function admin() {
	return hasFlag(FLAG_ADMIN) || superAdmin();
}
/**
 * Check if current logged user have got super admin flag set.
 */
function superAdmin() {
	return hasFlag(FLAG_SUPER_ADMIN);
}

/**
 * Format experience according to its amount (natural/negative number).
 *
 * @param int $exp Experience amount.
 * @param bool $color Should result be colorized?
 * @return string Resulted message attached in <span> tag.
 */
function formatExperience($exp, $color = true)
{
	$ret = '';
	if($color)
	{
		$ret .= '<span';
		if($exp != 0) {
			$ret .= ' style="color: ' . ($exp > 0 ? 'green' : 'red') . '">';
		}
		else {
			$ret .= '>';
		}
	}

	$ret .= '<b>' . ($exp > 0 ? '+' : '') . number_format($exp) . '</b>';
	if($color)
		$ret .= '</span>';

	return $ret;
}

function getExperienceForLevel($level): float|int {
	return ( 50 / 3 ) * pow( $level, 3 ) - ( 100 * pow( $level, 2 ) ) + ( ( 850 / 3 ) * $level ) - 200;
}

function get_locales()
{
	$ret = array();

	$path = LOCALE;
	foreach(scandir($path, 0) as $file)
	{
		if($file[0] != '.' && $file != '..' && is_dir($path . $file))
			$ret[] = $file;
	}

	return $ret;
}

function get_browser_languages()
{
	$ret = array();

	if(empty($_SERVER['HTTP_ACCEPT_LANGUAGE']))
		return $ret;

	$acceptLang = $_SERVER['HTTP_ACCEPT_LANGUAGE'];
	$languages = strtolower($acceptLang);
	// $languages = 'pl,en-us;q=0.7,en;q=0.3 ';
	// need to remove spaces from strings to avoid error
	$languages = str_replace(' ', '', $languages);

	foreach(explode(',', $languages) as $language_list)
		$ret[] = substr($language_list, 0, 2);

	return $ret;
}

/**
 * Generates list of templates, according to templates/ dir.
 */
function get_templates()
{
	$ret = array();

	$path = TEMPLATES;
	foreach(scandir($path, 0) as $file)
	{
		if($file[0] !== '.' && $file !== '..' && is_dir($path . $file))
			$ret[] = $file;
	}

	foreach (Plugins::getThemes() as $name => $path) {
		$ret[] = $name;
	}

	return $ret;
}

/**
 * Generates list of installed plugins
 * @return array $plugins
 */
function get_plugins($disabled = false): array
{
	$ret = [];

	$path = PLUGINS;
	foreach(scandir($path, SCANDIR_SORT_ASCENDING) as $file) {
		$file_ext = pathinfo($file, PATHINFO_EXTENSION);
		$file_name = pathinfo($file, PATHINFO_FILENAME);
		if ($file === '.' || $file === '..' || $file === 'example.json' || $file_ext !== 'json' || is_dir($path . $file)) {
			continue;
		}

		if (!$disabled && strpos($file, 'disabled.') !== false) {
			continue;
		}

		$ret[] = str_replace('.json', '', $file_name);
	}

	return $ret;
}
function getWorldName($id)
{
	global $config;
	if(isset($config['worlds'][$id]))
		return $config['worlds'][$id];

	return $config['lua']['serverName'];
}

/**
 * Mailing users.
 * Mailing has to be enabled in settings (in Admin Panel).
 *
 * @param string $to Recipient email address.
 * @param string $subject Subject of the message.
 * @param string $body Message body in HTML format.
 * @param string $altBody Alternative message body, plain text.
 * @return bool PHPMailer status returned (success/failure).
 * @throws \PHPMailer\PHPMailer\Exception
 */
function _mail(string $to, string $subject, string $body, string $altBody = ''): bool
{
	global $mailer, $config;

	if (!setting('core.mail_enabled')) {
		log_append('mailer-error.log', '_mail() function has been used, but Mail Support is disabled.');
		return false;
	}

	if(!$mailer)
	{
		$mailer = new PHPMailer();
		//$mailer->setLanguage('en', LIBS . 'phpmailer/language/');
	}
	else {
		$mailer->clearAllRecipients();
	}

	$mailOption = setting('core.mail_option');
	if($mailOption == MAIL_SMTP)
	{
		$mailer->isSMTP();
		$mailer->Host = setting('core.smtp_host');
		$mailer->Port = setting('core.smtp_port');
		$mailer->SMTPAuth = setting('core.smtp_auth');
		$mailer->Username = setting('core.smtp_user');
		$mailer->Password = setting('core.smtp_pass');

		$security = setting('core.smtp_security');

		$tmp = '';
		if ($security === SMTP_SECURITY_SSL) {
			$tmp = 'ssl';
		}
		else if ($security == SMTP_SECURITY_TLS) {
			$tmp = 'tls';
		}

		$mailer->SMTPSecure = $tmp;
	}
	else {
		$mailer->isMail();
	}

	$signature_html = setting('core.mail_signature_html');
	$tmp_body = $body . '<br/><br/>' . $signature_html;

	$mailer->isHTML(isset($body[0]) > 0);
	$mailer->From = setting('core.mail_address');
	$mailer->Sender = setting('core.mail_address');
	$mailer->CharSet = 'utf-8';
	$mailer->FromName = $config['lua']['serverName'];
	$mailer->Subject = $subject;
	$mailer->addAddress($to);
	$mailer->Body = $tmp_body;

	if(setting('core.smtp_debug')) {
		$mailer->SMTPDebug = 2;
		$mailer->Debugoutput = 'echo';
	}

	$signature_plain = setting('core.mail_signature_plain');
	if(isset($altBody[0])) {
		$mailer->AltBody = $altBody . $signature_plain;
	}
	else { // automatically generate plain html
		$mailer->AltBody = strip_tags(preg_replace('/<a(.*)href="([^"]*)"(.*)>/','$2', $body)) . "\n" . $signature_plain;
	}

	ob_start();
	if(!$mailer->Send()) {
		log_append('mailer-error.log', PHP_EOL . $mailer->ErrorInfo . PHP_EOL . ob_get_clean());
		return false;
	}

	ob_end_clean();
	return true;
}

function convert_bytes($size)
{
	$unit = array('b', 'kb', 'mb', 'gb', 'tb', 'pb');
	return @round($size / pow(1024, ($i = floor(log($size, 1024)))), 2) . ' ' . $unit[$i];
}

function log_append($file, $str, array $params = [])
{
	if(count($params) > 0) {
		$str .= print_r($params, true);
	}

	$f = fopen(LOGS . $file, 'ab');
	fwrite($f, '[' . date(DateTime::RFC1123) . '] ' . $str . PHP_EOL);
	fclose($f);
}

function load_config_lua($filename)
{
	global $config;

	$config_file = $filename;
	if(!@file_exists($config_file))
	{
		log_append('error.log', '[load_config_file] Fatal error: Cannot load config.lua (' . $filename . ').');
		throw new RuntimeException('ERROR: Cannot find ' . $filename . ' file.');
	}

	$result = array();
	$config_string = str_replace(array("\r\n", "\r"), "\n", file_get_contents($filename));
	$lines = explode("\n", $config_string);
	if(count($lines) > 0) {
		foreach($lines as $ln => $line)
		{
			$line = trim($line);
			if(isset($line[0]) && ($line[0] === '{' || $line[0] === '}')) {
				// arrays are not supported yet
				// just ignore the error
				continue;
			}

			$tmp_exp = explode('=', $line, 2);
			if(str_contains($line, 'dofile')) {
				$delimiter = '"';
				if(!str_contains($line, $delimiter)) {
					$delimiter = "'";
				}

				$tmp = explode($delimiter, $line);
				$result = array_merge($result, load_config_lua($config['server_path'] . $tmp[1]));
			}
			else if(count($tmp_exp) >= 2) {
				$key = trim($tmp_exp[0]);
				if(!str_starts_with($key, '--')) {
					$value = trim($tmp_exp[1]);
					if(str_contains($value, '--')) {// found some deep comment
						$value = preg_replace('/--.*$/i', '', $value);
					}

					if(is_numeric($value))
						$result[$key] = (float) $value;
					elseif(in_array(@$value[0], array("'", '"')) && in_array(@$value[strlen($value) - 1], array("'", '"')))
						$result[$key] = substr(substr($value, 1), 0, -1);
					elseif(in_array($value, array('true', 'false')))
						$result[$key] = $value === 'true';
					elseif(@$value[0] === '{') {
						// arrays are not supported yet
						// just ignore the error
						continue;
					}
					else
					{
						foreach($result as $tmp_key => $tmp_value) { // load values defined by other keys, like: dailyFragsToBlackSkull = dailyFragsToRedSkull
							$value = str_replace($tmp_key, $tmp_value, $value);
						}

						try {
							$ret = eval("return $value;");
						}
						catch (Throwable $e) {
							throw new RuntimeException('ERROR: Loading config.lua file. Line: ' . ($ln + 1) . ' - Unable to parse value "' . $value . '" - ' . $e->getMessage());
						}

						if((string) $ret == '' && trim($value) !== '""') {
							throw new RuntimeException('ERROR: Loading config.lua file. Line ' . ($ln + 1) . ' is not valid [key: ' . $key . ']');
						}
						$result[$key] = $ret;
					}
				}
			}
		}
	}

	return array_merge($result, $config['lua'] ?? []);
}

function str_replace_first($search,$replace, $subject) {
	$pos = strpos($subject, $search);
	if ($pos !== false) {
		return substr_replace($subject, $replace, $pos, strlen($search));
	}

	return $subject;
}

function get_browser_real_ip() {
	if (isset($_SERVER['HTTP_CF_CONNECTING_IP'])) {
		$_SERVER['REMOTE_ADDR'] = $_SERVER['HTTP_CF_CONNECTING_IP'];
	}

	if(isset($_SERVER['REMOTE_ADDR']) && !empty($_SERVER['REMOTE_ADDR']))
		return $_SERVER['REMOTE_ADDR'];
	else if(isset($_SERVER['HTTP_CLIENT_IP']) && !empty($_SERVER['HTTP_CLIENT_IP']))
		return $_SERVER['HTTP_CLIENT_IP'];
	else if(isset($_SERVER['HTTP_X_FORWARDED_FOR']) && !empty($_SERVER['HTTP_X_FORWARDED_FOR']))
		return $_SERVER['HTTP_X_FORWARDED_FOR'];

	return '0';
}
function setSession($key, $value = null): void {
	if (!is_array($key)) {
		$key = [$key => $value];
	}

	foreach ($key as $arrayKey => $arrayValue) {
		if (is_null($arrayValue)) {
			unsetSession($arrayKey);
		}
		else {
			$_SESSION[setting('core.session_prefix') . $arrayKey] = $arrayValue;
		}
	}
}
function getSession($key) {
	return $_SESSION[setting('core.session_prefix') . $key] ?? null;
}
function unsetSession($key): void {
	unset($_SESSION[setting('core.session_prefix') . $key]);
}

function session($key): mixed {
	if (is_array($key)) {
		setSession($key);
		return null;
	}

	return getSession($key);
}

function csrf(bool $return = false): string {
	return CsrfToken::create($return);
}

function csrfToken(): string {
	return CsrfToken::get();
}

function isValidToken(): bool {
	$token = $_POST['csrf_token'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? null;
	return (!isRequestMethod('post') || (isset($token) && CsrfToken::isValid($token)));
}

function csrfProtect(): void
{
	if (!isValidToken()) {
		$lastUri = BASE_URL . str_replace_first('/', '', getSession('last_uri'));
		echo 'Request has been cancelled due to security reasons - token is invalid. Go <a href="' . $lastUri . '">back</a>';
		exit();
	}
}

function getSkillIdByName(string $name): int|null
{
	$skills = [
		'level' => POT::SKILL_LEVEL,
		'experience' => POT::SKILL_LEVEL,

		'magic' => POT::SKILL_MAGIC,
		'maglevel' => POT::SKILL_MAGIC,

		'balance' => SKILL_BALANCE,
		'frags' => SKILL_FRAGS,

		'club' => POT::SKILL_CLUB,
		'sword' => POT::SKILL_SWORD,
		'axe' => POT::SKILL_AXE,
		'dist' => POT::SKILL_DIST,
		'distance' => POT::SKILL_DIST,
		'shield' => POT::SKILL_SHIELD,
		'shielding' => POT::SKILL_SHIELD,
		'fish' => POT::SKILL_FISH,
		'fishing' => POT::SKILL_FISH,
	];

	return $skills[$name] ?? null;
}

function getTopPlayers($limit = 5, $skill = POT::SKILL_LEVEL)
{
	global $db;

	$skillOriginal = $skill;

	if (is_string($skill)) {
		$skill = getSkillIdByName($skill);
	}

	if (!is_numeric($skill)) {
		throw new RuntimeException("getTopPlayers: Invalid skill: $skillOriginal");
	}

	return Cache::remember("top_{$limit}_{$skill}", 2 * 60, function () use ($db, $limit, $skill) {
		$columns = [
			'id', 'name', 'level', 'vocation', 'experience', 'balance',
			'looktype', 'lookhead', 'lookbody', 'looklegs', 'lookfeet'
		];

		if ($db->hasColumn('players', 'promotion')) {
			$columns[] = 'promotion';
		}

		if ($db->hasColumn('players', 'lookaddons')) {
			$columns[] = 'lookaddons';
		}

		if ($db->hasColumn('players', 'lookmount')) {
			$columns[] = 'lookmount';
		}

		$query = Player::query()
			->select($columns)
			->withOnlineStatus()
			->notDeleted()
			->where('group_id', '<', setting('core.highscores_groups_hidden'))
			->whereNotIn('id', setting('core.highscores_ids_hidden'))
			->where('account_id', '!=', 1)
			->orderByDesc('value');

		if ($limit > 0) {
			$query->limit($limit);
		}

		if ($skill >= POT::SKILL_FIRST && $skill <= POT::SKILL_LAST) { // skills
			if ($db->hasColumn('players', 'skill_fist')) {// tfs 1.0
				$skill_ids = array(
					POT::SKILL_FIST => 'skill_fist',
					POT::SKILL_CLUB => 'skill_club',
					POT::SKILL_SWORD => 'skill_sword',
					POT::SKILL_AXE => 'skill_axe',
					POT::SKILL_DIST => 'skill_dist',
					POT::SKILL_SHIELD => 'skill_shielding',
					POT::SKILL_FISH => 'skill_fishing',
				);

				$query
					->addSelect($skill_ids[$skill] . ' as value')
					->orderByDesc($skill_ids[$skill] . '_tries');
			} else {
				$query
					->join('player_skills', 'player_skills.player_id', '=', 'players.id')
					->where('skillid', $skill)
					->addSelect('player_skills.value as value');
			}
		} else if ($skill == SKILL_FRAGS) // frags
		{
			if ($db->hasTable('player_killers')) {
				$query->addSelect(['value' => PlayerKillers::whereColumn('player_killers.player_id', 'players.id')->selectRaw('COUNT(*)')]);
			} else {
				$query->addSelect(['value' => PlayerDeath::unjustified()->whereColumn('player_deaths.killed_by', 'players.name')->selectRaw('COUNT(*)')]);
			}
		} else if ($skill == SKILL_BALANCE) // balance
		{
			$query
				->addSelect('players.balance as value');
		} else {
			if ($skill == POT::SKILL_MAGIC) {
				$query
					->addSelect('players.maglevel as value', 'players.maglevel')
					->orderByDesc('manaspent');
			} else { // level
				$query
					->addSelect('players.level as value', 'players.experience')
					->orderByDesc('experience');
			}
		}

		return $query
			->get()
			->map(function ($e, $i) {
				$row = $e->toArray();
				$row['online'] = $e->online_status;
				$row['rank'] = $i + 1;
				$row['outfit_url'] = $e->outfit_url;

				unset($row['online_table']);

				return $row;
			})->toArray();
	});
}

function deleteDirectory($dir, $ignore = array(), $contentOnly = false): bool
{
	if(!file_exists($dir)) {
		return true;
	}

	if(!is_dir($dir)) {
		return unlink($dir);
	}

	foreach(scandir($dir, 0) as $item) {
		if($item === '.' || $item === '..' || in_array($item, $ignore, true)) {
			continue;
		}

		if(!in_array($item, $ignore, true) && !deleteDirectory($dir . DIRECTORY_SEPARATOR . $item)) {
			return false;
		}
	}

	if($contentOnly) {
		return true;
	}

	return rmdir($dir);
}

function ensureFolderExists($dir): void
{
	if (!file_exists($dir)) {
		mkdir($dir, 0777, true);
	}
}

function ensureIndexExists($dir): void
{
	$dir = rtrim($dir, '/');
	if (!file_exists($file = $dir . '/index.html')) {
		touch($file);
	}
}

function config($key) {
	global $config;
	if (is_array($key)) {
		if (is_null($key[1])) {
			unset($config[$key[0]]);
		}
		return $config[$key[0]] = $key[1];
	}

	return @$config[$key];
}

function configLua($key) {
	global $config;
	if (is_array($key)) {
		return $config['lua'][$key[0]] = $key[1];
	}

	return @$config['lua'][$key];
}

function setting($key)
{
	$settings = Settings::getInstance();

	if (is_array($key)) {
		if (is_null($key[1])) {
			unset($settings[$key[0]]);
		}

		return $settings[$key[0]] = $key[1];
	}

	$ret = $settings[$key];
	return isset($ret) ? $ret['value'] : null;
}

function clearCache()
{
	News::clearCache();

	$cache = Cache::getInstance();
	if($cache->enabled()) {
		$keysToClear = [
			'status', 'templates',
			'config_lua',
			'towns', 'groups', 'vocations',
			'visitors', 'views_counter', 'failed_logins',
			'template_menus',
			'last_kills',
			'hooks', 'plugins_hooks', 'plugins_routes', 'plugins_settings', 'plugins_themes', 'plugins_commands',
			'settings',
		];

		foreach (get_templates() as $template) {
			$keysToClear[] = 'template_ini_' . $template;
		}

		// highscores cache
		$configHighscoresPerPage = setting('core.highscores_per_page');
		$skills = [POT::SKILL_FIST, POT::SKILL_CLUB, POT::SKILL_SWORD, POT::SKILL_AXE, POT::SKILL_DIST, POT::SKILL_SHIELD, POT::SKILL_FISH, POT::SKILL_LEVEL, POT::SKILL__MAGLEVEL, SKILL_FRAGS, SKILL_BALANCE];
		foreach ($skills as $skill) {
			// config('vocations') may be empty after previous cache clear
			$vocations = (config('vocations') ?? []) + ['all'];
			foreach ($vocations as $vocation) {
				for($page = 0; $page < 10; $page++) {
					$cacheKey = 'highscores_' . $skill . '_' . strtolower($vocation) . '_' . $page . '_' . $configHighscoresPerPage;
					$keysToClear[] = $cacheKey;
				}
			}
		}

		foreach ($keysToClear as $item) {
			$tmp = '';
			if ($cache->fetch($item, $tmp)) {
				$cache->delete($item);
			}
		}

		global $db;
		$db->setClearCacheAfter(true);
	}

	if (function_exists('apcu_clear_cache')) {
		apcu_clear_cache();
	}

	deleteDirectory(CACHE . 'signatures', ['index.html'], true);
	deleteDirectory(CACHE . 'twig', ['index.html'], true);
	deleteDirectory(CACHE . 'plugins', ['index.html'], true);
	deleteDirectory(CACHE, ['signatures', 'twig', 'plugins', 'index.html', 'persistent'], true);

	global $hooks;
	$hooks->trigger(HOOK_CACHE_CLEAR, ['cache' => Cache::getInstance()]);

	return true;
}

function clearRouteCache(): void
{
	$routeCacheFile = CACHE . 'route.cache';
	if (file_exists($routeCacheFile)) {
		unlink($routeCacheFile);
	}
}

function getCustomPageInfo($name)
{
	global $logged_access;
	$page = Pages::isPublic()
		->where('name', 'LIKE', $name)
		->where('access', '<=', $logged_access)
		->first();

	if (!$page) {
		return null;
	}

	return $page->toArray();
}
function getCustomPage($name, &$success): string
{
	global $twig, $title, $ignore;

	$success = false;
	$content = '';
	$page = getCustomPageInfo($name);

	if($page) // found page
	{
		$success = $ignore = true;
		$title = $page['title'];

		if($page['php'] == '1') // execute it as php code
		{
			$tmp = substr($page['body'], 0, 10);
			if(($pos = strpos($tmp, '<?php')) !== false) {
				$tmp = preg_replace('/<\?php/', '', $page['body'], 1);
			}
			else if(($pos = strpos($tmp, '<?')) !== false) {
				$tmp = preg_replace('/<\?/', '', $page['body'], 1);
			}
			else
				$tmp = $page['body'];

			global $config;
			if(setting('core.backward_support')) {
				global $SQL, $main_content, $subtopic;
			}

			ob_start();
			eval($tmp);
			$content .= ob_get_contents();
			ob_end_clean();
		}
		else {
			$content .= $twig->renderInline($page['body']);
		}
	}

	return $content;
}

function getBanReason($reasonId)
{
	switch($reasonId)
	{
		case 0:
			return "Offensive Name";
		case 1:
			return "Invalid Name Format";
		case 2:
			return "Unsuitable Name";
		case 3:
			return "Name Inciting Rule Violation";
		case 4:
			return "Offensive Statement";
		case 5:
			return "Spamming";
		case 6:
			return "Illegal Advertising";
		case 7:
			return "Off-Topic Public Statement";
		case 8:
			return "Non-English Public Statement";
		case 9:
			return "Inciting Rule Violation";
		case 10:
			return "Bug Abuse";
		case 11:
			return "Game Weakness Abuse";
		case 12:
			return "Using Unofficial Software to Play";
		case 13:
			return "Hacking";
		case 14:
			return "Multi-Clienting";
		case 15:
			return "Account Trading or Sharing";
		case 16:
			return "Threatening Gamemaster";
		case 17:
			return "Pretending to Have Influence on Rule Enforcement";
		case 18:
			return "False Report to Gamemaster";
		case 19:
			return "Destructive Behaviour";
		case 20:
			return "Excessive Unjustified Player Killing";
		case 21:
			return "Invalid Payment";
		case 22:
			return "Spoiling Auction";
	}

	return "Unknown Reason";
}

function getBanType($typeId)
{
	switch($typeId)
	{
		case 1:
			return "IP Banishment";
		case 2:
			return "Namelock";
		case 3:
			return "Banishment";
		case 4:
			return "Notation";
		case 5:
			return "Deletion";
	}

	return "Unknown Type";
}

function getChangelogType($v)
{
	switch($v) {
		case 1:
			return 'added';
		case 2:
			return 'removed';
		case 3:
			return 'changed';
		case 4:
			return 'fixed';
	}

	return 'unknown';
}

function getChangelogWhere($v)
{
	switch($v) {
		case 1:
			return 'server';
		case 2:
			return 'website';
	}

	return 'unknown';
}

function getPlayerNameByAccountId($id)
{
	if (!is_numeric($id)) {
		return '';
	}

	$account = \MyAAC\Models\Account::find(intval($id), ['id']);
	if ($account) {
		$player = \MyAAC\Models\Player::where('account_id', $account->id)->orderByDesc('lastlogin')->select('name')->first();
		if (!$player) {
			return '';
		}
		return $player->name;
	}

	return '';
}

function getPlayerNameByAccount($account) {
	if (is_numeric($account)) {
		return getPlayerNameByAccountId($account);
	}

	return '';
}

function getPlayerNameById($id)
{
	if (!is_numeric($id)) {
		return '';
	}

	$player = \MyAAC\Models\Player::find((int)$id, ['name']);
	if ($player) {
		return $player->name;
	}

	return '';
}

function echo_success($message)
{
	echo '<div class="col-12 alert alert-success mb-2">' . $message . '</div>';
}

function echo_error($message)
{
	global $error;
	echo '<div class="col-12 alert alert-danger mb-2">' . $message . '</div>';
	$error = true;
}

function verify_number($number, $name, $max_length)
{
	if (!Validator::number($number))
		echo_error($name . ' can contain only numbers.');

	$number_length = strlen($number);
	if ($number_length <= 0 || $number_length > $max_length)
		echo_error($name . ' cannot be longer than ' . $max_length . ' digits.');
}

function Outfits_loadfromXML()
{
	global $config;
	$file_path = $config['data_path'] . 'XML/outfits.xml';
	if (!file_exists($file_path)) {	return null; }

	$xml = new DOMDocument;
	$xml->load($file_path);

	$outfits = null;
	foreach ($xml->getElementsByTagName('outfit') as $outfit) {
		$outfits[] = Outfit_parseNode($outfit);
	}
	return $outfits;
}

 function Outfit_parseNode($node) {
	$looktype = (int)$node->getAttribute('looktype');
	$type = (int)$node->getAttribute('type');
	$lookname = $node->getAttribute('name');
	$premium = $node->getAttribute('premium');
	$unlocked = $node->getAttribute('unlocked');
	$enabled = $node->getAttribute('enabled');
	return array('id' => $looktype, 'type' => $type, 'name' => $lookname, 'premium' => $premium, 'unlocked' => $unlocked, 'enabled' => $enabled);
}

function Mounts_loadfromXML()
{
	global $config;
	$file_path = $config['data_path'] . 'XML/mounts.xml';
	if (!file_exists($file_path)) {	return null; }

	$xml = new DOMDocument;
	$xml->load($file_path);

	$mounts = null;
	foreach ($xml->getElementsByTagName('mount') as $mount) {
		$mounts[] = Mount_parseNode($mount);
	}
	return $mounts;
}

 function Mount_parseNode($node) {
	$id = (int)$node->getAttribute('id');
	$clientid = (int)$node->getAttribute('clientid');
	$name = $node->getAttribute('name');
	$speed = (int)$node->getAttribute('speed');
	$premium = $node->getAttribute('premium');
	$type = $node->getAttribute('type');
	return array('id' => $id, 'clientid' => $clientid, 'name' => $name, 'speed' => $speed, 'premium' => $premium, 'type' => $type);
}

function left($str, $length) {
	return substr($str, 0, $length);
}

function right($str, $length) {
	return substr($str, -$length);
}

function between($x, $lim1, $lim2) {
	if ($lim1 < $lim2) {
		$lower = $lim1; $upper = $lim2;
	}
	else {
		$lower = $lim2; $upper = $lim1;
	}
	return (($x >= $lower) && ($x <= $upper));
}

function truncate($string, $length)
{
	if (strlen($string) > $length) {
		$string = substr($string, 0, $length) . '...';
	}
	return $string;
}

function getAccountLoginByLabel()
{
	$ret = '';
	if (config('account_login_by_email')) {
		$ret = 'Email Address';
		if (config('account_login_by_email_fallback')) {
			$ret .= ' or ';
		}
	}

	if (!config('account_login_by_email') || config('account_login_by_email_fallback')) {
		$ret .= 'Account ' . (USE_ACCOUNT_NAME ? 'Name' : 'Number');
	}

	return $ret;
}

function camelCaseToUnderscore($input)
{
	return ltrim(strtolower(preg_replace('/[A-Z]([A-Z](?![a-z]))*/', '_$0', $input)), '_');
}

function removeIfFirstSlash(&$text): void
{
	if(strpos($text, '/') === 0) {
		$text = str_replace_first('/', '', $text);
	}
};

function escapeHtml($html): string {
	return htmlspecialchars($html);
}

function getGuildNameById($id)
{
	$guild = Guild::where('id', intval($id))->select('name')->first();
	if ($guild) {
		return $guild->name;
	}

	return false;
}

function getGuildLogoById($id): string
{
	$logo = 'default.gif';

	$guild = Guild::where('id', intval($id))->select('logo_name')->first();
	if ($guild) {
		$guildLogo = $guild->logo_name;

		if (!empty($guildLogo) && file_exists(GUILD_IMAGES_DIR . $guildLogo)) {
			$logo = $guildLogo;
		}
	}

	return BASE_URL . GUILD_IMAGES_DIR . $logo;
}

function displayErrorBoxWithBackButton($errors, $action = null): void
{
	global $twig;
	$twig->display('error_box.html.twig', ['errors' => $errors]);
	$twig->display('account.back_button.html.twig', [
		'action' => $action ?: getLink('')
	]);
}

function makeLinksClickable($text, $blank = true) {
	return preg_replace('!(((f|ht)tp(s)?://)[-a-zA-Zа-яА-Я()0-9@:%_+.~#?&;//=]+)!i', '<a href="$1"' . (!$blank ?: ' target="_blank"') . '>$1</a>', $text);
}

function isRequestMethod(string $method): bool {
	return strtolower($_SERVER['REQUEST_METHOD']) == strtolower($method);
}

function getAccountIdentityColumn(): string
{
	if (USE_ACCOUNT_NAME) {
		return 'name';
	}
	elseif (USE_ACCOUNT_NUMBER) {
		return 'number';
	}

	return 'id';
}

function isCanary(): bool
{
	$dataPackDirectory = configLua('dataPackDirectory');
	return isset($dataPackDirectory);
}

function getStatusUptimeReadable(int $uptime): string
{
	$fullMinute = 60;
	$fullHour = (60 * $fullMinute);
	$fullDay = (24 * $fullHour);
	$fullMonth = (30 * $fullDay);
	$fullYear = (365 * $fullDay);

	// years
	$years = floor($uptime / $fullYear);
	$y = ($years > 1 ? "$years years, " : ($years == 1 ? 'year, ' : ''));

	$uptime -= $years * $fullYear;

	// months
	$months = floor($uptime / $fullMonth);
	$m = ($months > 1 ? "$months months, " : ($months == 1 ? 'month, ' : ''));

	$uptime -= $months * $fullMonth;

	// days
	$days = floor($uptime / $fullDay);
	$d = ($days > 1 ? "$days days, " : ($days == 1 ? 'day, ' : ''));

	$uptime -= $days * $fullDay;

	// hours
	$hours = floor($uptime / $fullHour);

	$uptime -= $hours * $fullHour;

	// minutes
	$min = floor($uptime / $fullMinute);

	return "{$y}{$m}{$d}{$hours}h {$min}m";
}

// validator functions
require_once SYSTEM . 'compat/base.php';

// custom functions
$customFunctions = SYSTEM . 'functions_custom.php';
if (is_file($customFunctions)) {
	require $customFunctions;
}
