<?php

/**
 * Maintain a set of markup/templates to inject inside various regions
 */
class CRM_Core_Region implements CRM_Core_Resources_CollectionInterface, CRM_Core_Resources_CollectionAdderInterface {

  /**
   * Obtain the content for a given region.
   *
   * @param string $name
   * @param bool $autocreate
   *   Whether to automatically create an empty region.
   * @return CRM_Core_Region
   */
  public static function &instance($name, $autocreate = TRUE) {
    if ($autocreate && !isset(Civi::$statics[__CLASS__][$name])) {
      Civi::$statics[__CLASS__][$name] = new CRM_Core_Region($name);
    }
    return Civi::$statics[__CLASS__][$name];
  }

  use CRM_Core_Resources_CollectionTrait;

  /**
   * Symbolic name of this region
   *
   * @var string
   */
  public $_name;

  /**
   * @param string $name
   */
  public function __construct($name) {
    $this->_name = $name;
    $this->types = ['markup', 'template', 'callback', 'scriptFile', 'scriptUrl', 'script', 'jquery', 'settings', 'style', 'styleFile', 'styleUrl'];
    $this->defaults['region'] = $name;

    // Placeholder which represents any of the default content generated by the main Smarty template
    $this->add([
      'name' => 'default',
      'type' => 'markup',
      'markup' => '',
      'weight' => 0,
    ]);
  }

  /**
   * Render all the snippets in a region.
   *
   * @param string $default
   *   HTML, the initial content of the region.
   * @param bool $allowCmsOverride
   *   Allow CMS to override rendering of region.
   * @return string, HTML
   */
  public function render($default, $allowCmsOverride = TRUE) {
    // $default is just another part of the region
    if (is_array($this->snippets['default'])) {
      $this->snippets['default']['markup'] = $default;
    }

    if (defined('CIVICRM_IFRAME')) {
      $allowCmsOverride = FALSE;
    }
    if (Civi::settings()->get('debug_enabled')) {
      // The point of debug mode is to allow debugging. The CMS override only applies on BD/D7, and it makes debugging harder.
      // https://lab.civicrm.org/dev/core/-/issues/5712
      $allowCmsOverride = FALSE;
    }

    Civi::dispatcher()->dispatch('civi.region.render', \Civi\Core\Event\GenericHookEvent::create(['region' => $this]));

    $this->sort();

    $cms = CRM_Core_Config::singleton()->userSystem;
    $smarty = CRM_Core_Smarty::singleton();
    $html = '';

    $renderSnippet = function($snippet) use (&$html, $smarty, $cms, $allowCmsOverride, &$renderSnippet) {
      switch ($snippet['type']) {
        case 'markup':
          $html .= $snippet['markup'];
          break;

        case 'template':
          $tmp = $smarty->getTemplateVars('snippet');
          $smarty->assign('snippet', $snippet);
          $html .= $smarty->fetch($snippet['template']);
          $smarty->assign('snippet', $tmp);
          break;

        case 'callback':
          $args = $snippet['arguments'] ?? [&$snippet, &$html];
          $html .= call_user_func_array($snippet['callback'], $args);
          break;

        case 'scriptUrl':
          // ECMAScript Modules (ESMs) are basically Javascript files, but they require a slightly different incantation.
          if (!empty($snippet['esm'])) {
            $html .= Civi::service('esm.loader')->renderModule($snippet);
          }
          elseif (!$allowCmsOverride || !$cms->addScriptUrl($snippet['scriptUrl'], $this->_name)) {
            $html .= sprintf("<script type=\"text/javascript\" src=\"%s\">\n</script>\n", $snippet['scriptUrl']);
          }
          break;

        case 'jquery':
          $renderSnippet([
            'type' => 'script',
            'script' => sprintf("CRM.\$(function(\$) {\n%s\n});", $snippet['jquery']),
          ]);
          break;

        case 'scriptFile':
          foreach ($snippet['scriptFileUrls'] as $url) {
            $html .= $renderSnippet(['type' => 'scriptUrl', 'scriptUrl' => $url] + $snippet);
          }
          break;

        case 'script':
          // ECMAScript Modules (ESMs) are basically Javascript files, but they require a slightly different incantation.
          if (!empty($snippet['esm'])) {
            $html .= Civi::service('esm.loader')->renderModule($snippet);
          }
          elseif (!$allowCmsOverride || !$cms->addScript($snippet['script'], $this->_name)) {
            $html .= sprintf("<script type=\"text/javascript\">\n%s\n</script>\n", $snippet['script']);
          }
          break;

        case 'styleFile':
          foreach ($snippet['styleFileUrls'] as $url) {
            $html .= $renderSnippet(['type' => 'styleUrl', 'styleUrl' => $url] + $snippet);
          }
          break;

        case 'styleUrl':
          if (!$allowCmsOverride || !$cms->addStyleUrl($snippet['styleUrl'], $this->_name)) {
            $html .= sprintf("<link href=\"%s\" rel=\"stylesheet\" type=\"text/css\"/>\n", $snippet['styleUrl']);
          }
          break;

        case 'style':
          if (!$allowCmsOverride || !$cms->addStyle($snippet['style'], $this->_name)) {
            $html .= sprintf("<style>\n%s\n</style>\n", $snippet['style']);
          }
          break;

        case 'settings':
          $settingsData = CRM_Utils_JSON::encodeScriptVar($this->getSettings());
          $js = "(function(vars) {
            if (window.CRM) CRM.$.extend(true, CRM, vars); else window.CRM = vars;
            })($settingsData)";
          $html .= sprintf("<script type=\"text/javascript\">\n%s\n</script>\n", $js);
          break;

        default:
          throw new CRM_Core_Exception(ts('Snippet type %1 is unrecognized',
            [1 => $snippet['type']]));
      }
    };

    foreach ($this->snippets as $snippet) {
      if (empty($snippet['disabled'])) {
        $renderSnippet($snippet);
      }
    }
    return $html;
  }

}
