<?php

/*
 * LimeSurvey
 * Copyright (C) 2007-2026 The LimeSurvey Project Team
 * All rights reserved.
 * License: GNU/GPL License v2 or later, see LICENSE.php
 * LimeSurvey is free software. This version may have been modified pursuant
 * to the GNU General Public License, and as distributed it includes or
 * is derivative of works licensed under the GNU General Public License or
 * other free or open source software licenses.
 * See COPYRIGHT.php for copyright notices and details.
 *
  */


/**
 * Class QuestionTemplate
 *
 * Handles question-level template overrides. Each question can have its own
 * template that replaces default views and registers custom CSS/JS assets.
 *
 * @see QuestionTheme
 */
class QuestionTemplate extends CFormModel
{
    // Main variables
    /** @var Question $oQuestion The current question */
    public $oQuestion;
    /** @var boolean $bHasTemplate Does this question has a template? */
    public $bHasTemplate;

    /** @var string $sTemplateFolderName The folder of the template
     * applied to this question (if no template applied, it's false) */
    public $sTemplateFolderName;
    /** @var array $aViews Array of views the template can handle ($aViews['path_to_my_view']==true) */
    public $aViews;
    /** @var SimpleXMLElement $oConfig */
    public $oConfig;
    /** @var boolean $bHasCustomAttributes Does the template provides custom attributes? */
    public $bHasCustomAttributes;
    /** @var array $aCustomAttributes array (attribute=>value) */
    public $aCustomAttributes;

    /** @var string $sTemplatePath The path to the template */
    private $sTemplatePath;
    /** @var string $sTemplateUrl */
    private $sTemplateUrl;
    /** @var string $sTemplateQuestionPath The path to the folder corresponding
     * to the current question type */
    private $sTemplateQuestionPath;
    /** @var boolean $bHasConfigFile */
    private $bHasConfigFile;
    /** @var string $xmlFile The path to the xml file */
    private $xmlFile;
    /** @var boolean $bLoadCoreJs Should it render the core javascript of
     * this question (script are registered in qanda) */
    private $bLoadCoreJs;
    /** @var boolean $bLoadCoreCss Should it render the core CSS of this question
     * (script are registered in qanda) */
    private $bLoadCoreCss;
    /** @var boolean $bLoadCorePackage Should it render the core packages
     * of this question (script are registered in qanda) */
    private $bLoadCorePackage;

    /** @var QuestionTemplate $instance The instance of question template object */
    private static $instance;



    /**
     * Get a new instance of the template object
     * Each question on the page could have a different template.
     * So each question must have a new instance
     * @param Question $oQuestion
     * @return QuestionTemplate
     */
    public static function getNewInstance($oQuestion)
    {
        self::$instance = new QuestionTemplate();
        self::$instance->oQuestion = $oQuestion;
        self::$instance->aViews    = array();
        self::$instance->getQuestionTemplateFolderName(); // Will initiate $sTemplateFolderName and $bHasTemplate.
        self::$instance->setConfig();
        return self::$instance;
    }

    /**
     * Get the current instance of template question object.
     *
     * @param Question $oQuestion
     * @return QuestionTemplate
     * @internal param string $sTemplateName
     * @internal param int $iSurveyId
     */
    public static function getInstance($oQuestion = null)
    {
        if (empty(self::$instance) && $oQuestion != null) {
            self::getNewInstance($oQuestion);
        }
        return self::$instance;
    }


    /**
     * Check if the question template offers a specific replacement for the given view file.
     * @param string $sView The view name to check
     * @return bool True if the template provides a replacement for the view
     */
    public function checkIfTemplateHasView($sView)
    {
        if (!isset($this->aViews[$sView])) {
            $sTemplatePath = $this->getTemplatePath();
            if (!empty($sTemplatePath) && is_file("$sTemplatePath/$sView.twig")) {
                $this->aViews[$sView] = true;
            } else {
                $this->aViews[$sView] = false;
            }
        }
        return $this->aViews[$sView];
    }

    /**
     * Retrieve the template base path if exist
     * @return null|string
     */
    public function getTemplatePath()
    {
        if (!isset($this->sTemplatePath)) {
            $sTemplateFolderName    = $this->getQuestionTemplateFolderName();
            $sCoreQTemplateRootDir  = Yii::app()->getConfig("corequestionthemerootdir");
            $sUserQTemplateRootDir  = Yii::app()->getConfig("userquestionthemerootdir");

            // Core templates come first
            if (is_dir("$sCoreQTemplateRootDir/$sTemplateFolderName")) {
                $this->sTemplatePath = "$sCoreQTemplateRootDir/$sTemplateFolderName";
            } elseif (is_dir("$sUserQTemplateRootDir/$sTemplateFolderName")) {
                $this->sTemplatePath = "$sUserQTemplateRootDir/$sTemplateFolderName";
            }
        }
        return $this->sTemplatePath;
    }

    /**
     * Get the template folder name
     * @return false|string
     */
    public function getQuestionTemplateFolderName()
    {
        if ($this->sTemplateFolderName === null) {
            $aQuestionAttributes = QuestionAttribute::model()->getQuestionAttributes($this->oQuestion->qid);
            /** @var string|null */
            $questionThemeName = $this->oQuestion->question_theme_name;
            $this->sTemplateFolderName = (!empty($questionThemeName) && $questionThemeName != 'core') ? $questionThemeName : false;
        }
        $this->bHasTemplate = ($this->sTemplateFolderName != false);
        return $this->sTemplateFolderName;
    }

    /**
     * Register a core script file
     * @param string $sFile
     * @param int $pos
     */
    public function registerScriptFile($sFile, $pos = CClientScript::POS_BEGIN)
    {
        if ($this->templateLoadsCoreJs) {
            Yii::app()->getClientScript()->registerScriptFile($sFile, $pos);
        }
    }

    /**
     * Register a core script
     * @param string $sScript
     * @param int $pos
     */
    public function registerScript($sScript, $pos = CClientScript::POS_BEGIN)
    {
        if ($this->templateLoadsCoreJs) {
            Yii::app()->getClientScript()->registerScript($sScript, $pos);
        }
    }

    /**
     * Register a core css file
     * @param string $sCssFile
     * @param string $media
     */
    public function registerCssFile($sCssFile, $media = '')
    {
        if ($this->templateLoadsCoreCss) {
            Yii::app()->getClientScript()->registerCssFile($sCssFile, $media);
        }
    }

    /**
     * Register a core package file
     * @param string $sPackage
     */
    public function registerPackage($sPackage)
    {
        if ($this->templateLoadsCorePackage) {
            Yii::app()->getClientScript()->registerPackage($sPackage);
        }
    }

    /**
     * Return true if the core JavaScript should be loaded.
     * @return bool|null
     */
    public function templateLoadsCoreJs()
    {
        if (!isset($this->bLoadCoreJs)) {
            if ($this->bHasTemplate) {
                // Init config ($this->bHasConfigFile and $this->bLoadCoreJs )
                $this->setConfig();
                if ($this->bHasConfigFile) {
                    return $this->bLoadCoreJs;
                }
            }
            $this->bLoadCoreJs = true;
        }
        return $this->bLoadCoreJs;
    }

    /**
     * Return true if the core CSS should be loaded.
     * @return bool|null
     */
    public function templateLoadsCoreCss()
    {
        if (!isset($this->bLoadCoreCss)) {
            if ($this->bHasTemplate) {
                // Init config ($this->bHasConfigFile and $this->bLoadCoreCss )
                $this->setConfig();
                if ($this->bHasConfigFile) {
                    return $this->bLoadCoreCss;
                }
            }
            $this->bLoadCoreCss = true;
        }
        return $this->bLoadCoreCss;
    }

    /**
     * Return true if the core packages should be loaded.
     * @return bool|null
     */
    public function templateLoadsCorePackage()
    {
        if (!isset($this->bLoadCorePackage)) {
            if ($this->bHasTemplate) {
                // Init config ($this->bHasConfigFile and $this->bLoadCorePackage )
                $this->setConfig();
                if ($this->bHasConfigFile) {
                    return $this->bLoadCorePackage;
                }
            }
            $this->bLoadCoreCss = true;
        }
        return $this->bLoadCoreCss;
    }


    /**
     * Load and parse the question template XML configuration.
     * Sets template paths, engine flags (JS/CSS/package loading), and custom attributes.
     * @return void
     */
    public function setConfig()
    {
        if (!isset($this->oConfig)) {
            $oQuestion                    = $this->oQuestion;
            $sTemplatePath                = $this->getTemplatePath();
            if (empty($sTemplatePath)) {
                return;
            }
            $sFolderName                  = self::getFolderName($oQuestion->type);
            $this->sTemplateQuestionPath  = $sTemplatePath . '/survey/questions/answer/' . $sFolderName;
            $xmlFile                      = $this->sTemplateQuestionPath . '/config.xml';
            $this->bHasConfigFile         = is_file($xmlFile);

            if ($this->bHasConfigFile) {
                $sXMLConfigFile               = file_get_contents(realpath($xmlFile)); // Entity loader is disabled, so we can't use simplexml_load_file; so we must read the file with file_get_contents and convert it as a string
                $this->xmlFile                = $xmlFile;
                $this->oConfig                 = simplexml_load_string($sXMLConfigFile);

                $this->bLoadCoreJs             = $this->oConfig->engine->load_core_js;
                $this->bLoadCoreCss            = $this->oConfig->engine->load_core_css;
                $this->bLoadCorePackage        = $this->oConfig->engine->load_core_package;
                $this->bHasCustomAttributes    = !empty($this->oConfig->attributes);

                // Set the custom attributes
                // In QuestionTheme set at a complete array using json_decode(json_encode((array)$xml_config->attributes), true);
                if ($this->bHasCustomAttributes) {
                    $this->aCustomAttributes = array();
                    foreach ($this->oConfig->attributes->attribute as $oCustomAttribute) {
                        $attribute_name = (string) $oCustomAttribute->name;
                        if (isset($oCustomAttribute->i18n) && strval($oCustomAttribute->i18n)) {
                            $sLang = App()->language;
                            $oAttributeValue = QuestionAttribute::model()->find("qid=:qid and attribute=:custom_attribute and language =:language", array('qid' => $oQuestion->qid, 'custom_attribute' => $attribute_name, 'language' => $sLang));
                        } else {
                            $oAttributeValue = QuestionAttribute::model()->find("qid=:qid and attribute=:custom_attribute", array('qid' => $oQuestion->qid, 'custom_attribute' => $attribute_name));
                        }
                        if (is_object($oAttributeValue)) {
                            $this->aCustomAttributes[$attribute_name] = $oAttributeValue->value;
                        } else {
                            $this->aCustomAttributes[$attribute_name] = (string) $oCustomAttribute->default;
                        }
                    }
                }
            }
        }
    }

    /**
     * Register custom CSS and JavaScript assets defined in the question template config.
     * @return void
     */
    public function registerAssets()
    {
        if ($this->bHasConfigFile) {
            // Load the custom JS/CSS
            $aCssFiles   = (array) $this->oConfig->files->css->filename; // The CSS files of this template
            $aJsFiles    = (array) $this->oConfig->files->js->filename; // The JS files of this template

            if (!empty($aCssFiles) || !empty($aJsFiles)) {
                // It will create the asset directory, and publish the css and js files
                $questionTemplatePath = 'question.' . $this->oQuestion->qid . '.template.path';
                $package              = 'question-template_' . $this->oQuestion->qid;

                Yii::setPathOfAlias($questionTemplatePath, $this->sTemplateQuestionPath . '/assets'); // The package creation/publication need an alias
                Yii::app()->clientScript->addPackage($package, array(
                    'basePath'    => $questionTemplatePath,
                    'css'         => $aCssFiles,
                    'js'          => $aJsFiles,
                    'position'    => LSYii_ClientScript::POS_BEGIN
                ));

                if (!YII_DEBUG || Yii::app()->getConfig('use_asset_manager')) {
                    Yii::app()->clientScript->registerPackage($package);
                } else {
                    $templateurl = $this->getTemplateUrl();
                    foreach ($aCssFiles as $sCssFile) {
                        Yii::app()->getClientScript()->registerCssFile("{$templateurl}$sCssFile");
                    }
                    foreach ($aJsFiles as $sJsFile) {
                        Yii::app()->getClientScript()->registerScriptFile("{$templateurl}$sJsFile", LSYii_ClientScript::POS_BEGIN);
                    }
                }
            }
        }
    }

    /**
     * @return string
     */
    public function getTemplateUrl()
    {
        if (!isset($this->sTemplateUrl)) {
            $sBaseUrl               = Yii::app()->getBaseUrl(true);
            $sFolderName            = self::getFolderName($this->oQuestion->type);
            $sTemplateFolderName    = $this->getQuestionTemplateFolderName();
            $sCoreQTemplateRootDir  = Yii::app()->getConfig("corequestionthemerootdir");
            $sUserQTemplateRootDir  = Yii::app()->getConfig("userquestionthemerootdir");

            $sCoreQTemplateDir = Yii::app()->getConfig("corequestionthemedir");

            // Core templates come first
            if (is_dir("$sCoreQTemplateRootDir/$sTemplateFolderName/")) {
                $this->sTemplateUrl = "$sBaseUrl/$sCoreQTemplateDir/$sTemplateFolderName/survey/questions/answer/$sFolderName/assets/";
            } elseif (is_dir("$sUserQTemplateRootDir/$sTemplateFolderName/")) {
                $this->sTemplateUrl = "$sBaseUrl/$sUserQTemplateRootDir/$sTemplateFolderName/survey/questions/answer/$sFolderName/assets/";
            }
        }
        return $this->sTemplateUrl;
    }

    /**
     * Return the custom attributes defined by the question template.
     * @return array|null Array of attribute name => value pairs, or null if no custom attributes
     */
    public function getCustomAttributes()
    {
        if ($this->bHasCustomAttributes) {
            return $this->aCustomAttributes;
        }
        return null;
    }

    /**
     * Called from admin, to generate the template list for a given question type
     * @param string $type
     * @return array
     * @todo Move to QuestionTheme?
     * @todo This is not the same as QuestionTheme::findQuestionMetaDataForAllTypes() which is the database layer
     * @todo this should check the filestructure instead of the database as this is the filestructure layer
     */
    public static function getQuestionTemplateList($type)
    {
        // todo: incorrect, this should check the filestructure instead of the database as this is the filestructure layer
        /** @var QuestionTheme[] */
        $questionThemes = QuestionTheme::model()->findAllByAttributes(
            [],
            'question_type = :question_type',
            ['question_type' => $type]
        );
        $aQuestionTemplates = [];

        foreach ($questionThemes as $questionTheme) {
            if ($questionTheme->core_theme == true && empty($questionTheme->extends)) {
                $aQuestionTemplates['core'] = [
                    'title' => gT('Default'),
                    'preview' => $questionTheme->image_path
                ];
            } else {
                $aQuestionTemplates[$questionTheme->name] = [
                    'title' => $questionTheme->title,
                    'preview' => $questionTheme->image_path
                ];
            }
        }
        return $aQuestionTemplates;
    }

    /**
     * @param string $type
     * @return string|null
     * @deprecated use QuestionTheme::getQuestionXMLPathForBaseType
     */
    public static function getFolderName($type)
    {
        if ($type) {
            $aTypeToFolder  = self::getTypeToFolder();
            $sFolderName    = $aTypeToFolder[$type];
            return $sFolderName;
        }
        return null;
    }

    /**
     * Correspondence between question type and the view folder name
     * Rem: should be in question model. We keep it here for easy access
     * @return array
     * @deprecated
     */
    public static function getTypeToFolder()
    {
        return array(
            "1" => 'arrays/dualscale',
            "5" => '5pointchoice',
            "A" => 'arrays/5point',
            "B" => 'arrays/10point',
            "C" => 'arrays/yesnouncertain',
            "D" => 'date',
            "E" => 'arrays/increasesamedecrease',
            "F" => 'arrays/array',
            "G" => 'gender',
            "H" => 'arrays/column',
            "I" => 'language',
            "K" => 'multiplenumeric',
            "L" => 'listradio',
            "M" => 'multiplechoice',
            "N" => 'numerical',
            "O" => 'list_with_comment',
            "P" => 'multiplechoice_with_comments',
            "Q" => 'multipleshorttext',
            "R" => 'ranking',
            "S" => 'shortfreetext',
            "T" => 'longfreetext',
            "U" => 'hugefreetext',
            "X" => 'boilerplate',
            "Y" => 'yesno',
            "!" => 'list_dropdown',
            ":" => 'arrays/multiflexi',
            ";" => 'arrays/texts',
            "|" => 'file_upload',
            "*" => 'equation',
        );
    }
}
