/*
 * Copyright 2000-2017 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.openapi.editor;

import com.intellij.openapi.actionSystem.IdeActions;
import com.intellij.openapi.actionSystem.MouseShortcut;
import com.intellij.openapi.editor.ex.EditorEx;
import com.intellij.openapi.editor.ex.EditorSettingsExternalizable;
import com.intellij.openapi.editor.impl.AbstractEditorTest;
import com.intellij.openapi.ide.CopyPasteManager;
import com.intellij.openapi.keymap.Keymap;
import com.intellij.openapi.keymap.KeymapManager;
import com.intellij.testFramework.EditorTestUtil;
import com.intellij.testFramework.fixtures.EditorMouseFixture;
import com.intellij.util.ThrowableRunnable;

import java.awt.datatransfer.StringSelection;
import java.awt.event.InputEvent;
import java.util.Arrays;

public class EditorMultiCaretTest extends AbstractEditorTest {
  private boolean myStoredVirtualSpaceSetting;

  public void setUp() throws Exception {
    super.setUp();
    myStoredVirtualSpaceSetting = EditorSettingsExternalizable.getInstance().isVirtualSpace();
    EditorSettingsExternalizable.getInstance().setVirtualSpace(false);
  }

  public void tearDown() throws Exception {
    try {
      EditorSettingsExternalizable.getInstance().setVirtualSpace(myStoredVirtualSpaceSetting);
    }
    finally {
      super.tearDown();
    }
  }

  public void testCaretAddingAndRemoval() throws Exception {
    initText("some <selection>t<caret>ext</selection>\n" +
             "another line");

    mouse().alt().shift().clickAt(1, 1); // alt-shift-click in a 'free space'
    checkResultByText("some <selection>t<caret>ext</selection>\n" +
                      "a<caret>nother line");

    mouse().alt().shift().clickAt(0, 8); // alt-shift-click in existing selection
    checkResultByText("some <selection>t<caret>ext</selection>\n" +
                      "a<caret>nother line");

    mouse().alt().shift().clickAt(0, 6); // alt-shift-click at existing caret with selection
    checkResultByText("some text\n" +
                      "a<caret>nother line");

    mouse().alt().shift().clickAt(1, 1); // alt-shift-click at the sole caret
    checkResultByText("some text\n" +
                      "a<caret>nother line");

    mouse().alt().shift().clickAt(0, 30); // alt-shift-click in virtual space
    checkResultByText("some text<caret>\n" +
                      "a<caret>nother line");

    mouse().clickAt(0, 0); // plain mouse click
    checkResultByText("<caret>some text\n" +
                      "another line");
  }

  public void testCustomShortcut() throws Throwable {
    doWithAltClickShortcut(() -> {
      initText("<caret>text");
      mouse().alt().clickAt(0, 2);
      checkResultByText("<caret>te<caret>xt");
    });
  }
  
  public void testCaretRemovalWithCustomShortcutDoesntAffectOtherSelections() throws Throwable {
    doWithAltClickShortcut(() -> {
      initText("<selection>some<caret></selection> text");
      mouse().alt().clickAt(0, 6);
      mouse().alt().clickAt(0, 6);
      checkResultByText("<selection>some<caret></selection> text");
    });
  }

  public void testAltDragStartingFromWithinLine() throws Exception {
    initText("<caret>line\n" +
             "long line\n" +
             "very long line\n" +
             "long line\n" +
             "line");
    setEditorVisibleSize(1000, 1000);

    EditorMouseFixture mouse = mouse();
    mouse.alt().pressAt(1, 6);
    checkResultByText("line\n" +
                      "long l<caret>ine\n" +
                      "very long line\n" +
                      "long line\n" +
                      "line");

    mouse.dragTo(4, 6); // still holding Alt
    checkResultByText("line\n" +
                      "long l<caret>ine\n" +
                      "very l<caret>ong line\n" +
                      "long l<caret>ine\n" +
                      "line<caret>");

    mouse.dragTo(4, 8); // still holding Alt
    checkResultByText("line\n" +
                      "long l<selection>in<caret></selection>e\n" +
                      "very l<selection>on<caret></selection>g line\n" +
                      "long l<selection>in<caret></selection>e\n" +
                      "line");

    mouse.dragTo(4, 10).release(); // still holding Alt
    checkResultByText("line\n" +
                      "long l<selection>ine<caret></selection>\n" +
                      "very l<selection>ong <caret></selection>line\n" +
                      "long l<selection>ine<caret></selection>\n" +
                      "line");
  }

  public void testMiddleButtonDragStartingFromVirtualSpace() throws Exception {
    initText("<caret>line\n" +
             "long line\n" +
             "very long line\n" +
             "long line\n" +
             "line");
    setEditorVisibleSize(1000, 1000);

    EditorMouseFixture mouse = mouse();
    mouse.middle().pressAt(1, 17);
    checkResultByText("line\n" +
                      "long line<caret>\n" +
                      "very long line\n" +
                      "long line\n" +
                      "line");

    mouse.dragTo(2, 16);
    checkResultByText("line\n" +
                      "long line<caret>\n" +
                      "very long line<caret>\n" +
                      "long line\n" +
                      "line");

    mouse.dragTo(3, 12);
    checkResultByText("line\n" +
                      "long line\n" +
                      "very long li<selection><caret>ne</selection>\n" +
                      "long line\n" +
                      "line");

    mouse.dragTo(3, 6).release();
    checkResultByText("line\n" +
                      "long l<selection><caret>ine</selection>\n" +
                      "very l<selection><caret>ong line</selection>\n" +
                      "long l<selection><caret>ine</selection>\n" +
                      "line");
  }

  public void testAltOnOffWhileDragging() throws Exception {
    initText("line1\n" +
             "line2\n" +
             "line3");
    setEditorVisibleSize(1000, 1000);

    EditorMouseFixture mouse = mouse();
    mouse.pressAt(0, 1).dragTo(1, 2);
    checkResultByText("l<selection>ine1\n" +
                      "li<caret></selection>ne2\n" +
                      "line3");
    mouse.alt().dragTo(1, 3);
    checkResultByText("l<selection>in<caret></selection>e1\n" +
                      "l<selection>in<caret></selection>e2\n" +
                      "line3");
    mouse.noModifiers().dragTo(2, 4).release();
    checkResultByText("l<selection>ine1\n" +
                      "line2\n" +
                      "line<caret></selection>3");
  }

  public void testTyping() throws Exception {
    initText("some<caret> text<caret>\n" +
             "some <selection><caret>other</selection> <selection>text<caret></selection>\n" +
             "<selection>ano<caret>ther</selection> line");
    type('A');
    checkResultByText("someA<caret> textA<caret>\n" +
                      "some A<caret> A<caret>\n" +
                      "A<caret> line");
  }

  public void testCopyPaste() throws Exception {
    initText("<selection><caret>one</selection> two \n" +
             "<selection><caret>three</selection> four ");
    executeAction("EditorCopy");
    executeAction("EditorLineEnd");
    executeAction("EditorPaste");
    checkResultByText("one twoone<caret> \n" +
                      "three fourthree<caret> ");
  }

  public void testCutAndPaste() throws Exception {
    initText("<selection>one<caret></selection> two \n" +
             "<selection>three<caret></selection> four ");
    executeAction("EditorCut");
    executeAction("EditorLineEnd");
    executeAction("EditorPaste");
    checkResultByText(" twoone<caret> \n" +
                      " fourthree<caret> ");
  }

  public void testPasteSingleItem() throws Exception {
    initText("<selection>one<caret></selection> two \n" +
             "three four ");
    executeAction("EditorCopy");
    executeAction("EditorCloneCaretBelow");
    executeAction("EditorLineEnd");
    executeAction("EditorPaste");
    checkResultByText("one twoone<caret> \n" +
                      "three fourone<caret> ");
  }

  public void testCutAndPasteMultiline() throws Exception {
    initText("one <selection>two \n" +
             "three<caret></selection> four \n" +
             "five <selection>six \n" +
             "seven<caret></selection> eight");
    executeAction("EditorCut");
    executeAction("EditorLineEnd");
    executeAction("EditorPaste");
    checkResultByText("one  fourtwo \n" +
                      "three<caret> \n" +
                      "five  eightsix \n" +
                      "seven<caret>");
  }

  public void testCopyMultilineFromOneCaretPasteIntoTwo() throws Exception {
    initText("<selection>one\n" +
             "two<caret></selection>\n" +
             "three\n" +
             "four");
    executeAction("EditorCopy");
    executeAction("EditorTextStart");
    executeAction("EditorCloneCaretBelow");
    executeAction("EditorPaste");
    checkResultByText("one\n" +
                      "two<caret>one\n" +
                      "one\n" +
                      "two<caret>two\n" +
                      "three\n" +
                      "four");
  }

  public void testCopyPasteDoesNothingWithUnevenSelection() throws Exception {
    initText("<selection>one\n" +
             "two<caret></selection>\n" +
             "<selection>three<caret></selection>\n" +
             "four");
    executeAction("EditorCopy");
    executeAction("EditorPaste");
    checkResultByText("one\n" +
                      "two<caret>\n" +
                      "three<caret>\n" +
                      "four");
  }

  public void testPastingAtDifferentNumberOfCarets() throws Exception {
    initText("<selection>one<caret></selection>\n" +
             "<selection>two<caret></selection>\n" +
             "<selection>three<caret></selection>\n" +
             "<selection>four<caret></selection>");
    copy();
    myEditor.getCaretModel().setCaretsAndSelections(Arrays.asList(new CaretState(new LogicalPosition(0, 0),
                                                                                 new LogicalPosition(0, 0),
                                                                                 new LogicalPosition(0, 0)),
                                                                  new CaretState(new LogicalPosition(1, 0),
                                                                                 new LogicalPosition(1, 0),
                                                                                 new LogicalPosition(1, 0))));
    paste();
    checkResultByText("oneone\n" +
                      "twotwo\n" +
                      "three\n" +
                      "four");
  }

  public void testPastingLineWithBreakFromOutside() throws Exception {
    initText("<caret>\n" +
             "<caret>");
    CopyPasteManager.getInstance().setContents(new StringSelection("abc\n"));
    paste();
    checkResultByText("abc<caret>\n" +
                      "abc<caret>");
  }

  public void testEscapeAfterDragDown() throws Exception {
    initText("line1\n" +
             "line2");
    setEditorVisibleSize(1000, 1000);

    mouse().alt().pressAt(0, 1).dragTo(1, 2).release();
    executeAction("EditorEscape");
    checkResultByText("li<caret>ne1\n" +
                      "line2");
  }

  public void testEscapeAfterDragUp() throws Exception {
    initText("line1\n" +
             "line2");
    setEditorVisibleSize(1000, 1000);

    mouse().alt().pressAt(1, 1).dragTo(0, 2).release();
    executeAction("EditorEscape");
    checkResultByText("line1\n" +
                      "li<caret>ne2");
  }

  public void testAltShiftDoubleClick() throws Exception {
    initText("q<caret>uick brown fox");
    mouse().alt().shift().doubleClickAt(0, 8);
    checkResultByText("q<caret>uick <selection>brown<caret></selection> fox");
  }

  public void testAltShiftDoubleClickAtExistingCaret() throws Exception {
    initText("q<caret>uick br<caret>own fox");
    mouse().alt().shift().doubleClickAt(0, 8);
    checkResultByText("q<caret>uick brown fox");
  }

  public void testAltShiftTripleClick() throws Exception {
    initText("q<caret>uick\n" +
             "brown\n" +
             "fox");
    mouse().alt().shift().tripleClickAt(1, 2);
    checkResultByText("q<caret>uick\n" +
                      "<selection>br<caret>own\n" +
                      "</selection>fox");
  }

  public void testAltShiftTripleClickAtExistingCaret() throws Exception {
    initText("q<caret>uick\n" +
             "br<caret>own\n" +
             "fox");
    mouse().alt().shift().tripleClickAt(1, 2);
    checkResultByText("q<caret>uick\n" +
                      "brown\n" +
                      "fox");
  }

  public void testCaretPositionsRecalculationOnDocumentChange() throws Exception {
    initText("\n" +
             "<selection><caret>word</selection>\n" +
             "some long prefix <selection><caret>word</selection>-suffix");
    EditorTestUtil.configureSoftWraps(myEditor, 17); // wrapping right before 'word-suffix'

    delete();

    checkResultByText("\n" +
                      "<caret>\n" +
                      "some long prefix <caret>-suffix");
    verifySoftWrapPositions(19);
  }

  public void testCreateRectangularSelectionWithMouseClicks() throws Exception {
    initText("<caret>line\n" +
             "long line\n" +
             "very long line\n" +
             "long line\n" +
             "line");
    mouse().alt().shift().middle().clickAt(2, 2);
    checkResultByText("<selection>li<caret></selection>ne\n" +
                      "<selection>lo<caret></selection>ng line\n" +
                      "<selection>ve<caret></selection>ry long line\n" +
                      "long line\n" +
                      "line");
  }

  public void testCreateRectangularSelectionExtendsSelection() throws Exception {
    initText("<caret>line\n" +
             "long line\n" +
             "very long line\n" +
             "long line\n" +
             "line");
    mouse().alt().shift().middle().clickAt(1, 1);
    checkResultByText("<selection>l<caret></selection>ine\n" +
                      "<selection>l<caret></selection>ong line\n" +
                      "very long line\n" +
                      "long line\n" +
                      "line");
    mouse().alt().shift().middle().clickAt(2, 2);
    checkResultByText("<selection>li<caret></selection>ne\n" +
                      "<selection>lo<caret></selection>ng line\n" +
                      "<selection>ve<caret></selection>ry long line\n" +
                      "long line\n" +
                      "line");
  }

  public void testAddingMultipleSelectionsUsingMouse() throws Exception {
    initText("s<selection>om<caret></selection>e text\nother text");
    setEditorVisibleSize(1000, 1000);
    mouse().alt().shift().pressAt(0, 5).dragTo(1, 2).release();
    checkResultByText("s<selection>om<caret></selection>e <selection>text\not<caret></selection>her text");
  }

  public void testAddingMultipleSelectionsUsingMouseInColumnSelectionMode() throws Exception {
    initText("s<selection>om<caret></selection>e text\nother text");
    setEditorVisibleSize(1000, 1000);
    ((EditorEx)myEditor).setColumnMode(true);
    mouse().alt().shift().pressAt(0, 5).dragTo(1, 2).release();
    checkResultByText("s<selection>om<caret></selection>e <selection>text\not<caret></selection>her text");
  }

  public void testAltShiftDragAfterRemovingCaret() throws Exception {
    initText("<selection>a<caret></selection>b<caret>racadabra");
    setEditorVisibleSize(1000, 1000);
    mouse().alt().shift().pressAt(0, 2).dragTo(0, 3).release();
    checkResultByText("<selection>a<caret></selection>bracadabra");
  }

  public void testAddingRectangualSelectionUsingMouse() throws Exception {
    initText("s<selection>om<caret></selection>e text\nother text");
    setEditorVisibleSize(1000, 1000);
    mouse().ctrl().alt().shift().pressAt(0, 7).dragTo(1, 5).release();
    checkResultByText("s<selection>om<caret></selection>e <selection><caret>te</selection>xt\nother<selection><caret> t</selection>ext");
  }
  
  public void testCaretPositionUpdateOnFolding() throws Exception {
    initText("line1\n" +
             "line2\n" +
             "l<caret>ine3\n" +
             "line<caret>4");
    addCollapsedFoldRegion(0, 6, "...");
    verifyCaretsAndSelections(1, 1, 1, 1,
                              2, 4, 4, 4);
  }

  public void testCaretStaysPrimaryOnMerging() throws Exception {
    initText("word\n" +
             "<caret>word word\n" +
             "");
    myEditor.getCaretModel().addCaret(new VisualPosition(0, 0));
    myEditor.getCaretModel().addCaret(new VisualPosition(1, 5));
    assertEquals(new VisualPosition(1, 5), myEditor.getCaretModel().getPrimaryCaret().getVisualPosition());
    down();
    checkResultByText("word\n" +
                      "<caret>word word\n" +
                      "<caret>");
    assertEquals(new VisualPosition(2, 0), myEditor.getCaretModel().getPrimaryCaret().getVisualPosition());
  }

  private static void doWithAltClickShortcut(ThrowableRunnable runnable) throws Throwable {
    Keymap keymap = KeymapManager.getInstance().getActiveKeymap();
    MouseShortcut shortcut = new MouseShortcut(1, InputEvent.ALT_DOWN_MASK, 1);
    try {
      keymap.addShortcut(IdeActions.ACTION_EDITOR_ADD_OR_REMOVE_CARET, shortcut);

      runnable.run();
    }
    finally {
      keymap.removeShortcut(IdeActions.ACTION_EDITOR_ADD_OR_REMOVE_CARET, shortcut);
    }
  }

  public void testTypingAdjacentSpaces() throws Exception {
    initText("<caret>\t<caret>\t");
    rightWithSelection();
    type(' ');
    checkResultByText(" <caret> <caret>");
  }
}
