<?php

// SPDX-FileCopyrightText: 2021 Icinga GmbH <https://icinga.com>
// SPDX-License-Identifier: GPL-3.0-or-later

namespace Icinga\Module\Icingadb\Util;

use Icinga\Module\Icingadb\Common\ServiceStates;
use Icinga\Web\Widget\Chart\InlinePie;
use InvalidArgumentException;
use LogicException;

class PerfData
{
    public const PERFDATA_OK = 'ok';
    public const PERFDATA_WARNING = 'warning';
    public const PERFDATA_CRITICAL = 'critical';

    /**
     * The performance data value being parsed
     *
     * @var string
     */
    protected $perfdataValue;

    /**
     * Unit of measurement (UOM)
     *
     * @var string
     */
    protected $unit;

    /**
     * The label
     *
     * @var string
     */
    protected $label;

    /**
     * The value
     *
     * @var ?float
     */
    protected $value;

    /**
     * The minimum value
     *
     * @var ?float
     */
    protected $minValue;

    /**
     * The maximum value
     *
     * @var ?float
     */
    protected $maxValue;

    /**
     * The WARNING threshold
     *
     * @var ThresholdRange
     */
    protected $warningThreshold;

    /**
     * The CRITICAL threshold
     *
     * @var ThresholdRange
     */
    protected $criticalThreshold;

    /**
     * The raw value
     *
     * @var ?string
     */
    protected $rawValue;

    /**
     * The raw minimum value
     *
     * @var ?string
     */
    protected $rawMinValue;

    /**
     * The raw maximum value
     *
     * @var ?string
     */
    protected $rawMaxValue;

    /**
     * Create a new PerfData object based on the given performance data label and value
     *
     * @param   string      $label      The perfdata label
     * @param   string      $value      The perfdata value
     */
    public function __construct(string $label, string $value)
    {
        $this->perfdataValue = $value;
        $this->label = $label;
        $this->parse();

        if ($this->unit === '%') {
            if ($this->minValue === null) {
                $this->minValue = 0.0;
            }
            if ($this->maxValue === null) {
                $this->maxValue = 100.0;
            }
        }

        $warn = $this->warningThreshold->getMax();
        if ($warn !== null) {
            $crit = $this->criticalThreshold->getMax();
            if ($crit !== null && $warn > $crit) {
                $this->warningThreshold->setInverted();
                $this->criticalThreshold->setInverted();
            }
        }
    }

    /**
     * Return a new PerfData object based on the given performance data key=value pair
     *
     * @param   string      $perfdata       The key=value pair to parse
     *
     * @return  PerfData
     *
     * @throws  InvalidArgumentException    In case the given performance data has no content or a invalid format
     */
    public static function fromString(string $perfdata): self
    {
        if (empty($perfdata)) {
            throw new InvalidArgumentException('PerfData::fromString expects a string with content');
        } elseif (strpos($perfdata, '=') === false) {
            throw new InvalidArgumentException(
                'PerfData::fromString expects a key=value formatted string. Got "' . $perfdata . '" instead'
            );
        }

        list($label, $value) = explode('=', $perfdata, 2);
        return new static(trim($label), trim($value));
    }

    /**
     * Return whether this performance data's value is a number
     *
     * @return  bool    True in case it's a number, otherwise False
     */
    public function isNumber(): bool
    {
        return $this->unit === null;
    }

    /**
     * Return whether this performance data's value are seconds
     *
     * @return  bool    True in case it's seconds, otherwise False
     */
    public function isSeconds(): bool
    {
        return $this->unit === 's';
    }

    /**
     * Return whether this performance data's value is a temperature
     *
     * @return  bool    True in case it's temperature, otherwise False
     */
    public function isTemperature(): bool
    {
        return in_array($this->unit, array('C', 'F', 'K'));
    }

    /**
     * Return whether this performance data's value is in percentage
     *
     * @return  bool    True in case it's in percentage, otherwise False
     */
    public function isPercentage(): bool
    {
        return $this->unit === '%';
    }

    /**
     * Get whether this perf data's value is in packets
     *
     * @return bool   True in case it's in packets
     */
    public function isPackets(): bool
    {
        return $this->unit === 'packets';
    }

    /**
     * Get whether this perf data's value is in lumen
     *
     * @return bool
     */
    public function isLumens(): bool
    {
        return $this->unit === 'lm';
    }

    /**
     * Get whether this perf data's value is in decibel-milliwatts
     *
     * @return bool
     */
    public function isDecibelMilliWatts(): bool
    {
        return $this->unit === 'dBm';
    }

    /**
     * Get whether this data's value is in bits
     *
     * @return bool
     */
    public function isBits(): bool
    {
        return $this->unit === 'b';
    }

    /**
     * Return whether this performance data's value is in bytes
     *
     * @return  bool    True in case it's in bytes, otherwise False
     */
    public function isBytes(): bool
    {
        return $this->unit === 'B';
    }

    /**
     * Get whether this data's value is in watt hours
     *
     * @return bool
     */
    public function isWattHours(): bool
    {
        return $this->unit === 'Wh';
    }

    /**
     * Get whether this data's value is in watt
     *
     * @return bool
     */
    public function isWatts(): bool
    {
        return $this->unit === 'W';
    }

    /**
     * Get whether this data's value is in ampere
     *
     * @return bool
     */
    public function isAmperes(): bool
    {
        return $this->unit === 'A';
    }

    /**
     * Get whether this data's value is in ampere seconds
     *
     * @return bool
     */
    public function isAmpSeconds(): bool
    {
        return $this->unit === 'As';
    }

    /**
     * Get whether this data's value is in volts
     *
     * @return bool
     */
    public function isVolts(): bool
    {
        return $this->unit === 'V';
    }

    /**
     * Get whether this data's value is in ohm
     *
     * @return bool
     */
    public function isOhms(): bool
    {
        return $this->unit === 'O';
    }

    /**
     * Get whether this data's value is in grams
     *
     * @return bool
     */
    public function isGrams(): bool
    {
        return $this->unit === 'g';
    }

    /**
     * Get whether this data's value is in Litters
     *
     * @return bool
     */
    public function isLiters(): bool
    {
        return $this->unit === 'l';
    }

    /**
     * Return whether this performance data's value is a counter
     *
     * @return  bool    True in case it's a counter, otherwise False
     */
    public function isCounter(): bool
    {
        return $this->unit === 'c';
    }

    /**
     * Returns whether it is possible to display a visual representation
     *
     * @return  bool    True when the perfdata is visualizable
     */
    public function isVisualizable(): bool
    {
        return isset($this->minValue, $this->maxValue, $this->value) && $this->isValid();
    }

    /**
     * Return this perfomance data's label
     */
    public function getLabel(): string
    {
        return $this->label;
    }

    /**
     * Return the value or null if it is unknown (U)
     *
     * @return  null|float
     */
    public function getValue()
    {
        return $this->value;
    }

    /**
     * Return the unit as a string
     *
     * @return ?string
     */
    public function getUnit()
    {
        return $this->unit;
    }

    /**
     * Return the value as percentage (0-100)
     *
     * @return  null|float
     */
    public function getPercentage()
    {
        if ($this->isPercentage()) {
            return $this->value;
        }

        if ($this->maxValue !== null) {
            $minValue = $this->minValue !== null ? $this->minValue : 0.0;
            if ($this->maxValue == $minValue) {
                return null;
            }

            if ($this->value > $minValue) {
                return (($this->value - $minValue) / ($this->maxValue - $minValue)) * 100;
            }
        }
    }

    /**
     * Return this performance data's warning treshold
     *
     * @return  ThresholdRange
     */
    public function getWarningThreshold(): ThresholdRange
    {
        return $this->warningThreshold;
    }

    /**
     * Return this performance data's critical treshold
     *
     * @return  ThresholdRange
     */
    public function getCriticalThreshold(): ThresholdRange
    {
        return $this->criticalThreshold;
    }

    /**
     * Return the minimum value or null if it is not available
     *
     * @return ?float
     */
    public function getMinimumValue()
    {
        return $this->minValue;
    }

    /**
     * Return the maximum value or null if it is not available
     *
     * @return  null|float
     */
    public function getMaximumValue()
    {
        return $this->maxValue;
    }

    /**
     * Return this performance data as string
     *
     * @return  string
     */
    public function __toString()
    {
        return $this->formatLabel();
    }

    /**
     * Parse the current performance data value
     *
     * @todo    Handle optional min/max if UOM == %
     */
    protected function parse()
    {
        $parts = explode(';', $this->perfdataValue);

        $matches = array();
        if (preg_match('@^(U|-?(?:\d+)?(?:\.\d+)?)([a-zA-TV-Z%°]{1,3})$@u', $parts[0], $matches)) {
            $this->unit = $matches[2];
            $value = $matches[1];
        } else {
            $value = $parts[0];
        }

        if (! is_numeric($value)) {
            if ($value !== 'U') {
                $this->rawValue = $parts[0];
            }

            $this->value = null;
        } else {
            $this->value = floatval($value);
        }

        switch (count($parts)) {
            /* @noinspection PhpMissingBreakStatementInspection */
            case 5:
                if ($parts[4] !== '') {
                    if (is_numeric($parts[4])) {
                        $this->maxValue = floatval($parts[4]);
                    } else {
                        $this->rawMaxValue = $parts[4];
                    }
                }
            /* @noinspection PhpMissingBreakStatementInspection */
            case 4:
                if ($parts[3] !== '') {
                    if (is_numeric($parts[3])) {
                        $this->minValue = floatval($parts[3]);
                    } else {
                        $this->rawMinValue = $parts[3];
                    }
                }
            /* @noinspection PhpMissingBreakStatementInspection */
            case 3:
                $this->criticalThreshold = ThresholdRange::fromString(trim($parts[2]));
            // Fallthrough
            case 2:
                $this->warningThreshold = ThresholdRange::fromString(trim($parts[1]));
        }

        if ($this->warningThreshold === null) {
            $this->warningThreshold = new ThresholdRange();
        }
        if ($this->criticalThreshold === null) {
            $this->criticalThreshold = new ThresholdRange();
        }
    }

    protected function calculatePieChartData(): array
    {
        $rawValue = $this->getValue();
        $minValue = $this->getMinimumValue() !== null ? $this->getMinimumValue() : 0;
        $usedValue = ($rawValue - $minValue);

        $green = $orange = $red = 0;

        if ($this->criticalThreshold->contains($rawValue)) {
            if ($this->warningThreshold->contains($rawValue)) {
                $green = $usedValue;
            } else {
                $orange = $usedValue;
            }
        } else {
            $red = $usedValue;
        }

        return array($green, $orange, $red, ($this->getMaximumValue() - $minValue) - $usedValue);
    }


    public function asInlinePie(): InlinePie
    {
        if (! $this->isVisualizable()) {
            throw new LogicException('Cannot calculate piechart data for unvisualizable perfdata entry.');
        }

        $data = $this->calculatePieChartData();
        $pieChart = new InlinePie($data, $this);
        $pieChart->setColors(array('#44bb77', '#ffaa44', '#ff5566', '#ddccdd'));

        return $pieChart;
    }

    /**
     * Format the given value depending on the currently used unit
     */
    protected function format($value)
    {
        if ($value === null) {
            return null;
        }

        if ($value instanceof ThresholdRange) {
            if (! $value->isValid()) {
                return (string) $value;
            }

            if ($value->getMin()) {
                return (string) $value;
            }

            $max = $value->getMax();
            return $max === null ? '' : $this->format($max);
        }

        switch (true) {
            case $this->isPercentage():
                return (string) $value . '%';
            case $this->isPackets():
                return (string) $value . 'packets';
            case $this->isLumens():
                return (string) $value . 'lm';
            case $this->isDecibelMilliWatts():
                return (string) $value . 'dBm';
            case $this->isCounter():
                return (string) $value . 'c';
            case $this->isTemperature():
                return (string) $value . $this->unit;
            case $this->isBits():
                return PerfDataFormat::bits($value);
            case $this->isBytes():
                return PerfDataFormat::bytes($value);
            case $this->isSeconds():
                return PerfDataFormat::seconds($value);
            case $this->isWatts():
                return PerfDataFormat::watts($value);
            case $this->isWattHours():
                return PerfDataFormat::wattHours($value);
            case $this->isAmperes():
                return PerfDataFormat::amperes($value);
            case $this->isAmpSeconds():
                return PerfDataFormat::ampereSeconds($value);
            case $this->isVolts():
                return PerfDataFormat::volts($value);
            case $this->isOhms():
                return PerfDataFormat::ohms($value);
            case $this->isGrams():
                return PerfDataFormat::grams($value);
            case $this->isLiters():
                return PerfDataFormat::liters($value);
            case ! is_numeric($value):
                return $value;
            default:
                return number_format($value, 2) . ($this->unit !== null ? ' ' . $this->unit : '');
        }
    }

    /**
     * Format the title string that represents this perfdata set
     *
     * @param bool $html
     *
     * @return string
     */
    public function formatLabel(bool $html = false): string
    {
        return sprintf(
            $html ? '<b>%s %s</b> (%s%%)' : '%s %s (%s%%)',
            htmlspecialchars($this->getLabel()),
            $this->format($this->value),
            number_format($this->getPercentage() ?? 0, 2)
        );
    }

    public function toArray(): array
    {
        return [
            'label' => $this->getLabel(),
            'value' => isset($this->value) ? $this->format($this->value) : $this->rawValue,
            'min'   => (string) (
                ! $this->isPercentage()
                ? (isset($this->minValue) ? $this->format($this->minValue) : $this->rawMinValue)
                : null
            ),
            'max'   => (string) (
                ! $this->isPercentage()
                ? (isset($this->maxValue) ? $this->format($this->maxValue) : $this->rawMaxValue)
                : null
            ),
            'warn'  => $this->format($this->warningThreshold),
            'crit'  => $this->format($this->criticalThreshold)
        ];
    }

    /**
     * Return the state indicated by this perfdata
     *
     * @return int
     */
    public function getState(): int
    {
        if (! is_numeric($this->value)) {
            return ServiceStates::UNKNOWN;
        }

        if (! $this->criticalThreshold->contains($this->value)) {
            return ServiceStates::CRITICAL;
        }

        if (! $this->warningThreshold->contains($this->value)) {
            return ServiceStates::WARNING;
        }

        return ServiceStates::OK;
    }

    /**
     * Return whether the state indicated by this perfdata is worse than
     * the state indicated by the other perfdata
     * CRITICAL > UNKNOWN > WARNING > OK
     *
     * @param PerfData $rhs     the other perfdata
     *
     * @return bool
     */
    public function worseThan(PerfData $rhs): bool
    {
        if (($state = $this->getState()) === ($rhsState = $rhs->getState())) {
            return $this->getPercentage() > $rhs->getPercentage();
        }

        if ($state === ServiceStates::CRITICAL) {
            return true;
        }

        if ($state === ServiceStates::UNKNOWN) {
            return $rhsState !== ServiceStates::CRITICAL;
        }

        if ($state === ServiceStates::WARNING) {
            return $rhsState === ServiceStates::OK;
        }

        return false;
    }

    /**
     * Returns whether the performance data can be evaluated
     *
     * @return bool
     */
    public function isValid(): bool
    {
        return ! isset($this->rawValue)
            && ! isset($this->rawMinValue)
            && ! isset($this->rawMaxValue)
            && $this->criticalThreshold->isValid()
            && $this->warningThreshold->isValid();
    }
}
