<?php
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */
namespace Magento\Framework;

use Magento\Framework\Profiler\Driver\Factory;
use Magento\Framework\Profiler\DriverInterface;

/**
 * Static class that represents profiling tool
 *
 * @api
 * @since 100.0.2
 */
class Profiler
{
    /**
     * Separator literal to assemble timer identifier from timer names
     */
    const NESTING_SEPARATOR = '->';

    /**
     * Whether profiling is active or not
     *
     * @var bool
     */
    private static $_enabled = false;

    /**
     * Nesting path that represents namespace to resolve timer names
     *
     * @var string[]
     */
    private static $_currentPath = [];

    /**
     * Count of elements in $_currentPath
     *
     * @var int
     */
    private static $_pathCount = 0;

    /**
     * Index for counting of $_pathCount for timer names
     *
     * @var array
     */
    private static $_pathIndex = [];

    /**
     * Collection for profiler drivers.
     *
     * @var DriverInterface[]
     */
    private static $_drivers = [];

    /**
     * List of default tags.
     *
     * @var array
     */
    private static $_defaultTags = [];

    /**
     * Collection of tag filters.
     *
     * @var array
     */
    private static $_tagFilters = [];

    /**
     * Has tag filters flag for faster checks of filters availability.
     *
     * @var bool
     */
    private static $_hasTagFilters = false;

    /**
     * Set default tags
     *
     * @param array $tags
     * @return void
     */
    public static function setDefaultTags(array $tags)
    {
        self::$_defaultTags = $tags;
    }

    /**
     * Add tag filter.
     *
     * @param string $tagName
     * @param string $tagValue
     * @return void
     */
    public static function addTagFilter($tagName, $tagValue)
    {
        if (!isset(self::$_tagFilters[$tagName])) {
            self::$_tagFilters[$tagName] = [];
        }
        self::$_tagFilters[$tagName][] = $tagValue;
        self::$_hasTagFilters = true;
    }

    /**
     * Check tags with tag filters.
     *
     * @param array|null $tags
     * @return bool
     */
    private static function _checkTags(array $tags = null)
    {
        if (self::$_hasTagFilters) {
            if (is_array($tags)) {
                $keysToCheck = array_intersect(array_keys(self::$_tagFilters), array_keys($tags));
                if ($keysToCheck) {
                    foreach ($keysToCheck as $keyToCheck) {
                        if (in_array($tags[$keyToCheck], self::$_tagFilters[$keyToCheck])) {
                            return true;
                        }
                    }
                }
            }
            return false;
        }
        return true;
    }

    /**
     * Add profiler driver.
     *
     * @param DriverInterface $driver
     * @return void
     */
    public static function add(DriverInterface $driver)
    {
        self::$_drivers[] = $driver;
        self::enable();
    }

    /**
     * Retrieve unique identifier among all timers
     *
     * @param string|null $timerName Timer name
     * @return string
     */
    private static function _getTimerId($timerName = null)
    {
        if (!self::$_currentPath) {
            return (string)$timerName;
        } elseif ($timerName) {
            return implode(self::NESTING_SEPARATOR, self::$_currentPath) . self::NESTING_SEPARATOR . $timerName;
        } else {
            return implode(self::NESTING_SEPARATOR, self::$_currentPath);
        }
    }

    /**
     * Get tags list.
     *
     * @param array|null $tags
     * @return array|null
     */
    private static function _getTags(array $tags = null)
    {
        if (self::$_defaultTags) {
            return (array)$tags + self::$_defaultTags;
        } else {
            return $tags;
        }
    }

    /**
     * Enable profiling.
     *
     * Any call to profiler does nothing until profiler is enabled.
     *
     * @return void
     */
    public static function enable()
    {
        self::$_enabled = true;
    }

    /**
     * Disable profiling.
     *
     * Any call to profiler is silently ignored while profiler is disabled.
     *
     * @return void
     */
    public static function disable()
    {
        self::$_enabled = false;
    }

    /**
     * Get profiler enable status.
     *
     * @return bool
     */
    public static function isEnabled()
    {
        return self::$_enabled;
    }

    /**
     * Clear collected statistics for specified timer or for whole profiler if timer id is omitted
     *
     * @param string|null $timerName
     * @return void
     * @throws \InvalidArgumentException
     */
    public static function clear($timerName = null)
    {
        if ($timerName && strpos($timerName, self::NESTING_SEPARATOR) !== false) {
            throw new \InvalidArgumentException('Timer name must not contain a nesting separator.');
        }
        $timerId = self::_getTimerId($timerName);
        /** @var DriverInterface $driver */
        foreach (self::$_drivers as $driver) {
            $driver->clear($timerId);
        }
    }

    /**
     * Reset profiler to initial state
     *
     * @return void
     */
    public static function reset()
    {
        self::clear();
        self::$_enabled = false;
        self::$_currentPath = [];
        self::$_tagFilters = [];
        self::$_defaultTags = [];
        self::$_hasTagFilters = false;
        self::$_drivers = [];
        self::$_pathCount = 0;
        self::$_pathIndex = [];
    }

    /**
     * Start collecting statistics for specified timer
     *
     * @param string $timerName
     * @param array|null $tags
     * @return void
     * @throws \InvalidArgumentException
     */
    public static function start($timerName, array $tags = null)
    {
        if (!self::$_enabled) {
            return;
        }

        $tags = self::_getTags($tags);
        if (!self::_checkTags($tags)) {
            return;
        }

        if (strpos($timerName, self::NESTING_SEPARATOR) !== false) {
            throw new \InvalidArgumentException('Timer name must not contain a nesting separator.');
        }

        $timerId = self::_getTimerId($timerName);
        /** @var DriverInterface $driver */
        foreach (self::$_drivers as $driver) {
            $driver->start($timerId, $tags);
        }
        /* Continue collecting timers statistics under the latest started one */
        self::$_currentPath[] = $timerName;
        self::$_pathCount++;
        self::$_pathIndex[$timerName][] = self::$_pathCount;
    }

    /**
     * Stop recording statistics for specified timer.
     *
     * Call with no arguments to stop the recently started timer.
     * Only the latest started timer can be stopped.
     *
     * @param string|null $timerName
     * @return void
     * @throws \InvalidArgumentException
     */
    public static function stop($timerName = null)
    {
        if (!self::$_enabled || !self::_checkTags(self::_getTags())) {
            return;
        }

        if ($timerName === null) {
            $timersToStop = 1;
        } else {
            $timerPosition = false;
            if (!empty(self::$_pathIndex[$timerName])) {
                $timerPosition = array_pop(self::$_pathIndex[$timerName]);
            }
            if ($timerPosition === false) {
                throw new \InvalidArgumentException(sprintf('Timer "%s" has not been started.', $timerName));
            } elseif ($timerPosition === 1) {
                $timersToStop = 1;
            } else {
                $timersToStop = self::$_pathCount + 1 - $timerPosition;
            }
        }

        for ($i = 0; $i < $timersToStop; $i++) {
            $timerId = self::_getTimerId();
            /** @var DriverInterface $driver */
            foreach (self::$_drivers as $driver) {
                $driver->stop($timerId);
            }
            /* Move one level up in timers nesting tree */
            array_pop(self::$_currentPath);
            self::$_pathCount--;
        }
    }

    /**
     * Init profiler
     *
     * @param array|string $config
     * @param string $baseDir
     * @param bool $isAjax
     * @return void
     */
    public static function applyConfig($config, $baseDir, $isAjax = false)
    {
        $config = self::_parseConfig($config, $baseDir, $isAjax);
        if ($config['driverConfigs']) {
            foreach ($config['driverConfigs'] as $driverConfig) {
                self::add($config['driverFactory']->create($driverConfig));
            }
        }
        foreach ($config['tagFilters'] as $tagName => $tagValue) {
            self::addTagFilter($tagName, $tagValue);
        }
    }

    /**
     * Parses config
     *
     * @param array|string $profilerConfig
     * @param string $baseDir
     * @param bool $isAjax
     * @return array
     * @SuppressWarnings(PHPMD.NPathComplexity)
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
     */
    protected static function _parseConfig($profilerConfig, $baseDir, $isAjax)
    {
        $config = ['baseDir' => $baseDir, 'tagFilters' => []];

        if (is_scalar($profilerConfig)) {
            $config['drivers'] = [
                ['output' => is_numeric($profilerConfig) ? 'html' : $profilerConfig],
            ];
        } else {
            $config = array_merge($config, $profilerConfig);
        }

        $driverConfigs = (array)(isset($config['drivers']) ? $config['drivers'] : []);
        $driverFactory = isset($config['driverFactory']) ? $config['driverFactory'] : new Factory();
        $tagFilters = (array)(isset($config['tagFilters']) ? $config['tagFilters'] : []);

        $result = [
            'driverConfigs' => self::_parseDriverConfigs($driverConfigs, $config['baseDir']),
            'driverFactory' => $driverFactory,
            'tagFilters' => $tagFilters,
            'baseDir' => $config['baseDir'],
        ];
        return $result;
    }

    /**
     * Parses list of driver configurations
     *
     * @param array $driverConfigs
     * @param string $baseDir
     * @return array
     */
    protected static function _parseDriverConfigs(array $driverConfigs, $baseDir)
    {
        $result = [];
        foreach ($driverConfigs as $code => $driverConfig) {
            $driverConfig = self::_parseDriverConfig($driverConfig);
            if ($driverConfig === false) {
                continue;
            }
            if (!isset($driverConfig['type']) && !is_numeric($code)) {
                $driverConfig['type'] = $code;
            }
            if (!isset($driverConfig['baseDir']) && $baseDir) {
                $driverConfig['baseDir'] = $baseDir;
            }
            $result[] = $driverConfig;
        }
        return $result;
    }

    /**
     * Parses driver config
     *
     * @param mixed $driverConfig
     * @return array|false
     */
    protected static function _parseDriverConfig($driverConfig)
    {
        $result = false;
        if (is_array($driverConfig)) {
            $result = $driverConfig;
        } elseif (is_scalar($driverConfig) && $driverConfig) {
            if (is_numeric($driverConfig)) {
                $result = [];
            } else {
                $result = ['type' => $driverConfig];
            }
        }
        return $result;
    }
}
