<?php
/**
 * @package   admintools
 * @copyright Copyright (c)2010-2026 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 3, or later
 */

namespace Akeeba\Plugin\Actionlog\AdminTools\Extension;

defined('_JEXEC') or die;

use Akeeba\Component\AdminTools\Administrator\Controller\DatabasetoolsController;
use Akeeba\Component\AdminTools\Administrator\Model\DatabasetoolsModel;
use Akeeba\Component\AdminTools\Administrator\Model\ScanalertsModel;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\MVC\Controller\BaseController;
use Joomla\CMS\User\User;
use Joomla\Component\Actionlogs\Administrator\Plugin\ActionLogPlugin;
use Joomla\Event\Event;
use Joomla\Event\SubscriberInterface;
use ReflectionMethod;

class AdminTools extends ActionLogPlugin implements SubscriberInterface
{
	/**
	 * Load the language file on instantiation.
	 *
	 * @var    boolean
	 * @since  3.1
	 */
	protected $autoloadLanguage = true;

	private $defaultExtension = 'com_admintools';

	/**
	 * Returns an array of events this subscriber will listen to.
	 *
	 * @return  array
	 *
	 * @since   9.0.0
	 */
	public static function getSubscribedEvents(): array
	{
		// Only subscribe events if the component is installed and enabled
		if (!ComponentHelper::isEnabled('com_akeebabackup'))
		{
			return [];
		}

		// Register all public onSomething methods as event handlers
		$events   = [];
		$refClass = new \ReflectionClass(__CLASS__);
		$methods  = $refClass->getMethods(ReflectionMethod::IS_PUBLIC);

		foreach ($methods as $method)
		{
			$name = $method->getName();

			if (substr($name, 0, 2) != 'on')
			{
				continue;
			}

			$events[$name] = $name;
		}

		return $events;
	}

	public function onComAdmintoolsAdminallowlistControllerAfterApply($event)
	{
		$this->logCRUDSave($event, 'id', 'ip', 'COM_ADMINTOOLS_LOGS_WHITELISTEDADDRESSES_EDIT_2');
	}

	public function onComAdmintoolsAdminallowlistControllerAfterSave($event)
	{
		$this->logCRUDSave($event, 'id', 'ip', 'COM_ADMINTOOLS_LOGS_WHITELISTEDADDRESSES_EDIT_2');
	}

	public function onComAdmintoolsAdminallowlistControllerAfterSave2new($event)
	{
		$this->logCRUDSave($event, 'id', 'ip', 'COM_ADMINTOOLS_LOGS_WHITELISTEDADDRESSES_EDIT_2');
	}

	public function onComAdmintoolsAdminallowlistsControllerBeforeDelete($event)
	{
		$this->logCRUDAction($event, 'ip', 'COM_ADMINTOOLS_LOGS_WHITELISTEDADDRESSES_DELETE');
	}

	public function onComAdmintoolsAdminpasswordControllerBeforeProtect($event)
	{
		$this->logUserAction('', 'COM_ADMINTOOLS_LOGS_ADMINPASSWORD_ENABLE', 'com_admintools');
	}

	public function onComAdmintoolsAdminpasswordControllerBeforeUnprotect($event)
	{
		$this->logUserAction('', 'COM_ADMINTOOLS_LOGS_ADMINPASSWORD_DISABLE', 'com_admintools');
	}

	public function onComAdmintoolsAutobannedaddressesControllerBeforeDelete($event)
	{
		$this->logCRUDAction($event, 'ip', 'COM_ADMINTOOLS_LOGS_AUTOBANNEDADDRESSES_DELETE');
	}

	public function onComAdmintoolsBadwordControllerAfterApply($event)
	{
		$this->logCRUDSave($event, 'id', 'word', 'COM_ADMINTOOLS_LOGS_BADWORDS_EDIT_2');
	}

	public function onComAdmintoolsBadwordControllerAfterSave($event)
	{
		$this->logCRUDSave($event, 'id', 'word', 'COM_ADMINTOOLS_LOGS_BADWORDS_EDIT_2');
	}

	public function onComAdmintoolsBadwordControllerAfterSave2new($event)
	{
		$this->logCRUDSave($event, 'id', 'word', 'COM_ADMINTOOLS_LOGS_BADWORDS_EDIT_2');
	}

	public function onComAdmintoolsBadwordsControllerBeforeDelete($event)
	{
		$this->logCRUDAction($event, 'word', 'COM_ADMINTOOLS_LOGS_BADWORDS_DELETE');
	}

	public function onComAdmintoolsBlockedrequestslogControllerBeforeBan($event)
	{
		$this->logCRUDSave($event, 'id', 'ip', 'COM_ADMINTOOLS_LOGS_SECURITYEXCEPTIONS_BAN_2');
	}

	public function onComAdmintoolsBlockedrequestslogControllerBeforeDelete($event)
	{
		$this->logCRUDAction($event, 'ip', 'COM_ADMINTOOLS_LOGS_SECURITYEXCEPTIONS_DELETE');
	}

	public function onComAdmintoolsBlockedrequestslogControllerBeforeUnban($event)
	{
		$this->logCRUDSave($event, 'id', 'ip', 'COM_ADMINTOOLS_LOGS_SECURITYEXCEPTIONS_UNBAN_2');
	}

	public function onComAdmintoolsChecktempandlogdirectoriesControllerBeforeCheck($event)
	{
		$this->logUserAction('', 'COM_ADMINTOOLS_LOGS_CHECKTEMPANDLOGDIRECTORIES_RUN', 'com_admintools');
	}

	public function onComAdmintoolsCleantempdirectoryControllerBeforeMain($event)
	{
		$this->logUserAction('', 'COM_ADMINTOOLS_LOGS_CLEANTEMPDIRECTORY_RUN', 'com_admintools');
	}

	public function onComAdmintoolsConfigurepermissionsControllerAfterSavedefaults($event)
	{
		$this->logUserAction('', 'COM_ADMINTOOLS_LOGS_CONFIGUREFIXPERMISSIONS_DEFAULTS', 'com_admintools');
	}

	public function onComAdmintoolsConfigurepermissionsControllerBeforeSaveapplyperms($event)
	{
		$this->logUserAction('', 'COM_ADMINTOOLS_LOGS_CONFIGUREFIXPERMISSIONS_SAVEAPPLYPERMS', 'com_admintools');
	}

	public function onComAdmintoolsConfigurepermissionsControllerBeforeSaveperms($event)
	{
		$this->logUserAction('', 'COM_ADMINTOOLS_LOGS_CONFIGUREFIXPERMISSIONS_SAVEPERMS', 'com_admintools');
	}

	public function onComAdmintoolsConfigurewafControllerAfterApply($event)
	{
		$this->logUserAction('', 'COM_ADMINTOOLS_LOGS_CONFIGUREWAF_EDIT', 'com_admintools');
	}

	public function onComAdmintoolsConfigurewafControllerAfterSave($event)
	{
		$this->logUserAction('', 'COM_ADMINTOOLS_LOGS_CONFIGUREWAF_EDIT', 'com_admintools');
	}

	/**
	 * @param   DatabasetoolsController  $controller
	 */
	public function onComAdmintoolsDatabasetoolsControllerAfterOptimize($event)
	{
		$arguments = array_values($event->getArguments());
		/** @var BaseController $controller */
		$controller = $arguments[0];
		/** @var DatabasetoolsModel $model */
		$model   = $controller->getModel();
		$percent = $model->getState('percent', 0);

		if ($percent >= 100)
		{
			$this->logUserAction('', 'COM_ADMINTOOLS_LOGS_DATABASETOOLS_REPAIR', 'com_admintools');
		}
	}

	public function onComAdmintoolsDatabasetoolsControllerBeforePurgesessions($event)
	{
		$this->logUserAction('', 'COM_ADMINTOOLS_LOGS_DATABASETOOLS_PURGESESSIONS', 'com_admintools');
	}

	public function onComAdmintoolsEmergencyofflineControllerBeforeOffline($event)
	{
		$this->logUserAction('', 'COM_ADMINTOOLS_LOGS_EMERGENCYOFFLINE_ENABLE', 'com_admintools');
	}

	public function onComAdmintoolsEmergencyofflineControllerBeforeOnline($event)
	{
		$this->logUserAction('', 'COM_ADMINTOOLS_LOGS_EMERGENCYOFFLINE_DISABLE', 'com_admintools');
	}

	public function onComAdmintoolsExportimportControllerBeforeDoexport($event)
	{
		$this->logUserAction('', 'COM_ADMINTOOLS_LOGS_IMPORANDEXPORT_EXPORT', 'com_admintools');
	}

	public function onComAdmintoolsExportimportControllerBeforeDoimport($event)
	{
		$this->logUserAction('', 'COM_ADMINTOOLS_LOGS_IMPORANDEXPORT_IMPORT', 'com_admintools');
	}

	public function onComAdmintoolsFixpermissionsControllerBeforeMain($event)
	{
		$this->logUserAction('', 'COM_ADMINTOOLS_LOGS_FIXPERMISSIONS_RUN', 'com_admintools');
	}

	public function onComAdmintoolsHtaccessmakerControllerAfterApply($event)
	{
		$this->logUserAction('', 'COM_ADMINTOOLS_LOGS_HTACCESSMAKER_EDIT', 'com_admintools');
	}

	public function onComAdmintoolsHtaccessmakerControllerAfterSave($event)
	{
		$this->logUserAction('', 'COM_ADMINTOOLS_LOGS_HTACCESSMAKER_EDIT', 'com_admintools');
	}

	public function onComAdmintoolsIPDenyListControllerAfterApply($event)
	{
		$this->logCRUDSave($event, 'id', 'ip', 'COM_ADMINTOOLS_LOGS_BLACKLISTEDADDRESSES_EDIT_2');
	}

	/* Start of CRUD tasks */

	public function onComAdmintoolsIPDenyListControllerAfterSave($event)
	{
		$this->logCRUDSave($event, 'id', 'ip', 'COM_ADMINTOOLS_LOGS_BLACKLISTEDADDRESSES_EDIT_2');
	}

	public function onComAdmintoolsIPDenyListControllerAfterSave2new($event)
	{
		$this->logCRUDSave($event, 'id', 'ip', 'COM_ADMINTOOLS_LOGS_BLACKLISTEDADDRESSES_EDIT_2');
	}

	public function onComAdmintoolsIPDenyListsControllerBeforeDelete($event)
	{
		$this->logCRUDAction($event, 'ip', 'COM_ADMINTOOLS_LOGS_BLACKLISTEDADDRESSES_DELETE');
	}

	public function onComAdmintoolsIpautobanhistoriesControllerBeforeDelete($event)
	{
		$this->logCRUDAction($event, 'ip', 'COM_ADMINTOOLS_LOGS_IPAUTOBANHISTORIES_DELETE');
	}

	public function onComAdmintoolsMainpasswordControllerAfterApply($event)
	{
		$this->logUserAction('', 'COM_ADMINTOOLS_LOGS_MASTERPASSWORD_EDIT', 'com_admintools');
	}

	public function onComAdmintoolsMainpasswordControllerAfterSave($event)
	{
		$this->logUserAction('', 'COM_ADMINTOOLS_LOGS_MASTERPASSWORD_EDIT', 'com_admintools');
	}

	public function onComAdmintoolsNginxconfmakerControllerAfterApply($event)
	{
		$this->logUserAction('', 'COM_ADMINTOOLS_LOGS_NGINXCONFMAKER_EDIT', 'com_admintools');
	}

	public function onComAdmintoolsNginxconfmakerControllerAfterSave($event)
	{
		$this->logUserAction('', 'COM_ADMINTOOLS_LOGS_NGINXCONFMAKER_EDIT', 'com_admintools');
	}

	public function onComAdmintoolsQuickstartControllerAfterCommit($event)
	{
		$this->logUserAction('', 'COM_ADMINTOOLS_LOGS_QUICKSTART_SAVE', 'com_admintools');
	}

	public function onComAdmintoolsScanalertsControllerAfterMarkallsafe($event)
	{
		$scan_id = $this->getApplication()->getInput()->getInt('scan_id', 0);

		if (empty($scan_id))
		{
			return;
		}

		$this->logUserAction($scan_id, 'COM_ADMINTOOLS_LOGS_SCANALERTS_MARKEDALLSAFE', 'com_admintools');
	}

	public function onComAdmintoolsScanalertsControllerAfterPublish($event)
	{
		$arguments = array_values($event->getArguments());
		/** @var BaseController $controller */
		$controller = $arguments[0];
		/** @var ScanalertsModel $model */
		$model = $controller->getModel();
		$ids   = $this->getIDsFromRequest();

		if (!$ids)
		{
			return;
		}

		$table = $model->getTable('Scanalert', 'Administrator');

		foreach ($ids as $id)
		{
			if (!$table->load($id))
			{
				continue;
			}

			$this->logUserAction($table->path, 'COM_ADMINTOOLS_LOGS_SCANALERTS_MARKEDSAFE', 'com_admintools');
		}
	}

	public function onComAdmintoolsScanalertsControllerAfterUnpublish($event)
	{
		$arguments = array_values($event->getArguments());
		/** @var BaseController $controller */
		$controller = $arguments[0];
		/** @var ScanalertsModel $model */
		$model = $controller->getModel();
		$ids   = $this->getIDsFromRequest();

		if (!$ids)
		{
			return;
		}

		$table = $model->getTable('Scanalert', 'Administrator');

		foreach ($ids as $id)
		{
			if (!$table->load($id))
			{
				continue;
			}

			$this->logUserAction($table->path, 'COM_ADMINTOOLS_LOGS_SCANALERTS_MARKEDUNSAFE', 'com_admintools');
		}
	}

	public function onComAdmintoolsScansControllerBeforeStartscan($event)
	{
		$this->logUserAction('', 'COM_ADMINTOOLS_LOGS_SCANS_RUN', 'com_admintools');
	}

	public function onComAdmintoolsSeoandlinktoolsControllerAfterApply($event)
	{
		$this->logUserAction('', 'COM_ADMINTOOLS_LOGS_SEOANDLINKTOOLS_EDIT', 'com_admintools');
	}

	public function onComAdmintoolsSeoandlinktoolsControllerAfterSave($event)
	{
		$this->logUserAction('', 'COM_ADMINTOOLS_LOGS_SEOANDLINKTOOLS_EDIT', 'com_admintools');
	}

	public function onComAdmintoolsUrlredirectionControllerAfterApply($event)
	{
		$this->logCRUDSave($event, 'id', 'dest', 'COM_ADMINTOOLS_LOGS_REDIRECTIONS_EDIT_2');
	}

	public function onComAdmintoolsUrlredirectionControllerAfterSave($event)
	{
		$this->logCRUDSave($event, 'id', 'dest', 'COM_ADMINTOOLS_LOGS_REDIRECTIONS_EDIT_2');
	}

	public function onComAdmintoolsUrlredirectionControllerAfterSave2new($event)
	{
		$this->logCRUDSave($event, 'id', 'dest', 'COM_ADMINTOOLS_LOGS_REDIRECTIONS_EDIT_2');
	}

	public function onComAdmintoolsUrlredirectionsControllerAfterPublish($event)
	{
		$this->logCRUDAction($event, 'dest', 'COM_ADMINTOOLS_LOGS_REDIRECTIONS_PUBLISH_2');
	}

	public function onComAdmintoolsUrlredirectionsControllerAfterUnpublish($event)
	{
		$this->logCRUDAction($event, 'dest', 'COM_ADMINTOOLS_LOGS_REDIRECTIONS_UNPUBLISH_2');
	}

	public function onComAdmintoolsUrlredirectionsControllerBeforeDelete($event)
	{
		$this->logCRUDAction($event, 'dest', 'COM_ADMINTOOLS_LOGS_REDIRECTIONS_DELETE');
	}

	public function onComAdmintoolsWafdenylistControllerAfterApply($event)
	{
		$this->logCRUDSave($event, 'id', 'id', 'COM_ADMINTOOLS_LOGS_WAFBLACKLIST_EDIT_2');
	}

	public function onComAdmintoolsWafdenylistControllerAfterSave($event)
	{
		$this->logCRUDSave($event, 'id', 'id', 'COM_ADMINTOOLS_LOGS_WAFBLACKLIST_EDIT_2');
	}

	public function onComAdmintoolsWafdenylistControllerAfterSave2new($event)
	{
		$this->logCRUDSave($event, 'id', 'id', 'COM_ADMINTOOLS_LOGS_WAFBLACKLIST_EDIT_2');
	}

	public function onComAdmintoolsWafdenylistsControllerAfterDelete($event)
	{
		$this->logCRUDAction($event, 'id', 'COM_ADMINTOOLS_LOGS_WAFBLACKLIST_DELETE');
	}

	public function onComAdmintoolsWafdenylistsControllerAfterPublish($event)
	{
		$this->logCRUDAction($event, 'id', 'COM_ADMINTOOLS_LOGS_WAFBLACKLIST_PUBLISH_2');
	}

	public function onComAdmintoolsWafdenylistsControllerAfterUnpublish($event)
	{
		$this->logCRUDAction($event, 'id', 'COM_ADMINTOOLS_LOGS_WAFBLACKLIST_UNPUBLISH_2');
	}

	public function onComAdmintoolsWafexceptionControllerAfterApply($event)
	{
		$this->logCRUDSave($event, 'id', 'id', 'COM_ADMINTOOLS_LOGS_WAFEXCEPTIONS_EDIT_2');
	}

	public function onComAdmintoolsWafexceptionControllerAfterSave($event)
	{
		$this->logCRUDSave($event, 'id', 'id', 'COM_ADMINTOOLS_LOGS_WAFEXCEPTIONS_EDIT_2');
	}

	public function onComAdmintoolsWafexceptionControllerAfterSave2new($event)
	{
		$this->logCRUDSave($event, 'id', 'id', 'COM_ADMINTOOLS_LOGS_WAFEXCEPTIONS_EDIT_2');
	}

	public function onComAdmintoolsWafexceptionsControllerBeforeDelete($event)
	{
		$this->logCRUDAction($event, 'id', 'COM_ADMINTOOLS_LOGS_WAFEXCEPTIONS_DELETE');
	}

	public function onComAdmintoolsWebconfigmakerControllerAfterApply($event)
	{
		$this->logUserAction('', 'COM_ADMINTOOLS_LOGS_WEBCONFIGMAKER_EDIT', 'com_admintools');
	}

	public function onComAdmintoolsWebconfigmakerControllerAfterSave($event)
	{
		$this->logUserAction('', 'COM_ADMINTOOLS_LOGS_WEBCONFIGMAKER_EDIT', 'com_admintools');
	}

	/* End of CRUD tasks */

	/**
	 * Gets the list of IDs from the request data
	 *
	 * @return array
	 */
	private function getIDsFromRequest()
	{
		// Get the ID or list of IDs from the request or the configuration
		$cid = $this->getApplication()->getInput()->get('cid', [], 'array');
		$id  = $this->getApplication()->getInput()->getInt('id', 0);

		$ids = [];

		if (is_array($cid) && !empty($cid))
		{
			$ids = $cid;
		}
		elseif (!empty($id))
		{
			$ids = [$id];
		}

		return $ids;
	}

	/**
	 * @param           $controller
	 * @param   string  $displayKey
	 * @param   string  $translationKey
	 */
	private function logCRUDAction(Event $event, string $displayKey, string $translationKey): void
	{
		$arguments = array_values($event->getArguments());
		/** @var BaseController $controller */
		$controller = $arguments[0];
		$ids        = $this->getIDsFromRequest();
		$model      = $controller->getModel();
		$tableName  = $controller->getName();

		try
		{
			$table = $model->getTable($tableName, 'Administrator');
		}
		catch (\Exception $e)
		{
			$tableName = $this->singularize($tableName);
			$table     = $model->getTable($tableName, 'Administrator');
		}

		foreach ($ids as $id)
		{
			if (!$table->load($id))
			{
				continue;
			}

			$primaryKey = $table->getKeyName(false);
			$link       = sprintf(
				"index.php?option=com_admintools&view=%s&task=edit&%s=%s",
				urlencode($tableName),
				urlencode($primaryKey),
				urlencode($table->{$primaryKey}));

			$this->logUserAction([
				'title' => $table->{$displayKey},
				'link'  => $link,
			], $translationKey, 'com_admintools');
		}
	}

	/**
	 * @param           $controller
	 * @param   string  $primaryKey
	 * @param   string  $displayKey
	 * @param   string  $translationKey
	 */
	private function logCRUDSave(Event $event, string $primaryKey, string $displayKey, string $translationKey): void
	{
		$arguments = array_values($event->getArguments());
		/** @var BaseController $controller */
		$controller     = $arguments[0];
		$model          = $controller->getModel();
		$controllerName = $controller->getName();
		$tableName      = $controller->getName();
		try
		{
			$table = $model->getTable($tableName, 'Administrator');
		}
		catch (\Exception $e)
		{
			$tableName = $this->singularize($tableName);
			$table     = $model->getTable($tableName, 'Administrator');
		}

		if (!$table->load($this->getApplication()->getInput()->getInt($primaryKey)))
		{
			return;
		}

		$link = sprintf(
			"index.php?option=com_admintools&view=%s&task=edit&%s=%s",
			urlencode($controllerName),
			urlencode($primaryKey),
			urlencode($table->{$primaryKey}));

		$this->logUserAction([
			'title' => $table->{$displayKey},
			'link'  => $link,
		], $translationKey, 'com_admintools');
	}

	/**
	 * Log a user action.
	 *
	 * This is a simple wrapper around self::addLog
	 *
	 * @param   string|array  $title               Language key for title or an array of additional data to record in
	 *                                             the audit log.
	 * @param   string        $messageLanguageKey  Language key describing the user action taken.
	 * @param   string|null   $context             The name of the extension being logged (default: use
	 *                                             $this->defaultExtension).
	 * @param   User|null     $user                User object taking this action (default: currently logged in user).
	 *
	 * @return  void
	 *
	 * @see     self::addLog
	 * @since   9.0.0
	 */
	private function logUserAction($title, string $messageLanguageKey, ?string $context = null, ?User $user = null): void
	{
		// Get the user if not defined
		$user = $user ?? $this->getApplication()->getIdentity();

		// No log for guests
		if (empty($user) || ($user->guest))
		{
			return;
		}

		// Default extension if none defined
		$context = $context ?? $this->defaultExtension;

		$message = [
			'username'    => $user->username,
			'accountlink' => 'index.php?option=com_users&task=user.edit&id=' . $user->id,
		];

		if (!is_array($title))
		{
			$title = [
				'title' => $title,
			];
		}

		$message = array_merge($message, $title);

		$this->addLog([$message], $messageLanguageKey, $context, $user->id);
	}

	/**
	 * Convert an English word to singular.
	 *
	 * @param   string  $word
	 *
	 * @return  string
	 * @since   7.7.0
	 */
	private function singularize(string $word): string
	{
		$rules = [
			// Singularization rules. The regex on the left transforms to the regex on the right.
			'singularization' => [
				'/cookies$/i'                                                      => 'cookie',
				'/moves$/i'                                                        => 'move',
				'/sexes$/i'                                                        => 'sex',
				'/children$/i'                                                     => 'child',
				'/men$/i'                                                          => 'man',
				'/feet$/i'                                                         => 'foot',
				'/people$/i'                                                       => 'person',
				'/taxa$/i'                                                         => 'taxon',
				'/databases$/i'                                                    => 'database',
				'/menus$/i'                                                        => 'menu',
				'/(quiz)zes$/i'                                                    => '\1',
				'/(matr|suff)ices$/i'                                              => '\1ix',
				'/(vert|ind|cod)ices$/i'                                           => '\1ex',
				'/^(ox)en/i'                                                       => '\1',
				'/(alias|status)es$/i'                                             => '\1',
				'/(tomato|hero|buffalo)es$/i'                                      => '\1',
				'/([octop|vir])i$/i'                                               => '\1us',
				'/(gen)era$/i'                                                     => '\1us',
				'/(cris|^ax|test)es$/i'                                            => '\1is',
				'/is$/i'                                                           => 'is',
				'/us$/i'                                                           => 'us',
				'/ias$/i'                                                          => 'ias',
				'/(shoe)s$/i'                                                      => '\1',
				'/(o)es$/i'                                                        => '\1e',
				'/(bus)es$/i'                                                      => '\1',
				'/(campus)es$/i'                                                   => '\1',
				'/([m|l])ice$/i'                                                   => '\1ouse',
				'/(x|ch|ss|sh)es$/i'                                               => '\1',
				'/(m)ovies$/i'                                                     => '\1ovie',
				'/(s)eries$/i'                                                     => '\1eries',
				'/(v)ies$/i'                                                       => '\1ie',
				'/([^aeiouy]|qu)ies$/i'                                            => '\1y',
				'/([lr])ves$/i'                                                    => '\1f',
				'/(tive)s$/i'                                                      => '\1',
				'/(hive)s$/i'                                                      => '\1',
				'/([^f])ves$/i'                                                    => '\1fe',
				'/(^analy)ses$/i'                                                  => '\1sis',
				'/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i' => '\1\2sis',
				'/([ti]|addend)a$/i'                                               => '\1um',
				'/(alumn|formul)ae$/i'                                             => '$1a',
				'/(n)ews$/i'                                                       => '\1ews',
				'/(.*)ss$/i'                                                       => '\1ss',
				'/(.*)s$/i'                                                        => '\1',
			],
			// Uncountable objects are always singular
			'uncountable'     => [
				'aircraft',
				'cannon',
				'deer',
				'equipment',
				'fish',
				'information',
				'money',
				'moose',
				'news',
				'rice',
				'series',
				'sheep',
				'species',
				'swine',
			],
		];

		if (in_array($word, $rules['uncountable']))
		{
			return $word;
		}

		foreach ($rules['singularization'] as $regexp => $replacement)
		{
			$matches  = null;
			$singular = preg_replace($regexp, $replacement, $word, -1, $matches);

			if ($matches > 0)
			{
				return $singular;
			}
		}

		return $word;
	}
}