<?php
/**
 * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
 * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
 *
 * Licensed under The MIT License
 * Redistributions of files must retain the above copyright notice.
 *
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
 * @link          http://cakephp.org CakePHP(tm) Project
 * @license       http://www.opensource.org/licenses/mit-license.php MIT License
 */
namespace DebugKit\Panel;

use Cake\Collection\Collection;
use Cake\Datasource\EntityInterface;
use Cake\Event\Event;
use Cake\Form\Form;
use Cake\ORM\Query;
use Cake\ORM\ResultSet;
use Cake\Utility\Hash;
use Closure;
use DebugKit\DebugPanel;
use Exception;
use InvalidArgumentException;
use PDO;
use RuntimeException;
use SimpleXMLElement;

/**
 * Provides debug information on the View variables.
 *
 */
class VariablesPanel extends DebugPanel
{

    /**
     * Extracts nested validation errors
     *
     * @param EntityInterface $entity Entity to extract
     *
     * @return array
     */
    protected function _getErrors(EntityInterface $entity)
    {
        $errors = $entity->errors();

        foreach ($entity->visibleProperties() as $property) {
            $v = $entity[$property];
            if ($v instanceof EntityInterface) {
                $errors[$property] = $this->_getErrors($v);
            } elseif (is_array($v)) {
                foreach ($v as $key => $varValue) {
                    if ($varValue instanceof EntityInterface) {
                        $errors[$property][$key] = $this->_getErrors($varValue);
                    }
                }
            }
        }

        return Hash::filter($errors);
    }

    /**
     * Safely retrieves debug information from an object
     * and applies a callback.
     *
     * @param callable $walker The walker to apply on the debug info array.
     * @param object $item The item whose debug info to retrieve.
     *
     * @return array|string
     */
    protected function _walkDebugInfo(callable $walker, $item)
    {
        try {
            $info = $item->__debugInfo();
        } catch (\Exception $exception) {
            return __d('debug_kit', 'Could not retrieve debug info - {0}. Error: {1} in {2}, line {3}', get_class($item), $exception->getMessage(), $exception->getFile(), $exception->getLine());
        }

        return array_map($walker, $info);
    }

    /**
     * Shutdown event
     *
     * @param \Cake\Event\Event $event The event
     * @return void
     */
    public function shutdown(Event $event)
    {
        /* @var \Cake\Controller\Controller $controller */
        $controller = $event->getSubject();
        $errors = [];

        $walker = function (&$item) use (&$walker) {
            if (
                $item instanceof Collection ||
                $item instanceof Query ||
                $item instanceof ResultSet
            ) {
                try {
                    $item = $item->toArray();
                } catch (\Cake\Database\Exception $e) {
                    //Likely issue is unbuffered query; fall back to __debugInfo
                    $item = $this->_walkDebugInfo($walker, $item);
                } catch (RuntimeException $e) {
                    // Likely a non-select query.
                    $item = $this->_walkDebugInfo($walker, $item);
                } catch (InvalidArgumentException $e) {
                    $item = $this->_walkDebugInfo($walker, $item);
                }
            } elseif (
                $item instanceof Closure ||
                $item instanceof PDO ||
                $item instanceof SimpleXMLElement
            ) {
                $item = 'Unserializable object - ' . get_class($item);
            } elseif ($item instanceof Exception) {
                $item = sprintf(
                    'Unserializable object - %s. Error: %s in %s, line %s',
                    get_class($item),
                    $item->getMessage(),
                    $item->getFile(),
                    $item->getLine()
                );
            } elseif (is_object($item)) {
                if (method_exists($item, '__debugInfo')) {
                    // Convert objects into using __debugInfo.
                    $item = $this->_walkDebugInfo($walker, $item);
                } else {
                    $item = $this->trySerialize($item);
                }
            } elseif (is_resource($item)) {
                $item = sprintf('[%s] %s', get_resource_type($item), $item);
            }

            return $this->trySerialize($item);
        };
        // Copy so viewVars is not mutated.
        $vars = $controller->viewVars;
        array_walk_recursive($vars, $walker);

        foreach ($vars as $k => $v) {
            // Get the validation errors for Entity
            if ($v instanceof EntityInterface) {
                $errors[$k] = $this->_getErrors($v);
            } elseif ($v instanceof Form) {
                $formError = $v->getErrors();
                if (!empty($formError)) {
                    $errors[$k] = $formError;
                }
            }
        }

        $this->_data = [
            'content' => $vars,
            'errors' => $errors,
        ];
    }

    /**
     * Try to serialize an item, provide an error message if not possible
     *
     * @param mixed $item Item to check
     * @return mixed The $item if it is serializable, error message if not
     */
    protected function trySerialize($item)
    {
        try {
            serialize($item);

            return $item;
        } catch (\Exception $e) {
            if (is_object($item)) {
                return __d('debug_kit', 'Unserializable object - {0}. Error: {1} in {2}, line {3}', get_class($item), $e->getMessage(), $e->getFile(), $e->getLine());
            }

            return __d('debug_kit', 'Unserializable Error: {1} in {2}, line {3}', $e->getMessage(), $e->getFile(), $e->getLine());
        }
    }

    /**
     * Get summary data for the variables panel.
     *
     * @return string
     */
    public function summary()
    {
        if (!isset($this->_data['content'])) {
            return '0';
        }

        return (string)count($this->_data['content']);
    }
}
