/*
 * Copyright 2000-2012 JetBrains s.r.o.
 *
 * 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.intellij.formatting;

import com.intellij.lang.ASTNode;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.codeStyle.CommonCodeStyleSettings;
import javax.annotation.Nullable;

public class LeafBlockWrapper extends AbstractBlockWrapper {
  private static final int CONTAIN_LINE_FEEDS = 4;
  private static final int READ_ONLY = 8;
  private static final int LEAF = 16;

  private final int mySymbolsAtTheLastLine;
  private LeafBlockWrapper myPreviousBlock;
  private LeafBlockWrapper myNextBlock;
  private SpacingImpl mySpaceProperty;

  /**
   * Shortcut for calling
   * {@link #LeafBlockWrapper(Block, CompositeBlockWrapper, WhiteSpace, FormattingDocumentModel, CommonCodeStyleSettings.IndentOptions, LeafBlockWrapper, boolean, TextRange)}
   * with {@link Block#getTextRange() text range associated with the given block}.
   *
   * @param block               block to wrap
   * @param parent              wrapped parent block
   * @param whiteSpaceBefore    white space before the target block to wrap
   * @param model               formatting model to use during current wrapper initialization
   * @param options             code formatting options
   * @param previousTokenBlock  previous token block
   * @param isReadOnly          flag that indicates if target block is read-only
   */
  public LeafBlockWrapper(final Block block,
                          @Nullable CompositeBlockWrapper parent,
                          WhiteSpace whiteSpaceBefore,
                          FormattingDocumentModel model,
                          CommonCodeStyleSettings.IndentOptions options,
                          LeafBlockWrapper previousTokenBlock,
                          boolean isReadOnly)
  {
    this(block, parent, whiteSpaceBefore, model, options, previousTokenBlock, isReadOnly, block.getTextRange());
  }

  public LeafBlockWrapper(final Block block,
                          CompositeBlockWrapper parent,
                          WhiteSpace whiteSpaceBefore,
                          FormattingDocumentModel model,
                          CommonCodeStyleSettings.IndentOptions options,
                          LeafBlockWrapper previousTokenBlock,
                          boolean isReadOnly,
                          final TextRange textRange)
  {
    super(block, whiteSpaceBefore, parent, textRange);
    myPreviousBlock = previousTokenBlock;
    final int lastLineNumber = model.getLineNumber(textRange.getEndOffset());

    int flagsValue = myFlags;
    final boolean containsLineFeeds = model.getLineNumber(textRange.getStartOffset()) != lastLineNumber;
    flagsValue |= containsLineFeeds ? CONTAIN_LINE_FEEDS:0;

    // We need to perform such a complex calculation because block construction algorithm is allowed to create 'leaf' blocks
    // that contain more than one token interleaved white space that contains either tabulations or line breaks.
    // E.g. consider the following code:
    //
    // public
    //  void foo() {}
    //
    // 'public void' here is a single 'leaf' token and it's second part 'void' is preceeded by tabulaton symbol. Hence, we need
    // correctly calculate number of symbols occupied by the current token at last line.
    int start = containsLineFeeds ? model.getLineStartOffset(lastLineNumber) : textRange.getStartOffset();
    int symbols = 0;
    CharSequence text = model.getDocument().getCharsSequence();
    for (int i = start; i < textRange.getEndOffset(); i++) {
      if (text.charAt(i) == '\t') {
        symbols += options.TAB_SIZE;
      } else {
        symbols++;
      }
    }
    mySymbolsAtTheLastLine = symbols;
    flagsValue |= isReadOnly ? READ_ONLY:0;
    final boolean isLeaf = block.isLeaf();
    flagsValue |= isLeaf ? LEAF : 0;

    myFlags = flagsValue;
  }

  public final boolean containsLineFeeds() {
    return (myFlags & CONTAIN_LINE_FEEDS) != 0;
  }

  public int getSymbolsAtTheLastLine() {
    return mySymbolsAtTheLastLine;
  }

  @Override
  public LeafBlockWrapper getPreviousBlock() {
    return myPreviousBlock;
  }

  public LeafBlockWrapper getNextBlock() {
    return myNextBlock;
  }

  public void setNextBlock(final LeafBlockWrapper nextBlock) {
    myNextBlock = nextBlock;
  }

  @Override
  protected boolean indentAlreadyUsedBefore(final AbstractBlockWrapper child) {
    return false;
  }

  @Override
  public IndentData getNumberOfSymbolsBeforeBlock() {
    int spaces = getWhiteSpace().getSpaces();
    int indentSpaces = getWhiteSpace().getIndentSpaces();

    if (getWhiteSpace().containsLineFeeds()) {
      return new IndentData(indentSpaces, spaces);
    }

    for (LeafBlockWrapper current = this.getPreviousBlock(); current != null; current = current.getPreviousBlock()) {
      spaces += current.getWhiteSpace().getSpaces();
      spaces += current.getSymbolsAtTheLastLine();
      indentSpaces += current.getWhiteSpace().getIndentSpaces();
      if (current.getWhiteSpace().containsLineFeeds()) {
        break;
      }
    }
    return new IndentData(indentSpaces, spaces);
  }

  @Override
  public void dispose() {
    super.dispose();
    myPreviousBlock = null;
    myNextBlock = null;
    mySpaceProperty = null;
  }

  /**
   * @return    spacing between current block and its left sibling
   */
  public SpacingImpl getSpaceProperty() {
    return mySpaceProperty;
  }

  public IndentData calculateOffset(final CommonCodeStyleSettings.IndentOptions options) {
    // Calculate result as an indent of current block from parent plus parent block indent.
    if (myIndentFromParent != null) {
      final AbstractBlockWrapper firstIndentedParent = findFirstIndentedParent();
      final IndentData indentData = new IndentData(myIndentFromParent.getIndentSpaces(), myIndentFromParent.getSpaces());
      if (firstIndentedParent == null) {
        return indentData;
      } else {
        final WhiteSpace whiteSpace = firstIndentedParent.getWhiteSpace();
        return new IndentData(whiteSpace.getIndentOffset(), whiteSpace.getSpaces()).add(indentData);
      }
    }

    // Consider that current block is not indented if it doesn't have a parent block.
    if (myParent == null) return new IndentData(0);

    // Define that current block and all its parents that start at the same offset can't use first child indent as block indent.
    if (getIndent().isAbsolute()) {
      setCanUseFirstChildIndentAsBlockIndent(false);
      AbstractBlockWrapper current = this;
      while (current != null && current.getStartOffset() == getStartOffset()) {
        current.setCanUseFirstChildIndentAsBlockIndent(false);
        current = current.myParent;
      }
    }

    return myParent.getChildOffset(this, options, this.getStartOffset());
  }

  public void setSpaceProperty(@Nullable final SpacingImpl currentSpaceProperty) {
    mySpaceProperty = currentSpaceProperty;
  }

  @Nullable
  public IndentInfo calcIndentFromParent() {
    AbstractBlockWrapper firstIndentedParent = findFirstIndentedParent();
    final WhiteSpace mySpace = getWhiteSpace();
    if (firstIndentedParent != null) {
      final WhiteSpace parentSpace = firstIndentedParent.getWhiteSpace();
      return new IndentInfo(0,
                            mySpace.getIndentOffset() - parentSpace.getIndentOffset(),
                            mySpace.getSpaces() - parentSpace.getSpaces());
    } else {
      return null;
    }
  }

  public final boolean isLeaf() {
    return (myFlags & LEAF) != 0;
  }

  public boolean contains(final int offset) {
    return myStart < offset && myEnd > offset;
  }

  public TextRange getTextRange() {
    return new TextRange(myStart, myEnd);
  }

  public boolean isEndOfCodeBlock() {
    ASTNode node = getNode();
    return node != null && node.getTextLength() == 1 && node.getChars().charAt(0) == '}';
  }
}