<?php
/**
 * For the full copyright and license information, please view the
 * docs/licenses/LICENSE.txt file that was distributed with this source code.
 */
declare(strict_types=1);

class Profiler
{
    /** @var array */
    protected $hooksPerfs = [];

    /** @var array */
    protected $modulesPerfs = [];

    /** @var array */
    protected $profiler = [];

    /** @var array */
    protected $globalVarSize = [];

    /** @var array */
    protected $queries = [];

    /** @var int */
    protected $totalFilesize = 0;

    /** @var int */
    protected $totalGlobalVarSize = 0;

    /** @var float */
    protected $totalQueryTime = 0;

    /** @var float */
    protected $totalModulesTime = 0;

    /** @var int */
    protected $totalModulesMemory = 0;

    /** @var float */
    protected $totalHooksTime = 0;

    /** @var int */
    protected $totalHooksMemory = 0;

    /** @var float */
    protected $startTime = 0;

    /** @var int */
    protected $totalCacheSize = 0;

    protected static $instance = null;

    private function __construct()
    {
        global $start_time;
        if (isset($_SERVER['REQUEST_TIME_FLOAT'])) {
            $this->startTime = (float) $_SERVER['REQUEST_TIME_FLOAT'];
        } elseif (!empty($start_time)) {
            $this->startTime = $start_time;
        } else {
            $this->startTime = microtime(true);
        }
    }

    /**
     * Return profiler instance
     *
     * @return self
     */
    public static function getInstance()
    {
        if (static::$instance === null) {
            static::$instance = new static();
        }

        return static::$instance;
    }

    /**
     * Sort array by query time
     *
     * @param array $a
     * @param array $b
     *
     * @return int
     */
    public function sortByQueryTime(array $a, array $b): int
    {
        if ($a['time'] == $b['time']) {
            return 0;
        }

        return ($a['time'] > $b['time']) ? -1 : 1;
    }

    /**
     * Stamp the profiling
     *
     * @param string $block
     */
    public function stamp(string $block)
    {
        $this->profiler[] = [
            'block' => $block,
            'memory_usage' => memory_get_usage(),
            'peak_memory_usage' => memory_get_peak_usage(),
            'time' => microtime(true),
        ];
    }

    /**
     * Get var size
     *
     * @param mixed $var
     */
    private function getVarSize($var)
    {
        $start_memory = memory_get_usage();

        try {
            $tmp = Tools::unSerialize(serialize($var));
        } catch (Exception $e) {
            $tmp = $this->getVarData($var);
        }

        $size = memory_get_usage() - $start_memory;

        return $size;
    }

    /**
     * Get var data
     *
     * @param mixed $var
     *
     * @return string|object|array
     */
    private function getVarData($var)
    {
        if (is_object($var)) {
            return $var;
        }

        if (is_array($var)) {
            return $var;
        }

        return (string) $var;
    }

    /**
     * Intercept hook and register its data
     *
     * @param string $hookName
     * @param array $params
     */
    public function interceptHook(string $hookName, array $params)
    {
        if (empty($this->hooksPerfs[$hookName])) {
            $this->hooksPerfs[$hookName] = [
                'time' => 0,
                'memory' => 0,
                'modules' => [],
            ];
        }

        $this->hooksPerfs[$hookName]['time'] += $params['time'];
        $this->hooksPerfs[$hookName]['memory'] += $params['memory'];
        $this->hooksPerfs[$hookName]['modules'][] = $params;
        $this->totalHooksMemory += $params['memory'];
        $this->totalHooksTime += $params['time'];
    }

    /**
     * Intercept module
     *
     * @param array $params
     */
    public function interceptModule(array $params)
    {
        $this->modulesPerfs[$params['module']][] = $params;

        $this->totalModulesTime += $params['time'];
        $this->totalModulesMemory += $params['memory'];
    }

    /**
     * Process all data such as Global vars and
     * database queries
     */
    public function processData()
    {
        // Including a lot of files uses memory
        foreach (get_included_files() as $file) {
            if (file_exists($file)) {
                $this->totalFilesize += filesize($file);
            }
        }

        foreach ($GLOBALS as $key => $value) {
            if ($key === 'GLOBALS') {
                continue;
            }
            $this->totalGlobalVarSize += ($size = $this->getVarSize($value));

            if ($size > 1024) {
                $this->globalVarSize[$key] = round($size / 1024);
            }
        }

        arsort($this->globalVarSize);

        $cache = Cache::retrieveAll();
        $this->totalCacheSize = $this->getVarSize($cache);

        // Sum querying time
        /* @phpstan-ignore-next-line */
        $queries = Db::getInstance()->queries;
        uasort($queries, [$this, 'sortByQueryTime']);
        foreach ($queries as $id => $data) {
            $this->totalQueryTime += $data['time'];

            $queryRow = [
                'time' => $data['time'],
                'query' => $data['query'],
                'location' => str_replace('\\', '/', substr($data['stack'][0]['file'], strlen(_PS_ROOT_DIR_))) . ':' . $data['stack'][0]['line'],
                'filesort' => false,
                'rows' => 1,
                'group_by' => false,
                'stack' => [],
                'id' => $id,
            ];

            if (preg_match('/^\s*select\s+/i', $data['query'])) {
                $explain = Db::getInstance()->executeS('explain ' . $data['query']);
                if (isset($explain[0]['Extra']) && stristr($explain[0]['Extra'], 'filesort')) {
                    $queryRow['filesort'] = true;
                }

                if (is_array($explain)) {
                    foreach ($explain as $row) {
                        $queryRow['rows'] *= (int) $row['rows'];
                    }
                } else {
                    $queryRow['rows'] = 'N/A';
                }

                if (stristr($data['query'], 'group by') && !preg_match('/(avg|count|min|max|group_concat|sum)\s*\(/i', $data['query'])) {
                    $queryRow['group_by'] = true;
                }
            }

            array_shift($data['stack']);
            foreach ($data['stack'] as $call) {
                $queryRow['stack'][] = str_replace('\\', '/', substr($call['file'], strlen(_PS_ROOT_DIR_))) . ':' . $call['line'] . ' (' . $call['function'] . ')';
            }

            $this->queries[] = $queryRow;
        }

        /* @phpstan-ignore-next-line */
        uasort(ObjectModel::$debug_list, function ($a, $b) { return (count($a) < count($b)) ? 1 : -1; });
        /* @phpstan-ignore-next-line */
        arsort(Db::getInstance()->tables);
        /* @phpstan-ignore-next-line */
        arsort(Db::getInstance()->uniqQueries);
        uasort($this->hooksPerfs, [$this, 'sortByQueryTime']);
    }

    /**
     * Format performance details for modules
     *
     * @return array
     */
    public function getFormattedModulePerfs(): array
    {
        $formattedOutput = [];
        foreach ($this->modulesPerfs as $moduleName => $perfs) {
            $formattedOutput[$moduleName] = [
                'total_time' => array_reduce(
                    $perfs,
                    function ($res, $item) {
                        return $res + $item['time'];
                    },
                    0
                ),
                'total_memory' => array_reduce(
                    $perfs,
                    function ($res, $item) {
                        return $res + $item['memory'];
                    },
                    0
                ),
                'details' => $perfs,
            ];
        }

        return $formattedOutput;
    }

    /**
     * Prepare and return smarty variables
     *
     * @return array
     */
    public function getSmartyVariables(): array
    {
        /* @phpstan-ignore-next-line */
        $doublesQueries = Db::getInstance()->uniqQueries;
        /* @phpstan-ignore-next-line */
        $tableStress = Db::getInstance()->tables;

        return [
            'summary' => [
                'loadTime' => empty($this->profiler) ? null : $this->profiler[count($this->profiler) - 1]['time'] - $this->startTime,
                'queryTime' => round(1000 * $this->totalQueryTime),
                'nbQueries' => count($this->queries),
                'peakMemoryUsage' => empty($this->profiler) ? null : $this->profiler[count($this->profiler) - 1]['peak_memory_usage'],
                'globalVarSize' => $this->globalVarSize,
                'includedFiles' => count(get_included_files()),
                'totalFileSize' => $this->totalFilesize,
                'totalCacheSize' => $this->totalCacheSize,
                'totalGlobalVarSize' => $this->totalGlobalVarSize,
            ],
            'configuration' => [
                'psVersion' => _PS_VERSION_,
                'phpVersion' => PHP_VERSION,
                'mysqlVersion' => Db::getInstance()->getVersion(),
                'memoryLimit' => ini_get('memory_limit'),
                'maxExecutionTime' => ini_get('max_execution_time'),
                'smartyCache' => Configuration::get('PS_SMARTY_CACHE'),
                'smartyCompilation' => Configuration::get('PS_SMARTY_FORCE_COMPILE'),
            ],
            'run' => [
                'startTime' => $this->startTime,
                'profiler' => $this->profiler,
            ],
            'hooks' => [
                'perfs' => $this->hooksPerfs,
                'totalHooksTime' => $this->totalHooksTime,
                'totalHooksMemory' => $this->totalHooksMemory,
            ],
            'modules' => [
                'perfs' => $this->getFormattedModulePerfs(),
                'totalHooksTime' => $this->totalModulesTime,
                'totalHooksMemory' => $this->totalModulesMemory,
            ],
            'stopwatchQueries' => $this->queries,
            'doublesQueries' => $doublesQueries,
            'tableStress' => $tableStress,
            'objectmodel' => ObjectModel::$debug_list,
            'files' => get_included_files(),
        ];
    }
}
