<?php

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

namespace bdk\Debug\Route;

use bdk\Debug;
use bdk\Debug\LogEntry;
use bdk\PubSub\Event;

/**
 * Output log via FirePHP
 */
class Firephp extends AbstractRoute
{
    const FIREPHP_PROTO_VER = '0.3';

    /** @var bool */
    protected $appendsHeaders = true;

    /** @var array<string,mixed> */
    protected $cfg = array(
        'channels' => ['*'],
        'channelsExclude' => [
            'events',
            'files',
        ],
        'group' => false, // contain/wrap log in a group?
        'messageLimit' => 99999,
    );

    /** @var array<string,string> */
    protected $firephpMethods = array(
        'error' => 'ERROR',
        'group' => 'GROUP_START',
        'groupCollapsed' => 'GROUP_START',
        'groupEnd' => 'GROUP_END',
        'info' => 'INFO',
        'log' => 'LOG',
        'table' => 'TABLE',
        'warn' => 'WARN',
    );

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

    /** @var Event|null */
    protected $outputEvent;

    /**
     * Constructor
     *
     * @param Debug $debug debug instance
     */
    public function __construct(Debug $debug)
    {
        parent::__construct($debug);
        $this->dumper = $debug->getDump('base');
    }

    /**
     * Output the log via FirePHP headers
     *
     * @param Event|null $event Debug::EVENT_OUTPUT Event object
     *
     * @return void
     */
    public function processLogEntries($event = null)
    {
        \bdk\Debug\Utility\PhpType::assertType($event, 'bdk\PubSub\Event|null');

        $this->dumper->crateRaw = false;
        $this->outputEvent = $event;
        $this->data = $this->debug->data->get();
        $event['headers'][] = ['X-Wf-Protocol-1', 'http://meta.wildfirehq.org/Protocol/JsonStream/0.2'];
        $event['headers'][] = ['X-Wf-1-Plugin-1', 'http://meta.firephp.org/Wildfire/Plugin/FirePHP/Library-FirePHPCore/' . self::FIREPHP_PROTO_VER];
        $event['headers'][] = ['X-Wf-1-Structure-1', 'http://meta.firephp.org/Wildfire/Structure/FirePHP/FirebugConsole/0.1'];
        if ($this->cfg['group']) {
            $this->processLogEntryViaEvent(new LogEntry(
                $this->debug,
                'groupCollapsed',
                ['PHP: ' . $this->getRequestMethodUri()]
            ));
        }
        $this->processChannels();
        if ($this->cfg['group']) {
            $this->processLogEntryViaEvent(new LogEntry($this->debug, 'groupEnd'));
        }
        $event['headers'][] = ['X-Wf-1-Index', $this->messageIndex];
        $this->data = array();
        $this->dumper->crateRaw = true;
    }

    /**
     * LogEntry to firePhp header(s)
     *
     * @param LogEntry $logEntry LogEntry instance
     *
     * @return void
     */
    public function processLogEntry(LogEntry $logEntry)
    {
        $this->setFirephpMeta($logEntry);
        $value = $this->handleLogEntry($logEntry);
        if ($this->messageIndex < $this->cfg['messageLimit']) {
            $this->setFirephpHeader($logEntry['firephpMeta'], $value);
        } elseif ($this->messageIndex === $this->cfg['messageLimit']) {
            $this->setFirephpHeader(
                array('Type' => $this->firephpMethods['warn']),
                'FirePhp\'s limit of ' . \number_format($this->cfg['messageLimit']) . ' messages reached!'
            );
        }
    }

    /**
     * Handle log entry based on its method and return firephp value
     *
     * @param LogEntry $logEntry logEntry instance
     *
     * @return mixed
     */
    private function handleLogEntry(LogEntry $logEntry)
    {
        $method = $logEntry['method'];
        $args = $logEntry['args'];
        $value = null;
        if ($method === 'alert') {
            $value = $this->methodAlert($logEntry);
        } elseif (\in_array($method, ['group', 'groupCollapsed'], true)) {
            $value = $this->methodGroup($logEntry);
        } elseif (\in_array($method, ['profileEnd', 'table', 'trace'], true)) {
            $value = $this->methodTabular($logEntry);
        } elseif (\count($args)) {
            $this->dumper->processLogEntry($logEntry);
            $value = $this->methodDefault($logEntry);
        }
        return $value;
    }

    /**
     * Handle alert method
     *
     * @param LogEntry $logEntry LogEntry instance
     *
     * @return string firephp value
     */
    private function methodAlert(LogEntry $logEntry)
    {
        $level = $logEntry->getMeta('level');
        $levelToMethod = array(
            'error' => 'error',
            'info' => 'info',
            'success' => 'info',
            'warn' => 'warn',
        );
        $method = $levelToMethod[$level];
        $logEntry['firephpMeta']['Type'] = $this->firephpMethods[$method];
        if ($logEntry->containsSubstitutions()) {
            $this->dumper->processLogEntry($logEntry);
        }
        return $this->methodDefault($logEntry);
    }

    /**
     * Handle methods that do not have a specific handler
     *
     * @param LogEntry $logEntry LogEntry instance
     *
     * @return mixed firephp value
     */
    private function methodDefault(LogEntry $logEntry)
    {
        $args = $logEntry['args'];
        if (\count($args) === 1) {
            return $args[0];
            // no label;
        }
        $logEntry['firephpMeta']['Label'] = \array_shift($args);
        return \count($args) > 1
            ? $args // firephp only supports label/value...  we'll pass multiple values as an array
            : $args[0];
    }

    /**
     * Handle group and groupCollapsed methods
     *
     * @param LogEntry $logEntry LogEntry instance
     *
     * @return null
     */
    private function methodGroup(LogEntry $logEntry)
    {
        $args = $logEntry['args'];
        $method = $logEntry['method'];
        $logEntry['firephpMeta']['Label'] = $args[0];
        $logEntry['firephpMeta']['Collapsed'] = $method === 'groupCollapsed'
            // yes, strings
            ? 'true'
            : 'false';
        return null;
    }

    /**
     * handle tabular type log entry
     *
     * @param LogEntry $logEntry logEntry instance
     *
     * @return mixed
     */
    private function methodTabular(LogEntry $logEntry)
    {
        $logEntry->setMeta('undefinedAs', 'null');
        $this->dumper->processLogEntry($logEntry);
        $logEntry['firephpMeta']['Type'] = $this->firephpMethods['table'];
        $caption = $logEntry->getMeta('caption');
        if ($caption) {
            $logEntry['firephpMeta']['Label'] = $caption;
        }
        $args = $logEntry['args'];
        $firephpTable = true;
        if (!$firephpTable) {
            return $this->dumper->valDumper->dump($args[0]);
        }
        $value = array();
        $keys = \array_map(static function (array $colInfo) {
            return $colInfo['key'];
        }, $logEntry['meta']['tableInfo']['columns']);
        if ($logEntry['meta']['tableInfo']['haveObjRow']) {
            \array_unshift($keys, '___class_name');
        }
        \array_unshift($keys, ''); // key/index
        $value[] = $keys;
        foreach ($args[0] as $k => $row) {
            $value[] = \array_merge([$k], \array_values($row));
        }
        return $this->dumper->valDumper->dump($value);
    }

    /**
     * {@inheritDoc}
     */
    protected function processChannel(Debug $instance)
    {
        $messageIndexBefore = $this->messageIndex;
        parent::processChannel($instance);
        if ($this->messageIndex === $messageIndexBefore + 2) {
            // we only output the group start and end
            // remove the group start and end from the output
            $this->messageIndex -= 2;
            \array_splice($this->outputEvent['headers'], -2, 2);
        }
    }

    /**
     * Set FirePHP log entry header(s)
     *
     * @param array $meta  meta information
     * @param mixed $value value
     *
     * @return void
     */
    private function setFirephpHeader($meta, $value = null)
    {
        \ksort($meta);  // for consistency / testing
        $msg = \json_encode([
            $meta,
            $value,
        ], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        $structureIndex = 1;    // refers to X-Wf-1-Structure-1
        $parts = \explode("\n", \rtrim(\chunk_split($msg, 5000, "\n")));
        $numParts = \count($parts);
        for ($i = 0; $i < $numParts; $i++) {
            $part = $parts[$i];
            $this->messageIndex++;
            $headerName = 'X-Wf-1-' . $structureIndex . '-1-' . $this->messageIndex;
            $headerValue = ($i === 0 ? \strlen($msg) : '')
                . '|' . $part . '|'
                . ($i < $numParts - 1 ? '\\' : '');
            $this->outputEvent['headers'][] = [$headerName, $headerValue];
        }
    }

    /**
     * Initialize firephp's meta array
     *
     * @param LogEntry $logEntry LogEntry instance
     *
     * @return void
     */
    private function setFirephpMeta(LogEntry $logEntry)
    {
        $method = $logEntry['method'];
        $meta = $logEntry['meta'];
        $firephpMeta = array(
            'Type' => isset($this->firephpMethods[$method])
                ? $this->firephpMethods[$method]
                : $this->firephpMethods['log'],
            // Label
            // File
            // Line
            // Collapsed  (for group)
        );
        if (isset($meta['file'])) {
            $firephpMeta['File'] = $meta['file'];
            $firephpMeta['Line'] = $meta['line'];
        }
        $logEntry['firephpMeta'] = $firephpMeta;
    }
}
