/*******************************************************************************
 * Copyright (c) 2015 Eclipse RDF4J contributors, Aduna, and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Distribution License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *******************************************************************************/
package org.eclipse.rdf4j.sail.nativerdf.btree;

import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.util.Arrays;
import java.util.BitSet;

import org.eclipse.rdf4j.common.io.ByteArrayUtil;
import org.eclipse.rdf4j.common.io.NioFile;

/**
 * List of allocated BTree nodes, persisted to a file on disk.
 *
 * @author Arjohn Kampman
 */
class AllocatedNodesList implements Closeable {

	/*-----------*
	 * Constants *
	 *-----------*/

	/**
	 * Magic number "Allocated Nodes File" to detect whether the file is actually an allocated nodes file. The first
	 * three bytes of the file should be equal to this magic number.
	 */
	private static final byte[] MAGIC_NUMBER = new byte[] { 'a', 'n', 'f' };

	/**
	 * The file format version number, stored as the fourth byte in allocated nodes files.
	 */
	private static final byte FILE_FORMAT_VERSION = 1;

	private static final int HEADER_LENGTH = MAGIC_NUMBER.length + 1;

	/*-----------*
	 * Variables *
	 *-----------*/

	/**
	 * The BTree associated with this allocated nodes list.
	 */
	private final BTree btree;

	/**
	 * The allocated nodes file.
	 */
	private final NioFile nioFile;

	/**
	 * Bit set recording which nodes have been allocated, using node IDs as index.
	 */
	private BitSet allocatedNodes;

	/**
	 * Flag indicating whether the set of allocated nodes has changed and needs to be written to file.
	 */
	private boolean needsSync = false;

	/**
	 * Flag indicating whether file writes should be forced to disk using {@link FileChannel#force(boolean)}.
	 */
	private final boolean forceSync;

	/*--------------*
	 * Constructors *
	 *--------------*/

	/**
	 * Creates a new AllocatedNodelist for the specified BTree.
	 */
	public AllocatedNodesList(File allocNodesFile, BTree btree, boolean forceSync) throws IOException {
		if (allocNodesFile == null) {
			throw new IllegalArgumentException("allocNodesFile must not be null");
		}
		if (btree == null) {
			throw new IllegalArgumentException("btree muts not be null");
		}

		this.nioFile = new NioFile(allocNodesFile);
		this.btree = btree;
		this.forceSync = forceSync;
	}

	/*---------*
	 * Methods *
	 *---------*/

	/**
	 * Gets the allocated nodes file.
	 */
	public File getFile() {
		return nioFile.getFile();
	}

	@Override
	public synchronized void close() throws IOException {
		close(true);
	}

	/**
	 * Deletes the allocated nodes file.
	 *
	 * @return <tt>true</tt> if the file was deleted.
	 */
	public synchronized boolean delete() throws IOException {
		close(false);
		return nioFile.delete();
	}

	public synchronized void close(boolean syncChanges) throws IOException {
		if (syncChanges) {
			sync();
		}
		allocatedNodes = null;
		needsSync = false;
		nioFile.close();
	}

	/**
	 * Writes any changes that are cached in memory to disk.
	 *
	 * @throws IOException
	 */
	public synchronized void sync() throws IOException {
		if (needsSync) {
			// Trim bit set
			BitSet bitSet = allocatedNodes;
			int bitSetLength = allocatedNodes.length();
			if (bitSetLength < allocatedNodes.size()) {
				bitSet = allocatedNodes.get(0, bitSetLength);
			}

			byte[] data = ByteArrayUtil.toByteArray(bitSet);

			// Write bit set to file
			nioFile.truncate(HEADER_LENGTH + data.length);
			nioFile.writeBytes(MAGIC_NUMBER, 0);
			nioFile.writeByte(FILE_FORMAT_VERSION, MAGIC_NUMBER.length);
			nioFile.writeBytes(data, HEADER_LENGTH);

			if (forceSync) {
				nioFile.force(false);
			}

			needsSync = false;
		}
	}

	private void scheduleSync() throws IOException {
		if (needsSync == false) {
			nioFile.truncate(0);
			needsSync = true;
		}
	}

	/**
	 * Clears the allocated nodes list.
	 *
	 * @throws IOException If an I/O error occurred.
	 */
	public synchronized void clear() throws IOException {
		if (allocatedNodes != null) {
			allocatedNodes.clear();
		} else {
			// bit set has not yet been initialized
			allocatedNodes = new BitSet();
		}

		scheduleSync();
	}

	public synchronized int allocateNode() throws IOException {
		initAllocatedNodes();

		int newNodeID = allocatedNodes.nextClearBit(1);
		allocatedNodes.set(newNodeID);

		scheduleSync();

		return newNodeID;
	}

	public synchronized void freeNode(int nodeID) throws IOException {
		initAllocatedNodes();
		allocatedNodes.clear(nodeID);
		scheduleSync();
	}

	/**
	 * Returns the highest allocated node ID.
	 */
	public synchronized int getMaxNodeID() throws IOException {
		initAllocatedNodes();
		return Math.max(0, allocatedNodes.length() - 1);
	}

	/**
	 * Returns the number of allocated nodes.
	 */
	public synchronized int getNodeCount() throws IOException {
		initAllocatedNodes();
		return allocatedNodes.cardinality();
	}

	private void initAllocatedNodes() throws IOException {
		if (allocatedNodes == null) {
			if (nioFile.size() > 0L) {
				loadAllocatedNodesInfo();
			} else {
				crawlAllocatedNodes();
			}
		}
	}

	private void loadAllocatedNodesInfo() throws IOException {
		byte[] data;

		if (nioFile.size() >= HEADER_LENGTH && Arrays.equals(MAGIC_NUMBER, nioFile.readBytes(0, MAGIC_NUMBER.length))) {
			byte version = nioFile.readByte(MAGIC_NUMBER.length);
			if (version > FILE_FORMAT_VERSION) {
				throw new IOException("Unable to read allocated nodes file; it uses a newer file format");
			} else if (version != FILE_FORMAT_VERSION) {
				throw new IOException("Unable to read allocated nodes file; invalid file format version: " + version);
			}

			data = nioFile.readBytes(HEADER_LENGTH, (int) (nioFile.size() - HEADER_LENGTH));
		} else {
			// assume header is missing (old file format)
			data = nioFile.readBytes(0, (int) nioFile.size());
			scheduleSync();
		}

		allocatedNodes = ByteArrayUtil.toBitSet(data);
	}

	private void crawlAllocatedNodes() throws IOException {
		allocatedNodes = new BitSet();

		Node rootNode = btree.readRootNode();
		if (rootNode != null) {
			crawlAllocatedNodes(rootNode);
		}

		scheduleSync();
	}

	private void crawlAllocatedNodes(Node node) throws IOException {
		try {
			allocatedNodes.set(node.getID());

			if (!node.isLeaf()) {
				for (int i = 0; i < node.getValueCount() + 1; i++) {
					crawlAllocatedNodes(node.getChildNode(i));
				}
			}

		} finally {
			node.release();
		}
	}
}
