/*
 * 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: 30-Nov-2006
 */
package uk.me.parabola.imgfmt.sys;

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

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

/**
 * An entry within a directory.  This holds its name and a list
 * of blocks that go to make up this file.
 *
 * A directory entry may take more than block in the file system.
 *
 * <p>All documentation seems to point to the block numbers having to be
 * contiguous, but seems strange so I shall experiment.
 *
 * <p>Entries are in blocks of 512 bytes, regardless of the block size.
 *
 * @author Steve Ratcliffe
 */
class Dirent implements DirectoryEntry {
	protected static final Logger log = Logger.getLogger(Dirent.class);

	// Constants.
	static final int MAX_FILE_LEN = 8;
	static final int MAX_EXT_LEN = 3;

	// Offsets
	static final int OFF_FILE_USED = 0x00;
	static final int OFF_NAME = 0x01;
	static final int OFF_EXT = 0x09;
	static final int OFF_FLAG = 0x10;
	static final int OFF_FILE_PART = 0x11;
	private static final int OFF_SIZE = 0x0c;

	// File names are a base+extension
	private String name;
	private String ext;

	// The file size - always use getSize() rather than this field.
	private int _size;

	// This may be used when writing the file
	private Sized sizeSource;

	private final BlockManager blockManager;

	// The block table holds all the blocks that belong to this file.  The
	// documentation suggests that block numbers are always contiguous.
	private final BlockTable blockTable;

	private boolean special;
	private static final int OFF_USED_FLAG = 0;
	private boolean initialized;

	Dirent(String name, BlockManager blockManager) {
		this.blockManager = blockManager;

		int dot = name.lastIndexOf('.');
		if (dot >= 0) {
			setName(name.substring(0, dot));
			setExt(name.substring(dot+1));
		} else
			throw new IllegalArgumentException("Filename did not have dot");

		blockTable = new BlockTable();

		sizeSource = () -> _size;
	}

	/**
	 * Write this entry out to disk.  Note that these are 512 bytes, regardless
	 * of the block size.
	 *
	 * @param file The file to write to.
	 * @throws IOException If writing fails for any reason.
	 */
	void sync(ImgChannel file) throws IOException {
		int ntables = blockTable.getNBlockTables();
		ByteBuffer buf = ByteBuffer.allocate(ENTRY_SIZE * ntables);
		buf.order(ByteOrder.LITTLE_ENDIAN);

		for (int part = 0; part < ntables; part++) {
			log.debug("position at part", part, "is", buf.position());
			
			buf.put((byte) 1);

			buf.put(Utils.toBytes(name, MAX_FILE_LEN, (byte) ' '));
			buf.put(Utils.toBytes(ext, MAX_EXT_LEN, (byte) ' '));

			// Size is only present in the first part
			if (part == 0) {
				log.debug("dirent", name, '.', ext, "size is going to", getSize());
				buf.putInt(getSize());
			} else {
				buf.putInt(0);
			}

			buf.put((byte) (special? 0x3: 0));
			buf.putChar((char) part);

			// Write out the allocation of blocks for this entry.
			buf.position(ENTRY_SIZE * part + 0x20);
			blockTable.writeTable(buf, part);
		}

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

	/**
	 * Get the file name.
	 *
	 * @return The file name.
	 */
	public String getName() {
		return name;
	}

	/**
	 * Get the file extension.
	 *
	 * @return The file extension.
	 */
	public String getExt() {
		return ext;
	}

	/**
	 * The full name is of the form 8+3 with a dot in between the name and
	 * extension.  The full name is used as the index in the directory.
	 *
	 * @return The full name.
	 */
	public String getFullName() {
		return name + '.' + ext;
	}

	/**
	 * Read in the block numbers from the given buffer.  If this is the first
	 * directory block for this file, then the size is set too.
	 *
	 * @param buf The data as read from the file.
	 */
	void initBlocks(ByteBuffer buf) {

		byte used = buf.get(OFF_USED_FLAG);
		if (used != 1)
			return;

		int part = buf.get(OFF_FILE_PART) & 0xff;
		if (part == 0 || (isSpecial() && part == 3))
			_size = buf.getInt(OFF_SIZE);

		blockTable.readTable(buf);
		initialized = true;
	}

	/**
	 * Get the file size.
	 *
	 * @return The size of the file in bytes.
	 */
	public int getSize() {
		return (int) sizeSource.getSize();
	}

	/**
	 * Calls to getSize() will use this to get the value.
	 */
	public void setSizeSource(Sized sizeSource) {
		this.sizeSource = sizeSource;
	}

	/**
	 * Set the file name.  The name should be exactly eight characters long
	 * and it is truncated or left padded with zeros to make this true.
	 *
	 * @param name The file name.
	 */
	private void setName(String name) {
		int len = name.length();
		if (len > MAX_FILE_LEN) {
			this.name = name.substring(0, 8);
		} else if (len < MAX_FILE_LEN) {
			StringBuilder sb = new StringBuilder();
			for (int i = 0; i < MAX_FILE_LEN - len; i++) {
				sb.append('0');
			}
			sb.append(name);
			this.name = sb.toString();
		} else
			this.name = name;
	}

	/**
	 * Set the file extension.  Can't be longer than three characters.
	 * @param ext The file extension.
	 */
	private void setExt(String ext) {
		log.debug("ext len", ext.length());
		if (ext.length() != MAX_EXT_LEN)
			throw new IllegalArgumentException("File extension is wrong size");
		this.ext = ext;
	}

	/**
	 * The number of blocks that the header covers.  The header includes
	 * the directory for the purposes of this routine.
	 *
	 * @return The total number of header basic blocks (blocks of 512 bytes).
	 */
	int numberHeaderBlocks() {
		return blockTable.getNBlockTables();
	}

	void setSize(int size) {
		if (log.isDebugEnabled())
			log.debug("setting size", getName(), getExt(), "to", size);
		this._size = size;
	}

	/**
	 * Add a block without increasing the size of the file.
	 *
	 * @param n The block number.
	 */
	void addBlock(int n) {
		blockTable.addBlock(n);
	}

	/**
	 * Set for the first directory entry that covers the header and directory
	 * itself.
	 *
	 * @param special Set to true to mark as the special first entry.
	 */
	public void setSpecial(boolean special) {
		this.special = special;
	}

	public boolean isSpecial() {
		return special;
	}

	/**
	 * Converts from a logical block to a physical block.  If the block does
	 * not exist then 0xffff will be returned.
	 *
	 * @param lblock The logical block in the file.
	 * @return The corresponding physical block in the filesystem.
	 */
	public int getPhysicalBlock(int lblock) {
		return blockTable.physFromLogical(lblock);
	}

	public BlockManager getBlockManager() {
		return blockManager;
	}

	protected void setInitialized(boolean initialized) {
		this.initialized = initialized;
	}
	
	protected boolean isInitialized() {
		return initialized;
	}
}
