/*******************************************************************************
 * Copyright (c) 2000, 2011 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.jdt.text.tests;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestName;

import org.eclipse.jdt.testplugin.JavaProjectHelper;
import org.eclipse.jdt.text.tests.performance.DisplayHelper;
import org.eclipse.jdt.text.tests.performance.EditorTestHelper;

import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.widgets.Event;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Path;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.ResourcesPlugin;

import org.eclipse.text.tests.Accessor;

import org.eclipse.jface.preference.IPreferenceStore;

import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.jface.text.TextSelection;
import org.eclipse.jface.text.link.ILinkedModeListener;
import org.eclipse.jface.text.link.LinkedModeModel;
import org.eclipse.jface.text.link.LinkedPosition;

import org.eclipse.ui.PartInitException;

import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IPackageFragment;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;

import org.eclipse.jdt.ui.PreferenceConstants;

import org.eclipse.jdt.internal.ui.JavaPlugin;
import org.eclipse.jdt.internal.ui.javaeditor.JavaEditor;

/**
 * Tests the automatic bracket insertion feature of the CUEditor. Also tests
 * linked mode along the way.
 *
 * @since 3.1
 */
public class BracketInserterTest {
	@Rule
	public TestName tn= new TestName();

	private static final String SRC= "src";
	private static final String SEP= "/";
	private static final String CU_NAME= "PR75423.java";
	private static final String CU_CONTENTS= "package com.example.bugs;\n" +
				"\n" +
				"import java.lang.String;\n" +
				"import java.lang.Integer;\n" +
				"\n" +
				"public class PR75423 {\n" +
				"    String string;\n" +
				"    Integer integer;\n" +
				"\n" +
				"    public static void main(String[] args) {\n" +
				"        \n" +
				"    }\n" +
				"    void foo(String[] args) {\n" +
				"        \n" +
				"    }\n" +
				"    \n" +
				"    HashMap hm= new HashMap();\n" +
				"}\n";

	// document offsets
	private static final int BODY_OFFSET= 196;
	private static final int FIRST_COLUMN_OFFSET= 188;
	private static final int ARGS_OFFSET= 171;
	private static final int BRACKETS_OFFSET= 178;
	private static final int MAIN_VOID_OFFSET= 161;
	private static final int FOO_VOID_OFFSET= 207;
	private static final int FIELD_OFFSET= 264;

	private JavaEditor fEditor;
	private StyledText fTextWidget;
	private IDocument fDocument;
	private Accessor fAccessor;
	private IJavaProject fProject;

	@Before
	public void setUp() throws Exception {
		IPreferenceStore store= JavaPlugin.getDefault().getPreferenceStore();
		store.setValue(PreferenceConstants.EDITOR_CLOSE_BRACKETS, true);
	}

	private void setUpProject(String sourceLevel) throws CoreException, JavaModelException {
		fProject= JavaProjectHelper.createJavaProject(tn.getMethodName(), "bin");
		fProject.setOption(JavaCore.COMPILER_SOURCE, sourceLevel);
		JavaProjectHelper.addSourceContainer(fProject, SRC);
		IPackageFragment fragment= fProject.findPackageFragment(new Path(SEP + tn.getMethodName() + SEP + SRC));
		fragment.createCompilationUnit(CU_NAME, CU_CONTENTS, true, new NullProgressMonitor());
	}

	private void setUpEditor() {
		fEditor= openJavaEditor(new Path(SEP + tn.getMethodName() + SEP + SRC + SEP + CU_NAME));
		assertNotNull(fEditor);
		fTextWidget= fEditor.getViewer().getTextWidget();
		assertNotNull(fTextWidget);
		fAccessor= new Accessor(fTextWidget, StyledText.class);
		fDocument= fEditor.getDocumentProvider().getDocument(fEditor.getEditorInput());
		assertNotNull(fDocument);
		assertEquals(CU_CONTENTS, fDocument.get());
	}

	private JavaEditor openJavaEditor(IPath path) {
		IFile file= ResourcesPlugin.getWorkspace().getRoot().getFile(path);
		assertNotNull(file);
		assertTrue(file.exists());
		try {
			return (JavaEditor)EditorTestHelper.openInEditor(file, true);
		} catch (PartInitException e) {
			fail();
			return null;
		}
	}

	@After
	public void tearDown() throws Exception {
		EditorTestHelper.closeEditor(fEditor);
		fEditor= null;
		if (fProject != null) {
			JavaProjectHelper.delete(fProject);
			fProject= null;
		}

		// reset to defaults
		IPreferenceStore store= JavaPlugin.getDefault().getPreferenceStore();
		store.setValue(PreferenceConstants.EDITOR_CLOSE_BRACKETS, true);
	}

	@Test
	public void testInsertClosingParenthesis() throws BadLocationException, JavaModelException, CoreException, JavaModelException, CoreException {
		use14();
		setCaret(BODY_OFFSET);
		type('(');

		assertEquals("()", fDocument.get(BODY_OFFSET, 2));
		assertSingleLinkedPosition(BODY_OFFSET + 1);
	}

	@Test
	public void testDeletingParenthesis() throws JavaModelException, CoreException {
		use14();
		setCaret(BODY_OFFSET);
		type('(');
		type(SWT.BS);

		assertEquals(CU_CONTENTS, fDocument.get());
		assertFalse(LinkedModeModel.hasInstalledModel(fDocument));
	}

	@Test
	public void testMultipleParenthesisInsertion() throws BadLocationException, JavaModelException, CoreException {
		use14();
		setCaret(BODY_OFFSET);
		type("((((");

		assertEquals("(((())))", fDocument.get(BODY_OFFSET, 8));
		assertEquals(BODY_OFFSET + 4, getCaret());

		assertModel(true);
	}

	@Test
	public void testDeletingMultipleParenthesisInertion() throws BadLocationException, JavaModelException, CoreException {
		use14();
		setCaret(BODY_OFFSET);
		type("((((");

		// delete two levels
		linkedType(SWT.BS, true, ILinkedModeListener.EXTERNAL_MODIFICATION);
		linkedType(SWT.BS, true, ILinkedModeListener.EXTERNAL_MODIFICATION);

		assertEquals("(())", fDocument.get(BODY_OFFSET, 4));
		assertEquals(BODY_OFFSET + 2, getCaret());

		// delete the second-last level
		linkedType(SWT.BS, true, ILinkedModeListener.EXTERNAL_MODIFICATION);
		assertEquals("()", fDocument.get(BODY_OFFSET, 2));
		assertEquals(BODY_OFFSET + 1, getCaret());

		// delete last level
		linkedType(SWT.BS, false, ILinkedModeListener.EXTERNAL_MODIFICATION);
		assertEquals(CU_CONTENTS, fDocument.get());
		assertEquals(BODY_OFFSET, getCaret());

		assertEquals(CU_CONTENTS, fDocument.get());
		assertFalse(LinkedModeModel.hasInstalledModel(fDocument));
	}

	@Test
	public void testNoInsertInsideText() throws BadLocationException, JavaModelException, CoreException {
		use14();
		setCaret(ARGS_OFFSET);
		type('(');

		assertEquals("(St", fDocument.get(ARGS_OFFSET, 3));
		assertEquals(ARGS_OFFSET + 1, getCaret());
		assertFalse(LinkedModeModel.hasInstalledModel(fDocument));
	}

	@Test
	public void testInsertInsideBrackets() throws BadLocationException, JavaModelException, CoreException {
		use14();
		setCaret(BRACKETS_OFFSET);
		type('(');

		assertEquals("()", fDocument.get(BRACKETS_OFFSET, 2));
		assertSingleLinkedPosition(BRACKETS_OFFSET + 1);
	}

	@Test
	public void testPeerEntry() throws BadLocationException, JavaModelException, CoreException {
		use14();
		setCaret(BODY_OFFSET);
		type("()");

		assertEquals("()", fDocument.get(BODY_OFFSET, 2));
		assertEquals(BODY_OFFSET + 2, getCaret());

		assertFalse(LinkedModeModel.hasInstalledModel(fDocument));
	}

//	public void testLoop() throws Exception {
//		for (int i= 0; i < 50; i++) {
//			setUp();
//			testExitOnTab();
//			tearDown();
//		}
//	}
//
	@Test
	public void testMultiplePeerEntry() throws BadLocationException, JavaModelException, CoreException {
		use14();
		setCaret(BODY_OFFSET);
		type("((((");

		linkedType(')', true, ILinkedModeListener.UPDATE_CARET);
		linkedType(')', true, ILinkedModeListener.UPDATE_CARET);
		linkedType(')', true, ILinkedModeListener.UPDATE_CARET);

		assertEquals("(((())))", fDocument.get(BODY_OFFSET, 8));
		assertEquals(BODY_OFFSET + 7, getCaret());

		LinkedPosition position= assertModel(false).findPosition(new LinkedPosition(fDocument, BODY_OFFSET + 1, 0));
		assertNotNull(position);
		assertEquals(BODY_OFFSET + 1, position.getOffset());
		assertEquals(6, position.getLength());

		linkedType(')', false, ILinkedModeListener.UPDATE_CARET);

		assertEquals("(((())))", fDocument.get(BODY_OFFSET, 8));
		assertEquals(BODY_OFFSET + 8, getCaret());
		assertFalse(LinkedModeModel.hasInstalledModel(fDocument));
	}

	@Test
	public void testExitOnTab() throws BadLocationException, JavaModelException, CoreException {
		use14();
		setCaret(BODY_OFFSET);
		type("((((");
		linkedType('\t', true, ILinkedModeListener.NONE);

		assertEquals("(((())))", fDocument.get(BODY_OFFSET, 8));
		assertEquals(BODY_OFFSET + 5, getCaret());

		linkedType('\t', true, ILinkedModeListener.NONE);
		linkedType('\t', true, ILinkedModeListener.NONE);

		assertEquals("(((())))", fDocument.get(BODY_OFFSET, 8));
		assertEquals(BODY_OFFSET + 7, getCaret());

		linkedType('\t', false, ILinkedModeListener.NONE);

		assertEquals("(((())))", fDocument.get(BODY_OFFSET, 8));
		assertEquals(BODY_OFFSET + 8, getCaret());

		assertFalse(LinkedModeModel.hasInstalledModel(fDocument));
	}

	@Test
	public void testExitOnReturn() throws BadLocationException, JavaModelException, CoreException {
		use14();
		setCaret(BODY_OFFSET);
		type("((((");
		linkedType(SWT.CR, true, ILinkedModeListener.UPDATE_CARET | ILinkedModeListener.EXIT_ALL);

		assertEquals("(((())))", fDocument.get(BODY_OFFSET, 8));
		assertEquals(BODY_OFFSET + 8, getCaret());

		assertFalse(LinkedModeModel.hasInstalledModel(fDocument));
	}

	@Test
	public void testExitOnEsc() throws BadLocationException, JavaModelException, CoreException {
		use14();
		setCaret(BODY_OFFSET);
		type("((((");
		linkedType(SWT.ESC, true, ILinkedModeListener.EXIT_ALL);

		assertEquals("(((())))", fDocument.get(BODY_OFFSET, 8));
		assertEquals(BODY_OFFSET + 4, getCaret());

		assertFalse(LinkedModeModel.hasInstalledModel(fDocument));
	}

	@Test
	public void testInsertClosingQuote() throws BadLocationException, JavaModelException, CoreException {
		use14();
		setCaret(BODY_OFFSET);
		type('"');

		assertEquals("\"\"", fDocument.get(BODY_OFFSET, 2));

		assertSingleLinkedPosition(BODY_OFFSET + 1);
	}

	@Test
	public void testPreferences() throws BadLocationException, JavaModelException, CoreException {
		use14();
		IPreferenceStore store= JavaPlugin.getDefault().getPreferenceStore();
		store.setValue(PreferenceConstants.EDITOR_CLOSE_BRACKETS, false);

		setCaret(BODY_OFFSET);
		type('(');

		assertEquals("(", fDocument.get(BODY_OFFSET, 1));
		assertEquals(BODY_OFFSET + 1, getCaret());

		assertFalse(LinkedModeModel.hasInstalledModel(fDocument));
	}

	@Test
	public void testAngleBracketsAsOperator() throws Exception {
		use15();
		setCaret(BODY_OFFSET);
		type("test<");

		assertEquals("test<", fDocument.get(BODY_OFFSET, 5));
		assertNotEquals(">", fDocument.get(BODY_OFFSET + 5, 1));
		assertEquals(BODY_OFFSET + 5, getCaret());

		assertFalse(LinkedModeModel.hasInstalledModel(fDocument));
	}

	@Test
	public void testAngleBracketsIn14Project() throws BadLocationException, JavaModelException, CoreException {
		use14();
		setCaret(BODY_OFFSET);
		type("Test<");

		assertEquals("Test<", fDocument.get(BODY_OFFSET, 5));
		assertNotEquals(">", fDocument.get(BODY_OFFSET + 5, 1));
		assertEquals(BODY_OFFSET + 5, getCaret());

		assertFalse(LinkedModeModel.hasInstalledModel(fDocument));
	}

	@Test
	public void testAngleBracketsIn15Project() throws Exception {
		use15();

		setCaret(BODY_OFFSET);
		type("Test<");

		assertEquals("Test<>", fDocument.get(BODY_OFFSET, 6));
		assertSingleLinkedPosition(BODY_OFFSET + 5);
	}

	@Test
	public void testAngleBracketsInFieldDecl15() throws Exception {
		use15();

		setCaret(FIELD_OFFSET);
		type('<');

		assertEquals("HashMap<> hm", fDocument.get(FIELD_OFFSET - 7, 12));
		assertSingleLinkedPosition(FIELD_OFFSET + 1);
	}

	@Test
	public void testAngleBracketsInsideMethodDecl15() throws Exception {
		use15();

		setCaret(MAIN_VOID_OFFSET);
		type('<');

		assertEquals("public static <>void", fDocument.get(MAIN_VOID_OFFSET - 14, 20));
		assertSingleLinkedPosition(MAIN_VOID_OFFSET + 1);
	}

	@Test
	public void testAngleBracketsBeforeMethodDecl15() throws Exception {
		use15();

		setCaret(FOO_VOID_OFFSET);

		type('<');

		assertEquals("<>void foo", fDocument.get(FOO_VOID_OFFSET, 10));
		assertSingleLinkedPosition(FOO_VOID_OFFSET + 1);
	}

	@Test
	public void testAngleBracketsBeforeTypeArgument15() throws Exception {
		use15();

		String PRE= "new ArrayList";
		String POST= "String>();";

		fDocument.replace(BODY_OFFSET, 0, PRE + POST);
		setCaret(BODY_OFFSET + PRE.length());

		type('<');

		assertEquals(PRE + '<' + POST, fDocument.get(BODY_OFFSET, PRE.length() + 1 + POST.length()));
		assertFalse(LinkedModeModel.hasInstalledModel(fDocument));
	}

	@Test
	public void testAngleBracketsBeforeWildcard15() throws Exception {
		use15();

		String PRE= "new ArrayList";
		String POST= "? extends Number>();";

		fDocument.replace(BODY_OFFSET, 0, PRE + POST);
		setCaret(BODY_OFFSET + PRE.length());

		type('<');

		assertEquals(PRE + '<' + POST, fDocument.get(BODY_OFFSET, PRE.length() + 1 + POST.length()));
		assertFalse(LinkedModeModel.hasInstalledModel(fDocument));
	}

	@Test
	public void testAngleBracketsAfterIdentifierOnFirstColumn1_15() throws Exception {
		//https://bugs.eclipse.org/bugs/show_bug.cgi?id=347734
		use15();

		String PRE= "x";

		fDocument.replace(FIRST_COLUMN_OFFSET, 0, PRE);
		setCaret(FIRST_COLUMN_OFFSET + PRE.length());

		type('<');

		assertEquals(PRE + "< ", fDocument.get(FIRST_COLUMN_OFFSET, PRE.length() + 2));
		assertFalse(LinkedModeModel.hasInstalledModel(fDocument));
	}

	@Test
	public void testAngleBracketsAfterIdentifierOnFirstColumn2_15() throws Exception {
		//https://bugs.eclipse.org/bugs/show_bug.cgi?id=347734
		use15();

		String PRE= "List";

		fDocument.replace(FIRST_COLUMN_OFFSET, 0, PRE);
		setCaret(FIRST_COLUMN_OFFSET + PRE.length());

		type('<');

		assertEquals(PRE + "<>", fDocument.get(FIRST_COLUMN_OFFSET, PRE.length() + 2));
		assertSingleLinkedPosition(FIRST_COLUMN_OFFSET + PRE.length() + 1);
	}

	/* utilities */

	private void assertSingleLinkedPosition(int offset) {
		assertEquals(offset, getCaret());

		LinkedPosition position= assertModel(false).findPosition(new LinkedPosition(fDocument, offset, 0));
		assertNotNull(position);
		assertEquals(offset, position.getOffset());
		assertEquals(0, position.getLength());
	}

	private void use15() throws CoreException, JavaModelException {
		setUpProject(JavaCore.VERSION_1_5);
		setUpEditor();
	}

	private void use14() throws CoreException, JavaModelException {
		setUpProject(JavaCore.VERSION_1_4);
		setUpEditor();
	}

	/**
	 * Type characters into the styled text.
	 *
	 * @param characters the characters to type
	 */
	private void type(CharSequence characters) {
		for (int i= 0; i < characters.length(); i++)
			type(characters.charAt(i), 0, 0);
	}

	/**
	 * Type a character into the styled text.
	 *
	 * @param character the character to type
	 */
	private void type(char character) {
		type(character, 0, 0);
	}

	/**
	 * Ensure there is a linked mode and type a character into the styled text.
	 *
	 * @param character the character to type
	 * @param nested whether the linked mode is expected to be nested or not
	 * @param expectedExitFlags the expected exit flags for the current linked mode after typing the character, -1 for no exit
	 */
	private void linkedType(char character, boolean nested, int expectedExitFlags) {
		final int[] exitFlags= { -1 };
		assertModel(nested).addLinkingListener(new ILinkedModeListener() {
			@Override
			public void left(LinkedModeModel model, int flags) {
				exitFlags[0]= flags;
			}
			@Override
			public void resume(LinkedModeModel model, int flags) {
			}
			@Override
			public void suspend(LinkedModeModel model) {
			}
		});
		type(character, 0, 0);
		assertEquals(expectedExitFlags, exitFlags[0]);
	}

	private LinkedModeModel assertModel(boolean nested) {
		LinkedModeModel model= LinkedModeModel.getModel(fDocument, 0); // offset does not matter
		assertNotNull(model);
		assertEquals(nested, model.isNested());
		return model;
	}

	/**
	 * Type a character into the styled text.
	 *
	 * @param character the character to type
	 * @param keyCode the key code
	 * @param stateMask the state mask
	 */
	private void type(char character, int keyCode, int stateMask) {
		Event event= new Event();
		event.character= character;
		event.keyCode= keyCode;
		event.stateMask= stateMask;
		fAccessor.invoke("handleKeyDown", new Object[] {event});

		new DisplayHelper() {
			@Override
			protected boolean condition() {
				return false;
			}
		}.waitForCondition(EditorTestHelper.getActiveDisplay(), 200);

	}

	private int getCaret() {
		return ((ITextSelection) fEditor.getSelectionProvider().getSelection()).getOffset();
	}

	private void setCaret(int offset) {
		fEditor.getSelectionProvider().setSelection(new TextSelection(offset, 0));
		int newOffset= ((ITextSelection)fEditor.getSelectionProvider().getSelection()).getOffset();
		assertEquals(offset, newOffset);
	}
}
