/*
 * Copyright 2000-2009 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.psi.formatter;

import com.intellij.formatting.*;
import com.intellij.lang.ASTNode;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiFile;
import com.intellij.psi.codeStyle.CodeStyleSettings;
import com.intellij.psi.codeStyle.CommonCodeStyleSettings;
import com.intellij.psi.impl.source.codeStyle.CodeEditUtil;
import com.intellij.util.text.CharArrayUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/**
 * @author lesya
 */
public class DocumentBasedFormattingModel implements FormattingModelEx {
  private final Block                   myRootBlock;
  private final FormattingDocumentModel myDocumentModel;
  @Nullable private final FormattingModel myOriginalFormattingModel;
  @NotNull private final Document       myDocument;
  private final Project                 myProject;
  private final CodeStyleSettings       mySettings;
  private final FileType                myFileType;
  private final PsiFile                 myFile;

  @Deprecated
  public DocumentBasedFormattingModel(final Block rootBlock,
                                      @NotNull final Document document,
                                      final Project project,
                                      final CodeStyleSettings settings,
                                      final FileType fileType,
                                      final PsiFile file) {
    myRootBlock = rootBlock;
    myDocument = document;
    myProject = project;
    mySettings = settings;
    myFileType = fileType;
    myFile = file;
    myDocumentModel = new FormattingDocumentModelImpl(document,file);
    myOriginalFormattingModel = null;
  }

  public DocumentBasedFormattingModel(final Block rootBlock,
                                      final Project project,
                                      final CodeStyleSettings settings,
                                      final FileType fileType,
                                      final PsiFile file) {
    myRootBlock = rootBlock;
    myProject = project;
    mySettings = settings;
    myFileType = fileType;
    myFile = file;
    myDocumentModel = FormattingDocumentModelImpl.createOn(file);
    myDocument = myDocumentModel.getDocument();
    myOriginalFormattingModel = null;
  }

  public DocumentBasedFormattingModel(@NotNull final FormattingModel originalModel,
                                      @NotNull final Document document,
                                      final Project project,
                                      final CodeStyleSettings settings,
                                      final FileType fileType,
                                      final PsiFile file) {
    myOriginalFormattingModel = originalModel;
    myRootBlock = originalModel.getRootBlock();
    myDocument = document;
    myProject = project;
    mySettings = settings;
    myFileType = fileType;
    myFile = file;
    myDocumentModel = new FormattingDocumentModelImpl(document,file);
  }

  @Override
  @NotNull
  public Block getRootBlock() {
    return myRootBlock;
  }

  @Override
  @NotNull
  public FormattingDocumentModel getDocumentModel() {
    return myDocumentModel;
  }


  @Override
  public TextRange replaceWhiteSpace(TextRange textRange, String whiteSpace) {
    return replaceWhiteSpace(textRange, null, whiteSpace);
  }

  @Override
  public TextRange replaceWhiteSpace(TextRange textRange, ASTNode nodeAfter, String whiteSpace) {
    if (myOriginalFormattingModel instanceof FormattingModelWithShiftIndentInsideDocumentRange) {
      whiteSpace = ((FormattingModelWithShiftIndentInsideDocumentRange)myOriginalFormattingModel).adjustWhiteSpaceInsideDocument(
        nodeAfter,
        whiteSpace);
    }

    boolean removesStartMarker;
    String marker;

    // When processing injection in cdata / comment we need not remove start / end markers that present as whitespace during check in
    // com.intellij.formatting.WhiteSpace and during building formatter model = blocks in e.g. com.intellij.psi.formatter.xml.XmlTagBlock
    if ((removesStartMarker = removesPattern(textRange, whiteSpace, marker = "<![CDATA[") ||
        removesPattern(textRange, whiteSpace, marker ="<!--[")) ||
        removesPattern(textRange, whiteSpace, marker = "]]>") ||
        removesPattern(textRange, whiteSpace, marker = "]-->")
      ) {
      String newWs = null;

      if (removesStartMarker) {    // TODO once we reformat comments we will need to handle their markers as well
        int at = CharArrayUtil.indexOf(myDocument.getCharsSequence(), marker, textRange.getStartOffset(), textRange.getEndOffset() + 1);
        String ws = myDocument.getCharsSequence().subSequence(textRange.getStartOffset(), textRange.getEndOffset()).toString();
        newWs = mergeWsWithCdataMarker(whiteSpace, ws, at - textRange.getStartOffset());

        if (removesPattern(textRange, newWs != null ? newWs: whiteSpace, marker = "]]>")) {
          int i;
          if (newWs != null && (i = newWs.lastIndexOf('\n')) > 0) {
            int cdataStart = newWs.indexOf("<![CDATA[");
            int i2 = newWs.lastIndexOf('\n', cdataStart);
            String cdataIndent = i2 != -1 ? newWs.substring(i2 + 1, cdataStart):"";
            newWs = newWs.substring(0, i) + cdataIndent + marker + newWs.substring(i);
          }
        }
      }

      if (newWs == null) return textRange;
      whiteSpace = newWs;
    }

    CharSequence whiteSpaceToUse = getDocumentModel().adjustWhiteSpaceIfNecessary(
      whiteSpace, textRange.getStartOffset(), textRange.getEndOffset(), nodeAfter, false
    );

    myDocument.replaceString(textRange.getStartOffset(),
                           textRange.getEndOffset(),
                           whiteSpaceToUse);

    return new TextRange(textRange.getStartOffset(), textRange.getStartOffset() + whiteSpaceToUse.length());
  }

  private boolean removesPattern(final TextRange textRange, final String whiteSpace, final String pattern) {
    return CharArrayUtil.indexOf(myDocument.getCharsSequence(), pattern, textRange.getStartOffset(), textRange.getEndOffset() + 1) >= 0 &&
        CharArrayUtil.indexOf(whiteSpace, pattern, 0) < 0;
  }

  @Override
  public TextRange shiftIndentInsideRange(ASTNode node, TextRange range, int indent) {
    if (myOriginalFormattingModel instanceof FormattingModelWithShiftIndentInsideDocumentRange) {
      final TextRange newRange =
        ((FormattingModelWithShiftIndentInsideDocumentRange)myOriginalFormattingModel).shiftIndentInsideDocumentRange(myDocument, node, range, indent);
      if (newRange != null)
        return newRange;
    }

    final int newLength = shiftIndentInside(range, indent);
    return new TextRange(range.getStartOffset(), range.getStartOffset() + newLength);
  }

  @Override
  public void commitChanges() {
    CodeEditUtil.allowToMarkNodesForPostponedFormatting(false);
    try {
      PsiDocumentManager.getInstance(myProject).commitDocument(myDocument);
    }
    finally {
      CodeEditUtil.allowToMarkNodesForPostponedFormatting(true);
    }
  }

  private int shiftIndentInside(final TextRange elementRange, final int shift) {
    final StringBuilder buffer = new StringBuilder();
    StringBuilder afterWhiteSpace = new StringBuilder();
    int whiteSpaceLength = 0;
    boolean insideWhiteSpace = true;
    int line = 0;
    for (int i = elementRange.getStartOffset(); i < elementRange.getEndOffset(); i++) {
      final char c = myDocument.getCharsSequence().charAt(i);
      switch (c) {
        case '\n':
          if (line > 0) {
            createWhiteSpace(whiteSpaceLength + shift, buffer);
          }
          buffer.append(afterWhiteSpace.toString());
          insideWhiteSpace = true;
          whiteSpaceLength = 0;
          afterWhiteSpace = new StringBuilder();
          buffer.append(c);
          line++;
          break;
        case ' ':
          if (insideWhiteSpace) {
            whiteSpaceLength += 1;
          }
          else {
            afterWhiteSpace.append(c);
          }
          break;
        case '\t':
          if (insideWhiteSpace) {
            whiteSpaceLength += getIndentOptions().TAB_SIZE;
          }
          else {
            afterWhiteSpace.append(c);
          }

          break;
        default:
          insideWhiteSpace = false;
          afterWhiteSpace.append(c);
      }
    }
    if (line > 0 && afterWhiteSpace.length() > 0) {
      createWhiteSpace(whiteSpaceLength + shift, buffer);
      buffer.append(afterWhiteSpace.toString());
    }
    myDocument.replaceString(elementRange.getStartOffset(), elementRange.getEndOffset(), buffer.toString());
    return buffer.length();
  }

  private void createWhiteSpace(final int whiteSpaceLength, StringBuilder buffer) {
    if (whiteSpaceLength < 0) return;
    final CommonCodeStyleSettings.IndentOptions indentOptions = getIndentOptions();
    if (indentOptions.USE_TAB_CHARACTER) {
      int tabs = whiteSpaceLength / indentOptions.TAB_SIZE;
      int spaces = whiteSpaceLength - tabs * indentOptions.TAB_SIZE;
      StringUtil.repeatSymbol(buffer, '\t', tabs);
      StringUtil.repeatSymbol(buffer, ' ', spaces);
    }
    else {
      StringUtil.repeatSymbol(buffer, ' ', whiteSpaceLength);
    }
  }

  private CommonCodeStyleSettings.IndentOptions getIndentOptions() {
    return mySettings.getIndentOptions(myFileType);
  }

  @NotNull
  public Document getDocument() {
    return myDocument;
  }

  public Project getProject() {
    return myProject;
  }

  public PsiFile getFile() {
    return myFile;
  }

  @Nullable
  public static String mergeWsWithCdataMarker(String whiteSpace, final String s, final int cdataPos) {
    final int firstCrInGeneratedWs = whiteSpace.indexOf('\n');
    final int secondCrInGeneratedWs = firstCrInGeneratedWs != -1 ? whiteSpace.indexOf('\n', firstCrInGeneratedWs + 1) : -1;
    final int firstCrInPreviousWs = s.indexOf('\n');
    final int secondCrInPreviousWs = firstCrInPreviousWs != -1 ? s.indexOf('\n', firstCrInPreviousWs + 1) : -1;

    boolean knowHowToModifyCData = false;

    if (secondCrInPreviousWs != -1 && secondCrInGeneratedWs != -1 && cdataPos > firstCrInPreviousWs && cdataPos < secondCrInPreviousWs) {
      whiteSpace = whiteSpace.substring(0, secondCrInGeneratedWs) +
                   s.substring(firstCrInPreviousWs + 1, secondCrInPreviousWs) +
                   whiteSpace.substring(secondCrInGeneratedWs);
      knowHowToModifyCData = true;
    }
    if (!knowHowToModifyCData) whiteSpace = null;
    return whiteSpace;
  }
}
