<?php

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

namespace bdk\Debug\Abstraction\Object;

use bdk\Debug;
use bdk\Debug\Abstraction\Abstracter;
use bdk\Debug\Abstraction\AbstractObject;
use bdk\Debug\Abstraction\Object\Definition as AbstractObjectDefinition;
use bdk\Debug\Abstraction\Object\PropertiesDom;
use bdk\Debug\Data;
use bdk\Debug\Utility\PhpDoc;
use bdk\PubSub\SubscriberInterface;
use Error;
use Exception;
use mysqli;
use ReflectionFunction;
use RuntimeException;
use UnitEnum;

/**
 * Internal subscriber to ABSTRACT_START and ABSTRACT_END events
 */
class Subscriber implements SubscriberInterface
{
    /** @var AbstractObject */
    protected $abstractObject;

    /** @var PropertiesDom */
    private $dom;

    /**
     * Constructor
     *
     * @param AbstractObject $abstractObject Object abstracter
     */
    public function __construct(AbstractObject $abstractObject)
    {
        $this->abstractObject = $abstractObject;
        $this->dom = new PropertiesDom($abstractObject->abstracter);
    }

    /**
     * {@inheritDoc}
     */
    public function getSubscriptions()
    {
        return array(
            Debug::EVENT_OBJ_ABSTRACT_END => 'onEnd',
            Debug::EVENT_OBJ_ABSTRACT_START => 'onStart',
        );
    }

    /**
     * Debug::EVENT_OBJ_ABSTRACT_START event subscriber
     *
     * @param Abstraction $abs Abstraction instance
     *
     * @return void
     */
    public function onStart(Abstraction $abs)
    {
        $obj = $abs->getSubject();
        switch (true) {
            case $obj instanceof \DateTime || $obj instanceof \DateTimeImmutable:
                // check for both DateTime and DateTimeImmutable
                //   DateTimeInterface (and DateTimeImmutable) not available until Php 5.5
                $abs['isTraverseOnly'] = false;
                $abs['stringified'] = $obj->format(\DateTime::ISO8601);
                break;
            case $obj instanceof mysqli:
                $this->onStartMysqli($abs);
                break;
            case $obj instanceof Data:
                $abs['propertyOverrideValues']['data'] = Abstracter::NOT_INSPECTED;
                break;
            case $obj instanceof PhpDoc:
                $abs['propertyOverrideValues']['cache'] = Abstracter::NOT_INSPECTED;
                break;
            case $obj instanceof UnitEnum:
                $abs['isTraverseOnly'] = false;
                break;
            case $obj instanceof AbstractObjectDefinition:
                $abs['propertyOverrideValues']['cache'] = Abstracter::NOT_INSPECTED;
                break;
            case $abs['className'] === 'Closure':
                $this->onStartClosure($abs);
                break;
        }
    }

    /**
     * Debug::EVENT_OBJ_ABSTRACT_END event subscriber
     *
     * @param Abstraction $abs Abstraction instance
     *
     * @return void
     */
    public function onEnd(Abstraction $abs)
    {
        $obj = $abs->getSubject();
        if ($obj instanceof Exception && isset($abs['properties']['xdebug_message'])) {
            $abs['properties']['xdebug_message']['debugInfoExcluded'] = true;
        } elseif ($obj instanceof mysqli && !$abs['collectPropertyValues']) {
            $this->onEndMysqli($abs);
        } elseif ($obj instanceof UnitEnum) {
            $this->onEndEnum($abs);
        }
        $this->dom->add($abs);
        if (isset($abs['methods']['__toString'])) {
            $abs['methods']['__toString']['returnValue'] = $this->abstractObject->methods->toString($abs);
        }
        $this->promoteParamDescs($abs);
    }

    /**
     * Add enum case's @var desc (if exists) to phpDoc
     *
     * @param Abstraction $abs Abstraction instance
     *
     * @return void
     */
    private function onEndEnum(Abstraction $abs)
    {
        if ($abs['debugMethod'] === 'table') {
            $abs['cfgFlags'] |= AbstractObject::BRIEF;
        }
        if (!($abs['cfgFlags'] & AbstractObject::PHPDOC_COLLECT)) {
            return;
        }
        $reflector = $abs['reflector'];
        $name = $abs->getSubject()->name;
        $caseReflector = $reflector->getCase($name);
        $phpDocCase = $this->abstractObject->helper->getPhpDocVar($caseReflector);
        if ($phpDocCase) {
            $phpDoc = $this->abstractObject->helper->getPhpDoc($reflector);
            $phpDoc = \array_merge($phpDoc, array(
                'desc' => \trim($phpDoc['summary'] . "\n" . $phpDoc['desc']),
                'summary' => $phpDocCase['summary'],
            ));
            \ksort($phpDoc);
            $abs['phpDoc'] = $phpDoc;
        }
    }

    /**
     * Add mysqli property values
     *
     * @param Abstraction $abs Abstraction instance
     *
     * @return void
     */
    private function onEndMysqli(Abstraction $abs)
    {
        $obj = $abs->getSubject();
        $propsAlwaysAvail = [
            'client_info','client_version','connect_errno','connect_error','errno','error','stat',
        ];
        \set_error_handler(static function () {
            // ignore error
        });
        $refObject = $abs['reflector'];
        foreach ($propsAlwaysAvail as $name) {
            if (!isset($abs['properties'][$name])) {
                // stat property may be missing in php 7.4??
                continue;
            }
            $abs['properties'][$name]['value'] = $refObject->getProperty($name)->getValue($obj);
        }
        \restore_error_handler();
    }

    /**
     * Set Closure definition and debug properties
     *
     * @param Abstraction $abs Abstraction instance
     *
     * @return void
     */
    private function onStartClosure(Abstraction $abs)
    {
        // get the per-instance __invoke signature
        $this->abstractObject->methods->add($abs);
        $ref = new ReflectionFunction($abs->getSubject());
        $abs['definition'] = array(
            'extensionName' => $ref->getExtensionName(),
            'fileName' => $ref->getFileName(),
            'startLine' => $ref->getStartLine(),
        );
        $this->abstractObject->properties->addDebugProperties($abs);
    }

    /**
     * Test if we can collect mysqli property values
     *
     * @param Abstraction $abs Abstraction instance
     *
     * @return void
     */
    private function onStartMysqli(Abstraction $abs)
    {
        /*
            test if stat() throws an error (ie "Property access is not allowed yet")
            if so, don't collect property values
        */
        $haveError = false;
        \set_error_handler(static function () use (&$haveError) {
            $haveError = true;
            return true;
        }, E_ALL);
        try {
            $mysqli = $abs->getSubject();
            $mysqli->stat();
        } catch (Error $e) {
            $haveError = true;
        } catch (RuntimeException $e) {
            $haveError = true;
        }
        if ($haveError) {
            $abs['collectPropertyValues'] = false;
        }
        \restore_error_handler();
    }

    /**
     * Reuse the phpDoc description from promoted __construct params
     *
     * @param Abstraction $abs Abstraction instance
     *
     * @return void
     */
    private function promoteParamDescs(Abstraction $abs)
    {
        if (isset($abs['methods']['__construct']) === false) {
            return;
        }
        foreach ($abs['methods']['__construct']['params'] as $info) {
            if ($info['isPromoted'] && $info['desc']) {
                $paramName = $info['name'];
                $abs['properties'][$paramName]['phpDoc']['summary'] = $info['desc'];
            }
        }
    }
}
