<?php

/**
 * @package     Joomla.Installation
 * @subpackage  Application
 *
 * @copyright   (C) 2013 Open Source Matters, Inc. <https://www.joomla.org>
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

namespace Joomla\CMS\Installation\Application;

use Joomla\Application\Web\WebClient;
use Joomla\CMS\Application\CMSApplication;
use Joomla\CMS\Date\Date;
use Joomla\CMS\Document\Document;
use Joomla\CMS\Document\FactoryInterface;
use Joomla\CMS\Document\HtmlDocument;
use Joomla\CMS\Exception\ExceptionHandler;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\LanguageFactoryInterface;
use Joomla\CMS\Language\LanguageHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Factory\MVCFactory;
use Joomla\CMS\Uri\Uri;
use Joomla\Database\DatabaseInterface;
use Joomla\DI\Container;
use Joomla\Filesystem\Folder;
use Joomla\Input\Input;
use Joomla\Registry\Registry;
use Joomla\Session\SessionEvent;

// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects

/**
 * Joomla! Installation Application class.
 *
 * @since  3.1
 */
final class InstallationApplication extends CMSApplication
{
    use \Joomla\CMS\Application\ExtensionNamespaceMapper;

    /**
     * Class constructor.
     *
     * @param   ?Input      $input      An optional argument to provide dependency injection for the application's input
     *                                  object.  If the argument is a Input object that object will become the
     *                                  application's input object, otherwise a default input object is created.
     * @param   ?Registry   $config     An optional argument to provide dependency injection for the application's
     *                                  config object.  If the argument is a Registry object that object will become
     *                                  the application's config object, otherwise a default config object is created.
     * @param   ?WebClient  $client     An optional argument to provide dependency injection for the application's
     *                                  client object.  If the argument is a WebClient object that object will become the
     *                                  application's client object, otherwise a default client object is created.
     * @param   ?Container  $container  Dependency injection container.
     *
     * @since   3.1
     */
    public function __construct(?Input $input = null, ?Registry $config = null, ?WebClient $client = null, ?Container $container = null)
    {
        // Register the application name.
        $this->name = 'installation';

        // Register the client ID.
        $this->clientId = 2;

        // Run the parent constructor.
        parent::__construct($input, $config, $client, $container);

        // Store the debug value to config based on the JDEBUG flag.
        $this->config->set('debug', JDEBUG);

        // Register the config to Factory.
        Factory::$config = $this->config;

        // Set the root in the URI one level up.
        $parts = explode('/', Uri::base(true));
        array_pop($parts);
        Uri::root(null, implode('/', $parts));
    }

    /**
     * After the session has been started we need to populate it with some default values.
     *
     * @param   SessionEvent  $event  Session event being triggered
     *
     * @return  void
     *
     * @since   4.0.0
     */
    public function afterSessionStart(SessionEvent $event)
    {
        $session = $event->getSession();

        if ($session->isNew()) {
            $session->set('registry', new Registry('session'));
        }
    }

    /**
     * Method to display errors in language parsing.
     *
     * @return  string  Language debug output.
     *
     * @since   3.1
     */
    public function debugLanguage()
    {
        if ($this->getDocument()->getType() != 'html') {
            return '';
        }

        $lang   = Factory::getLanguage();
        $output = '<h4>' . Text::_('JDEBUG_LANGUAGE_FILES_IN_ERROR') . '</h4>';

        $errorfiles = $lang->getErrorFiles();

        if (\count($errorfiles)) {
            $output .= '<ul>';

            foreach ($errorfiles as $error) {
                $output .= "<li>$error</li>";
            }

            $output .= '</ul>';
        } else {
            $output .= '<pre>' . Text::_('JNONE') . '</pre>';
        }

        $output .= '<h4>' . Text::_('JDEBUG_LANGUAGE_UNTRANSLATED_STRING') . '</h4>';
        $output .= '<pre>';
        $orphans = $lang->getOrphans();

        if (\count($orphans)) {
            ksort($orphans, SORT_STRING);

            $guesses = [];

            foreach ($orphans as $key => $occurrence) {
                $guess = str_replace('_', ' ', $key);

                $parts = explode(' ', $guess);

                if (\count($parts) > 1) {
                    array_shift($parts);
                    $guess = implode(' ', $parts);
                }

                $guess = trim($guess);

                $key = strtoupper(trim($key));
                $key = preg_replace('#\s+#', '_', $key);
                $key = preg_replace('#\W#', '', $key);

                // Prepare the text.
                $guesses[] = $key . '="' . $guess . '"';
            }

            $output .= implode("\n", $guesses);
        } else {
            $output .= '<pre>' . Text::_('JNONE') . '</pre>';
        }

        $output .= '</pre>';

        return $output;
    }

    /**
     * Dispatch the application.
     *
     * @return  void
     *
     * @since   3.1
     */
    public function dispatch()
    {
        // Load the document to the API.
        $this->loadDocument();

        // Set up the params
        $document = $this->getDocument();

        // Register the document object with Factory.
        Factory::$document = $document;

        // Execute the task.
        ob_start();
        $this->executeController();
        $contents = ob_get_clean();

        // If debug language is set, append its output to the contents.
        if ($this->config->get('debug_lang')) {
            $contents .= $this->debugLanguage();
        }

        // Set the content on the document
        $this->getDocument()->setBuffer($contents, 'component');

        // Set the document title
        $document->setTitle(Text::_('INSTL_PAGE_TITLE'));
    }

    /**
     * Method to run the Web application routines.
     *
     * @return  void
     *
     * @since   3.1
     */
    protected function doExecute()
    {
        // Ensure we load the namespace loader
        $this->createExtensionNamespaceMap();

        // Initialise the application.
        $this->initialiseApp();

        // Dispatch the application.
        $this->dispatch();
    }

    /**
     * Execute the application.
     *
     * @return  void
     *
     * @since   4.0.0
     */
    public function execute()
    {
        try {
            // Perform application routines.
            $this->doExecute();

            // If we have an application document object, render it.
            if ($this->document instanceof Document) {
                // Render the application output.
                $this->render();
            }

            // If gzip compression is enabled in configuration and the server is compliant, compress the output.
            if ($this->get('gzip') && !\ini_get('zlib.output_compression') && (\ini_get('output_handler') != 'ob_gzhandler')) {
                $this->compress();
            }
        } catch (\Throwable $throwable) {
            ExceptionHandler::render($throwable);
        }

        // Send the application response.
        $this->respond();
    }

    /**
     * Method to load a PHP configuration class file based on convention and return the instantiated data object.  You
     * will extend this method in child classes to provide configuration data from whatever data source is relevant
     * for your specific application.
     *
     * @param   string  $file   The path and filename of the configuration file. If not provided, configuration.php
     *                          in JPATH_BASE will be used.
     * @param   string  $class  The class name to instantiate.
     *
     * @return  mixed   Either an array or object to be loaded into the configuration object.
     *
     * @since   1.7.3
     * @throws  \RuntimeException
     */
    protected function fetchConfigurationData($file = '', $class = 'JConfig')
    {
        return [];
    }

    /**
     * Executed a controller from the input task.
     *
     * @return  void
     *
     * @since   4.0.0
     */
    private function executeController()
    {
        $task = $this->input->getCmd('task', '');

        // The name of the controller
        $controllerName = 'display';

        // Parse task in format controller.task
        if ($task !== '') {
            [$controllerName, $task] = explode('.', $task, 2);
        }

        $factory = new MVCFactory('Joomla\\CMS', $this->getLogger());
        $factory->setDatabase($this->getContainer()->get(DatabaseInterface::class));

        // Create the instance
        $controller = $factory->createController($controllerName, 'Installation', ['base_path' => JPATH_INSTALLATION], $this, $this->input);

        // Execute the task
        $controller->execute($task);
    }

    /**
     * Returns the language code and help URL set in the localise.xml file.
     * Used for forcing a particular language in localised releases.
     *
     * @return  mixed  False on failure, array on success.
     *
     * @since   3.1
     */
    public function getLocalise()
    {
        $xml = simplexml_load_file(JPATH_INSTALLATION . '/localise.xml');

        if (!$xml) {
            return false;
        }

        // Check that it's a localise file.
        if ($xml->getName() !== 'localise') {
            return false;
        }

        $ret = [];

        $ret['language']   = (string) $xml->forceLang;
        $ret['debug']      = (string) $xml->debug;
        $ret['sampledata'] = (string) $xml->sampledata;

        return $ret;
    }

    /**
     * Returns the installed language files in the administrative and frontend area.
     *
     * @param   ?DatabaseInterface  $db  Database driver.
     *
     * @return  array  Array with installed language packs in admin and site area.
     *
     * @since   3.1
     */
    public function getLocaliseAdmin(?DatabaseInterface $db = null)
    {
        $langfiles = [];

        // If db connection, fetch them from the database.
        if ($db) {
            foreach (LanguageHelper::getInstalledLanguages(null, null, null, null, null, null, $db) as $clientId => $language) {
                $clientName = $clientId === 0 ? 'site' : 'admin';

                foreach ($language as $languageCode => $lang) {
                    $langfiles[$clientName][] = $lang->element;
                }
            }
        } else {
            // Read the folder names in the site and admin area.
            $langfiles['site']  = Folder::folders(LanguageHelper::getLanguagePath(JPATH_SITE));
            $langfiles['admin'] = Folder::folders(LanguageHelper::getLanguagePath(JPATH_ADMINISTRATOR));
        }

        return $langfiles;
    }

    /**
     * Initialise the application.
     *
     * @param   array  $options  An optional associative array of configuration settings.
     *
     * @return  void
     *
     * @since   3.1
     */
    protected function initialiseApp($options = [])
    {
        // Get the localisation information provided in the localise.xml file.
        $forced = $this->getLocalise();

        // Check the request data for the language.
        if (empty($options['language'])) {
            $requestLang = $this->input->getCmd('lang', null);

            if ($requestLang !== null) {
                $options['language'] = $requestLang;
            }
        }

        // Check the session for the language.
        if (empty($options['language'])) {
            $sessionOptions = $this->getSession()->get('setup.options');

            if (isset($sessionOptions['language'])) {
                $options['language'] = $sessionOptions['language'];
            }
        }

        // This could be a first-time visit - try to determine what the client accepts.
        if (empty($options['language'])) {
            if (!empty($forced['language'])) {
                $options['language'] = $forced['language'];
            } else {
                $options['language'] = LanguageHelper::detectLanguage();

                if (empty($options['language'])) {
                    $options['language'] = 'en-GB';
                }
            }
        }

        // Give the user English.
        if (empty($options['language'])) {
            $options['language'] = 'en-GB';
        }

        // Set the official helpurl.
        $options['helpurl'] = 'https://help.joomla.org/proxy?keyref=Help{major}{minor}:{keyref}&lang={langcode}';

        // Store helpurl in the session.
        $this->getSession()->set('setup.helpurl', $options['helpurl']);

        // Set the language in the class.
        $this->config->set('language', $options['language']);
        $this->config->set('debug_lang', $forced['debug']);
        $this->config->set('sampledata', $forced['sampledata']);
        $this->config->set('helpurl', $options['helpurl']);

        // Build our language object
        $lang = $this->getContainer()->get(LanguageFactoryInterface::class)->createLanguage($options['language'], $forced['debug']);

        // Load the language to the API
        $this->loadLanguage($lang);

        // Register the language object with Factory
        Factory::$language = $this->getLanguage();
    }

    /**
     * Allows the application to load a custom or default document.
     *
     * The logic and options for creating this object are adequately generic for default cases
     * but for many applications it will make sense to override this method and create a document,
     * if required, based on more specific needs.
     *
     * @param   ?Document  $document  An optional document object. If omitted, the factory document is created.
     *
     * @return  InstallationApplication This method is chainable.
     *
     * @since   3.2
     */
    public function loadDocument(?Document $document = null)
    {
        if ($document === null) {
            $lang = $this->getLanguage();
            $type = $this->input->get('format', 'html', 'word');
            $date = new Date('now');

            $attributes = [
                'charset'      => 'utf-8',
                'lineend'      => 'unix',
                'tab'          => "\t",
                'language'     => $lang->getTag(),
                'direction'    => $lang->isRtl() ? 'rtl' : 'ltr',
                'mediaversion' => md5($date->format('YmdHi')),
            ];

            $document = $this->getContainer()->get(FactoryInterface::class)->createDocument($type, $attributes);

            // Register the instance to Factory.
            Factory::$document = $document;
        }

        $this->document = $document;

        return $this;
    }

    /**
     * Rendering is the process of pushing the document buffers into the template
     * placeholders, retrieving data from the document and pushing it into
     * the application response buffer.
     *
     * @return  void
     *
     * @since   3.1
     */
    public function render()
    {
        $options = [];

        if ($this->document instanceof HtmlDocument) {
            $file = $this->input->getCmd('tmpl', 'index');

            $options = [
                'template'         => 'template',
                'file'             => $file . '.php',
                'directory'        => JPATH_THEMES,
                'params'           => '{}',
                "templateInherits" => '',
            ];
        }

        // Parse the document.
        $this->document->parse($options);

        // Render the document.
        $data = $this->document->render($this->get('cache_enabled'), $options);

        // Set the application output data.
        $this->setBody($data);
    }

    /**
     * Set configuration values.
     *
     * @param   array   $vars       Array of configuration values
     * @param   string  $namespace  The namespace
     *
     * @return  void
     *
     * @since   3.1
     */
    public function setCfg(array $vars = [], $namespace = 'config')
    {
        $this->config->loadArray($vars, $namespace);
    }

    /**
     * Returns the application \JMenu object.
     *
     * @param   string|null  $name     The name of the application/client.
     * @param   array        $options  An optional associative array of configuration settings.
     *
     * @return  null
     *
     * @since   3.2
     */
    public function getMenu($name = null, $options = [])
    {
        return null;
    }

    /**
     * Initialise the template.
     *
     * @return  void
     *
     * @since   6.1.0
     */
    protected function initialiseTemplate(): void
    {
        $this->template = (object) [
            'template'    => 'template',
            'params'      => new Registry(),
            'inheritable' => 0,
            'parent'      => '',
        ];
    }
}
