/*******************************************************************************
 * Copyright (c) 2004, 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:
 *     IBM Rational Software - Initial API and implementation
 *     Anton Leherbauer (Wind River Systems)
 *     Bryan Wilkinson (QNX)
 *     Markus Schorn (Wind River Systems)
 *     Thomas Corbat (IFS)
 *     Sergey Prigogin (Google)
 *     Mohamed Azab (Mentor Graphics)
 *******************************************************************************/
package org.eclipse.cdt.ui.tests.text.contentassist2;

import static org.eclipse.cdt.core.formatter.DefaultCodeFormatterConstants.FORMATTER_INSERT_SPACE_AFTER_COMMA_IN_METHOD_DECLARATION_PARAMETERS;
import static org.eclipse.cdt.core.formatter.DefaultCodeFormatterConstants.FORMATTER_INSERT_SPACE_AFTER_COMMA_IN_TEMPLATE_PARAMETERS;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.eclipse.cdt.core.dom.IPDOMManager;
import org.eclipse.cdt.core.model.CModelException;
import org.eclipse.cdt.core.model.ICProject;
import org.eclipse.cdt.core.testplugin.CProjectHelper;
import org.eclipse.cdt.internal.core.dom.parser.cpp.CPPASTNameBase;
import org.eclipse.cdt.internal.ui.text.contentassist.CCompletionProposal;
import org.eclipse.cdt.internal.ui.text.contentassist.CContentAssistProcessor;
import org.eclipse.cdt.internal.ui.text.contentassist.ContentAssistMessages;
import org.eclipse.cdt.internal.ui.text.contentassist.ContentAssistPreference;
import org.eclipse.cdt.internal.ui.text.contentassist.ParameterGuessingProposal;
import org.eclipse.cdt.internal.ui.text.contentassist.RelevanceConstants;
import org.eclipse.cdt.ui.CUIPlugin;
import org.eclipse.cdt.ui.testplugin.CTestPlugin;
import org.eclipse.cdt.ui.testplugin.EditorTestHelper;
import org.eclipse.cdt.ui.tests.BaseUITestCase;
import org.eclipse.cdt.ui.text.ICCompletionProposal;
import org.eclipse.cdt.ui.text.ICPartitions;
import org.eclipse.cdt.ui.text.contentassist.ContentAssistInvocationContext;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.TextUtilities;
import org.eclipse.jface.text.contentassist.ContentAssistant;
import org.eclipse.jface.text.contentassist.ICompletionProposal;
import org.eclipse.jface.text.contentassist.IContextInformation;
import org.eclipse.jface.text.source.ISourceViewer;
import org.eclipse.jface.text.templates.TemplateProposal;
import org.eclipse.ui.texteditor.AbstractTextEditor;
import org.eclipse.ui.texteditor.ITextEditor;

public abstract class AbstractContentAssistTest extends BaseUITestCase {
	public static enum CompareType {
		ID, DISPLAY, REPLACEMENT, CONTEXT, INFORMATION
	}

	protected final static int IS_COMPLETION = 0x01;
	protected final static int IS_TEMPLATE = 0x02;
	protected final static int FILTER_RESULTS = 0x04;
	protected final static int ALLOW_EXTRA_RESULTS = 0x08;
	protected final static int CHECK_ORDER = 0x10;

	protected final static int DEFAULT_FLAGS = FILTER_RESULTS;

	protected class ContentAssistResult {
		long startTime;
		long endTime;
		Object[] results;

		public ContentAssistResult(long startTime, long endTime, Object[] results) {
			this.startTime = startTime;
			this.endTime = endTime;
			this.results = results;
		}
	}

	protected ICProject fCProject;
	private IFile fCFile;
	protected ITextEditor fEditor;
	private final boolean fIsCpp;
	protected boolean fProcessorNeedsConfiguring;

	public AbstractContentAssistTest(String name, boolean isCpp) {
		super(name);
		fIsCpp = isCpp;
	}

	@Override
	protected void setUp() throws Exception {
		super.setUp();
		if (fIsCpp) {
			fCProject = CProjectHelper.createCCProject(getName(), "unused", IPDOMManager.ID_FAST_INDEXER);
		} else {
			fCProject = CProjectHelper.createCProject(getName(), "unused", IPDOMManager.ID_FAST_INDEXER);
		}
		fCFile = setUpProjectContent(fCProject.getProject());
		assertNotNull(fCFile);
		waitForIndexer(fCProject);
		fEditor = (ITextEditor) EditorTestHelper.openInEditor(fCFile, true);
		assertNotNull(fEditor);
		CPPASTNameBase.sAllowNameComputation = true;

		//		EditorTestHelper.joinBackgroundActivities((AbstractTextEditor) fEditor);
	}

	/**
	 * Sets up the project's content.
	 * @param project
	 * @return  the file to be opened in the editor
	 */
	protected abstract IFile setUpProjectContent(IProject project) throws Exception;

	@Override
	protected void tearDown() throws Exception {
		ContentAssistInvocationContext.assertNoUndisposedContexts();
		EditorTestHelper.closeEditor(fEditor);
		fEditor = null;
		CProjectHelper.delete(fCProject);
		fCProject = null;
		fCFile = null;
		super.tearDown();
	}

	protected static IPreferenceStore getPreferenceStore() {
		return CUIPlugin.getDefault().getPreferenceStore();
	}

	protected ContentAssistResult invokeContentAssist(int offset, int length, boolean isCompletion, boolean isTemplate,
			boolean filterResults) throws Exception {
		if (CTestPlugin.getDefault().isDebugging()) {
			System.out.println("\n\n\n\n\nTesting " + this.getClass().getName());
		}

		// Call the CContentAssistProcessor
		ISourceViewer sourceViewer = EditorTestHelper.getSourceViewer((AbstractTextEditor) fEditor);
		String contentType = TextUtilities.getContentType(sourceViewer.getDocument(), ICPartitions.C_PARTITIONING,
				offset, true);
		boolean isCode = IDocument.DEFAULT_CONTENT_TYPE.equals(contentType);
		ContentAssistant assistant = new ContentAssistant();
		CContentAssistProcessor processor = new CContentAssistProcessor(fEditor, assistant, contentType);
		assistant.setContentAssistProcessor(processor, contentType);
		if (fProcessorNeedsConfiguring) {
			ContentAssistPreference.configure(assistant, getPreferenceStore());
		}
		long startTime = System.currentTimeMillis();
		sourceViewer.setSelectedRange(offset, length);
		Object[] results = isCompletion ? (Object[]) processor.computeCompletionProposals(sourceViewer, offset)
				: (Object[]) processor.computeContextInformation(sourceViewer, offset);
		long endTime = System.currentTimeMillis();
		assertTrue(results != null);
		// Make sure no exception was thrown during content assist invocation.
		assertTrue(processor.getErrorMessage() == null
				|| processor.getErrorMessage().equals(ContentAssistMessages.ContentAssistProcessor_no_completions));

		if (filterResults) {
			if (isTemplate) {
				results = filterResultsKeepTemplates(results);
			} else {
				results = filterResults(results, isCode);
			}
		}
		return new ContentAssistResult(startTime, endTime, results);
	}

	protected void assertContentAssistResults(int offset, int length, String[] expected, int flags,
			CompareType compareType) throws Exception {
		boolean isCompletion = (flags & IS_COMPLETION) != 0;
		boolean isTemplate = (flags & IS_TEMPLATE) != 0;
		boolean filterResults = (flags & FILTER_RESULTS) != 0;
		boolean allowExtraResults = (flags & ALLOW_EXTRA_RESULTS) != 0;
		boolean checkOrder = (flags & CHECK_ORDER) != 0;

		ContentAssistResult r = invokeContentAssist(offset, length, isCompletion, isTemplate, filterResults);

		String[] resultStrings = toStringArray(r.results, compareType);
		if (!checkOrder) {
			Arrays.sort(expected);
			Arrays.sort(resultStrings);
		}

		if (CTestPlugin.getDefault().isDebugging()) {
			System.out.println("Time: " + (r.endTime - r.startTime) + " ms");
			for (String proposal : resultStrings) {
				System.out.println("Result: " + proposal);
			}
		}

		boolean allFound = true; // For the time being, let's be optimistic.

		for (String element : expected) {
			boolean found = false;
			for (String proposal : resultStrings) {
				if (element.equals(proposal)) {
					found = true;
					if (CTestPlugin.getDefault().isDebugging()) {
						System.out.println("Lookup success for " + element);
					}
					break;
				}
			}
			if (!found) {
				allFound = false;
				if (CTestPlugin.getDefault().isDebugging()) {
					System.out.println("Lookup failed for " + element); //$NON-NLS-1$
				}
			}
		}

		if (!allFound) {
			assertEquals("Missing results!", toString(expected), toString(resultStrings));
		} else if (!allowExtraResults) {
			assertEquals("Extra results!", toString(expected), toString(resultStrings));
		}
	}

	protected void assertContentAssistResults(int offset, int length, Map<String, String[][]> expected, int flags,
			CompareType compareType) throws Exception {
		boolean isCompletion = (flags & IS_COMPLETION) != 0;
		boolean isTemplate = (flags & IS_TEMPLATE) != 0;
		boolean filterResults = (flags & FILTER_RESULTS) != 0;
		ContentAssistResult r = invokeContentAssist(offset, length, isCompletion, isTemplate, filterResults);
		Map<String, String[][]> resultMap = toMap(r.results, compareType);

		if (CTestPlugin.getDefault().isDebugging()) {
			System.out.println("Time : " + (r.endTime - r.startTime) + " ms");
			for (String proposal : resultMap.keySet()) {
				System.out.println("Result: " + proposal);
				String[][] result = resultMap.get(proposal);
				for (String[] row : result) {
					for (String s : row) {
						System.out.print(s + " ");
					}
					System.out.println();
				}
			}
			System.out.println();
		}

		for (String proposal : expected.keySet()) {
			String[][] result = resultMap.get(proposal);
			assertNotNull(result);
			String[][] expectedGuesses = expected.get(proposal);
			String exp = "";
			String guess = "";
			assertEquals(expectedGuesses.length, result.length);
			for (int i = 0; i < result.length; i++) {
				String[] tmp = expectedGuesses[i];
				Arrays.sort(tmp);
				exp += toString(tmp) + "\n";
				tmp = result[i];
				Arrays.sort(tmp);
				guess += toString(tmp) + "\n";
			}
			assertEquals(exp, guess);
		}
	}

	private Map<String, String[][]> toMap(Object[] results, CompareType compareType) throws CModelException {
		Map<String, String[][]> resultsMap = new HashMap<>();
		for (Object result : results) {
			switch (compareType) {
			case REPLACEMENT:
				if (result instanceof ParameterGuessingProposal) {
					ParameterGuessingProposal proposal = (ParameterGuessingProposal) result;
					proposal.generateParameterGuesses();
					String pName = proposal.getReplacementString();
					ICompletionProposal[][] pProposals = proposal.getParametersGuesses();
					String[][] p;
					if (pProposals != null) {
						p = new String[pProposals.length][];
						for (int i = 0; i < pProposals.length; i++) {
							p[i] = new String[pProposals[i].length];
							for (int j = 0; j < pProposals[i].length; j++) {
								p[i][j] = pProposals[i][j].getDisplayString();
							}
						}
					} else {
						p = new String[0][];
					}
					resultsMap.put(pName, p);
				}
				break;
			}
		}
		return resultsMap;
	}

	protected void assertContentAssistResults(int offset, String[] expected, int flags, CompareType compareType)
			throws Exception {
		assertContentAssistResults(offset, 0, expected, flags, compareType);
	}

	/**
	 * Filter out template and keyword proposals.
	 *
	 * @param results
	 * @param isCodeCompletion  completion is in code, not preprocessor, etc.
	 * @return filtered proposals
	 */
	private Object[] filterResults(Object[] results, boolean isCodeCompletion) {
		List<Object> filtered = new ArrayList<>();
		for (Object result : results) {
			if (result instanceof TemplateProposal) {
				continue;
			}
			if (result instanceof ICCompletionProposal) {
				if (isCodeCompletion) {
					// Check for keywords proposal.
					int relevance = ((ICCompletionProposal) result).getRelevance();
					if (relevance >= RelevanceConstants.CASE_MATCH_RELEVANCE) {
						relevance -= RelevanceConstants.CASE_MATCH_RELEVANCE;
					}
					if (relevance <= RelevanceConstants.KEYWORD_TYPE_RELEVANCE) {
						continue;
					}
				}
				filtered.add(result);
			} else if (result instanceof IContextInformation) {
				filtered.add(result);
			}
		}
		return filtered.toArray();
	}

	/**
	 * Filter out proposals, keep only templates
	 */
	private Object[] filterResultsKeepTemplates(Object[] results) {
		List<Object> filtered = new ArrayList<>();
		for (Object result : results) {
			if (result instanceof TemplateProposal) {
				filtered.add(result);
			}
		}
		return filtered.toArray();
	}

	private String[] toStringArray(Object[] results, CompareType type) {
		String[] strings = new String[results.length];

		for (int i = 0; i < results.length; i++) {
			Object result = results[i];
			switch (type) {
			case ID:
				if (result instanceof ICCompletionProposal) {
					strings[i] = ((ICCompletionProposal) result).getIdString();
				}
				break;

			case DISPLAY:
				if (result instanceof ICompletionProposal) {
					strings[i] = ((ICompletionProposal) result).getDisplayString();
				}
				break;

			case REPLACEMENT:
				if (result instanceof CCompletionProposal) {
					strings[i] = ((CCompletionProposal) result).getReplacementString();
				} else if (result instanceof ICCompletionProposal) {
					strings[i] = ((ICCompletionProposal) result).getDisplayString();
				}
				break;

			case CONTEXT:
				if (result instanceof ICompletionProposal) {
					result = ((ICompletionProposal) result).getContextInformation();
				}
				if (result instanceof IContextInformation) {
					strings[i] = ((IContextInformation) result).getContextDisplayString();
				}
				break;

			case INFORMATION:
				if (result instanceof ICompletionProposal) {
					result = ((ICompletionProposal) result).getContextInformation();
				}
				if (result instanceof IContextInformation) {
					strings[i] = ((IContextInformation) result).getInformationDisplayString();
				}
				break;
			}
		}
		return strings;
	}

	private String toString(String[] strings) {
		StringBuilder buf = new StringBuilder();
		for (String string : strings) {
			buf.append(string).append('\n');
		}
		return buf.toString();
	}

	/**
	 * Returns the content of the editor buffer
	 */
	protected String getBuffer() {
		return getDocument().get();
	}

	/**
	 * Returns the editor document
	 */
	protected IDocument getDocument() {
		return EditorTestHelper.getDocument(fEditor);
	}

	protected void setCommaAfterFunctionParameter(String value) {
		fCProject.setOption(FORMATTER_INSERT_SPACE_AFTER_COMMA_IN_METHOD_DECLARATION_PARAMETERS, value);
	}

	protected void setCommaAfterTemplateParameter(String value) {
		fCProject.setOption(FORMATTER_INSERT_SPACE_AFTER_COMMA_IN_TEMPLATE_PARAMETERS, value);
	}
}
