<?php
/*
 *   $Id$
 *
 *   AbanteCart, Ideal OpenSource Ecommerce Solution
 *   http://www.AbanteCart.com
 *
 *   Copyright © 2011-2025 Belavier Commerce LLC
 *
 *   This source file is subject to Open Software License (OSL 3.0)
 *   License details are bundled with this package in the file LICENSE.txt.
 *   It is also available at this URL:
 *   <http://www.opensource.org/licenses/OSL-3.0>
 *
 *  UPGRADE NOTE:
 *    Do not edit or add to this file if you wish to upgrade AbanteCart to newer
 *    versions in the future. If you wish to customize AbanteCart for your
 *    needs, please refer to http://www.AbanteCart.com for more information.
 */

/**
 * Class ADispatcher
 *
 * @property ARequest $request
 * @property AResponse $response
 * @property AView $view
 * @property ExtensionsApi $extensions
 */
final class ADispatcher
{
    /** @var Registry */
    protected $registry;
    /** @var string */
    protected $file;
    /** @var string */
    protected $class;
    /** @var string */
    protected $method;
    /** @var string */
    protected $controller;
    /** @var string */
    protected $controller_type;
    /** @var array */
    protected $args = [];

    /**
     * @param string $rt
     * @param array $args
     */
    public function __construct($rt, $args = [])
    {
        $this->registry = Registry::getInstance();
        $rt = str_replace(['../', '..' . DS], '', $rt);
        if (!empty($args)) {
            $this->args = $args;
        }

        ADebug::checkpoint('ADispatch: ' . $rt . ' construct start');
        // We always get full RT (route) to dispatcher. Needs to have pages/ or responses/
        if (!$this->processPath($rt)) {
            $warning_txt = 'ADispatch: ' . $rt . ' construct FAILED.'
                . ' Missing or incorrect controller route path. '
                . 'Possibly, layout block is enabled for disabled or missing extension! '
                . genExecTrace('full');
            $warning = new AWarning($warning_txt);
            $warning->toDebug();
        }
        ADebug::checkpoint('ADispatch: ' . $rt . ' construct end. file: class: ' . $this->class . '; method: ' . $this->method);
    }

    public function __destruct()
    {
        $this->clear();
    }

    public function __get($key)
    {
        return $this->registry->get($key);
    }

    public function __set($key, $value)
    {
        $this->registry->set($key, $value);
    }

    /**
     * @param string $rt
     *
     * @return bool
     */
    protected function processPath($rt)
    {
        $path_build = '';
        $pathfound = false;
        // Build the path based on the route, example, rt=information/contact/success
        $path_nodes = explode('/', $rt);
        $this->controller_type = $path_nodes[0];

        //looking for controller in admin/storefront section
        $dir_app = DIR_APP_SECTION . 'controller' . DS;
        foreach ($path_nodes as $path_node) {
            $path_build .= $path_node;

            if (is_dir($dir_app . $path_build)) {
                $path_build .= '/';
                array_shift($path_nodes);
                continue;
            }
            $testFileName = str_replace('/', DS, $dir_app . $path_build . '.php');
            if (is_file($testFileName)) {
                //Set pure controller route
                $this->controller = $path_build;
                //Set full file path to controller
                $this->file = $testFileName;
                //Build Controller class name
                $this->class = 'Controller' . preg_replace('/[^a-zA-Z0-9]/', '', $path_build);
                array_shift($path_nodes);
                $pathfound = true;
                break;
            }
        }

        //Last part is the method of function to call
        $method_to_call = array_shift($path_nodes);
        if ($method_to_call) {
            $this->method = $method_to_call;
        } else {
            //Set default method
            $this->method = 'main';
        }

        // Already found the path, so return.
        // This will optimize performance and will not allow overriding core controllers.
        if ($pathfound) {
            return true;
        }

        // looking for a controller in an extensions section
        $result = $this->registry->get('extensions')->isExtensionController($rt);
        if ($result) {
            $this->controller = $result['route'];
            $this->file = $result['file'];
            $this->class = $result['class'];
            $this->method = $result['method'];
            // if a controller was found in admin/storefront section && in extensions section
            // warning will be added to the log about controller override
            $warning = new AWarning("Extension <b>" . $result['extension'] . "</b> override controller <b>" . $rt . "</b>");
            $warning->toDebug();
            $pathfound = true;
        }

        return $pathfound;
    }

    // Clear function is public in case the controller needs to be cleaned explicitly
    public function clear()
    {
        $vars = get_object_vars($this);
        foreach ($vars as $key => $val) {
            $this->$key = null;
        }
    }

    /**
     * @param string $route
     *
     * @return string
     * @throws AException
     * @throws ReflectionException
     */
    protected function dispatchPrePost($route)
    {
        $result = '';
        if ($this->extensions->isExtensionController($route)) {
            //save output
            $output = $this->response->getOutput();
            //reset to save controller output
            $this->response->setOutput('');

            $dispatch_pre = new ADispatcher($route, ["instance_id" => '']);
            $dispatch_pre->dispatch();
            $result = $this->response->getOutput();

            //restore output
            $this->response->setOutput($output);
        }

        return $result;
    }

    /**
     * This function to dispatch the controller and get and destroy it's output
     *
     * @param string $controller
     *
     * @return string
     * @throws AException
     * @throws ReflectionException
     */
    public function dispatchGetOutput($controller = '')
    {
        $this->dispatch($controller);
        $output = $this->response->getOutput();
        $this->response->setOutput('');
        return $output;
    }

    /**
     * @param string $parent_controller
     *
     * @return null|string
     * @throws AException
     * @throws ReflectionException
     */
    public function dispatch($parent_controller = '')
    {
        ADebug::checkpoint($this->class . '/' . $this->method . ' dispatch START');

        //Process the controller, layout and children

        //check if we have missing class or everything
        if (empty($this->class) && has_value($this->file)) {
            #Build back trace of calling functions to provide more details
            $backtrace = debug_backtrace();
            $function_stack = '';
            if (is_object($parent_controller) && strlen($parent_controller->rt()) > 1) {
                $function_stack = 'Parent Controller: ' . $parent_controller->rt() . ' | ';
            }

            for ($i = 1; $i < sizeof($backtrace); $i++) {
                $function_stack .= ' < ' . $backtrace[$i]['function'];
            }
            $url = $this->request->server['REQUEST_URI'];
            $error = new AError(
                'Error: URL: ' . $url
                . ' Could not load controller ' . $this->controller . '! Call stack: '
                . $function_stack,
                AC_ERR_CLASS_CLASS_NOT_EXIST
            );
            $error->toLog()->toDebug();
            return null;
        } elseif (
            empty($this->file)
            && empty($this->class)
            || empty($this->method)
        ) {
            $warning_txt = 'ADispatch: skipping unavailable controller …';
            $warning = new AWarning($warning_txt);
            $warning->toDebug();
            return null;
        }

        //check for controller.pre
        $output_pre = $this->dispatchPrePost($this->controller . POSTFIX_PRE);

        /** @noinspection PhpIncludeInspection */
        require_once($this->file);
        /**
         * @var $controller AController
         */
        $controller = null;
        if (class_exists($this->class)) {
            $controller = new $this->class(
                $this->registry,
                $this->args["instance_id"] ?? null,
                $this->controller,
                $parent_controller
            );
            $controller->dispatcher = $this;
        } else {
            $error = new AError('Error: controller class not exist ' . $this->class . '!', AC_ERR_CLASS_CLASS_NOT_EXIST);
            $error->toLog()->toDebug();
        }
        try {
            if (is_callable([$controller, $this->method])) {
                /**
                 * @var $dispatch ADispatcher
                 */
                $r = new ReflectionMethod($controller, $this->method);
                $params = $r->getParameters();
                if (!$params) {
                    $args = [];
                } else {
                    $args = $this->args;
                }

                $rfl = new ReflectionClass($controller);
                $method = $rfl->getMethod($this->method);
                if ($method) {
                    $allParameters = $method->getParameters();
                    if ($allParameters) {
                        $methodParams = [];
                        foreach ($allParameters as $p) {
                            if (isset($args[$p->name])) {
                                $methodParams[$p->name] = $args[$p->name];
                            }
                        }
                        if ($methodParams && isAssocArray($args)) {
                            $args = $methodParams;
                        } elseif (!$methodParams && isAssocArray($args)) {
                            $args = [];
                        }
                    }
                }

                $dispatch = call_user_func_array([$controller, $this->method], $args);
                //Check if return is a dispatch and need to call new page
                if ($dispatch && is_object($dispatch)) {
                    if ($this->args["instance_id"] == 0) {
                        //If main controller come back for new dispatch
                        return $dispatch->getController() . '/' . $dispatch->getMethod();
                    } else {
                        // Call new dispatch for new controller and exit
                        $dispatch->dispatch();
                        return null;
                    }
                } else {
                    if ($dispatch == 'completed') {
                        //Check if we have message completed in controller response.
                        //If completed. stop further execution.
                        return 'completed';
                    }
                }
                /**
                 * Load layout and process children controllers
                 * @method AController getChildren()
                 */
                $children = $controller->getChildren();

                ADebug::variable('Processing children of ' . $this->controller, $children);

                //Process each child controller
                foreach ($children as $child) {
                    //Add highest Debug level here with backtrace to review this
                    ADebug::checkpoint(
                        $child['controller'] . ' ( child of ' . $this->controller
                        . ', instance_id: ' . $child['instance_id'] . ' ) dispatch START'
                    );
                    //Process each child and create dispatch to call recursive
                    $dispatch = new ADispatcher($child['controller'], ["instance_id" => $child['instance_id']]);
                    $dispatch->dispatch($controller);
                    // Append output of child controller to current controller
                    if (isset($child['position'])
                        && $child['position']) { // made for recognizing few custom_blocks in the same placeholder
                        $controller->view->assign(
                            $child['block_txt_id'] . '_' . $child['instance_id'],
                            $this->response->getOutput()
                        );
                    } else {
                        $controller->view->assign($child['block_txt_id'], $this->response->getOutput());
                    }
                    //clean up and remove output
                    $this->response->setOutput('');
                    ADebug::checkpoint($child['controller'] . ' ( child of ' . $this->controller . ' ) dispatch END');
                }
                //Request controller to generate output
                $controller->finalize();

                //check for controller.pre
                $output_post = $this->dispatchPrePost($this->controller . POSTFIX_POST);
                //add "pre" and "post" controllers output
                $this->response->setOutput($output_pre . $this->response->getOutput() . $output_post);

                //clean up and destroy the object
                unset($controller, $dispatch);
            } else {
                $err = new AError(
                    'Error: controller method not exist ' . $this->class . '::'
                    . $this->method . '!' . PHP_EOL . 'GET: ' . var_export($_GET, true),
                    AC_ERR_CLASS_METHOD_NOT_EXIST
                );
                $err->toLog()->toDebug();
                if (in_array($this->controller_type, ['responses', 'api', 'task'])) {
                    $dd = new ADispatcher('responses/error/ajaxerror/not_found');
                    $dd->dispatch();
                }
                if ($this->controller_type == 'pages') {
                    redirect($this?->registry->get('html')?->getSecureURL('index/home'));
                }
            }
        } //catching output of around hook (it can be only one)
        catch (AException $e) {
            if ($e->getCode() != AC_HOOK_OVERRIDE) {
                throw $e;
            }
        }
        ADebug::checkpoint($this->class . '/' . $this->method . ' dispatch END');
        return null;
    }

    /**
     * @return string
     */
    public function getType()
    {
        return $this->controller_type;
    }

    /**
     * @return string
     */
    public function getController()
    {
        return $this->controller;
    }

    /**
     * @return string
     */
    public function getFile()
    {
        return $this->file;
    }

    /**
     * @return string
     */
    public function getClass()
    {
        return $this->class;
    }

    /**
     * @return string
     */
    public function getMethod()
    {
        return $this->method;
    }

    /**
     * @return array
     */
    public function getArgs()
    {
        return $this->args;
    }
}
