<?php
/**
 * Piwik - Open source web analytics
 *
 * @link http://piwik.org
 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
 *
 * @category Piwik
 * @package Piwik
 */
namespace Piwik;

use Exception;
use Piwik\AssetManager\UIAsset;
use Piwik\AssetManager\UIAsset\InMemoryUIAsset;
use Piwik\AssetManager\UIAsset\OnDiskUIAsset;
use Piwik\AssetManager\UIAssetCacheBuster;
use Piwik\AssetManager\UIAssetFetcher\JScriptUIAssetFetcher;
use Piwik\AssetManager\UIAssetFetcher\StaticUIAssetFetcher;
use Piwik\AssetManager\UIAssetFetcher\StylesheetUIAssetFetcher;
use Piwik\AssetManager\UIAssetFetcher;
use Piwik\AssetManager\UIAssetMerger\JScriptUIAssetMerger;
use Piwik\AssetManager\UIAssetMerger\StylesheetUIAssetMerger;
use Piwik\Plugin\Manager;
use Piwik\Translate;
use Piwik\Config as PiwikConfig;

/**
 * AssetManager is the class used to manage the inclusion of UI assets:
 * JavaScript and CSS files.
 *
 * It performs the following actions:
 *  - Identifies required assets
 *  - Includes assets in the rendered HTML page
 *  - Manages asset merging and minifying
 *  - Manages server-side cache
 *
 * Whether assets are included individually or as merged files is defined by
 * the global option 'disable_merged_assets'. When set to 1, files will be
 * included individually.
 * When set to 0, files will be included within a pair of files: 1 JavaScript
 * and 1 css file.
 *
 * @method static \Piwik\AssetManager getInstance()
 * @package Piwik
 */
class AssetManager extends Singleton
{
    const MERGED_CSS_FILE = "asset_manager_global_css.css";
    const MERGED_CORE_JS_FILE = "asset_manager_core_js.js";
    const MERGED_NON_CORE_JS_FILE = "asset_manager_non_core_js.js";

    const CSS_IMPORT_DIRECTIVE = "<link rel=\"stylesheet\" type=\"text/css\" href=\"%s\" />\n";
    const JS_IMPORT_DIRECTIVE = "<script type=\"text/javascript\" src=\"%s\"></script>\n";
    const GET_CSS_MODULE_ACTION = "index.php?module=Proxy&action=getCss";
    const GET_CORE_JS_MODULE_ACTION = "index.php?module=Proxy&action=getCoreJs";
    const GET_NON_CORE_JS_MODULE_ACTION = "index.php?module=Proxy&action=getNonCoreJs";

    /**
     * @var UIAssetCacheBuster
     */
    private $cacheBuster;

    /**
     * @var UIAssetFetcher
     */
    private $minimalStylesheetFetcher;

    /**
     * @var Theme
     */
    private $theme;

    function __construct()
    {
        $this->cacheBuster = UIAssetCacheBuster::getInstance();
        $this->minimalStylesheetFetcher =  new StaticUIAssetFetcher(array('plugins/Zeitgeist/stylesheets/base.less'), array(), $this->theme);

        if(Manager::getInstance()->getThemeEnabled() != null)
            $this->theme = new Theme();
    }

    /**
     * @param UIAssetCacheBuster $cacheBuster
     */
    public function setCacheBuster($cacheBuster)
    {
        $this->cacheBuster = $cacheBuster;
    }

    /**
     * @param UIAssetFetcher $minimalStylesheetFetcher
     */
    public function setMinimalStylesheetFetcher($minimalStylesheetFetcher)
    {
        $this->minimalStylesheetFetcher = $minimalStylesheetFetcher;
    }

    /**
     * @param Theme $theme
     */
    public function setTheme($theme)
    {
        $this->theme = $theme;
    }

    /**
     * Return CSS file inclusion directive(s) using the markup <link>
     *
     * @return string
     */
    public function getCssInclusionDirective()
    {
        return sprintf(self::CSS_IMPORT_DIRECTIVE, self::GET_CSS_MODULE_ACTION);
    }

    /**
     * Return JS file inclusion directive(s) using the markup <script>
     *
     * @return string
     */
    public function getJsInclusionDirective()
    {
        $result = "<script type=\"text/javascript\">\n" . Translate::getJavascriptTranslations() . "\n</script>";

        if ($this->isMergedAssetsDisabled()) {

            $this->getMergedCoreJSAsset()->delete();
            $this->getMergedNonCoreJSAsset()->delete();

            $result .= $this->getIndividualJsIncludes();

        } else {

            $result .= sprintf(self::JS_IMPORT_DIRECTIVE, self::GET_CORE_JS_MODULE_ACTION);
            $result .= sprintf(self::JS_IMPORT_DIRECTIVE, self::GET_NON_CORE_JS_MODULE_ACTION);
        }

        return $result;
    }

    /**
     * Return the base.less compiled to css
     *
     * @return UIAsset
     */
    public function getCompiledBaseCss()
    {
        $mergedAsset = new InMemoryUIAsset();

        $assetMerger = new StylesheetUIAssetMerger($mergedAsset, $this->minimalStylesheetFetcher, $this->cacheBuster);

        $assetMerger->generateFile();

        return $mergedAsset;
    }

    /**
     * Return the css merged file absolute location.
     * If there is none, the generation process will be triggered.
     *
     * @return UIAsset
     */
    public function getMergedStylesheet()
    {
        $mergedAsset = $this->getMergedStylesheetAsset();

        $assetFetcher = new StylesheetUIAssetFetcher(Manager::getInstance()->getLoadedPluginsName(), $this->theme);

        $assetMerger = new StylesheetUIAssetMerger($mergedAsset, $assetFetcher, $this->cacheBuster);

        $assetMerger->generateFile();

        return $mergedAsset;
    }

    /**
     * Return the core js merged file absolute location.
     * If there is none, the generation process will be triggered.
     *
     * @return UIAsset
     */
    public function getMergedCoreJavaScript()
    {
        return $this->getMergedJavascript($this->getCoreJScriptFetcher(), $this->getMergedCoreJSAsset());
    }

    /**
     * Return the non core js merged file absolute location.
     * If there is none, the generation process will be triggered.
     *
     * @return UIAsset
     */
    public function getMergedNonCoreJavaScript()
    {
        return $this->getMergedJavascript($this->getNonCoreJScriptFetcher(), $this->getMergedNonCoreJSAsset());
    }

    /**
     * @param boolean $core
     * @return string[]
     */
    public function getLoadedPlugins($core)
    {
        $loadedPlugins = array();

        foreach(Manager::getInstance()->getLoadedPluginsName() as $pluginName) {

            $pluginIsCore = Manager::getInstance()->isPluginBundledWithCore($pluginName);

            if(($pluginIsCore && $core) || (!$pluginIsCore && !$core))
                $loadedPlugins[] = $pluginName;
        }

        return $loadedPlugins;
    }

    /**
     * Remove previous merged assets
     */
    public function removeMergedAssets($pluginName = false)
    {
        $assetsToRemove = array($this->getMergedStylesheetAsset());

        if($pluginName) {

            if($this->pluginContainsJScriptAssets($pluginName)) {

                PiwikConfig::getInstance()->init();
                if(Manager::getInstance()->isPluginBundledWithCore($pluginName)) {

                    $assetsToRemove[] = $this->getMergedCoreJSAsset();

                } else {

                    $assetsToRemove[] = $this->getMergedNonCoreJSAsset();
                }
            }

        } else {

            $assetsToRemove[] = $this->getMergedCoreJSAsset();
            $assetsToRemove[] = $this->getMergedNonCoreJSAsset();
        }

        $this->removeAssets($assetsToRemove);
    }

    /**
     * Check if the merged file directory exists and is writable.
     *
     * @return string The directory location
     * @throws Exception if directory is not writable.
     */
    public function getAssetDirectory()
    {
        $mergedFileDirectory = PIWIK_USER_PATH . "/tmp/assets";
        $mergedFileDirectory = SettingsPiwik::rewriteTmpPathWithHostname($mergedFileDirectory);

        if (!is_dir($mergedFileDirectory)) {
            Filesystem::mkdir($mergedFileDirectory);
        }

        if (!is_writable($mergedFileDirectory)) {
            throw new Exception("Directory " . $mergedFileDirectory . " has to be writable.");
        }

        return $mergedFileDirectory;
    }

    /**
     * Return the global option disable_merged_assets
     *
     * @return boolean
     */
    public function isMergedAssetsDisabled()
    {
        return Config::getInstance()->Debug['disable_merged_assets'];
    }

    /**
     * @param UIAssetFetcher $assetFetcher
     * @param UIAsset $mergedAsset
     * @return UIAsset
     */
    private function getMergedJavascript($assetFetcher, $mergedAsset)
    {
        $assetMerger = new JScriptUIAssetMerger($mergedAsset, $assetFetcher, $this->cacheBuster);

        $assetMerger->generateFile();

        return $mergedAsset;
    }

    /**
     * Return individual JS file inclusion directive(s) using the markup <script>
     *
     * @return string
     */
    private function getIndividualJsIncludes()
    {
        return
            $this->getIndividualJsIncludesFromAssetFetcher($this->getCoreJScriptFetcher()) .
            $this->getIndividualJsIncludesFromAssetFetcher($this->getNonCoreJScriptFetcher());
    }

    /**
     * @param UIAssetFetcher $assetFetcher
     * @return string
     */
    private function getIndividualJsIncludesFromAssetFetcher($assetFetcher)
    {
        $jsIncludeString = '';

        foreach ($assetFetcher->getCatalog()->getAssets() as $jsFile) {

            $jsFile->validateFile();
            $jsIncludeString = $jsIncludeString . sprintf(self::JS_IMPORT_DIRECTIVE, $jsFile->getRelativeLocation());
        }

        return $jsIncludeString;
    }

    private function getCoreJScriptFetcher()
    {
        return new JScriptUIAssetFetcher($this->getLoadedPlugins(true), $this->theme);
    }

    private function getNonCoreJScriptFetcher()
    {
        return new JScriptUIAssetFetcher($this->getLoadedPlugins(false), $this->theme);
    }

    /**
     * @param string $pluginName
     * @return boolean
     */
    private function pluginContainsJScriptAssets($pluginName)
    {
        $fetcher = new JScriptUIAssetFetcher(array($pluginName), $this->theme);

        try {
            $assets = $fetcher->getCatalog()->getAssets();
        } catch(\Exception $e) {
            // This can happen when a plugin is not valid (eg. Piwik 1.x format)
            // When posting the event to the plugin, it returns an exception "Plugin has not been loaded"
            return false;
        }

        $plugin = Manager::getInstance()->getLoadedPlugin($pluginName);

        if($plugin->isTheme()) {

            $theme = Manager::getInstance()->getTheme($pluginName);

            $javaScriptFiles = $theme->getJavaScriptFiles();

            if(!empty($javaScriptFiles))
                $assets = array_merge($assets, $javaScriptFiles);
        }

        return !empty($assets);
    }

    /**
     * @param UIAsset[] $uiAssets
     */
    private function removeAssets($uiAssets)
    {
        foreach($uiAssets as $uiAsset) {
            $uiAsset->delete();
        }
    }

    /**
     * @return UIAsset
     */
    private function getMergedStylesheetAsset()
    {
        return $this->getMergedUIAsset(self::MERGED_CSS_FILE);
    }

    /**
     * @return UIAsset
     */
    private function getMergedCoreJSAsset()
    {
        return $this->getMergedUIAsset(self::MERGED_CORE_JS_FILE);
    }

    /**
     * @return UIAsset
     */
    private function getMergedNonCoreJSAsset()
    {
        return $this->getMergedUIAsset(self::MERGED_NON_CORE_JS_FILE);
    }

    /**
     * @param string $fileName
     * @return UIAsset
     */
    private function getMergedUIAsset($fileName)
    {
        return new OnDiskUIAsset($this->getAssetDirectory(), $fileName);
    }
}