/*******************************************************************************
 * Copyright (c) 2006, 2016 Symbian Software Systems 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:
 *     Symbian - Initial implementation
 *     Markus Schorn (Wind River Systems)
 *******************************************************************************/
package org.eclipse.cdt.internal.pdom.tests;

import java.io.File;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import java.util.SortedSet;
import java.util.TreeSet;

import org.eclipse.cdt.core.testplugin.util.BaseTestCase;
import org.eclipse.cdt.internal.core.pdom.db.BTree;
import org.eclipse.cdt.internal.core.pdom.db.ChunkCache;
import org.eclipse.cdt.internal.core.pdom.db.Database;
import org.eclipse.cdt.internal.core.pdom.db.IBTreeComparator;
import org.eclipse.cdt.internal.core.pdom.db.IBTreeVisitor;
import org.eclipse.core.runtime.CoreException;

import junit.framework.Test;

/**
 * Test insertion/deletion of records of a mock record type in a B-tree.
 *
 * @author aferguso
 */
public class BTreeTests extends BaseTestCase {
	private static int DEBUG = 0;
	protected File dbFile;
	protected Database db;
	protected BTree btree;
	protected int rootRecord;
	protected IBTreeComparator comparator;

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

	// setUp is not used since we need to parameterize this method,
	// and invoke it multiple times per Junit test
	protected void init(int degree) throws Exception {
		dbFile = File.createTempFile("pdomtest", "db");
		db = new Database(dbFile, new ChunkCache(), 0, false);
		db.setExclusiveLock();
		rootRecord = Database.DATA_AREA;
		comparator = new BTMockRecordComparator();
		btree = new BTree(db, rootRecord, degree, comparator);
	}

	// tearDown is not used for the same reason as above
	protected void finish() throws Exception {
		db.close();
		dbFile.deleteOnExit();
	}

	public void testBySortedSetMirrorLite() throws Exception {
		sortedMirrorTest(8);
	}

	/**
	 * Test random (but reproducible via known seed) sequences of insertions/deletions
	 * and use TreeSet as a reference implementation to check behaviour against.
	 * @throws Exception
	 */
	protected void sortedMirrorTest(int noTrials) throws Exception {
		Random seeder = new Random(90210);

		for (int i = 0; i < noTrials; i++) {
			int seed = seeder.nextInt();
			if (DEBUG > 0)
				System.out.println("Iteration #" + i);
			trial(seed, false);
		}
	}

	/**
	 * Test random (but reproducible via known seed) sequence of insertions
	 * and use TreeSet as a reference implementation to check behaviour against.
	 * @throws Exception
	 */
	public void testInsertion() throws Exception {
		Random seeder = new Random();

		for (int i = 0; i < 6; i++) {
			int seed = seeder.nextInt();
			if (DEBUG > 0)
				System.out.println("Iteration #" + i);
			trialImp(seed, false, new Random(seed * 2), 1);
		}
	}

	/**
	 * Bug 402177: BTree.insert should return the matching record if the new record was not inserted.
	 */
	public void testEquivalentRecordInsert_Bug402177() throws Exception {
		init(8);
		try {
			BTMockRecord value1 = new BTMockRecord(db, 42);
			BTMockRecord value2 = new BTMockRecord(db, 42);

			long insert1 = btree.insert(value1.getRecord());
			long insert2 = btree.insert(value2.getRecord());
			assertEquals(insert1, insert2);
		} finally {
			finish();
		}
	}

	/**
	 * Insert/Delete a random number of records into/from the B-tree
	 * @param seed the seed for obtaining the deterministic random testing
	 * @param checkCorrectnessEachIteration if true, then on every single insertion/deletion check that the B-tree invariants
	 * still hold
	 * @throws Exception
	 */
	protected void trial(int seed, final boolean checkCorrectnessEachIteration) throws Exception {
		Random random = new Random(seed);

		// the probabilty that a particular iterations action will be an insertion
		double pInsert = Math.min(0.5 + random.nextDouble(), 1);

		trialImp(seed, checkCorrectnessEachIteration, random, pInsert);
	}

	private void trialImp(int seed, final boolean checkCorrectnessEachIteration, Random random, double pInsert)
			throws Exception {
		final int degree = 2 + random.nextInt(11);
		final int nIterations = random.nextInt(100000);
		final SortedSet expected = new TreeSet();
		final List history = new ArrayList();

		init(degree);

		if (DEBUG > 0)
			System.out.print("\t " + seed + " " + (nIterations / 1000) + "K: ");
		for (int i = 0; i < nIterations; i++) {
			if (random.nextDouble() < pInsert) {
				Integer value = random.nextInt(Integer.MAX_VALUE);
				boolean newEntry = expected.add(value);
				if (newEntry) {
					BTMockRecord btValue = new BTMockRecord(db, value.intValue());
					history.add(btValue);
					if (DEBUG > 1)
						System.out.println("Add: " + value + " @ " + btValue.record);
					btree.insert(btValue.getRecord());
				}
			} else {
				if (!history.isEmpty()) {
					int index = random.nextInt(history.size());
					BTMockRecord btValue = (BTMockRecord) history.get(index);
					history.remove(index);
					expected.remove(Integer.valueOf(btValue.intValue()));
					if (DEBUG > 1)
						System.out.println("Remove: " + btValue.intValue() + " @ " + btValue.record);
					btree.delete(btValue.getRecord());
				}
			}
			if (i % 1000 == 0 && DEBUG > 0) {
				System.out.print(".");
			}
			if (checkCorrectnessEachIteration) {
				assertBTreeMatchesSortedSet("[iteration " + i + "] ", btree, expected);
				assertBTreeInvariantsHold("[iteration " + i + "] ");
			}
		}
		if (DEBUG > 0)
			System.out.println();

		assertBTreeMatchesSortedSet("[Trial end] ", btree, expected);
		assertBTreeInvariantsHold("[Trial end]");

		finish();
	}

	public void assertBTreeInvariantsHold(String msg) throws CoreException {
		String errorReport = btree.getInvariantsErrorReport();
		if (!errorReport.isEmpty()) {
			fail("Invariants do not hold: " + errorReport);
		}
	}

	public void assertBTreeMatchesSortedSet(final String msg, BTree actual, SortedSet expected) throws CoreException {
		final Iterator i = expected.iterator();
		btree.accept(new IBTreeVisitor() {
			int k;

			@Override
			public int compare(long record) throws CoreException {
				return 0;
			}

			@Override
			public boolean visit(long record) throws CoreException {
				if (record != 0) {
					BTMockRecord btValue = new BTMockRecord(record, db);
					if (i.hasNext()) {
						Integer exp = ((Integer) i.next());
						assertEquals(msg + " Differ at index: " + k, btValue.intValue(), exp.intValue());
						k++;
					} else {
						fail("Sizes different");
						return false;
					}
				}
				return true;
			}
		});
	}

	private static class BTMockRecord {
		public static final int VALUE_PTR = 0;
		public static final int RECORD_SIZE = Database.INT_SIZE;
		long record;
		Database db;

		/**
		 * Make a new record
		 */
		public BTMockRecord(Database db, int value) throws CoreException {
			this.db = db;
			record = db.malloc(BTMockRecord.RECORD_SIZE);
			db.putInt(record + VALUE_PTR, value);
		}

		/**
		 * Get an existing record
		 */
		public BTMockRecord(long record, Database db) {
			this.db = db;
			this.record = record;
		}

		public int intValue() throws CoreException {
			return db.getInt(record);
		}

		public long getRecord() {
			return record;
		}
	}

	private class BTMockRecordComparator implements IBTreeComparator {
		@Override
		public int compare(long record1, long record2) throws CoreException {
			return db.getInt(record1) - db.getInt(record2);
		}
	}
}
