/*******************************************************************************
 * Copyright (c) 2015, 2016 Google, Inc 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:
 *   Stefan Xenos (Google) - Initial implementation
 *******************************************************************************/
package org.eclipse.jdt.core.tests.nd;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;

import org.eclipse.jdt.core.tests.nd.util.BaseTestCase;
import org.eclipse.jdt.internal.core.nd.Nd;
import org.eclipse.jdt.internal.core.nd.NdNode;
import org.eclipse.jdt.internal.core.nd.NdNodeTypeRegistry;
import org.eclipse.jdt.internal.core.nd.RawGrowableArray;
import org.eclipse.jdt.internal.core.nd.db.Database;
import org.eclipse.jdt.internal.core.nd.field.FieldInt;
import org.eclipse.jdt.internal.core.nd.field.FieldManyToOne;
import org.eclipse.jdt.internal.core.nd.field.FieldOneToMany;
import org.eclipse.jdt.internal.core.nd.field.StructDef;

import junit.framework.Test;

public class FieldBackPointerTest extends BaseTestCase {
	public static class ForwardPointerStruct extends NdNode {
		public static final FieldManyToOne<BackPointerStruct> FORWARD;
		public static final FieldManyToOne<BackPointerStruct> OWNER;

		@SuppressWarnings("hiding")
		public static final StructDef<ForwardPointerStruct> type;

		static {
			type = StructDef.create(ForwardPointerStruct.class, NdNode.type);

			FORWARD = FieldManyToOne.create(type, BackPointerStruct.BACK);
			OWNER = FieldManyToOne.createOwner(type, BackPointerStruct.OWNED);
			type.done();
		}

		public ForwardPointerStruct(Nd nd) {
			super(nd);
		}

		public ForwardPointerStruct(Nd nd, long record) {
			super(nd, record);
		}

		public void setBp(BackPointerStruct toSet) {
			FORWARD.put(getNd(), this.address, toSet);
		}

		public BackPointerStruct getBp() {
			return FORWARD.get(getNd(), this.address);
		}

		public void setOwner(BackPointerStruct owner) {
			OWNER.put(getNd(), this.address, owner);
		}

		public BackPointerStruct getOwner() {
			return OWNER.get(getNd(), this.address);
		}
	}

	public static class BackPointerStruct extends NdNode {
		public static final FieldOneToMany<ForwardPointerStruct> BACK;
		public static final FieldOneToMany<ForwardPointerStruct> OWNED;
		public static final FieldInt SOMEINT;

		@SuppressWarnings("hiding")
		public static final StructDef<BackPointerStruct> type;

		static {
			type = StructDef.create(BackPointerStruct.class, NdNode.type);

			BACK = FieldOneToMany.create(type, ForwardPointerStruct.FORWARD, 2);
			OWNED = FieldOneToMany.create(type, ForwardPointerStruct.OWNER, 0);
			SOMEINT = type.addInt();
			type.done();
		}

		public BackPointerStruct(Nd nd) {
			super(nd);

			// Fill with nonzero values to ensure that "OWNED" doesn't read beyond its boundary
			SOMEINT.put(nd, this.address, 0xf0f0f0f0);
		}

		public BackPointerStruct(Nd nd, long record) {
			super(nd, record);
		}

		public void ensureBackPointerCapacity(int capacity) {
			BACK.ensureCapacity(getNd(), this.address, capacity);
		}

		public int getBackPointerCapacity() {
			return BACK.getCapacity(getNd(), this.address);
		}

		public long getBackpointerAddress(int idx) {
			return BACK.getAddressOf(getNd(), this.address, idx);
		}

		public List<ForwardPointerStruct> getBackPointers() {
			return BACK.asList(getNd(), this.address);
		}

		public List<ForwardPointerStruct> getOwned() {
			return OWNED.asList(getNd(), this.address);
		}

		public int backPointerSize() {
			return BACK.size(getNd(), this.address);
		}

		public boolean backPointersAreEmpty() {
			return BACK.isEmpty(getNd(), this.address);
		}

		public boolean ownedPointersAreEmpty() {
			return OWNED.isEmpty(getNd(), this.address);
		}

		public ForwardPointerStruct getBackPointer(int i) {
			return BACK.get(getNd(), this.address, i);
		}
	}

	ForwardPointerStruct fa;
	ForwardPointerStruct fb;
	ForwardPointerStruct fc;
	ForwardPointerStruct fd;
	BackPointerStruct ba;
	BackPointerStruct bb;
	private Nd nd;

	@Override
	protected void setUp() throws Exception {
		super.setUp();

		NdNodeTypeRegistry<NdNode> registry = new NdNodeTypeRegistry<>();
		registry.register(0, BackPointerStruct.type.getFactory());
		registry.register(1, ForwardPointerStruct.type.getFactory());
		this.nd = DatabaseTestUtil.createEmptyNd(getName(), registry);
		this.nd.getDB().setExclusiveLock();
		this.ba = new BackPointerStruct(this.nd);
		this.bb = new BackPointerStruct(this.nd);
		this.fa = new ForwardPointerStruct(this.nd);
		this.fb = new ForwardPointerStruct(this.nd);
		this.fc = new ForwardPointerStruct(this.nd);
		this.fd = new ForwardPointerStruct(this.nd);
	}

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

	void assertBackPointers(BackPointerStruct bp, ForwardPointerStruct... fp) {
		HashSet<ForwardPointerStruct> backPointers = new HashSet<>(bp.getBackPointers());
		HashSet<ForwardPointerStruct> desired = new HashSet<>();

		desired.addAll(Arrays.asList(fp));
		assertEquals(desired, backPointers);
	}

	public void testLargeBlockBackPointerTest() throws Exception {
		this.nd.getDB().giveUpExclusiveLock();
		// Allocate enough entries to cause the metablock array to resize twice
		int totalSize = Database.CHUNK_SIZE * 0x400;

		long initialAllocations = this.nd.getDB().getBytesAllocated() - this.nd.getDB().getBytesFreed();
		this.nd.acquireWriteLock(null);
		ForwardPointerStruct[] forwardPointer = new ForwardPointerStruct[totalSize];
		for (int idx = 0; idx < totalSize; idx++) {
			forwardPointer[idx] = new ForwardPointerStruct(this.nd);
			forwardPointer[idx].setBp(this.ba);
		}

		for (int idx = 0; idx < totalSize; idx++) {
			assertEquals(forwardPointer[idx].getAddress(), this.ba.getBackpointerAddress(idx));
		}

		for (int idx = 0; idx < totalSize; idx++) {
			forwardPointer[idx].delete();
		}

		this.nd.releaseWriteLock();

		long finalAllocations = this.nd.getDB().getBytesAllocated() - this.nd.getDB().getBytesFreed();

		// Verify no memory leaks
		assertEquals(initialAllocations, finalAllocations);
	}

	public void testWriteFollowedByReadReturnsSameThing() throws Exception {
		this.fa.setBp(this.ba);
		BackPointerStruct backpointer = this.fa.getBp();

		assertEquals(this.ba, backpointer);
	}

	public void testListWithoutInlineElementsCanBeEmpty() throws Exception {
		assertTrue(this.ba.ownedPointersAreEmpty());
	}

	public void testReadNull() throws Exception {
		assertEquals(null, this.fa.getBp());
	}

	public void testAssigningTheSamePointerTwiceIsANoop() throws Exception {
		this.fa.setBp(this.ba);

		assertBackPointers(this.ba, this.fa);

		// Now do the same thing again
		this.fa.setBp(this.ba);

		assertBackPointers(this.ba, this.fa);
	}

	public void testAssigningForwardPointerInsertsBackPointer() throws Exception {
		this.fa.setBp(this.ba);

		assertEquals(Arrays.asList(this.fa), this.ba.getBackPointers());
		assertEquals(1, this.ba.backPointerSize());
	}

	public void testRemovesInlineElement() throws Exception {
		this.fa.setBp(this.ba);
		this.fb.setBp(this.ba);
		this.fc.setBp(this.ba);
		this.fd.setBp(this.ba);

		assertEquals(4, this.ba.backPointerSize());
		this.fb.setBp(null);
		assertEquals(3, this.ba.backPointerSize());

		assertBackPointers(this.ba, this.fa, this.fc, this.fd);
	}

	public void testRemovesElementFromGrowableBlock() throws Exception {
		this.fa.setBp(this.ba);
		this.fb.setBp(this.ba);
		this.fc.setBp(this.ba);
		this.fd.setBp(this.ba);

		assertEquals(4, this.ba.backPointerSize());
		this.fc.setBp(null);
		assertEquals(3, this.ba.backPointerSize());

		assertBackPointers(this.ba, this.fa, this.fb, this.fd);
	}

	public void testDestructingForwardPointerRemovesBackPointer() throws Exception {
		this.fa.setBp(this.ba);
		this.fb.setBp(this.ba);
		this.fc.setBp(this.ba);

		this.fb.delete();
		this.nd.processDeletions();

		assertBackPointers(this.ba, this.fa, this.fc);
	}

	public void testDestructingBackPointerClearsForwardPointers() throws Exception {
		this.fa.setBp(this.ba);
		this.fb.setBp(this.ba);
		this.fc.setBp(this.ba);

		this.ba.delete();
		this.nd.processDeletions();

		assertEquals(null, this.fa.getBp());
		assertEquals(null, this.fb.getBp());
		assertEquals(null, this.fc.getBp());
	}

	public void testElementsRemainInInsertionOrderIfNoRemovals() throws Exception {
		this.fa.setBp(this.ba);
		this.fb.setBp(this.ba);
		this.fc.setBp(this.ba);
		this.fd.setBp(this.ba);

		assertEquals(Arrays.asList(this.fa, this.fb, this.fc, this.fd), this.ba.getBackPointers());
	}

	public void testDeletingOwnerDeletesOwned() throws Exception {
		this.fa.setBp(this.ba);
		this.fa.setOwner(this.bb);

		this.fb.setBp(this.ba);
		this.fb.setOwner(this.bb);

		this.fc.setBp(this.ba);

		this.bb.delete();
		this.nd.processDeletions();

		assertBackPointers(this.ba, this.fc);
	}

	public void testEnsureCapacityDoesNothingIfLessThanInlineElements() throws Exception {
		this.ba.ensureBackPointerCapacity(1);
		assertEquals(2, this.ba.getBackPointerCapacity());
	}

	public void testEnsureCapacityAllocatesPowersOfTwoPlusInlineSize() throws Exception {
		this.ba.ensureBackPointerCapacity(60);
		assertEquals(66, this.ba.getBackPointerCapacity());
	}

	public void testEnsureCapacityAllocatesMinimumSize() throws Exception {
		this.ba.ensureBackPointerCapacity(3);
		assertEquals(4, this.ba.getBackPointerCapacity());
	}

	public void testEnsureCapacityClampsToChunkSize() throws Exception {
		this.ba.ensureBackPointerCapacity(RawGrowableArray.getMaxGrowableBlockSize() - 40);
		assertEquals(RawGrowableArray.getMaxGrowableBlockSize() + 2, this.ba.getBackPointerCapacity());
	}

	public void testEnsureCapacityGrowsByMultiplesOfMaxBlockSizeOnceMetablockInUse() throws Exception {
		int maxBlockSize = RawGrowableArray.getMaxGrowableBlockSize();
		this.ba.ensureBackPointerCapacity(maxBlockSize * 3 - 100);
		assertEquals(maxBlockSize * 3 + 2, this.ba.getBackPointerCapacity());
	}

	public void testAdditionsWontReduceCapacity() throws Exception {
		int maxBlockSize = RawGrowableArray.getMaxGrowableBlockSize();
		this.ba.ensureBackPointerCapacity(maxBlockSize);

		this.fa.setBp(this.ba);
		this.fb.setBp(this.ba);
		this.fc.setBp(this.ba);
		this.fd.setBp(this.ba);

		assertEquals(maxBlockSize + 2, this.ba.getBackPointerCapacity());
	}

	public void testIsEmpty() throws Exception {
		assertTrue(this.ba.backPointersAreEmpty());
		this.fa.setBp(this.ba);
		assertFalse(this.ba.backPointersAreEmpty());
		this.fb.setBp(this.ba);
		this.fc.setBp(this.ba);
		this.fd.setBp(this.ba);
		assertFalse(this.ba.backPointersAreEmpty());
	}

	public void testRemovalsReduceCapacity() throws Exception {
		int maxBlockSize = RawGrowableArray.getMaxGrowableBlockSize();
		this.ba.ensureBackPointerCapacity(maxBlockSize);

		this.fa.setBp(this.ba);
		this.fb.setBp(this.ba);
		this.fc.setBp(this.ba);
		assertEquals(maxBlockSize + 2, this.ba.getBackPointerCapacity());

		this.fb.setBp(null);
		this.fc.setBp(null);

		assertEquals(2, this.ba.getBackPointerCapacity());
	}

	public void testInsertEnoughToUseMetablock() throws Exception {
		// We need enough instances to fill several full blocks since we don't reclaim
		// memory until there are two unused blocks.
		int numToAllocate = RawGrowableArray.getMaxGrowableBlockSize() * 4 + 1;

		List<ForwardPointerStruct> allocated = new ArrayList<>();

		for (int count = 0; count < numToAllocate; count++) {
			ForwardPointerStruct next = new ForwardPointerStruct(this.nd);

			next.setBp(this.ba);
			assertEquals(next, this.ba.getBackPointer(count));
			allocated.add(next);
			assertEquals(count + 1, this.ba.backPointerSize());
		}

		assertEquals(allocated.get(numToAllocate - 1), this.ba.getBackPointer(numToAllocate - 1));
		assertEquals(numToAllocate, this.ba.backPointerSize());

		int correctSize = numToAllocate;
		for (ForwardPointerStruct next : allocated) {
			next.setBp(null);
			assertEquals(--correctSize, this.ba.backPointerSize());
		}

		assertEquals(0, this.ba.backPointerSize());
		assertEquals(2, this.ba.getBackPointerCapacity());
	}

	public void testGrowExistingMetablock() throws Exception {
		int blockSize = RawGrowableArray.getMaxGrowableBlockSize();

		this.ba.ensureBackPointerCapacity(2 * blockSize);

		assertEquals(2 * blockSize + 2, this.ba.getBackPointerCapacity());

		this.ba.ensureBackPointerCapacity(6 * blockSize);

		assertEquals(6 * blockSize + 2, this.ba.getBackPointerCapacity());
	}
}
