/*******************************************************************************
 * Copyright (c) 2010 Wind River Systems, 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:
 *     Markus Schorn - initial API and implementation
 *     Sergey Prigogin (Google)
 *******************************************************************************/ 
package org.eclipse.cdt.internal.core.parser.scanner;

import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.List;

/**
 * Implementation of char array for a file referencing content via soft references.
 * Because of bug 320157 we need to deal with chunks of different length.
 */
public abstract class LazyCharArray extends AbstractCharArray {
	private final static int CHUNK_BITS= 16;  // 2^16 == 64K
	public final static int CHUNK_SIZE= 1 << CHUNK_BITS;

	protected static class Chunk {
		final int fCharOffset;
		final int fCharEndOffset;
		final long fSourceOffset;
		final long fSourceEndOffset;
		private SoftReference<char[]> fCharsReference;

		private Chunk(long sourceOffset, long sourceEndOffset, int charOffset, char[] chars) {
			fCharOffset= charOffset;
			fCharEndOffset= charOffset+ chars.length;
			fSourceOffset= sourceOffset;
			fSourceEndOffset= sourceEndOffset;
			fCharsReference= new SoftReference<char[]>(chars);
		}
	}

	private int fLength= -1;
	private List<Chunk> fChunks= new ArrayList<Chunk>();
	private StreamHasher fHasher;
	private long fHash64;
	// Make a reference to the currently used char[], such that it is not collected.
	private char[] fCurrentChars;

	protected LazyCharArray() {
		fHasher = new StreamHasher();
	}

	@Override
	public final int tryGetLength() {
		return fLength;
	}

	@Override
	public final int getLength() {
		readAllChunks();
		return fLength;
	}

	@Override
	public final boolean isValidOffset(int offset) {
		if (offset < 0)
			return false;

		if (fLength >= 0)
			return offset < fLength;

		return getChunkForOffset(offset) != null;
	}

	@Override
	public long getContentsHash() {
		if (fHasher != null) {
			readAllChunks();
			fHash64 = fHasher.computeHash();
			fHasher = null;
		}
		return fHash64;
	}

	@Override
	public final char get(int offset) {
		Chunk chunk= getChunkForOffset(offset);
		if (chunk != null) {
			return getChunkData(chunk)[offset - chunk.fCharOffset];
		}
		return 0;
	}

	@Override
	public final void arraycopy(int offset, char[] destination, int destinationPos, int length) {
		final Chunk chunk= getChunkForOffset(offset);
		final int offsetInChunk= offset - chunk.fCharOffset;
		final char[] data= getChunkData(chunk);
		final int maxLenInChunk = data.length - offsetInChunk;
		if (length <= maxLenInChunk) {
			System.arraycopy(data, offsetInChunk, destination, destinationPos, length);
		} else {
			System.arraycopy(data, offsetInChunk, destination, destinationPos, maxLenInChunk);
			arraycopy(offset + maxLenInChunk, destination, destinationPos + maxLenInChunk, length - maxLenInChunk);
		}
	}

	private void readAllChunks() {
		if (fLength < 0) {
			getChunkForOffset(Integer.MAX_VALUE);
		}
	}

	private Chunk getChunkForOffset(int offset) {
		int minChunkNumber= offset >> CHUNK_BITS;
		for(;;) {
			Chunk chunk= getChunkByNumber(minChunkNumber);
			if (chunk == null) 
				return null;
			
			if (offset < chunk.fCharEndOffset) {
				return chunk;
			}
			minChunkNumber++;
		}
	}

	private Chunk getChunkByNumber(int chunkNumber) {
		final int chunkCount = fChunks.size();
		if (chunkNumber < chunkCount)
			return fChunks.get(chunkNumber);

		if (fLength >=0)
			return null;

		return createChunk(chunkNumber);
	}

	/**
	 * Called when a chunk is requested for the first time. There is no
	 * need to override this method.
	 */
	protected Chunk createChunk(int chunkNumber) {
		for (int i = fChunks.size(); i <= chunkNumber; i++) {
			Chunk chunk= nextChunk();
			if (chunk == null) {
				final int chunkCount= fChunks.size();
				fLength= chunkCount == 0 ? 0 : fChunks.get(chunkCount-1).fCharEndOffset;
				break;
			}
			if (fHasher != null) {
				final char[] chunkData = getChunkData(chunk);
				fHasher.addChunk(chunkData);
			}
			fChunks.add(chunk);
		}

		if (chunkNumber < fChunks.size())
			return fChunks.get(chunkNumber);

		return null;
	}
	
	/**
	 * Creates a new chunk.
	 */
	protected Chunk newChunk(long sourceOffset, long sourceEndOffset, int charOffset, char[] chars) {
		fCurrentChars= chars;
		return new Chunk(sourceOffset, sourceEndOffset, charOffset, chars);
	}

	/**
	 * Read the next chunk from the input.
	 */
	protected abstract Chunk nextChunk();

	private char[] getChunkData(Chunk chunk) {
		char[] data= chunk.fCharsReference.get();
		if (data == null) {
			data= new char[chunk.fCharEndOffset - chunk.fCharOffset];
			rereadChunkData(chunk, data);
			chunk.fCharsReference= new SoftReference<char[]>(data);
		}
		return fCurrentChars= data;
	}

	/**
	 * Reread the data for the chunk. In case the source range no longer (fully) exists,
	 * read as much as possible.
	 */
	protected abstract void rereadChunkData(Chunk chunk, char[] data);

	/** 
	 * For testing purposes: Simulates that all the data gets collected.
	 */
	public void testClearData() {
		for (Chunk chunk : fChunks) {
			chunk.fCharsReference= new SoftReference<char[]>(null);
		}
		if (fCurrentChars != null)
			fCurrentChars= null;
	}
}
