<?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/>
 *
 */

/**
 * Created by PhpStorm.
 * Date: 24/08/2018
 * Time: 14:35
 */
class ormSet
{
	protected $sClass; // class of the field
	protected $sAttCode; // attcode of the field
	protected $aOriginalObjects = null;
	protected $m_bDisplayPartial = false;

	/**
	 * Object from the original set, minus the removed objects
	 */
	protected $aPreserved = [];

	/**
	 * New items
	 */
	protected $aAdded = [];

	/**
	 * Removed items
	 */
	protected $aRemoved = [];

	/**
	 * Modified items (mass edit)
	 */
	protected $aModified = [];

	/**
	 * @var int Max number of tags in collection
	 */
	protected $iLimit;

	/**
	 * __toString magical function overload.
	 */
	public function __toString()
	{
		$aValue = $this->GetValues();
		if (!empty($aValue)) {
			return implode(', ', $aValue);
		} else {
			return ' ';
		}
	}

	/**
	 * ormSet constructor.
	 *
	 * @param string $sClass
	 * @param string $sAttCode
	 * @param int $iLimit
	 *
	 * @throws \Exception
	 */
	public function __construct($sClass, $sAttCode, $iLimit = 12)
	{
		$this->sAttCode = $sAttCode;

		$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
		if (!$oAttDef instanceof AttributeSet) {
			throw new Exception("ormSet: field {$sClass}:{$sAttCode} is not a set");
		}
		$this->sClass = $sClass;
		$this->iLimit = $iLimit;
	}

	/**
	 * @return string
	 */
	public function GetClass()
	{
		return $this->sClass;
	}

	/**
	 * @return string
	 */
	public function GetAttCode()
	{
		return $this->sAttCode;
	}

	/**
	 *
	 * @param string[] $aItems
	 *
	 * @throws \CoreException
	 * @throws \CoreUnexpectedValue when a code is invalid
	 */
	public function SetValues($aItems)
	{
		if (!is_array($aItems)) {
			throw new CoreUnexpectedValue("Wrong value {$aItems} for {$this->sClass}:{$this->sAttCode}");
		}

		$aValues = [];
		$iCount = 0;
		$bError = false;
		foreach ($aItems as $sItem) {
			$iCount++;
			if (($this->iLimit != 0) && ($iCount > $this->iLimit)) {
				$bError = true;
				continue;
			}
			$aValues[] = $sItem;
		}

		$this->aPreserved = &$aValues;
		$this->aRemoved = [];
		$this->aAdded = [];
		$this->aModified = [];
		$this->aOriginalObjects = $aValues;

		if ($bError) {
			throw new CoreException("Maximum number of items ({$this->iLimit}) reached for {$this->sClass}:{$this->sAttCode}");
		}
	}

	public function Count()
	{
		return count($this->aPreserved) + count($this->aAdded) - count($this->aRemoved);
	}

	/**
	 * @return array of codes
	 */
	public function GetValues()
	{
		$aValues = array_merge($this->aPreserved, $this->aAdded);
		sort($aValues);
		return $aValues;
	}

	public function GetLabels()
	{
		$aLabels = [];
		$aValues = $this->GetValues();
		foreach ($aValues as $sValue) {
			$aLabels[$sValue] = $sValue;
		}
		return $aLabels;
	}

	/**
	 * @return array of tag labels indexed by code for only the added tags
	 */
	private function GetAdded()
	{
		return $this->aAdded;
	}

	/**
	 * @return array of tag labels indexed by code for only the removed tags
	 */
	private function GetRemoved()
	{
		return $this->aRemoved;
	}

	/** Get the delta with another ItemSet
	 *
	 *  $aDelta['added] = array of tag codes for only the added tags
	 *  $aDelta['removed'] = array of tag codes for only the removed tags
	 *
	 * @param \ormSet $oOtherSet
	 *
	 * @return array
	 *
	 * @throws \CoreException
	 * @throws \CoreUnexpectedValue
	 * @throws \Exception
	 */
	public function GetDelta(ormSet $oOtherSet)
	{
		$oSet = new ormSet($this->sClass, $this->sAttCode, $this->iLimit);
		// Set the initial value
		$aOrigItems = $this->GetValues();
		$oSet->SetValues($aOrigItems);

		// now remove everything
		foreach ($aOrigItems as $oItem) {
			$oSet->Remove($oItem);
		}

		// now add the tags of the other ItemSet
		foreach ($oOtherSet->GetValues() as $oItem) {
			$oSet->Add($oItem);
		}

		$aDelta = [];
		$aDelta['added'] = $oSet->GetAdded();
		$aDelta['removed'] = $oSet->GetRemoved();

		return $aDelta;
	}

	/**
	 * @return string[] list of codes for partial entries
	 */
	public function GetModified()
	{
		return $this->aModified;
	}

	/**
	 * Apply a delta to the current ItemSet
	 *  $aDelta['added] = array of added items
	 *  $aDelta['removed'] = array of removed items
	 *
	 * @param $aDelta
	 *
	 * @throws \CoreException
	 */
	public function ApplyDelta($aDelta)
	{
		if (isset($aDelta['removed'])) {
			foreach ($aDelta['removed'] as $oItem) {
				$this->Remove($oItem);
			}
		}
		if (isset($aDelta['added'])) {
			foreach ($aDelta['added'] as $oItem) {
				$this->Add($oItem);
			}
		}

		// Reset the object
		$this->SetValues($this->GetValues());
	}

	/**
	 * @param string $oItem
	 *
	 * @throws \CoreException
	 */
	public function Add($oItem)
	{
		if (($this->iLimit != 0) && ($this->Count() > $this->iLimit)) {
			throw new CoreException("Maximum number of items ({$this->iLimit}) reached for {$this->sClass}:{$this->sAttCode}");
		}
		if ($this->IsItemInList($this->aPreserved, $oItem) || $this->IsItemInList($this->aAdded, $oItem)) {
			// nothing to do, already existing tag
			return;
		}
		// if removed and added again
		if (($this->RemoveItemFromList($this->aRemoved, $oItem)) !== false) {
			// put it back into preserved
			$this->aPreserved[] = $oItem;
			// no need to add it to aModified : was already done when calling RemoveItem method
		} else {
			$this->aAdded[] = $oItem;
			$this->aModified[] = $oItem;
		}
	}

	/**
	 * @param $oItem
	 */
	public function Remove($oItem)
	{
		if ($this->IsItemInList($this->aRemoved, $oItem)) {
			// nothing to do, already removed tag
			return;
		}

		if ($this->RemoveItemFromList($this->aAdded, $oItem) !== false) {
			$this->aModified[] = $oItem;

			return; // if present in added, can't be in preserved !
		}

		if ($this->RemoveItemFromList($this->aPreserved, $oItem) !== false) {
			$this->aModified[] = $oItem;
			$this->aRemoved[] = $oItem;
		}
	}

	private function IsItemInList($aItemList, $oItem)
	{
		return in_array($oItem, $aItemList);
	}

	/**
	 * @param \DBObject[] $aItemList
	 * @param $oItem
	 *
	 * @return bool|\DBObject false if not found, else the removed element
	 */
	private function RemoveItemFromList(&$aItemList, $oItem)
	{
		if (!($this->IsItemInList($aItemList, $oItem))) {
			return false;
		}
		foreach ($aItemList as $index => $value) {
			if ($value === $oItem) {
				unset($aItemList[$index]);
				return $oItem;
			}
		}

		return false;
	}

	/**
	 * Populates the added and removed arrays for bulk edit
	 *
	 * @param string[] $aItems
	 *
	 * @throws \CoreException
	 */
	public function GenerateDiffFromArray($aItems)
	{
		foreach ($this->GetValues() as $oCurrentItem) {
			if (!in_array($oCurrentItem, $aItems)) {
				$this->Remove($oCurrentItem);
			}
		}

		foreach ($aItems as $oNewItem) {
			$this->Add($oNewItem);
		}
	}

	/**
	 * Compare Item Set
	 *
	 * @param \ormSet $other
	 *
	 * @return bool true if same tag set
	 */
	public function Equals(ormSet $other)
	{
		return implode(', ', $this->GetValues()) === implode(', ', $other->GetValues());
	}

	/**
	 * @return bool
	 */
	public function DisplayPartial()
	{
		return $this->m_bDisplayPartial;
	}

	/**
	 * @param bool $m_bDisplayPartial
	 */
	public function SetDisplayPartial($m_bDisplayPartial)
	{
		$this->m_bDisplayPartial = $m_bDisplayPartial;
	}
}
