<?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\Dump\TextAnsi;

use bdk\Debug\Abstraction\Abstraction;
use bdk\Debug\Abstraction\Object\Abstraction as ObjectAbstraction;
use bdk\Debug\Abstraction\Type;
use bdk\Debug\Dump\Text\Value as TextValue;
use bdk\Debug\Dump\TextAnsi as Dumper;
use bdk\Debug\Dump\TextAnsi\TextAnsiObject;
use bdk\Debug\Utility\Utf8;

/**
 * Base output plugin
 */
class Value extends TextValue
{
    const TYPE_ABSTRACTION = 'bdk\Debug\Abstraction\Abstraction|null';

    const ANSI_BOLD = "\e[1m";
    const ANSI_DIM = "\e[2m";
    const ANSI_BOLD_DIM_RESET = "\e[22m";

    /** @var string */
    protected $escapeReset = "\e[0m";

    /** @var TextAnsiObject */
    protected $lazyObject;

    /**
     * Constructor
     *
     * @param Dumper $dumper "parent" dump class
     */
    public function __construct(Dumper $dumper)
    {
        parent::__construct($dumper); // sets debug and dumper
        $this->optionStackPush(array(
            'charReplace' => false,
        ));
    }

    /**
     * Get escape reset sequence
     *
     * @return string
     */
    public function getEscapeReset()
    {
        return $this->escapeReset;
    }

    /**
     * Set escape reset sequence
     *
     * @param string $escapeReset ("\e[0m") escape reset sequence
     *
     * @return void
     */
    public function setEscapeReset($escapeReset = "\e[0m")
    {
        $this->escapeReset = $escapeReset;
    }

    /**
     * Add ansi escape sequences for classname type strings
     *
     * @param string|array $val  classname or classname(::|->)name (method/property/const)
     * @param string       $what ("classname"), "const", or "function" - specify what we're marking if ambiguous
     *
     * @return string
     */
    public function markupIdentifier($val, $what = 'className')
    {
        $parts = $this->parseIdentifier($val, $what);
        $escapeReset = $this->escapeReset;
        $operator = $this->cfg['escapeCodes']['operator'] . $parts['operator'] . $this->escapeReset;
        $identifier = '';
        $classnameOut = $parts['classname']
            ? $this->markupIdentifierClassname($parts['classname'])
            : '';
        $namespaceOut = $parts['namespace']
            ? $this->markupIdentifierNamespace($parts['namespace'])
            : '';
        if ($parts['name']) {
            $this->escapeReset = "\e[0;1m";
            $identifier = self::ANSI_BOLD . $this->highlightChars($parts['name']) . self::ANSI_BOLD_DIM_RESET;
            $this->escapeReset = $escapeReset;
        }
        $parts = \array_filter([$namespaceOut, $classnameOut, $identifier], 'strlen');
        return \implode($operator, $parts);
    }

    /**
     * Wrap string in quotes
     *
     * @param string $val string value
     *
     * @return string
     */
    protected function addQuotes($val)
    {
        $ansiQuote = $this->cfg['escapeCodes']['quote'] . '"' . $this->escapeReset;
        return $this->optionGet('addQuotes')
            ? $ansiQuote . $val . $ansiQuote
            : $val;
    }

    /**
     * {@inheritDoc}
     */
    protected function dumpArray(array $array, $abs = null)
    {
        \bdk\Debug\Utility\PhpType::assertType($abs, self::TYPE_ABSTRACTION, 'abs');

        $this->valDepth++;
        $isNested = $this->valDepth > 0;
        $escapeCodes = $this->cfg['escapeCodes'];
        if ($this->optionGet('isMaxDepth')) {
            return $this->cfg['escapeCodes']['keyword'] . 'array '
                . $this->cfg['escapeCodes']['recursion'] . '*' . $this->debug->i18n->trans('abs.max-depth') . '*'
                . $this->escapeReset;
        }
        $absKeys = isset($abs['keys'])
            ? $abs['keys']
            : array();
        $openParen = $escapeCodes['punct'] . '(' . $this->escapeReset;
        $closeParen = $escapeCodes['punct'] . ')' . $this->escapeReset;
        $str = $escapeCodes['keyword'] . 'array' . $openParen . "\n"
            . $this->dumpArrayValues($array, $absKeys);
        if (!$array) {
            $str = \str_replace("\n", '', $str);
        } elseif ($isNested) {
            $str = \str_replace("\n", "\n    ", $str);
        }
        return \rtrim($str, ' ') . $closeParen;
    }

    /**
     * Dump an array key/value pair
     *
     * @param array $array   array to output
     * @param array $absKeys keys that required abstraction (ie, non-utf8, or containing confusable characters)
     *
     * @return string
     */
    private function dumpArrayValues(array $array, array $absKeys)
    {
        $str = '';
        $escapeCodes = $this->cfg['escapeCodes'];
        $escapeResetBackup = $this->escapeReset;
        $regexRemoveKeyReset = '/' . \preg_quote($escapeCodes['arrayKey']) . '$/';
        foreach ($array as $key => $val) {
            if (isset($absKeys[$key])) {
                $key = $absKeys[$key];
            }
            $this->escapeReset = \str_replace('m', ';49m', $escapeCodes['arrayKey']);
            $key = (\is_int($key) ? '' : $escapeCodes['arrayKey']) . $this->dump($key, array('addQuotes' => false));
            $key = \preg_replace($regexRemoveKeyReset, '', $key);
            $this->escapeReset = $escapeResetBackup;
            $str .= ''
                . $escapeCodes['punct'] . '[' . $key . $escapeCodes['punct'] . ']'
                . $escapeCodes['operator'] . ' => ' . $this->escapeReset
                . $this->dump($val) . "\n";
        }
        return $str;
    }

    /**
     * Dump boolean
     *
     * @param bool $val boolean value
     *
     * @return string
     */
    protected function dumpBool($val)
    {
        return $val
            ? $this->cfg['escapeCodes']['true'] . 'true' . $this->escapeReset
            : $this->cfg['escapeCodes']['false'] . 'false' . $this->escapeReset;
    }

    /**
     * Dump float value
     *
     * @param float            $val float value
     * @param Abstraction|null $abs (optional) full abstraction
     *
     * @return float|string
     */
    protected function dumpFloat($val, $abs = null)
    {
        \bdk\Debug\Utility\PhpType::assertType($abs, self::TYPE_ABSTRACTION, 'abs');

        if ($val === Type::TYPE_FLOAT_INF) {
            $val = 'INF';
        } elseif ($val === Type::TYPE_FLOAT_NAN) {
            $val = 'NaN';
        }
        $date = $this->checkTimestamp($val, $abs);
        $val = $this->cfg['escapeCodes']['numeric'] . $val . $this->escapeReset;
        return $date
            ? '📅 ' . $val . ' ' . $this->cfg['escapeCodes']['muted'] . '(' . $date . ')' . $this->escapeReset
            : $val;
    }

    /**
     * Dump identifier (constant, classname, method, property)
     *
     * @param Abstraction $abs constant abstraction
     *
     * @return string
     */
    protected function dumpIdentifier(Abstraction $abs)
    {
        return $this->markupIdentifier($abs['value'], $abs['typeMore']);
    }

    /**
     * Dump null value
     *
     * @return string
     */
    protected function dumpNull()
    {
        return $this->cfg['escapeCodes']['muted'] . 'null' . $this->escapeReset;
    }

    /**
     * Dump object
     *
     * @param ObjectAbstraction $abs Object Abstraction instance
     *
     * @return string
     */
    protected function dumpObject(ObjectAbstraction $abs)
    {
        return $this->object->dump($abs);
    }

    /**
     * Dump recursion (array recursion)
     *
     * @return string
     */
    protected function dumpRecursion()
    {
        return $this->cfg['escapeCodes']['keyword'] . 'array '
            . $this->cfg['escapeCodes']['recursion'] . '*' . $this->debug->i18n->trans('abs.recursion') . '*'
            . $this->escapeReset;
    }

    /**
     * Dump string
     *
     * @param string           $val string value
     * @param Abstraction|null $abs (optional) full abstraction
     *
     * @return string
     */
    protected function dumpString($val, $abs = null)
    {
        \bdk\Debug\Utility\PhpType::assertType($abs, self::TYPE_ABSTRACTION, 'abs');

        if (\is_numeric($val)) {
            return $this->dumpStringNumeric($val, $abs);
        }
        if ($abs) {
            return $this->dumpStringAbs($abs);
        }
        $val = $this->highlightChars($val);
        return $this->addQuotes($val);
    }

    /**
     * Dump string abstraction
     *
     * @param Abstraction $abs Abstraction instance
     *
     * @return string
     */
    protected function dumpStringAbs(Abstraction $abs)
    {
        $val = $abs['typeMore'] === Type::TYPE_STRING_BINARY
            ? $this->dumpStringBinary($abs)
            : $this->highlightChars((string) $abs);
        $strLenDiff = 0;
        if ($abs['strlenValue']) {
            $abs['strlen'] = $abs['strlen'] ?: \strlen((string) $abs['value']);
            $strLenDiff = $abs['strlen'] - $abs['strlenValue'];
        }
        if ($strLenDiff) {
            $val .= $this->cfg['escapeCodes']['maxlen']
            . '[' . $this->debug->i18n->trans('string.more-bytes', array('bytes' => $strLenDiff)) . ']'
            . $this->escapeReset;
        }
        return $this->addQuotes($val);
    }

    /**
     * Get binary value
     *
     * @param Abstraction $abs Abstraction instance
     *
     * @return string
     */
    protected function dumpStringBinary(Abstraction $abs)
    {
        if ($abs['brief'] && $abs['contentType']) {
            return $this->cfg['escapeCodes']['keyword'] . 'string' . $this->escapeReset
                . $this->cfg['escapeCodes']['muted'] . '(' . $abs['contentType'] . ')' . $this->escapeReset . ': '
                . $this->debug->utility->getBytes($abs['strlen']);
        }
        if ($abs['value']) {
            return $this->cfg['escapeCodes']['binary'] . $abs['value'] . $this->escapeReset;
        }
        return \implode('', \array_map(function ($chunk) {
            return $chunk[0] === Utf8::TYPE_OTHER
                ? $this->cfg['escapeCodes']['binary'] . $chunk[1] . $this->escapeReset
                : $this->highlightChars($chunk[1]);
        }, $abs['chunks'] ?: array()));
    }

    /**
     * Dump numeric string
     *
     * @param string           $val numeric string value
     * @param Abstraction|null $abs (optional) full abstraction
     *
     * @return string
     */
    private function dumpStringNumeric($val, $abs = null)
    {
        \bdk\Debug\Utility\PhpType::assertType($abs, self::TYPE_ABSTRACTION, 'abs');

        $escapeCodes = $this->cfg['escapeCodes'];
        $date = $this->checkTimestamp($val, $abs);
        $val = $escapeCodes['numeric'] . $val . $this->escapeReset;
        $val = $this->addQuotes($val);
        return $date
            ? '📅 ' . $val . ' ' . $escapeCodes['muted'] . '(' . $date . ')' . $this->escapeReset
            : $val;
    }

    /**
     * Dump undefined
     *
     * @return string
     */
    protected function dumpUndefined()
    {
        return self::ANSI_DIM . $this->debug->i18n->trans('abs.undefined') . self::ANSI_BOLD_DIM_RESET;
    }

    /**
     * Getter for this->object
     *
     * @return TextAnsiObject
     */
    protected function getObject()
    {
        if (!$this->lazyObject) {
            $this->lazyObject = new TextAnsiObject($this);
        }
        return $this->lazyObject;
    }

    /**
     * Highlight confusable and other characters
     *
     * @param string $str HTML String to update
     *
     * @return string
     */
    protected function highlightChars($str)
    {
        $chars = $this->findChars($str);
        $charReplace = $this->optionGet('charReplace');
        foreach ($chars as $char) {
            $replacement = $this->cfg['escapeCodes']['char']
                . $this->charReplacement($char, $charReplace)
                . $this->escapeReset;
            $str = \str_replace($char, $replacement, $str);
        }
        return $str;
    }

    /**
     * Markup classname (namespace & classname) portion of identifier string
     *
     * @param string $classname classname
     *
     * @return string
     */
    private function markupIdentifierClassname($classname)
    {
        $classnameOut = '';
        $idx = \strrpos($classname, '\\');
        if ($idx) {
            $namespace = \substr($classname, 0, $idx + 1);
            $classname = \substr($classname, $idx + 1);
            $classnameOut = $this->markupIdentifierNamespace($namespace);
        }
        $escapeReset = $this->escapeReset;
        $this->escapeReset = "\e[0;1m";
        $classnameOut .= self::ANSI_BOLD . $this->highlightChars($classname) . self::ANSI_BOLD_DIM_RESET;
        $this->escapeReset = $escapeReset;
        return $classnameOut;
    }

    /**
     * Markup namespace portion of identifier string
     *
     * @param string $namespace namespace
     *
     * @return string
     */
    private function markupIdentifierNamespace($namespace)
    {
        $escapeReset = $this->escapeReset;
        $this->escapeReset = $this->cfg['escapeCodes']['muted'];
        $namespace = $this->cfg['escapeCodes']['muted'] . $this->highlightChars($namespace) . $escapeReset;
        $this->escapeReset = $escapeReset;
        return $namespace;
    }
}
