<?php
namespace Concrete\Core\View;

use Concrete\Core\Asset\Asset;
use Concrete\Core\Asset\Output\StandardFormatter;
use Concrete\Core\Feature\Traits\HandleRequiredFeaturesTrait;
use Concrete\Core\Filesystem\FileLocator;
use Concrete\Core\Http\ResponseAssetGroup;
use Concrete\Core\Page\Theme\ThemeRouteCollection;
use Concrete\Core\Page\View\Preview\SkinCustomizerPreviewRequest;
use Concrete\Core\Site\Service;
use Concrete\Core\StyleCustomizer\Skin\SkinInterface;
use Concrete\Core\StyleCustomizer\Skin\Stylesheet\Stylesheet;
use Environment;
use Events;
use Concrete\Core\Support\Facade\Facade;
use HtmlObject\Element;
use PageTheme;
use Page;
use Config;
use Illuminate\Filesystem\Filesystem;

class View extends AbstractView
{
    protected $viewPath;
    protected $innerContentFile;
    protected $themeHandle;
    protected $themeObject;
    protected $themeRelativePath;
    protected $themeAbsolutePath;
    protected $viewPkgHandle;
    protected $themePkgHandle;
    protected $viewRootDirectoryName = DIRNAME_VIEWS;

    use HandleRequiredFeaturesTrait;

    protected function constructView($path = false)
    {
        $path = '/' . trim((string) $path, '/');
        $this->viewPath = $path;
    }

    public function setPackageHandle($pkgHandle)
    {
        $this->viewPkgHandle = $pkgHandle;
    }

    public function getThemeDirectory()
    {
        return $this->themeAbsolutePath;
    }
    public function getViewPath()
    {
        return $this->viewPath;
    }

    /**
     * @return mixed
     */
    public function getThemeObject()
    {
        return $this->themeObject;
    }

    /**
     * gets the relative theme path for use in templates.
     *
     *
     * @return string $themePath
     */
    public function getThemePath()
    {
        return $this->themeRelativePath;
    }
    public function getThemeHandle()
    {
        return $this->themeHandle;
    }

    public function setInnerContentFile($innerContentFile)
    {
        $this->innerContentFile = $innerContentFile;
    }

    /**
     * @return mixed
     */
    public function getInnerContentFile()
    {
        return $this->innerContentFile;
    }

    public function setViewRootDirectoryName($directory)
    {
        $this->viewRootDirectoryName = $directory;
    }

    public function inc($file, $args = [])
    {
        $__data__ = [
            'scopedItems' => $this->getScopeItems(),
        ];
        if ($args && is_array($args)) {
            $__data__['scopedItems'] += $args;
        }
        $env = Environment::get();
        $path = $env->getPath(DIRNAME_THEMES.'/'.$this->themeHandle.'/'.$file, $this->themePkgHandle);
        if (!file_exists($path)) {
            $path2 = $env->getPath(DIRNAME_THEMES.'/'.$this->themeHandle.'/'.$file, $this->viewPkgHandle);
            if (file_exists($path2)) {
                $path = $path2;
            }
            unset($path2);
        }
        $__data__['path'] = $path;
        unset($file);
        unset($args);
        unset($env);
        unset($path);
        if (!empty($__data__['scopedItems'])) {
            if (array_key_exists('__data__', $__data__['scopedItems'])) {
                throw new \Exception(t(/*i18n: %1$s is a variable name, %2$s is a function name*/'Illegal variable name \'%1$s\' in %2$s args.', '__data__', __CLASS__.'::'.__METHOD__));
            }
            extract($__data__['scopedItems']);
        }
        include $__data__['path'];
    }

    /**
     * A shortcut to posting back to the current page with a task and optional parameters. Only works in the context of.
     *
     * @param string $action
     * @param string $task
     *
     * @return string $url
     */
    public function action($action)
    {
        $a = func_get_args();
        $controllerPath = $this->controller->getControllerActionPath();
        array_unshift($a, $controllerPath);
        $ret = call_user_func_array([$this, 'url'], $a);

        return $ret;
    }

    /**
     * Fix https://github.com/concretecms/concretecms/issues/11283
     *
     * @return bool
     */
    public function isEditingDisabled(): bool
    {
        return false;
    }

    public function setViewTheme($theme)
    {
        if (is_object($theme)) {
            $this->themeHandle = $theme->getThemeHandle();
        } else {
            $this->themeHandle = $theme;
        }

        if (isset($this->themeObject)) {
            $this->themeObject = null;
            $this->loadViewThemeObject();
        }
    }

    public function getViewTemplateFile()
    {
        $app = Facade::getFacadeApplication();
        $collection = $app->make(ThemeRouteCollection::class);
        $tmpTheme = $collection->getThemeByRoute($this->getViewPath());
        if ($tmpTheme) {
            return $tmpTheme[1];
        }

        if (isset($this->template)) {
            return $this->template;
        }

        return FILENAME_THEMES_VIEW;
    }

    /**
     * Load all the theme-related variables for which theme to use for this request. May update the themeHandle
     * property on the view based on themeByRoute settings.
     */
    protected function loadViewThemeObject()
    {
        $env = Environment::get();
        $app = Facade::getFacadeApplication();
        // Note: Making this ALWAYS override the $controller->setTheme() was making this really inflexible.
        // We need to be able to set site themes from dashboard pages for complex board rendering. So I'm only going
        // to go to the theme route collection if the theme isn't set explicitly in the controller.
        if (!$this->getThemeHandle()) {
            $tmpTheme = $app->make(ThemeRouteCollection::class)
                ->getThemeByRoute($this->getViewPath());
            if (isset($tmpTheme[0])) {
                $this->themeHandle = $tmpTheme[0];
            }
        }

        if ($this->themeHandle) {
            switch ($this->themeHandle) {
                case VIEW_CORE_THEME:
                    $this->themeObject = new \Concrete\Theme\Concrete\PageTheme();
                    $this->themePkgHandle = false;
                    break;
                case 'dashboard':
                    $this->themeObject = new \Concrete\Theme\Dashboard\PageTheme();
                    $this->themePkgHandle = false;
                    break;
                default:
                    if (!isset($this->themeObject)) {
                        $this->themeObject = PageTheme::getByHandle($this->themeHandle);
                        $this->themePkgHandle = $this->themeObject->getPackageHandle();
                    }

            }
            $this->themeAbsolutePath = $env->getPath(DIRNAME_THEMES.'/'.$this->themeHandle, $this->themePkgHandle);
            $this->themeRelativePath = $env->getURL(DIRNAME_THEMES.'/'.$this->themeHandle, $this->themePkgHandle);

            if ($this->themeObject) {
                $this->handleRequiredFeatures($this->controller, $this->themeObject);
            }

        }
    }

    /**
     * Begin the render.
     */
    public function start($state)
    {
    }

    public function setupRender()
    {
        // Set the theme object that we should use for this requested page.
        // Only run setup if the theme is unset. Usually it will be but if we set it
        // programmatically we already have a theme.
        $this->loadViewThemeObject();
        $env = Environment::get();
        if (!$this->innerContentFile) {
            $innerContentFilePath = null;
            // This modification allows us to have things like views/oauth/authorize.php within our packages/whatever_theme
            // And override the core with it. This should _already_ work because of themeroutecollection but it wasn't
            // being checked properly.
            if (!isset($this->viewPkgHandle) && !empty($this->themePkgHandle)) {
                $this->viewPkgHandle = $this->themePkgHandle;
            }
            $themeRec = $env->getRecord($this->viewRootDirectoryName.'/'.trim($this->viewPath, '/').'.php', $this->viewPkgHandle);
            if ($themeRec->exists()) {
                $innerContentFilePath = $themeRec->file;
            } else {
                $innerContentFilePath = $env->getPath($this->viewRootDirectoryName.'/'.trim($this->viewPath, '/').'.php');
            }
            $this->setInnerContentFile($innerContentFilePath);
        }
        if ($this->themeHandle) {
            if (is_object($this->controller)) {
                $this->controller->setViewObject($this);
                $templateFile = $this->controller->getThemeViewTemplate();
            } else {
                $templateFile = $this->getViewTemplateFile();
            }
            $this->setViewTemplate($env->getPath(DIRNAME_THEMES.'/'.$this->themeHandle.'/'.$templateFile, $this->themePkgHandle));
        }

    }

    public function startRender()
    {
        $event = new \Symfony\Component\EventDispatcher\GenericEvent();
        $event->setArgument('view', $this);
        Events::dispatch('on_start', $event);
        parent::startRender();
    }

    protected function onBeforeGetContents()
    {
        $this->themeObject->registerAssets();
        $event = new \Symfony\Component\EventDispatcher\GenericEvent();
        $event->setArgument('view', $this);
        Events::dispatch('on_before_render', $event);
    }

    public function renderViewContents($scopeItems)
    {
        $contents = '';

        // Render the main view file
        if ($this->innerContentFile) {
            $contents = $this->renderInnerContents($scopeItems);
        }

        // Render the template around it
        if ($this->template != '' && file_exists($this->template)) {
            $contents = $this->renderTemplate($scopeItems, $contents);
        }

        return $contents;
    }

    /**
     * Render the file set to $this->innerContentFile
     * @param $scopeItems
     * @return string
     */
    protected function renderInnerContents($scopeItems)
    {
        // Extract the items into the current scope
        extract($scopeItems);

        ob_start();
        include $this->innerContentFile;
        $innerContent = ob_get_contents();
        ob_end_clean();

        return $innerContent;
    }

    /**
     * Render the file set to $this->template
     * @param $scopeItems
     * @return string
     */
    protected function renderTemplate($scopeItems, $innerContent)
    {
        // Extract the items into the current scope
        extract($scopeItems);

        ob_start();

        // Fire a `before` event
        $this->onBeforeGetContents();
        include $this->template;

        // Fire an `after` event
        $this->onAfterGetContents();
        $contents = ob_get_contents();
        ob_end_clean();

        return $contents;
    }

    public function finishRender($contents)
    {
        $event = new \Symfony\Component\EventDispatcher\GenericEvent();
        $event->setArgument('view', $this);
        Events::dispatch('on_render_complete', $event);

        return $contents;
    }

    /**
     * Function responsible for outputting header items.
     */
    public function markHeaderAssetPosition()
    {
        echo '<!--ccm:assets:'.Asset::ASSET_POSITION_HEADER.'//-->';
    }

    /**
     * Function responsible for outputting footer items.
     */
    public function markFooterAssetPosition()
    {
        echo '<!--ccm:assets:'.Asset::ASSET_POSITION_FOOTER.'//-->';
    }

    protected function getAssetsToOutput()
    {
        $responseGroup = ResponseAssetGroup::get();
        $assets = $responseGroup->getAssetsToOutput();

        return $assets;
    }

    public function postProcessViewContents($contents)
    {
        $assets = $this->getAssetsToOutput();

        $contents = $this->replaceAssetPlaceholders($assets, $contents);

        // replace any empty placeholders
        $contents = $this->replaceEmptyAssetPlaceholders($contents);

        return $contents;
    }

    protected function postProcessAssets($assets)
    {
        $c = Page::getCurrentPage();
        if (!Config::get('concrete.cache.assets')) {
            return $assets;
        }

        if (!count($assets)) {
            return [];
        }

        // goes through all assets in this list, creating new URLs and post-processing them where possible.
        $segment = 0;
        $groupedAssets = [];
        for ($i = 0; $i < count($assets); ++$i) {
            $asset = $assets[$i];
            $nextasset = isset($assets[$i + 1]) ? $assets[$i + 1] : null;

            $groupedAssets[$segment][] = $asset;
            if (!($asset instanceof Asset) || !($nextasset instanceof Asset)) {
                ++$segment;
                continue;
            }

            if ($asset->getOutputAssetType() != $nextasset->getOutputAssetType()) {
                ++$segment;
                continue;
            }

            if (!$asset->assetSupportsCombination() || !$nextasset->assetSupportsCombination()) {
                ++$segment;
                continue;
            }
        }

        $return = [];
        // now we have a sub assets array with different segments split by whether they can be combined.

        foreach ($groupedAssets as $assets) {
            if (
                ($assets[0] instanceof Asset)
                &&
                (
                    (count($assets) > 1)
                    ||
                    $assets[0]->assetSupportsMinification()
                )
            ) {
                $class = get_class($assets[0]);
                $assets = call_user_func([$class, 'process'], $assets);
            }
            $return = array_merge($return, $assets);
        }

        return $return;
    }

    protected function replaceEmptyAssetPlaceholders($pageContent)
    {
        foreach (['<!--ccm:assets:'.Asset::ASSET_POSITION_HEADER.'//-->', '<!--ccm:assets:'.Asset::ASSET_POSITION_FOOTER.'//-->'] as $comment) {
            $pageContent = str_replace($comment, '', $pageContent);
        }

        return $pageContent;
    }

    protected function replaceAssetPlaceholders($outputAssets, $pageContent)
    {
        $outputItems = [];
        foreach ($outputAssets as $position => $assets) {
            $output = '';
            $transformed = $this->postProcessAssets($assets);
            foreach ($transformed as $item) {
                $itemstring = (string) $item;
                if (!in_array($itemstring, $outputItems)) {
                    $output .= $this->outputAssetIntoView($item);
                    $outputItems[] = $itemstring;
                }
            }
            $pageContent = str_replace('<!--ccm:assets:'.$position.'//-->', $output, $pageContent);
        }

        return $pageContent;
    }

    protected function outputAssetIntoView($item)
    {
        $formatter = new StandardFormatter();
        if ($item instanceof Asset) {
            return $formatter->output($item) . "\n";
        } else {
            return $item . "\n";
        }
    }

    public function getThemeStyles()
    {
        $site = app(Service::class)->getSite();
        $skinIdentifier = SkinInterface::SKIN_DEFAULT;
        if ($site) {
            if ($site->getThemeSkinIdentifier()) {
                $skinIdentifier = $site->getThemeSkinIdentifier();
            }
        }
        $skin = $this->themeObject->getSkinByIdentifier($skinIdentifier);
        $stylesheet = $skin->getStylesheet();
        return $stylesheet->getElement();
    }


    public static function element($_file, $args = null, $_pkgHandle = null)
    {
        if (is_array($args)) {
            $collisions = array_intersect(['_file', '_pkgHandle'], array_keys($args));
            if ($collisions) {
                throw new \Exception(t("Illegal variable name '%s' in element args.", implode(', ', $collisions)));
            }
            $collisions = null;
            extract($args);
        }
        $view = self::getRequestInstance();

        $_c = Page::getCurrentPage();
        $_app = Facade::getFacadeApplication();
        if (is_object($_c)) {
            $_theme = $_c->getCollectionThemeObject();
        } else if ($_app->isInstalled()) {
            $_theme = PageTheme::getSiteTheme();
        }

        $_fs = $_app->make(Filesystem::class);
        $_locator = new FileLocator($_fs, $_app);
        if (isset($_theme) && is_object($_theme)) {
            $_locator->addLocation(new FileLocator\ThemeElementLocation($_theme));
        }
        if ($_pkgHandle) {
            $_locator->addPackageLocation($_pkgHandle);
        }

        $_record = $_locator->getRecord(DIRNAME_ELEMENTS . '/' . $_file . '.php');
        $_file = $_record->getFile();

        unset($_record);
        unset($_app);
        unset($_fs);
        unset($_locator);
        unset($_theme);

        include $_file;
    }
}
