// 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.psi.formatter.xml;

import com.intellij.formatting.*;
import com.intellij.lang.ASTNode;
import com.intellij.lang.xml.XMLLanguage;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiWhiteSpace;
import com.intellij.psi.TokenType;
import com.intellij.psi.formatter.common.AbstractBlock;
import com.intellij.psi.impl.source.SourceTreeToPsiMap;
import com.intellij.psi.templateLanguages.OuterLanguageElement;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.xml.*;
import com.intellij.util.SmartList;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
import java.util.List;

public class XmlBlock extends AbstractXmlBlock {
  private final Indent myIndent;
  private final TextRange myTextRange;

  private static final Logger LOG = Logger.getInstance(XmlBlock.class);

  public XmlBlock(final ASTNode node,
                    final Wrap wrap,
                    final Alignment alignment,
                    final XmlFormattingPolicy policy,
                    final Indent indent,
                    final TextRange textRange) {
    this(node, wrap, alignment, policy, indent, textRange, false);
  }

  public XmlBlock(final ASTNode node,
                  final Wrap wrap,
                  final Alignment alignment,
                  final XmlFormattingPolicy policy,
                  final Indent indent,
                  final TextRange textRange,
                  final boolean preserveSpace) {
    super(node, wrap, alignment, policy, preserveSpace);
    myIndent = indent;
    myTextRange = textRange;
  }

  @Override
  public @NotNull TextRange getTextRange() {
    if (myTextRange != null && !(isCDATAStart() || isCDATAEnd())) {
      return myTextRange;
    }
    else {
      return super.getTextRange();
    }
  }

  @Override
  protected List<Block> buildChildren() {

    //
    // Fix for EA-19269:
    // Split XML attribute value to the value itself and delimiters (needed for the case when it contains
    // template language tags inside).
    //
    if (myNode.getElementType() == XmlElementType.XML_ATTRIBUTE_VALUE) {
      return splitAttribute(myNode, myXmlFormattingPolicy);
    }

    if (myNode.getElementType() == XmlElementType.XML_COMMENT) {
      List<Block> result = new SmartList<>();
      if (buildInjectedPsiBlocks(result, myNode, myWrap, null, Indent.getNoneIndent())) {
        return result;
      }
      return splitComment();
    }

    if (myNode.getFirstChildNode() != null) {
      boolean keepWhitespaces = shouldKeepWhitespaces();
      final ArrayList<Block> result = new ArrayList<>(5);
      ASTNode child = myNode.getFirstChildNode();
      while (child != null) {
        if (child.getTextLength() > 0) {
          if (containsWhiteSpacesOnly(child)) {
            if (keepWhitespaces) {
              result.add(new ReadOnlyBlock(child));
            }
          }
          else {
            child = processChild(result, child, getDefaultWrap(child), null, getChildDefaultIndent());
          }
        }
        if (child != null) {
          LOG.assertTrue(child.getTreeParent() == myNode);
          child = child.getTreeNext();
        }
      }
      return result;
    }
    else {
      return AbstractBlock.EMPTY;
    }
  }

  private boolean shouldKeepWhitespaces() {
    if (isTextNode(myNode.getElementType())) {
      if (myXmlFormattingPolicy.getShouldKeepWhiteSpaces()) {
        return true;
      }
      else {
        final ASTNode treeParent = myNode.getTreeParent();
        final XmlTag tag = getTag(treeParent);
        if (tag != null) {
          if (myXmlFormattingPolicy.keepWhiteSpacesInsideTag(tag)) {
            return true;
          }
        }
      }
    }
    return false;
  }


  protected List<Block> splitAttribute(ASTNode node, XmlFormattingPolicy formattingPolicy) {
    final ArrayList<Block> result = new ArrayList<>(3);
    ASTNode child = node.getFirstChildNode();
    while (child != null) {
      if (child.getElementType() == XmlTokenType.XML_ATTRIBUTE_VALUE_START_DELIMITER ||
          child.getElementType() == XmlTokenType.XML_ATTRIBUTE_VALUE_END_DELIMITER) {
        result.add(new XmlBlock(child, null, null, formattingPolicy, null, null, isPreserveSpace()));
      }
      else if (!child.getPsi().getLanguage().isKindOf(XMLLanguage.INSTANCE) && containsOuterLanguageElement(child)) {
        // Fix for EA-20311:
        // In case of another embedded language create a splittable XML block which can be
        // merged with other language's code blocks.
        createLeafBlocks(child, result);
      }
      else if (child.getElementType() != TokenType.ERROR_ELEMENT || child.getFirstChildNode() != null) {
        result.add(new ReadOnlyBlock(child));
      }
      child = child.getTreeNext();
    }
    return result;
  }

  private void createLeafBlocks(ASTNode node, List<Block> result) {
    if (node instanceof OuterLanguageElement) {
      processChild(result, node, null, null, null);
      return;
    }

    ASTNode child = node.getFirstChildNode();
    if (child == null && !(node instanceof PsiWhiteSpace) && node.getElementType() != TokenType.ERROR_ELEMENT && node.getTextLength() > 0) {
      result.add(new ReadOnlyBlock(node));
      return;
    }
    while (child != null) {
      createLeafBlocks(child, result);
      child = child.getTreeNext();
    }
  }


  private static boolean containsOuterLanguageElement(ASTNode node) {
    if (node instanceof OuterLanguageElement) {
      return true;
    }
    ASTNode child = node.getFirstChildNode();
    while (child != null) {
      if (child instanceof OuterLanguageElement) {
        return true;
      }
      if (containsOuterLanguageElement(child)) return true;
      child = child.getTreeNext();
    }
    return false;
  }


  protected List<Block> splitComment() {
    if (myNode.getElementType() != XmlElementType.XML_COMMENT) return AbstractBlock.EMPTY;
    final ArrayList<Block> result = new ArrayList<>(3);
    ASTNode child = myNode.getFirstChildNode();
    boolean hasOuterLangElements = false;
    while (child != null) {
      if (child instanceof OuterLanguageElement) {
        hasOuterLangElements = true;
      }
      result.add(new XmlBlock(child, null, null, myXmlFormattingPolicy, getChildIndent(), null, isPreserveSpace()));
      child = child.getTreeNext();
    }
    if (hasOuterLangElements) {
      return result;
    }
    else {
      return AbstractBlock.EMPTY;
    }
  }

  protected @Nullable Wrap getDefaultWrap(ASTNode node) {
    return null;
  }

  protected @Nullable Indent getChildDefaultIndent() {
    if (myNode.getElementType() == XmlElementType.HTML_DOCUMENT) {
      return Indent.getNoneIndent();
    }
    if (myNode.getElementType() == TokenType.DUMMY_HOLDER) {
      return Indent.getNoneIndent();
    }
    if (myNode.getElementType() == XmlElementType.XML_PROLOG) {
      return Indent.getNoneIndent();
    }
    else {
      return null;
    }
  }

  @Override
  public Spacing getSpacing(Block child1, @NotNull Block child2) {
    if (!(child1 instanceof AbstractBlock) || !(child2 instanceof AbstractBlock)) {
      return null;
    }

    final IElementType elementType = myNode.getElementType();
    final ASTNode node1 = ((AbstractBlock)child1).getNode();
    final IElementType type1 = node1.getElementType();
    final ASTNode node2 = ((AbstractBlock)child2).getNode();
    final IElementType type2 = node2.getElementType();

    if ((isXmlTag(node2) || type2 == XmlTokenType.XML_END_TAG_START || isTextNode(type2)) && myXmlFormattingPolicy
      .getShouldKeepWhiteSpaces()) {
      return Spacing.getReadOnlySpacing();
    }

    if (isTextNode(elementType)) {
      return getSpacesInsideText(type1, type2);

    }
    else if (isAttributeElementType(elementType)) {
      return getSpacesInsideAttribute(type1, type2);
    }

    if (type1 == XmlElementType.XML_PROLOG) {
      return createDefaultSpace(true, false);
    }

    if (elementType == XmlElementType.XML_DOCTYPE) {
      return createDefaultSpace(true, false);
    }

    if (type1 == XmlElementType.XML_COMMENT) {
      return createDefaultSpace(true, false);
    }

    return createDefaultSpace(false, false);
  }

  protected Spacing getSpacesInsideAttribute(final IElementType type1, final IElementType type2) {
    if (type1 == XmlTokenType.XML_EQ || type2 == XmlTokenType.XML_EQ) {
      int spaces = myXmlFormattingPolicy.getShouldAddSpaceAroundEqualityInAttribute() ? 1 : 0;
      return Spacing
        .createSpacing(spaces, spaces, 0, myXmlFormattingPolicy.getShouldKeepLineBreaks(), myXmlFormattingPolicy.getKeepBlankLines());
    }
    else {
      return createDefaultSpace(false, false);
    }
  }

  private Spacing getSpacesInsideText(final IElementType type1, final IElementType type2) {
    if (type1 == XmlTokenType.XML_DATA_CHARACTERS && type2 == XmlTokenType.XML_DATA_CHARACTERS) {
      return Spacing
        .createSpacing(1, 1, 0, myXmlFormattingPolicy.getShouldKeepLineBreaksInText(), myXmlFormattingPolicy.getKeepBlankLines());
    }
    else {
      return createDefaultSpace(false, true);
    }
  }

  @Override
  public Indent getIndent() {
    if (myNode.getElementType() == XmlElementType.XML_PROLOG || myNode.getElementType() == XmlElementType.XML_DOCTYPE ||
        SourceTreeToPsiMap.treeElementToPsi(myNode) instanceof XmlDocument) {
      return Indent.getNoneIndent();
    }
    return myIndent;
  }

  @Override
  public boolean insertLineBreakBeforeTag() {
    return false;
  }

  @Override
  public boolean removeLineBreakBeforeTag() {
    return false;
  }

  @Override
  public boolean isTextElement() {
    return isTextNode(myNode.getElementType()) || myNode.getElementType() == XmlTokenType.XML_DATA_CHARACTERS ||
           myNode.getElementType() == XmlTokenType.XML_CHAR_ENTITY_REF || myNode.getElementType() == XmlElementType.XML_ENTITY_REF;
  }

  @Override
  public @NotNull ChildAttributes getChildAttributes(final int newChildIndex) {
    PsiElement element = myNode.getPsi();
    if (element instanceof PsiFile || element instanceof XmlDocument || element instanceof XmlProlog) {
      return new ChildAttributes(Indent.getNoneIndent(), null);
    }
    else {
      return super.getChildAttributes(newChildIndex);
    }
  }

  public XmlFormattingPolicy getPolicy() {
    return myXmlFormattingPolicy;
  }
}
