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

use Psr\Log\AbstractLogger;
use Psr\Log\LogLevel;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Helper\FormatterHelper;
use Magento\Framework\Filesystem;
use Magento\Framework\App\Filesystem\DirectoryList;
use Magento\Framework\Filesystem\Directory\ReadInterface;

/**
 * PSR logger implementation for CLI
 */
class ConsoleLogger extends AbstractLogger
{
    /**
     * Type for informational message
     */
    public const INFO = 'info';

    /**
     * Type for error message
     */
    public const ERROR = 'error';

    /**
     * Public static files directory read interface
     *
     * @var ReadInterface
     */
    private $tmpDir;

    /**
     * Console output interface
     *
     * @var OutputInterface
     */
    private $output;

    /**
     * Helper for preparing data of specific formats (date, percentage, etc)
     *
     * @var FormatterHelper
     */
    private $formatterHelper;

    /**
     * Maximum progress bar row string length
     *
     * @var int
     */
    private $initialMaxBarSize = 0;

    /**
     * Number of rendered lines
     *
     * Used for clearing previously rendered progress bars
     *
     * @var int
     */
    private $renderedLines = 0;

    /**
     * Time of previous rendering tick
     *
     * @var int
     */
    private $lastTimeRefreshed = 0;

    /**
     * @var array
     */
    private $verbosityLevelMap = [
        LogLevel::EMERGENCY => OutputInterface::VERBOSITY_NORMAL,
        LogLevel::ALERT => OutputInterface::VERBOSITY_NORMAL,
        LogLevel::CRITICAL => OutputInterface::VERBOSITY_NORMAL,
        LogLevel::ERROR => OutputInterface::VERBOSITY_NORMAL,
        LogLevel::WARNING => OutputInterface::VERBOSITY_NORMAL,
        LogLevel::NOTICE => OutputInterface::VERBOSITY_NORMAL,
        LogLevel::INFO => OutputInterface::VERBOSITY_VERBOSE,
        LogLevel::DEBUG => OutputInterface::VERBOSITY_DEBUG
    ];

    /**
     * @var array
     */
    private $formatLevelMap = [
        LogLevel::EMERGENCY => self::ERROR,
        LogLevel::ALERT => self::ERROR,
        LogLevel::CRITICAL => self::ERROR,
        LogLevel::ERROR => self::ERROR,
        LogLevel::WARNING => self::INFO,
        LogLevel::NOTICE => self::INFO,
        LogLevel::INFO => self::INFO,
        LogLevel::DEBUG => self::INFO
    ];

    /**
     * Running deployment processes info
     *
     * @var array[]
     */
    private $processes = [];

    /**
     * @param Filesystem $filesystem
     * @param OutputInterface $output
     * @param FormatterHelper $formatterHelper
     * @param array $verbosityLevelMap
     * @param array $formatLevelMap
     */
    public function __construct(
        Filesystem $filesystem,
        OutputInterface $output,
        FormatterHelper $formatterHelper,
        array $verbosityLevelMap = [],
        array $formatLevelMap = []
    ) {
        $this->tmpDir = $filesystem->getDirectoryWrite(DirectoryList::TMP_MATERIALIZATION_DIR);
        $this->output = $output;
        $this->formatterHelper = $formatterHelper;
        $this->verbosityLevelMap = $verbosityLevelMap + $this->verbosityLevelMap;
        $this->formatLevelMap = $formatLevelMap + $this->formatLevelMap;
    }

    /**
     * @inheritdoc
     */
    public function log($level, $message, array $context = []): void
    {
        if (!isset($this->verbosityLevelMap[$level])) {
            $level = self::INFO;
        }

        // Write to the error output if necessary and available
        if ($this->formatLevelMap[$level] === self::ERROR && $this->output instanceof ConsoleOutputInterface) {
            $output = $this->output->getErrorOutput();
        } else {
            $output = $this->output;
        }

        if (isset($context['process'])) {
            $this->registerProcess($context);
        } else {
            $this->refresh($output);
        }

        if ($output->getVerbosity() >= $this->verbosityLevelMap[$level]) {
            $output->writeln(sprintf('<%1$s>%2$s</%1$s>', $this->formatLevelMap[$level], $message));
        }
    }

    /**
     * Add deployment process to rendering stack
     *
     * @param array $context
     * @return void
     */
    private function registerProcess(array $context)
    {
        $name = isset($context['process']) ? $context['process'] : 'main';
        if (!isset($this->processes[$name])) {
            $context['start'] = time();
            $context['elapsed'] = 0;
            $this->processes[$name] = $context;
        }
    }

    /**
     * Refresh CLI output
     *
     * @param OutputInterface $output
     * @return void
     */
    private function refresh(OutputInterface $output)
    {
        if (!count($this->processes) || (time() - $this->lastTimeRefreshed < 1)) {
            return;
        }

        $this->cleanUp();

        $bars = [];
        $maxBarSize = 0;
        foreach ($this->processes as $name => & $process) {
            $this->updateProcessInfo($name, $process);
            $bar = $this->renderProgressBar($output, $process);
            $maxBarSize = strlen($bar) > $maxBarSize ? strlen($bar) : $maxBarSize;
            $bars[] = $bar;
        }
        if (!$this->initialMaxBarSize) {
            $this->initialMaxBarSize = $maxBarSize + 10;
        }
        if ($bars) {
            $this->renderedLines = count($bars);
            $bar = '';
            foreach ($bars as &$bar) {
                if ($this->initialMaxBarSize > strlen($bar)) {
                    $bar .= str_pad(" ", ($this->initialMaxBarSize - strlen($bar)));
                }
            }
            $bar = trim($bar);
            $output->writeln(implode("\n", $bars));
        }
    }

    /**
     * Update process information
     *
     * @param string $deployedPackagePath
     * @param array $process
     * @return void
     */
    private function updateProcessInfo($deployedPackagePath, array &$process)
    {
        $packageDeploymentInfo = $this->getPackageDeploymentInfo($deployedPackagePath . '/info.json');
        if ($packageDeploymentInfo) {
            $process['done'] = $packageDeploymentInfo['count'];
        } else {
            $process['done'] = 0;
        }
        if ($process['done'] > $process['count']) {
            $process['count'] = $process['done'];
        }
        if ($process['done'] !== $process['count']) {
            $process['elapsed'] = $this->formatterHelper->formatTime(time() - $process['start']);
        }
        $process['percent'] = floor(
            ($process['count'] ? (float)$process['done'] / $process['count'] : 0) * 100
        );
    }

    /**
     * Clear rendered lines
     *
     * @return void
     */
    private function cleanUp()
    {
        $this->lastTimeRefreshed = time();
        // Erase previous lines
        if ($this->renderedLines > 0) {
            for ($i = 0; $i < $this->renderedLines; ++$i) {
                $this->output->write("\x1B[1A\x1B[2K", false, OutputInterface::OUTPUT_RAW);
            }
        }
        $this->renderedLines = 0;
    }

    /**
     * Generate progress bar part
     *
     * @param OutputInterface $output
     * @param array $process
     * @return string
     */
    private function renderProgressBar(OutputInterface $output, array $process)
    {
        $title = "{$process['process']}";
        $titlePad = str_pad(' ', (40 - strlen($title)));
        $count = "{$process['done']}/{$process['count']}";
        $countPad = str_pad(' ', (20 - strlen($count)));
        $percent = "{$process['percent']}% ";
        $percentPad = str_pad(' ', (7 - strlen($percent)));
        return "{$title}{$titlePad}"
        . "{$count}{$countPad}"
        . "{$this->renderBar($output, $process)} "
        . "{$percent}{$percentPad}"
        . "{$process['elapsed']}   ";
    }

    /**
     * Generate progress bar row
     *
     * @param OutputInterface $output
     * @param array $process
     * @return string
     */
    private function renderBar(OutputInterface $output, array $process)
    {
        $completeBars = floor(
            $process['count'] > 0 ? ($process['done'] / $process['count']) * 28 : $process['done'] % 28
        );

        $display = str_repeat('=', $completeBars);
        if ($completeBars < 28) {
            $emptyBars = 28 - $completeBars
                - FormatterHelper::width($this->formatterHelper->removeDecoration($output->getFormatter(), '>'));
            $display .= '>' . str_repeat('-', $emptyBars);
        }
        return $display;
    }

    /**
     * Retrieve package deployment process information
     *
     * @param string $relativePath
     * @return string|false
     */
    private function getPackageDeploymentInfo($relativePath)
    {
        if ($this->tmpDir->isFile($relativePath)) {
            $info = $this->tmpDir->readFile($relativePath);
            $info = json_decode($info, true);
        } else {
            $info = [];
        }
        return $info;
    }
}
