<?php namespace ProcessWire;

/**
 * ProcessWire Base Class "Wire"
 * 
 * #pw-summary Wire is the base class for most ProcessWire classes and modules. 
 * #pw-body = 
 * Wire derived classes have a `$this->wire()` method that provides access to ProcessWire’s API variables.
 * API variables can also be accessed as local properties in most cases. Wire also provides basic methods 
 * for tracking changes and managing runtime notices specific to the instance. 
 * 
 * Wire derived classes can specify which methods are “hookable” by precending the method name with 
 * 3 underscores like this: `___myMethod()`. Other classes can then hook either before or after that method, 
 * modifying arguments or return values. Several other hook methods are also provided for Wire derived 
 * classes that are hooking into others. 
 * #pw-body
 * #pw-order-groups common,identification,hooks,notices,changes,hooker,api-helpers
 * #pw-summary-api-helpers Shortcuts to ProcessWire API variables. Access without any arguments returns the API variable. Some support arguments as shortcuts to methods in the API variable.
 * #pw-summary-changes Methods to support tracking and retrieval of changes made to the object.
 * #pw-summary-hooks Methods for managing hooks for an object instance or class. 
 * 
 * ProcessWire 3.x, Copyright 2022 by Ryan Cramer
 * https://processwire.com
 * 
 * #pw-use-constants
 * 
 * @property string $className #pw-internal
 * 
 * API variables accessible as properties (unless $useFuel has been set to false):
 * 
 * @property AdminTheme|AdminThemeFramework|null $adminTheme #pw-internal
 * @property WireCache $cache #pw-internal
 * @property Config $config #pw-internal
 * @property WireDatabasePDO $database #pw-internal
 * @property Database $db #pw-internal deprecated
 * @property WireDateTime $datetime #pw-internal
 * @property Fieldgroups|Fieldgroup[] $fieldgroups #pw-internal
 * @property Fields|Field[] $fields #pw-internal
 * @property Fieldtypes|Fieldtype[] $fieldtypes #pw-internal
 * @property WireFileTools $files #pw-internal
 * @property Fuel $fuel #pw-internal
 * @property WireHooks $hooks #pw-internal
 * @property WireInput $input #pw-internal
 * @property Languages|Language[] $languages (present only if LanguageSupport installed) #pw-internal
 * @property WireLog $log #pw-internal
 * @property WireMailTools $mail #pw-internal
 * @property Modules $modules #pw-internal
 * @property Notices|Notice[] $notices #pw-internal
 * @property Page $page #pw-internal
 * @property Pages $pages #pw-internal
 * @property Permissions|Permission[] $permissions #pw-internal
 * @property Process|ProcessPageView $process #pw-internal
 * @property WireProfilerInterface $profiler #pw-internal
 * @property Roles|Role[] $roles #pw-internal
 * @property Sanitizer $sanitizer #pw-internal
 * @property Session $session #pw-internal
 * @property Templates|Template[] $templates #pw-internal
 * @property Paths $urls #pw-internal
 * @property User $user #pw-internal
 * @property Users $users #pw-internal
 * @property ProcessWire $wire #pw-internal
 * 
 * The following map API variables to function names and apply only if another function in the class does not 
 * already have the same name, which would override. All defined API variables can be accessed as functions 
 * that return the API variable, whether documented below or not. 
 *
 * @method WireCache|string|array|PageArray|null cache($name = '', $expire = null, $func = null) Access the $cache API variable as a function.  #pw-group-api-helpers
 * @method Config|mixed config($key = '', $value = null) Access the $config API variable as a function. #pw-group-api-helpers
 * @method WireDatabasePDO database() Access the $database API variable as a function.  #pw-group-api-helpers
 * @method WireDateTime|string|int datetime($format = '', $value = '') Access the $datetime API variable as a function.  #pw-group-api-helpers
 * @method Field|Fields|null fields($name = '') Access the $fields API variable as a function.  #pw-group-api-helpers
 * @method WireFileTools files() Access the $files API variable as a function.  #pw-group-api-helpers
 * @method WireInput|WireInputData|WireInputDataCookie|array|string|int|null input($type = '', $key = '', $sanitizer = '') Access the $input API variable as a function.  #pw-group-api-helpers
 * @method WireInputDataCookie|string|int|array|null inputCookie($key = '', $sanitizer = '') Access the $input->cookie() API variable as a function.  #pw-group-api-helpers
 * @method WireInputData|string|int|array|null inputGet($key = '', $sanitizer = '') Access the $input->get() API variable as a function.  #pw-group-api-helpers
 * @method WireInputData|string|int|array|null inputPost($key = '', $sanitizer = '') Access the $input->post() API variable as a function.  #pw-group-api-helpers
 * @method Languages|Language|NullPage|null languages($name = '') Access the $languages API variable as a function.  #pw-group-api-helpers
 * @method Modules|Module|ConfigurableModule|null modules($name = '') Access the $modules API variable as a function. #pw-group-api-helpers
 * @method Page|Mixed page($key = '', $value = null) Access the $page API variable as a function. #pw-group-api-helpers
 * @method Pages|PageArray|Page|NullPage pages($selector = '') Access the $pages API variable as a function. #pw-group-api-helpers
 * @method Permissions|Permission|PageArray|null|NullPage permissions($selector = '') Access the $permissions API variable as a function.  #pw-group-api-helpers
 * @method Roles|Role|PageArray|null|NullPage roles($selector = '') Access the $roles API variable as a function.  #pw-group-api-helpers
 * @method Sanitizer|string|int|array|null|mixed sanitizer($name = '', $value = '') Access the $sanitizer API variable as a function.  #pw-group-api-helpers
 * @method Session|mixed session($key = '', $value = null) Access the $session API variable as a function.  #pw-group-api-helpers
 * @method Templates|Template|null templates($name = '') Access the $templates API variable as a function. #pw-group-api-helpers
 * @method User|mixed user($key = '', $value = null) Access the $user API variable as a function. #pw-group-api-helpers
 * @method Users|PageArray|User|mixed users($selector = '') Access the $users API variable as a function. #pw-group-api-helpers
 * 
 * Other standard hookable methods
 * 
 * @method changed(string $what, $old = null, $new = null) See Wire::___changed()
 * @method log($str = '', array $options = array()) See Wire::___log()
 * @method callUnknown($method, $arguments) See Wire::___callUnknown()
 * @method Wire trackException(\Exception $e, $severe = true, $text = null)
 * 
 * 
 */

abstract class Wire implements WireTranslatable, WireFuelable, WireTrackable {

	/*******************************************************************************************************
	 * API VARIABLE/FUEL INJECTION AND ACCESS
	 * 
	 * PLEASE NOTE: All the following fuel related variables/methods will be going away in PW 3.0.
	 * You should use the $this->wire() method instead for compatibility with PW 3.0. The only methods
	 * and variables sticking around for PW 3.0 are:
	 * 
	 * $this->wire(...);
	 * $this->useFuel(bool);
	 * $this->useFuel
	 * 
	 */

	/**
	 * Whether this class may use fuel variables in local scope, like $this->item
	 * 
	 * @var bool
	 *
	 */
	protected $useFuel = true;
	
	/**
	 * Total number of Wire class instances
	 *
	 * @var int
	 *
	 */
	static private $_instanceTotal = 0;

	/**
	 * ID of this Wire class instance
	 *
	 * @var int
	 *
	 */
	private $_instanceNum = 0;
	
	/**
	 * Construct
	 * 
	 */
	public function __construct() {}

	/**
	 * Clone this Wire instance
	 * 
	 */
	public function __clone() {
		$this->_instanceNum = 0;
		$this->getInstanceNum();
	}
	
	/**
	 * Get this Wire object’s instance number
	 * 
	 * - This is a unique number among all other Wire (or derived) instances in the system.
	 * - If this instance ID has not yet been set, this will set it. 
	 * - Note that this is different from the ProcessWire instance ID.
	 * 
	 * #pw-group-identification
	 *
	 * @param bool $getTotal Specify true to get the total quantity of Wire instances rather than this instance number. 
	 * @return int Instance number
	 *
	 */
	public function getInstanceNum($getTotal = false) {
		if(!$this->_instanceNum) {
			self::$_instanceTotal++;
			$this->_instanceNum = self::$_instanceTotal;
		}
		if($getTotal) return self::$_instanceTotal;
		return $this->_instanceNum;
	}

	/**
	 * Add fuel to all classes descending from Wire
	 * 
	 * #pw-internal
	 *
	 * @param string $name 
	 * @param mixed $value 
	 * @param bool $lock Whether the API value should be locked (non-overwritable)
	 * @internal Fuel is an internal-only keyword.
	 * 	Unless static needed, use $this->wire($name, $value) instead.
	 * @deprecated Use $this->wire($name, $value, $lock) instead.
	 *
	 */
	public static function setFuel($name, $value, $lock = false) {
		$wire = ProcessWire::getCurrentInstance();
		$log = $wire->wire()->log;
		if($log) $log->deprecatedCall();
		$wire->fuel()->set($name, $value, $lock);
	}

	/**
	 * Get the Fuel specified by $name or NULL if it doesn't exist
	 * 
	 * #pw-internal
	 *
	 * @param string $name
	 * @return mixed|null
	 * @internal Fuel is an internal-only keyword.  
	 * 	Use $this->wire(name) or $this->wire()->name instead, unless static is required.
	 * @deprecated
	 *
	 */
	public static function getFuel($name = '') {
		$wire = ProcessWire::getCurrentInstance();
		$log = $wire->wire()->log;
		if($log) $log->deprecatedCall();
		if(empty($name)) return $wire->fuel();	
		return $wire->fuel()->$name;
	}

	/**
	 * Returns an iterable Fuel object of all Fuel currently loaded
	 * 
	 * #pw-internal
	 *
	 * @return Fuel
	 * @deprecated This method will be going away. 
	 * 	Use $this->wire() instead, or if static required use: Wire::getFuel() with no arguments
	 *
	 */
	public static function getAllFuel() {
		$wire = ProcessWire::getCurrentInstance();
		$log = $wire->wire()->log;
		if($log) $log->deprecatedCall();
		return $wire->fuel();	
	}

	/**
	 * Get the Fuel specified by $name or NULL if it doesn't exist (DEPRECATED)
	 * 
	 * #pw-internal
	 * 
	 * DO NOT USE THIS METHOD: It is deprecated and only used by the ProcessWire class. 
	 * It is here in the Wire class for legacy support only. Use the wire() method instead.
	 *
	 * @param string $name
	 * @return mixed|null
	 *
	 */
	public function fuel($name = '') {
		$wire = $this->wire();
		$log = $wire->wire()->log;
		if($log) $log->deprecatedCall();
		return $wire->fuel($name);
	}
	
	/**
	 * Should fuel vars be scoped locally to this class instance? (internal use only)
	 *
	 * If so, you can do things like $this->apivar.
	 * If not, then you'd have to do $this->wire('apivar').
	 *
	 * If you specify a value, it will set the value of useFuel to true or false.
	 * If you don't specify a value, the current value will be returned.
	 *
	 * Local fuel scope should be disabled in classes where it might cause any conflict with class vars.
	 * 
	 * #pw-internal
	 *
	 * @param bool $useFuel Optional boolean to turn it on or off.
	 * @return bool Current value of $useFuel
	 *
	 */
	public function useFuel($useFuel = null) {
		if($useFuel !== null) $this->useFuel = $useFuel ? true : false;
		return $this->useFuel;
	}


	/*******************************************************************************************************
	 * IDENTIFICATION
	 *
	 */
	
	/**
	 * Return this object’s class name
	 * 
	 * By default, this method returns the class name without namespace. To include the namespace, call it
	 * with boolean true as the first argument. 
	 * 
	 * ~~~~~
	 * echo $page->className(); // outputs: Page
	 * echo $page->className(true); // outputs: ProcessWire\Page
	 * ~~~~~
	 * 
	 * #pw-group-identification
	 *
	 * @param array|bool|null $options Specify boolean `true` to return class name with namespace, or specify an array of
	 *  one or more options:
	 * 	- `lowercase` (bool): Specify true to make it return hyphenated lowercase version of class name (default=false).
	 * 	- `namespace` (bool): Specify true to include namespace from returned class name (default=false). 
	 * 	- *Note: The lowercase and namespace options may not both be true at the same time.*
	 * @return string String with class name
	 *
	 */
	public function className($options = null) {
		
		if(is_bool($options)) {
			$options = array('namespace' => $options);
		} else if(is_array($options)) {
			if(!empty($options['lowercase'])) $options['namespace'] = false;
		} else {
			$options = array();
		}

		if(isset($options['namespace']) && $options['namespace'] === true) {
			$className = get_class($this);
			if(strpos($className, '\\') === false) $className = "\\$className";
		} else {
			$className = wireClassName($this, false);
		}

		if(!empty($options['lowercase'])) {
			static $cache = array();
			if(isset($cache[$className])) {
				$className = $cache[$className];
			} else {
				$_className = $className;
				$part = substr($className, 1);
				if(strtolower($part) != $part) {
					// contains more than 1 uppercase character, convert to hyphenated lowercase
					$className = substr($className, 0, 1) . preg_replace('/([A-Z])/', '-$1', $part);
				}
				$className = strtolower($className);
				$cache[$_className] = $className;
			}
		}
		
		return $className;
	}


	/**
	 * Unless overridden, classes descending from Wire return their class name when typecast as a string
	 * 
	 * @return string
	 *
	 */
	public function __toString() {
		return $this->className();
	}


	/*******************************************************************************************************
	 * HOOKS
	 *
	 */
	
	/**
	 * Hooks that are local to this instance of the class only.
	 *
	 */
	protected $localHooks = array();

	/**
	 * @var WireHooks|null
	 * 
	 */
	private $_wireHooks = null;

	/**
	 * @return WireHooks|null
	 * @since 3.0.171
	 * 
	 */
	protected function _wireHooks() {
		if($this->_wireHooks === null) $this->_wireHooks = $this->wire()->hooks;
		return $this->_wireHooks;
	}

	/**
	 * Return all local hooks for this instance
	 * 
	 * #pw-internal
	 * 
	 * @return array
	 * 
	 */
	public function getLocalHooks() {
		return $this->localHooks;
	}

	/**
	 * Set local hooks for this instance
	 * 
	 * #pw-internal
	 * 
	 * @param array $hooks
	 * 
	 */
	public function setLocalHooks(array $hooks) {
		$this->localHooks = $hooks;
	}

	/**
	 * Call a method in this object, for use by WireHooks
	 * 
	 * #pw-internal
	 * 
	 * @param string $method
	 * @param array $arguments
	 * @return mixed
	 * 
	 */
	public function _callMethod($method, $arguments) {
		$qty = $arguments ? count($arguments) : 0;
		switch($qty) {
			case 0:
				$result = $this->$method();
				break;
			case 1:
				$result = $this->$method($arguments[0]);
				break;
			case 2:
				$result = $this->$method($arguments[0], $arguments[1]);
				break;
			case 3:
				$result = $this->$method($arguments[0], $arguments[1], $arguments[2]);
				break;
			default:
				$result = call_user_func_array(array($this, $method), $arguments);
		}
		return $result;
	}

	/**
	 * Call a hook method (optimization when it's known for certain the method exists)
	 * 
	 * #pw-internal
	 * 
	 * @param string $method Method name, without leading "___"
	 * @param array $arguments
	 * @return mixed
	 * 
	 */
	public function _callHookMethod($method, array $arguments = array()) {
		if(method_exists($this, $method)) {
			return $this->_callMethod($method, $arguments);
		}
		/** @var WireHooks $hooks */
		$hooks = $this->_wireHooks();
		if($hooks && $hooks->isMethodHooked($this, $method)) {
			$result = $hooks->runHooks($this, $method, $arguments);
			return $result['return'];
		} else {
			return $this->_callMethod("___$method", $arguments);
		}
	}

	/**
	 * Provides the gateway for calling hooks in ProcessWire
	 * 
	 * When a non-existant method is called, this checks to see if any hooks have been defined and sends the call to them. 
	 * 
	 * Hooks are defined by preceding the "hookable" method in a descending class with 3 underscores, like __myMethod().
	 * When the API calls $myObject->myMethod(), it gets sent to $myObject->___myMethod() after any 'before' hooks have been called. 
	 * Then after the ___myMethod() call, any "after" hooks are then called. "after" hooks have the opportunity to change the return value.
	 *
	 * Hooks can also be added for methods that don't actually exist in the class, allowing another class to add methods to this class. 
	 *
	 * See the Wire::runHooks() method for the full implementation of hook calls.
	 *
	 * @param string $method
	 * @param array $arguments
	 * @return mixed
	 * @throws WireException
	 *
	 */ 
	public function __call($method, $arguments) {
		if(empty($arguments) && Fuel::isCommon($method)) { 
			// faster version of _callWireAPI for when conditions allow
			if($this->_wire && !method_exists($this, "___$method")) {
				// get a common API var with no arguments as method call more quickly 
				$val = $this->_wire->fuel($method);
				if($val !== null) return $val;
			}
		}
		$hooks = $this->_wireHooks();
		if($hooks) {
			$result = $hooks->runHooks($this, $method, $arguments);
			if(!$result['methodExists'] && !$result['numHooksRun']) {
				$result = $this->_callWireAPI($method, $arguments);
				if(!$result) return $this->callUnknown($method, $arguments);
			}
		} else {
			$result = $this->_callWireAPI($method, $arguments);
			if(!$result) return $this->___callUnknown($method, $arguments);
		}
		return $result['return'];
	}

	/**
	 * Helper to __call() method that maps a call to an API variable when appropriate
	 * 
	 * @param string $method
	 * @param array $arguments
	 * @return array|bool
	 * @internal
	 * 
	 */
	protected function _callWireAPI($method, $arguments) {
		$var = $this->_wire ? $this->_wire->fuel()->$method : null;
		if(!$var) return false;
		// requested method maps to an API variable
		$result = array('return' => null);
		if(count($arguments)) {
			$funcName = 'wire' . ucfirst($method);
			if(__NAMESPACE__) $funcName = __NAMESPACE__ . "\\$funcName";
			if(function_exists($funcName)) {
				// a function exists with this API var name
				$wire = ProcessWire::getCurrentInstance();
				// ensure function call maps to this PW instance
				if($wire !== $this->_wire) ProcessWire::setCurrentInstance($this->_wire);
				$result['return'] = call_user_func_array($funcName, $arguments);
				if($wire !== $this->_wire) ProcessWire::setCurrentInstance($wire);
			}
		} else {
			// if no arguments provided, just return API var
			$result['return'] = $var;
		}
		return $result;
	}

	/**
	 * If method call resulted in no handler, this hookable method is called. 
	 * 
	 * This standard implementation just throws an exception. This is a template method, so the reason it
	 * exists is so that other classes can override and provide their own handler. Classes that provide
	 * their own handler should not do a `parent::__callUnknown()` unless they also fail, as that will 
	 * cause an exception to be thrown. 
	 * 
	 * If you want to override this method with a hook, see the example below. 
	 * ~~~~~
	 * $wire->addHookBefore('Wire::callUnknown', function(HookEvent $event) {
	 *   // Get information about unknown method that was called
	 *   $methodObject = $event->object; 
	 *   $methodName = $event->arguments(0); // string
	 *   $methodArgs = $event->arguments(1); // array
	 *   // The replace option replaces the method and blocks the exception
	 *   $event->replace = true; 
	 *   // Now do something with the information you have, for example
	 *   // you might want to populate a value to $event->return if 
	 *   // you want the unknown method to return a value. 
	 * }); 
	 * ~~~~~
	 * 
	 * #pw-hooker
	 * 
	 * @param string $method Requested method name
	 * @param array $arguments Arguments provided
	 * @return null|mixed Return value of method (if applicable)
	 * @throws WireException
	 * 
	 */
	protected function ___callUnknown($method, $arguments) {
		if($arguments) {} // intentional to avoid unused argument notice
		$config = $this->wire()->config;
		if($config && $config->disableUnknownMethodException) return null;
		throw new WireException("Method " . $this->className() . "::$method does not exist or is not callable in this context"); 
	}

	/**
	 * Provides the implementation for calling hooks in ProcessWire
	 *
	 * Unlike __call, this method won't trigger an Exception if the hook and method don't exist. 
	 * Instead it returns a result array containing information about the call. 
	 * 
	 * #pw-internal
	 *
	 * @param string $method Method or property to run hooks for.
	 * @param array $arguments Arguments passed to the method and hook. 
	 * @param string|array $type May be either 'method', 'property' or array of hooks (from getHooks) to run. Default is 'method'.
	 * @return array Returns an array with the following information: 
	 * 	[return] => The value returned from the hook or NULL if no value returned or hook didn't exist. 
	 *	[numHooksRun] => The number of hooks that were actually run. 
	 *	[methodExists] => Did the hook method exist as a real method in the class? (i.e. with 3 underscores ___method).
	 *	[replace] => Set by the hook at runtime if it wants to prevent execution of the original hooked method.
	 *
	 */
	public function runHooks($method, $arguments, $type = 'method') {
		return $this->_wireHooks()->runHooks($this, $method, $arguments, $type);
	}

	/**
	 * Return all hooks associated with this class instance or method (if specified)
	 * 
	 * #pw-group-hooks
	 *
	 * @param string $method Optional method that hooks will be limited to. Or specify '*' to return all hooks everywhere.
	 * @param int $type Type of hooks to return, specify one of the following constants (from the WireHooks class):
	 * 	- `WireHooks::getHooksAll` returns all hooks (default).
	 * 	- `WireHooks::getHooksLocal` returns local hooks only.
	 * 	- `WireHooks::getHooksStatic` returns static hooks only.
	 * @return array
	 *
	 */
	public function getHooks($method = '', $type = 0) {
		return $this->_wireHooks()->getHooks($this, $method, $type); 
	}
	
	/**
	 * Returns true if the method/property is hooked, false if it isn’t.
	 * 
	 * This is for optimization use. It does not distinguish about class instance. 
	 * It only distinguishes about class if you provide a class with the `$method` argument (i.e. `Class::`).
	 * As a result, a true return value indicates something "might" be hooked, as opposed to be 
	 * being definitely hooked. 
	 *
	 * If checking for a hooked method, it should be in the form "Class::method()" or "method()". 
	 * If checking for a hooked property, it should be in the form "Class::property" or "property". 
	 * 
	 * #pw-internal
	 * 
	 * @param string $method Method or property name in one of the following formats:
	 * 	Class::method()
	 * 	Class::property
	 * 	method()
	 * 	property
	 * @param Wire|null $instance Optional instance to check against (see hasHook method for details)
	 * 	Note that if specifying an $instance, you may not use the Class::method() or Class::property options for $method argument.
	 * @return bool
	 * @deprecated 
	 *
	 */
	static public function isHooked($method, ?Wire $instance = null) {
		/** @var ProcessWire $wire */
		$wire = $instance ? $instance->wire() : ProcessWire::getCurrentInstance();
		if($instance) return $instance->wire()->hooks->hasHook($instance, $method);
		return $wire->hooks->isHooked($method);
	}

	/**
	 * Returns true if the method or property is hooked, false if it isn’t.
	 *
	 * - This method checks for both static hooks and local hooks.
	 * - Accepts a `method()` or `property` name as an argument. 
	 * - Class context is assumed to be the current class this method is called on. 
	 * - Also considers the class parents for hooks. 
	 * 
	 * ~~~~~
	 * if($pages->hasHook('find()')) {
	 *   // the Pages::find() method is hooked
	 * }
	 * ~~~~~
	 *
	 * #pw-group-hooks
	 *
	 * @param string $name Method() name or property name:
	 *   - If checking for a hooked method, it should be in the form `method()`.
	 *   - If checking for a hooked property, it should be in the form `property`.
	 * @return bool True if this class instance has the hook, false if not. 
	 * @throws WireException When you try to call it with a Class::something() type method, which is not supported. 
	 *
	 */
	public function hasHook($name) {
		return $this->_wireHooks()->hasHook($this, $name);
	}

	/**
	 * Hook a function/method to a hookable method call in this object
	 *
	 * - This method provides the implementation for addHookBefore(), addHookAfter(), addHookProperty(), addHookMethod()
	 * - Hookable method calls are methods preceded by three underscores. 
	 * - You may also specify a method that doesn't exist already in the class.
	 * - The hook method that you define may be part of a class or a globally scoped function. 
	 * 
	 * #pw-internal
	 *
	 * @param string|array $method Method name to hook into, NOT including the three preceding underscores. 
	 * 	May also be Class::Method for same result as using the fromClass option.
	 *  May also be array or CSV string of hook definitions to attach multiple to the same $toMethod (since 3.0.137). 
	 * @param object|null|callable $toObject Object to call $toMethod from,
	 * 	Or null if $toMethod is a function outside of an object,
	 * 	Or function|callable if $toObject is not applicable or function is provided as a closure.
	 * @param string|array $toMethod Method from $toObject, or function name to call on a hook event, or $options array. Optional.
	 * @param array $options Options that can modify default behaviors: 
	 *  - `type` (string): May be 'method', 'property' or 'either'. If property, then it will respond to $obj->property
	 *     rather than $obj->method(). If 'either' it will respond to both. The default type is 'method'.
	 *  - `before` (bool): Execute the hook before the method call? (allows modification of arguments).
	 *     Not applicable if 'type' is 'property'.
	 *  - `after` (bool): Execute the hook after the method call? (allows modification of return value).
	 *     Not applicable if 'type' is 'property'.
	 *  - `priority` (int): A number determining the priority of a hook, where lower numbers are executed before
	 *     higher numbers. The default priority is 100.
	 *  - `allInstances` (bool): attach the hook to all instances of this object? Set automatically, but you may
	 *     still use in some instances.
	 *  - `fromClass` (string): The name of the class containing the hooked method, if not the object where addHook
	 *     was called. Set automatically, but you may still use in some instances.
	 *  - `argMatch` (array|null): An array of Selectors objects where the indexed argument (n) to the hooked method
	 *     must match, in order to execute hook. Default is null.
	 *  - `objMatch` (array|null): Selectors object that the current object must match in order to execute hook.
	 *     Default is null. 
	 * @return string A special Hook ID that should be retained if you need to remove the hook later.
	 *  If multiple methods were hooked then it is a CSV string of hook IDs, accepted removeHook method (since 3.0.137). 
	 * @throws WireException
	 * @see https://processwire.com/docs/modules/hooks/
	 *
	 */
	public function addHook($method, $toObject, $toMethod = null, $options = array()) {
		return $this->_wireHooks()->addHook($this, $method, $toObject, $toMethod, $options);
	}

	/**
	 * Add a hook to be executed before the hooked method 
	 * 
	 * - Use a "before" hook when you have code that should execute before a hookable method executes. 
	 * - One benefit of using a "before" hook is that you can have it modify the arguments that are sent to the hookable method. 
	 * - This type of hook can also completely replace a hookable method if hook populates an `$event->replace` property.
	 *   See the HookEvent class for details. 
	 * 
	 * ~~~~~
	 * // Attach hook to a method in current object
	 * $this->addHookBefore('Page::path', $this, 'yourHookMethodName'); 
	 *   
	 * // Attach hook to an inline function
	 * $this->addHookBefore('Page::path', function($event) { ... }); 
	 *   
	 * // Attach hook to a procedural function
	 * $this->addHookBefore('Page::path', 'your_function_name'); 
	 *   
	 * // Attach hook from single object instance ($page) to inline function
	 * $page->addHookBefore('path', function($event) { ... }); 
	 * ~~~~~
	 * 
	 * #pw-group-hooks
	 *
	 * @param string|array $method Method to hook in one of the following formats (please omit 3 leading underscores): 
	 *  - `Class::method` - If hooking to *all* object instances of the class. 
	 *  - `method` - If hooking to a single object instance. 
	 *  - Since 3.0.137 it may also be multiple methods to hook in CSV string or array. 
	 * @param object|null|callable $toObject Specify one of the following: 
	 *  - Object instance to call `$toMethod` from (like `$this`).
	 *  - Inline function (closure) if providing implemention inline. 
	 *  - Procedural function name, if hook is implemented by a procedural function. 
	 *  - Null if you want to use the 3rd argument and don't need this argument. 
	 * @param string|array $toMethod Method from $toObject, or function name to call on a hook event. 
	 *   This argument can be sustituted as the 2nd argument when the 2nd argument isn’t needed,
	 *   or it can be the $options argument. 
	 * @param array $options Array of options that can modify behavior: 
	 *  - `type` (string): May be either 'method' or 'property'. If property, then it will respond to $obj->property 
	 *     rather than $obj->method(). The default type is 'method'.
	 *  - `priority` (int): A number determining the priority of a hook, where lower numbers are executed before 
	 *     higher numbers. The default priority is 100. 
	 * @return string A special Hook ID (or CSV string of hook IDs) that should be retained if you need to remove the hook later.
	 * @see https://processwire.com/docs/modules/hooks/
	 *
	 */
	public function addHookBefore($method, $toObject, $toMethod = null, $options = array()) {
		// This is the same as calling addHook with the 'before' option set the $options array.
		$options['before'] = true; 
		if(!isset($options['after'])) $options['after'] = false; 
		return $this->_wireHooks()->addHook($this, $method, $toObject, $toMethod, $options); 
	}

	/**
	 * Add a hook to be executed after the hooked method
	 * 
	 * - Use an "after" hook when you have code that should execute after a hookable method executes.
	 * - One benefit of using an "after" hook is that you can have it modify the return value. 
	 *
	 * ~~~~~
	 * // Attach hook to a method in current object
	 * $this->addHookAfter('Page::path', $this, 'yourHookMethodName');
	 *  
	 * // Attach hook to an inline function
	 * $this->addHookAfter('Page::path', function($event) { ... });
	 *  
	 * // Attach hook to a procedural function
	 * $this->addHookAfter('Page::path', 'your_function_name');
	 *  
	 * // Attach hook from single object instance ($page) to inline function
	 * $page->addHookAfter('path', function($event) { ... });
	 * ~~~~~
	 * 
	 * #pw-group-hooks
	 *
	 * @param string|array $method Method to hook in one of the following formats (please omit 3 leading underscores):
	 *  - `Class::method` - If hooking to *all* object instances of the class.
	 *  - `method` - If hooking to a single object instance.
	 *  - Since 3.0.137 it may also be multiple methods to hook in CSV string or array. 
	 * @param object|null|callable $toObject Specify one of the following:
	 *  - Object instance to call `$toMethod` from (like `$this`).
	 *  - Inline function (closure) if providing implemention inline.
	 *  - Procedural function name, if hook is implemented by a procedural function.
	 *  - Null if you want to use the 3rd argument and don't need this argument.
	 * @param string|array $toMethod Method from $toObject, or function name to call on a hook event.
	 *   This argument can be sustituted as the 2nd argument when the 2nd argument isn't needed,
	 *   or it can be the $options argument. 
	 * @param array $options Array of options that can modify behavior:
	 *  - `type` (string): May be either 'method' or 'property'. If property, then it will respond to $obj->property
	 *     rather than $obj->method(). The default type is 'method'.
	 *  - `priority` (int): A number determining the priority of a hook, where lower numbers are executed before
	 *     higher numbers. The default priority is 100.
	 * @return string A special Hook ID (or CSV string of hook IDs) that should be retained if you need to remove the hook later.
	 * @see https://processwire.com/docs/modules/hooks/
	 *
	 */
	public function addHookAfter($method, $toObject, $toMethod = null, $options = array()) {
		$options['after'] = true; 
		if(!isset($options['before'])) $options['before'] = false; 
		return $this->_wireHooks()->addHook($this, $method, $toObject, $toMethod, $options); 
	}

	/**
	 * Add a hook that will be accessible as a new object property. 
	 * 
	 * This enables you to add a new accessible property to an existing object, which will execute
	 * your hook implementation method when called upon. 
	 * 
	 * Note that adding a hook with this just makes it possible to call the hook as a property. 
	 * Any hook property you add can also be called as a method, i.e. `$obj->foo` and `$obj->foo()`
	 * are the same.
	 * 
	 * ~~~~~
	 * // Adding a hook property
	 * $wire->addHookProperty('Page::lastModifiedStr', function($event) {
	 *   $page = $event->object; 
	 *   $event->return = wireDate('relative', $page->modified); 
	 * });
	 * 
	 * // Accessing the property (from any instance)
	 * echo $page->lastModifiedStr; // outputs: "10 days ago"
	 * ~~~~~
	 * 
	 * #pw-group-hooks
	 *
	 * @param string|array $property Name of property you want to add, must not collide with existing property or method names:
	 *  - `Class::property` to add the property to all instances of Class. 
	 *  - `property` if just adding to a single object instance.
	 *  - Since 3.0.137 it may also be multiple properties to hook in CSV string or array. 
	 * @param object|null|callable $toObject Specify one of the following:
	 *  - Object instance to call `$toMethod` from (like `$this`).
	 *  - Inline function (closure) if providing implemention inline.
	 *  - Procedural function name, if hook is implemented by a procedural function.
	 *  - Null if you want to use the 3rd argument and don't need this argument.
	 * @param string|array $toMethod Method from $toObject, or function name to call on a hook event.
	 *   This argument can be sustituted as the 2nd argument when the 2nd argument isn’t needed,
	 *   or it can be the $options argument. 
	 * @param array $options Options typically aren't used in this context, but see Wire::addHookBefore() $options if you'd like.
	 * @return string A special Hook ID (or CSV string of hook IDs) that should be retained if you need to remove the hook later.
	 * @see https://processwire.com/docs/modules/hooks/
	 *
	 */
	public function addHookProperty($property, $toObject, $toMethod = null, $options = array()) {
		// This is the same as calling addHook with the 'type' option set to 'property' in the $options array. 
	    // Note that descending classes that override __get must call getHook($property) and/or runHook($property).
		$options['type'] = 'property'; 
		return $this->_wireHooks()->addHook($this, $property, $toObject, $toMethod, $options); 
	}
	
	/**
	 * Add a hook accessible as a new public method in a class (or object) 
	 * 
	 * - This enables you to add a new accessible public method to an existing object, which will execute
	 *   your hook implementation method when called upon. 
	 *   
	 * - Hook method can accept arguments and/or populate return values, just like any other regular method 
	 *   in the class. However, methods such as this do not have access to private or protected 
	 *   properties/methods in the class. 
	 *   
	 * - Methods added like this themselves become hookable as well. 
	 * 
	 * #pw-group-hooks
	 *
	 * ~~~~~
	 * // Adds a myHasParent($parent) method to all Page objects
	 * $wire->addHookMethod('Page::myHasParent', function($event) {
	 *   $page = $event->object;
	 *   $parent = $event->arguments(0); 
	 *   if(!$parent instanceof Page) {
	 *     throw new WireException("Page::myHasParent() requires a Page argument"); 
	 *   }
	 *   if($page->parents()->has($parent)) {
	 *     // this page has the given parent
	 *     $event->return = true; 
	 *   } else {
	 *     // does not have the given parent
	 *     $event->return = false; 
	 *   }
	 * });
	 *
	 * // Calling the new method (from any instance)
	 * $parent = $pages->get('/products/'); 
	 * if($page->myHasParent($parent)) {
	 *   // $page has the given $parent
	 * } 
	 * ~~~~~
	 *
	 * @param string $method Name of method you want to add, must not collide with existing property or method names:
	 *  - `Class::method` to add the method to all instances of Class.
	 *  - `method` to just add to a single object instance.
	 *  - Since 3.0.137 it may also be multiple methods to hook in CSV string or array. 
	 * @param object|null|callable $toObject Specify one of the following:
	 *  - Object instance to call `$toMethod` from (like `$this`).
	 *  - Inline function (closure) if providing implemention inline.
	 *  - Procedural function name, if hook is implemented by a procedural function.
	 *  - Null if you want to use the 3rd argument and don't need this argument.
	 * @param string|array $toMethod Method from $toObject, or function name to call on a hook event.
	 *   This argument can be sustituted as the 2nd argument when the 2nd argument isn’t needed, 
	 *   or it can be the $options argument. 
	 * @param array $options Options typically aren't used in this context, but see Wire::addHookBefore() $options if you'd like.
	 * @return string A special Hook ID (or CSV string of hook IDs) that should be retained if you need to remove the hook later.
	 * @since 3.0.16 Added as an alias to addHook() for syntactic clarity, previous versions can use addHook() method with same arguments. 
	 * @see https://processwire.com/docs/modules/hooks/
	 *
	 */
	public function addHookMethod($method, $toObject, $toMethod = null, $options = array()) {
		return $this->_wireHooks()->addHook($this, $method, $toObject, $toMethod, $options);
	}

	/**
	 * Given a Hook ID, remove the hook
	 * 
	 * Once a hook is removed, it will no longer execute. 
	 * 
	 * ~~~~~
	 * // Add a hook
	 * $hookID = $pages->addHookAfter('find', function($event) {
	 *   // do something
	 * });
	 * 
	 * // Remove the hook
	 * $pages->removeHook($hookID); 
	 * ~~~~~
	 * ~~~~~
	 * // Hook function that removes itself
	 * $hookID = $pages->addHookAfter('find', function($event) {
	 *   // do something
	 *   $event->removeHook(null); // note: calling removeHook on $event
	 * });
	 * ~~~~~
	 * 
	 * #pw-group-hooks
	 *
	 * @param string|array|null $hookId ID of hook to remove (ID is returned by the addHook() methods)
	 *  Since 3.0.137 it may also be an array or CSV string of hook IDs to remove.
	 * @return $this
	 *
	 */
	public function removeHook($hookId) {
		return $this->_wireHooks()->removeHook($this, $hookId);
	}

	
	/*******************************************************************************************************
	 * CHANGE TRACKING
	 *
	 */

	/**
	 * For setTrackChanges() method flags: track names only (default).
	 * 
	 * #pw-group-changes
	 *
	 */
	const trackChangesOn = 2;
	
	/**
	 * For setTrackChanges() method flags: track names and values.
	 * 
	 * #pw-group-changes
	 *
	 */
	const trackChangesValues = 4;
	
	/**
	 * Track changes mode
	 * 
	 * @var int Bitmask
	 *
	 */
	protected $trackChanges = 0;

	/**
	 * Array containing the names of properties (as array keys) that were changed while change tracking was ON.
	 * 
	 * Array values are insignificant unless trackChangeMode is trackChangesValues (1), in which case the values are the previous values.
	 * 
	 * @var array
	 *
	 */
	private $changes = array();
	
	/**
	 * Does the object have changes, or has the given property changed? 
	 *
	 * Applicable only when object has change tracking enabled. 
	 * 
	 * ~~~~~
	 * // Check if page has changed
	 * if($page->isChanged()) {
	 *   // Page has changes
	 * }
	 * 
	 * // Check if the page title field has changed
	 * if($page->isChanged('title')) {
	 *   // The title has changed
	 * }
	 * ~~~~~
	 * 
	 * #pw-group-changes
	 *
	 * @param string $what Name of property, or if left blank, checks if any properties have changed. 
	 * @return bool True if property has changed, false if not. 
	 *
	 */
	public function isChanged($what = '') {
		if(!$what) return count($this->changes) > 0; 
		return array_key_exists($what, $this->changes); 
	}

	/**
	 * Hookable method that is called whenever a property has changed while change tracking is enabled. 
	 *
	 * - Enables hooks to monitor changes to the object. 
	 * - Do not call this method directly, as the `Wire::trackChange()` method already does so. 
	 * - Descending classes should call `$this->trackChange('name', $oldValue, $newValue);` when a property they are tracking has changed.
	 *
	 * #pw-group-hooker
	 * 
	 * @param string $what Name of property that changed
	 * @param mixed $old Previous value before change 
	 * @param mixed $new New value
	 * @see Wire::trackChange()
	 *
	 */
	public function ___changed($what, $old = null, $new = null) {
		// for hooks to listen to 
	}

	/**
	 * Track a change to a property in this object
	 *
	 * The change will only be recorded if change tracking is enabled for this object instance. 
	 * 
	 * #pw-group-changes
	 *
	 * @param string $what Name of property that changed
	 * @param mixed $old Previous value before change
	 * @param mixed $new New value
	 * @return $this
	 * 
	 */
	public function trackChange($what, $old = null, $new = null) {
		
		if($this->trackChanges & self::trackChangesOn) {
			
			// establish it as changed
			if(array_key_exists($what, $this->changes)) {
				// remember last value so we can avoid duplication in hooks or storage
				$lastValue = end($this->changes[$what]); 
			} else {
				$lastValue = null;
				$this->changes[$what] = array();
			}
		
			if(is_null($old) || is_null($new) || $lastValue !== $new) {
				/** @var WireHooks $hooks */
				$hooks = $this->_wireHooks();
				if(($hooks && $hooks->isHooked('changed()')) || !$hooks) {
					$this->changed($what, $old, $new); // triggers ___changed hook
				} else {
					$this->___changed($what, $old, $new); 
				}
			}
			
			if($this->trackChanges & self::trackChangesValues) {
				// track changed values, but avoid successive duplication of same value
				if(is_object($old) && $old === $new) $old = clone $old; // keep separate copy of objects for old value
				if($lastValue !== $old || !count($this->changes[$what])) $this->changes[$what][] = $old; 
				
			} else {
				// don't track changed values, just names of fields
				$this->changes[$what][] = null;
			}
			
		}
		
		return $this; 
	}

	/**
	 * Untrack a change to a property in this object
	 * 
	 * #pw-group-changes
	 *
	 * @param string $what Name of property that you want to remove its change being tracked
	 * @return $this
	 * 
	 */
	public function untrackChange($what) {
		unset($this->changes[$what]); 
		return $this; 
	}

	/**
	 * Turn change tracking ON or OFF
	 * 
	 * ~~~~~
	 * // Enable change tracking
	 * $page->setTrackChanges(true);
	 * 
	 * // Disable change tracking
	 * $page->setTrackChanges(false);
	 * 
	 * // Enable change tracking and remember values
	 * $page->setTrackChanges(Wire::trackChangesValues); 
	 * $page->setTrackChanges(true);
	 * ~~~~~
	 * 
	 * #pw-group-changes
	 *
	 * @param bool|int $trackChanges Specify one of the following: 
	 *   - `true` (bool): Enables change tracking. 
	 *   - `false` (bool): Disables change tracking
	 *   - `Wire::trackChangesOn` (constant): Enables change tracking (same as specifying boolean true).
	 *   - `Wire::trackChangesValues` (constant): Enables tracking of changed values when change tracking is already on. 
	 *     This uses more memory since it keeps previous values, so it is not enabled by default. Once enabled, the 
	 *     setting will persist through boolean true|false arguments. 
	 * @return $this
	 *
	 */
	public function setTrackChanges($trackChanges = true) {
		if(is_bool($trackChanges) || !$trackChanges) {
			// turn change track on or off
			if($trackChanges) {
				$this->trackChanges = $this->trackChanges | self::trackChangesOn; // add bit
			} else {
				$this->trackChanges = $this->trackChanges & ~self::trackChangesOn; // remove bit
			}
		} else if(is_int($trackChanges)) {
			// set bitmask
			$allowed = array(
				self::trackChangesOn, 
				self::trackChangesValues, 
				self::trackChangesOn | self::trackChangesValues
			); 
			if(in_array($trackChanges, $allowed)) $this->trackChanges = $trackChanges; 
		}
		return $this; 
	}

	/**
	 * Returns true or 1 if change tracking is on, or false or 0 if it is not, or mode bitmask (int) if requested. 
	 * 
	 * #pw-group-changes
	 *
	 * @param bool $getMode When true, the track changes mode bitmask will be returned 
	 * @return int 0/false if off, 1/true if On, or mode bitmask if requested 
	 * 
	 */
	public function trackChanges($getMode = false) {
		if($getMode) return $this->trackChanges; 
		return $this->trackChanges & self::trackChangesOn;	
	}

	/**
	 * Clears out any tracked changes and turns change tracking ON or OFF
	 * 
	 * ~~~~
	 * // Clear any changes that have been tracked and start fresh
	 * $page->resetTrackChanges();
	 * ~~~~
	 * 
	 * #pw-group-changes
	 *
	 * @param bool $trackChanges True to turn change tracking ON, or false to turn OFF. Default of true is assumed.
	 * @return $this
	 *
	 */
	public function resetTrackChanges($trackChanges = true) {
		$this->changes = array();
		return $this->setTrackChanges($trackChanges); 
	}

	/**
	 * Return an array of properties that have changed while change tracking was on. 
	 * 
	 * ~~~~~
	 * // Get an array of changed field names
	 * $changes = $page->getChanges();
	 * ~~~~~
	 * 
	 * #pw-group-changes
	 *
	 * @param bool $getValues Specify one of the following, or omit for default setting. 
	 *  - `false` (bool): return array of changed property names (default setting).
	 *  - `true` (bool): return an associative array containing an array of previous values, indexed by 
	 *     property name, oldest to newest. Requires Wire::trackChangesValues mode to be enabled. 
	 *  - `2` (int): Return array where both keys and values are changed property names. 
	 * @return array
	 *
	 */
	public function getChanges($getValues = false) {
		if($getValues === 2) {
			$changes = array();
			foreach($this->changes as $name => $value) {
				if($value) {} // value ignored
				$changes[$name] = $name;
			}
			return $changes;
		} else if($getValues) {
			return $this->changes;
		} else {
			return array_keys($this->changes);
		}
	}

	
	/*******************************************************************************************************
	 * NOTICES AND LOGS
	 *
	 */

	/**
	 * @var Notices[]
	 * 
	 */
	protected $_notices = array(
		'errors' => null, 
		'warnings' => null, 
		'messages' => null
	);

	/**
	 * Record a Notice, internal use (contains the code for message, warning and error methods)
	 * 
	 * @param string|array|Wire $text Title of notice
	 * @param int|string $flags Flags bitmask or space separated string of flag names
	 * @param string $name Name of container
	 * @param string $class Name of Notice class
	 * @return $this
	 * 
	 */
	protected function _notice($text, $flags, $name, $class) {
		if($flags === true) $flags = Notice::log;
		$class = wireClassName($class, true);
		$notice = $this->wire(new $class($text, $flags)); /** @var Notice $notice */
		$notice->class = $this->className();
		if($this->_notices[$name] === null) $this->_notices[$name] = $this->wire(new Notices());
		$notices = $this->wire()->notices;
		if($notices) $notices->add($notice); // system wide
		if(!($notice->flags & Notice::logOnly)) $this->_notices[$name]->add($notice); // local only
		return $this; 
	}

	/**
	 * Record an informational or “success” message in the system-wide notices. 
	 *
	 * This method automatically identifies the message as coming from this class.
	 * 
	 * ~~~~~
	 * $this->message("This is the notice text");
	 * $this->message("This notice is also logged", true);
	 * $this->message("This notice is only shown in debug mode", Notice::debug);
	 * $this->message("This notice allows <em>markup</em>", Notice::allowMarkup);
	 * $this->message("Notice using multiple flags", Notice::debug | Notice::logOnly);
	 * ~~~~~
	 * 
	 * #pw-group-notices
	 *
	 * @param string|array|Wire $text Text to include in the notice
	 * @param int|bool|string $flags Optional flags to alter default behavior:
	 *  - `Notice::admin` (constant): Show notice only if user is in the admin.
	 *  - `Notice::allowMarkdown` (constant): Allow basic markdown and bracket markup (see $sanitizer->entitiesMarkdown()).
	 *  - `Notice::allowMarkup` (constant): Indicates notice should allow the use of HTML markup tags.
	 *  - `Notice::debug` (constant): Indicates notice should only be shown when debug mode is active.
	 *  - `Notice::log` (constant): Indicates notice should also be logged.
	 *  - `Notice::logOnly` (constant): Indicates notice should only be logged.
	 *  - `Notice::login` (constant): Show notice only if it will be seen by a logged-in user.
	 *  - `Notice::noGroup` (constant): Indicates notice should not group with others of the same type (where supported).
	 *  - `Notice::prepend` (constant): Indicates notice should prepend rather than append.
	 *  - `Notice::superuser` (constant): Show notice only if current user is a superuser.
	 *  - `true` (boolean): Shortcut for the `Notice::log` constant.
	 *  - In 3.0.149+ you may also specify a space-separated string of flag names, i.e. "admin log noGroup". 
	 * @return $this
	 * @see Wire::messages(), Wire::warning(), Wire::error()
	 *
	 */
	public function message($text, $flags = 0) {
		return $this->_notice($text, $flags, 'messages', 'NoticeMessage'); 
	}
	
	/**
	 * Record a warning error message in the system-wide notices.
	 *
	 * This method automatically identifies the warning as coming from this class.
	 * 
	 * ~~~~~
	 * $this->warning("This is the notice text");
	 * $this->warning("This notice is also logged", true);
	 * $this->warning("This notice is only shown in debug mode", Notice::debug);
	 * $this->warning("This notice allows <em>markup</em>", Notice::allowMarkup);
	 * $this->warning("Notice using multiple flags", Notice::debug | Notice::logOnly);
	 * ~~~~~
	 * 
	 * #pw-group-notices
	 *
	 * @param string|array|Wire $text Text to include in the notice
	 * @param int|bool|string $flags Optional flags to alter default behavior:
	 *  - `Notice::admin` (constant): Show notice only if user is in the admin.
	 *  - `Notice::allowMarkdown` (constant): Allow basic markdown and bracket markup (see $sanitizer->entitiesMarkdown()).
	 *  - `Notice::allowMarkup` (constant): Indicates notice should allow the use of HTML markup tags.
	 *  - `Notice::debug` (constant): Indicates notice should only be shown when debug mode is active.
	 *  - `Notice::log` (constant): Indicates notice should also be logged.
	 *  - `Notice::logOnly` (constant): Indicates notice should only be logged.
	 *  - `Notice::login` (constant): Show notice only if it will be seen by a logged-in user.
	 *  - `Notice::noGroup` (constant): Indicates notice should not group with others of the same type (where supported).
	 *  - `Notice::prepend` (constant): Indicates notice should prepend rather than append.
	 *  - `Notice::superuser` (constant): Show notice only if current user is a superuser.
	 *  - `true` (boolean): Shortcut for the `Notice::log` constant.
	 *  - In 3.0.149+ you may also specify a space-separated string of flag names, i.e. "admin log noGroup". 
	 * @return $this
	 * @see Wire::warnings(), Wire::message(), Wire::error()
	 *
	 *
	 */
	public function warning($text, $flags = 0) {
		return $this->_notice($text, $flags, 'warnings', 'NoticeWarning'); 
	}

	/**
	 * Record an non-fatal error message in the system-wide notices. 
	 *
	 * - This method automatically identifies the error as coming from this class. 
	 * - You should still make fatal errors throw a `WireException` (or class derived from it).
	 * 
	 * ~~~~~
	 * $this->error("This is the notice text"); 
	 * $this->error("This notice is also logged", true);
	 * $this->error("This notice is only shown in debug mode", Notice::debug);
	 * $this->error("This notice allows <em>markup</em>", Notice::allowMarkup);
	 * $this->error("Notice using multiple flags", Notice::debug | Notice::logOnly);
	 * ~~~~~
	 * 
	 * #pw-group-notices
	 *
	 * @param string|array|Wire $text Text to include in the notice
	 * @param int|bool|string $flags Optional flags to alter default behavior:
	 *  - `Notice::admin` (constant): Show notice only if user is in the admin.
	 *  - `Notice::allowMarkdown` (constant): Allow basic markdown and bracket markup (see $sanitizer->entitiesMarkdown()).
	 *  - `Notice::allowMarkup` (constant): Indicates notice should allow the use of HTML markup tags.
	 *  - `Notice::debug` (constant): Indicates notice should only be shown when debug mode is active.
	 *  - `Notice::log` (constant): Indicates notice should also be logged.
	 *  - `Notice::logOnly` (constant): Indicates notice should only be logged.
	 *  - `Notice::login` (constant): Show notice only if it will be seen by a logged-in user.
	 *  - `Notice::noGroup` (constant): Indicates notice should not group with others of the same type (where supported).
	 *  - `Notice::prepend` (constant): Indicates notice should prepend rather than append.
	 *  - `Notice::superuser` (constant): Show notice only if current user is a superuser.
	 *  - `true` (boolean): Shortcut for the `Notice::log` constant.
	 *  - In 3.0.149+ you may also specify a space-separated string of flag names, i.e. "admin log noGroup". 
	 * @return $this
	 * @see Wire::errors(), Wire::message(), Wire::warning()
	 *
	 */
	public function error($text, $flags = 0) {
		return $this->_notice($text, $flags, 'errors', 'NoticeError'); 
	}

	/**
	 * Hookable method called when an Exception (or Error) occurs
	 * 
	 * - It will log Exception to `exceptions.txt` log if 'exceptions' is in `$config->logs`.
	 * - It will log Error to `errors.txt` log if 'errors' is in `$config->logs`.
	 * - It will re-throw Exception or Error if `$config->allowExceptions` is true. 
	 * - If additional `$text` is provided, it will be sent to notice method call. 
	 * 
	 * Please note that if your root /index.php version is less than 302 it will only receive 
	 * Exception (and not Error) objects. 
	 * 
	 * #pw-hooker
	 * 
	 * @param \Throwable $e Exception or Error object that was thrown. 
	 * @param bool|int $severe Whether or not it should be considered severe (default=true).
	 * @param string|array|object|true $text Additional details (optional):
	 * 	- When provided, it will be sent to `$this->error($text)` if $severe is true, or `$this->warning($text)` if $severe is false.
	 * 	- Specify boolean `true` to just send the `$e->getMessage()` to `$this->error()` or `$this->warning()`. 
	 * @return $this
	 * @throws \Exception|\Error If `$severe==true` and `$config->allowExceptions==true`
	 * 
	 */
	public function ___trackException($e, $severe = true, $text = null) {
		$config = $this->wire()->config;
		$log = $this->wire()->log;
		$msg = $e->getMessage();
		if($text !== null) {
			if($text === true) $text = $msg;
			$severe ? $this->error($text) : $this->warning($text);
			if(strlen($msg) && strpos($text, $msg) === false) $msg = "$text - $msg";
		}
		$type = $e instanceof \Exception ? 'exceptions' : 'errors';
		if(in_array($type, $config->logs) && $log) {
			$msg .= ' (' . str_replace($config->paths->root, '/', $e->getFile()) . ':' . $e->getLine() . ')';
			$log->save($type, $msg);
		}
		if($severe && $config->allowExceptions) {
			throw $e; // re-throw, if requested
		}
		return $this;
	}

	/**
	 * Return or manage errors recorded by just this object or all Wire objects
	 * 
	 * This method returns and manages errors that were previously set by `Wire::error()`. 
	 * 
	 * ~~~~~
	 * // Get errors for one object
	 * $errors = $obj->errors();
	 * 
	 * // Get first error in object
	 * $error = $obj->errors('first');
	 * 
	 * // Get errors for all Wire objects
	 * $errors = $obj->errors('all'); 
	 * 
	 * // Get and clear all errors for all Wire objects
	 * $errors = $obj->errors('clear all'); 
	 * ~~~~~
	 * 
	 * #pw-group-notices
	 * 
	 * @param string|array $options One or more of array elements or space separated string of:
	 * 	- `first` - only first item will be returned 
	 * 	- `last` - only last item will be returned 
	 * 	- `all` - include all errors, including those beyond the scope of this object
	 * 	- `clear` - clear out all items that are returned from this method
	 * 	- `array` - return an array of strings rather than series of Notice objects.
	 * 	- `string` - return a newline separated string rather than array/Notice objects. 
	 * @return Notices|array|string Array of `NoticeError` errors, or string if last, first or str option was specified.
	 * 
	 */
	public function errors($options = array()) {
		if(!is_array($options)) $options = explode(' ', strtolower($options)); 
		$options[] = 'errors';
		return $this->messages($options); 
	}

	/**
	 * Return or manage warnings recorded by just this object or all Wire objects
	 * 
	 * This method returns and manages warnings that were previously set by `Wire::warning()`. 
	 * 
	 * ~~~~~
	 * // Get warnings for one object
	 * $warnings = $obj->warnings();
	 *
	 * // Get first warning in object
	 * $warning = $obj->warnings('first');
	 *
	 * // Get warnings for all Wire objects
	 * $warnings = $obj->warnings('all');
	 *
	 * // Get and clear all warnings for all Wire objects
	 * $warnings = $obj->warnings('clear all');
	 * ~~~~~
	 * 
	 * #pw-group-notices
	 *
	 * @param string|array $options One or more of array elements or space separated string of:
	 * 	- `first` - only first item will be returned
	 * 	- `last` - only last item will be returned
	 * 	- `all` - include all errors, including those beyond the scope of this object
	 * 	- `clear` - clear out all items that are returned from this method
	 * 	- `array` - return an array of strings rather than series of Notice objects.
	 * 	- `string` - return a newline separated string rather than array/Notice objects.
	 * @return Notices|array|string Array of `NoticeWarning` warnings, or string if last, first or str option was specified.
	 * 
	 *
	 */
	public function warnings($options = array()) {
		if(!is_array($options)) $options = explode(' ', strtolower($options));
		$options[] = 'warnings';
		return $this->messages($options); 
	}

	/**
	 * Return or manage messages recorded by just this object or all Wire objects
	 * 
	 * This method returns and manages messages that were previously set by `Wire::message()`. 
	 *
	 * ~~~~~
	 * // Get messages for one object
	 * $messages = $obj->messages();
	 *
	 * // Get first message in object
	 * $message = $obj->messages('first');
	 *
	 * // Get messages for all Wire objects
	 * $messages = $obj->messages('all');
	 *
	 * // Get and clear all messages for all Wire objects
	 * $messages = $obj->messages('clear all');
	 * ~~~~~
	 * 
	 * #pw-group-notices
	 *
	 * @param string|array $options One or more of array elements or space separated string of:
	 * 	- `first` - only first item will be returned
	 * 	- `last` - only last item will be returned
	 * 	- `all` - include all messages, including those beyond the scope of this object
	 * 	- `clear` - clear out all items that are returned from this method
	 * 	- `array` - return an array of strings rather than series of Notice objects.
	 * 	- `string` - return a newline separated string rather than array/Notice objects.
	 * @return Notices|array|string Array of `NoticeMessage` messages, or string if last, first or str option was specified.
	 *
	 */
	public function messages($options = array()) {
		if(!is_array($options)) $options = explode(' ', strtolower($options)); 
		if(in_array('errors', $options)) {
			$type = 'errors';
		} else if(in_array('warnings', $options)) {
			$type = 'warnings';
		} else {
			$type = 'messages';
		}
		$clear = in_array('clear', $options); 
		if(in_array('all', $options)) {
			// get all of either messages, warnings or errors (either in or out of this object instance)
			$notices = $this->wire()->notices; 
			$value = $this->wire(new Notices()); /** @var Notices $value */
			foreach($notices as $notice) {
				if($notice->getName() != $type) continue;
				$value->add($notice);
				if($clear) $notices->remove($notice); // clear global
			}
			if($clear) $this->_notices[$type] = null; // clear local
		} else {
			// get messages, warnings or errors specific to this object instance
			/** @var Notices $value */
			$value = $this->_notices[$type] === null ? $this->wire(new Notices()) : $this->_notices[$type];
			if(in_array('first', $options)) {
				$value = $clear ? $value->shift() : $value->first();
			} else if(in_array('last', $options)) {
				$value = $clear ? $value->pop() : $value->last();
			} else if($clear) {
				$this->_notices[$type] = null;
			}
			if($clear && $value) {
				$this->wire()->notices->removeItems($value); // clear from global notices
			}
		}
		if(in_array('array', $options) || in_array('string', $options)) {
			if($value instanceof Notice) {
				$value = array($value->text);
			} else {
				$_value = array();
				foreach($value as $notice) {
					/** @var Notice $notice */
					$_value[] = $notice->text;
				}
				$value = $_value; 
			}
			if(in_array('string', $options)) {
				$value = implode("\n", $value); 
			}
		}
		return $value; 
	}

	/**
	 * Log a message for this class
	 * 
	 * Message is saved to a log file in ProcessWire's logs path to a file with 
	 * the same name as the class, converted to hyphenated lowercase. For example, 
	 * a class named `MyWidgetData` would have a log named `my-widget-data.txt`.
	 * 
	 * ~~~~~
	 * $this->log("This message will be logged"); 
	 * ~~~~~
	 * 
	 * #pw-group-notices
	 * 
	 * @param string $str Text to log, or omit to return the `$log` API variable.
	 * @param array $options Optional extras to include: 
	 *  - `url` (string): URL to record the with the log entry (default=auto-detect)
	 *  - `name` (string): Name of log to use (default=auto-detect)
	 *  - `user` (User|string|null): User instance, user name, or null to log for current User. (default=null)
	 * @return WireLog
	 *
	 */
	public function ___log($str = '', array $options = array()) {
		$log = $this->wire()->log;
		if($log && strlen($str)) {
			if(isset($options['name'])) {
				$name = $options['name'];
				unset($options['name']);
			} else {
				$name = $this->className(array('lowercase' => true));
			}
			$log->save($name, $str, $options);
		}
		return $log; 
	}
	
	/*******************************************************************************************************
	 * TRANSLATION 
	 * 
	 */

	/**
	 * Translate the given text string into the current language if available. 
	 *
	 * If not available, or if the current language is the native language, then it returns the text as is. 
	 * 
	 * #pw-group-translation
	 *
	 * @param string|array $text Text string to translate (or array in 3.0.151 also supported)
	 * @return string
	 *
	 */
	public function _($text) {
		return __($text, $this); 
	}

	/**
	 * Perform a language translation in a specific context
	 * 
	 * Used when to text strings might be the same in English, but different in other languages. 
	 * 
	 * #pw-group-translation
	 * 
	 * @param string|array $text Text for translation. 
	 * @param string $context Name of context
	 * @return string Translated text or original text if translation not available.
	 *
	 */
	public function _x($text, $context) {
		return _x($text, $context, $this); 
	}

	/**
	 * Perform a language translation with singular and plural versions
	 * 
	 * #pw-group-translation
	 * 
	 * @param string $textSingular Singular version of text (when there is 1 item).
	 * @param string $textPlural Plural version of text (when there are multiple items or 0 items).
	 * @param int $count Quantity used to determine whether singular or plural.
	 * @return string Translated text or original text if translation not available.
	 *
	 */
	public function _n($textSingular, $textPlural, $count) {
		return _n($textSingular, $textPlural, $count, $this); 
	}
	
	/*******************************************************************************************************
	 * API VARIABLE MANAGEMENT
	 * 
	 * To replace fuel in PW 3.0
	 *
	 */

	/**
	 * ProcessWire instance
	 *
	 * @var ProcessWire|bool|null
	 *
	 */
	protected $_wire = null;

	/**
	 * Set the current ProcessWire instance for this object (PW 3.0)
	 * 
	 * #pw-internal
	 *
	 * @param ProcessWire $wire
	 *
	 */
	public function setWire(ProcessWire $wire) {
		$wired = $this->_wire;
		if($wired === $wire) return;
		$this->_wire = $wire;
		if($this->_wireHooks) $this->_wireHooks = $wire->wire()->hooks;
		if($wired) return;
		$this->getInstanceNum();
		$this->wired();
	}

	/**
	 * Get the current ProcessWire instance (PW 3.0)
	 * 
	 * You can also use the wire() method with no arguments.
	 * 
	 * #pw-internal
	 *
	 * @return null|ProcessWire
	 *
	 */
	public function getWire() {
		return $this->_wire ? $this->_wire : null;
	}

	/**
	 * Is this object wired to a ProcessWire instance?
	 * 
	 * #pw-internal
	 * 
	 * @return bool
	 * 
	 */
	public function isWired() {
		return $this->_wire ? true : false;
	}
	
	/**
	 * Get an API variable, create an API variable, or inject dependencies.
	 * 
	 * This method provides the following:
	 * 
	 * - Access to API variables:   
	 *   `$pages = $this->wire('pages');`
	 *   
	 * - Access to current ProcessWire instance:   
	 *   `$wire = $this->wire();`
	 *   
	 * - Creating new API variables:   
	 *   `$this->wire('widgets', $widgets);`
	 *   
	 * - Injection of dependencies to Wire derived objects:   
	 *   `$this->wire($widgets);`
	 * 
	 * Most Wire derived objects also support access to API variables directly via `$this->apiVar`. 
	 * 
	 * There is also the `wire()` procedural function, which provides the same access to get API 
	 * variables. Note however the procedural version does not support creating API variables or 
	 * injection of dependencies. 
	 * 
	 * ~~~~~
	 * // Get the 'pages' API variable
	 * $pages = $this->wire('pages');
	 *   
	 * // Get the 'pages' API variable using alternate syntax
	 * $pages = $this->wire()->pages; 
	 *  
	 * // Get all API variables (returns a Fuel object)
	 * $all = $this->wire('all');
	 *   
	 * // Get the current ProcessWire instance (no arguments)
	 * $wire = $this->wire(); 
	 *  
	 * // Create a new API variable named 'widgets'
	 * $this->wire('widgets', $widgets);
	 *  
	 * // Create new API variable and lock it so nothing can overwrite 
	 * $this->wire('widgets', $widgets, true); 
	 *   
	 * // Alternate syntax for the two above
	 * $this->wire()->set('widgets', $widgets);
	 * $this->wire()->set('widgets', $widgets, true); // lock 
	 *   
	 * // Inject dependencies into Wire derived object
	 * $this->wire($widgets); 
	 *   
	 * // Inject dependencies during construct
	 * $newPage = $this->wire(new Page());
	 * ~~~~~
	 *
	 * @param string|object $name Name of API variable to retrieve, set, or omit to retrieve the master ProcessWire object.
	 * @param null|mixed $value Value to set if using this as a setter, otherwise omit.
	 * @param bool $lock When using as a setter, specify true if you want to lock the value from future changes (default=false).
	 * @return ProcessWire|Wire|Session|Page|Pages|Modules|User|Users|Roles|Permissions|Templates|Fields|Fieldtypes|Sanitizer|Config|Notices|WireDatabasePDO|WireHooks|WireDateTime|WireFileTools|WireMailTools|WireInput|PagesVersions|string|mixed
	 * @throws WireException
	 *
	 *
	 */
	public function wire($name = '', $value = null, $lock = false) {

		if($this->_wire) {
			// this instance is wired
			$wire = $this->_wire;
			// quick exit when _wire already set and not getting/setting API var
			if($name === '') return $wire;
		} else {
			// this object has not yet been wired! use last known current instance as fallback
			// note this condition is unsafe in multi-instance mode
			$wire = ProcessWire::getCurrentInstance();
			if(!$wire) return is_object($name) ? $name : null; // there are no ProcessWire instances
			if($name && $this->_wire === null) {
				$this->_wire = false; // false prevents this from being called another time for this object
				$wire->_objectNotWired($this, $name, $value);
			}
		}

		if(is_object($name)) {
			// make an object wired (inject ProcessWire instance to object)
			if($name instanceof WireFuelable) {
				if($this->_wire) $name->setWire($wire); // inject fuel, PW 3.0 
				if(is_string($value) && $value) {
					// set as new API var if API var name specified in $value
					$wire->fuel()->set($value, $name, $lock);
				}
				$value = $name; // return the provided instance
			} else {
				throw new WireException('Wire::wire($o) expected WireFuelable for $o and was given ' . get_class($name));
			}

		} else if($value !== null) {
			// setting a API variable/fuel value, and make it wired
			if($value instanceof WireFuelable && $this->_wire) $value->setWire($wire);
			$wire->fuel()->set($name, $value, $lock);
			
		} else if(empty($name)) {
			// return ProcessWire instance
			$value = $wire;
			
		} else if($name === '*' || $name === 'all' || $name == 'fuel') {
			// return Fuel instance
			$value = $wire->fuel();
			
		} else {
			// get API variable
			$value = $wire->fuel()->$name;
		}
		
		return $value;
	}

	/**
	 * Initialization called when object injected with ProcessWire instance (aka “wired”)
	 *
	 * - Can be used for any constructor-type initialization that depends on API vars.
	 * - Called automatically when object is “wired”, do not call it on your own.
	 * - Expects to be called only once per object instance.
	 * - Typically called after `__construct()` but before any other method calls.
	 * - Please note: If object is never “wired” then this method will not be called!
	 *
	 * ~~~~~
	 * class Test extends Wire {
	 *   function wired() { 
	 *     echo "Wired to ProcessWire instance: "; 
	 *     echo $this->wire()->getProcessWireInstanceID();
	 *   }
	 * }
	 *
	 * // objects in ProcessWire are “wired” like this:
	 * $o = new Test();
	 * $this->wire($o); // outputs "ProcessWire instance: n"
	 *
	 * // or on one line, like this…
	 * $this->wire(new Test()); // outputs "ProcessWire instance: n"
	 * ~~~~~
	 *
	 * #pw-internal
	 *
	 * @since 3.0.158
	 *
	 */
	public function wired() { }

	/**
	 * Get an object property by direct reference or NULL if it doesn't exist
	 *
	 * If not overridden, this is primarily used as a shortcut for the fuel() method.
	 *
	 * Descending classes may have their own __get() but must pass control to this one when they can't find something.
	 *
	 * @param string $name
	 * @return mixed|null
	 *
	 */
	public function __get($name) {

		if($name === 'wire') return $this->wire();
		if($name === 'fuel') return $this->wire('fuel');
		if($name === 'className') return $this->className();

		if($this->useFuel()) {
			$value = $this->wire($name);
			if($value !== null) return $value; 
		}

		$hooks = $this->_wireHooks(); /** @var WireHooks $hooks */
		if($hooks && $hooks->isHooked($name)) { // potential property hook
			$result = $hooks->runHooks($this, $name, array(), 'property');
			return $result['return'];
		}

		return null;
	}

	/**
	 * debugInfo PHP 5.6+ magic method
	 *
	 * This is used when you print_r() an object instance.
	 *
	 * @return array
	 *
	 */
	public function __debugInfo() {
		/** @var WireDebugInfo $debugInfo */
		require_once(__DIR__ . '/WireDebugInfo.php');
		$debugInfo = $this->wire(new WireDebugInfo());
		return $debugInfo->getDebugInfo($this, true);
	}

	/**
	 * Minimal/small debug info
	 * 
	 * Same as __debugInfo() but with no hooks info, no change tracking info, and less verbose 
	 * 
	 * #pw-internal
	 * 
	 * @return array
	 * @since 3.0.130
	 * 
	 */
	public function debugInfoSmall() {
		/** @var WireDebugInfo $debugInfo */
		$debugInfo = $this->wire(new WireDebugInfo());
		return $debugInfo->getDebugInfo($this, true);
	}


}
