// 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.client.autoindenter;

import com.google.collide.client.documentparser.DocumentParser;
import com.google.collide.client.editor.Editor;
import com.google.collide.client.editor.EditorDocumentMutator;
import com.google.collide.codemirror2.SyntaxType;
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.ListenerRegistrar.Remover;
import com.google.collide.shared.util.StringUtils;
import com.google.collide.shared.util.TextUtils;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.regexp.shared.RegExp;

/**
 * A class responsible for automatically adding indentation when appropriate.
 */
public class Autoindenter {

  private static final RegExp WHITESPACES = RegExp.compile("^\\s*$");

  private interface IndentationStrategy {

    Runnable handleTextChange(TextChange textChange, EditorDocumentMutator editorDocumentMutator);
  }

  private static class PreviousLineMatchingIndentationStrategy implements IndentationStrategy {
    @Override
    public Runnable handleTextChange(
        TextChange textChange, final EditorDocumentMutator editorDocumentMutator) {
      String text = textChange.getText();
      if (!"\n".equals(text)) {
        return null;
      }

      final int toInsert = TextUtils.countWhitespacesAtTheBeginningOfLine(
          textChange.getLine().getText());

      if (toInsert == 0) {
        return null;
      }

      final Line line = LineUtils.getLine(textChange.getLine(), 1);
      final int lineNumber = textChange.getLineNumber() + 1;
      return new Runnable() {
        @Override
        public void run() {
          String addend = StringUtils.getSpaces(toInsert);
          editorDocumentMutator.insertText(line, lineNumber, 0, addend);
        }
      };
    }
  }

  private static class CodeMirrorIndentationStrategy implements IndentationStrategy {
    private final DocumentParser documentParser;

    CodeMirrorIndentationStrategy(DocumentParser parser) {
      documentParser = parser;
    }

    @Override
    public Runnable handleTextChange(
        TextChange textChange, final EditorDocumentMutator editorDocumentMutator) {
      String text = textChange.getText();

      if (!"\n".equals(text)) {
        // TODO: We should incrementally apply autoindention to
        //               multiline pastes.
        // TODO: Take electric characters into account:
        //               documentParser.getElectricCharacters.
        return null;
      }

      // TODO: Ask parser to reparse changed line.
      final Line line = LineUtils.getLine(textChange.getLine(), 1);

      // Special case: pressing ENTER in the middle of whitespaces line should
      // not fix indentation (use case: press ENTER on empty line).
      Line prevLine = textChange.getLine();
      if (WHITESPACES.test(prevLine.getText()) && WHITESPACES.test(line.getText())) {
        return null;
      }

      final int lineNumber = textChange.getLineNumber() + 1;
      final int indentation = documentParser.getIndentation(line);
      if (indentation < 0) {
        return null;
      }

      final int oldIndentation = TextUtils.countWhitespacesAtTheBeginningOfLine(line.getText());
      if (indentation == oldIndentation) {
        return null;
      }

      return new Runnable() {
        @Override
        public void run() {
          if (indentation < oldIndentation) {
            editorDocumentMutator.deleteText(line, lineNumber, 0, oldIndentation - indentation);
          } else {
            String addend = StringUtils.getSpaces(indentation - oldIndentation);
            editorDocumentMutator.insertText(line, lineNumber, 0, addend);
          }
        }
      };
    }
  }

  /**
   * Creates an instance of {@link Autoindenter} that is configured to take on
   * the appropriate indentation strategy depending on the document parser.
   */
  public static Autoindenter create(DocumentParser documentParser, Editor editor) {
    if (documentParser.getSyntaxType() != SyntaxType.NONE && documentParser.hasSmartIndent()) {
      return new Autoindenter(new CodeMirrorIndentationStrategy(documentParser), editor);
    }
    return new Autoindenter(new PreviousLineMatchingIndentationStrategy(), editor);
  }

  private final Editor editor;
  private final IndentationStrategy indentationStrategy;
  private boolean isMutatingDocument;
  private final Editor.TextListener textListener = new Editor.TextListener() {
    @Override
    public void onTextChange(TextChange textChange) {
      handleTextChange(textChange);
    }
  };
  private final Remover textListenerRemover;

  private Autoindenter(IndentationStrategy indentationStrategy, Editor editor) {
    this.indentationStrategy = indentationStrategy;
    this.editor = editor;

    textListenerRemover = editor.getTextListenerRegistrar().add(textListener);
  }

  public void teardown() {
    textListenerRemover.remove();
  }

  private void handleTextChange(TextChange textChange) {
    if (isMutatingDocument || editor.isMutatingDocumentFromUndoOrRedo()
        || textChange.getType() != TextChange.Type.INSERT) {
      return;
    }

    final Runnable mutator = indentationStrategy.handleTextChange(
        textChange, editor.getEditorDocumentMutator());

    if (mutator == null) {
      return;
    }

    // We shouldn't be touching the document in this callback, so defer.
    Scheduler.get().scheduleFinally(new ScheduledCommand() {
      @Override
      public void execute() {
        isMutatingDocument = true;
        try {
          mutator.run();
        } finally {
          isMutatingDocument = false;
        }
      }
    });
  }
}
