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

import java.util.ArrayList;
import java.util.List;

import org.aspectj.org.eclipse.jdt.internal.core.nd.ITypeFactory;
import org.aspectj.org.eclipse.jdt.internal.core.nd.Nd;
import org.aspectj.org.eclipse.jdt.internal.core.nd.db.Database;
import org.aspectj.org.eclipse.jdt.internal.core.nd.db.ModificationLog;
import org.aspectj.org.eclipse.jdt.internal.core.nd.db.ModificationLog.Tag;
import org.aspectj.org.eclipse.jdt.internal.core.nd.util.MathUtils;

/**
 * Stores a singly-linked list of blocks, each of which contains a variable number of embedded elements.
 * Each block contains a header containing the size of the block and pointer to the next block, followed
 * by the embedded elements themselves.
 */
public class FieldList<T> extends BaseField implements IDestructableField {
	/**
	 * Pointer to the first block.
	 */
	public final static FieldPointer FIRST_BLOCK;
	/**
	 * Pointer to the block where insertions are currently happening. This is only null if there are no allocated
	 * blocks. If there are any blocks containing elements, this points to the last block with a nonzero number of
	 * elements.
	 */
	public final static FieldPointer LAST_BLOCK_WITH_ELEMENTS;

	@SuppressWarnings("rawtypes")
	private static final StructDef<FieldList> type;
	private static final int LIST_HEADER_BYTES;
	private static final long MAX_BYTES_IN_A_CHUNK = Database.getBytesThatFitInChunks(1);

	private final StructDef<T> elementType;
	private final int elementsPerBlock;
	private final StructDef<?> ownerType;
	private final Tag allocateTag;
	private final Tag appendTag;
	private final Tag destructTag;

	static {
		type = StructDef.createAbstract(FieldList.class);
		FIRST_BLOCK = type.addPointer();
		LAST_BLOCK_WITH_ELEMENTS = type.addPointer();

		type.done();
		LIST_HEADER_BYTES = MathUtils.roundUpToNearestMultipleOfPowerOfTwo(type.size(), Database.BLOCK_SIZE_DELTA);
	}

	private static class BlockHeader {
		// This points to the next block if there is one, or null if not.
		public final static FieldPointer NEXT_BLOCK;
		public final static FieldShort BLOCK_SIZE;
		public final static FieldShort ELEMENTS_IN_USE;
		public static final int BLOCK_HEADER_BYTES;

		@SuppressWarnings("hiding")
		private static final StructDef<BlockHeader> type;

		static {
			type = StructDef.createAbstract(BlockHeader.class);

			NEXT_BLOCK = type.addPointer();
			BLOCK_SIZE = type.addShort();
			ELEMENTS_IN_USE = type.addShort();
			type.done();

			BLOCK_HEADER_BYTES = MathUtils.roundUpToNearestMultipleOfPowerOfTwo(type.size(), Database.BLOCK_SIZE_DELTA);
		}
	}

	private FieldList(StructDef<?> ownerType, StructDef<T> elementType,  int elementsPerBlock) {
		this.elementType = elementType;
		this.elementsPerBlock = elementsPerBlock;
		this.ownerType = ownerType;
		int fieldNumber = ownerType.getNumFields();
		setFieldName("field " + fieldNumber + ", a " + getClass().getSimpleName() //$NON-NLS-1$//$NON-NLS-2$
				+ " in struct " + ownerType.getStructName());//$NON-NLS-1$
		this.allocateTag = ModificationLog.createTag("Allocating elements for " + getFieldName()); //$NON-NLS-1$
		this.appendTag = ModificationLog.createTag("Appending to " + getFieldName()); //$NON-NLS-1$
		this.destructTag = ModificationLog.createTag("Deallocating " + getFieldName()); //$NON-NLS-1$
	}

	/**
	 * Creates a new {@link FieldList} in the given struct which contains elements of the given type. The resulting list
	 * will grow by 1 element each time it overflows.
	 *
	 * @param ownerStruct
	 *            the struct to which the new list field will be added. Must not have had {@link StructDef#done()}
	 *            invoked on it yet.
	 * @param elementType
	 *            the type of elements that will be contained in the struct.
	 * @return a newly-constructed list field in the given struct.
	 */
	public static <T> FieldList<T> create(StructDef<?> ownerStruct, StructDef<T> elementType) {
		return create(ownerStruct, elementType, 1);
	}

	/**
	 * Creates a new {@link FieldList} in the given struct which contains elements of the given type. The resulting list
	 * will grow by the given number of elements each time it overflows.
	 *
	 * @param ownerStruct
	 *            the struct to which the new list field will be added. Must not have had {@link StructDef#done()}
	 *            invoked on it yet.
	 * @param elementType
	 *            the type of elements that will be contained in the struct.
	 * @param elementsPerBlock
	 *            the number of elements that will be allocated each time the list overflows.
	 * @return a newly-constructed list field in the given struct.
	 */
	public static <T> FieldList<T> create(StructDef<?> ownerStruct, StructDef<T> elementType, int elementsPerBlock) {
		FieldList<T> result = new FieldList<>(ownerStruct, elementType, elementsPerBlock);
		ownerStruct.add(result);
		ownerStruct.addDestructableField(result);
		return result;
	}

	private int getElementSize() {
		int recordSize = this.elementType.getFactory().getRecordSize();
		return MathUtils.roundUpToNearestMultipleOfPowerOfTwo(recordSize, Database.BLOCK_SIZE_DELTA);
	}

	@Override
	public int getRecordSize() {
		return LIST_HEADER_BYTES;
	}

	/**
	 * Returns the contents of the receiver as a {@link List}.
	 *
	 * @param nd the database to be queried.
	 * @param address the address of the parent struct
	 */
	public List<T> asList(Nd nd, long address) {
		long headerStartAddress = address + this.offset;
		long firstBlockAddress = FIRST_BLOCK.get(nd, headerStartAddress);

		List<T> result = new ArrayList<>();

		long nextBlockAddress = firstBlockAddress;
		while (nextBlockAddress != 0) {
			long currentBlockAddress = nextBlockAddress;
			nextBlockAddress = BlockHeader.NEXT_BLOCK.get(nd, currentBlockAddress);
			int elementsInBlock = BlockHeader.ELEMENTS_IN_USE.get(nd, currentBlockAddress);
			long firstElementInBlockAddress = currentBlockAddress + BlockHeader.BLOCK_HEADER_BYTES;

			readElements(result, nd, firstElementInBlockAddress, elementsInBlock);
		}

		return result;
	}

	private void readElements(List<T> result, Nd nd, long nextElementAddress, int count) {
		ITypeFactory<T> factory = this.elementType.getFactory();

		int size = getElementSize();
		for (; count > 0; count--) {
			result.add(factory.create(nd, nextElementAddress));
			nextElementAddress += size;
		}
	}

	public T append(Nd nd, long address) {
		Database db = nd.getDB();
		db.getLog().start(this.appendTag);
		try {
			long headerStartAddress = address + this.offset;
			long nextBlockAddress = LAST_BLOCK_WITH_ELEMENTS.get(nd, headerStartAddress);

			// Ensure that there's at least one block
			long insertionBlockAddress = nextBlockAddress;
			if (nextBlockAddress == 0) {
				long newBlockAddress = allocateNewBlock(nd, this.elementsPerBlock);
				LAST_BLOCK_WITH_ELEMENTS.put(nd, headerStartAddress, newBlockAddress);
				FIRST_BLOCK.put(nd, headerStartAddress, newBlockAddress);
				insertionBlockAddress = newBlockAddress;
			}

			// Check if there's any free space in this block
			int elementsInBlock = BlockHeader.ELEMENTS_IN_USE.get(nd, insertionBlockAddress);
			int blockSize = BlockHeader.BLOCK_SIZE.get(nd, insertionBlockAddress);

			if (elementsInBlock >= blockSize) {
				long nextBlock = BlockHeader.NEXT_BLOCK.get(nd, insertionBlockAddress);
				if (nextBlock == 0) {
					nextBlock = allocateNewBlock(nd, this.elementsPerBlock);
					BlockHeader.NEXT_BLOCK.put(nd, insertionBlockAddress, nextBlock);
				}
				LAST_BLOCK_WITH_ELEMENTS.put(nd, headerStartAddress, nextBlock);
				insertionBlockAddress = nextBlock;
				elementsInBlock = BlockHeader.ELEMENTS_IN_USE.get(nd, insertionBlockAddress);
			}

			BlockHeader.ELEMENTS_IN_USE.put(nd, insertionBlockAddress, (short) (elementsInBlock + 1));
			int elementSize = getElementSize();

			long resultAddress = insertionBlockAddress + BlockHeader.BLOCK_HEADER_BYTES + elementsInBlock * elementSize;
			assert ((resultAddress - Database.BLOCK_HEADER_SIZE) & (Database.BLOCK_SIZE_DELTA - 1)) == 0;
			return this.elementType.getFactory().create(nd, resultAddress);
		} finally {
			db.getLog().end(this.appendTag);
		}
	}

	/**
	 * Ensures that the receiver will have space for the given number of elements without additional
	 * allocation. Callers should invoke this prior to a sequence of {@link FieldList#append(Nd, long)}
	 * calls if they know in advance how many elements will be appended. Will create the minimum number
	 * of extra blocks needed to the given number of additional elements.
	 */
	public void allocate(Nd nd, long address, int numElements) {
		Database db = nd.getDB();
		db.getLog().start(this.allocateTag);
		try {
			if (numElements == 0) {
				// Not an error, but there's nothing to do if the caller didn't actually ask for anything to be allocated.
				return;
			}
			long headerStartAddress = address + this.offset;
			long nextBlockAddress = LAST_BLOCK_WITH_ELEMENTS.get(nd, headerStartAddress);

			int maxBlockSizeThatFitsInAChunk = (int) ((MAX_BYTES_IN_A_CHUNK - BlockHeader.BLOCK_HEADER_BYTES)
					/ getElementSize());

			// Ensure that there's at least one block
			if (nextBlockAddress == 0) {
				int firstAllocation = Math.min(numElements, maxBlockSizeThatFitsInAChunk);
				nextBlockAddress = allocateNewBlock(nd, firstAllocation);
				LAST_BLOCK_WITH_ELEMENTS.put(nd, headerStartAddress, nextBlockAddress);
				FIRST_BLOCK.put(nd, headerStartAddress, nextBlockAddress);
			}

			// Check if there's any free space in this block
			int remainingToAllocate = numElements;
			while (true) {
				long currentBlockAddress = nextBlockAddress;
				nextBlockAddress = BlockHeader.NEXT_BLOCK.get(nd, currentBlockAddress);
				int elementsInUse = BlockHeader.ELEMENTS_IN_USE.get(nd, currentBlockAddress);
				int blockSize = BlockHeader.BLOCK_SIZE.get(nd, currentBlockAddress);

				remainingToAllocate -= (blockSize - elementsInUse);
				if (remainingToAllocate <= 0) {
					break;
				}

				if (nextBlockAddress == 0) {
					nextBlockAddress = allocateNewBlock(nd, Math.min(maxBlockSizeThatFitsInAChunk, numElements));
					BlockHeader.NEXT_BLOCK.put(nd, currentBlockAddress, nextBlockAddress);
				}
			}
		} finally {
			db.getLog().end(this.allocateTag);
		}
	}

	private long allocateNewBlock(Nd nd, int blockSize) {
		short poolId = getMemoryPoolId(nd);
		int elementSize = getElementSize();
		long bytesNeeded = BlockHeader.BLOCK_HEADER_BYTES + blockSize * elementSize;
		// If we're close enough to filling the chunk that we wouldn't be able to fit any more elements anyway, allocate
		// the entire chunk. Although it wastes a small amount of space, it ensures that the blocks can be more easily
		// reused rather than being fragmented. It also allows freed blocks to be merged via the large block allocator.
		if (MAX_BYTES_IN_A_CHUNK - bytesNeeded < elementSize) {
			bytesNeeded = MAX_BYTES_IN_A_CHUNK;
		}
		long result = nd.getDB().malloc(bytesNeeded, poolId);
		BlockHeader.BLOCK_SIZE.put(nd, result, (short) blockSize);
		return result;
	}

	private short getMemoryPoolId(Nd nd) {
		short poolId = Database.POOL_LINKED_LIST;
		if (this.ownerType != null) {
			Class<?> structClass = this.ownerType.getStructClass();
			if (nd.getTypeRegistry().isRegisteredClass(structClass)) {
				poolId = (short) (Database.POOL_FIRST_NODE_TYPE + nd.getNodeType(structClass));
			}
		}
		return poolId;
	}

	@Override
	public void destruct(Nd nd, long address) {
		Database db = nd.getDB();
		db.getLog().start(this.destructTag);
		try {
			short poolId = getMemoryPoolId(nd);
			long headerStartAddress = address + this.offset;
			long firstBlockAddress = FIRST_BLOCK.get(nd, headerStartAddress);

			long nextBlockAddress = firstBlockAddress;
			while (nextBlockAddress != 0) {
				long currentBlockAddress = nextBlockAddress;
				nextBlockAddress = BlockHeader.NEXT_BLOCK.get(nd, currentBlockAddress);
				int elementsInBlock = BlockHeader.ELEMENTS_IN_USE.get(nd, currentBlockAddress);
				destructElements(nd, currentBlockAddress + BlockHeader.BLOCK_HEADER_BYTES, elementsInBlock);
				db.free(currentBlockAddress, poolId);
			}

			db.clearRange(headerStartAddress, getRecordSize());
		} finally {
			db.getLog().end(this.destructTag);
		}
	}

	private void destructElements(Nd nd, long nextElementAddress, int count) {
		ITypeFactory<T> factory = this.elementType.getFactory();

		int size = getElementSize();
		while (--count >= 0) {
			factory.destruct(nd, nextElementAddress);
			nextElementAddress += size;
		}
	}
}
