<?php
/*********************************************************************************
 *
 * TimeTrex is a Workforce Management program developed by
 * TimeTrex Software Inc. Copyright (C) 2003 - 2021 TimeTrex Software Inc.
 *
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Affero General Public License version 3 as published by
 * the Free Software Foundation with the addition of the following permission
 * added to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED
 * WORK IN WHICH THE COPYRIGHT IS OWNED BY TIMETREX, TIMETREX DISCLAIMS THE
 * WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more
 * details.
 *
 *
 * You should have received a copy of the GNU Affero General Public License along
 * with this program; if not, see http://www.gnu.org/licenses or write to the Free
 * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301 USA.
 *
 *
 * You can contact TimeTrex headquarters at Unit 22 - 2475 Dobbin Rd. Suite
 * #292 West Kelowna, BC V4T 2E9, Canada or at email address info@timetrex.com.
 *
 *
 * The interactive user interfaces in modified source and object code versions
 * of this program must display Appropriate Legal Notices, as required under
 * Section 5 of the GNU Affero General Public License version 3.
 *
 *
 * In accordance with Section 7(b) of the GNU Affero General Public License
 * version 3, these Appropriate Legal Notices must retain the display of the
 * "Powered by TimeTrex" logo. If the display of the logo is not reasonably
 * feasible for technical reasons, the Appropriate Legal Notices must display
 * the words "Powered by TimeTrex".
 *
 ********************************************************************************/


/**
 * @package Core
 */
class AuthorizationFactory extends Factory {
	protected $table = 'authorizations';
	protected $pk_sequence_name = 'authorizations_id_seq'; //PK Sequence name

	protected $obj_handler = null;
	protected $obj_handler_obj = null;
	protected $hierarchy_arr = null;

	/**
	 * @param $name
	 * @param null $parent
	 * @return array|null
	 */
	function _getFactoryOptions( $name, $parent = null ) {

		$retval = null;
		switch ( $name ) {
			case 'object_type':
				$retval = [
					//10 => 'default_schedule',
					//20 => 'schedule_amendment',
					//30 => 'shift_amendment',
					//40 => 'pay_stub_amendment',

					//52 => 'request_vacation',
					//54 => 'request_missed_punch',
					//56 => 'request_edit_punch',
					//58 => 'request_absence',
					//59 => 'request_schedule',
					90 => 'timesheet',

					200  => 'expense',

					//50 => 'request', //request_other
					1010 => 'request_punch',
					1020 => 'request_punch_adjust',
					1030 => 'request_absence',
					1040 => 'request_schedule',
					1100 => 'request_other',
				];
				break;
			case 'columns':
				$retval = [

						'-1010-created_by'   => TTi18n::gettext( 'Name' ),
						'-1020-created_date' => TTi18n::gettext( 'Date' ),
						'-1030-authorized'   => TTi18n::gettext( 'Authorized' ),
						//'-1100-object_type' => TTi18n::gettext('Object Type'),

						//'-2020-updated_by' => TTi18n::gettext('Updated By'),
						//'-2030-updated_date' => TTi18n::gettext('Updated Date'),
				];
				break;
			case 'list_columns':
				$retval = Misc::arrayIntersectByKey( $this->getOptions( 'default_display_columns' ), Misc::trimSortPrefix( $this->getOptions( 'columns' ) ) );
				break;
			case 'default_display_columns': //Columns that are displayed by default.
				$retval = [
						'created_by',
						'created_date',
						'authorized',
				];
				break;
		}

		return $retval;
	}

	/**
	 * @param $data
	 * @return array
	 */
	function _getVariableToFunctionMap( $data ) {
		$variable_function_map = [
				'id'             => 'ID',
				'object_type_id' => 'ObjectType',
				'object_type'    => false,
				'object_id'      => 'Object',
				'authorized'     => 'Authorized',
				'deleted'        => 'Deleted',
		];

		return $variable_function_map;
	}

	/**
	 * @return bool
	 */
	function getCurrentUserObject() {
		return $this->getGenericObject( 'UserListFactory', $this->getCurrentUser(), 'user_obj' );
	}

	/**
	 * Stores the current user in memory, so we can determine if its the employee verifying, or a superior.
	 * @return mixed
	 */
	function getCurrentUser() {
		return $this->getGenericTempDataValue( 'current_user_id' );
	}

	/**
	 * @param string $value UUID
	 * @return bool
	 */
	function setCurrentUser( $value ) {
		$value = trim( $value );

		return $this->setGenericTempDataValue( 'current_user_id', $value );
	}


	/**
	 * @return array|bool|null
	 */
	function getHierarchyArray() {
		if ( is_array( $this->hierarchy_arr ) ) {
			return $this->hierarchy_arr;
		} else {
			$user_id = $this->getCurrentUser();

			if ( is_object( $this->getObjectHandler() ) ) {
				$this->getObjectHandler()->getByID( $this->getObject() );
				$current_obj = $this->getObjectHandler()->getCurrent();
				$object_user_id = $current_obj->getUser();

				if ( TTUUID::isUUID( $object_user_id ) && $object_user_id != TTUUID::getZeroID() && $object_user_id != TTUUID::getNotExistID() ) {
					$ulf = TTnew( 'UserListFactory' ); /** @var UserListFactory $ulf */
					$company_id = $ulf->getById( $object_user_id )->getCurrent()->getCompany();
					Debug::Text( ' Authorizing User ID: ' . $user_id .' Object User ID: ' . $object_user_id . ' Company ID: ' . $company_id, __FILE__, __LINE__, __METHOD__, 10 );

					$hlf = TTnew( 'HierarchyListFactory' ); /** @var HierarchyListFactory $hlf */
					$this->hierarchy_arr = $hlf->getHierarchyParentByCompanyIdAndUserIdAndObjectTypeID( $company_id, $object_user_id, $this->getObjectType(), false );
					Debug::Arr( $this->hierarchy_arr, ' Hierarchy Arr: ', __FILE__, __LINE__, __METHOD__, 10 );

					return $this->hierarchy_arr;
				} else {
					Debug::Text( ' Could not find Object User ID: ' . $user_id, __FILE__, __LINE__, __METHOD__, 10 );
				}
			} else {
				Debug::Text( ' ERROR: No ObjectHandler defined...', __FILE__, __LINE__, __METHOD__, 10 );
			}
		}

		return false;
	}


	/**
	 * @return array|bool
	 */
	function getHierarchyChildLevelArray() {
		$retval = [];

		$user_id = $this->getCurrentUser();
		$parent_arr = $this->getHierarchyArray();
		if ( is_array( $parent_arr ) && count( $parent_arr ) > 0 ) {
			$next_level = false;
			foreach ( $parent_arr as $level_parent_arr ) {
				if ( in_array( $user_id, $level_parent_arr ) ) {
					$next_level = true;
					continue;
				}

				if ( $next_level == true ) {
					//Debug::Arr( $level_parent_arr, ' Child: Level: '. $level, __FILE__, __LINE__, __METHOD__, 10 );
					$retval = array_merge( $retval, $level_parent_arr ); //Append from all levels.
				}
			}
		}

		if ( count( $retval ) > 0 ) {
			return $retval;
		}

		return false;
	}

	/**
	 * @param bool $force
	 * @return bool|mixed
	 */
	function getHierarchyCurrentLevelArray( $force = false ) {
		$retval = false;

		$user_id = $this->getCurrentUser();
		$parent_arr = $this->getHierarchyArray();
		if ( is_array( $parent_arr ) && count( $parent_arr ) > 0 ) {
			$next_level = false;
			foreach ( $parent_arr as $level_parent_arr ) {
				if ( in_array( $user_id, $level_parent_arr ) ) {
					$next_level = true;
					if ( $force == false ) {
						continue;
					}
				}

				if ( $next_level == true ) { //Current level is alway one level lower, as this often gets called after the level has been changed.
					$retval = $level_parent_arr;
					//Debug::Arr( $level_parent_arr, ' Current: Level: ' . $level, __FILE__, __LINE__, __METHOD__, 10 );
					break;
				}
			}

			if ( $next_level == true && $retval == false ) {
				//Current level was the top and only level.
				$retval = $level_parent_arr;
				//Debug::Arr( $level_parent_arr, ' Current: Level: ' . $level, __FILE__, __LINE__, __METHOD__, 10 );
			}
		}

		return $retval;
	}

	/**
	 * @return array|bool|mixed
	 */
	function getHierarchyParentLevelArray() {
		$retval = false;

		$user_id = TTUUID::castUUID( $this->getCurrentUser() );
		$parent_arr = array_reverse( (array)$this->getHierarchyArray() );
		if ( is_array( $parent_arr ) && count( $parent_arr ) > 0 ) {
			$next_level = false;
			foreach ( $parent_arr as $level_parent_arr ) {
				if ( is_array( $level_parent_arr ) && in_array( $user_id, $level_parent_arr ) ) {
					$next_level = true;
					continue;
				}

				//Since this loops in reverse, always assume the first element is the parent for cases where a subordinate may be submitting the object (ie: request) and it needs to go to the direct superiors.
				if ( $next_level == true ) {
					//Debug::Arr( $level_parent_arr, ' Parents: Level: '. $level, __FILE__, __LINE__, __METHOD__, 10 );
					$retval = $level_parent_arr;
					break;
				}
			}

			//If we get here without finding a parent, use the lowest lower parents by default.
			if ( $next_level == false ) {
				reset( $parent_arr );
				$retval = $parent_arr[key( $parent_arr )];
			}
		}

		return $retval;
	}

	/**
	 * This will return false if it can't find a hierarchy, or if its at the top level (1) and can't find a higher level.
	 * @return bool|int|string
	 */
	function getNextHierarchyLevel() {
		$retval = false;

		$user_id = $this->getCurrentUser();
		$parent_arr = $this->getHierarchyArray();
		if ( is_array( $parent_arr ) && count( $parent_arr ) > 0 ) {
			foreach ( $parent_arr as $level => $level_parent_arr ) {
				if ( in_array( $user_id, $level_parent_arr ) ) {
					break;
				}
				$retval = $level;
			}
		}

		if ( $retval < 1 ) {
			Debug::Text( ' ERROR, hierarchy level goes past 1... This shouldnt happen...', __FILE__, __LINE__, __METHOD__, 10 );
			$retval = false;
		}

		return $retval;
	}

	/**
	 * @param string $company_id UUID
	 * @param string $user_id    UUID
	 * @param int $hierarchy_type_id
	 * @return int|mixed
	 */
	static function getInitialHierarchyLevel( $company_id, $user_id, $hierarchy_type_id ) {
		$hierarchy_highest_level = 99;
		if ( $company_id != '' && $user_id != '' && $hierarchy_type_id > 0 ) {
			$hlf = TTnew( 'HierarchyListFactory' ); /** @var HierarchyListFactory $hlf */
			$hierarchy_arr = $hlf->getHierarchyParentByCompanyIdAndUserIdAndObjectTypeID( $company_id, $user_id, $hierarchy_type_id, false );
			if ( isset( $hierarchy_arr ) && is_array( $hierarchy_arr ) ) {
				//Debug::Arr( $hierarchy_arr, ' aUser ID ' . $user_id . ' Type ID: ' . $hierarchy_type_id . ' Array: ', __FILE__, __LINE__, __METHOD__, 10 );

				//See if current user is in superior list, if so, start at one level up in the hierarchy, unless its level 1.
				foreach ( $hierarchy_arr as $level => $superior_user_ids ) {
					if ( in_array( $user_id, $superior_user_ids, true ) == true ) {
						Debug::Text( '   Found user in superior list at level: ' . $level, __FILE__, __LINE__, __METHOD__, 10 );

						$i = $level;
						while ( isset( $hierarchy_arr[$i] ) ) {
							if ( $i != 1 ) {
								Debug::Text( '    Removing lower level: ' . $i, __FILE__, __LINE__, __METHOD__, 10 );
								unset( $hierarchy_arr[$i] );
							}
							$i++;
						}
					}
				}

				//Debug::Arr( $hierarchy_arr, ' bUser ID ' . $user_id . ' Type ID: ' . $hierarchy_type_id . ' Array: ', __FILE__, __LINE__, __METHOD__, 10 );
				$hierarchy_arr = array_keys( $hierarchy_arr );
				$hierarchy_highest_level = end( $hierarchy_arr );
			}
		}

		Debug::Text( ' Returning initial hierarchy level to: ' . $hierarchy_highest_level, __FILE__, __LINE__, __METHOD__, 10 );

		return $hierarchy_highest_level;
	}

	/**
	 * @return bool
	 */
	function isValidParent() {
		$user_id = $this->getCurrentUser();
		$parent_arr = $this->getHierarchyArray();
		if ( is_array( $parent_arr ) && count( $parent_arr ) > 0 ) {
			krsort( $parent_arr );
			foreach ( $parent_arr as $level_parent_arr ) {
				if ( in_array( $user_id, $level_parent_arr ) ) {
					return true;
				}
			}
		}

		Debug::Text( ' Authorizing User is not a parent of the object owner: ', __FILE__, __LINE__, __METHOD__, 10 );

		return false;
	}

	/**
	 * @return bool
	 */
	function isFinalAuthorization() {
		$user_id = $this->getCurrentUser();
		$parent_arr = $this->getHierarchyArray();
		if ( is_array( $parent_arr ) && count( $parent_arr ) > 0 ) {
			//Check that level 1 parent exists
			if ( isset( $parent_arr[1] ) && in_array( $user_id, $parent_arr[1] ) ) {
				Debug::Text( ' Final Authorization!', __FILE__, __LINE__, __METHOD__, 10 );

				return true;
			}
		}

		Debug::Text( ' NOT Final Authorization!', __FILE__, __LINE__, __METHOD__, 10 );

		return false;
	}

	/**
	 * Checks to see if the currently logged in user is the only superior in the hierarchy at the current level.
	 *   This would normally be paired with a isFinalAuthorization() check as well.
	 * @return bool
	 */
	function isCurrentUserOnlySuperior() {
		$hierarchy_current_level_user_ids = $this->getHierarchyCurrentLevelArray();
		if ( count( $hierarchy_current_level_user_ids ) == 1 && in_array( $this->getCurrentUser(), $hierarchy_current_level_user_ids ) ) {
			return true;
		}

		return false;
	}

	/**
	 * @return null|object
	 */
	function getObjectHandler() {
		if ( is_object( $this->obj_handler ) ) {
			return $this->obj_handler;
		} else {
			switch ( $this->getObjectType() ) {
				case 90: //TimeSheet
					$this->obj_handler = TTnew( 'PayPeriodTimeSheetVerifyListFactory' );
					break;
				case 200:
					$this->obj_handler = TTnew( 'UserExpenseListFactory' );
					break;
				case 50: //Requests
				case 1010:
				case 1020:
				case 1030:
				case 1040:
				case 1100:
					$this->obj_handler = TTnew( 'RequestListFactory' );
					break;
			}

			return $this->obj_handler;
		}
	}

	/**
	 * @return bool|int
	 */
	function getObjectType() {
		return $this->getGenericDataValue( 'object_type_id' );
	}

	/**
	 * @param $value
	 * @return bool
	 */
	function setObjectType( $value ) {
		$value = (int)trim( $value );

		return $this->setGenericDataValue( 'object_type_id', $value );
	}

	/**
	 * @return bool|mixed
	 */
	function getObject() {
		return $this->getGenericDataValue( 'object_id' );
	}

	/**
	 * @param string $value UUID
	 * @return bool
	 */
	function setObject( $value ) {
		$value = TTUUID::castUUID( $value );

		return $this->setGenericDataValue( 'object_id', $value );
	}

	/**
	 * @return bool
	 */
	function getAuthorized() {
		return $this->fromBool( $this->getGenericDataValue( 'authorized' ) );
	}

	/**
	 * @param $value
	 * @return bool
	 */
	function setAuthorized( $value ) {
		return $this->setGenericDataValue( 'authorized', $this->toBool( $value ) );
	}

	/**
	 * @return bool
	 */
	function clearHistory() {
		Debug::text( 'Clearing Authorization History For Type: ' . $this->getObjectType() . ' ID: ' . $this->getObject(), __FILE__, __LINE__, __METHOD__, 10 );

		if ( $this->getObjectType() === false || $this->getObject() === false ) {
			Debug::text( 'Clearing Authorization History FAILED!', __FILE__, __LINE__, __METHOD__, 10 );

			return false;
		}

		$alf = TTnew( 'AuthorizationListFactory' ); /** @var AuthorizationListFactory $alf */
		$alf->getByObjectTypeAndObjectId( $this->getObjectType(), $this->getObject() );
		foreach ( $alf as $authorization_obj ) {
			$authorization_obj->setDeleted( true );
			$authorization_obj->Save();
		}

		return true;
	}

	/**
	 * @return object
	 */
	function getObjectHandlerObject() {
		if ( is_object( $this->obj_handler_obj ) ) {
			return $this->obj_handler_obj;
		} else {
			//Get user_id of object.
			$this->getObjectHandler()->getByID( $this->getObject() );
			$this->obj_handler_obj = $this->getObjectHandler()->getCurrent();
//			if ( method_exists( $this->obj_handler_obj, 'setCurrentUser' ) AND $this->obj_handler_obj->getCurrentUser() != $this->getCurrentUser() ) { //Required for authorizing TimeSheets from MyAccount -> TimeSheet Authorization.
//				$this->obj_handler_obj->setCurrentUser( $this->getCurrentUser() );
//			}

			return $this->obj_handler_obj;
		}
	}

	/**
	 * @return boolean
	 */
	function setObjectHandlerStatus() {
		$is_final_authorization = $this->isFinalAuthorization();

		$this->obj_handler_obj = $this->getObjectHandlerObject();
		if ( $this->getAuthorized() === true ) {
			if ( $is_final_authorization === true ) {
				//If no other superiors exist in the hierarchy and we are at the top level, assume its authorized.
				if ( $this->getCurrentUser() != $this->obj_handler_obj->getUser() || $this->isCurrentUserOnlySuperior() == true ) {
					Debug::Text( '  Approving Authorization... Final Authorizing Object: ' . $this->getObject() . ' - Type: ' . $this->getObjectType(), __FILE__, __LINE__, __METHOD__, 10 );
					$this->obj_handler_obj->setAuthorizationLevel( 1 );
					$this->obj_handler_obj->setStatus( 50 ); //Active/Authorized
					$this->obj_handler_obj->setAuthorized( true );
				} else {
					Debug::Text( '  Currently logged in user is authorizing (or submitting as new) their own request, when other superiors exist in the hierarchy, not authorizing...', __FILE__, __LINE__, __METHOD__, 10 );
				}
			} else {
				Debug::text( '  Approving Authorization, moving to next level up...', __FILE__, __LINE__, __METHOD__, 10 );
				$current_level = $this->obj_handler_obj->getAuthorizationLevel();
				if ( $current_level > 1 ) { //Highest level is 1, so no point in making it less than that.

					//Get the next level above the current user doing the authorization, in case they have dropped down a level or two.
					$next_level = $this->getNextHierarchyLevel();
					if ( $next_level !== false && $next_level < $current_level ) {
						Debug::text( '  Current Level: ' . $current_level . ' Moving Up To Level: ' . $next_level, __FILE__, __LINE__, __METHOD__, 10 );
						$this->obj_handler_obj->setAuthorizationLevel( $next_level );
					}
				}
				unset( $current_level, $next_level );
			}
		} else {
			Debug::text( '  Declining Authorization...', __FILE__, __LINE__, __METHOD__, 10 );
			$this->obj_handler_obj->setStatus( 55 ); //'AUTHORIZATION DECLINED'
			$this->obj_handler_obj->setAuthorized( false );
		}

		return true;
	}


	/**
	 * @return array|bool
	 */
	function getUserAuthorizationIds() {
		$object_handler_user_id = $this->getObjectHandlerObject()->getUser(); //Object handler (request) user_id.

		$is_final_authorization = $this->isFinalAuthorization();
		$authorization_level = $this->getObjectHandlerObject()->getAuthorizationLevel(); //This is the *new* level, not the old level.

		$hierarchy_current_level_arr = $this->getHierarchyCurrentLevelArray();
		Debug::Arr( $hierarchy_current_level_arr, '  Authorization Level: ' . $authorization_level . ' Authorized: ' . (int)$this->getAuthorized() . ' Is Final Auth: ' . (int)$is_final_authorization . ' Object Handler User ID: ' . $object_handler_user_id, __FILE__, __LINE__, __METHOD__, 10 );

		if ( $this->getAuthorized() == true && $authorization_level == 0 ) {
			//Final authorization has taken place
			//Notify original submittor and all lower level superiors?
			$user_ids = $this->getHierarchyChildLevelArray();

			if ( is_a( $this->getObjectHandlerObject(), 'PayPeriodTimeSheetVerify' ) ) { //is_a() will match on plugin class names too because it also checks the parent class name.
				//Check to see what type of timesheet verification is required, if its superior only, don't notify the employee to avoid confusion.
				if ( $this->getObjectHandlerObject()->getVerificationType() != 30 ) {
					$user_ids[] = $object_handler_user_id;
				} else {
					Debug::text( '  TimeSheetVerification for superior only, dont motify employee...', __FILE__, __LINE__, __METHOD__, 10 );
				}
			} else {
				$user_ids[] = $object_handler_user_id;
			}
			//Debug::Arr($user_ids , '  aAuthorization Level: '. $authorization_level .' Authorized: '. (int)$this->getAuthorized() .' Child: ' , __FILE__, __LINE__, __METHOD__, 10);
		} else {
			//Debug::Text('  bAuthorization Level: '. $authorization_level .' Authorized: '. (int)$this->getAuthorized(), __FILE__, __LINE__, __METHOD__, 10);
			//Final authorization has *not* yet taken place
			if ( $this->getObjectHandlerObject()->getStatus() == 55 ) { //Declined
				//Authorization declined. Notify original submittor and all lower level superiors?
				$user_ids = $this->getHierarchyChildLevelArray();
				$user_ids[] = $object_handler_user_id;
				//Debug::Arr($user_ids , '  b1Authorization Level: '. $authorization_level .' Authorized: '. (int)$this->getAuthorized() .' Child: ', __FILE__, __LINE__, __METHOD__, 10);
			} else if ( $is_final_authorization == true && $this->getCurrentUser() == $object_handler_user_id && $this->getAuthorized() == true && $authorization_level == 1 ) {
				//Subordinate who is also a superior at the top and only level of the hierarchy is submitting a request.
				$user_ids = $this->getHierarchyCurrentLevelArray( true ); //Force to real current level.
				//Debug::Arr($user_ids , '  b2Authorization Level: '. $authorization_level .' Authorized: '. (int)$this->getAuthorized() .' Child: ', __FILE__, __LINE__, __METHOD__, 10);
			} else {
				//Authorized at a middle level, notify current level superiors only so they know its waiting on them.
				$user_ids = $this->getHierarchyParentLevelArray();
				//Debug::Arr($user_ids , '  b3Authorization Level: '. $authorization_level .' Authorized: '. (int)$this->getAuthorized() .' Parent: ', __FILE__, __LINE__, __METHOD__, 10);
			}
		}

		if( isset( $user_ids ) && !empty( $user_ids ) ) {
			//Remove the current authorizing user from the array, as they don't need to be notified as they are performing the action.
			$user_ids = array_diff( (array)$user_ids, [ $this->getCurrentUser() ] );         //CurrentUser is currently logged in user.

			//remove duplicate user_ids
			$user_ids = array_unique( $user_ids );
			return $user_ids;
		}

		return [];
	}

	/**
	 * @return bool
	 */
	function sendNotificationAuthorization( ) {
		Debug::Text( 'getNotificationData: ', __FILE__, __LINE__, __METHOD__, 10 );

		$user_ids = $this->getUserAuthorizationIds();
		if ( empty ( $user_ids ) ) {
			return false;
		}

		//Get initiator user from User Object so we can include more information in the message.
		if ( is_object( $this->getCurrentUserObject() ) ) {
			$u_obj = $this->getCurrentUserObject();
		} else {
			Debug::Text( 'From object does not exist: ' . $this->getCurrentUser(), __FILE__, __LINE__, __METHOD__, 10 );

			return false;
		}

		foreach ( $user_ids as $user_id ) {
			//Grab each users preferences as they can be custom to them and their language etc.
			$ulf = TTnew( 'UserListFactory' ); /** @var UserListFactory $ulf */
			$ulf->getById( $user_id );
			if ( $ulf->getRecordCount() == 1 ) {
				$user_to_obj = $ulf->getCurrent();

				if ( is_object( $user_to_obj ) ) {
					$user_to_pref_obj = $user_to_obj->getUserPreferenceObject(); /** @var UserPreferenceFactory $user_to_pref_obj */
					$user_to_pref_obj->setDateTimePreferences();
					TTi18n::setLanguage( $user_to_pref_obj->getLanguage() );
					TTi18n::setCountry( $user_to_obj->getCountry() );
					TTi18n::setLocale();
				} else {
					return false;
				}
			} else {
				Debug::Text( 'ERROR: User does not exist: ' . $user_id, __FILE__, __LINE__, __METHOD__, 10 );
				return false;
			}

			$object_handler_user_obj = $this->getObjectHandlerObject()->getUserObject();                                                                                       //Object handler (request) user_id.
			$status_label = TTi18n::ucfirst( TTi18n::strtolower( Option::getByKey( $this->getObjectHandlerObject()->getStatus(), Misc::trimSortPrefix( $this->getObjectHandlerObject()->getOptions( 'status' ) ) ) ) ); //PENDING, AUTHORIZED, DECLINED

			$title_short = '#object_type# '. TTi18n::gettext( 'by' ) .' #object_employee_first_name# #object_employee_last_name# #status#.';
			$title_long = '#object_type# '. TTi18n::gettext( 'by' ) .' #object_employee_first_name# #object_employee_last_name# #status# '. TTi18n::gettext( 'for' ) .' #date#';

			switch ( $this->getObjectType() ) {
				case 90: //TimeSheet
					$object_type = TTi18n::getText( 'TimeSheet' );
					$notification_object_type = 90;
					if ( $this->getAuthorized() == true && $this->getObjectHandlerObject()->getAuthorizationLevel() == 0 ) {
						// Timesheet has been verified link back to timesheet.
						$link = Misc::getURLProtocol() . '://' . Misc::getHostName() . Environment::getDefaultInterfaceBaseURL() . 'html5/#!m=TimeSheet';
						// If timesheet belongs to user being notified set type as timesheet_verify timesheet_authorize for supervisors.
						if ( $object_handler_user_obj->getId() === $user_to_obj->getId() ) {
							$notification_type = 'timesheet_verify';
						} else {
							$notification_type = 'timesheet_authorize';
						}
					} else {
						$link = Misc::getURLProtocol() . '://' . Misc::getHostName() . Environment::getDefaultInterfaceBaseURL() . 'html5/#!m=TimeSheetAuthorization&a=view&id=' . $this->getObject() . '&tab=TimeSheetVerification';
						$notification_type = 'timesheet_authorize';
					}
					$display_date = TTDate::getDate( 'DATE', $this->getObjectHandlerObject()->getPayPeriodObject()->getEndDate() );
					$body_short = TTi18n::getText( 'Pay Period' ) . ': ' . TTDate::getDate( 'DATE', $this->getObjectHandlerObject()->getPayPeriodObject()->getStartDate() ) . ' -> ' . TTDate::getDate( 'DATE', $this->getObjectHandlerObject()->getPayPeriodObject()->getEndDate() );
					break;
				case 200: //Expense
					$object_type = TTi18n::getText( 'Expense' );
					$notification_object_type = 110;
					if ( $this->getAuthorized() == true && $this->getObjectHandlerObject()->getAuthorizationLevel() == 0 ) {
						// Expense has been authorized link back to original expense and not the authorization view.
						$link = Misc::getURLProtocol() . '://' . Misc::getHostName() . Environment::getDefaultInterfaceBaseURL() . 'html5/#!m=LoginUserExpense&a=view&id=' . $this->getObject() . '&tab=Expense';
						// If expense belongs to user being notified set type as expense_verify else expense_authorize for supervisors.
						if ( $object_handler_user_obj->getId() === $user_to_obj->getId() ) {
							$notification_type = 'expense_verify';
						} else {
							$notification_type = 'expense_authorize';
						}
					} else {
						$link = Misc::getURLProtocol() . '://' . Misc::getHostName() . Environment::getDefaultInterfaceBaseURL() . 'html5/#!m=ExpenseAuthorization&a=edit&id=' . $this->getObject() . '&tab=Expense';
						$notification_type = 'expense_authorize';
					}
					$display_date = TTDate::getDate( 'DATE', $this->getObjectHandlerObject()->getIncurredDate() );

					//Check if its a custom unit or just dollars so the message can be formatted properly for each.
					if ( is_object( $this->getObjectHandlerObject()->getExpensePolicyObject() ) && $this->getObjectHandlerObject()->getExpensePolicyObject()->getType() == 30 ) { //30=Per Unit
						$body_short = $this->getObjectHandlerObject()->getGrossAmount() . ' ' . $this->getObjectHandlerObject()->getExpensePolicyObject()->getUnitName() . ' ' . TTi18n::getText( 'incurred on' ) . ': ' . TTDate::getDate( 'DATE', $this->getObjectHandlerObject()->getIncurredDate() );
					} else {
						$body_short = '$' . $this->getObjectHandlerObject()->getGrossAmount() . ' ' . TTi18n::getText( 'incurred on' ) . ': ' . TTDate::getDate( 'DATE', $this->getObjectHandlerObject()->getIncurredDate() );
					}

					//Add the reimbursable amount so its clear to the end-user.
					$body_short .= "\n". TTi18n::getText( 'Reimbursable Amount' ) .': $'. $this->getObjectHandlerObject()->getReimburseAmount();

					break;
				case 50: //Requests
				case 1010:
				case 1020:
				case 1030:
				case 1040:
				case 1100:
					$object_type = TTi18n::getText( 'Request' );
					$notification_object_type = 50;
					if ( $this->getAuthorized() == true && $this->getObjectHandlerObject()->getAuthorizationLevel() == 0 ) {
						// Request has been authorized link back to original request and not the authorization view.
						$link = Misc::getURLProtocol() . '://' . Misc::getHostName() . Environment::getDefaultInterfaceBaseURL() . 'html5/#!m=Request&a=view&id=' . $this->getObject() . '&tab=Request';
						// If request belongs to user being notified set type as request else request_authorize for supervisors.
						if ( $object_handler_user_obj->getId() === $user_to_obj->getId() ) {
							$notification_type = 'request';
						} else {
							$notification_type = 'request_authorize';
						}
					} else {
						$link = Misc::getURLProtocol() . '://' . Misc::getHostName() . Environment::getDefaultInterfaceBaseURL() . 'html5/#!m=RequestAuthorization&a=view&id=' . $this->getObject() . '&tab=Request';
						$notification_type = 'request_authorize';
					}
					$display_date = TTDate::getDate( 'DATE', $this->getObjectHandlerObject()->getDateStamp() );
					$body_short = Option::getByKey( $this->getObjectHandlerObject()->getType(), Misc::trimSortPrefix( $this->getObjectHandlerObject()->getOptions( 'type' ) ) ) . ' ' . TTi18n::getText( 'on' ) . ' ' . TTDate::getDate( 'DATE', $this->getObjectHandlerObject()->getDateStamp() );
					break;
			}

			//Define title_short/body variables here.
			$search_arr = [
					'#object_type#',
					'#object_type_long_description#',
					'#status#',
					'#date#',

					'#current_employee_first_name#',
					'#current_employee_last_name#',

					'#object_employee_first_name#',
					'#object_employee_last_name#',
					'#object_employee_default_branch#',
					'#object_employee_default_department#',
					'#object_employee_group#',
					'#object_employee_title#',

					'#company_name#',
					'#url#',
			];

			$replace_arr = Misc::escapeHTML( [
					$object_type,
					$body_short,
					$status_label,
					$display_date,

					$u_obj->getFirstName(),
					$u_obj->getLastName(),

					$object_handler_user_obj->getFirstName(),
					$object_handler_user_obj->getLastName(),
					( is_object( $object_handler_user_obj->getDefaultBranchObject() ) ) ? $object_handler_user_obj->getDefaultBranchObject()->getName() : null,
					( is_object( $object_handler_user_obj->getDefaultDepartmentObject() ) ) ? $object_handler_user_obj->getDefaultDepartmentObject()->getName() : null,
					( is_object( $object_handler_user_obj->getGroupObject() ) ) ? $object_handler_user_obj->getGroupObject()->getName() : null,
					( is_object( $object_handler_user_obj->getTitleObject() ) ) ? $object_handler_user_obj->getTitleObject()->getName() : null,

					( is_object( $object_handler_user_obj->getCompanyObject() ) ) ? $object_handler_user_obj->getCompanyObject()->getName() : null,
					( Misc::getURLProtocol() . '://' . Misc::getHostName() . Environment::getDefaultInterfaceBaseURL() ),
			] );

			$title_short = str_replace( $search_arr, $replace_arr, $title_short );
			$title_long = str_replace( $search_arr, $replace_arr, $title_long );
			$body_short = str_replace( $search_arr, $replace_arr, $body_short );

			//$body_long = TTi18n::gettext( '*DO NOT REPLY TO THIS EMAIL - PLEASE USE THE LINK BELOW INSTEAD*' ) . "\n\n";
			$body_long = '#object_type# '. TTi18n::gettext( 'by' ) .' #object_employee_first_name# #object_employee_last_name# #status#'. "\n";
			$body_long .= ( $replace_arr[1] != '' ) ? '#object_type_long_description#' . "\n" : null;
			$body_long .= "\n";
			$body_long .= ( $replace_arr[8] != '' ) ? TTi18n::gettext( 'Default Branch' ) . ': #object_employee_default_branch#' . "\n" : null;
			$body_long .= ( $replace_arr[9] != '' ) ? TTi18n::gettext( 'Default Department' ) . ': #object_employee_default_department#' . "\n" : null;
			$body_long .= ( $replace_arr[10] != '' ) ? TTi18n::gettext( 'Group' ) . ': #object_employee_group#' . "\n" : null;
			$body_long .= ( $replace_arr[11] != '' ) ? TTi18n::gettext( 'Title' ) . ': #object_employee_title#' . "\n" : null;
			$body_long .= TTi18n::gettext( 'Link' ) . ': <a href="#url#">' . APPLICATION_NAME . ' ' . TTi18n::gettext( 'Login' ) . '</a>' . "\n";

			$body_long .= NotificationFactory::addEmailFooter( ( ( is_object( $object_handler_user_obj->getCompanyObject() ) ) ? $object_handler_user_obj->getCompanyObject()->getName() : null ) );
			$body_long = '<html><body><pre>' . str_replace( $search_arr, $replace_arr, $body_long ) . '</pre></body></html>';

			$notification_data = [
					'object_id'      => $this->getObject(),
					'user_id'        => $user_id,
					'type_id'        => $notification_type,
					'object_type_id' => $notification_object_type,
					'title_short'    => $title_short,
					'title_long'     => $title_long,
					'body_short'     => $body_short,
					'body_long_html' => $body_long, //For emails
					'payload'        => [ 'link' => $link ],
			];

			Notification::sendNotification( $notification_data );
		}

		//reset datetime and tti8n preferences to current user
		$user_pref_obj = $u_obj->getUserPreferenceObject(); /** @var UserPreferenceFactory $user_pref_obj */
		$user_pref_obj->setDateTimePreferences();
		TTi18n::setLanguage( $user_pref_obj->getLanguage() );
		TTi18n::setCountry( $u_obj->getCountry() );
		TTi18n::setLocale();

		return true;
	}

	function markRelatedNotificationsAsRead() {
		$request_object_type_to_notification_object_type_map = [
			90 => 90, //'timesheet',
			200  => 110, //'expense',
			//50 => 'request', //request_other
			1010 => 50, //'request_punch',
			1020 => 50, //'request_punch_adjust',
			1030 => 50, //'request_absence',
			1040 => 50, //'request_schedule',
			1100 => 50, //'request_other',
		];

		if ( isset( $request_object_type_to_notification_object_type_map[$this->getObjectType()] ) ) {
			$notification_object_type_id = $request_object_type_to_notification_object_type_map[$this->getObjectType()];
			if ( $this->isFinalAuthorization() == true ) {
				//If its a final authorization, mark notification as read for *all* notifications/users at any level.
				NotificationFactory::updateStatusByObjectIdAndObjectTypeId( $notification_object_type_id, $this->getObject() ); //Mark any notifications linked to these exceptions as read.
			} else {
				//If its a superior at a low level, only mark notifications as read for any other superior at the same level.
				$hierarchy_current_level_user_ids = $this->getHierarchyCurrentLevelArray();
				NotificationFactory::updateStatusByObjectIdAndObjectTypeId( $notification_object_type_id, $this->getObject(), $hierarchy_current_level_user_ids ); //Mark any notifications linked to these exceptions as read.
			}
		}

		return true;
	}

	/**
	 * Used by Request/TimeSheetVerification/Expense when initially saving a record to notify the immediate superiors, rather than using the message notification.
	 * @param string $current_user_id UUID
	 * @param int $object_type_id
	 * @param string $object_id       UUID
	 * @return bool
	 */
	static function sendNotificationAuthorizationOnInitialObjectSave( $current_user_id, $object_type_id, $object_id ) {
		$authorization_obj = TTNew( 'AuthorizationFactory' ); /** @var AuthorizationFactory $authorization_obj */
		$authorization_obj->setObjectType( $object_type_id );
		$authorization_obj->setObject( $object_id );
		$authorization_obj->setCurrentUser( $current_user_id );
		$authorization_obj->setAuthorized( true );
		$authorization_obj->sendNotificationAuthorization();
	}

	/**
	 * @return bool
	 */
	function isUnique() {
		$ph = [
				'object_type' => (int)$this->getObjectType(),
				'object_id'   => TTUUID::castUUID( $this->getObject() ),
				'authorized'  => (int)$this->getAuthorized(),
				'created_by'  => TTUUID::castUUID( $this->getCreatedBy() ),
		];

		$query = 'select id from ' . $this->getTable() . ' where object_type_id = ? AND object_id = ? AND authorized = ? AND created_by = ?';
		$id = $this->db->GetOne( $query, $ph );
		Debug::Arr( $id, 'Unique Authorization: ' . $id, __FILE__, __LINE__, __METHOD__, 10 );

		if ( $id === false ) {
			return true;
		} else {
			if ( $id == $this->getId() ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * @param bool $ignore_warning
	 * @return bool
	 */
	function Validate( $ignore_warning = true ) {
		//
		// BELOW: Validation code moved from set*() functions.
		//
		// Object Type
		$this->Validator->inArrayKey( 'object_type',
									  $this->getObjectType(),
									  TTi18n::gettext( 'Object Type is invalid' ),
									  $this->getOptions( 'object_type' )
		);
		// Object ID
		$this->Validator->isResultSetWithRows( 'object',
											   ( is_object( $this->getObjectHandler() ) ) ? $this->getObjectHandler()->getByID( $this->getObject() ) : false,
											   TTi18n::gettext( 'Object ID is invalid' )
		);

		//Prevent duplicate authorizations by the same person.
		// This may cause problems if the hierarchy is changed and the same superior needs to authorize the request again though?
		//   By definition this should never happen at the final authorization level, so someone higher up in the hierarchy could always drop down and authorize it during the transition.
		if ( $this->getDeleted() == false ) {
			if ( $this->Validator->getValidateOnly() == false && $this->isUnique() == false ) {
				$this->Validator->isTrue( 'object',
										  false,
										  TTi18n::gettext( 'Record has already been authorized/declined by you' ) );
			}
		}

		//
		// ABOVE: Validation code moved from set*() functions.
		//
		if ( $this->getDeleted() === false
				&& $this->isFinalAuthorization() === false
				&& $this->isValidParent() === false ) {
			//FYI: This error may occur on timesheet authorization if the timesheet cannot be verified because pending requests or critical severity exceptions exist. Though it should display a proper validation message to that affect instead.
			$this->Validator->isTrue( 'parent',
									  false,
									  TTi18n::gettext( 'Employee authorizing this object is not a superior in the hierarchy that controls it' ) );

			return false;
		}

		$this->setObjectHandlerStatus();

		if ( $this->getDeleted() == false && is_object( $this->getObjectHandlerObject() ) && $this->getObjectHandlerObject()->isValid() == false ) {
			Debug::text( '  ObjectHandler Validation Failed, pass validation errors up the chain...', __FILE__, __LINE__, __METHOD__, 10 );
			$this->Validator->merge( $this->getObjectHandlerObject()->Validator );
		}

		return true;
	}

	/**
	 * @return bool
	 */
	function preSave() {
		//Debug::Text(' Calling preSave!: ', __FILE__, __LINE__, __METHOD__, 10);
		$this->StartTransaction();

		return true;
	}

	/**
	 * @return bool
	 */
	function postSave() {
		if ( $this->getDeleted() == false ) {
			if ( is_object( $this->getObjectHandlerObject() ) && $this->getObjectHandlerObject()->isValid() == true ) {
				Debug::text( '  Object Valid...', __FILE__, __LINE__, __METHOD__, 10 );
				//Return true if object saved correctly.
				$retval = $this->getObjectHandlerObject()->Save( false );
				if ( $this->getObjectHandlerObject()->isValid() == false ) {
					Debug::text( '  Object postSave validation FAILED!', __FILE__, __LINE__, __METHOD__, 10 );
					$this->Validator->merge( $this->getObjectHandlerObject()->Validator );
				} else {
					Debug::text( '  Object postSave validation SUCCESS!', __FILE__, __LINE__, __METHOD__, 10 );
					$this->markRelatedNotificationsAsRead(); //Mark existing notifications as read before new ones are sent.
					$this->sendNotificationAuthorization();
				}

				if ( $retval === true ) {
					$this->CommitTransaction();

					return true;
				} else {
					$this->FailTransaction();
				}
			} else {
				//Always fail the transaction if we get this far.
				//This stops authorization entries from being inserted.
				$this->FailTransaction();
			}

			$this->CommitTransaction(); //preSave() starts the transaction

			return false;
		}

		$this->CommitTransaction(); //preSave() starts the transaction

		return true;
	}

	/**
	 * @param $data
	 * @return bool
	 */
	function setObjectFromArray( $data ) {
		if ( is_array( $data ) ) {
			$variable_function_map = $this->getVariableToFunctionMap();
			foreach ( $variable_function_map as $key => $function ) {
				if ( isset( $data[$key] ) ) {

					$function = 'set' . $function;
					switch ( $key ) {
						default:
							if ( method_exists( $this, $function ) ) {
								$this->$function( $data[$key] );
							}
							break;
					}
				}
			}

			$this->setCreatedAndUpdatedColumns( $data );

			return true;
		}

		return false;
	}

	/**
	 * @param null $include_columns
	 * @param bool $permission_children_ids
	 * @return array
	 */
	function getObjectAsArray( $include_columns = null, $permission_children_ids = false ) {
		$data = [];
		$variable_function_map = $this->getVariableToFunctionMap();
		if ( is_array( $variable_function_map ) ) {
			foreach ( $variable_function_map as $variable => $function_stub ) {
				if ( $include_columns == null || ( isset( $include_columns[$variable] ) && $include_columns[$variable] == true ) ) {

					$function = 'get' . $function_stub;
					switch ( $variable ) {
						case 'object_type':
							Debug::text( '  Object Type...', __FILE__, __LINE__, __METHOD__, 10 );
							$data[$variable] = Option::getByKey( $this->getObjectType(), $this->getOptions( $variable ) );
							break;
						default:
							if ( method_exists( $this, $function ) ) {
								$data[$variable] = $this->$function();
							}
							break;
					}
				}
			}
			$this->getPermissionColumns( $data, $this->getColumn( 'user_id' ), $this->getCreatedBy(), $permission_children_ids, $include_columns );
			$this->getCreatedAndUpdatedColumns( $data, $include_columns );
		}

		return $data;
	}

	/**
	 * @param $log_action
	 * @return bool
	 */
	function addLog( $log_action ) {
		if ( $this->getAuthorized() === true ) {
			$authorized = TTi18n::getText( 'True' );
		} else {
			$authorized = TTi18n::getText( 'False' );
		}

		return TTLog::addEntry( $this->getId(), $log_action, TTi18n::getText( 'Authorization Object Type' ) . ': ' . ucwords( str_replace( '_', ' ', Option::getByKey( $this->getObjectType(), $this->getOptions( 'object_type' ) ) ) ) . ' ' . TTi18n::getText( 'Authorized' ) . ': ' . $authorized, null, $this->getTable() );
	}
}

?>