// Copyright 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.bamboo.soy.format.blocks;

import com.google.bamboo.soy.SoyLanguage;
import com.google.bamboo.soy.elements.ParamElement;
import com.google.bamboo.soy.elements.StatementElement;
import com.google.bamboo.soy.elements.TagBlockElement;
import com.google.bamboo.soy.elements.TagElement;
import com.google.bamboo.soy.elements.WhitespaceUtils;
import com.google.bamboo.soy.format.SoySpacing;
import com.google.bamboo.soy.parser.SoyAtInjectSingle;
import com.google.bamboo.soy.parser.SoyAtParamSingle;
import com.google.bamboo.soy.parser.SoyAtStateSingle;
import com.google.bamboo.soy.parser.SoyChoiceClause;
import com.google.bamboo.soy.parser.SoyNullCheckTernaryColon;
import com.google.bamboo.soy.parser.SoyNullCheckTernaryExpr;
import com.google.bamboo.soy.parser.SoyNullCheckTernaryQmark;
import com.google.bamboo.soy.parser.SoyRecordFieldValue;
import com.google.bamboo.soy.parser.SoyStatementList;
import com.google.bamboo.soy.parser.SoyTypes;
import com.intellij.formatting.Alignment;
import com.intellij.formatting.Block;
import com.intellij.formatting.Indent;
import com.intellij.formatting.Spacing;
import com.intellij.formatting.templateLanguages.BlockWithParent;
import com.intellij.formatting.templateLanguages.DataLanguageBlockWrapper;
import com.intellij.formatting.templateLanguages.TemplateLanguageBlock;
import com.intellij.formatting.templateLanguages.TemplateLanguageBlockFactory;
import com.intellij.lang.ASTNode;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiElement;
import com.intellij.psi.codeStyle.CodeStyleSettings;
import com.intellij.psi.formatter.xml.HtmlPolicy;
import com.intellij.psi.formatter.xml.SyntheticBlock;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.xml.XmlTag;
import java.util.List;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

public class SoyBlock extends TemplateLanguageBlock {

  private HtmlPolicy myHtmlPolicy;

  public SoyBlock(
      @NotNull TemplateLanguageBlockFactory blockFactory,
      @NotNull CodeStyleSettings settings,
      @NotNull ASTNode node,
      @Nullable List<DataLanguageBlockWrapper> foreignChildren,
      HtmlPolicy htmlPolicy) {
    super(blockFactory, settings, node, foreignChildren);
    myHtmlPolicy = htmlPolicy;
  }

  private static boolean isSynthetic(BlockWithParent block) {
    return block instanceof DataLanguageBlockWrapper && ((DataLanguageBlockWrapper) block)
        .getOriginal() instanceof SyntheticBlock;
  }

  private static boolean isXMLTagBlock(BlockWithParent block) {
    return block instanceof DataLanguageBlockWrapper && ((DataLanguageBlockWrapper) block)
        .getNode() instanceof XmlTag;
  }

  private static boolean isAlwaysIndented(PsiElement element) {
    return element instanceof ParamElement
        || element instanceof SoyAtParamSingle
        || element instanceof SoyAtInjectSingle
        || element instanceof SoyAtStateSingle
        || element instanceof SoyChoiceClause;
  }

  private static <T> T findLastDescendantOfType(PsiElement el, Class<T> clazz) {
    while (el != null && !clazz.isInstance(el)) {
      el = el.getLastChild();
    }
    return clazz.cast(el);
  }

  /**
   * Three basic considerations around which this method is built:
   *
   * <p>1. All HTML-content nodes must be siblings (not nephews) to the statements that follow
   * them, otherwise the latter won't form a child [Block]. For example, for a case like this:
   *
   * <pre>
   * <div>
   *   {msg} ... {/msg}
   *   ...
   * </pre>
   *
   * this [PsiElement] subtree is acceptable:
   *
   * <pre>
   * OTHER
   * MsgStatementElement
   * </pre>
   *
   * and this one is not:
   *
   * <pre>
   * Content
   * |- OTHER
   * MsgStatementElement
   * </pre>
   *
   * <p>2. All HTML-content nodes must not be direct children of their parent logical blocks,
   * because the indentation cannot be applied to an HTML-content node itself.
   *
   * <p>From 1. and 2. follows the necessary [PsiElement] tree structure, i.e.,
   *
   * <pre>
   * IfStatement
   * ...
   * |- StatementList
   *    |- OTHER
   *    |- MsgStatementElement
   *    ...
   * </pre>
   *
   * for
   *
   * <pre>
   * {if $condition}
   *   <div>
   *     {msg}
   *       ...
   *     {/msg}
   * </pre>
   *
   * <p>3. HTML-content manages indentation of the child blocks independently. That means that
   * naive implementation, in which, for example, all StatementLists are indented from their
   * parents, can lead to an undesirable effect:
   *
   * <pre>
   * <div>
   *   {msg}
   *   [soy indent][HTML indent]<span> ... <span>
   *   [soy indent]{some other soy statement}
   *   {/msg}
   * </div>
   * </pre>
   *
   * In which HTML indent is automagically added, because from HTML perspective the tree looks
   * like this:
   *
   * <pre>
   * <div>
   * [HTML indent]<span> ... </span>
   * </div>
   * </pre>
   *
   * <p>The conclusion from all 3 premises is as follows: <b>Outside an HTML [Block]
   * StatementLists should be indented (2), so Statements should not; inside an HTML [Block]
   * StatementLists should not be indented (3), so Statements should.</b>
   *
   * <p>The last consideration is a simple optimisation idea:
   *
   * <p>1. StatementLists and Statements always interleave.
   *
   * <p>2. A logical child to an HTML Block can only be a Statement Block.
   *
   * <p>3. It is inefficient to go up from each of them to the root of the tree to discover
   * whether there is an HTML Block somewhere.
   *
   * <p>So we can simply always indent a direct logical child of an HTML Block (which would be a
   * Statement) and for all other Statements/StatementLists indent them <i>iff their closest
   * Statement/StatementList ancestor is not indented</i>. (You can quickly check that it works).
   */
  @Override
  public Indent getIndent() {
    if (myNode.getText().trim().length() == 0) {
      return Indent.getNoneIndent();
    }

    if (isAlwaysIndented(myNode.getPsi())) {
      return Indent.getNormalIndent();
    }

    if (isDirectXMLTagChild()) {
      return null;
    }

    if (hasIndentingForeignBlockParent()) {
      return Indent.getNormalIndent();
    }

    if (isStatementOrStatementContainer() && !isParentStatementOrStatementContainerIndented()) {
      return Indent.getNormalIndent();
    }

    if (isRecordFieldValue() || isCheckDelimiter() || isTernaryBranchExpr()) {
      return Indent.getContinuationIndent();
    }
    if (isDirectTagChild()) {
      return Indent.getContinuationWithoutFirstIndent();
    } else {
      return Indent.getNoneIndent();
    }
  }

  private boolean isCheckDelimiter() {
    if (myNode.getElementType() == SoyTypes.TERNARY_COALESCER) {
      return true;
    }
    PsiElement psiElement = myNode.getPsi();
    return psiElement instanceof SoyNullCheckTernaryQmark
        || psiElement instanceof SoyNullCheckTernaryColon;
  }

  private boolean isTernaryBranchExpr() {
    PsiElement psiElement = myNode.getPsi();
    if (psiElement == null) {
      return false;
    }
    PsiElement parent = psiElement.getParent();
    return parent instanceof SoyNullCheckTernaryExpr
        && psiElement != WhitespaceUtils.getFirstMeaningChild(parent);
  }

  @Override
  public Alignment getAlignment() {
    return null;
  }

  @Override
  protected IElementType getTemplateTextElementType() {
    return SoyTypes.OTHER;
  }

  @Override
  public boolean isRequiredRange(TextRange range) {
    return false;
  }

  @Override
  public Indent getChildIndent() {
    PsiElement element = myNode.getPsi();
    if (element instanceof TagBlockElement) {
      return Indent.getNormalIndent();
    } else if (myNode.getPsi() instanceof TagElement) {
      return Indent.getContinuationWithoutFirstIndent();
    } else {
      return Indent.getNoneIndent();
    }
  }

  @Override
  public Spacing getSpacing(Block child1, Block child2) {
    if (getNode().getElementType() == SoyTypes.LITERAL_STATEMENT) {
      // No custom spacing inside literal statements whatsoever.
      return null;
    }
    Spacing spacing = super.getSpacing(child1, child2);
    return spacing != null ? spacing : SoySpacing.getSpacing(getSettings().getCommonSettings(
        SoyLanguage.INSTANCE), this, child1, child2);
  }

  @Override
  public boolean isIncomplete() {
    TagBlockElement block = findLastDescendantOfType(myNode.getPsi(), TagBlockElement.class);
    if (block != null) {
      return block.isIncomplete();
    } else {
      TagElement tag = findLastDescendantOfType(myNode.getPsi(), TagElement.class);
      return tag != null && tag.isIncomplete();
    }
  }

  private boolean isRecordFieldValue() {
    return myNode.getPsi() instanceof SoyRecordFieldValue;
  }

  private boolean isDirectXMLTagChild() {
    BlockWithParent parent = getParent();
    if (parent == null) {
      return false;
    }
    BlockWithParent grandParent = getParent().getParent();
    return isSynthetic(parent) && isXMLTagBlock(grandParent)
        && ((Block) grandParent).getTextRange().getStartOffset() == ((Block) parent).getTextRange()
        .getStartOffset();
  }

  private boolean hasIndentingForeignBlockParent() {
    BlockWithParent parent = getParent();

    while (parent instanceof DataLanguageBlockWrapper) {
      if (!isSynthetic(parent)) {
        ASTNode foreignNode = ((DataLanguageBlockWrapper) parent).getNode();
        // Returning false if it is an XmlTag that doesn't force indentation.
        return !(foreignNode instanceof XmlTag)
            || myHtmlPolicy.indentChildrenOf((XmlTag) foreignNode);
      }
      parent = parent.getParent();
    }
    return false;
  }

  private boolean isParentStatementOrStatementContainerIndented() {
    BlockWithParent parent = getParent();
    while (parent instanceof SoyBlock || isSynthetic(parent)) {
      if (parent instanceof SoyBlock && ((SoyBlock) parent).isStatementOrStatementContainer()) {
        return ((SoyBlock) parent).getIndent() != Indent.getNoneIndent();
      }
      parent = parent.getParent();
    }
    return false;
  }

  private boolean isStatementOrStatementContainer() {
    return myNode.getPsi() instanceof SoyStatementList
        || myNode.getPsi() instanceof StatementElement;
  }

  private boolean isDirectTagChild() {
    return myNode.getPsi().getParent() instanceof TagElement
        && WhitespaceUtils.getPrevMeaningSibling(myNode.getPsi()) != null;
  }
}