/*******************************************************************************
 * Copyright (c) 2007, 2013 Symbian Software Systems and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     Andrew Ferguson (Symbian) - Initial implementation
 *     Sergey Prigogin (Google)
 *******************************************************************************/
package org.eclipse.cdt.internal.index.tests;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;

import junit.framework.Test;

import org.eclipse.cdt.core.CCorePlugin;
import org.eclipse.cdt.core.dom.IPDOMManager;
import org.eclipse.cdt.core.dom.ast.DOMException;
import org.eclipse.cdt.core.dom.ast.IBinding;
import org.eclipse.cdt.core.dom.ast.ICompositeType;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPMethod;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPNamespace;
import org.eclipse.cdt.core.index.IIndex;
import org.eclipse.cdt.core.index.IIndexManager;
import org.eclipse.cdt.core.index.IndexFilter;
import org.eclipse.cdt.core.model.ICProject;
import org.eclipse.cdt.core.testplugin.CProjectHelper;
import org.eclipse.cdt.core.testplugin.CTestPlugin;
import org.eclipse.cdt.core.testplugin.util.BaseTestCase;
import org.eclipse.cdt.core.testplugin.util.TestSourceReader;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IProjectDescription;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Path;

/**
 * Tests the behavior of the IIndex API when dealing with multiple projects
 */
public class IndexCompositeTests extends BaseTestCase {
	Set<IProject> createdProjects = new HashSet<IProject>();

	/*
	 * Convenience class for setting up projects.
	 */
	private class ProjectBuilder {
		private final String name;
		private final boolean cpp;
		private List<IProject> dependencies = new ArrayList<IProject>();
		private Map<String, String> path2content = new HashMap<String, String>();
	
		ProjectBuilder(String name, boolean cpp) {
			this.name = name;
			this.cpp = cpp;
		}
	
		ProjectBuilder addDependency(IProject project) {
			dependencies.add(project);
			return this;
		}
	
		ProjectBuilder addFile(String relativePath, CharSequence content) {
			path2content.put(relativePath, content.toString());
			return this;
		}
	
		ICProject create() throws Exception {
			ICProject result = cpp ?
					CProjectHelper.createCCProject(name, "bin", IPDOMManager.ID_NO_INDEXER) :
					CProjectHelper.createCProject(name, "bin", IPDOMManager.ID_NO_INDEXER);
			createdProjects.add(result.getProject());
	
			IFile lastFile= null;
			for (Map.Entry<String, String> entry : path2content.entrySet()) {
				lastFile= TestSourceReader.createFile(result.getProject(), new Path(entry.getKey()), entry.getValue());
			}
	
			IProjectDescription desc = result.getProject().getDescription();
			desc.setReferencedProjects(dependencies.toArray(new IProject[dependencies.size()]));
			result.getProject().setDescription(desc, new NullProgressMonitor());
	
			IIndexManager indexManager = CCorePlugin.getIndexManager();
			indexManager.setIndexerId(result, IPDOMManager.ID_FAST_INDEXER);
			if (lastFile != null) {
				// Call reindex explicitly since setting indexer ID doesn't trigger reindexing.
				indexManager.reindex(result);
				IIndex index= indexManager.getIndex(result);
				TestSourceReader.waitUntilFileIsIndexed(index, lastFile, INDEXER_TIMEOUT_SEC * 1000);
			} 
			BaseTestCase.waitForIndexer(result);
			return result;
		}
	}

	public static Test suite() {
		return suite(IndexCompositeTests.class);
	}

	private static final int NONE = 0;
	private static final int REFS = IIndexManager.ADD_DEPENDENCIES;
	private static final int REFD = IIndexManager.ADD_DEPENDENT;
	private static final int BOTH = REFS | REFD;

	private static final IndexFilter FILTER= new IndexFilter() {
		@Override
		public boolean acceptBinding(IBinding binding) throws CoreException {
			if (binding instanceof ICPPMethod) {
				return !((ICPPMethod) binding).isImplicit();
			}
			return true;
		}
	};
	
	IIndex index;
	
	protected StringBuilder[] getContentsForTest(int blocks) throws IOException {
		return TestSourceReader.getContentsForTest(
				CTestPlugin.getDefault().getBundle(), "parser", getClass(), getName(), blocks);
	}

	// class A {};

	// class B {};
	public void testPairDisjointContent() throws Exception {
		CharSequence[] contents = getContentsForTest(2);
		List<ICProject> projects = new ArrayList<ICProject>();

		try {
			ProjectBuilder pb = new ProjectBuilder("projB_" + getName(), true);
			pb.addFile("h1.h", contents[0]);
			ICProject cprojB = pb.create();
			projects.add(cprojB);

			pb = new ProjectBuilder("projA_" + getName(), true);
			pb.addFile("h2.h", contents[1]).addDependency(cprojB.getProject());
			ICProject cprojA = pb.create();
			projects.add(cprojA);

			setIndex(cprojB, NONE);	assertBCount(1, 1);
			setIndex(cprojB, REFS);	assertBCount(1, 1);
			setIndex(cprojB, REFD);	assertBCount(2, 2);
			setIndex(cprojB, BOTH);	assertBCount(2, 2);

			setIndex(cprojA, NONE);	assertBCount(1, 1);
			setIndex(cprojA, REFS);	assertBCount(2, 2);
			setIndex(cprojA, REFD);	assertBCount(1, 1);
			setIndex(cprojA, BOTH);	assertBCount(2, 2);
		} finally {
			for (ICProject project : projects) {
				project.getProject().delete(true, true, new NullProgressMonitor());
			}
		}
	}

	// class C1 {public: int i;};
	// namespace X { class C2 {}; }
	// enum E {E1,E2};
	// void foo(C1 c) {}

	// #include "h3.h"
	// class B1 {};
	// namespace X { class B2 {}; }
	// C1 c1;
	// X::C2 c2;
	// void foo(B1 c) {}
	// void foo(X::C2 c) {}

	// #include "h2.h"
	// class A1 {};
	// void foo(X::B2 c) {}
	// namespace X { class A2 {}; B2 b; C2 c; }
	public void testTripleLinear() throws Exception {
		CharSequence[] contents = getContentsForTest(3);
		List<ICProject> projects = new ArrayList<ICProject>();

		try {
			ProjectBuilder pb = new ProjectBuilder("projC_" + getName(), true);
			pb.addFile("h3.h", contents[0]);
			ICProject cprojC = pb.create();
			projects.add(cprojC);

			pb = new ProjectBuilder("projB_" + getName(), true);
			pb.addFile("h2.h", contents[1]).addDependency(cprojC.getProject());
			ICProject cprojB = pb.create();
			projects.add(cprojB);

			pb = new ProjectBuilder("projA_" + getName(), true);
			pb.addFile("h1.h", contents[2]).addDependency(cprojB.getProject());
			ICProject cprojA = pb.create();
			projects.add(cprojA);

			/* Defines Global, Defines Namespace, References Global, References Namespace
			 * projC: 6, 2, 0, 0
			 * projB: 6, 1, 1, 1 + projC
			 * projA: 3, 3, 0, 2 + projB + projC
			 */

			final int gC= 6, aC= gC + 2;
			final int gB= 6, aB= gB + 1;
			final int gA= 3, aA= gA + 3;
			
			final int gBC= gB + gC - 1, aBC= aB + aC - 1;
			final int gABC= gA + gBC - 1, aABC= aA + aBC - 1;
			
			setIndex(cprojC, NONE);
			assertBCount(gC, aC); assertNamespaceXMemberCount(1);
			assertFieldCount("C1", 1);
			
			setIndex(cprojC, REFS);
			assertBCount(gC, aC);
			assertNamespaceXMemberCount(1);
			assertFieldCount("C1", 1);
			
			setIndex(cprojC, REFD);
			assertBCount(gABC, aABC);
			assertNamespaceXMemberCount(5);
			assertFieldCount("C1", 1);
			
			setIndex(cprojC, BOTH);
			assertBCount(gABC, aABC);
			assertNamespaceXMemberCount(5);
			assertFieldCount("C1", 1);

			setIndex(cprojB, NONE);
			assertBCount(gBC, aBC);
			assertNamespaceXMemberCount(2);
			assertFieldCount("C1", 1);
			
			setIndex(cprojB, REFS);
			assertBCount(gBC, aBC);
			assertNamespaceXMemberCount(2);
			assertFieldCount("C1", 1);
			
			setIndex(cprojB, REFD);
			assertBCount(gABC, aABC);
			assertNamespaceXMemberCount(5);
			assertFieldCount("C1", 1);
			
			setIndex(cprojB, BOTH);
			assertBCount(gABC, aABC);
			assertNamespaceXMemberCount(5);
			assertFieldCount("C1", 1);
			
			setIndex(cprojA, NONE);
			assertBCount(gABC, aABC);
			assertNamespaceXMemberCount(5);
			// binding C1 is not referenced by cprojA
			
			setIndex(cprojA, REFS);
			assertBCount(gABC, aABC);
			assertNamespaceXMemberCount(5);
			assertFieldCount("C1", 1);
			
			setIndex(cprojA, REFD);
			assertBCount(gABC, aABC);
			assertNamespaceXMemberCount(5);
			// binding C1 is not referenced by cprojA
			
			setIndex(cprojA, BOTH);
			assertBCount(gABC, aABC);
			assertNamespaceXMemberCount(5);
			assertFieldCount("C1", 1);
		} finally {
			for (ICProject project : projects) {
				project.getProject().delete(true, true, new NullProgressMonitor());
			}
		}
	}

	// class B1 {};
	// namespace X { class B2 {}; }
	// void foo(B1 c) {}
	// void foo(X::B2 c, B1 c) {}

	// #include "h2.h"
	// class A1 {};
	// void foo(X::B2 c) {}
	// namespace X { class A2 {}; }
	// B1 ab;

	// #include "h2.h"
	// class C1 {};
	// namespace X { class C2 {}; B1 b; }
	// enum E {E1,E2};
	// X::B2 cb;
	// void foo(C1 c) {}
	public void testTripleUpwardV() throws Exception {
		CharSequence[] contents = getContentsForTest(3);
		List<ICProject> projects = new ArrayList<ICProject>();
		
		try {
			ProjectBuilder pb = new ProjectBuilder("projB_" + getName(), true);
			pb.addFile("h2.h", contents[0]);
			ICProject cprojB = pb.create();
			projects.add(cprojB);

			pb = new ProjectBuilder("projA_" + getName(), true);
			pb.addFile("h1.h", contents[1]).addDependency(cprojB.getProject());
			ICProject cprojA = pb.create();
			projects.add(cprojA);

			pb = new ProjectBuilder("projC_" + getName(), true);
			pb.addFile("h3.h", contents[2]).addDependency(cprojB.getProject());
			ICProject cprojC = pb.create();
			projects.add(cprojC);

			/*  A   C    |
		     *   \ /     | Depends On / References
		     *    B      V
		     *    
			 * Defines Global, Defines Namespace, Ext. References Global, Ext. References Namespace
			 * projC: 7, 2, 1, 1
			 * projB: 4, 1, 0, 0
			 * projA: 4, 1, 1, 1
			 */
			
			final int gC= 7, aC= gC + 2;
			final int gB= 4, aB= gB + 1;
			final int gA= 4, aA= gA + 1;
			
			final int gBC= gB + gC - 1, aBC= aB + aC - 1;
			final int gAB= gA + gB - 1, aAB= aA + aB - 1;
			final int gABC= gA + gBC - 1, aABC= aA + aBC - 1;

			setIndex(cprojC, NONE);
			assertBCount(gBC, aBC);
			assertNamespaceXMemberCount(3);
			setIndex(cprojC, REFS);
			assertBCount(gBC, aBC);
			assertNamespaceXMemberCount(3);
			setIndex(cprojC, REFD);
			assertBCount(gBC, aBC);
			assertNamespaceXMemberCount(3);
			setIndex(cprojC, BOTH);
			assertBCount(gABC, aABC);
			assertNamespaceXMemberCount(4);

			setIndex(cprojB, NONE);
			assertBCount(gB, aB);
			assertNamespaceXMemberCount(1);
			setIndex(cprojB, REFS);
			assertBCount(gB, aB);
			assertNamespaceXMemberCount(1);
			setIndex(cprojB, REFD);
			assertBCount(gABC, aABC);
			assertNamespaceXMemberCount(4);
			setIndex(cprojB, BOTH);
			assertBCount(gABC, aABC);
			assertNamespaceXMemberCount(4);

			setIndex(cprojA, NONE);
			assertBCount(gAB, aAB);
			assertNamespaceXMemberCount(2);
			setIndex(cprojA, REFS);
			assertBCount(gAB, aAB);
			assertNamespaceXMemberCount(2);
			setIndex(cprojA, REFD);
			assertBCount(gAB, aAB);
			assertNamespaceXMemberCount(2);
			setIndex(cprojA, BOTH);
			assertBCount(gABC, aABC);
			assertNamespaceXMemberCount(4);
		} finally {
			for (ICProject project : projects) {
				project.getProject().delete(true, true, new NullProgressMonitor());
			}
		}
	}

	// class C1 {};
	// namespace X { class C2 {}; }
	// enum E {E1,E2};
	// void foo(C1 c) {}

	// #include "h3.h"
	// #include "h1.h"
	// class B1 {};
	// namespace X { class B2 {}; C1 c; }
	// void foo(A1 c) {}
	// void foo(X::A2 c, B1 c) {}
	
	// class A1 {};
	// void foo(A1 a, A1 b) {}
	// namespace X { class A2 {}; }
	public void testTripleDownwardV() throws Exception {
		CharSequence[] contents = getContentsForTest(3);
		List<ICProject> projects = new ArrayList<ICProject>();

		try {
			ProjectBuilder pb = new ProjectBuilder("projC_" + getName(), true);
			pb.addFile("h3.h", contents[0]);
			ICProject cprojC = pb.create();
			projects.add(cprojC);

			pb = new ProjectBuilder("projA_" + getName(), true);
			pb.addFile("h1.h", contents[2]);
			ICProject cprojA = pb.create();
			projects.add(cprojA);

			pb = new ProjectBuilder("projB_" + getName(), true);
			pb.addFile("h2.h", contents[1]).addDependency(cprojC.getProject()).addDependency(cprojA.getProject());
			ICProject cprojB = pb.create();
			projects.add(cprojB);

			/*    B     |
		     *   / \    | Depends On / References
		     *  A   C   V
		     *    
			 *  Defines Global, Defines Namespace, References Global, References Namespace
			 * projC: 6, 1, 0, 0
			 * projB: 4, 2, 2, 1
			 * projA: 3, 1, 0, 0
			 */

			final int gC= 6, aC= gC + 1;
			final int gB= 4, aB= gB + 2;
			final int gA= 3, aA= gA + 1;
			
			final int gBC= gB + gC - 1, aBC= aB + aC - 1;
			final int gAB= gA + gB - 1, aAB= aA + aB - 1;
			final int gABC= gA + gBC - 1, aABC= aA + aBC - 1;

			setIndex(cprojC, NONE);
			assertBCount(gC, aC);
			assertNamespaceXMemberCount(1);
			setIndex(cprojC, REFS);
			assertBCount(gC, aC);
			assertNamespaceXMemberCount(1);
			setIndex(cprojC, REFD);
			assertBCount(gABC, aABC);
			assertNamespaceXMemberCount(4);
			setIndex(cprojC, BOTH);
			assertBCount(gABC, aABC);
			assertNamespaceXMemberCount(4);

			setIndex(cprojB, NONE);
			assertBCount(gABC, aABC);
			assertNamespaceXMemberCount(4);
			setIndex(cprojB, REFS);
			assertBCount(gABC, aABC);
			assertNamespaceXMemberCount(4);
			setIndex(cprojB, REFD);
			assertBCount(gABC, aABC);
			assertNamespaceXMemberCount(4);
			setIndex(cprojB, BOTH);
			assertBCount(gABC, aABC);
			assertNamespaceXMemberCount(4);

			setIndex(cprojA, NONE);
			assertBCount(gA, aA);
			assertNamespaceXMemberCount(1);
			setIndex(cprojA, REFS);
			assertBCount(gA, aA);
			assertNamespaceXMemberCount(1);
			setIndex(cprojA, REFD);
			assertBCount(gABC, aABC);
			assertNamespaceXMemberCount(4);
			setIndex(cprojA, BOTH);
			assertBCount(gABC, aABC);
			assertNamespaceXMemberCount(4);
		} finally {
			for (ICProject project : projects) {
				project.getProject().delete(true, true, new NullProgressMonitor());
			}
		}
	}
	
	/**
	 * Asserts binding counts, and returns the index tested against
	 * @param global the number of bindings expected to be found at global scope
	 * @param all the number of bindings expected to be found at all scopes
	 * @return the index
	 * @throws CoreException
	 */
	private IIndex assertBCount(int global, int all) throws CoreException {
		IBinding[] bindings = index.findBindings(Pattern.compile(".*"), true, FILTER, new NullProgressMonitor());
		assertEquals(global, bindings.length);
		bindings = index.findBindings(Pattern.compile(".*"), false, FILTER, new NullProgressMonitor());
		assertEquals(all, bindings.length);
		return index;
	}
	
	private void assertNamespaceXMemberCount(int count) throws CoreException, DOMException {
		IBinding[] bindings = index.findBindings(Pattern.compile("X"), true, FILTER, new NullProgressMonitor());
		assertEquals(1, bindings.length);
		assertEquals(count, ((ICPPNamespace) bindings[0]).getMemberBindings().length);
	}
	
	private void assertFieldCount(String qnPattern, int count) throws CoreException, DOMException {
		IBinding[] bindings = index.findBindings(Pattern.compile(qnPattern), true, FILTER, new NullProgressMonitor());
		assertEquals(1, bindings.length);
		assertEquals(count, ((ICompositeType) bindings[0]).getFields().length);
	}
	
	private void setIndex(ICProject project, int options) throws CoreException, InterruptedException {
		if (index != null) {
			index.releaseReadLock();
		}
		index = CCorePlugin.getIndexManager().getIndex(project, options);
		index.acquireReadLock();
	}
	
	@Override
	protected void tearDown() throws Exception {
		if (index != null) {
			index.releaseReadLock();
			index = null;
		}
		for (IProject project : createdProjects) {
			project.delete(true, npm());
		}
		createdProjects.clear();
		super.tearDown();
	}
}
