/*******************************************************************************
 * Copyright (c) 2005, 2016 QNX 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:
 *     QNX - Initial API and implementation
 *     Markus Schorn (Wind River Systems)
 *     IBM Corporation
 *     Sergey Prigogin (Google)
 *******************************************************************************/
package org.eclipse.jdt.internal.core.nd.db;

import java.io.IOException;
import java.nio.ByteBuffer;

/**
 * Caches the content of a piece of the database.
 */
final class Chunk {
	final private byte[] fBuffer= new byte[Database.CHUNK_SIZE];

	final Database fDatabase;
	/**
	 * Holds the database-specific chunk number. This is the index into the database's chunk array and indicates the
	 * start of the range of addresses held by this chunk. Non-negative. 
	 */
	final int fSequenceNumber;
	/**
	 * True iff this chunk contains data that hasn't yet been written to disk. This is protected by the write lock
	 * on the corresponding {@link Database}.
	 */
	boolean fDirty;
	/**
	 * True iff this {@link Chunk} was accessed since the last time it was tested for eviction in the
	 * {@link ChunkCache}. Protected by synchronizing on the {@link ChunkCache} itself.
	 */
	boolean fCacheHitFlag;
	/**
	 * Holds the index into the {@link ChunkCache}'s page table, or -1 if this {@link Chunk} isn't present in the page
	 * table. Protected by synchronizing on the {@link ChunkCache} itself.
	 */
	int fCacheIndex= -1;

	Chunk(Database db, int sequenceNumber) {
		this.fDatabase= db;
		this.fSequenceNumber= sequenceNumber;
	}

	public void makeDirty() {
		if (this.fSequenceNumber >= Database.NUM_HEADER_CHUNKS) {
			Chunk chunk = this.fDatabase.fChunks[this.fSequenceNumber];
			if (chunk != this) {
				throw new IllegalStateException("CHUNK " + this.fSequenceNumber + ": found two copies. Copy 1: " //$NON-NLS-1$ //$NON-NLS-2$
						+ System.identityHashCode(this) + ", Copy 2: " + System.identityHashCode(chunk)); //$NON-NLS-1$
			}
		}
		if (!this.fDirty) {
			if (Database.DEBUG_PAGE_CACHE) {
				System.out.println(
						"CHUNK " + this.fSequenceNumber + ": dirtied - instance " + System.identityHashCode(this)); //$NON-NLS-1$ //$NON-NLS-2$
			}
			if (this.fSequenceNumber >= Database.NUM_HEADER_CHUNKS
					&& this.fDatabase.fMostRecentlyFetchedChunk != this) {
				throw new IllegalStateException("CHUNK " + this.fSequenceNumber //$NON-NLS-1$
						+ " dirtied out of order: Only the most-recently-fetched chunk is allowed to be dirtied"); //$NON-NLS-1$
			}
			this.fDirty = true;
			this.fDatabase.chunkDirtied(this);
		}
	}

	void read() throws IndexException {
		try {
			final ByteBuffer buf= ByteBuffer.wrap(this.fBuffer);
			this.fDatabase.read(buf, (long) this.fSequenceNumber * Database.CHUNK_SIZE);
		} catch (IOException e) {
			throw new IndexException(new DBStatus(e));
		}
	}

	/**
	 * Uninterruptable. Returns true iff an attempt was made to interrupt the flush with
	 * {@link Thread#interrupt()}.
	 */
	boolean flush() throws IndexException {
		if (Database.DEBUG_PAGE_CACHE) {
			System.out.println(
					"CHUNK " + this.fSequenceNumber + ": flushing - instance " + System.identityHashCode(this)); //$NON-NLS-1$//$NON-NLS-2$
		}
		boolean wasCanceled = false;
		try {
			final ByteBuffer buf= ByteBuffer.wrap(this.fBuffer);
			wasCanceled = this.fDatabase.write(buf, (long) this.fSequenceNumber * Database.CHUNK_SIZE);
		} catch (IOException e) {
			throw new IndexException(new DBStatus(e));
		}
		this.fDirty = false;
		this.fDatabase.chunkCleaned(this);
		return wasCanceled;
	}

	static int recPtrToIndex(final long offset) {
		return (int) (offset & Database.OFFSET_IN_CHUNK_MASK);
	}

	public void putByte(final long offset, final byte value) {
		makeDirty();
		this.fBuffer[recPtrToIndex(offset)]= value;
		recordWrite(offset, 1);
	}

	public byte getByte(final long offset) {
		return this.fBuffer[recPtrToIndex(offset)];
	}

	/**
	 * Returns a copy of the entire chunk.
	 */
	public byte[] getBytes() {
		final byte[] bytes = new byte[this.fBuffer.length];
		System.arraycopy(this.fBuffer, 0, bytes, 0, this.fBuffer.length);
		return bytes;
	}

	public byte[] getBytes(final long offset, final int length) {
		final byte[] bytes = new byte[length];
		System.arraycopy(this.fBuffer, recPtrToIndex(offset), bytes, 0, length);
		return bytes;
	}

	public void putBytes(final long offset, final byte[] bytes) {
		makeDirty();
		System.arraycopy(bytes, 0, this.fBuffer, recPtrToIndex(offset), bytes.length);
		recordWrite(offset, bytes.length);
	}

	public void putInt(final long offset, final int value) {
		makeDirty();
		int idx= recPtrToIndex(offset);
		putInt(value, this.fBuffer, idx);
		recordWrite(offset, 4);
	}

	static final void putInt(final int value, final byte[] buffer, int idx) {
		buffer[idx]=   (byte) (value >> 24);
		buffer[++idx]= (byte) (value >> 16);
		buffer[++idx]= (byte) (value >> 8);
		buffer[++idx]= (byte) (value);
	}

	public int getInt(final long offset) {
		return getInt(this.fBuffer, recPtrToIndex(offset));
	}

	static final int getInt(final byte[] buffer, int idx) {
		return ((buffer[idx] & 0xff) << 24) |
				((buffer[++idx] & 0xff) << 16) |
				((buffer[++idx] & 0xff) <<  8) |
				((buffer[++idx] & 0xff) <<  0);
	}

	/**
	 * A free Record Pointer is a pointer to a raw block, i.e. the
	 * pointer is not moved past the BLOCK_HEADER_SIZE.
	 */
	static int compressFreeRecPtr(final long value) {
		// This assert verifies the alignment. We expect the low bits to be clear.
		assert (value & (Database.BLOCK_SIZE_DELTA - 1)) == 0;
		final int dense = (int) (value >> Database.BLOCK_SIZE_DELTA_BITS);
		return dense;
	}

	/**
	 * A free Record Pointer is a pointer to a raw block,
	 * i.e. the pointer is not moved past the BLOCK_HEADER_SIZE.
	 */
	static long expandToFreeRecPtr(int value) {
		/*
		 * We need to properly manage the integer that was read. The value will be sign-extended
		 * so if the most significant bit is set, the resulting long will look negative. By
		 * masking it with ((long)1 << 32) - 1 we remove all the sign-extended bits and just
		 * have an unsigned 32-bit value as a long. This gives us one more useful bit in the
		 * stored record pointers.
		 */
		long address = value & 0xFFFFFFFFL;
		return address << Database.BLOCK_SIZE_DELTA_BITS;
	}

	/**
	 * A Record Pointer is a pointer as returned by Database.malloc().
	 * This is a pointer to a block + BLOCK_HEADER_SIZE.
	 */
	public void putRecPtr(final long offset, final long value) {
		makeDirty();
		int idx = recPtrToIndex(offset);
		Database.putRecPtr(value, this.fBuffer, idx);
		recordWrite(offset, 4);
	}

	/**
	 * A free Record Pointer is a pointer to a raw block,
	 * i.e. the pointer is not moved past the BLOCK_HEADER_SIZE.
	 */
	public void putFreeRecPtr(final long offset, final long value) {
		makeDirty();
		int idx = recPtrToIndex(offset);
		putInt(compressFreeRecPtr(value), this.fBuffer, idx);
		recordWrite(offset, 4);
	}

	public long getRecPtr(final long offset) {
		final int idx = recPtrToIndex(offset);
		return Database.getRecPtr(this.fBuffer, idx);
	}

	public long getFreeRecPtr(final long offset) {
		final int idx = recPtrToIndex(offset);
		int value = getInt(this.fBuffer, idx);
		return expandToFreeRecPtr(value);
	}

	public void put3ByteUnsignedInt(final long offset, final int value) {
		makeDirty();
		int idx= recPtrToIndex(offset);
		this.fBuffer[idx]= (byte) (value >> 16);
		this.fBuffer[++idx]= (byte) (value >> 8);
		this.fBuffer[++idx]= (byte) (value);
		recordWrite(offset, 3);
	}

	public int get3ByteUnsignedInt(final long offset) {
		int idx= recPtrToIndex(offset);
		return ((this.fBuffer[idx] & 0xff) << 16) |
				((this.fBuffer[++idx] & 0xff) <<  8) |
				((this.fBuffer[++idx] & 0xff) <<  0);
	}

	public void putShort(final long offset, final short value) {
		makeDirty();
		int idx= recPtrToIndex(offset);
		this.fBuffer[idx]= (byte) (value >> 8);
		this.fBuffer[++idx]= (byte) (value);
		recordWrite(offset, 2);
	}

	private void recordWrite(long offset, int size) {
		this.fDatabase.getLog().recordWrite(offset, size);
	}

	public short getShort(final long offset) {
		int idx= recPtrToIndex(offset);
		return (short) (((this.fBuffer[idx] << 8) | (this.fBuffer[++idx] & 0xff)));
	}

	public long getLong(final long offset) {
		int idx= recPtrToIndex(offset);
		return ((((long) this.fBuffer[idx] & 0xff) << 56) |
				(((long) this.fBuffer[++idx] & 0xff) << 48) |
				(((long) this.fBuffer[++idx] & 0xff) << 40) |
				(((long) this.fBuffer[++idx] & 0xff) << 32) |
				(((long) this.fBuffer[++idx] & 0xff) << 24) |
				(((long) this.fBuffer[++idx] & 0xff) << 16) |
				(((long) this.fBuffer[++idx] & 0xff) <<  8) |
				(((long) this.fBuffer[++idx] & 0xff) <<  0));
	}

	public double getDouble(long offset) {
		return Double.longBitsToDouble(getLong(offset));
	}

	public float getFloat(long offset) {
		return Float.intBitsToFloat(getInt(offset));
	}

	public void putLong(final long offset, final long value) {
		makeDirty();
		int idx= recPtrToIndex(offset);

		this.fBuffer[idx]=   (byte) (value >> 56);
		this.fBuffer[++idx]= (byte) (value >> 48);
		this.fBuffer[++idx]= (byte) (value >> 40);
		this.fBuffer[++idx]= (byte) (value >> 32);
		this.fBuffer[++idx]= (byte) (value >> 24);
		this.fBuffer[++idx]= (byte) (value >> 16);
		this.fBuffer[++idx]= (byte) (value >> 8);
		this.fBuffer[++idx]= (byte) (value);
		recordWrite(offset, 8);
	}

	public void putChar(final long offset, final char value) {
		makeDirty();
		int idx= recPtrToIndex(offset);
		this.fBuffer[idx]= (byte) (value >> 8);
		this.fBuffer[++idx]= (byte) (value);
		recordWrite(offset, 2);
	}

	public void putChars(final long offset, char[] chars, int start, int len) {
		makeDirty();
		int idx= recPtrToIndex(offset)-1;
		final int end= start + len;
		for (int i = start; i < end; i++) {
			char value= chars[i];
			this.fBuffer[++idx]= (byte) (value >> 8);
			this.fBuffer[++idx]= (byte) (value);
		}
		recordWrite(offset, len * 2);
	}

	public void putCharsAsBytes(final long offset, char[] chars, int start, int len) {
		makeDirty();
		int idx= recPtrToIndex(offset)-1;
		final int end= start + len;
		for (int i = start; i < end; i++) {
			char value= chars[i];
			this.fBuffer[++idx]= (byte) (value);
		}
		recordWrite(offset, len);
	}

	public void putDouble(final long offset, double value) {
		putLong(offset, Double.doubleToLongBits(value));
	}

	public void putFloat(final long offset, float value) {
		putInt(offset, Float.floatToIntBits(value));
	}

	public char getChar(final long offset) {
		int idx= recPtrToIndex(offset);
		return (char) (((this.fBuffer[idx] << 8) | (this.fBuffer[++idx] & 0xff)));
	}

	public void getChars(final long offset, final char[] result, int start, int len) {
		final ByteBuffer buf= ByteBuffer.wrap(this.fBuffer);
		buf.position(recPtrToIndex(offset));
		buf.asCharBuffer().get(result, start, len);
	}

	public void getCharsFromBytes(final long offset, final char[] result, int start, int len) {
		final int pos = recPtrToIndex(offset);
		for (int i = 0; i < len; i++) {
			result[start + i] =  (char) (this.fBuffer[pos + i] & 0xff);
		}
	}

	void clear(final long offset, final int length) {
		makeDirty();
		int idx = recPtrToIndex(offset);
		final int end = idx + length;
		if (end > this.fBuffer.length) {
			throw new IndexException("Attempting to clear beyond end of chunk. Chunk = " + this.fSequenceNumber //$NON-NLS-1$
					+ ", offset = " + offset + ", length = " + length); //$NON-NLS-1$//$NON-NLS-2$
		}
		for (; idx < end; idx++) {
			this.fBuffer[idx] = 0;
		}
		recordWrite(offset, length);
	}

	void put(final long offset, final byte[] data, final int len) {
		put(offset, data, 0, len);
	}

	void put(final long offset, final byte[] data, int dataPos, final int len) {
		makeDirty();
		int idx = recPtrToIndex(offset);
		System.arraycopy(data, dataPos, this.fBuffer, idx, len);
		recordWrite(offset, len);
	}

	public void get(final long offset, byte[] data) {
		get(offset, data, 0, data.length);
	}

	public void get(final long offset, byte[] data, int dataPos, int len) {
		int idx = recPtrToIndex(offset);
		System.arraycopy(this.fBuffer, idx, data, dataPos, len);
	}

	/**
	 * Returns a dirtied, writable version of this chunk whose identity won't change until the write lock is released.
	 */
	public Chunk getWritableChunk() {
		Chunk result = this.fDatabase.getChunk((long) this.fSequenceNumber * Database.CHUNK_SIZE);
		result.makeDirty();
		return result;
	}
}
