<?php

/**
 * @package   bdk/debug
 * @author    Brad Kent <bkfake-github@yahoo.com>
 * @license   http://opensource.org/licenses/MIT MIT
 * @copyright 2014-2025 Brad Kent
 * @since     2.3
 */

namespace bdk\Debug\Utility;

/**
 * Utility to convert error level mask to user friendly string
 *
 * @see http://php.net/manual/en/errorfunc.constants.php
 * @see https://github.com/maximivanov/php-error-reporting-calculator javascript code inspiration
 */
class ErrorLevel
{
    /**
     * Get error level constants understood by specified php version
     *
     * @param string $phpVer (PHP_VERSION) PHP version
     *
     * @return array<string,int>
     */
    public static function getConstants($phpVer = null)
    {
        $phpVer = $phpVer ?: PHP_VERSION;
        $phpVer = self::normalizePhpVer($phpVer);
        // @phpcs:ignore SlevomatCodingStandard.Arrays.AlphabeticallySortedByKeys.IncorrectKeyOrder
        $constants = array(
            'E_ERROR' => 1,
            'E_WARNING' => 2,
            'E_PARSE' => 4,
            'E_NOTICE' => 8,
            'E_CORE_ERROR' => 16,
            'E_CORE_WARNING' => 32,
            'E_COMPILE_ERROR' => 64,
            'E_COMPILE_WARNING' => 128,
            'E_USER_ERROR' => 256,
            'E_USER_WARNING' => 512,
            'E_USER_NOTICE' => 1024,
            'E_STRICT' => self::getConstValStrict($phpVer),
            'E_RECOVERABLE_ERROR' => \version_compare($phpVer, '5.2.0', '>=') ? 4096 : null,
            'E_DEPRECATED' => \version_compare($phpVer, '5.3.0', '>=') ? 8192 : null,
            'E_USER_DEPRECATED' => \version_compare($phpVer, '5.3.0', '>=') ? 16384 : null,
            'E_ALL' => null, // calculated below
        );
        $constants = \array_filter($constants);
        $constants['E_ALL'] = static::calculateEall($constants, $phpVer);
        return $constants;
    }

    /**
     * Convert PHP error-level integer (bitmask) to constant bitwise representation
     *
     * @param int    $errorReportingLevel Error Level (bitmask) value
     * @param string $phpVer              (PHP_VERSION) php Version
     * @param bool   $explicitStrict      if level === E_ALL, always include/exclude E_STRICT for disambiguation / portability
     *                                      defaults to true if $phpVer < 8.0, false otherwise
     *
     * @return string
     */
    public static function toConstantString($errorReportingLevel = null, $phpVer = null, $explicitStrict = null)
    {
        $errorReportingLevel = $errorReportingLevel === null
            ? \error_reporting()
            : $errorReportingLevel;
        $phpVer = $phpVer ?: PHP_VERSION;
        if (\is_bool($explicitStrict) === false) {
            $explicitStrict = \version_compare($phpVer, '8.0.0', '<');
        }
        $allConstants = self::getConstants($phpVer); // includes E_ALL
        /**
         * @var array{
         *    off: string[],
         *    on: string[],
         * }
         */
        $flags = array(
            'off' => [],
            'on' => \array_keys(self::filterConstantsByLevel($allConstants, $errorReportingLevel)), // excludes E_ALL
        );
        $eAll = $allConstants['E_ALL'];
        unset($allConstants['E_ALL']);
        if (\count($flags['on']) > \count($allConstants) / 2) {
            $flags = self::getNegateFlags($errorReportingLevel, $allConstants, $eAll, $explicitStrict);
        }
        $string = self::joinOnOff($flags['on'], $flags['off']);
        return $string ?: '0';
    }

    /**
     * Calculate E_ALL for given php version
     *
     * E_ALL value:
     *   >= 8.4  30719 (no more E_STRICT)
     *   >= 5.4: 32767
     *      5.3: 30719 (doesn't include E_STRICT)
     *      5.2: 6143 (doesn't include E_STRICT)
     *    < 5.2: 2047 (doesn't include E_STRICT)
     *
     * @param array<string,int> $constants constant values (sans E_ALL)
     * @param string            $phpVer    php version
     *
     * @return int
     */
    private static function calculateEall(array $constants, $phpVer)
    {
        $eAll = \array_sum($constants);
        if (isset($constants['E_STRICT']) && \version_compare($phpVer, '5.4.0', '<')) {
            // E_STRICT not included in E_ALL until 5.4
            $eAll -= $constants['E_STRICT'];
        }
        return $eAll;
    }

    /**
     * Get all constants included in specified error level
     * excludes E_ALL
     *
     * @param array<string,int> $constants constName => value array
     * @param int               $level     error level
     *
     * @return array
     */
    private static function filterConstantsByLevel(array $constants, $level)
    {
        foreach ($constants as $constName => $constValue) {
            if (!self::inBitmask($constValue, $level)) {
                unset($constants[$constName]);
            }
        }
        unset($constants['E_ALL']);
        return $constants;
    }

    /**
     * Get the value of E_STRICT constant for givent php version
     *
     * @param string $phpVer (PHP_VERSION) PHP version
     *
     * @return int|null
     */
    private static function getConstValStrict($phpVer)
    {
        return \version_compare($phpVer, '5.0.0', '>=') && \version_compare($phpVer, '8.4.0-dev', '<')
            ? 2048
            : null;
    }

    /**
     * Get on/off flags starting with E_ALL
     *
     * @param int               $errorReportingLevel Error Level (bitmask) value
     * @param array<string,int> $allConstants        constName => $constValue array
     * @param int               $eAll                E_ALL value for specified php version
     * @param bool              $explicitStrict      explicitly specify E_STRICT?
     *
     * @return array
     */
    private static function getNegateFlags($errorReportingLevel, $allConstants, $eAll, $explicitStrict)
    {
        $flags = array(
            'off' => [],
            'on' => ['E_ALL'],
        );
        foreach ($allConstants as $constName => $constValue) {
            if (!self::shouldIncludeInFlags($constName, $constValue, $eAll, $errorReportingLevel, $explicitStrict)) {
                continue;
            }
            $inclInLevel = self::inBitmask($constValue, $errorReportingLevel);
            $onOrOff = $inclInLevel
                ? 'on'
                : 'off';
            $flags[$onOrOff][] = $constName;
        }
        return $flags;
    }

    /**
     * Test if value is incl in bitmask
     *
     * @param int $value   value to check
     * @param int $bitmask bitmask
     *
     * @return bool
     */
    private static function inBitmask($value, $bitmask)
    {
        return ($bitmask & $value) === $value;
    }

    /**
     * Build error-level string representation from on & off flags
     *
     * @param string[] $flagsOn  constant names
     * @param string[] $flagsOff constant names
     *
     * @return string
     */
    private static function joinOnOff(array $flagsOn, array $flagsOff)
    {
        $flagsOn = \count($flagsOn) > 1 && $flagsOff
            ? '( ' . \implode(' | ', $flagsOn) . ' )'
            : \implode(' | ', $flagsOn);
        $flagsOff = \implode('', \array_map(static function ($flag) {
            return ' & ~' . $flag;
        }, $flagsOff));
        return $flagsOn . $flagsOff;
    }

    /**
     * Make sur phpVersion string is of form x.x.x
     * version_compare considers 1 < 1.0 < 1.0.0
     *
     * @param string $phpVer Php version string
     *
     * @return string
     */
    private static function normalizePhpVer($phpVer)
    {
        return \preg_match('/^\d+\.\d+$/', $phpVer)
            ? $phpVer . '.0'
            : $phpVer;
    }

    /**
     * Determine if a constant should be included in the flags
     *
     * @param string $constName           Constant name
     * @param int    $constValue          Constant value
     * @param int    $eAll                E_ALL value
     * @param int    $errorReportingLevel Error reporting level
     * @param bool   $explicitStrict      Whether to explicitly include E_STRICT
     *
     * @return bool
     */
    private static function shouldIncludeInFlags($constName, $constValue, $eAll, $errorReportingLevel, $explicitStrict)
    {
        $isExplicit = $explicitStrict && $constName === 'E_STRICT';
        // only thing that may not be in E_ALL is E_STRICT
        $inclInEall = self::inBitmask($constValue, $eAll);
        $inclInLevel = self::inBitmask($constValue, $errorReportingLevel);

        // Include if
        //   inclusion in E_ALL differs from inclusion in level,
        //   or if it's E_STRICT and explicitStrict is true
        return ($inclInEall !== $inclInLevel) || $isExplicit;
    }
}
