// Copyright 2012 Google Inc. All Rights Reserved.
//
// 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.collide.shared.ot;

import static com.google.collide.dto.DocOpComponent.Type.DELETE;
import static com.google.collide.dto.DocOpComponent.Type.INSERT;
import static com.google.collide.dto.DocOpComponent.Type.RETAIN;
import static com.google.collide.dto.DocOpComponent.Type.RETAIN_LINE;

import java.util.List;

import com.google.collide.dto.DocOp;
import com.google.collide.dto.DocOpComponent;
import com.google.collide.dto.DocOpComponent.Delete;
import com.google.collide.dto.DocOpComponent.Insert;
import com.google.collide.dto.DocOpComponent.Retain;
import com.google.collide.dto.DocOpComponent.RetainLine;
import com.google.collide.dto.shared.DocOpFactory;
import com.google.collide.json.shared.JsonArray;
import com.google.collide.shared.document.Line;
import com.google.collide.shared.document.TextChange;
import com.google.collide.shared.document.util.LineUtils;
import com.google.collide.shared.util.StringUtils;
import com.google.common.base.Preconditions;

/**
 * Utility methods for document operation manipulation.
 *
 */
public final class DocOpUtils {

  public static void accept(DocOp docOp, DocOpCursor visitor) {
    JsonArray<DocOpComponent> components = docOp.getComponents();

    for (int i = 0, n = components.size(); i < n; i++) {
      acceptComponent(components.get(i), visitor);
    }
  }

  public static void acceptComponent(DocOpComponent component, DocOpCursor visitor) {
    switch (component.getType()) {
      case DELETE:
        visitor.delete(((Delete) component).getText());
        break;

      case INSERT:
        visitor.insert(((Insert) component).getText());
        break;

      case RETAIN:
        Retain retain = (Retain) component;
        visitor.retain(retain.getCount(), retain.hasTrailingNewline());
        break;

      case RETAIN_LINE:
        visitor.retainLine(((RetainLine) component).getLineCount());
        break;

      default:
        throw new IllegalArgumentException(
            "Unknown doc op component with ordinal " + component.getType());
    }
  }

  public static DocOp createFromTextChange(DocOpFactory factory, TextChange textChange) {

    DocOp docOp = factory.createDocOp();
    JsonArray<DocOpComponent> components = docOp.getComponents();

    int lineNumber = textChange.getLineNumber();
    if (lineNumber > 0) {
      components.add(factory.createRetainLine(lineNumber));
    }

    int column = textChange.getColumn();
    if (column > 0) {
      components.add(factory.createRetain(column, false));
    }

    String text = textChange.getText();

    /*
     * Split the potentially multiline text into a component per line
     */
    JsonArray<String> lineTexts = StringUtils.split(text, "\n");

    // Create components for all but the last line
    int nMinusOne = lineTexts.size() - 1;
    for (int i = 0; i < nMinusOne; i++) {
      components.add(createComponentFromTextChange(factory, textChange, lineTexts.get(i) + "\n"));
    }

    String lastLineText = lineTexts.get(nMinusOne);
    if (!lastLineText.isEmpty()) {
      // Create a component for the last line
      components.add(createComponentFromTextChange(factory, textChange, lastLineText));
    }

    // Create a retain, if required
    int remainingRetainCount;
    int numNewlines = lineTexts.size() - 1;
    if (textChange.getType() == TextChange.Type.INSERT) {
      Line lastModifiedLine = LineUtils.getLine(textChange.getLine(), numNewlines);

      remainingRetainCount = lastModifiedLine.getText().length() - lastLineText.length();

      if (numNewlines == 0) {
        remainingRetainCount -= column;
      }
    } else { // DELETE
      remainingRetainCount = textChange.getLine().getText().length() - column;
    }

    // Create a retain line, if required
    int docLineCount = textChange.getLine().getDocument().getLineCount();
    int numNewlinesFromTextChangeInCurDoc =
        textChange.getType() == TextChange.Type.DELETE ? 0 : numNewlines;
    int remainingLineCount = docLineCount - (lineNumber + numNewlinesFromTextChangeInCurDoc + 1);

    // Add the retain and retain line components
    if (remainingRetainCount > 0) {
      // This retain has a trailing new line if it is NOT on the last line
      components.add(factory.createRetain(remainingRetainCount, remainingLineCount > 0));
    }

    if (remainingLineCount > 0) {
      components.add(factory.createRetainLine(remainingLineCount));
    } else {
      Preconditions.checkState(remainingLineCount == 0, "How is it negative?");
      /*
       * If the retainingLineCount calculation resulted in 0, there's still a
       * chance that there is a empty last line that needs to be retained. Our
       * contract says if the resulting document (that is, the document in its
       * state right now) has an empty last line, we should have a RetainLine
       * that accounts for it. In addition if the document contained an empty
       * last line before the delete we should also emit a retain line.
       *
       * Since we didn't emit the RetainLine above (since remainingLineCount ==
       * 0), we can check and emit one here.
       */
      // to check if the document ended in a new line before the change, we check if the change
      // endsWith \n and remainingRetainCount = 0;
      boolean didDocumentEndInEmptyLineBeforeDelete = textChange.getType() == TextChange.Type.DELETE
          && remainingRetainCount == 0 && text.endsWith("\n");
             
      boolean isLastLineEmptyAfterTextChange =
          textChange.getLine().getDocument().getLastLine().getText().length() == 0;
      if (isLastLineEmptyAfterTextChange || didDocumentEndInEmptyLineBeforeDelete) {
        components.add(factory.createRetainLine(1));
      }
    }

    return docOp;
  }

  /**
   * Creates a single doc op composed of docops converted from a collection
   * of text changes. For a single text change use
   * {@link #createFromTextChange(DocOpFactory, TextChange)}.
   *
   * @param factory doc ops factory
   * @param textChanges list of changes to convert to doc op
   * @return composed doc op, or {@code null} if text changes array is empty
   * @throws Composer.ComposeException if error happens during composal
   */
  public static DocOp createFromTextChanges(DocOpFactory factory,
      JsonArray<TextChange> textChanges) throws Composer.ComposeException {
    DocOp result = null;
    for (int i = 0, n = textChanges.size(); i < n; i++) {
      TextChange textChange = textChanges.get(i);
      DocOp curOp = DocOpUtils.createFromTextChange(factory, textChange);
      result = result != null ? Composer.compose(factory, result, curOp) : curOp;
    }
    return result;
  }

  public static boolean containsMutation(Iterable<DocOp> docOps) {
    for (DocOp docOp : docOps) {
      if (containsMutation(docOp)) {
        return true;
      }
    }
    return false;
  }

  public static boolean containsMutation(DocOp docOp) {
    for (int i = 0; i < docOp.getComponents().size(); i++) {
      DocOpComponent component = docOp.getComponents().get(i);
      switch (component.getType()) {
        case DocOpComponent.Type.DELETE:
        case DocOpComponent.Type.INSERT:
          return true;
        case DocOpComponent.Type.RETAIN:
        case DocOpComponent.Type.RETAIN_LINE:
          // Retains do not dirty the contents of a file
          break;
        default:
          throw new IllegalArgumentException("Got an unknown doc op type " + component.getType());
      }
    }
    return false;
  }

  public static String toString(DocOp docOp, boolean verbose) {
    StringBuilder sb = new StringBuilder();
    for (int i = 0, n = docOp.getComponents().size(); i < n; i++) {
      sb.append(toString(docOp.getComponents().get(i), verbose));
    }

    return sb.toString();
  }

  public static String toString(DocOpComponent component, boolean verbose) {
    switch (component.getType()) {
      case DELETE:
        String deleteText = ((Delete) component).getText();
        return "D(" + toStringForComponentText(deleteText, verbose) + ")";

      case INSERT:
        String insertText = ((Insert) component).getText();
        return "I(" + toStringForComponentText(insertText, verbose) + ")";

      case RETAIN:
        Retain retain = (Retain) component;
        return "R(" + (retain.hasTrailingNewline() ? (retain.getCount() - 1) + "\\n" : ""
            + retain.getCount()) + ")";

      case RETAIN_LINE:
        return "RL(" + ((RetainLine) component).getLineCount() + ")";

      default:
        return "?(???)";
    }
  }

  public static String toString(
      List<? extends DocOp> docOps, int firstIndex, int lastIndex, boolean verbose) {
    StringBuilder sb = new StringBuilder("[");
    for (int i = firstIndex; i <= lastIndex; i++) {
      DocOp docOp = docOps.get(i);
      if (docOp == null) {
        sb.append("<null doc op>,");
      } else {
        sb.append(toString(docOp, verbose)).append(',');
      }
    }
    sb.setLength(sb.length() - 1);
    sb.append(']');

    return sb.toString();
  }

  private static DocOpComponent createComponentFromTextChange(
      DocOpFactory factory, TextChange textChange, String text) {
    switch (textChange.getType()) {
      case INSERT:
        return factory.createInsert(text);

      case DELETE:
        return factory.createDelete(text);

      default:
        throw new IllegalArgumentException(
            "Unknown text change type with ordinal " + textChange.getType().ordinal());
    }
  }

  private static String toStringForComponentText(String componentText, boolean verbose) {
    if (verbose) {
      return componentText.endsWith("\n") ? componentText.substring(0, componentText.length() - 1)
          + "\\n" : componentText;
    } else {
      return componentText.endsWith("\n") ? (componentText.length() - 1) + "\\n" : ""
          + componentText.length();
    }
  }
}
