// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInsight.template.emmet.filters;

import com.intellij.application.options.emmet.EmmetOptions;
import com.intellij.codeInsight.template.emmet.XmlEmmetBundle;
import com.intellij.codeInsight.template.emmet.nodes.GenerationNode;
import com.intellij.lang.xml.XMLLanguage;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiElement;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.xml.util.HtmlUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.*;
import java.util.regex.Pattern;

/**
 * Bem filter for emmet support.
 * See the original source code here: <a href="https://github.com/emmetio/emmet/blob/master/javascript/filters/bem.js">bem.js</a>
 * And documentation here: <a href="http://docs.emmet.io/filters/bem/">emmet docs</a>
 */
public class BemEmmetFilter extends ZenCodingFilter {
  private static final String SUFFIX = "bem";
  private static final Key<BemState> BEM_STATE = Key.create("BEM_STATE");
  private static final Pattern BLOCK_NAME_PATTERN = Pattern.compile("^[A-z]-");

  @Override
  public @NotNull String getDisplayName() {
    return XmlEmmetBundle.message("emmet.filter.BEM");
  }

  @Override
  public @NotNull String getSuffix() {
    return SUFFIX;
  }

  @Override
  public boolean isMyContext(@NotNull PsiElement context) {
    return context.getLanguage() instanceof XMLLanguage;
  }

  @Override
  public @NotNull GenerationNode filterNode(final @NotNull GenerationNode node) {
    final Map<String, String> attributes = node.getTemplateToken().getAttributes();
    String classAttributeName = HtmlUtil.CLASS_ATTRIBUTE_NAME;
    String classValue = attributes.get(classAttributeName);
    EmmetOptions emmetOptions = EmmetOptions.getInstance();
    if (classValue != null && emmetOptions != null) {
      String elementSeparator = emmetOptions.getBemElementSeparator();
      String modifierSeparator = emmetOptions.getBemModifierSeparator();
      String shortElementPrefix = emmetOptions.getBemShortElementPrefix();

      List<String> classNames = ContainerUtil.map(HtmlUtil.splitClassNames(classValue), (s) -> normalizeClassName(s, elementSeparator, shortElementPrefix));

      BEM_STATE.set(node, new BemState(suggestBlockName(classNames), null, null));
      Set<String> newClassNames = new LinkedHashSet<>();
      for (String className : classNames) {
        ContainerUtil.addAll(newClassNames, processClassName(className, node, elementSeparator, modifierSeparator));
      }
      attributes.put(classAttributeName, StringUtil.join(newClassNames, " "));
    }
    return node;
  }

  private static Iterable<String> processClassName(@NotNull String className, @NotNull GenerationNode node,
                                                   @NotNull String elementSeparator, @NotNull String modifierSeparator) {
    className = fillWithBemElements(className, node, elementSeparator);
    className = fillWithBemModifiers(className, node, modifierSeparator);

    BemState nodeBemState = BEM_STATE.get(node);
    BemState bemState = extractBemStateFromClassName(className, elementSeparator, modifierSeparator);
    List<String> result = new ArrayList<>();
    if (!bemState.isEmpty()) {
      String nodeBlockValue = nodeBemState != null ? nodeBemState.getBlock() : null;

      String block = bemState.getBlock();
      if (StringUtil.isEmpty(block)) {
        block = StringUtil.notNullize(nodeBlockValue);
        bemState.setBlock(block);
      }
      String prefix = block;
      String element = bemState.getElement();
      if (StringUtil.isNotEmpty(element)) {
        prefix += elementSeparator + element;
      }
      result.add(prefix);
      String modifier = bemState.getModifier();
      if (StringUtil.isNotEmpty(modifier)) {
        result.add(prefix + modifierSeparator + modifier);
      }

      BemState newNodeBemState = bemState.copy();
      if (StringUtil.isNotEmpty(nodeBlockValue) && StringUtil.isEmpty(modifier)) {
        // save old value
        newNodeBemState.setBlock(nodeBlockValue);
      }
      BEM_STATE.set(node, newNodeBemState);
    }
    else {
      result.add(className);
    }
    return result;
  }

  private static @NotNull BemState extractBemStateFromClassName(@NotNull String className, String elementSeparator, String modifierSeparator) {
    final BemState result = new BemState();
    int indexOfElementSeparator = className.indexOf(elementSeparator);
    if (indexOfElementSeparator >= 0) {
      result.setBlock(className.substring(0, indexOfElementSeparator));
      result.setElement(className.substring(indexOfElementSeparator + elementSeparator.length()));

      int lastIndexOfElementSeparator = className.lastIndexOf(elementSeparator);
      assert lastIndexOfElementSeparator >= 0;
      int indexOfModifierSeparator = className.indexOf(modifierSeparator, lastIndexOfElementSeparator + elementSeparator.length());
      if (indexOfModifierSeparator >= 0) {
        result.setModifier(className.substring(indexOfModifierSeparator + modifierSeparator.length()));
        result.setElement(className.substring(indexOfElementSeparator + elementSeparator.length(), indexOfModifierSeparator));
      }
    }
    else {
      int indexOfModifierSeparator = className.indexOf(modifierSeparator);
      if (indexOfModifierSeparator >= 0) {
        result.setBlock(className.substring(0, indexOfModifierSeparator));
        result.setModifier(className.substring(indexOfModifierSeparator + modifierSeparator.length()));
      }
    }
    return result;
  }

  private static @NotNull String fillWithBemElements(@NotNull String className, @NotNull GenerationNode node, @NotNull String separator) {
    return transformClassNameToBemFormat(className, separator, node, false);
  }

  private static @NotNull String fillWithBemModifiers(@NotNull String className, @NotNull GenerationNode node, @NotNull String separator) {
    return transformClassNameToBemFormat(className, separator, node, true);
  }


  /**
   * Adjusting className to BEM format according to tags structure.
   *
   * @param separator           handling separator
   * @param node                current node
   * @param isModifierSeparator whether we're trying to handle modifier
   * @return class name in BEM format
   */
  private static @NotNull String transformClassNameToBemFormat(@NotNull String className, @NotNull String separator,
                                                      @NotNull GenerationNode node, boolean isModifierSeparator) {
    Pair<String, Integer> cleanStringAndDepth = getCleanStringAndDepth(className, separator);
    Integer depth = cleanStringAndDepth.second;
    if (depth > 0) {
      GenerationNode donor = node;
      while (donor.getParent() != null && depth > 0) {
        donor = donor.getParent();
        depth--;
      }

      BemState bemState = BEM_STATE.get(donor);
      if (bemState != null) {
        String prefix = bemState.getBlock();
        if (!StringUtil.isEmpty(prefix)) {
          String element = bemState.getElement();
          if (isModifierSeparator && !StringUtil.isEmpty(element)) {
            prefix = prefix + separator + element;
          }
          return prefix + separator + cleanStringAndDepth.first;
        }
      }
    }
    return className;
  }

  /**
   * Counts separators at the start of className and retrieve className without these separators.
   *
   * @return pair like <name_without_separator_at_the_start, count_of_separators_at_the_start_of_string>
   */
  private static @NotNull Pair<String, Integer> getCleanStringAndDepth(@NotNull String name, @NotNull String separator) {
    int result = 0;
    while (!separator.isEmpty() && name.startsWith(separator)) {
      result++;
      name = name.substring(separator.length());
    }
    return Pair.create(name, result);
  }

  /**
   * Suggest block name by class names.
   * Returns first class started with pattern [a-z]-
   * or first class started with letter.
   *
   * @return suggested block name for given classes. Empty string if name can't be suggested.
   */
  private static @NotNull String suggestBlockName(Iterable<String> classNames) {
    String result = ContainerUtil.find(classNames, className -> BLOCK_NAME_PATTERN.matcher(className).matches());
    if (result == null) {
      result = ContainerUtil.find(classNames, s -> s != null && !s.isEmpty() && Character.isLetter(s.charAt(0)));
    }
    return StringUtil.notNullize(result);
  }

  private static @NotNull String normalizeClassName(@NotNull String className,
                                                    @NotNull String elementSeparator,
                                                    @NotNull String shortElementPrefix) {
    if (shortElementPrefix.isEmpty() || !className.startsWith(shortElementPrefix)) {
      return className;
    }

    StringBuilder result = new StringBuilder();
    while (className.startsWith(shortElementPrefix)) {
      className = className.substring(shortElementPrefix.length());
      result.append(elementSeparator);
    }
    return result.append(className).toString();
  }

  private static final class BemState {
    private @Nullable String block;
    private @Nullable String element;
    private @Nullable String modifier;

    private BemState() {
    }

    private BemState(@Nullable String block, @Nullable String element, @Nullable String modifier) {
      this.block = block;
      this.element = element;
      this.modifier = modifier;
    }

    public void setModifier(@Nullable String modifier) {
      this.modifier = modifier;
    }

    public void setElement(@Nullable String element) {
      this.element = element;
    }

    public void setBlock(@Nullable String block) {
      this.block = block;
    }

    public @Nullable String getBlock() {
      return block;
    }

    public @Nullable String getElement() {
      return element;
    }

    public @Nullable String getModifier() {
      return modifier;
    }

    public boolean isEmpty() {
      return StringUtil.isEmpty(block) && StringUtil.isEmpty(element) && StringUtil.isEmpty(modifier);
    }

    public @NotNull BemState copy() {
      return new BemState(block, element, modifier);
    }
  }
}
