/*
 * Copyright (C) 2006 Steve Ratcliffe
 * 
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2 as
 *  published by the Free Software Foundation.
 * 
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 * 
 * 
 * Author: Steve Ratcliffe
 * Create date: 03-Dec-2006
 */
package uk.me.parabola.imgfmt.sys;

import java.io.Closeable;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousCloseException;
import java.nio.channels.ClosedByInterruptException;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.FileChannel;
import java.nio.channels.NonReadableChannelException;
import java.nio.channels.NonWritableChannelException;

import uk.me.parabola.imgfmt.Sized;
import uk.me.parabola.imgfmt.fs.ImgChannel;
import uk.me.parabola.log.Logger;

/**
 * The internal representation of a file in the file system.  In use it
 * should only be referred to by the {@link ImgChannel} interface.
 *
 * @author Steve Ratcliffe
 */
public class FileNode implements ImgChannel, FileLink {
	private static final Logger log = Logger.getLogger(FileNode.class);

	private boolean open;
	private boolean writeable;
	private boolean readable;

	private final FileChannel file;
	private final BlockManager blockManager;
	private final Dirent dirent;

	// The position in this file
	private long position;

	private byte xorByte;
	private Closeable outerClose;

	/**
	 * Creates a new file in the file system.  You can treat this just like
	 * a regular file and write or read from it.
	 * Operations to two different files may not be interleaved although
	 * it may be possible to implement this.
	 *
	 * @param file The handle to the underlying file.
	 * @param dir The directory entry associated with this file.
	 * @param mode The mode "rw" for read and write etc.
	 */
	public FileNode(FileChannel file, Dirent dir, String mode)
	{
		this.file = file;
		this.dirent = dir;

		if (mode.indexOf('r') >= 0)
			readable = true;
		if (mode.indexOf('w') >= 0)
			writeable = true;
		if (!(readable || writeable))
			throw new IllegalArgumentException("File must be readable or writeable");

		blockManager = dir.getBlockManager();
		if (blockManager == null)
			throw new IllegalArgumentException("no file system supplied");

		open = true;
	}

	/**
	 * Closes this channel.
	 * <p/>
	 * <p> After a channel is closed, any further attempt to invoke I/O
	 * operations upon it will cause a {@link ClosedChannelException} to be
	 * thrown.
	 * <p/>
	 * <p> If this channel is already closed then invoking this method has no
	 * effect.
	 * <p/>
	 * <p> This method may be invoked at any time.  If some other thread has
	 * already invoked it, however, then another invocation will block until
	 * the first invocation is complete, after which it will return without
	 * effect. </p>
	 *
	 * @throws IOException If an I/O error occurs
	 */
	public void close() throws IOException {
		if (!open)
			return;

		if (writeable && outerClose != null)
			outerClose.close();

		sync();

		open = false;
		readable = false;
		writeable = false;
	}

	/**
	 * Tells whether or not this channel is open.  </p>
	 *
	 * @return <tt>true</tt> if, and only if, this channel is open
	 */
	public boolean isOpen() {
		return open;
	}

	/**
	 * Reads a sequence of bytes from this channel into the given buffer.
	 *
	 * @param dst The buffer into which bytes are to be transferred
	 *
	 * @return The number of bytes read, possibly zero, or <tt>-1</tt> if the
	 * channel has reached end-of-stream
	 *
	 * @throws NonReadableChannelException If this channel was not opened for reading
	 * @throws ClosedChannelException If this channel is closed
	 * @throws AsynchronousCloseException If another thread closes this channel
	 * while the read operation is in progress
	 * @throws ClosedByInterruptException If another thread interrupts the
	 * current thread while the read operation is in progress, thereby closing
	 * the channel and setting the current thread's interrupt status
	 * @throws IOException If some other I/O error occurs
	 */
	public int read(ByteBuffer dst) throws IOException {
		if (!open)
			throw new ClosedChannelException();
		if (!readable)
			throw new NonReadableChannelException();

		int blockSize = blockManager.getBlockSize();

		long size = dst.remaining();
		long fileSize = dirent.getSize();
		if (position >= fileSize)
			return -1;
		size = Math.min(size, fileSize - position);

		int totalRead = 0;

		while (size > 0) {
			// Tet the logical block number, as we see it in our file.
			int lblock = (int) (position / blockSize);

			// Get the physical block number, the actual block number in
			// the underlying file.
			int pblock = dirent.getPhysicalBlock(lblock);
			if (pblock == 0xffff) {
				// We are at the end of the file.
				log.debug("at eof");
				break;
			}

			// Position the underlying file
			int off = (int) (position - lblock*blockSize);
			file.position((long) pblock * blockSize + off);

			int n = (int) size;
			if (n > blockSize)
				n = blockSize;

			if (off != 0)
				n = Math.min(n, blockSize - off);

			dst.limit(dst.position() + n);

			int pos = dst.position();
			int nr = file.read(dst);
			if (nr == -1)
				return -1;
			if (nr == 0)
				throw new IOException("Read nothing");

			if(xorByte != 0) {
				byte[] bufBytes = dst.array();
				for(int i = pos + n - 1; i >= pos; --i)
					bufBytes[i] ^= xorByte;
			}

			// Update the file positions
			size -= nr;
			position += nr;
			totalRead += nr;
		}

		log.debug("read ret", totalRead);
		return totalRead;
	}

	/**
	 * Writes a sequence of bytes to this channel from the given buffer.
	 * <p/>
	 * <p> An attempt is made to write up to <i>r</i> bytes to the channel,
	 * where <i>r</i> is the number of bytes remaining in the buffer, that is,
	 * <tt>dst.remaining()</tt>, at the moment this method is invoked.
	 * <p>The logical block has to be converted to a physical block in the
	 * underlying file.
	 *
	 * @param src The buffer from which bytes are to be retrieved
	 * @return The number of bytes written, possibly zero
	 * @throws NonWritableChannelException
	 *                             If this channel was not opened for writing
	 * @throws ClosedChannelException
	 *                             If this channel is closed
	 * @throws IOException If some other I/O error occurs
	 */
	public int write(ByteBuffer src) throws IOException {
		if (!open)
			throw new ClosedChannelException();

		int blockSize = blockManager.getBlockSize();

		// Get the size of this write
		int size = src.remaining();

		// Loop over each block, this is to support the case (which we may
		// not implement) of non-contiguous blocks.

		int totalWritten = 0;
		while (size > 0) {
			// Get the logical block, ie the block as we see it in our file.
			int lblock = (int) (position/blockSize);

			// First need to allocate enough blocks for this write. First check
			// if the block exists already
			int pblock = dirent.getPhysicalBlock(lblock);
			log.debug("lblock / pblock", lblock, '/', pblock);
			if (pblock == 0xffff) {
				log.debug("allocating new block");
				pblock = blockManager.allocate();
				dirent.addBlock(pblock);
			}

			// Position the underlying file, so that it is in the correct place.
			int off = (int) (position - lblock*blockSize);
			file.position((long) pblock * blockSize + off);

			int n = size;
			if (n > blockSize)
				n = blockSize;

			if (off != 0)
				n = Math.min(n, blockSize - off);

			src.limit(src.position() + n);

			// Write to the underlying file.
			int nw = file.write(src);
			if (nw == 0)
				throw new IOException("Wrote nothing");

			// Update the file positions
			size -= nw;
			position += nw;
			totalWritten += nw;

			// Update file size.
			if (position > dirent.getSize())
				dirent.setSize((int) position);
		}

		return totalWritten;
	}

	public long position() {
		return position;
	}

	public void position(long pos) {
		int blockSize = blockManager.getBlockSize();

		while (pos > position) {
			long lblock = position / blockSize;
			int pblock = dirent.getPhysicalBlock((int) lblock);

			if (pblock == 0xffff) {
				if (writeable) {
					log.debug("setting position allocating new block", lblock);
					pblock = blockManager.allocate();
					dirent.addBlock(pblock);
				}
			}
			position = (lblock+1) * blockSize;
		}

		this.position = pos;
	}

	public void link(Sized source, Closeable closeable) {
		dirent.setSizeSource(source);
		outerClose = closeable;
	}

	public long getSize() {
		return dirent.getSize();
	}

	/**
	 * Write out any unsaved data to disk.
	 *
	 * @throws IOException If there is an error writing to disk.
	 */
	private void sync() throws IOException {
		if (!writeable)
			return;

		// Ensure that a complete block is written out.
		int bs = blockManager.getBlockSize();
		long rem = bs - (file.position() % bs);

		ByteBuffer buf = ByteBuffer.allocate(blockManager.getBlockSize());

		// Complete any partial block.
		for (int i = 0; i < rem; i++)
			buf.put((byte) 0);

		buf.flip();
		file.write(buf);
	}

	public void setXorByte(byte xorByte) {
		this.xorByte = xorByte;
	}

	public String toString() {
		return String.format("%s %d", dirent.getFullName(), getSize());
	}
}
