/*
 * Copyright 2000-2015 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.jetbrains.python.documentation.docstrings;

import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.jetbrains.python.psi.PyIndentUtil;
import com.jetbrains.python.toolbox.Substring;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

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

/**
 * @author Mikhail Golubev
 */
public abstract class DocStringUpdater<T extends DocStringLineParser> {
  protected final T myOriginalDocString;
  private final StringBuilder myBuilder;
  private final List<Modification> myUpdates = new ArrayList<>();
  protected final String myMinContentIndent;

  public DocStringUpdater(@NotNull T docString, @NotNull String minContentIndent) {
    myBuilder = new StringBuilder(docString.getDocStringContent().getSuperString());
    myOriginalDocString = docString;
    myMinContentIndent = minContentIndent;
  }

  protected final void replace(@NotNull TextRange range, @NotNull String text) {
    myUpdates.add(new Modification(range, text));
  }

  protected final void replace(int startOffset, int endOffset, @NotNull String text) {
    replace(new TextRange(startOffset, endOffset), text);
  }

  protected final void insert(int offset, @NotNull String text) {
    replace(offset, offset, text);
  }

  protected final void insertAfterLine(int lineNumber, @NotNull String text) {
    final Substring line = myOriginalDocString.getLines().get(lineNumber);
    insert(line.getEndOffset(), '\n' + text);
  }

  protected final void remove(int startOffset, int endOffset) {
    replace(startOffset, endOffset, "");
  }

  /**
   * @param startLine inclusive
   * @param endLine exclusive
   */
  protected final void removeLines(int startLine, int endLine) {
    final List<Substring> lines = myOriginalDocString.getLines();
    final int startOffset = lines.get(startLine).getStartOffset();
    final int endOffset = endLine < lines.size() ? lines.get(endLine).getStartOffset() : lines.get(endLine - 1).getEndOffset();
    remove(startOffset, endOffset);
  }

  protected final void removeLinesAndSpacesAfter(int startLine, int endLine) {
    removeLines(startLine, skipEmptyLines(endLine));
  }

  private int skipEmptyLines(int startLine) {
    return Math.min(myOriginalDocString.consumeEmptyLines(startLine), myOriginalDocString.getLineCount() - 1);
  }

  protected final void removeLine(int line) {
    removeLines(line, line + 1);
  }

  protected final void insertBeforeLine(int lineNumber, @NotNull String text) {
    final Substring line = myOriginalDocString.getLines().get(lineNumber);
    insert(line.getStartOffset(), text + '\n');
  }

  public final @NotNull String getDocStringText() {
    beforeApplyingModifications();
    // Move closing quotes to the next line, if new lines are going to be inserted
    if (myOriginalDocString.getLineCount() == 1 && !myUpdates.isEmpty()) {
      insertAfterLine(0, myMinContentIndent);
    }
    // If several updates insert in one place (e.g. new field), insert them in backward order,
    // so the first added is placed above
    Collections.reverse(myUpdates);
    myUpdates.sort(Collections.reverseOrder());
    for (final Modification update : myUpdates) {
      final TextRange updateRange = update.range;
      if (updateRange.getStartOffset() == updateRange.getEndOffset()) {
        myBuilder.insert(updateRange.getStartOffset(), update.text);
      }
      else {
        myBuilder.replace(updateRange.getStartOffset(), updateRange.getEndOffset(), update.text);
      }
    }
    return myBuilder.toString();
  }

  protected void beforeApplyingModifications() {

  }

  public @NotNull T getOriginalDocString() {
    return myOriginalDocString;
  }

  protected @NotNull String getLineIndent(int lineNum) {
    final String lastLineIndent = myOriginalDocString.getLineIndent(lineNum);
    if (PyIndentUtil.getLineIndentSize(lastLineIndent) < PyIndentUtil.getLineIndentSize(myMinContentIndent)) {
      return myMinContentIndent;
    }
    return lastLineIndent;
  }

  protected int getLineIndentSize(int lineNum) {
    return PyIndentUtil.getLineIndentSize(getLineIndent(lineNum));
  }

  protected int findLastNonEmptyLine() {
    for (int i = myOriginalDocString.getLineCount() - 1; i >= 0; i--) {
      if (!StringUtil.isEmptyOrSpaces(myOriginalDocString.getLine(i))) {
        return i;
      }
    }
    return 0;
  }

  public abstract void addParameter(@NotNull String name, @Nullable String type);

  public abstract void addReturnValue(@Nullable String type);

  public abstract void removeParameter(@NotNull String name);

  private static class Modification implements Comparable<Modification> {
    final @NotNull TextRange range;
    final @NotNull String text;

    Modification(@NotNull TextRange range, @NotNull String newText) {
      this.range = range;
      this.text = newText;
    }

    @Override
    public int compareTo(Modification o) {
      return range.getStartOffset() - o.range.getStartOffset();
    }
  }
}
