<?php

namespace Drupal\Core\Asset;

use Drupal\Component\Datetime\TimeInterface;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\File\FileUrlGeneratorInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Theme\ThemeManagerInterface;
use Symfony\Component\HttpFoundation\RequestStack;

/**
 * Optimizes CSS assets.
 */
class CssCollectionOptimizerLazy implements AssetCollectionGroupOptimizerInterface {

  use AssetGroupSetHashTrait;

  /**
   * Constructs a CssCollectionOptimizerLazy.
   *
   * @param \Drupal\Core\Asset\AssetCollectionGrouperInterface $grouper
   *   The grouper for CSS assets.
   * @param \Drupal\Core\Asset\AssetOptimizerInterface $optimizer
   *   The asset optimizer.
   * @param \Drupal\Core\Theme\ThemeManagerInterface $themeManager
   *   The theme manager.
   * @param \Drupal\Core\Asset\LibraryDependencyResolverInterface $dependencyResolver
   *   The library dependency resolver.
   * @param \Symfony\Component\HttpFoundation\RequestStack $requestStack
   *   The request stack.
   * @param \Drupal\Core\File\FileSystemInterface $fileSystem
   *   The file system service.
   * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
   *   The config factory.
   * @param \Drupal\Core\File\FileUrlGeneratorInterface $fileUrlGenerator
   *   The file URL generator.
   * @param \Drupal\Component\Datetime\TimeInterface $time
   *   The time service.
   * @param \Drupal\Core\Language\LanguageManagerInterface $languageManager
   *   The language manager.
   */
  public function __construct(
    protected readonly AssetCollectionGrouperInterface $grouper,
    protected readonly AssetOptimizerInterface $optimizer,
    protected readonly ThemeManagerInterface $themeManager,
    protected readonly LibraryDependencyResolverInterface $dependencyResolver,
    protected readonly RequestStack $requestStack,
    protected readonly FileSystemInterface $fileSystem,
    protected readonly ConfigFactoryInterface $configFactory,
    protected readonly FileUrlGeneratorInterface $fileUrlGenerator,
    protected readonly TimeInterface $time,
    protected readonly LanguageManagerInterface $languageManager,
  ) {}

  /**
   * {@inheritdoc}
   */
  public function optimize(array $css_assets, array $libraries) {
    // File names are generated based on library/asset definitions. This
    // includes a hash of the assets and the group index. Additionally, the full
    // set of libraries, already loaded libraries and theme are sent as query
    // parameters to allow a PHP controller to generate a valid file with
    // sufficient information. Files are not generated by this method since
    // they're assumed to be successfully returned from the URL created whether
    // on disk or not.

    // Group the assets.
    $css_groups = $this->grouper->group($css_assets);

    $css_assets = [];
    foreach ($css_groups as $order => $css_group) {
      // We have to return a single asset, not a group of assets. It is now up
      // to one of the pieces of code in the switch statement below to set the
      // 'data' property to the appropriate value.
      $css_assets[$order] = $css_group;

      if ($css_group['type'] === 'file') {
        // No preprocessing, single CSS asset: just use the existing URI.
        if (!$css_group['preprocess']) {
          $uri = $css_group['items'][0]['data'];
          $css_assets[$order]['data'] = $uri;
        }
        else {
          // To reproduce the full context of assets outside of the request,
          // we must know the entire set of libraries used to generate all CSS
          // groups, whether or not files in a group are from a particular
          // library or not.
          $css_assets[$order]['preprocessed'] = TRUE;
        }
      }
      if ($css_group['type'] === 'external') {
        // We don't do any aggregation and hence also no caching for external
        // CSS assets.
        $uri = $css_group['items'][0]['data'];
        $css_assets[$order]['data'] = $uri;
      }
    }

    // All asset group URLs will have exactly the same query arguments, except
    // for the delta, so prepare them in advance.
    $query_args = [
      'language' => $this->languageManager->getCurrentLanguage()->getId(),
      'theme' => $this->themeManager->getActiveTheme()->getName(),
      'include' => UrlHelper::compressQueryParameter(implode(',', $this->dependencyResolver->getMinimalRepresentativeSubset($libraries))),
    ];
    $ajax_page_state = $this->requestStack->getCurrentRequest()->get('ajax_page_state');
    $already_loaded = isset($ajax_page_state) ? explode(',', $ajax_page_state['libraries']) : [];
    if ($already_loaded) {
      $query_args['exclude'] = UrlHelper::compressQueryParameter(implode(',', $this->dependencyResolver->getMinimalRepresentativeSubset($already_loaded)));
    }

    // Generate a URL for each group of assets, but do not process them inline,
    // this is done using optimizeGroup() when the asset path is requested.
    foreach ($css_assets as $order => $css_asset) {
      if (!empty($css_asset['preprocessed'])) {
        $query = ['delta' => "$order"] + $query_args;
        $filename = 'css_' . $this->generateHash($css_asset) . '.css';
        $uri = 'assets://css/' . $filename;
        $css_assets[$order]['data'] = $this->fileUrlGenerator->generateString($uri) . '?' . UrlHelper::buildQuery($query);
      }
      unset($css_assets[$order]['items']);
    }

    return $css_assets;
  }

  /**
   * {@inheritdoc}
   */
  public function deleteAll() {
    $this->fileSystem->deleteRecursive('assets://css');
  }

  /**
   * {@inheritdoc}
   */
  public function optimizeGroup(array $group): string {
    // Optimize each asset within the group.
    $data = '';
    $current_license = FALSE;
    foreach ($group['items'] as $css_asset) {
      // Ensure license information is available as a comment after
      // optimization.
      if ($css_asset['license'] !== $current_license) {
        $data .= "/* @license " . $css_asset['license']['name'] . " " . $css_asset['license']['url'] . " */\n";
      }
      $current_license = $css_asset['license'];

      // Only external minified files can skip optimization.
      // Local files (even if minified) must be processed to ensure resource
      // paths are rewritten relative to the cached aggregated file location.
      $is_minified = isset($css_asset['minified']) && $css_asset['minified'];
      $is_external = isset($css_asset['type']) && $css_asset['type'] === 'external';
      if ($is_minified && $is_external) {
        $data .= file_get_contents($css_asset['data']);
      }
      else {
        $data .= $this->optimizer->optimize($css_asset);
      }
    }
    // Per the W3C specification at
    // https://www.w3.org/TR/REC-CSS2/cascade.html#at-import, @import rules must
    // precede any other style, so we move those to the top. The regular
    // expression is expressed in NOWDOC since it is detecting backslashes as
    // well as single and double quotes. It is difficult to read when
    // represented as a quoted string.
    $regexp = <<<'REGEXP'
/@import\s*(?:'(?:\\'|.)*'|"(?:\\"|.)*"|url\(\s*(?:\\[\)\'\"]|[^'")])*\s*\)|url\(\s*'(?:\'|.)*'\s*\)|url\(\s*"(?:\"|.)*"\s*\)).*;/iU
REGEXP;
    preg_match_all($regexp, $data, $matches);
    $data = preg_replace($regexp, '', $data);
    return implode('', $matches[0]) . (!empty($matches[0]) ? "\n" : '') . $data;
  }

}
