<?php declare(strict_types = 0);
/*
** Zabbix
** Copyright (C) 2001-2023 Zabbix SIA
**
** This program is free software; you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation; either version 2 of the License, or
** (at your option) any later version.
**
** 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 General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program; if not, write to the Free Software
** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
**/


class CControllerWidgetItemView extends CControllerWidget {

	public const CHANGE_INDICATOR_UP = 1;
	public const CHANGE_INDICATOR_DOWN = 2;
	public const CHANGE_INDICATOR_UP_DOWN = 3;

	public function __construct() {
		parent::__construct();

		$this->setType(WIDGET_ITEM);
		$this->setValidationRules([
			'name' => 'string',
			'fields' => 'json',
			'dynamic_hostid' => 'db hosts.hostid'
		]);
	}

	protected function doAction() {
		$name = $this->getDefaultName();
		$cells = [];
		$url = null;
		$error = '';
		$fields = $this->getForm()->getFieldsData();
		$history_period = timeUnitToSeconds(CSettingsHelper::get(CSettingsHelper::HISTORY_PERIOD));
		$description = '';
		$value = null;
		$change_indicator = null;
		$time = '';
		$units = '';
		$decimals = null;
		$is_dynamic = ($this->hasInput('dynamic_hostid')
				&& ($this->getContext() === CWidgetConfig::CONTEXT_TEMPLATE_DASHBOARD
					|| $fields['dynamic'] == WIDGET_DYNAMIC_ITEM)
		);

		if ($is_dynamic) {
			$tmp_items = API::Item()->get([
				'output' => ['key_'],
				'itemids' => $fields['itemid'],
				'webitems' => true
			]);

			if ($tmp_items) {
				$options = [
					'output' => ['value_type'],
					'selectValueMap' => ['mappings'],
					'hostids' => [$this->getInput('dynamic_hostid')],
					'webitems' => true,
					'filter' => [
						'key_' => $tmp_items[0]['key_']
					],
					'preservekeys' => true
				];
			}
		}
		else {
			$options = [
				'output' => ['value_type'],
				'selectValueMap' => ['mappings'],
				'itemids' => $fields['itemid'],
				'webitems' => true,
				'preservekeys' => true
			];
		}

		$show = array_flip($fields['show']);

		/*
		 * Select original item name in several cases: if user is in normal dashboards or in template dashboards when
		 * user is in view mode to display that item name in widget name. Item name should be select only if it is not
		 * overwritten. Host name can be attached to item name with delimiter when user is in normal dashboards.
		 */
		if ($this->getInput('name', '') === '') {
			if ($this->getContext() === CWidgetConfig::CONTEXT_DASHBOARD
					|| $this->getContext() === CWidgetConfig::CONTEXT_TEMPLATE_DASHBOARD
					&& $this->hasInput('dynamic_hostid') && $tmp_items) {
				$options['output'] = array_merge($options['output'], ['name']);
			}

			if ($this->getContext() === CWidgetConfig::CONTEXT_DASHBOARD) {
				$options['selectHosts'] = ['name'];
			}
		}

		// Add other fields in case current widget is set in dynamic mode, template dashboard or has a specified host.
		if ($is_dynamic && $tmp_items || !$is_dynamic) {
			// If description contains user macros, we need "itemid" and "hostid" to resolve them.
			if (array_key_exists(WIDGET_ITEM_SHOW_DESCRIPTION, $show)) {
				$options['output'] = array_merge($options['output'], ['itemid', 'hostid']);
			}

			if (array_key_exists(WIDGET_ITEM_SHOW_VALUE, $show) && $fields['units_show'] == 1) {
				$options['output'][] = 'units';
			}
		}

		if ($is_dynamic) {
			if ($tmp_items) {
				$items = API::Item()->get($options);
				$itemid = key($items);
			}
			else {
				$items = [];
			}
		}
		else {
			$items = API::Item()->get($options);

			if ($fields['itemid']) {
				$itemid = $fields['itemid'][0];
			}
		}

		if ($items) {
			// Selecting data from history does not depend on "Show" checkboxes.
			$history = Manager::History()->getLastValues($items, 2, $history_period);
			$value_type = $items[$itemid]['value_type'];

			if ($history) {
				// Get values regardless of show status, since change indicator can be shown independently.
				$last_value = $history[$itemid][0]['value'];

				// Time can be shown independently.
				if (array_key_exists(WIDGET_ITEM_SHOW_TIME, $show)) {
					$time = date(ZBX_FULL_DATE_TIME, (int) $history[$itemid][0]['clock']);
				}

				switch ($value_type) {
					case ITEM_VALUE_TYPE_FLOAT:
					case ITEM_VALUE_TYPE_UINT64:
						// Override item units if needed.
						if (array_key_exists(WIDGET_ITEM_SHOW_VALUE, $show) && $fields['units_show'] == 1) {
							$units = ($fields['units'] === '')
								? $items[$itemid]['units']
								: $fields['units'];
						}

						// Apply unit conversion always because it will also convert values to scientific notation.
						$raw_units = convertUnitsRaw([
							'value' => $last_value,
							'units' => $units,
							'decimals' => $fields['decimal_places']
						]);
						// Get the converted value (this is not the final value).
						$value = $raw_units['value'];

						/*
						 * Get the converted units. If resulting units are empty, this could also mean value was
						 * converted to time.
						 */
						$units = $raw_units['units'];

						/*
						 * In case there is a numeric value, for example 0.001234 and decimal places are set to 2,
						 * convertUnitsRaw would return 0.0012, however in this widget we need to show the exact
						 * number. So we convert the value again which results in 0.00. In case decimal places are set
						 * to 10 (maximum), the value will be converted to 0.0012340000.
						 */
						if ($raw_units['is_numeric']) {
							$value = self::convertNumeric($value, $fields['decimal_places'], $value_type);
						}

						/*
						 * Regardless of unit conversion, separate the decimals from value. In case of scientific
						 * notation, use the whole string after decimal separator.
						 */
						$numeric_formatting = localeconv();
						$pos = strrpos($value, $numeric_formatting['decimal_point']);

						if ($pos !== false) {
							// Include the dot as part of decimal, so it can be shown in different font size.
							$decimals = substr($value, $pos);
							$value = substr($value, 0, $pos);
						}

						if ($items[$itemid]['valuemap']) {
							// Apply value mapping if it is set in item configuration.
							$value = CValueMapHelper::applyValueMap($value_type, $value,
								$items[$itemid]['valuemap']
							);

							// Show of hide change indicator for mapped value.
							if (array_key_exists(WIDGET_ITEM_SHOW_CHANGE_INDICATOR, $show)) {
								$change_indicator = self::CHANGE_INDICATOR_UP_DOWN;
							}
						}
						elseif (array_key_exists(1, $history[$itemid])
								&& array_key_exists(WIDGET_ITEM_SHOW_CHANGE_INDICATOR, $show)) {
							/*
							 * If there is no value mapping and there is more than one value, add up or down change
							 * indicator. Do not show change indicator if value is the same.
							 */
							$prev_value = $history[$itemid][1]['value'];

							if ($last_value > $prev_value) {
								$change_indicator = self::CHANGE_INDICATOR_UP;
							}
							elseif ($last_value < $prev_value) {
								$change_indicator = self::CHANGE_INDICATOR_DOWN;
							}
						}
						break;

					case ITEM_VALUE_TYPE_STR:
					case ITEM_VALUE_TYPE_TEXT:
					case ITEM_VALUE_TYPE_LOG:
						$value = $last_value;

						// Apply value mapping to string type values (same as in Latest Data).
						$mapping = CValueMapHelper::getMappedValue($value_type, $value,
							$items[$itemid]['valuemap']
						);

						if ($mapping !== false) {
							// Currently it is same as in latest data with original value in parenthesis.
							$value = $mapping.' ('.$value.')';
						}

						/*
						 * Even though \n does not affect HTML and would be shown in one line anyway, it is still
						 * better to process this and convert to empty space.
						 */
						$value = str_replace("\n", " ", $value);

						if (array_key_exists(1, $history[$itemid])
								&& array_key_exists(WIDGET_ITEM_SHOW_CHANGE_INDICATOR, $show)) {
							$prev_value = $history[$itemid][1]['value'];

							if ($last_value !== $prev_value) {
								$change_indicator = self::CHANGE_INDICATOR_UP_DOWN;
							}
						}
						break;
				}
			}
			else {
				$value_type = ITEM_VALUE_TYPE_TEXT;

				// Since there is no value, we can still show time.
				if (array_key_exists(WIDGET_ITEM_SHOW_TIME, $show)) {
					$time = date(ZBX_FULL_DATE_TIME);
				}
			}

			if ($this->getInput('name', '') === '') {
				if ($this->getContext() === CWidgetConfig::CONTEXT_DASHBOARD
						|| $this->getContext() === CWidgetConfig::CONTEXT_TEMPLATE_DASHBOARD
						&& $this->hasInput('dynamic_hostid')) {
					// Resolve original item name when user is in normal dashboards or template dashboards view mode.
					$name = $items[$itemid]['name'];
				}

				if ($this->getContext() === CWidgetConfig::CONTEXT_DASHBOARD) {
					$name = $items[$itemid]['hosts'][0]['name'].NAME_DELIMITER.$name;
				}
			}

			/*
			 * It doesn't matter if item has value or not, description can be resolved separately if needed. If item
			 * will have value it will resolve, otherwise it will not.
			 */
			if (array_key_exists(WIDGET_ITEM_SHOW_DESCRIPTION, $show)) {
				// Overwrite item name with the custom description.
				$items[$itemid]['name'] = $fields['description'];

				// Do not resolve macros if using template dashboard. Template dashboards only have edit mode.
				if ($this->getContext() === CWidgetConfig::CONTEXT_DASHBOARD
						|| $this->getContext() === CWidgetConfig::CONTEXT_TEMPLATE_DASHBOARD
						&& $this->hasInput('dynamic_hostid')) {
					$items = CMacrosResolverHelper::resolveWidgetItemNames($items);
				}

				// All macros in item name are resolved here.
				$description = $items[$itemid]['name'];
			}

			$cells = self::arrangeByCells($fields, [
				'description' => $description,
				'value_type' => $value_type,
				'units' => $units,
				'value' => $value,
				'decimals' => $decimals,
				'change_indicator' => $change_indicator,
				'time' => $time,
				'items' => $items,
				'itemid' => $itemid
			]);

			// Use the real item value type.
			$url = (new CUrl('history.php'))
				->setArgument('action',
					($items[$itemid]['value_type'] == ITEM_VALUE_TYPE_FLOAT
							|| $items[$itemid]['value_type'] == ITEM_VALUE_TYPE_UINT64)
						? HISTORY_GRAPH
						: HISTORY_VALUES
				)
				->setArgument('itemids[]', $itemid);
		}
		else {
			$error = _('No permissions to referred object or it does not exist!');
		}

		$this->setResponse(new CControllerResponseData([
			'name' => $this->getInput('name', $name),
			'cells' => $cells,
			'url' => $url,
			'bg_color' => $fields['bg_color'],
			'error' => $error,
			'user' => [
				'debug_mode' => $this->getDebugMode()
			]
		]));
	}

	/**
	 * Convert numeric value using precise decimal points.
	 *
	 * @param string $value       Value to convert.
	 * @param int    $decimals    Number of decimal places.
	 * @param string $value_type  Item value type.
	 *
	 * @return string
	 */
	private static function convertNumeric(string $value, int $decimals, string $value_type): string {
		if ($value >= pow(10, ZBX_FLOAT_DIG)) {
			return sprintf('%.'.ZBX_FLOAT_DIG.'E', $value);
		}

		if ($value_type == ITEM_VALUE_TYPE_FLOAT) {
			$numeric_formatting = localeconv();

			return number_format((float) $value, $decimals, $numeric_formatting['decimal_point'],
				$numeric_formatting['thousands_sep']
			);
		}

		return $value;
	}

	/**
	 * Arrange all widget parts by cells, apply all related configuration settings to each part.
	 *
	 * @static
	 *
	 * @param array       $fields                      Input fields from the form.
	 * @param array       $fields['show']              Flags to show description, value, time and change indicator.
	 * @param int         $fields['desc_v_pos']        Vertical position of the description.
	 * @param int         $fields['desc_h_pos']        Horizontal position of the description.
	 * @param int         $fields['desc_bold']         Font weight of the description (0 - normal, 1 - bold).
	 * @param int         $fields['desc_size']         Font size of the description.
	 * @param string      $fields['desc_color']        Font color of the description.
	 * @param int         $fields['value_v_pos']       Vertical position of the value.
	 * @param int         $fields['value_h_pos']       Horizontal position of the value.
	 * @param int         $fields['value_bold']        Font weight of the value (0 - normal, 1 - bold).
	 * @param int         $fields['value_size']        Font size of the value.
	 * @param string      $fields['value_color']       Font color of the value.
	 * @param int         $fields['units_show']        Display units or not (0 - hide, 1 - show).
	 * @param int         $fields['units_pos']         Position of the units.
	 * @param int         $fields['units_bold']        Font weight of the units (0 - normal, 1 - bold).
	 * @param int         $fields['units_size']        Font size of the units.
	 * @param string      $fields['units_color']       Font color of the units.
	 * @param int         $fields['decimal_size']      Font size of the fraction.
	 * @param int         $fields['time_v_pos']        Vertical position of the time.
	 * @param int         $fields['time_h_pos']        Horizontal position of the time.
	 * @param int         $fields['time_bold']         Font weight of the time (0 - normal, 1 - bold).
	 * @param int         $fields['time_size']         Font size of the time.
	 * @param string      $fields['time_color']        Font color of the time.
	 * @param array       $values                      Array of pre-processed data that needs to be displayed.
	 * @param string      $values['description']       Item description with all macros resolved.
	 * @param string      $values['value_type']        Calculated value type. It can be integer or text.
	 * @param string      $values['units']             Units of the item. Can be empty string if nothing to show.
	 * @param string|null $values['value']             Value of the item or NULL if there is no value.
	 * @param string|null $values['decimals']          Decimal places or NULL if there is no decimals to show.
	 * @param int|null    $values['change_indicator']  Change indicator type or NULL if indicator should not be shown.
	 * @param string      $values['time']              Time when item received the value or current time if no data.
	 * @param array       $values['items']             The original array of items.
	 * @param string      $values['itemid']            Item ID from the host.
	 *
	 * @return array
	 */
	private static function arrangeByCells(array $fields, array $values): array {
		$cells = [];

		$show = array_flip($fields['show']);

		if (array_key_exists(WIDGET_ITEM_SHOW_DESCRIPTION, $show)) {
			$cells[$fields['desc_v_pos']][$fields['desc_h_pos']] = [
				'item_description' => [
					'text' => $values['description'],
					'font_size' => $fields['desc_size'],
					'bold' => ($fields['desc_bold'] == 1),
					'color' => $fields['desc_color']
				]
			];
		}

		if (array_key_exists(WIDGET_ITEM_SHOW_VALUE, $show)) {
			$item_value_cell = [
				'value_type' => $values['value_type']
			];

			if ($fields['units_show'] == 1 && $values['units'] !== '') {
				$item_value_cell['parts']['units'] = [
					'text' => $values['units'],
					'font_size' => $fields['units_size'],
					'bold' => ($fields['units_bold'] == 1),
					'color' => $fields['units_color']
				];
				$item_value_cell['units_pos'] = $fields['units_pos'];
			}

			$item_value_cell['parts']['value'] = [
				'text' => $values['value'],
				'font_size' => $fields['value_size'],
				'bold' => ($fields['value_bold'] == 1),
				'color' => $fields['value_color']
			];

			if ($values['decimals'] !== null) {
				$item_value_cell['parts']['decimals'] = [
					'text' => $values['decimals'],
					'font_size' => $fields['decimal_size'],
					'bold' => ($fields['value_bold'] == 1),
					'color' => $fields['value_color']
				];
			}

			$cells[$fields['value_v_pos']][$fields['value_h_pos']] = [
				'item_value' => $item_value_cell
			];
		}

		if (array_key_exists(WIDGET_ITEM_SHOW_CHANGE_INDICATOR, $show) && $values['change_indicator'] !== null) {
			$colors = [
				self::CHANGE_INDICATOR_UP => $fields['up_color'],
				self::CHANGE_INDICATOR_DOWN => $fields['down_color'],
				self::CHANGE_INDICATOR_UP_DOWN => $fields['updown_color']
			];

			// Change indicator can be displayed with or without value.
			$cells[$fields['value_v_pos']][$fields['value_h_pos']]['item_value']['parts']['change_indicator'] = [
				'type' => $values['change_indicator'],
				'font_size' => ($values['decimals'] !== null)
					? max($fields['value_size'], $fields['decimal_size'])
					: $fields['value_size'],
				'color' => $colors[$values['change_indicator']]
			];
		}

		if (array_key_exists(WIDGET_ITEM_SHOW_TIME, $show)) {
			$cells[$fields['time_v_pos']][$fields['time_h_pos']] = [
				'item_time' => [
					'text' => $values['time'],
					'font_size' => $fields['time_size'],
					'bold' => ($fields['time_bold'] == 1),
					'color' => $fields['time_color']
				]
			];
		}

		// Sort data column blocks in order - left, center, right.
		foreach ($cells as &$row) {
			ksort($row);
		}
		unset($row);

		return $cells;
	}
}
