/*******************************************************************************
 * Copyright (c) 2005, 2014 QNX Software Systems
 * 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:
 *     QNX Software Systems - initial API and implementation
 *     Andrew Ferguson (Symbian)
 *     Markus Schorn (Wind River Systems)
 *******************************************************************************/
package org.eclipse.cdt.internal.pdom.tests;

import java.io.File;
import java.util.Random;

import junit.framework.Test;

import org.eclipse.cdt.core.testplugin.CTestPlugin;
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.cdt.internal.core.pdom.db.IString;
import org.eclipse.cdt.internal.core.pdom.db.ShortString;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;

/**
 * Tests for the {@link Database} class.
 */
public class DatabaseTest extends BaseTestCase {
	// This constant can be used to run the test with very large databases.
	// Try, for example, setting it to Integer.MAX_VALUE * 7L;
	private static final long TEST_OFFSET = 0;
	protected Database db;

	@Override
	protected void setUp() throws Exception {
		super.setUp();
		db = new Database(getTestDir().append(getName() + System.currentTimeMillis() + ".dat").toFile(),
				new ChunkCache(), 0, false);
		db.setExclusiveLock();

		// Allocate all database chunks up to TEST_OFFSET.
		int count = 0;
		for (long offset = 0; offset < TEST_OFFSET;) {
			offset = db.malloc(Database.MAX_MALLOC_SIZE);
			if (++count >= 1000) {
				db.flush();
				count = 0;
			}
		}
		db.flush();
	}

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

	protected IPath getTestDir() {
		IPath path = CTestPlugin.getDefault().getStateLocation().append("tests/");
		File file = path.toFile();
		if (!file.exists())
			file.mkdir();
		return path;
	}

	@Override
	protected void tearDown() throws Exception {
		db.close();
		if (!db.getLocation().delete()) {
			db.getLocation().deleteOnExit();
		}
		db= null;
	}

	public void testBlockSizeAndFirstBlock() throws Exception {
		assertEquals(0, db.getVersion());

		final int realsize = 42;
		final int deltas = (realsize + Database.BLOCK_HEADER_SIZE + Database.BLOCK_SIZE_DELTA - 1) / Database.BLOCK_SIZE_DELTA;
		final int blocksize = deltas * Database.BLOCK_SIZE_DELTA;
		final int freeDeltas= Database.CHUNK_SIZE / Database.BLOCK_SIZE_DELTA - deltas;

		long mem = db.malloc(realsize);
		assertEquals(-blocksize, db.getShort(mem - Database.BLOCK_HEADER_SIZE));
		db.free(mem);
		assertEquals(blocksize, db.getShort(mem - Database.BLOCK_HEADER_SIZE));
		assertEquals(mem, db.getRecPtr((deltas - Database.MIN_BLOCK_DELTAS +1 ) * Database.INT_SIZE));
		assertEquals(mem + blocksize, db.getRecPtr((freeDeltas - Database.MIN_BLOCK_DELTAS + 1) * Database.INT_SIZE));
	}

	public void testBug192437() throws Exception {
		File tmp= File.createTempFile("readOnlyEmpty", ".db");
		try {
			tmp.setReadOnly();

			/* check opening a readonly file for rw access fails */
			try {
				new Database(tmp, ChunkCache.getSharedInstance(), 0, false);
				fail("A readonly file should not be openable with write-access");
			} catch (CoreException e) {
				// we expect to get a failure here
			}

			/* check opening a readonly file for read access does not fail */
			try {
				new Database(tmp, ChunkCache.getSharedInstance(), 0, true);
			} catch (CoreException e) {
				fail("A readonly file should be readable by a permanently readonly database " + e);
			}
		} finally {
			tmp.delete(); // this may be pointless on some platforms
		}
	}

	public void testFreeBlockLinking() throws Exception {
		final int realsize = 42;
		final int deltas = (realsize + Database.BLOCK_HEADER_SIZE + Database.BLOCK_SIZE_DELTA - 1) / Database.BLOCK_SIZE_DELTA;
		final int blocksize = deltas * Database.BLOCK_SIZE_DELTA;
		final int freeDeltas= Database.MIN_BLOCK_DELTAS - deltas;

		long mem1 = db.malloc(realsize);
		long mem2 = db.malloc(realsize);
		db.free(mem1);
		db.free(mem2);
		assertEquals(mem2, db.getRecPtr((deltas - Database.MIN_BLOCK_DELTAS + 1) * Database.INT_SIZE));
		assertEquals(0, db.getRecPtr(mem2));
		assertEquals(mem1, db.getRecPtr(mem2 + Database.INT_SIZE));
		assertEquals(mem2, db.getRecPtr(mem1));
		assertEquals(0, db.getRecPtr(mem1 + Database.INT_SIZE));
	}

	public void testSimpleAllocationLifecycle() throws Exception {
		long mem1 = db.malloc(42);
		db.free(mem1);
		long mem2 = db.malloc(42);
		assertEquals(mem2, mem1);
	}

	private static class FindVisitor implements IBTreeVisitor {
		private Database db;
		private String key;
		private long record;

		public FindVisitor(Database db, String key) {
			this.db = db;
			this.key = key;
		}

		@Override
		public int compare(long record) throws CoreException {
			return db.getString(db.getRecPtr(record + 4)).compare(key, true);
		}

		@Override
		public boolean visit(long record) throws CoreException {
			this.record = record;
			return false;
		}

		public long getRecord() {
			return record;
		}
	}

	public void testStringsInBTree() throws Exception {
		String[] names = {
				"ARLENE",
				"BRET",
				"CINDY",
				"DENNIS",
				"EMILY",
				"FRANKLIN",
				"GERT",
				"HARVEY",
				"IRENE",
				"JOSE",
				"KATRINA",
				"LEE",
				"MARIA",
				"NATE",
				"OPHELIA",
				"PHILIPPE",
				"RITA",
				"STAN",
				"TAMMY",
				"VINCE",
				"WILMA",
				"ALPHA",
				"BETA"
		};

		IBTreeComparator comparator = new IBTreeComparator() {
			@Override
			public int compare(long record1, long record2) throws CoreException {
				IString string1 = db.getString(db.getRecPtr(record1 + 4));
				IString string2 = db.getString(db.getRecPtr(record2 + 4));
				return string1.compare(string2, true);
			}
		};

		BTree btree = new BTree(db, Database.DATA_AREA, comparator);
		for (int i = 0; i < names.length; ++i) {
			String name = names[i];
			long record = db.malloc(8);
			db.putInt(record + 0, i);
			IString string = db.newString(name);
			db.putRecPtr(record + 4, string.getRecord());
			btree.insert(record);
		}

		for (int i = 0; i < names.length; ++i) {
			String name = names[i];
			FindVisitor finder = new FindVisitor(db, name);
			btree.accept(finder);
			long record = finder.getRecord();
			assertTrue(record != 0);
			assertEquals(i, db.getInt(record));
			IString rname = db.getString(db.getRecPtr(record + 4));
			assertTrue(rname.equals(name));
		}
	}

	private final int GT = 1, LT = -1, EQ = 0;

	public void testShortStringComparison() throws CoreException {
		Random r= new Random(90210);

		assertCMP("",  EQ, "", true);
		assertCMP("",  EQ, "", false);

		doTrials(1000, 1, ShortString.MAX_BYTE_LENGTH / 2, r, true);
		doTrials(1000, 1, ShortString.MAX_BYTE_LENGTH / 2, r, false);
		doTrials(1000, 1, ShortString.MAX_BYTE_LENGTH, r, true);
		doTrials(1000, 1, ShortString.MAX_BYTE_LENGTH, r, false);

		assertCMP("a",  LT, "b", true);
		assertCMP("aa", LT, "ab", true);
		assertCMP("a",  EQ, "a", true);

		assertCMP("a",  GT, "A", true);
		assertCMP("aa", GT, "aA", true);
		assertCMP("a",  GT, "B", true);

		assertCMP("a",  EQ, "a", false);
		assertCMP("a",  EQ, "A", false);
	}

	public void testLongStringComparison() throws CoreException {
		Random r= new Random(314159265);
		doTrials(100, ShortString.MAX_BYTE_LENGTH + 1, ShortString.MAX_BYTE_LENGTH * 2, r, true);
		doTrials(100, ShortString.MAX_BYTE_LENGTH + 1, ShortString.MAX_BYTE_LENGTH * 2, r, false);
	}

	private void doTrials(int n, int min, int max, Random r, boolean caseSensitive) throws CoreException {
		long start = System.currentTimeMillis();
		for (int i= 0; i < n; i++) {
			String a = randomString(min, max, r);
			String b = randomString(min, max, r);
			int expected = caseSensitive ? a.compareTo(b) : a.compareToIgnoreCase(b);
			assertCMP(a, expected, b, caseSensitive);
		}
//		System.out.print("Trials: " + n + " Max length: " + max + " ignoreCase: " + !caseSensitive);
//		System.out.println(" Time: " + (System.currentTimeMillis() - start));
	}

	private String randomString(int min, int max, Random r) {
		int len = min + r.nextInt(max - min);
		return randomString(len, r);
	}

	private String randomString(int len, Random r) {
		StringBuilder result = new StringBuilder(len);
		for (int i= 0; i < len; i++) {
			result.append(randomChar(r));
		}
		return result.toString();
	}

	private char randomChar(Random r) {
		// We only match String.compareToIgnoreCase behavior within this limited range.
		return (char) (32 + r.nextInt(40));
	}

	private void assertCMP(String a, int expected, String b, boolean caseSensitive) throws CoreException {
		char[] acs = a.toCharArray();
		char[] bcs = b.toCharArray();
		IString aiss = db.newString(a);
		IString biss = db.newString(b);
		IString aisc = db.newString(acs);
		IString bisc = db.newString(bcs);

		assertEquals(a.hashCode(), aiss.hashCode());
		assertEquals(a.hashCode(), aisc.hashCode());
		assertEquals(b.hashCode(), biss.hashCode());
		assertEquals(b.hashCode(), bisc.hashCode());

		assertEquals(aiss, a);
		assertEquals(aisc, a);
		assertEquals(biss, b);
		assertEquals(bisc, b);

		assertSignEquals(expected, aiss.compare(bcs, caseSensitive));
		assertSignEquals(expected, aiss.compare(biss, caseSensitive));
		assertSignEquals(expected, aiss.compare(bisc, caseSensitive));
		assertSignEquals(expected, aiss.compare(b, caseSensitive));
		assertSignEquals(expected, aiss.comparePrefix(bcs, caseSensitive));

		assertSignEquals(expected, -biss.compare(acs, caseSensitive));
		assertSignEquals(expected, -biss.compare(aiss, caseSensitive));
		assertSignEquals(expected, -biss.compare(aisc, caseSensitive));
		assertSignEquals(expected, -biss.compare(a, caseSensitive));
		assertSignEquals(expected, -biss.comparePrefix(acs, caseSensitive));

		if (!caseSensitive && expected != 0) {
			assertSignEquals(expected, aiss.compareCompatibleWithIgnoreCase(bcs));
			assertSignEquals(expected, aiss.compareCompatibleWithIgnoreCase(biss));
			assertSignEquals(expected, aiss.compareCompatibleWithIgnoreCase(bisc));

			assertSignEquals(expected, -biss.compareCompatibleWithIgnoreCase(acs));
			assertSignEquals(expected, -biss.compareCompatibleWithIgnoreCase(aiss));
			assertSignEquals(expected, -biss.compareCompatibleWithIgnoreCase(aisc));
		}
	}

	private void assertSignEquals(int a, int b) {
		a= a < 0 ? -1 : (a > 0 ? 1 : 0);
		b= b < 0 ? -1 : (b > 0 ? 1 : 0);
		assertEquals(a, b);
	}
}
