<?php

namespace Drupal\language\Plugin\LanguageNegotiation;

use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\PathProcessor\InboundPathProcessorInterface;
use Drupal\Core\PathProcessor\OutboundPathProcessorInterface;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Url;
use Drupal\language\Attribute\LanguageNegotiation;
use Drupal\language\LanguageNegotiationMethodBase;
use Drupal\language\LanguageSwitcherInterface;
use Symfony\Component\HttpFoundation\Request;

/**
 * Class for identifying language via URL prefix or domain.
 */
#[LanguageNegotiation(
  id: LanguageNegotiationUrl::METHOD_ID,
  name: new TranslatableMarkup('URL'),
  types: [LanguageInterface::TYPE_INTERFACE,
    LanguageInterface::TYPE_CONTENT,
    LanguageInterface::TYPE_URL,
  ],
  weight: -8,
  description: new TranslatableMarkup("Language from the URL (Path prefix or domain)."),
  config_route_name: 'language.negotiation_url'
)]
class LanguageNegotiationUrl extends LanguageNegotiationMethodBase implements InboundPathProcessorInterface, OutboundPathProcessorInterface, LanguageSwitcherInterface {

  /**
   * The language negotiation method id.
   */
  const METHOD_ID = 'language-url';

  /**
   * URL language negotiation: use the path prefix as URL language indicator.
   */
  const CONFIG_PATH_PREFIX = 'path_prefix';

  /**
   * URL language negotiation: use the domain as URL language indicator.
   */
  const CONFIG_DOMAIN = 'domain';

  /**
   * {@inheritdoc}
   */
  public function getLangcode(?Request $request = NULL) {
    $langcode = NULL;

    if ($request && $this->languageManager) {
      $languages = $this->languageManager->getLanguages();
      $config = $this->config->get('language.negotiation')->get('url');

      switch ($config['source']) {
        case LanguageNegotiationUrl::CONFIG_PATH_PREFIX:
          $request_path = urldecode(trim($request->getPathInfo(), '/'));
          $path_args = explode('/', $request_path);
          $prefix = array_shift($path_args);

          // Search prefix within added languages.
          $negotiated_language = FALSE;
          foreach ($languages as $language) {
            if (isset($config['prefixes'][$language->getId()]) && $config['prefixes'][$language->getId()] == $prefix) {
              $negotiated_language = $language;
              break;
            }
          }

          if ($negotiated_language) {
            $langcode = $negotiated_language->getId();
          }
          break;

        case LanguageNegotiationUrl::CONFIG_DOMAIN:
          // Get only the host, not the port.
          $http_host = $request->getHost();
          foreach ($languages as $language) {
            // Skip the check if the language doesn't have a domain.
            if (!empty($config['domains'][$language->getId()])) {
              // Ensure that there is exactly one protocol in the URL when
              // checking the hostname.
              $host = 'http://' . str_replace(['http://', 'https://'], '', $config['domains'][$language->getId()]);
              $host = parse_url($host, PHP_URL_HOST);
              if ($http_host == $host) {
                $langcode = $language->getId();
                break;
              }
            }
          }
          break;
      }
    }

    return $langcode;
  }

  /**
   * {@inheritdoc}
   */
  public function processInbound($path, Request $request) {
    $config = $this->config->get('language.negotiation')->get('url');

    if ($config['source'] == LanguageNegotiationUrl::CONFIG_PATH_PREFIX) {
      $parts = explode('/', trim($path, '/'));
      $prefix = array_shift($parts);

      // Search prefix within added languages.
      foreach ($this->languageManager->getLanguages() as $language) {
        if (isset($config['prefixes'][$language->getId()]) && $config['prefixes'][$language->getId()] == $prefix) {
          // Rebuild $path with the language removed.
          $path = '/' . implode('/', $parts);
          break;
        }
      }
    }

    return $path;
  }

  /**
   * {@inheritdoc}
   */
  public function processOutbound($path, &$options = [], ?Request $request = NULL, ?BubbleableMetadata $bubbleable_metadata = NULL) {
    $url_scheme = 'http';
    $port = 80;
    if ($request) {
      $url_scheme = $request->getScheme();
      $port = $request->getPort();
    }
    $languages = array_flip(array_keys($this->languageManager->getLanguages()));
    // Language can be passed as an option, or we go for current URL language.
    if (!isset($options['language']) || ($options['language'] instanceof LanguageInterface && $options['language']->getId() == LanguageInterface::LANGCODE_NOT_SPECIFIED)) {
      $language_url = $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_URL);
      $options['language'] = $language_url;
    }
    // We allow only added languages here.
    elseif (!is_object($options['language']) || !isset($languages[$options['language']->getId()])) {
      return $path;
    }
    $config = $this->config->get('language.negotiation')->get('url');
    if ($config['source'] == LanguageNegotiationUrl::CONFIG_PATH_PREFIX) {
      if (is_object($options['language']) && !empty($config['prefixes'][$options['language']->getId()])) {
        $options['prefix'] = $config['prefixes'][$options['language']->getId()] . '/';
        if ($bubbleable_metadata) {
          $bubbleable_metadata->addCacheContexts(['languages:' . LanguageInterface::TYPE_URL]);
        }
      }
    }
    elseif ($config['source'] == LanguageNegotiationUrl::CONFIG_DOMAIN) {
      if (is_object($options['language']) && !empty($config['domains'][$options['language']->getId()])) {

        // Check if the base URLs match, return early if they do.
        if (isset($options['base_url']) && $request && $request->getHost() === parse_url($options['base_url'], PHP_URL_HOST)) {
          return $path;
        }

        // Save the original base URL. If it contains a port, we need to
        // retain it below.
        if (!empty($options['base_url'])) {
          // The colon in the URL scheme messes up the port checking below.
          $normalized_base_url = str_replace(['https://', 'http://'], '', $options['base_url']);
        }

        // Ask for an absolute URL with our modified base URL only if the domain
        // is different from the current request domain.
        if ($request && $request->getHost() !== $config['domains'][$options['language']->getId()]) {
          $options['absolute'] = TRUE;
          $options['base_url'] = $url_scheme . '://' . $config['domains'][$options['language']->getId()];

          // In case either the original base URL or the HTTP host contains a
          // port, retain it.
          if (isset($normalized_base_url) && str_contains($normalized_base_url, ':')) {
            [, $port] = explode(':', $normalized_base_url);
            $options['base_url'] .= ':' . $port;
          }
          elseif (($url_scheme == 'http' && $port != 80) || ($url_scheme == 'https' && $port != 443)) {
            $options['base_url'] .= ':' . $port;
          }

          if (isset($options['https'])) {
            if ($options['https'] === TRUE) {
              $options['base_url'] = str_replace('http://', 'https://', $options['base_url']);
            }
            elseif ($options['https'] === FALSE) {
              $options['base_url'] = str_replace('https://', 'http://', $options['base_url']);
            }
          }

          // Add Drupal's subfolder from the base_path if there is one.
          $options['base_url'] .= rtrim(base_path(), '/');
          if ($bubbleable_metadata) {
            $bubbleable_metadata->addCacheContexts(['languages:' . LanguageInterface::TYPE_URL, 'url.site']);
          }
        }
      }
    }
    return $path;
  }

  /**
   * {@inheritdoc}
   */
  public function getLanguageSwitchLinks(Request $request, $type, Url $url) {
    $links = [];
    $query = [];
    parse_str($request->getQueryString() ?? '', $query);

    foreach ($this->languageManager->getNativeLanguages() as $language) {
      $links[$language->getId()] = [
        // We need to clone the $url object to avoid using the same one for all
        // links. When the links are rendered, options are set on the $url
        // object, so if we use the same one, they would be set for all links.
        'url' => clone $url,
        'title' => $language->getName(),
        'language' => $language,
        'attributes' => ['class' => ['language-link']],
        'query' => $query,
      ];
    }

    return $links;
  }

}
