<?php
/**
 * Magento
 *
 * NOTICE OF LICENSE
 *
 * This source file is subject to the Open Software License (OSL 3.0)
 * that is bundled with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://opensource.org/licenses/osl-3.0.php
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@magento.com so we can send you a copy immediately.
 *
 * DISCLAIMER
 *
 * Do not edit or add to this file if you wish to upgrade Magento to newer
 * versions in the future. If you wish to customize Magento for your
 * needs please refer to http://www.magento.com for more information.
 *
 * @category    Mage
 * @package     Mage
 * @copyright  Copyright (c) 2006-2020 Magento, Inc. (http://www.magento.com)
 * @license    http://opensource.org/licenses/osl-3.0.php  Open Software License (OSL 3.0)
 */

/**
 * Main Mage hub class
 *
 * @author      Magento Core Team <core@magentocommerce.com>
 */
final class Mage
{
    /**
     * Registry collection
     *
     * @var array
     */
    static private $_registry = array();

    /**
     * Application model
     *
     * @var ?Mage_Core_Model_App
     */
    static private $_app;

    /**
     * Config Model
     *
     * @var Mage_Core_Model_Config
     */
    static private $_config;

    /**
     * Event Collection Object
     *
     * @var Varien_Event_Collection
     */
    static private $_events;

    /**
     * Object cache instance
     *
     * @var Varien_Object_Cache
     */
    static private $_objects;

    /**
     * Is developer mode flag
     */
    static private bool $_isDeveloperMode = false;

    /**
     * Is allow throw Exception about headers already sent
     */
    public static bool $headersSentThrowsException = true;

    /**
     * Is installed flag
     *
     * @var bool
     */
    static private $_isInstalled;

    /**
     * Magento edition constants
     */
    const EDITION_COMMUNITY    = 'Community';
    const EDITION_ENTERPRISE   = 'Enterprise';
    const EDITION_PROFESSIONAL = 'Professional';
    const EDITION_GO           = 'Go';

    /**
     * Current Magento edition.
     */
    static private string $_currentEdition = self::EDITION_COMMUNITY;

    /**
     * Gets the current Magento version string
     *
     * @return string
     */
    public static function getVersion(): string
    {
        $i = self::getVersionInfo();
        return trim("{$i['major']}.{$i['minor']}.{$i['revision']}" . ($i['patch'] != '' ? ".{$i['patch']}" : "")
                        . "-{$i['stability']}{$i['number']}", '.-');
    }

    /**
     * Gets the detailed Magento version information
     *
     * @return array
     */
    public static function getVersionInfo(): array
    {
        return array(
            'major'     => '1',
            'minor'     => '9',
            'revision'  => '4',
            'patch'     => '5',
            'stability' => '',
            'number'    => '',
        );
    }

    /**
     * Gets the current OpenMage version string
     * @link https://openmage.github.io/supported-versions.html
     * @link https://semver.org/
     */
    public static function getOpenMageVersion(): string
    {
        $i = self::getOpenMageVersionInfo();
        $versionString = "{$i['major']}.{$i['minor']}.{$i['patch']}";
        if ( $i['stability'] || $i['number'] ) {
            $versionString .= "-";
            if ( $i['stability'] && $i['number'] ) {
                $versionString .= implode('.', [$i['stability'], $i['number']]);
            } else {
                $versionString .= implode('', [$i['stability'], $i['number']]);
            }
        }
        return trim(
            $versionString,
            '.-'
        );
    }

    /**
     * Gets the detailed OpenMage version information
     * @link https://openmage.github.io/supported-versions.html
     * @link https://semver.org/
     */
    public static function getOpenMageVersionInfo(): array
    {
        $majorVersion = 20;

        /**
         * This code construct is to make merging for forward porting of changes easier.
         * By having the version numbers of different branches in own lines, they do not provoke a merge conflict
         * also as releases are usually done together, this could in theory be done at once.
         * The major Version then needs to be only changed once per branch.
         */
        if ($majorVersion === 20) {
            return array(
                'major'     => '20',
                'minor'     => '0',
                'patch'     => '15',
                'stability' => '', // beta,alpha,rc
                'number'    => '', // 1,2,3,0.3.7,x.7.z.92 @see https://semver.org/#spec-item-9
            );
        }

        return array(
            'major'     => '19',
            'minor'     => '4',
            'patch'     => '17',
            'stability' => '', // beta,alpha,rc
            'number'    => '', // 1,2,3,0.3.7,x.7.z.92 @see https://semver.org/#spec-item-9
        );
    }

    /**
     * Get current Magento edition
     */
    public static function getEdition(): string
    {
       return self::$_currentEdition;
    }

    /**
     * Set all my static data to defaults
     */
    public static function reset(): void
    {
        self::$_registry        = array();
        self::$_app             = null;
        self::$_config          = null;
        self::$_events          = null;
        self::$_objects         = null;
        self::$_isDeveloperMode = false;
        self::$_isInstalled     = null;
        // do not reset $headersSentThrowsException
    }

    /**
     * Register a new variable
     *
     * @param string $key
     * @param mixed $value
     * @param bool $graceful
     * @throws Mage_Core_Exception
     */
    public static function register($key, $value, $graceful = false): void
    {
        if (isset(self::$_registry[$key])) {
            if ($graceful) {
                return;
            }
            self::throwException('Mage registry key "'.$key.'" already exists');
        }
        self::$_registry[$key] = $value;
    }

    /**
     * Unregister a variable from register by key
     *
     * @param string $key
     */
    public static function unregister($key): void
    {
        if (isset(self::$_registry[$key])) {
            if (is_object(self::$_registry[$key]) && (method_exists(self::$_registry[$key], '__destruct'))) {
                self::$_registry[$key]->__destruct();
            }
            unset(self::$_registry[$key]);
        }
    }

    /**
     * Retrieve a value from registry by a key
     *
     * @param string $key
     * @return mixed
     */
    public static function registry($key)
    {
        return self::$_registry[$key] ?? null;
    }

    /**
     * Retrieve application root absolute path
     */
    public static function getRoot(): string
    {
        return __DIR__;
    }

    /**
     * Retrieve Events Collection
     *
     * @return Varien_Event_Collection $collection
     */
    public static function getEvents()
    {
        return self::$_events;
    }

    /**
     * Varien Objects Cache
     *
     * @param string $key optional, if specified will load this key
     * @return Varien_Object_Cache
     */
    public static function objects($key = null)
    {
        if (!self::$_objects) {
            self::$_objects = new Varien_Object_Cache;
        }
        if (is_null($key)) {
            return self::$_objects;
        } else {
            return self::$_objects->load($key);
        }
    }

    /**
     * Retrieve application root absolute path
     *
     * @param string $type
     * @return string
     */
    public static function getBaseDir($type = 'base')
    {
        return self::getConfig()->getOptions()->getDir($type);
    }

    /**
     * Retrieve module absolute path by directory type
     *
     * @param string $type
     * @param string $moduleName
     * @return string
     */
    public static function getModuleDir($type, $moduleName)
    {
        return self::getConfig()->getModuleDir($type, $moduleName);
    }

    /**
     * Retrieve config value for store by path
     *
     * @param string $path
     * @param null|string|bool|int|Mage_Core_Model_Store $store
     * @return mixed
     */
    public static function getStoreConfig($path, $store = null)
    {
        return self::app()->getStore($store)->getConfig($path);
    }

    /**
     * Retrieve config flag for store by path
     *
     * @param string $path
     * @param mixed $store
     * @return bool
     */
    public static function getStoreConfigFlag($path, $store = null)
    {
        $flag = strtolower(self::getStoreConfig($path, $store) ?? "");
        if (!empty($flag) && 'false' !== $flag) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * Get base URL path by type
     *
     * @param string $type
     * @param null|bool $secure
     * @return string
     */
    public static function getBaseUrl($type = Mage_Core_Model_Store::URL_TYPE_LINK, $secure = null)
    {
        return self::app()->getStore()->getBaseUrl($type, $secure);
    }

    /**
     * Generate url by route and parameters
     *
     * @param   string $route
     * @param   array $params
     * @return  string
     */
    public static function getUrl($route = '', $params = array())
    {
        return self::getModel('core/url')->getUrl($route, $params);
    }

    /**
     * Get design package singleton
     *
     * @return Mage_Core_Model_Design_Package
     */
    public static function getDesign()
    {
        return self::getSingleton('core/design_package');
    }

    /**
     * Retrieve a config instance
     *
     * @return Mage_Core_Model_Config
     */
    public static function getConfig()
    {
        return self::$_config;
    }

    /**
     * Add observer to even object
     *
     * @param string $eventName
     * @param callback $callback
     * @param array $data
     * @param string $observerName
     * @param string $observerClass
     * @return Varien_Event_Collection
     */
    public static function addObserver($eventName, $callback, $data = array(), $observerName = '', $observerClass = '')
    {
        if ($observerClass == '') {
            $observerClass = 'Varien_Event_Observer';
        }
        $observer = new $observerClass();
        $observer->setName($observerName)->addData($data)->setEventName($eventName)->setCallback($callback);
        return self::getEvents()->addObserver($observer);
    }

    /**
     * Dispatch event
     *
     * Calls all observer callbacks registered for this event
     * and multiple observers matching event name pattern
     *
     * @param string $name
     * @param array $data
     * @return Mage_Core_Model_App
     */
    public static function dispatchEvent($name, array $data = array())
    {
        Varien_Profiler::start('DISPATCH EVENT:'.$name);
        $result = self::app()->dispatchEvent($name, $data);
        Varien_Profiler::stop('DISPATCH EVENT:'.$name);
        return $result;
    }

    /**
     * Retrieve model object
     *
     * @link    Mage_Core_Model_Config::getModelInstance
     * @param   string $modelClass
     * @param   array|object $arguments
     * @return  Mage_Core_Model_Abstract|false
     */
    public static function getModel($modelClass = '', $arguments = array())
    {
        return self::getConfig()->getModelInstance($modelClass, $arguments);
    }

    /**
     * Retrieve model object singleton
     *
     * @param   string $modelClass
     * @param   array $arguments
     * @return  Mage_Core_Model_Abstract
     */
    public static function getSingleton($modelClass = '', array $arguments=array())
    {
        $registryKey = '_singleton/' . $modelClass;
        if (!isset(self::$_registry[$registryKey])) {
            self::register($registryKey, self::getModel($modelClass, $arguments));
        }
        return self::$_registry[$registryKey];
    }

    /**
     * Retrieve object of resource model
     *
     * @param   string $modelClass
     * @param   array $arguments
     * @return  Mage_Core_Model_Resource_Db_Collection_Abstract|false
     */
    public static function getResourceModel($modelClass, $arguments = array())
    {
        return self::getConfig()->getResourceModelInstance($modelClass, $arguments);
    }

    /**
     * Retrieve Controller instance by ClassName
     *
     * @param string $class
     * @param Mage_Core_Controller_Request_Http $request
     * @param Mage_Core_Controller_Response_Http $response
     * @param array $invokeArgs
     * @return Mage_Core_Controller_Front_Action
     */
    public static function getControllerInstance($class, $request, $response, array $invokeArgs = array())
    {
        return new $class($request, $response, $invokeArgs);
    }

    /**
     * Retrieve resource vodel object singleton
     *
     * @param   string $modelClass
     * @param   array $arguments
     * @return  object
     */
    public static function getResourceSingleton($modelClass = '', array $arguments = array())
    {
        $registryKey = '_resource_singleton/' . $modelClass;
        if (!isset(self::$_registry[$registryKey])) {
            self::register($registryKey, self::getResourceModel($modelClass, $arguments));
        }
        return self::$_registry[$registryKey];
    }

    /**
     * @deprecated, use self::helper()
     *
     * @param string $type
     * @return object|false
     */
    public static function getBlockSingleton($type)
    {
        $action = self::app()->getFrontController()->getAction();
        return $action ? $action->getLayout()->getBlockSingleton($type) : false;
    }

    /**
     * Retrieve helper object
     *
     * @param string $name the helper name
     * @return Mage_Core_Helper_Abstract
     */
    public static function helper($name)
    {
        $registryKey = '_helper/' . $name;
        if (!isset(self::$_registry[$registryKey])) {
            $helperClass = self::getConfig()->getHelperClassName($name);
            self::register($registryKey, new $helperClass);
        }
        return self::$_registry[$registryKey];
    }

    /**
     * Retrieve resource helper object
     *
     * @param string $moduleName
     * @return Mage_Core_Model_Resource_Helper_Abstract
     */
    public static function getResourceHelper($moduleName)
    {
        $registryKey = '_resource_helper/' . $moduleName;
        if (!isset(self::$_registry[$registryKey])) {
            $helperClass = self::getConfig()->getResourceHelper($moduleName);
            self::register($registryKey, $helperClass);
        }
        return self::$_registry[$registryKey];
    }

    /**
     * Return new exception by module to be thrown
     *
     * @param string $module
     * @param string $message
     * @param integer $code
     * @return Mage_Core_Exception
     */
    public static function exception($module = 'Mage_Core', $message = '', $code = 0)
    {
        $className = $module . '_Exception';
        return new $className($message, $code);
    }

    /**
     * Throw Exception
     *
     * @param string $message
     * @param string $messageStorage
     * @throws Mage_Core_Exception
     */
    public static function throwException($message, $messageStorage = null): void
    {
        if ($messageStorage && ($storage = self::getSingleton($messageStorage))) {
            $storage->addError($message);
        }
        throw new Mage_Core_Exception($message);
    }

    /**
     * Get initialized application object.
     *
     * @param string $code
     * @param string $type
     * @param array $options
     * @return Mage_Core_Model_App
     */
    public static function app($code = '', $type = 'store', $options = array())
    {
        if (null === self::$_app) {
            self::$_app = new Mage_Core_Model_App();
            self::$_events = new Varien_Event_Collection();
            self::_setIsInstalled($options);
            self::_setConfigModel($options);

            Varien_Profiler::start('self::app::init');
            self::$_app->init($code, $type, $options);
            Varien_Profiler::stop('self::app::init');
            self::$_app->loadAreaPart(Mage_Core_Model_App_Area::AREA_GLOBAL, Mage_Core_Model_App_Area::PART_EVENTS);
        }
        return self::$_app;
    }

    /**
     * @static
     * @param string $code
     * @param string $type
     * @param array $options
     * @param string|array $modules
     */
    public static function init($code = '', $type = 'store', $options = array(), $modules = array()): void
    {
        try {
            self::$_app     = new Mage_Core_Model_App();
            self::_setIsInstalled($options);
            self::_setConfigModel($options);

            if (!empty($modules)) {
                self::$_app->initSpecified($code, $type, $options, $modules);
            } else {
                self::$_app->init($code, $type, $options);
            }
        } catch (Mage_Core_Model_Session_Exception $e) {
            // TODO(m): Do we catch this?
            header('Location: ' . self::getBaseUrl());
            exit(0);
        } catch (Exception $e) {
            self::printException($e);
            exit(-1);
        }
    }

    /**
     * Front end main entry point
     *
     * @param string $code
     * @param string $type
     * @param array $options
     */
    public static function run($code = '', $type = 'store', $options = array()): void
    {
        try {
            Varien_Profiler::start('mage');
            if (isset($options['edition'])) {
                self::$_currentEdition = $options['edition'];
            }
            self::$_app    = new Mage_Core_Model_App();
            if (isset($options['request'])) {
                self::$_app->setRequest($options['request']);
            }
            if (isset($options['response'])) {
                self::$_app->setResponse($options['response']);
            }
            self::$_events = new Varien_Event_Collection();
            self::_setIsInstalled($options);
            self::_setConfigModel($options);
            self::$_app->run(array(
                'scope_code' => $code,
                'scope_type' => $type,
                'options'    => $options,
            ));
            Varien_Profiler::stop('mage');
        } catch (Mage_Core_Model_Session_Exception $e) {
            // TODO(m): Do anything with this?
            header('Location: ' . self::getBaseUrl());
            exit(0);
        } catch (Exception $e) {
            self::printException($e);
            exit(-1);
        }
    }

    /**
     * Set application isInstalled flag based on given options
     *
     * @param array $options
     */
    protected static function _setIsInstalled($options = array()): void
    {
        if (isset($options['is_installed']) && $options['is_installed']) {
            self::$_isInstalled = true;
        }
    }

    /**
     * Set application Config model
     *
     * @param array $options
     */
    protected static function _setConfigModel($options = array()): void
    {
        if (isset($options['config_model']) && class_exists($options['config_model'])) {
            $alternativeConfigModelName = $options['config_model'];
            unset($options['config_model']);
            $alternativeConfigModel = new $alternativeConfigModelName($options);
        } else {
            $alternativeConfigModel = null;
        }

        if (!is_null($alternativeConfigModel) && ($alternativeConfigModel instanceof Mage_Core_Model_Config)) {
            self::$_config = $alternativeConfigModel;
        } else {
            self::$_config = new Mage_Core_Model_Config($options);
        }
    }

    /**
     * Retrieve application installation flag
     *
     * @param string|array $options
     */
    public static function isInstalled($options = array()): bool
    {
        if (self::$_isInstalled === null) {
            if (is_string($options)) {
                $options = array('etc_dir' => $options);
            }
            $etcDir = self::getRoot() . DS . 'etc';
            if (!empty($options['etc_dir'])) {
                $etcDir = $options['etc_dir'];
            }
            $localConfigFile = $etcDir . DS . 'local.xml';

            self::$_isInstalled = false;

            if (is_readable($localConfigFile)) {
                $localConfig = simplexml_load_file($localConfigFile);
                date_default_timezone_set('UTC');
                if (($date = $localConfig->global->install->date) && strtotime($date)) {
                    self::$_isInstalled = true;
                }
            }
        }
        return self::$_isInstalled;
    }

    /**
     * log facility (??)
     *
     * @param string|array|object $message
     * @param ?Zend_Log::EMERG|Zend_Log::ALERT|Zend_Log::CRIT|Zend_Log::ERR|Zend_Log::WARN|Zend_Log::NOTICE|Zend_Log::INFO|Zend_Log::DEBUG $level
     * @param ?string $file
     */
    public static function log($message, $level = null, $file = ''): void {
        $channel = pathinfo($file ?: "system.log", PATHINFO_FILENAME);
        $level = is_null($level) ? Zend_Log::DEBUG : $level;

        if( ! $message instanceof Throwable) {
            if(is_array($message) || is_object($message)) {
                // TODO: Attempt to JSON-encode if it is an array/object?
                $message = print_r($message, true);
            }
        }

        Awardit_Magento_Logger::writeLog(
            $channel,
            $level,
            Awardit_Magento_Logger::LEVELS[$level],
            $message,
            [
                "caller" => Awardit_Magento_Logger::getCaller(),
            ]
        );
    }

    /**
     * Write exception to log
     *
     * @param Zend_Log::EMERG|Zend_Log::ALERT|Zend_Log::CRIT|Zend_Log::ERR|Zend_Log::WARN|Zend_Log::NOTICE|Zend_Log::INFO|Zend_Log::DEBUG $level
     */
    public static function logException(
        Throwable $e,
        string $channel = "exception",
        int $level = Zend_Log::ERR
    ): void {
        Awardit_Magento_Logger::writeLog(
            $channel,
            $level,
            Awardit_Magento_Logger::LEVELS[$level],
            $e,
            [
                "caller" => Awardit_Magento_Logger::getCaller(),
            ],
        );
    }

    public static function getCurrentStoreCode(): ?string {
        $app = self::$_app;

        if( ! $app) {
            return null;
        }

        return $app->getCurrentStoreCode();
    }

    /**
     * Set enabled developer mode
     *
     * @param bool $mode
     */
    public static function setIsDeveloperMode($mode): bool
    {
        self::$_isDeveloperMode = (bool)$mode;
        return self::$_isDeveloperMode;
    }

    /**
     * Retrieve enabled developer mode
     */
    public static function getIsDeveloperMode(): bool
    {
        return self::$_isDeveloperMode;
    }

    /**
     * Display exception
     *
     * @param Throwable $e
     * @param string $extra
     * @return never
     */
    public static function printException(Throwable $e, $extra = ''): void
    {
        Awardit_Magento_Logger::writeLog(
            "exception",
            Zend_Log::ERR,
            Awardit_Magento_Logger::LEVELS[Zend_Log::ERR],
            $e,
            [
                "caller" => Awardit_Magento_Logger::getCaller(),
            ],
        );

        if (self::$_isDeveloperMode) {
            $htmlErrors = in_array(strtolower(trim(@ini_get("html_errors") ?: "Off")), ["1", "on"]);
            $output = array_filter([
                $extra,
                $e->__toString(),
            ]);

            if($htmlErrors) {
                if( ! headers_sent()) {
                    header("Content-Type: text/html; charset=utf-8");
                }

                echo "<pre>".htmlspecialchars(implode("\n\n", $output), ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5, "UTF-8")."</pre>";
            }
            else {
                if( ! headers_sent()) {
                    header("Content-Type: text/plain; charset=utf-8");
                }

                echo implode("\n", $output);
            }
        }

        exit(-1);
    }

    /**
     * Set is downloader flag
     *
     * @deprecated
     */
    public static function setIsDownloader(): void
    {

    }
}
