/*******************************************************************************
 * Copyright (c) 2006, 2015 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:
 *     Christian Plesner Hansen (plesner@quenta.org) - initial API and implementation
 *******************************************************************************/
package org.eclipse.jface.text.tests;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

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

import org.junit.Test;

import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentExtension3;
import org.eclipse.jface.text.IDocumentPartitioner;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextStore;
import org.eclipse.jface.text.rules.FastPartitioner;
import org.eclipse.jface.text.rules.IPredicateRule;
import org.eclipse.jface.text.rules.RuleBasedPartitionScanner;
import org.eclipse.jface.text.rules.SingleLineRule;
import org.eclipse.jface.text.rules.Token;
import org.eclipse.jface.text.source.DefaultCharacterPairMatcher;
import org.eclipse.jface.text.source.ICharacterPairMatcher;
import org.eclipse.jface.text.source.ICharacterPairMatcherExtension;

/**
 * Generic test of simple character pair matchers
 *
 * @since 3.3
 */
public abstract class AbstractPairMatcherTest {

	private final boolean fCaretEitherSideOfBracket;

	public AbstractPairMatcherTest(boolean caretEitherSideOfBracket) {
		fCaretEitherSideOfBracket= caretEitherSideOfBracket;
	}

	/**
	 * Constructs a new character pair matcher.
	 * 
	 * @param chars the characters to match
	 * @return the character pair matcher
	 */
	protected ICharacterPairMatcher createMatcher(final String chars) {
		return new DefaultCharacterPairMatcher(chars.toCharArray(), getDocumentPartitioning(), fCaretEitherSideOfBracket);
	}

	/**
	 * Returns the partitioning treated by the matcher.
	 * 
	 * @return the partition
	 */
	protected String getDocumentPartitioning() {
		return IDocumentExtension3.DEFAULT_PARTITIONING;
	}

	/* --- T e s t s --- */

	/** Tests that the test case reader works */
	@Test
	public void testTestCaseReader() {
		performReaderTest("%( )#", 0, 3, "( )");
		performReaderTest("#%", 0, 0, "");
	}

	/**
	 * Very simple checks.
	 * 
	 * @throws BadLocationException test failure
	 */
	@Test
	public void testSimpleMatchSameMatcher() throws BadLocationException {
		final ICharacterPairMatcher matcher= createMatcher("()[]{}");
		performMatch(matcher, "#(   )%");
		performMatch(matcher, "#[   ]%");
		performMatch(matcher, "#{   }%");
		performMatch(matcher, "(%   )#");
		performMatch(matcher, "[%   ]#");
		performMatch(matcher, "{%   }#");

		performMatch(matcher, "#(   %)%");
		performMatch(matcher, "#[   %]%");
		performMatch(matcher, "#{   %}%");
		performMatch(matcher, "%(%   )#");
		performMatch(matcher, "%[%   ]#");
		performMatch(matcher, "%{%   }#");

		performMatch(matcher, "#(  %  )#");
		performMatch(matcher, "#[  %  ]#");
		performMatch(matcher, "#{  %  }#");

		performMatch(matcher, "#(  % %  )#");
		performMatch(matcher, "#[  % %  ]#");
		performMatch(matcher, "#{  % %  }#");

		matcher.dispose();
	}

	/**
	 * Very simple checks.
	 * 
	 * @throws BadLocationException test failure
	 */
	@Test
	public void testSimpleMatchDifferentMatchers() throws BadLocationException {
		performMatch("()[]{}", "#(   )%");
		performMatch("()[]{}", "#[   ]%");
		performMatch("()[]{}", "#{   }%");
		performMatch("()[]{}", "(%   )#");
		performMatch("()[]{}", "[%   ]#");
		performMatch("()[]{}", "{%   }#");

		performMatch("()[]{}", "#(  %  )#");
		performMatch("()[]{}", "#[  %  ]#");
		performMatch("()[]{}", "#{  %  }#");
	}

	/**
	 * Close matches.
	 * 
	 * @throws BadLocationException test failure
	 */
	@Test
	public void testCloseMatches() throws BadLocationException {
		final ICharacterPairMatcher matcher= createMatcher("()[]{}");
		performMatch(matcher, "(%)#");
		performMatch(matcher, "#(())%");
		performMatch(matcher, "(%())#");
		performMatch(matcher, "((%)#)");

		performMatch(matcher, "%(%)#");
		performMatch(matcher, "#(()%)%");
		performMatch(matcher, "%(%())#");
		performMatch(matcher, "(%(%)#)");

		performMatch(matcher, "#(%)#");
		performMatch(matcher, "#(%())#");

		performMatch(matcher, "#(% %)#");
		performMatch(matcher, "#(% %())#");

		matcher.dispose();
	}


	/**
	 * Checks of simple situations where no matches should be found.
	 * 
	 * @throws BadLocationException test failure
	 */
	@Test
	public void testIncompleteMatch() throws BadLocationException {
		final ICharacterPairMatcher matcher= createMatcher("()[]{}");
		performMatch(matcher, "(% ");
		performMatch(matcher, "( % )");
		performMatch(matcher, "%");
		matcher.dispose();
	}

	/**
	 * Test that it doesn't match across different partitions.
	 * 
	 * @throws BadLocationException test failure
	 */
	@Test
	public void testPartitioned() throws BadLocationException {
		final ICharacterPairMatcher matcher= createMatcher("()[]{}");
		performMatch(matcher, "(% |a a| )#");
		performMatch(matcher, "#( |a a| )%");
		performMatch(matcher, "|b #( )% b|");
		performMatch(matcher, "( |b )% b|");
		performMatch(matcher, "(% |b ) b|");
		performMatch(matcher, "|a ( a| )%");
		performMatch(matcher, "|a (% a| )");
		performMatch(matcher, "|c #( c| ) ( |c )% c|");
		performMatch(matcher, "|c (% c| ) ( |c )# c|");
		performMatch(matcher, "(% |a ) a| |b ) b| |c ) c| )#");

		performMatch(matcher, "#( % |a a| )#");
		performMatch(matcher, "|b #( % )# b|");
		performMatch(matcher, "|c #( % c| ) ( |c )# c|");
		performMatch(matcher, "|c #( c| ) ( |c % )# c|");
		performMatch(matcher, "#( % |a ) a| |b ) b| |c ) c| )#");

		performMatch(matcher, "#( |a % a| )#");
		performMatch(matcher, "( |a #( a| ( |a % a| ) |a )# a| )");

		performMatch(matcher, "#( % % |a a| )#");
		performMatch(matcher, "|b #( % % )# b|");
		performMatch(matcher, "|c #( % % c| ) ( |c )# c|");
		performMatch(matcher, "|c #( c| ) ( |c % % )# c|");
		performMatch(matcher, "#( % % |a ) a| |b ) b| |c ) c| )#");
		performMatch(matcher, " #( |c ( c| % % |c ) c| )#");

		matcher.dispose();
	}

	/**
	 * Test that it works properly next to partition boundaries.
	 * 
	 * @throws BadLocationException test failure
	 */
	@Test
	public void testTightPartitioned() throws BadLocationException {
		final ICharacterPairMatcher matcher= createMatcher("()[]{}");
		performMatch(matcher, "(|b)%b|");
		performMatch(matcher, "(%|b)b|");
		performMatch(matcher, "|a(a|)%");
		performMatch(matcher, "|a(%a|)");
		performMatch(matcher, "|c#(c|)(|c)%c|");
		performMatch(matcher, "|c(%c|)(|c)#c|");
		performMatch(matcher, "(%|a)a||b)b||c)c|)#");

		performMatch(matcher, "|c#(c|)(|%c)#c|");
		performMatch(matcher, "|c#(c%|)(|c)#c|");
		performMatch(matcher, "#(%|a)a||b)b||c)c|)#");

		matcher.dispose();
	}

	/** Test that nesting works properly */
	@Test
	public void testNesting() {
		final ICharacterPairMatcher matcher= createMatcher("()[]{}");
		performMatch(matcher, " ( #( ( ( ) ) ( ) )% ) ");
		performMatch(matcher, " ( (% ( ( ) ) ( ) )# ) ");
		performMatch(matcher, " ( #( { ( ) } [ ] )% ) ");
		performMatch(matcher, " ( (% { ( ) } [ ] )# ) ");
		performMatch(matcher, " ( ( #{ ( ) }% [ ] ) ) ");
		performMatch(matcher, " ( ( {% ( ) }# [ ] ) ) ");
		performMatch(matcher, "a(b#(c(d(e)f)g(h)i)%j)k");
		performMatch(matcher, "a(b(%c(d(e)f)g(h)i)#j)k");
		performMatch(matcher, "a(b#(c{d(e)f}g[h]i)%j)k");
		performMatch(matcher, "a(b(%c{d(e)f}g[h]i)#j)k");
		performMatch(matcher, "a(b(c#{d(e)f}%g[h]i)j)k");
		performMatch(matcher, "a(b(c{%d(e)f}#g[h]i)j)k");

		performMatch(matcher, " ( #( ( ( ) ) ( ) % )# ) ");
		performMatch(matcher, " ( #( % ( ( ) ) ( ) )# ) ");
		performMatch(matcher, " ( #( { ( ) } [ ] % )# ) ");
		performMatch(matcher, " ( #( % { ( ) } [ ] )# ) ");
		performMatch(matcher, " ( ( #{ ( ) % }# [ ] ) ) ");
		performMatch(matcher, " ( ( #{ % ( ) }# [ ] ) ) ");
		performMatch(matcher, "a(b#(c(d(e)f)g(h)i%)#j)k");
		performMatch(matcher, "a(b#(%c(d(e)f)g(h)i)#j)k");
		performMatch(matcher, "a(b#(c{d(e)f}g[h]i%)#j)k");
		performMatch(matcher, "a(b#(%c{d(e)f}g[h]i)#j)k");
		performMatch(matcher, "a(b(c#{d(e)f%}#g[h]i)j)k");
		performMatch(matcher, "a(b(c#{%d(e)f}#g[h]i)j)k");

		matcher.dispose();
	}

	/**
	 * Test a few boundary conditions.
	 * 
	 * * @throws BadLocationException test failure
	 */
	@Test
	public void testBoundaries() throws BadLocationException {
		final ICharacterPairMatcher matcher= createMatcher("()[]{}");
		final StringDocument doc= new StringDocument("abcdefghijkl");
		assertNull(matcher.match(null, 0));
		assertNull(matcher.match(doc, -1));
		assertNull(matcher.match(doc, doc.getLength() + 1));
		matcher.dispose();
	}

	@Test
	public void testBug156426() {
		final ICharacterPairMatcher matcher= createMatcher("()[]{}<>");
		performMatch(matcher, " #( a < b )% ");
		performMatch(matcher, " (% a < b )# ");
		performMatch(matcher, " #( a > b )% ");
		performMatch(matcher, " (% a > b )# ");
		matcher.dispose();
	}

	@Test
	public void testBug377417() {
		final ICharacterPairMatcher matcher= createMatcher("()[]{}");
		performMatch(matcher, "#( %  )%#");
		performMatch(matcher, "#[ %  ]%#");
		performMatch(matcher, "#{ %  }%#");
		matcher.dispose();
	}

	/* --- U t i l i t i e s --- */

	/**
	 * Checks that the test case reader reads the test case as specified.
	 * 
	 * @param testString the string to test
	 * @param expectedPos the expected position
	 * @param expectedMatch the expected match
	 * @param expectedString the expected string
	 */
	protected void performReaderTest(String testString, int expectedPos, int expectedMatch, String expectedString) {
		TestCase t0= createTestCase(testString);
		assertEquals(expectedPos, t0.fPos1);
		assertEquals(expectedMatch, t0.fMatch2);
		assertEquals(expectedString, t0.fString);
	}

	/**
	 * Checks that the given matcher matches the input as specified.
	 * 
	 * @param matcher the matcher
	 * @param testCase the test string
	 */
	protected void performMatch(final ICharacterPairMatcher matcher, final String testCase) {
		final TestCase test= createTestCase(testCase);
		matcher.clear();
		final IRegion region;

		if (test.isSelectionTestCase()) {
			assertTrue((matcher instanceof ICharacterPairMatcherExtension));
			ICharacterPairMatcherExtension matcherExtension= (ICharacterPairMatcherExtension)matcher;
			if (test.isEnclosingTestCase()) {
				region= matcherExtension.findEnclosingPeerCharacters(test.getDocument(), test.fPos1, test.fPos2 - test.fPos1);
			} else {
				region= matcherExtension.match(test.getDocument(), test.fPos1, test.fPos2 - test.fPos1);
			}
		} else {
			if (test.isEnclosingTestCase()) {
				assertTrue((matcher instanceof ICharacterPairMatcherExtension));
				ICharacterPairMatcherExtension matcherExtension= (ICharacterPairMatcherExtension)matcher;
				region= matcherExtension.findEnclosingPeerCharacters(test.getDocument(), test.fPos1, test.fPos2 - test.fPos1);
			} else {
				region= matcher.match(test.getDocument(), test.fPos1);
			}
		}

		if (test.fMatch2 == -1) {
			// if no match point has been specified there should be no match
			if (region != null) System.out.println(region.getOffset());
			assertNull(region);
		} else {
			assertNotNull(region);
			final boolean isBackward= test.isEnclosingTestCase() ? false : test.fPos1 > test.fMatch2;
			assertEquals(isBackward, matcher.getAnchor() == ICharacterPairMatcher.RIGHT);

			assertEquals(test.getLength(), region.getLength());
			assertEquals(test.getOffset(), region.getOffset());
		}
	}

	private void performMatch(final String delims, final String testCase) {
		final ICharacterPairMatcher matcher= createMatcher(delims);
		performMatch(matcher, testCase);
		matcher.dispose();
	}

	/**
	 * Creates a text case from a string. In the given string a '%' represents the position of the
	 * cursor and a '#' represents the position of the expected matching character.
	 * 
	 * @param str the string for which to create the test case
	 * @return the created test case
	 */
	public TestCase createTestCase(String str) {
		int pos1= str.indexOf("%");
		assertFalse(pos1 == -1);
		int pos2= str.lastIndexOf("%");
		boolean selectionTest= pos1 != pos2;

		int match1= str.indexOf("#");
		int match2= str.lastIndexOf("#");
		boolean enclosingTest= match1 != match2;

		// account for the length of marker characters
		if (selectionTest) {
			if (!enclosingTest) {
				assertTrue(pos2 - pos1 == 2);
				if (match1 != -1 && match1 < pos1) {
					pos1-= 1;
					pos2-= 2;
				}
				if (pos1 < match1) {
					pos2-= 1;
					match1-= 2;
				}
			} else {
				pos1-= 1;
				pos2-= 2;
				match2-= 3;
			}
		} else {
			if (!enclosingTest) {
				if (match1 != -1 && match1 < pos1)
					pos1-= 1;
				if (pos1 < match1)
					match1-= 1;
			} else {
				pos1-= 1;
				match2-= 2;
			}
			pos2= pos1;
		}

		final String stripped= str.replaceAll("%", "").replaceAll("#", "");

		if (enclosingTest) {
			return new TestCase(stripped, pos1, pos2, match1, match2);
		} else {
			if (selectionTest) {
				return new TestCase(stripped, pos1, pos2, pos1 < match1 ? pos1 : pos2, match1);
			} else {
				if (match1 == -1)
					return new TestCase(stripped, pos1, pos2, pos1, match1);

				match2= match1;
				match1= pos1;
				if (!selectionTest && !enclosingTest) {
					String chars= "()[]{}<>";
					if (fCaretEitherSideOfBracket && pos1 < stripped.length()) {
						char ch= stripped.charAt(pos1);
						char prevCh= pos1 - 1 >= 0 ? stripped.charAt(pos1 - 1) : Character.MIN_VALUE;
						if (chars.indexOf(ch) % 2 == 1 && chars.indexOf(prevCh) % 2 != 0) {
							match1++;
						}
					}
					if (pos1 - 1 >= 0) {
						char ch= stripped.charAt(pos1 - 1);
						if (chars.indexOf(ch) % 2 == 0) {
							match1--;
						}
					}
				}
				return new TestCase(stripped, pos1, pos2, match1, match2);
			}
		}
	}

	private class TestCase {

		public final String fString;

		public final int fPos1, fPos2, fMatch1, fMatch2;

		public TestCase(String string, int pos1, int pos2, int match1, int match2) {
			fString= string;
			fPos1= pos1;
			fPos2= pos2;
			fMatch1= match1;
			fMatch2= match2;
		}

		public IDocument getDocument() {
			return new StringDocument(fString);
		}

		public int getLength() {
			return Math.abs(fMatch1 - fMatch2);
		}

		public int getOffset() {
			if (fMatch1 > fMatch2)
				return fMatch2;
			return fMatch1;
		}

		public boolean isEnclosingTestCase() {
			return fPos1 != fMatch1 && fPos2 != fMatch1;
		}

		public boolean isSelectionTestCase() {
			return fPos1 != fPos2;
		}

	}

	private class StringDocument extends Document {

		public StringDocument(String str) {
			this.setTextStore(new StringTextStore(str));
			this.set(str);
			final IDocumentPartitioner part= createPartitioner();
			this.setDocumentPartitioner(getDocumentPartitioning(), part);
			part.connect(this);
		}

	}

	private static class StringTextStore implements ITextStore {

		private String fString;

		public StringTextStore(final String str) {
			fString= str;
		}

		@Override
		public char get(int offset) {
			return fString.charAt(offset);
		}

		@Override
		public String get(int offset, int length) {
			return fString.substring(offset, offset + length);
		}

		@Override
		public int getLength() {
			return fString.length();
		}

		@Override
		public void replace(int offset, int length, String text) {
			throw new UnsupportedOperationException();
		}

		@Override
		public void set(String text) {
			fString= text;
		}

	}

	private static String DEFAULT_PARTITION= IDocument.DEFAULT_CONTENT_TYPE;

	private static IDocumentPartitioner createPartitioner() {
		final RuleBasedPartitionScanner scan= new RuleBasedPartitionScanner();
		final List<SingleLineRule> rules= new ArrayList<>();
		rules.add(new SingleLineRule("|a", "a|", new Token("a")));
		rules.add(new SingleLineRule("|b", "b|", new Token("b")));
		rules.add(new SingleLineRule("|c", "c|", new Token("c")));
		scan.setPredicateRules(rules.toArray(new IPredicateRule[rules.size()]));
		scan.setDefaultReturnToken(new Token(DEFAULT_PARTITION));
		return new FastPartitioner(scan, new String[] { DEFAULT_PARTITION, "a", "b", "c" });
	}

}
