<?php
// Copyright (C) 2010-2024 Combodo SAS
//
//   This file is part of iTop.
//
//   iTop is free software; you can redistribute it and/or modify	
//   it under the terms of the GNU Affero General Public License as published by
//   the Free Software Foundation, either version 3 of the License, or
//   (at your option) any later version.
//
//   iTop 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 iTop. If not, see <http://www.gnu.org/licenses/>

require_once('backgroundprocess.inc.php');

/**
 * ormStopWatch
 * encapsulate the behavior of a stop watch that will be stored as an attribute of class AttributeStopWatch 
 *
 * @copyright   Copyright (C) 2010-2024 Combodo SAS
 * @license     http://opensource.org/licenses/AGPL-3.0
 */


/**
 * ormStopWatch
 * encapsulate the behavior of a stop watch that will be stored as an attribute of class AttributeStopWatch 
 *
 * @package     itopORM
 */
class ormStopWatch
{
	protected $iTimeSpent; // seconds
	protected $iStarted; // unix time (seconds)
	protected $iLastStart; // unix time (seconds)
	protected $iStopped; // unix time (seconds)
	protected $aThresholds;
	
	/**
	 * Constructor
	 */
	public function __construct($iTimeSpent = 0, $iStarted = null, $iLastStart = null, $iStopped = null)
	{
		$this->iTimeSpent = (int) $iTimeSpent;
		$this->iStarted = $iStarted;
		$this->iLastStart = $iLastStart;
		$this->iStopped = $iStopped;

		$this->aThresholds = array();
	}

	/**
	 * Necessary for the triggers
	 */	 	
	public function __toString()
	{
		return (string) $this->iTimeSpent;
	}

	public function DefineThreshold($iPercent, $tDeadline = null, $bPassed = false, $bTriggered = false, $iOverrun = null, $aHighlightDef = null)
	{
		$this->aThresholds[$iPercent] = array(
			'deadline' => $tDeadline, // unix time (seconds)
			'triggered' => $bTriggered,
			'overrun' => $iOverrun,
			'highlight' => $aHighlightDef, // array('code' => string, 'persistent' => boolean)
		);
	}

	public function MarkThresholdAsTriggered($iPercent)
	{
		$this->aThresholds[$iPercent]['triggered'] = true;
	}

	public function GetTimeSpent()
	{
		return $this->iTimeSpent;
	}

	/**
	 * Get the working elapsed time since the start of the stop watch
	 * even if it is currently running
	 *
	 * @param AttributeDefinition oAttDef  Attribute hosting the stop watch
	 * @param Object oObject    Hosting object (used for query parameters)
	 *
	 * @return int|mixed
	 * @throws \CoreException
	 */
	public function GetElapsedTime($oAttDef, $oObject)
	{
		if (is_null($this->iLastStart))
		{
			return $this->GetTimeSpent();
		}
		else
		{
			$iElapsed = $this->ComputeDuration($oObject, $oAttDef, $this->iLastStart, time());
			return $this->iTimeSpent + $iElapsed;
		}
	}


	public function GetStartDate()
	{
		return $this->iStarted;
	}

	public function GetLastStartDate()
	{
		return $this->iLastStart;
	}

	public function GetStopDate()
	{
		return $this->iStopped;
	}

	public function GetThresholdDate($iPercent)
	{
		if (array_key_exists($iPercent, $this->aThresholds))
		{
			return $this->aThresholds[$iPercent]['deadline'];
		}
		else
		{
			return null;
		}
	}

	public function GetOverrun($iPercent)
	{
		if (array_key_exists($iPercent, $this->aThresholds))
		{
			return $this->aThresholds[$iPercent]['overrun'];
		}
		else
		{
			return null;
		}
	}
	public function IsThresholdPassed($iPercent)
	{
		$bRet = false;
		if (array_key_exists($iPercent, $this->aThresholds))
		{
			$aThresholdData = $this->aThresholds[$iPercent];
			if (!is_null($aThresholdData['deadline']) && ($aThresholdData['deadline'] <= time()))
			{
				$bRet = true;
			}
			if (isset($aThresholdData['overrun']) && ($aThresholdData['overrun'] > 0))
			{
				$bRet = true;
			}
		}
		return $bRet;
	}
	public function IsThresholdTriggered($iPercent)
	{
		if (array_key_exists($iPercent, $this->aThresholds))
		{
			return $this->aThresholds[$iPercent]['triggered'];
		}
		else
		{
			return false;
		}
	}
	
	public function GetHighlightCode()
	{
		$sCode = '';
		// Process the thresholds in ascending order
		$aPercents = array();
		foreach($this->aThresholds as $iPercent => $aDefs)
		{
			$aPercents[] = $iPercent;
		}
		sort($aPercents, SORT_NUMERIC);
		foreach($aPercents as $iPercent)
		{
			$aDefs = $this->aThresholds[$iPercent];
			if (array_key_exists('highlight', $aDefs) && is_array($aDefs['highlight']) && $this->IsThresholdPassed($iPercent))
			{
				// If persistant or SW running...
				if (($aDefs['highlight']['persistent'] == true) || (($aDefs['highlight']['persistent'] == false) && !is_null($this->iLastStart)))
				{
					$sCode = $aDefs['highlight']['code'];
				}
			}
		}
		return $sCode;
	}

	public function GetAsHTML($oAttDef, $oHostObject = null)
	{
		$aProperties = array();

		$aProperties['States'] = implode(', ', $oAttDef->GetStates());

		if (is_null($this->iLastStart))
		{
			if (is_null($this->iStarted))
			{
				$aProperties['Elapsed'] = 'never started';
			}
			else
			{
				$aProperties['Elapsed'] = $this->iTimeSpent.' s';
			}
		}
		else
		{
			$aProperties['Elapsed'] = 'running <img src="' . utils::GetAbsoluteUrlAppRoot() . 'images/indicator.gif">';
		}

		$aProperties['Started'] = $oAttDef->SecondsToDate($this->iStarted);
		$aProperties['LastStart'] = $oAttDef->SecondsToDate($this->iLastStart);
		$aProperties['Stopped'] = $oAttDef->SecondsToDate($this->iStopped);

		foreach ($this->aThresholds as $iPercent => $aThresholdData)
		{
			$sThresholdDesc = $oAttDef->SecondsToDate($aThresholdData['deadline']);
			if ($aThresholdData['triggered'])
			{
				$sThresholdDesc .= " <b>TRIGGERED</b>";
			}
			if ($aThresholdData['overrun'])
			{
				$sThresholdDesc .= " Overrun:".(int) $aThresholdData['overrun']." sec.";
			}
			$aProperties[$iPercent.'%'] = $sThresholdDesc;
		}
		$sRes = "<TABLE>";
		$sRes .= "<TBODY>";
		foreach ($aProperties as $sProperty => $sValue)
		{
			$sRes .= "<TR>";
			$sCell = str_replace("\n", "<br>\n", $sValue ?? '');
			$sRes .= "<TD class=\"label\">$sProperty</TD><TD>$sCell</TD>";
			$sRes .= "</TR>";
		}
		$sRes .= "</TBODY>";
		$sRes .= "</TABLE>";
		return $sRes;
	}

	/**
	 * @param \DBObject $oObject
	 * @param \AttributeStopWatch $oAttDef
	 *
	 * @return float goal value (in second)
	 * @uses \iMetricComputer::ComputeMetric()
	 * @throws \CoreException
	 */
	protected function ComputeGoal($oObject, $oAttDef)
	{
		$sMetricComputer = $oAttDef->Get('goal_computing');
		/** @var \iMetricComputer $oComputer */
		$oComputer = new $sMetricComputer();

		$aCallSpec = array($oComputer, 'ComputeMetric');
		if (!is_callable($aCallSpec))
		{
			throw new CoreException("Unknown class/verb '$sMetricComputer/ComputeMetric'");
		}

		return $oComputer->ComputeMetric($oObject);
	}

	/**
	 * @param $oObject
	 * @param $oAttDef
	 * @param $iPercent
	 * @param $iStartTime
	 * @param $iDurationSec
	 *
	 * @return mixed
	 * @throws \CoreException
	 */
	protected function ComputeDeadline($oObject, $oAttDef, $iPercent, $iStartTime, $iDurationSec)
	{
		$sWorkingTimeComputer = $oAttDef->Get('working_time_computing');
		if ($sWorkingTimeComputer == '')
		{
			$sWorkingTimeComputer = MetaModel::GetWorkingTime(get_class($oObject));
		}
		$oComputer = new $sWorkingTimeComputer();
		$aCallSpec = array($oComputer, 'GetDeadline');
		if (!is_callable($aCallSpec))
		{
			throw new CoreException("Unknown class/verb '$sWorkingTimeComputer/GetDeadline'");
		}
		// GetDeadline($oObject, $iDuration, DateTime $oStartDate)
		$oStartDate = new DateTime('@'.$iStartTime); // setTimestamp not available in PHP 5.2
		$oDeadline = call_user_func($aCallSpec, $oObject, $iDurationSec, $oStartDate, $iPercent);
		$iRet = $oDeadline->format('U');
		return $iRet;
	}

	/**
	 * @param $oObject
	 * @param $oAttDef
	 * @param $iStartTime
	 * @param $iEndTime
	 *
	 * @return mixed
	 * @throws \CoreException
	 */
	protected function ComputeDuration($oObject, $oAttDef, $iStartTime, $iEndTime)
	{
		$sWorkingTimeComputer = $oAttDef->Get('working_time_computing');
		if ($sWorkingTimeComputer == '')
		{
			$sWorkingTimeComputer = MetaModel::GetWorkingTime(get_class($oObject));
		}
		$oComputer = new $sWorkingTimeComputer();
		$aCallSpec = array($oComputer, 'GetOpenDuration');
		if (!is_callable($aCallSpec))
		{
			throw new CoreException("Unknown class/verb '$sWorkingTimeComputer/GetOpenDuration'");
		}
		// GetOpenDuration($oObject, DateTime $oStartDate, DateTime $oEndDate)
		$oStartDate = new DateTime('@'.$iStartTime); // setTimestamp not available in PHP 5.2
		$oEndDate = new DateTime('@'.$iEndTime);
		$iRet = call_user_func($aCallSpec, $oObject, $oStartDate, $oEndDate);
		return $iRet;
	}

	public function Reset($oObject, $oAttDef)
	{
		$this->iTimeSpent = 0;
		$this->iStopped = null;
		$this->iStarted = null;

		foreach ($this->aThresholds as $iPercent => &$aThresholdData)
		{
			$aThresholdData['triggered'] = false;
			$aThresholdData['overrun'] = null;
		}

		if (!is_null($this->iLastStart))
		{
			// Currently running... starting again from now!
			$this->iStarted = time();
			$this->iLastStart = time();
			$this->ComputeDeadlines($oObject, $oAttDef);
		}
	}

	/**
	 * Start or continue
	 * It is the responsibility of the caller to compute the deadlines
	 * (to avoid computing twice for the same result) 	 
	 */	 	 	
	public function Start($oObject, $oAttDef, $iNow = null)
	{
		if (!is_null($this->iLastStart))
		{
			// Already started
			return false;
		}

		if (is_null($iNow))
		{
			$iNow = time();
		}

		if (is_null($this->iStarted))
		{
			$this->iStarted = $iNow;
		}
		$this->iLastStart = $iNow;
		$this->iStopped = null;

		return true;
	}

	/**
	 * Compute or recompute the goal and threshold deadlines
	 */	 	 	
	public function ComputeDeadlines($oObject, $oAttDef)
	{
		if (is_null($this->iLastStart))
		{
			// Currently stopped - do nothing
			return false;
		}

		$iDurationGoal = $this->ComputeGoal($oObject, $oAttDef);
		$iComputationRefTime = time();
		foreach ($this->aThresholds as $iPercent => &$aThresholdData)
		{
			if (is_null($iDurationGoal))
			{
				// No limit: leave null thresholds
				$aThresholdData['deadline'] = null;
			}
			else
			{
				$iThresholdDuration = round($iPercent * $iDurationGoal / 100);

				if (class_exists('WorkingTimeRecorder'))
				{
					$sClass = get_class($oObject);
					$sAttCode = $oAttDef->GetCode();
					WorkingTimeRecorder::Start($oObject, $iComputationRefTime, "ormStopWatch-Deadline-$iPercent-$sAttCode", 'Core:ExplainWTC:StopWatch-Deadline', array("Class:$sClass/Attribute:$sAttCode", $iPercent));
				}
				$iRemaining = $iThresholdDuration - $this->iTimeSpent;
				if ($iRemaining < 0)
				{
					if (class_exists('WorkingTimeRecorder'))
					{
						$sClass = get_class($oObject);
						$sKey = $oObject->GetKey();
						$sAttCode = $oAttDef->GetCode();
						$sDate = date('Y-m-d H:i:s', $aThresholdData['deadline']);
						WorkingTimeRecorder::Log(WorkingTimeRecorder::TRACE_INFO, "$sClass($sKey) ormStopWatch-Deadline-$iPercent-$sAttCode ($sDate) already reached, not changed.");
					}
					continue;
				}
				$aThresholdData['deadline'] = $this->ComputeDeadline($oObject, $oAttDef, $iPercent, $this->iLastStart, $iRemaining);
				// OR $aThresholdData['deadline'] = $this->ComputeDeadline($oObject, $oAttDef, $iPercent, $this->iStarted, $iThresholdDuration);

				if (class_exists('WorkingTimeRecorder'))
				{
					WorkingTimeRecorder::End();
				}
			}
			if (is_null($aThresholdData['deadline']) || ($aThresholdData['deadline'] > time()))
			{
				// The threshold is in the future, reset
				$aThresholdData['triggered'] = false;
				$aThresholdData['overrun'] = null;
			}
			// else
			{
				// The new threshold is in the past
				// Note: the overrun can be wrong, but the correct algorithm to compute
				// the overrun of a deadline in the past requires that the ormStopWatch keeps track of all its history!!!
			}
		}

		return true;
	}

	/**
	 * Stop counting if not already done
	 */	 	 	
	public function Stop($oObject, $oAttDef, $iNow = null)
	{
		if (is_null($this->iLastStart))
		{
			// Already stopped
			return false;
		}

		if (is_null($iNow))
		{
			$iNow = time();
		}

		if (class_exists('WorkingTimeRecorder'))
		{
			$sClass = get_class($oObject);
			$sAttCode = $oAttDef->GetCode();
			WorkingTimeRecorder::Start($oObject, $iNow, "ormStopWatch-TimeSpent-$sAttCode", 'Core:ExplainWTC:StopWatch-TimeSpent', array("Class:$sClass/Attribute:$sAttCode"), true /*cumulative*/);
		}
		$iElapsed = $this->ComputeDuration($oObject, $oAttDef, $this->iLastStart, $iNow);
		$this->iTimeSpent = $this->iTimeSpent + $iElapsed;
		if (class_exists('WorkingTimeRecorder'))
		{
			WorkingTimeRecorder::End();
		}

		foreach ($this->aThresholds as $iPercent => &$aThresholdData)
		{
			if (!is_null($aThresholdData['deadline']) && ($iNow > $aThresholdData['deadline']))
			{
				if ($aThresholdData['overrun'] > 0)
				{
					// Accumulate from last start
					$aThresholdData['overrun'] += $iElapsed;
				}
				else
				{
					// First stop after the deadline has been passed
					$iOverrun = $this->ComputeDuration($oObject, $oAttDef, $aThresholdData['deadline'], $iNow);
					$aThresholdData['overrun'] = $iOverrun;
				}
			}
			if ($aThresholdData['overrun'] == 0)
			{
				$aThresholdData['deadline'] = null;
			}
		}

		$this->iLastStart = null;
		$this->iStopped = $iNow;

		return true;
	}
}

/**
 * CheckStopWatchThresholds
 * Implements the automatic actions 
 *
 * @package     itopORM
 */
class CheckStopWatchThresholds implements iBackgroundProcess
{
	public function GetPeriodicity()
	{	
		return 10; // seconds
	}

	public function Process($iTimeLimit)
	{
		$aList = array();
		foreach (MetaModel::GetClasses() as $sClass)
		{
			foreach (MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef)
			{
				if ($oAttDef instanceof AttributeStopWatch)
				{
					foreach ($oAttDef->ListThresholds() as $iThreshold => $aThresholdData)
					{
						$iPercent = $aThresholdData['percent']; // could be different than the index !
		
						$sNow = date(AttributeDateTime::GetSQLFormat());
						$sExpression = "SELECT $sClass WHERE {$sAttCode}_laststart AND {$sAttCode}_{$iThreshold}_triggered = 0 AND {$sAttCode}_{$iThreshold}_deadline < :now";
						$oFilter = DBObjectSearch::FromOQL($sExpression);
						$oSet = new DBObjectSet($oFilter, array(), array('now' => $sNow));
						$oSet->OptimizeColumnLoad(array($sClass => array($sAttCode)));
						while ((time() < $iTimeLimit) && ($oObj = $oSet->Fetch()))
						{
							$sClass = get_class($oObj);

							$aList[] = $sClass.'::'.$oObj->GetKey().' '.$sAttCode.' '.$iThreshold;

							// Execute planned actions
							//
							foreach ($aThresholdData['actions'] as $aActionData)
							{
								$sVerb = $aActionData['verb'];
								$aParams = $aActionData['params'];
								$aValues = array();
								foreach($aParams as $def)
								{
									if (is_string($def))
									{
										// Old method (pre-2.1.0) non typed parameters
										$aValues[] = $def;
									}
									else // if(is_array($def))
									{
										$sParamType = array_key_exists('type', $def) ? $def['type'] : 'string';
										switch($sParamType)
										{
											case 'int':
												$value = (int)$def['value'];
												break;
										
											case 'float':
												$value = (float)$def['value'];
												break;
										
											case 'bool':
												$value = (bool)$def['value'];
												break;
										
											case 'reference':
												$value = ${$def['value']};
												break;
										
											case 'string':
											default:
												$value = (string)$def['value'];
										}
										$aValues[] = $value;
									}
								}
								$aCallSpec = array($oObj, $sVerb);
								call_user_func_array($aCallSpec, $aValues);
							}

							// Mark the threshold as "triggered"
							//
							$oSW = $oObj->Get($sAttCode);
							$oSW->MarkThresholdAsTriggered($iThreshold);
							$oObj->Set($sAttCode, $oSW);

							if ($oObj->IsModified()) {
								CMDBObject::SetCurrentChangeFromParams("Automatic - threshold triggered");

								$oObj->DBUpdate();
							}

							// Activate any existing trigger
							// 
							$sClassList = implode("', '", MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_ALL));

							$oTriggerSet = new DBObjectSet(
								DBObjectSearch::FromOQL("SELECT TriggerOnThresholdReached AS t WHERE t.target_class IN ('$sClassList') AND stop_watch_code MATCHES :stop_watch_code AND threshold_index = :threshold_index"),
								array(), // order by
								array('stop_watch_code' => $sAttCode, 'threshold_index' => $iThreshold)
							);
							while ($oTrigger = $oTriggerSet->Fetch())
							{
								try
								{
									$oTrigger->DoActivate($oObj->ToArgs('this'));
								}
								catch(Exception $e)
								{
									utils::EnrichRaisedException($oTrigger, $e);
								}
							}
						}
					}
				}
			}
		}

		$iProcessed = count($aList);
		return "Triggered $iProcessed threshold(s):".implode(", ", $aList);
	}
}
