/*******************************************************************************
 * Copyright (c) 2007, 2016 Wind River Systems, 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:
 *     Markus Schorn - initial API and implementation
 *******************************************************************************/
package org.aspectj.org.eclipse.jdt.internal.core.nd.db;

import org.eclipse.core.runtime.preferences.IEclipsePreferences;
import org.eclipse.core.runtime.preferences.InstanceScope;
import org.aspectj.org.eclipse.jdt.core.JavaCore;

public final class ChunkCache {
	private static ChunkCache sSharedInstance;

	private Chunk[] fPageTable;
	private boolean fTableIsFull;
	private int fPointer;

	public static final String CHUNK_CACHE_SIZE_MB = "chunkCacheSizeMb"; //$NON-NLS-1$
	public static final String CHUNK_CACHE_SIZE_PERCENT = "chunkCacheSizePercent"; //$NON-NLS-1$

	public static final double CHUNK_CACHE_SIZE_MB_DEFAULT = 128.0;
	public static final double CHUNK_CACHE_SIZE_PERCENT_DEFAULT = 5.0;

	static {
		IEclipsePreferences node = InstanceScope.INSTANCE.getNode(JavaCore.PLUGIN_ID);
		long chunkSize = getChunkCacheSize(node);
		sSharedInstance= new ChunkCache(chunkSize);
		node.addPreferenceChangeListener(event -> {
			String key = event.getKey();
			if (key.equals(CHUNK_CACHE_SIZE_MB) | key.equals(CHUNK_CACHE_SIZE_PERCENT)) {
				sSharedInstance.setMaxSize(getChunkCacheSize(node));
			}
		});
	}

	private static long getChunkCacheSize(IEclipsePreferences node) {
		double maxSizeMb = node.getDouble(CHUNK_CACHE_SIZE_MB, CHUNK_CACHE_SIZE_MB_DEFAULT);
		double maxSizePercent = node.getDouble(CHUNK_CACHE_SIZE_PERCENT, CHUNK_CACHE_SIZE_PERCENT_DEFAULT);

		maxSizePercent = Math.max(1.0, Math.min(50.0, maxSizePercent));
		maxSizeMb = Math.max(maxSizeMb, 1.0);

		long m1= (long) (Runtime.getRuntime().maxMemory() / 100.0 * maxSizePercent);
		return Math.min(m1, (long) (maxSizeMb * 1024.0 * 1024.0));
	}

	public static ChunkCache getSharedInstance() {
		return sSharedInstance;
	}

	public ChunkCache() {
		this(5 * 1024 * 1024);
	}

	public ChunkCache(long maxSize) {
		this.fPageTable= new Chunk[computeLength(maxSize)];
	}

	public synchronized void add(Chunk chunk) {
		if (chunk.fCacheIndex >= 0) {
			chunk.fCacheHitFlag= true;
			return;
		}
		if (this.fTableIsFull) {
			evictChunk();
			chunk.fCacheIndex= this.fPointer;
			this.fPageTable[this.fPointer]= chunk;
		} else {
			chunk.fCacheIndex= this.fPointer;
			this.fPageTable[this.fPointer]= chunk;

			this.fPointer++;
			if (this.fPointer == this.fPageTable.length) {
				this.fPointer= 0;
				this.fTableIsFull= true;
			}
		}
	}

	/**
	 * Evicts a chunk from the page table and the chunk table.
	 * After this method returns, {@link #fPointer}  will contain
	 * the index of the evicted chunk within the page table.
	 */
	private void evictChunk() {
		/*
		 * Use the CLOCK algorithm to determine which chunk to evict.
		 * i.e., if the chunk in the current slot of the page table has been
		 * recently referenced (i.e. the reference flag is set), unset the
		 * reference flag and move to the next slot.  Otherwise, evict the
		 * chunk in the current slot.
		 */
		while (true) {
			Chunk chunk = this.fPageTable[this.fPointer];
			if (chunk.fCacheHitFlag) {
				chunk.fCacheHitFlag = false;
				this.fPointer = (this.fPointer + 1) % this.fPageTable.length;
			} else {
				chunk.fCacheIndex = -1;
				chunk.fDatabase.checkIfChunkReleased(chunk);
				this.fPageTable[this.fPointer] = null;
				return;
			}
		}
	}

	public synchronized void remove(Chunk chunk) {
		final int idx= chunk.fCacheIndex;
		if (idx >= 0) {
			if (this.fTableIsFull) {
				this.fPointer= this.fPageTable.length-1;
				this.fTableIsFull= false;
			} else {
				this.fPointer--;
			}
			chunk.fCacheIndex= -1;
			final Chunk move= this.fPageTable[this.fPointer];
			this.fPageTable[idx]= move;
			move.fCacheIndex= idx;
			this.fPageTable[this.fPointer]= null;
		}
	}

	/**
	 * Returns the maximum size of the chunk cache in bytes.
	 */
	public synchronized long getMaxSize() {
		return (long) this.fPageTable.length * Database.CHUNK_SIZE;
	}

	/**
	 * Clears the page table and changes it to hold chunks with
	 * maximum total memory of <code>maxSize</code>.
	 * @param maxSize the total size of the chunks in bytes.
	 */
	public synchronized void setMaxSize(long maxSize) {
		final int newLength= computeLength(maxSize);
		final int oldLength= this.fTableIsFull ? this.fPageTable.length : this.fPointer;
		if (newLength > oldLength) {
			Chunk[] newTable= new Chunk[newLength];
			System.arraycopy(this.fPageTable, 0, newTable, 0, oldLength);
			this.fTableIsFull= false;
			this.fPointer= oldLength;
			this.fPageTable= newTable;
		} else {
			for (int i = newLength; i < oldLength; i++) {
				Chunk chunk = this.fPageTable[i];
				chunk.fCacheIndex = -1;
				chunk.fDatabase.checkIfChunkReleased(chunk);
			}
			Chunk[] newTable= new Chunk[newLength];
			System.arraycopy(this.fPageTable, 0, newTable, 0, newLength);
			this.fTableIsFull= true;
			this.fPointer= 0;
			this.fPageTable= newTable;
		}
	}

	private int computeLength(long maxSize) {
		long maxLength= Math.min(maxSize / Database.CHUNK_SIZE, Integer.MAX_VALUE);
		return Math.max(1, (int) maxLength);
	}

	public synchronized void clear() {
		for (int i = 0; i < this.fPageTable.length; i++) {
			Chunk chunk = this.fPageTable[i];
			if (chunk == null) {
				continue;
			}
			chunk.fCacheIndex = -1;
			chunk.fDatabase.checkIfChunkReleased(chunk);
			this.fPageTable[i] = null;
		}
		this.fTableIsFull = false;
		this.fPointer = 0;
	}
}
