/*
 * AppleCommander - An Apple ][ image utility.
 * Copyright (C) 2002-2022 by Robert Greene
 * robgreene at users.sourceforge.net
 *
 * This program is free software; you can redistribute it and/or modify it 
 * under the terms of the GNU General Public License as published by the 
 * Free Software Foundation; either version 2 of the License, or (at your 
 * option) any later version.
 *
 * 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.
 *
 * You should have received a copy of the GNU General Public License along 
 * with this program; if not, write to the Free Software Foundation, Inc., 
 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */
package com.webcodepro.applecommander.storage.os.prodos;

import com.webcodepro.applecommander.storage.*;
import com.webcodepro.applecommander.storage.physical.ImageOrder;
import com.webcodepro.applecommander.util.AppleUtil;
import com.webcodepro.applecommander.util.TextBundle;

import java.io.IOException;
import java.io.InputStream;
import java.util.*;

/**
 * Manages a disk that is in the ProDOS format.
 * <p>
 * Date created: Oct 3, 2002 11:45:25 PM
 * @author Rob Greene
 *
 * Changed at: Dec 1, 2017
 * @author Lisias Toledo
 */
public class ProdosFormatDisk extends FormattedDisk {
	private TextBundle textBundle = StorageBundle.getInstance();
	/**
	 * The location of the "next block" pointer in a directory entry.
	 * This is a 2-byte word (lo/hi) format.  $0000 is end of directory.
	 */
	private static final int NEXT_BLOCK_POINTER = 2;
	/**
	 * The location of the "previous block" pointer in a directory entry.
	 * This is a 2-byte word (lo/hi) format.  $0000 is start of directory.
	 */
	private static final int PREV_BLOCK_POINTER = 0;
	/**
	 * The Volume Directory block number.
	 */
	private static final int VOLUME_DIRECTORY_BLOCK = 2;
	/**
	 * A complete list of all known ProDOS filetypes.  Note that this
	 * list really cannot be complete, as there are multiple mappings per
	 * identifier in some cases - differentiated by AUXTYPE.  This is
	 * loaded via the static initializer.
	 */
	private static ProdosFileType[] fileTypes;
	/**
	 * This array of strings contains all filetypes.  This is lazy 
	 * initialized by getFiletypes.
	 */
	private static String[] filetypeStrings;
	/**
	 * Hold on to the volume directory header.
	 */
	private ProdosVolumeDirectoryHeader volumeHeader;

	/**
	 * This class holds filetype mappings.
	 */
	private static class ProdosFileType {
		private byte type;
		private String string;
		private boolean addressRequired;
		private boolean canCompile;
		public ProdosFileType(byte type, String string, boolean addressRequired, boolean canCompile) {
			this.type = type;
			this.string = string;
			this.addressRequired = addressRequired;
			this.canCompile = canCompile;
		}
		public byte getType() {
			return type;
		}
		public String getString() {
			return string;
		}
		public boolean needsAddress() {
			return addressRequired;
		}
		public boolean canCompile() {
			return canCompile;
		}
	}

	/**
	 * Use this inner interface for managing the disk usage data.
	 * This off-loads format-specific implementation to the implementing class.
	 */
	private class ProdosDiskUsage implements DiskUsage {
		private int location = -1;
		private transient byte[] data = null;
		public boolean hasNext() {
			return location == -1 || location < getVolumeHeader().getTotalBlocks() - 1;
		}
		public void next() {
			location++;
		}
		/**
		 * Get the free setting for the bitmap at the current location.
		 */
		public boolean isFree() {
			if (location == -1) {
				throw new IllegalArgumentException(StorageBundle.getInstance()
						.get("ProdosFormatDisk.InvalidDimensionError")); //$NON-NLS-1$
			}
			if (data == null) {
				data = readVolumeBitMap();
			}
			return isBlockFree(data, location);
		}
		public boolean isUsed() {
			return !isFree();
		}
	}

	/**
	 * Constructor for ProdosFormatDisk.
	 * @param filename
	 */
	public ProdosFormatDisk(String filename, ImageOrder imageOrder) {
		super(filename, imageOrder);
		volumeHeader = new ProdosVolumeDirectoryHeader(this);
	}
	
	/**
	 * Initialize all file types.
	 */
	static {
		fileTypes = new ProdosFileType[256];
		InputStream inputStream = 
			ProdosFormatDisk.class.getResourceAsStream("ProdosFileTypes.properties"); //$NON-NLS-1$
		Properties properties = new Properties();
		try {
			properties.load(inputStream);
			for (int i=0; i<256; i++) {
				String byt = AppleUtil.getFormattedByte(i).toLowerCase();
				String string = (String) properties.get("filetype." + byt); //$NON-NLS-1$
				if (string == null || string.length() == 0) {
					string = "$" + byt.toUpperCase(); //$NON-NLS-1$
				}
				boolean addressRequired = Boolean.valueOf((String) properties.get(
					"filetype." + byt + ".address")).booleanValue(); //$NON-NLS-1$ //$NON-NLS-2$
				boolean canCompile = Boolean.valueOf((String) properties.get(
					"filetype." + byt + ".compile")).booleanValue(); //$NON-NLS-1$ //$NON-NLS-2$
				fileTypes[i] = new ProdosFileType((byte)i, string, addressRequired, canCompile);
			}
		} catch (IOException ignored) {
			// Ignored
		}
	}

	/**
	 * Create a ProdosFormatDisk.
	 */
	public static ProdosFormatDisk[] create(String filename, String diskName, ImageOrder imageOrder) {
		ProdosFormatDisk disk = new ProdosFormatDisk(filename, imageOrder);
		disk.format();
		disk.setDiskName(diskName);
		return new ProdosFormatDisk[] { disk };
	}

	/**
	 * Identify the operating system format of this disk.
	 * @see com.webcodepro.applecommander.storage.FormattedDisk#getFormat()
	 */
	public String getFormat() {
		return textBundle.get("Prodos"); //$NON-NLS-1$
	}

	/**
	 * Create a FileEntry in the Volume Directory.
	 */
	public ProdosFileEntry createFile() throws DiskFullException {
		return createFile(volumeHeader);
	}
	
	/**
	 * Create a FileEntry in the given directory.
	 */
	public ProdosFileEntry createFile(ProdosCommonDirectoryHeader directory) 
		throws DiskFullException {
			
		int blockNumber = directory.getFileEntryBlock();
		int headerBlock = blockNumber;
		while (blockNumber != 0) {
			byte[] block = readBlock(blockNumber);
			int offset = 4;
			while (offset+ProdosCommonEntry.ENTRY_LENGTH < BLOCK_SIZE) {
				int value = AppleUtil.getUnsignedByte(block[offset]);
				if ((value & 0xf0) == 0) {
					ProdosFileEntry fileEntry = 
						new ProdosFileEntry(this, blockNumber, offset);
					fileEntry.setKeyPointer(0); //may have been recycled
					fileEntry.setCreationDate(new Date());
					fileEntry.setProdosVersion(0);
					fileEntry.setMinimumProdosVersion(0);
					fileEntry.setCanDestroy(true);
					fileEntry.setCanRead(true);
					fileEntry.setCanRename(true);
					fileEntry.setCanWrite(true);
					fileEntry.setSeedlingFile();
					fileEntry.setHeaderPointer(headerBlock);
					fileEntry.setFilename(textBundle.get("ProdosFormatDisk.Blank")); //$NON-NLS-1$
					directory.incrementFileCount();
					return fileEntry;
				}
				offset+= ProdosCommonEntry.ENTRY_LENGTH;
			}
			int nextBlockNumber = AppleUtil.getWordValue(block, NEXT_BLOCK_POINTER);
			if (nextBlockNumber == 0 && directory instanceof ProdosSubdirectoryHeader) {
				byte[] volumeBitmap = readVolumeBitMap();
				nextBlockNumber = findFreeBlock(volumeBitmap);
				setBlockUsed(volumeBitmap, nextBlockNumber);
				writeVolumeBitMap(volumeBitmap);
				byte[] oldBlock = readBlock(blockNumber);
				AppleUtil.setWordValue(oldBlock, NEXT_BLOCK_POINTER, nextBlockNumber);
				writeBlock(blockNumber, oldBlock);
				byte[] nextBlock = new byte[BLOCK_SIZE];
				AppleUtil.setWordValue(nextBlock, PREV_BLOCK_POINTER, blockNumber);
				writeBlock(nextBlockNumber, nextBlock);
				ProdosSubdirectoryHeader header = (ProdosSubdirectoryHeader) directory;
				int blockCount = header.getProdosDirectoryEntry().getBlocksUsed();
				blockCount++;
				header.getProdosDirectoryEntry().setBlocksUsed(blockCount);
				header.getProdosDirectoryEntry().setEofPosition(blockCount * BLOCK_SIZE);
			}
			blockNumber = nextBlockNumber;
		}
		if (directory instanceof ProdosSubdirectoryHeader) {
		    throw new DiskFullException(textBundle.get("ProdosFormatDisk.UnableToAllocateSpaceError"), this.getFilename()); //$NON-NLS-1$
		} else {
		    throw new DiskFullException(textBundle.get("ProdosFormatDisk.UnableToAllocateFileEntry"), this.getFilename());
	    }
	}

	/**
	 * Retrieve a list of files.
	 * @throws DiskException
	 * @see com.webcodepro.applecommander.storage.FormattedDisk#getFiles()
	 */
	public List<FileEntry> getFiles() throws DiskException {
		return getFiles(VOLUME_DIRECTORY_BLOCK);
	}

	/**
	 * Build a list of files, starting in the given block number.
	 * This works for the master as well as the subdirectories.
	 */		
	protected List<FileEntry> getFiles(int blockNumber) throws DiskException {
		List<FileEntry> files = new ArrayList<>();
		final Set<Integer> visits = new HashSet<>();
		while (blockNumber != 0) {
			// Prevents a recursive catalog crawling.
			if ( visits.contains(blockNumber)) throw new DiskCorruptException(this.getFilename(), DiskCorruptException.Kind.RECURSIVE_DIRECTORY_STRUCTURE, new ProdosBlockAddress(blockNumber));
			else visits.add(blockNumber);

			byte[] block = readBlock(blockNumber);
			int offset = 4;
			while (offset+ProdosCommonEntry.ENTRY_LENGTH < BLOCK_SIZE) {
				ProdosCommonEntry tester = 
					new ProdosCommonEntry(this, blockNumber, offset);
				if (tester.isVolumeHeader() || tester.isSubdirectoryHeader()) {
					// ignore it, we've already got it
				} else if (!tester.isEmpty()) {
					ProdosFileEntry fileEntry = 
						new ProdosFileEntry(this, blockNumber, offset);
					if (fileEntry.isDirectory()) {
						int keyPointer = fileEntry.getKeyPointer();
						ProdosDirectoryEntry directoryEntry =
							new ProdosDirectoryEntry(this, blockNumber, offset,
								new ProdosSubdirectoryHeader(this, keyPointer));
						files.add(directoryEntry);
					} else {
						files.add(fileEntry);
					}
				}
				offset+= ProdosCommonEntry.ENTRY_LENGTH;
			}
			blockNumber = AppleUtil.getWordValue(block, NEXT_BLOCK_POINTER);
		}
		return files;
	}
	
	/**
	 * Return the amount of free space in bytes.
	 * @see com.webcodepro.applecommander.storage.FormattedDisk#getFreeSpace()
	 */
	public int getFreeSpace() {
		return getFreeBlocks() * BLOCK_SIZE;
	}
	
	/**
	 * Return the number of free blocks on the disk.
	 */
	public int getFreeBlocks() {
		int freeBlocks = 0;
		int blocksToProcess = (volumeHeader.getTotalBlocks() + 4095) / 4096;
		int blockNumber = volumeHeader.getBitMapPointer();
		for (int ix=0; ix<blocksToProcess; ix++) {
			byte[] block = readBlock(blockNumber+ix);
			for (int byt=0; byt<block.length; byt++) {
				freeBlocks+= AppleUtil.getBitCount(block[byt]);
			}
		}
		return freeBlocks;
	}

	/**
	 * Return the amount of used space in bytes.
	 * @see com.webcodepro.applecommander.storage.FormattedDisk#getUsedSpace()
	 */
	public int getUsedSpace() {
		return getUsedBlocks() * BLOCK_SIZE;
	}
	
	/**
	 * Return the number of used blocks on the disk.
	 */
	public int getUsedBlocks() {
		return volumeHeader.getTotalBlocks() - getFreeBlocks();
	}

	/**
	 * Identify if additional directories can be created.  This
	 * may indicate that directories are not available to this
	 * operating system or simply that the disk image is "locked"
	 * to writing.
	 */
	public boolean canCreateDirectories() {
		return true;
	}
	
	/**
	 * Indicates if this disk image can create a file.
	 * If not, the reason may be as simple as it has not beem implemented
	 * to something specific about the disk.
	 */
	public boolean canCreateFile() {
		return true;
	}
	
	/**
	 * Return the name of the disk.
	 * @see com.webcodepro.applecommander.storage.FormattedDisk#getDiskName()
	 */
	public String getDiskName() {
		return "/" + volumeHeader.getVolumeName() + "/"; //$NON-NLS-1$ //$NON-NLS-2$
	}
	
	/**
	 * Set the name of the disk (volume name).
	 */
	public void setDiskName(String volumeName) {
		volumeHeader.setVolumeName(volumeName);
	}

	/**
	 * Get suggested dimensions for display of bitmap. There is no suggestion
	 * for a ProDOS volume - it is just a series of blocks.
	 */
	public int[] getBitmapDimensions() {
		return null;
	}

	/**
	 * Get the length of the bitmap.
	 */
	public int getBitmapLength() {
		return volumeHeader.getTotalBlocks();
	}
	
	/**
	 * Get the disk usage iterator.
	 */
	public DiskUsage getDiskUsage() {
		return new ProdosDiskUsage();
	}

	/**
	 * Get the labels to use in the bitmap.
	 */
	public String[] getBitmapLabels() {
		return new String[] { textBundle.get("Block") }; //$NON-NLS-1$
	}
	
	/**
	 * Get Pascal-specific disk information.
	 */
	public List<DiskInformation> getDiskInformation() {
		List<DiskInformation> list = super.getDiskInformation();
		list.add(new DiskInformation(textBundle.get("TotalBlocks"),  //$NON-NLS-1$
				volumeHeader.getTotalBlocks()));
		list.add(new DiskInformation(textBundle.get("FreeBlocks"),  //$NON-NLS-1$
				getFreeBlocks()));
		list.add(new DiskInformation(textBundle.get("UsedBlocks"),  //$NON-NLS-1$
				getUsedBlocks()));
		list.add(new DiskInformation(textBundle.get("ProdosFormatDisk.VolumeAccess"),  //$NON-NLS-1$
			(volumeHeader.canDestroy() ? textBundle.get("Destroy") : "") +  //$NON-NLS-1$//$NON-NLS-2$
			(volumeHeader.canRead() ? textBundle.get("Read") : "") +  //$NON-NLS-1$//$NON-NLS-2$
			(volumeHeader.canRename() ? textBundle.get("Rename") : "") +  //$NON-NLS-1$//$NON-NLS-2$
			(volumeHeader.canWrite() ? textBundle.get("Write") : "")));  //$NON-NLS-1$//$NON-NLS-2$
		list.add(new DiskInformation(textBundle.get("ProdosFormatDisk.BitmapBlockNumber"), volumeHeader.getBitMapPointer())); //$NON-NLS-1$
		list.add(new DiskInformation(textBundle.get("ProdosFormatDisk.CreationDate"), volumeHeader.getCreationDate())); //$NON-NLS-1$
		list.add(new DiskInformation(textBundle.get("ProdosFormatDisk.FileEntriesPerBlock"), volumeHeader.getEntriesPerBlock())); //$NON-NLS-1$
		list.add(new DiskInformation(textBundle.get("ProdosFormatDisk.FileEntryLength"), volumeHeader.getEntryLength())); //$NON-NLS-1$
		list.add(new DiskInformation(textBundle.get("ProdosFormatDisk.ActiveFilesInRootDirectory"), volumeHeader.getFileCount())); //$NON-NLS-1$
		list.add(new DiskInformation(textBundle.get("ProdosFormatDisk.MinimumVersionProdos"),  //$NON-NLS-1$
			volumeHeader.getMinimumProdosVersion()));
		list.add(new DiskInformation(textBundle.get("ProdosFormatDisk.CreationVersionProdos"), volumeHeader.getProdosVersion())); //$NON-NLS-1$
		list.add(new DiskInformation(textBundle.get("ProdosFormatDisk.VolumeName"), volumeHeader.getVolumeName())); //$NON-NLS-1$
		return list;
	}

	/**
	 * Get the standard file column header information.
	 * This default implementation is intended only for standard mode.
	 */
	public List<FileColumnHeader> getFileColumnHeaders(int displayMode) {
		List<FileColumnHeader> list = new ArrayList<>();
		switch (displayMode) {
			case FILE_DISPLAY_NATIVE:
				list.add(new FileColumnHeader(" ", 1, FileColumnHeader.ALIGN_CENTER, "locked"));
				list.add(new FileColumnHeader(textBundle.get("Name"), 15,
						FileColumnHeader.ALIGN_LEFT, "name"));
				list.add(new FileColumnHeader(textBundle.get("Filetype"), 8, 
						FileColumnHeader.ALIGN_CENTER, "type"));
				list.add(new FileColumnHeader(textBundle.get("Blocks"), 3,
						FileColumnHeader.ALIGN_RIGHT, "blocks"));
				list.add(new FileColumnHeader(textBundle.get("Modified"), 10,
						FileColumnHeader.ALIGN_CENTER, "modified"));
				list.add(new FileColumnHeader(
						textBundle.get("ProdosFormatDisk.Created"), 10,
						FileColumnHeader.ALIGN_CENTER, "created"));
				list.add(new FileColumnHeader(
						textBundle.get("ProdosFormatDisk.Length"), 10,
						FileColumnHeader.ALIGN_RIGHT, "size"));
				list.add(new FileColumnHeader(
						textBundle.get("ProdosFormatDisk.AuxType"), 8,
						FileColumnHeader.ALIGN_LEFT, "auxType"));
				break;
			case FILE_DISPLAY_DETAIL:
				list.add(new FileColumnHeader(" ", 1, FileColumnHeader.ALIGN_CENTER, "locked"));
				list.add(new FileColumnHeader(textBundle.get("Name"), 15,
						FileColumnHeader.ALIGN_LEFT, "name"));
				list.add(new FileColumnHeader(textBundle.get("DeletedQ"), 7,
						FileColumnHeader.ALIGN_CENTER, "deleted"));
				list.add(new FileColumnHeader(textBundle.get("ProdosFormatDisk.Permissions"), 8, 
						FileColumnHeader.ALIGN_LEFT, "permissions"));
				list.add(new FileColumnHeader(textBundle.get("Filetype"), 8, 
						FileColumnHeader.ALIGN_CENTER, "type"));
				list.add(new FileColumnHeader(textBundle.get("ProdosFormatDisk.DirectoryQ"), 9, 
						FileColumnHeader.ALIGN_CENTER, "directory"));
				list.add(new FileColumnHeader(textBundle.get("Blocks"), 3,
						FileColumnHeader.ALIGN_RIGHT, "blocks"));
				list.add(new FileColumnHeader(textBundle.get("Modified"), 16, 
						FileColumnHeader.ALIGN_CENTER, "modified"));
				list.add(new FileColumnHeader(textBundle.get("ProdosFormatDisk.Created"), 16, 
						FileColumnHeader.ALIGN_CENTER, "created"));
				list.add(new FileColumnHeader(textBundle.get("ProdosFormatDisk.Length"), 10, 
						FileColumnHeader.ALIGN_RIGHT, "size"));
				list.add(new FileColumnHeader(textBundle.get("ProdosFormatDisk.AuxType"), 8, 
						FileColumnHeader.ALIGN_LEFT, "auxType"));
				list.add(new FileColumnHeader(textBundle.get("ProdosFormatDisk.DirectoryHeader"), 5, 
						FileColumnHeader.ALIGN_RIGHT, "directory"));
				list.add(new FileColumnHeader(textBundle.get("ProdosFormatDisk.KeyBlock"), 5, 
						FileColumnHeader.ALIGN_RIGHT, "keyBlock"));
				list.add(new FileColumnHeader(textBundle.get("ProdosFormatDisk.KeyType"), 8, 
						FileColumnHeader.ALIGN_LEFT, "keyType"));
				list.add(new FileColumnHeader(textBundle.get("ProdosFormatDisk.Changed"), 5, 
						FileColumnHeader.ALIGN_CENTER, "changed"));
				list.add(new FileColumnHeader(textBundle.get("ProdosFormatDisk.MinimumProdosVersion"), 2, 
						FileColumnHeader.ALIGN_CENTER, "minimumProdosVersion"));
				list.add(new FileColumnHeader(textBundle.get("ProdosFormatDisk.ProdosVersion"), 2, 
						FileColumnHeader.ALIGN_CENTER, "prodosVersion"));
				break;
			default:	// FILE_DISPLAY_STANDARD
				list.addAll(super.getFileColumnHeaders(displayMode));
				break;
		}
		return list;
	}

	/**
	 * Indicates if this disk format supports "deleted" files.
	 */
	public boolean supportsDeletedFiles() {
		return true;
	}

	/**
	 * Indicates if this disk image can read data from a file.
	 */
	public boolean canReadFileData() {
		return true;
	}

	/**
	 * Identify if this disk format is capable of having directories.
	 * @see com.webcodepro.applecommander.storage.FormattedDisk#canHaveDirectories()
	 */
	public boolean canHaveDirectories() {
		return true;
	}

	/**
	 * Indicates if this disk image can write data to a file.
	 */
	public boolean canWriteFileData() {
		return true;
	}

	/**
	 * Indicates if this disk image can delete a file.
	 */
	public boolean canDeleteFile() {
		return true;
	}

	/**
	 * Get the data associated with the specified FileEntry.
	 * Note that this could return a 16MB file!  Sparse files are not treated specially.
	 */
	public byte[] getFileData(FileEntry fileEntry) {
		if ( !(fileEntry instanceof ProdosFileEntry)) {
			throw new IllegalArgumentException(textBundle.get("ProdosFormatDisk.MustHaveEntry")); //$NON-NLS-1$
		}
		ProdosFileEntry prodosEntry = (ProdosFileEntry) fileEntry;
		byte[] fileData = new byte[prodosEntry.getEofPosition()];
		if (prodosEntry.isSeedlingFile()) {
			byte[] blockData = readBlock(prodosEntry.getKeyPointer());
			System.arraycopy(blockData, 0, fileData, 0, prodosEntry.getEofPosition());
		} else if (prodosEntry.isSaplingFile()) {
			byte[] indexBlock = readBlock(prodosEntry.getKeyPointer());
			getIndexBlockData(fileData, indexBlock, 0);
		} else if (prodosEntry.isTreeFile()) {
			byte[] masterIndexBlock = readBlock(prodosEntry.getKeyPointer());
			int offset = 0;
			for (int i=0; i<0x100; i++) {
				int blockNumber = AppleUtil.getWordValue(masterIndexBlock[i], masterIndexBlock[i+0x100]);
				if (blockNumber > 0) {
					// FIXME - this may break sparse files!
					byte[] indexBlock = readBlock(blockNumber);
					offset= getIndexBlockData(fileData, indexBlock, offset);
				}
			}
		} else {
			throw new IllegalArgumentException(textBundle.get("ProdosFormatDisk.UnknownStorageType")); //$NON-NLS-1$
		}
		return fileData;
	}

	/**
	 * Free blocks used by a ProdosFileEntry.
	 */
	protected void freeBlocks(ProdosFileEntry prodosFileEntry) {
		byte[] bitmap = readVolumeBitMap();
		int block = prodosFileEntry.getKeyPointer();
		if (block == 0) return;	// new entry
		if (prodosFileEntry.isGEOSFile()) {
			// A GEOS file allocates another block, pointed to by the aux bytes.
			setBlockFree(bitmap,prodosFileEntry.getAuxiliaryType());
		}
		setBlockFree(bitmap,block);
		if (prodosFileEntry.isSaplingFile()) {
			freeBlocksInIndex(bitmap,block,false);
		} else if (prodosFileEntry.isTreeFile()) {
			byte[] masterIndexBlock = readBlock(block);
			for (int i=0; i<0x100; i++) {
				if (!prodosFileEntry.isGEOSFile() ||
				(prodosFileEntry.isGEOSFile() && (i < 0xfe)))
				{
					// As long as we're not deleting a GEOS file, delete all index entries.
					// GEOS uses records 0xfe and 0xff for space calculations, not pointers.
					int indexBlockNumber = AppleUtil.getWordValue(
							masterIndexBlock[i], masterIndexBlock[i+0x100]);
					if (indexBlockNumber > 0) freeBlocksInIndex(bitmap,indexBlockNumber,prodosFileEntry.isGEOSFile());
				}
			}
		}
		writeVolumeBitMap(bitmap);
	}

	/**
	 * Free the given index block and the data blocks it points to.
	 */
	private void freeBlocksInIndex(byte[] bitmap, int indexBlockNumber, boolean isGEOS) {
		setBlockFree(bitmap, indexBlockNumber);
		byte[] indexBlock = readBlock(indexBlockNumber);
		for (int i=0; i<0x100; i++) {
			if (!isGEOS ||
					(isGEOS && (i < 0xfe))) {
				// As long as we're not deleting a GEOS file, delete all entries.
				// GEOS uses records 0xfe and 0xff for space calculations, not pointers.
				int blockNumber = AppleUtil.getWordValue(indexBlock[i], indexBlock[i+0x100]);
				if (blockNumber > 0) setBlockFree(bitmap, blockNumber);
			}
		}
	}

	/**
	 * Read file data from the given index block.
	 * Note that block number 0 is an unused block.
	 */
	protected int getIndexBlockData(byte[] fileData, byte[] indexBlock, int offset) {
		for (int i=0; i<0x100; i++) {
			int blockNumber = AppleUtil.getWordValue(indexBlock[i], indexBlock[i+0x100]);
			byte[] blockData = readBlock(blockNumber);
			if (offset + blockData.length > fileData.length) { // end of file
				int bytesToCopy = fileData.length - offset;
				if (blockNumber != 0) System.arraycopy(blockData, 0, fileData, offset, bytesToCopy);
				offset+= bytesToCopy;
				break;
			}
			if (blockNumber != 0) System.arraycopy(blockData, 0, fileData, offset, blockData.length);
			offset+= blockData.length;
		}
		return offset;
	}

	/**
	 * Set the data associated with the specified ProdosFileEntry into sectors
	 * on the disk.  Automatically grows the filesystem structures from seedling
	 * to sapling to tree. 
	 */
	protected void setFileData(ProdosFileEntry fileEntry, byte[] fileData) 
		throws DiskFullException {

		if (fileEntry.isGEOSFile()) {
			// If this is a GEOS file, things are a bit different.
			setGEOSFileData(fileEntry, fileData);
		} else {
			// compute free space and see if the data will fit!
			int numberOfDataBlocks = (fileData.length + BLOCK_SIZE - 1) / BLOCK_SIZE;
			if (fileData.length == 0) numberOfDataBlocks = 1;
			int numberOfBlocks = numberOfDataBlocks;
			if (numberOfBlocks > 1) {
				numberOfBlocks+= ((numberOfDataBlocks-1) / 256) + 1;	// that's 128K
				if (numberOfDataBlocks > 256) {
					numberOfBlocks++;
				}
			}
			if (numberOfBlocks > getFreeBlocks() + fileEntry.getBlocksUsed()) {
				throw new DiskFullException(textBundle.
						format("ProdosFormatDisk.NotEnoughSpaceOnDiskError", //$NON-NLS-1$
								numberOfBlocks, getFreeBlocks())
						, this.getFilename());
			}
			// free "old" data and just rewrite stuff...
			freeBlocks(fileEntry);
			byte[] bitmap = readVolumeBitMap();
			int blockNumber = fileEntry.getKeyPointer();
			if (blockNumber == 0) {
				blockNumber = findFreeBlock(bitmap);
			}
			int indexBlockNumber = 0;
			byte[] indexBlockData = null;
			int masterIndexBlockNumber = 0;
			byte[] masterIndexBlockData = new byte[BLOCK_SIZE];
			int offset = 0;
			int blockCount = 0;
			// Need to let a file length go through once
			while ((offset < fileData.length) || ((fileData.length == 0) && (offset == 0))){
				if (blockCount > 0) blockNumber = findFreeBlock(bitmap);
				setBlockUsed(bitmap, blockNumber);
				blockCount++;
				byte[] blockData = new byte[BLOCK_SIZE];
				int length = Math.min(BLOCK_SIZE, fileData.length - offset);
				System.arraycopy(fileData,offset,blockData,0,length);
				writeBlock(blockNumber, blockData);
				if (numberOfDataBlocks > 1) {
					// growing to a tree file
					if (offset > 0 && (offset / BLOCK_SIZE) % 256 == 0) {
						if (masterIndexBlockNumber == 0) {
							masterIndexBlockNumber = findFreeBlock(bitmap);
							setBlockUsed(bitmap, masterIndexBlockNumber);
							blockCount++;
						}
						writeBlock(indexBlockNumber, indexBlockData);
						indexBlockData = null;
						indexBlockNumber = 0;
					}
					// new index block
					if (indexBlockData == null) {	// sapling files
						indexBlockNumber = findFreeBlock(bitmap);
						indexBlockData = new byte[BLOCK_SIZE];
						setBlockUsed(bitmap, indexBlockNumber);
						blockCount++;
						// This is only used for Tree files (but we always record it):
						int position = (offset / (BLOCK_SIZE * 256));
						byte low = (byte)(indexBlockNumber % 256);
						byte high = (byte)(indexBlockNumber / 256);
						masterIndexBlockData[position] = low;
						masterIndexBlockData[position + 0x100] = high;
					}
					// record last block position in index block
					int position = (offset / BLOCK_SIZE) % 256;
					byte low = (byte)(blockNumber % 256);
					byte high = (byte)(blockNumber / 256);
					indexBlockData[position] = low;
					indexBlockData[position + 0x100] = high;
				}
				offset+= BLOCK_SIZE;
			}
			if (numberOfDataBlocks == 1) {
				fileEntry.setKeyPointer(blockNumber);
				fileEntry.setSeedlingFile();
			} else if (numberOfDataBlocks <= 256) {
				writeBlock(indexBlockNumber, indexBlockData);
				fileEntry.setKeyPointer(indexBlockNumber);
				fileEntry.setSaplingFile();
			} else {
				writeBlock(indexBlockNumber, indexBlockData);
				writeBlock(masterIndexBlockNumber, masterIndexBlockData);
				fileEntry.setKeyPointer(masterIndexBlockNumber);
				fileEntry.setTreeFile();
			}
			fileEntry.setBlocksUsed(blockCount);
			fileEntry.setEofPosition(fileData.length);
			fileEntry.setLastModificationDate(new Date());
			writeVolumeBitMap(bitmap);
		}
	}
	
	/**
	 * Set the data associated with the specified ProdosFileEntry into sectors
	 * on the disk.  Automatically grows the filesystem structures from seedling
	 * to sapling to tree. 
	 */
	// TODO: the writing of a single fork can be factored out... it is very common to routines nearby.
	protected void setFileData(ProdosFileEntry fileEntry, byte[] dataFork, byte[] resourceFork) 
		throws DiskFullException {

		int dataLength = 0, resourceLength = 0;
		if (dataFork != null)
			dataLength = dataFork.length;
		if (resourceFork != null)
			resourceLength = resourceFork.length;
		// compute free space and see if the data will fit!
		int numberOfDataBlocks = (dataLength + BLOCK_SIZE - 1) / BLOCK_SIZE +
			(resourceLength + BLOCK_SIZE - 1) / BLOCK_SIZE;
		int numberOfBlocks = numberOfDataBlocks;
		if (numberOfBlocks > 1) {
			numberOfBlocks+= ((numberOfDataBlocks-1) / 256) + 1;	// that's 128K
			if (numberOfDataBlocks > 256) {
				numberOfBlocks++;
			}
		}
		if (numberOfBlocks > getFreeBlocks() + fileEntry.getBlocksUsed()) {
			throw new DiskFullException(textBundle.
					format("ProdosFormatDisk.NotEnoughSpaceOnDiskError", //$NON-NLS-1$
							numberOfBlocks, getFreeBlocks())
					, this.getFilename());
		}
		// free "old" data and just rewrite stuff...
		freeBlocks(fileEntry);
		byte[] bitmap = readVolumeBitMap();
		int blockNumber = fileEntry.getKeyPointer();
		if (blockNumber == 0) {
			blockNumber = findFreeBlock(bitmap);
		}
		int blockCount = 0;
		int extendedKeyBlockNumber = findFreeBlock(bitmap);
		setBlockUsed(bitmap, extendedKeyBlockNumber);
		byte[] extendedKeyBlockData = new byte[BLOCK_SIZE];
		int indexBlockNumber = 0;
		byte[] indexBlockData = null;
		int masterIndexBlockNumber = 0;
		byte[] masterIndexBlockData = new byte[BLOCK_SIZE];

		int offset = 0;
		numberOfDataBlocks = (dataLength + BLOCK_SIZE - 1) / BLOCK_SIZE ;
		while (offset < dataLength) {
			blockNumber = findFreeBlock(bitmap);
			setBlockUsed(bitmap, blockNumber);
			blockCount++;
			byte[] blockData = new byte[BLOCK_SIZE];
			int length = Math.min(BLOCK_SIZE, dataLength - offset);
			System.arraycopy(dataFork,offset,blockData,0,length);
			writeBlock(blockNumber, blockData);
			if (numberOfDataBlocks > 1) {
				// growing to a tree file
				if (offset > 0 && (offset / BLOCK_SIZE) % 256 == 0) {
					if (masterIndexBlockNumber == 0) {
						masterIndexBlockNumber = findFreeBlock(bitmap);
						setBlockUsed(bitmap, masterIndexBlockNumber);
						blockCount++;
					}
					writeBlock(indexBlockNumber, indexBlockData);
					indexBlockData = null;
					indexBlockNumber = 0;
				}
				// new index block
				if (indexBlockData == null) {	// sapling files
					indexBlockNumber = findFreeBlock(bitmap);
					indexBlockData = new byte[BLOCK_SIZE];
					setBlockUsed(bitmap, indexBlockNumber);
					blockCount++;
					// This is only used for Tree files (but we always record it):
					int position = (offset / (BLOCK_SIZE * 256));
					byte low = (byte)(indexBlockNumber % 256);
					byte high = (byte)(indexBlockNumber / 256);
					masterIndexBlockData[position] = low;
					masterIndexBlockData[position + 0x100] = high;
				}
				// record last block position in index block
				int position = (offset / BLOCK_SIZE) % 256;
				byte low = (byte)(blockNumber % 256);
				byte high = (byte)(blockNumber / 256);
				indexBlockData[position] = low;
				indexBlockData[position + 0x100] = high;
			}
			offset+= BLOCK_SIZE;
		}
		AppleUtil.setWordValue(extendedKeyBlockData,0x03,numberOfDataBlocks); // Set the number of blocks used
		AppleUtil.set3ByteValue(extendedKeyBlockData,0x05,	dataLength); // Set the number of bytes used
		if ((numberOfDataBlocks == 0) || (numberOfDataBlocks == 1)) {
			extendedKeyBlockData[0] = 1; // Set the seedling ProDOS storage type
			AppleUtil.setWordValue(extendedKeyBlockData,1,blockNumber); // Set the master block number
		} else if (numberOfDataBlocks <= 256) {
			writeBlock(indexBlockNumber, indexBlockData);
			AppleUtil.setWordValue(extendedKeyBlockData,1,indexBlockNumber); // Set the master block number
			extendedKeyBlockData[0] = 2; // Set the sapling ProDOS storage type
		} else {
			writeBlock(indexBlockNumber, indexBlockData);
			writeBlock(masterIndexBlockNumber, masterIndexBlockData);
			AppleUtil.setWordValue(extendedKeyBlockData,1,masterIndexBlockNumber); // Set the master block number
			extendedKeyBlockData[0] = 3; // Set the tree ProDOS storage type
		}
		blockCount++; // To account for extendedKeyBlock 

		offset = 0;
		indexBlockNumber = 0;
		indexBlockData = null;
		masterIndexBlockNumber = 0;
		numberOfDataBlocks = (resourceLength + BLOCK_SIZE - 1) / BLOCK_SIZE;
		while (offset < resourceLength) {
			if (blockCount > 0) blockNumber = findFreeBlock(bitmap);
			setBlockUsed(bitmap, blockNumber);
			blockCount++;
			byte[] blockData = new byte[BLOCK_SIZE];
			int length = Math.min(BLOCK_SIZE, resourceLength - offset);
			System.arraycopy(resourceFork,offset,blockData,0,length);
			writeBlock(blockNumber, blockData);
			if (numberOfDataBlocks > 1) {
				// growing to a tree file
				if (offset > 0 && (offset / BLOCK_SIZE) % 256 == 0) {
					if (masterIndexBlockNumber == 0) {
						masterIndexBlockNumber = findFreeBlock(bitmap);
						setBlockUsed(bitmap, masterIndexBlockNumber);
						blockCount++;
					}
					writeBlock(indexBlockNumber, indexBlockData);
					indexBlockData = null;
					indexBlockNumber = 0;
				}
				// new index block
				if (indexBlockData == null) {	// sapling files
					indexBlockNumber = findFreeBlock(bitmap);
					indexBlockData = new byte[BLOCK_SIZE];
					setBlockUsed(bitmap, indexBlockNumber);
					blockCount++;
					// This is only used for Tree files (but we always record it):
					int position = (offset / (BLOCK_SIZE * 256));
					byte low = (byte)(indexBlockNumber % 256);
					byte high = (byte)(indexBlockNumber / 256);
					masterIndexBlockData[position] = low;
					masterIndexBlockData[position + 0x100] = high;
				}
				// record last block position in index block
				int position = (offset / BLOCK_SIZE) % 256;
				byte low = (byte)(blockNumber % 256);
				byte high = (byte)(blockNumber / 256);
				indexBlockData[position] = low;
				indexBlockData[position + 0x100] = high;
			}
			offset+= BLOCK_SIZE;
		}
		AppleUtil.setWordValue(extendedKeyBlockData,0x103,numberOfDataBlocks); // Set the number of blocks used
		AppleUtil.set3ByteValue(extendedKeyBlockData,0x105,	resourceLength); // Set the number of bytes used
		if ((numberOfDataBlocks == 0) || (numberOfDataBlocks == 1)) {
			extendedKeyBlockData[0x100] = 1; // Set the seedling ProDOS storage type
			AppleUtil.setWordValue(extendedKeyBlockData,0x101,blockNumber); // Set the master block number
		} else if (numberOfDataBlocks <= 256) {
			writeBlock(indexBlockNumber, indexBlockData);
			AppleUtil.setWordValue(extendedKeyBlockData,0x101,indexBlockNumber); // Set the master block number
			extendedKeyBlockData[0x100] = 2; // Set the sapling ProDOS storage type
		} else {
			writeBlock(indexBlockNumber, indexBlockData);
			writeBlock(masterIndexBlockNumber, masterIndexBlockData);
			AppleUtil.setWordValue(extendedKeyBlockData,0x101,masterIndexBlockNumber); // Set the master block number
			extendedKeyBlockData[0x100] = 3; // Set the tree ProDOS storage type
		}
		writeBlock(extendedKeyBlockNumber, extendedKeyBlockData);

		fileEntry.setKeyPointer(extendedKeyBlockNumber);
		fileEntry.setBlocksUsed(blockCount);
		fileEntry.setEofPosition(dataLength+resourceLength);
		fileEntry.setLastModificationDate(new Date());
		writeVolumeBitMap(bitmap);
	}
	
	/**
	 * Set the data associated with the specified ProdosFileEntry into sectors
	 * on the disk.  Take GEOS file structures into account.
	 */
	protected void setGEOSFileData(ProdosFileEntry fileEntry, byte[] fileData) 
		throws DiskFullException {

		// compute free space and see if the data will fit!
		int numberOfDataBlocks = (fileData.length - 1) / BLOCK_SIZE;
		int numberOfBlocks = numberOfDataBlocks;
		numberOfBlocks+= ((numberOfDataBlocks-1) / 254) + 1;	// GEOS uses the last two blocks for eof calculations
		if (numberOfDataBlocks > 254) {
			numberOfBlocks++;
		}
		if (numberOfBlocks > getFreeBlocks() + fileEntry.getBlocksUsed()) {
			throw new DiskFullException(textBundle.
					format("ProdosFormatDisk.NotEnoughSpaceOnDiskError", //$NON-NLS-1$
					numberOfBlocks, getFreeBlocks())
					, this.getFilename());
		}
		// free "old" data and just rewrite stuff...
		freeBlocks(fileEntry);
		byte[] bitmap = readVolumeBitMap();

		// Place the first BLOCK_SIZE bytes of data in a block pointed to by the aux address.
		int headerBlockNumber = findFreeBlock(bitmap);
		byte[] headerData = new byte[BLOCK_SIZE];
		setBlockUsed(bitmap, headerBlockNumber);
		System.arraycopy(fileData,0,headerData,0,BLOCK_SIZE);
		writeBlock(headerBlockNumber, headerData);
		fileEntry.setAddress(headerBlockNumber);

		if (AppleUtil.getUnsignedByte(fileData[0x180]) >> 4 == 2) {
			setGEOSSaplingData(bitmap, fileEntry, fileData);
		} else {
			setGEOSTreeData(bitmap, fileEntry, fileData);
		}
		fileEntry.setGEOSMeta(headerData);
	}

	/**
	 * Set the GEOS "sapling" file data.
	 */
	protected void setGEOSSaplingData(byte[] bitmap, ProdosFileEntry fileEntry, byte[] fileData)
		throws DiskFullException {

		int indexBlockNumber = findFreeBlock(bitmap);
		setBlockUsed(bitmap, indexBlockNumber);
		byte[] indexBlockData = new byte[BLOCK_SIZE];
		int offset = BLOCK_SIZE;
		int blockNumber = 0;
		int blockCount = 1; // The header block counts for one
		while (offset < fileData.length) {
			blockNumber = findFreeBlock(bitmap);
			setBlockUsed(bitmap, blockNumber);
			blockCount++;
			byte[] blockData = new byte[BLOCK_SIZE];
			int length = Math.min(BLOCK_SIZE, fileData.length - offset);
			System.arraycopy(fileData,offset,blockData,0,length);
			writeBlock(blockNumber, blockData);
			// record last block position in index block
			int position = ((offset - BLOCK_SIZE) / BLOCK_SIZE) % 256;
			byte low = (byte)(blockNumber % 256);
			byte high = (byte)(blockNumber / 256);
			indexBlockData[position] = low;
			indexBlockData[position + 0x100] = high;
			offset+= BLOCK_SIZE;
		}
		indexBlockData[255] = (byte)((fileData.length - BLOCK_SIZE) % 256); // Lo file eof
		indexBlockData[511] = (byte)((fileData.length - BLOCK_SIZE) / 256); // Med file eof
		writeBlock(indexBlockNumber, indexBlockData);
		fileEntry.setKeyPointer(indexBlockNumber);
		fileEntry.setSaplingFile();
		fileEntry.setBlocksUsed(blockCount);
		fileEntry.setEofPosition(fileData.length - BLOCK_SIZE);
		fileEntry.setLastModificationDate(new Date());
		writeVolumeBitMap(bitmap);
	}

	/**
	 * Set the GEOS "tree" file data.
	 */
	protected void setGEOSTreeData(byte[] bitmap, ProdosFileEntry fileEntry, byte[] fileData)
		throws DiskFullException {

		int masterIndexBlockNumber = findFreeBlock(bitmap);
		setBlockUsed(bitmap, masterIndexBlockNumber);
		byte[] masterIndexBlockData = new byte[BLOCK_SIZE];
		int offset = BLOCK_SIZE;
		int blockCount = 2; // Start by counting the header block and master index
		int eofCount = 0;
		for (int masterIterator = 0; masterIterator < 254; masterIterator++) {
			if ((fileData[0x100+masterIterator] != 0xff) && (offset < fileData.length)){
				byte[] lengthData = new byte[BLOCK_SIZE];
				System.arraycopy(fileData,offset,lengthData,0,BLOCK_SIZE);
				offset += BLOCK_SIZE;
				int recordLength = AppleUtil.getUnsignedByte(lengthData[0xff]) 
				+ AppleUtil.getUnsignedByte(lengthData[0x1ff])*256;
				int indexBlockNumber = findFreeBlock(bitmap);
				setBlockUsed(bitmap, indexBlockNumber);
				blockCount +=1;
				byte[] indexBlockData = new byte[BLOCK_SIZE];
				int blockNumber = 0;
				int startingPoint = offset;
				while (offset < startingPoint + recordLength) {
					blockNumber = findFreeBlock(bitmap);
					setBlockUsed(bitmap, blockNumber);
					blockCount +=1;
					byte[] blockData = new byte[BLOCK_SIZE];
					int length = Math.min(BLOCK_SIZE, recordLength - offset + startingPoint);
					System.arraycopy(fileData,offset,blockData,0,length);
					writeBlock(blockNumber, blockData);
					eofCount += length;
					// record last block position in index block
					int position = ((offset-startingPoint) / BLOCK_SIZE) % 256;
					byte low = (byte)(blockNumber % 256);
					byte high = (byte)(blockNumber / 256);
					indexBlockData[position] = low;
					indexBlockData[position + 0x100] = high;
					offset+= BLOCK_SIZE;
				}
				indexBlockData[0xff] =   (byte) (recordLength & 0x0000ff);
				indexBlockData[0x1ff] = (byte)((recordLength & 0x00ff00) >> 8);
				indexBlockData[0x1fe] = (byte)((recordLength & 0xff0000) >> 16);
				writeBlock(indexBlockNumber, indexBlockData);
				byte low = (byte)(indexBlockNumber % 256);
				byte high = (byte)(indexBlockNumber / 256);
				masterIndexBlockData[masterIterator] = low;
				masterIndexBlockData[masterIterator + 0x100] = high;

			} else if (fileData[0x100+masterIterator] == 0xff) {
				masterIndexBlockData[masterIterator] = (byte)0xff;
				masterIndexBlockData[masterIterator+0x100] = (byte)0xff;
			}
		}
		masterIndexBlockData[0x0ff] = (byte) (eofCount & 0x0000ff);
		masterIndexBlockData[0x1ff] = (byte)((eofCount & 0x00ff00) >> 8);
		masterIndexBlockData[0x1fe] = (byte)((eofCount & 0xff0000) >> 16);
		writeBlock(masterIndexBlockNumber, masterIndexBlockData);
		fileEntry.setKeyPointer(masterIndexBlockNumber);
		fileEntry.setTreeFile();
		fileEntry.setBlocksUsed(blockCount);
		fileEntry.setEofPosition(eofCount);
		fileEntry.setLastModificationDate(new Date());
		writeVolumeBitMap(bitmap);
	}

	/**
	 * Locate a free block in the Volume Bitmap.
	 */
	protected int findFreeBlock(byte[] volumeBitmap) throws DiskFullException {
		int block = 1;
		int blocksOnDisk = getBitmapLength();
		while (block < blocksOnDisk) {
			if (isBlockFree(volumeBitmap,block)) {
				if ((block+1) * BLOCK_SIZE <= getPhysicalSize()) {
					return block;
				}
				throw new ProdosDiskSizeDoesNotMatchException(
					textBundle.get("ProdosFormatDisk.ProdosDiskSizeDoesNotMatchError") //$NON-NLS-1$
					, this.getFilename());
			}
			block++;
		}
		throw new DiskFullException(
			textBundle.get("ProdosFormatDisk.NoFreeBlockAvailableError") //$NON-NLS-1$
			, this.getFilename());
	}
	
	/**
	 * Read the Volume Bit Map.
	 */
	public byte[] readVolumeBitMap() {
		int volumeBitmapBlock = volumeHeader.getBitMapPointer();
		int volumeBitmapBlocks = volumeHeader.getTotalBlocks();
		int blocksToRead = (volumeBitmapBlocks / 4096) + 1;
		// Read in the entire volume bitmap:
		byte[] data = new byte[blocksToRead * BLOCK_SIZE];
		for (int i=0; i<blocksToRead; i++) {
			System.arraycopy(readBlock(volumeBitmapBlock+i), 0, data, i*BLOCK_SIZE, BLOCK_SIZE);
		}
		return data;
	}
	
	/**
	 * Write the Volume Bit Map.
	 */
	public void writeVolumeBitMap(byte[] data) {
		int volumeBitmapBlock = volumeHeader.getBitMapPointer();
		int volumeBitmapBlocks = volumeHeader.getTotalBlocks();
		int blocksToWrite = (volumeBitmapBlocks / 4096) + 1;
		if (data.length != blocksToWrite * BLOCK_SIZE) {
			throw new IllegalArgumentException(
				textBundle.get("ProdosFormatDisk.UnexpectedVolumeBitMapSizeError")); //$NON-NLS-1$
		}
		byte[] dataBlock = new byte[BLOCK_SIZE];
		for (int i=0; i<blocksToWrite; i++) {
			System.arraycopy(data, i*BLOCK_SIZE, dataBlock, 0, BLOCK_SIZE);
			writeBlock(volumeBitmapBlock+i, dataBlock);
		}
	}
	
	/**
	 * Determine if the specified block is free.
	 */
	public boolean isBlockFree(byte[] data, int blockNumber) {
		// Locate appropriate bit and check it:
		int byt = blockNumber / 8;
		int bit = 7 - (blockNumber % 8);
		boolean free = AppleUtil.isBitSet(data[byt], bit);
		return free;
	}

	/**
	 * Set if the specified block is free.
	 */
	public void setBlockFree(byte[] data, int blockNumber) {
		// Locate appropriate bit and check it:
		int byt = blockNumber / 8;
		int bit = 7 - (blockNumber % 8);
		data[byt] = AppleUtil.setBit(data[byt], bit);
	}
	
	/**
	 * Determine if the specified block is used.
	 */
	public boolean isBlockUsed(byte[] data, int blockNumber) {
		return !isBlockFree(data, blockNumber);
	}

	/**
	 * Set if the specified block is free.
	 */
	public void setBlockUsed(byte[] data, int blockNumber) {
		// Locate appropriate bit and check it:
		int byt = blockNumber / 8;
		int bit = 7 - (blockNumber % 8);
		data[byt] = AppleUtil.clearBit(data[byt], bit);
	}
	
	/**
	 * Format the ProDOS volume.
	 * @see com.webcodepro.applecommander.storage.FormattedDisk#format()
	 */
	public void format() {
		getImageOrder().format();
		writeBootCode();
		String volumeName = volumeHeader.getVolumeName();
		int totalBlocks = getPhysicalSize() / BLOCK_SIZE;
		int usedBlocks = (totalBlocks / 4096) + 7;
		// setup volume directory
		byte[] data = new byte[BLOCK_SIZE];
		for (int block=2; block<6; block++) {
			int nextBlock = (block < 5) ? block+1 : 0;
			int prevBlock = (block > 2) ? block-1 : 0;
			AppleUtil.setWordValue(data, 0, prevBlock);
			AppleUtil.setWordValue(data, 2, nextBlock);
			writeBlock(block, data);
		}
		// setup volume header information (each set will also save data)
		volumeHeader.setVolumeHeader();
		volumeHeader.setVolumeName(volumeName);
		volumeHeader.setCreationDate(new Date());
		volumeHeader.setProdosVersion(0);
		volumeHeader.setMinimumProdosVersion(0);
		volumeHeader.setHasChanged(true);
		volumeHeader.setCanDestroy(true);
		volumeHeader.setCanRead(true);
		volumeHeader.setCanRename(true);
		volumeHeader.setCanWrite(true);
		volumeHeader.setEntryLength();
		volumeHeader.setEntriesPerBlock();
		volumeHeader.setFileCount(0);
		volumeHeader.setBitMapPointer(6);
		volumeHeader.setTotalBlocks(totalBlocks);
		// setup bitmap usage
		byte[] bitmap = readVolumeBitMap();
		for (int block=0; block<totalBlocks; block++) {
			if (block < usedBlocks) {
				setBlockUsed(bitmap, block);
			} else {
				setBlockFree(bitmap, block);
			}
		}
		writeVolumeBitMap(bitmap);
	}

	/**
	 * Returns the logical disk number.  Returns a 0 to indicate no numbering.
	 */
	public int getLogicalDiskNumber() {
		return 0;
	}

	/**
	 * Returns a valid filename for the given filename.  ProDOS filenames
	 * have a maximum length of 15 characters, must start with a character
	 * and may contain characters (A-Z), digits (0-9), or the period (.).
	 */
	public String getSuggestedFilename(String filename) {
		StringBuffer newName = new StringBuffer();
		if (!Character.isLetter(filename.charAt(0))) {
			newName.append('A');
		}
		int i=0;
		while (newName.length() < 15 && i<filename.length()) {
			char ch = filename.charAt(i);
			if (Character.isLetterOrDigit(ch)) {
				newName.append(ch);
			} else {
			    newName.append('.');
			}
			i++;
		}
		return newName.toString().toUpperCase().trim();
	}

	/**
	 * Returns a valid filetype for the given filename.  The most simple
	 * format will just assume a filetype of binary.  This method is
	 * available for the interface to make an intelligent first guess
	 * as to the filetype.
	 */
	public String getSuggestedFiletype(String filename) {
		String filetype = "BIN"; //$NON-NLS-1$
		int pos = filename.lastIndexOf("."); //$NON-NLS-1$
		if (pos > 0) {
			String what = filename.substring(pos+1);
			ProdosFileType type = findFileType(what);
			if (type != null) {
				filetype = type.getString();
			}
		}
		return filetype;
	}

	/**
	 * Return the filetype of this file.  This will be three characters,
	 * according to ProDOS - a "$xx" if unknown.
	 */
	public static String getFiletype(int filetype) {
		ProdosFileType prodostype = fileTypes[filetype];
		return prodostype.getString();
	}

	/**
	 * Get the numerical filetype.
	 */
	public byte getFiletype(String filetype) {
		ProdosFileType type = findFileType(filetype);
		if (type != null) {
			return type.getType();
		}
		return 0x00;
	}
	
	/**
	 * Locate the associated ProdosFileType.
	 */
	static ProdosFileType findFileType(String filetype) {
		for (int i=0; i<fileTypes.length; i++) {
			if (filetype.equalsIgnoreCase(fileTypes[i].getString())) {
				return fileTypes[i];
			}
		}
		return null;
	}

	/**
	 * Utility method to indicate if this file type needs an address.
	 */
	public static boolean fileTypeNeedsAddress(String filetype) {
		ProdosFileType prodosFileType = findFileType(filetype);
		if (prodosFileType != null) {
			return prodosFileType.needsAddress();
		}
		return false;
	}

	/**
	 * Returns a list of possible file types.  Since the filetype is
	 * specific to each operating system, a simple String is used.
	 */
	public String[] getFiletypes() {
		if (filetypeStrings == null) {
			filetypeStrings = new String[fileTypes.length];
			for (int i = 0; i < fileTypes.length; i++) {
				filetypeStrings[i] = fileTypes[i].getString();
			}
		}
		return filetypeStrings;
	}

	/**
	 * Indicates if this filetype requires an address component.
	 */
	public boolean needsAddress(String filetype) {
		ProdosFileType fileType = findFileType(filetype);
		if (fileType != null) {
			return fileType.needsAddress();
		}
		return false;
	}

	/**
	 * Indicates if this filetype can be compiled.
	 */
	public boolean canCompile(String filetype) {
		ProdosFileType fileType = findFileType(filetype);
		if (fileType != null) {
			return fileType.canCompile();
		}
		return false;
	}

	/**
	 * Indicates if this FormattedDisk supports a disk map.
	 */	
	public boolean supportsDiskMap() {
		return true;
	}

	/**
	 * Change to a different ImageOrder.  Remains in ProDOS format but
	 * the underlying order can change.
	 * @see ImageOrder
	 */
	public void changeImageOrder(ImageOrder imageOrder) {
		AppleUtil.changeImageOrderByBlock(getImageOrder(), imageOrder);
		setImageOrder(imageOrder);
	}

	/**
	 * Writes the raw bytes into the file.  This bypasses any special formatting
	 * of the data (such as prepending the data with a length and/or an address).
	 * Typically, the FileEntry.setFileData method should be used. 
	 */
	public void setFileData(FileEntry fileEntry, byte[] fileData) throws DiskFullException {
		setFileData((ProdosFileEntry)fileEntry, fileData);
	}

	protected ProdosVolumeDirectoryHeader getVolumeHeader() {
		return volumeHeader;
	}

	/**
	 * Create a new DirectoryEntry.
	 * @see com.webcodepro.applecommander.storage.DirectoryEntry#createDirectory(String)
	 */
	public DirectoryEntry createDirectory(String name) throws DiskFullException {
		return createDirectory(getVolumeHeader(), name);
	}

	/**
	 * Create a new DirectoryEntry.
	 */
	public DirectoryEntry createDirectory(ProdosCommonDirectoryHeader directory, String name) throws DiskFullException {
		int blockNumber = directory.getFileEntryBlock();
		while (blockNumber != 0) {
			byte[] block = readBlock(blockNumber);
			int entryNum = 1; // Beneath ProDOS says this starts at zero, but subsequent correction says it starts at 1.
			int offset = 4;
			while (offset+ProdosCommonEntry.ENTRY_LENGTH < BLOCK_SIZE) {
				int value = AppleUtil.getUnsignedByte(block[offset]);
				if ((value & 0xf0) == 0) {
					// First, create a new block to contain our subdirectory
					byte[] volumeBitmap = readVolumeBitMap();
					int newDirBlockNumber = findFreeBlock(volumeBitmap);
					setBlockUsed(volumeBitmap, newDirBlockNumber);
					// Clean out the block - it may have been recycled, and control structures need to be gone
					byte[] cleanBlock = new byte[512];
					for (int i = 0;i<512;i++)
						cleanBlock[i] = 0;
					writeBlock(newDirBlockNumber, cleanBlock);
					writeVolumeBitMap(volumeBitmap);
					ProdosSubdirectoryHeader newHeader = new ProdosSubdirectoryHeader(this, newDirBlockNumber);
					ProdosFileEntry subdirEntry = (ProdosFileEntry)createFile(newHeader);
					subdirEntry.setFilename(name);
					newHeader.setHousekeeping();
					newHeader.setCreationDate(new Date());
					newHeader.setParentPointer(blockNumber);
					newHeader.setParentEntry(entryNum);
					newHeader.setParentEntryLength(ProdosCommonEntry.ENTRY_LENGTH);
					// Now, add an entry for this subdirectory 
					ProdosDirectoryEntry fileEntry = 
						new ProdosDirectoryEntry(this, blockNumber, offset, newHeader);
					fileEntry.setBlocksUsed(1); // Mark ourselves as the one block in use in this new subdirectory
					fileEntry.setEofPosition(BLOCK_SIZE);
					fileEntry.setKeyPointer(newDirBlockNumber);
					fileEntry.setCreationDate(new Date());
					fileEntry.setLastModificationDate(new Date());
					fileEntry.setProdosVersion(0);
					fileEntry.setMinimumProdosVersion(0);
					fileEntry.setCanDestroy(true);
					fileEntry.setCanRead(true);
					fileEntry.setCanRename(true);
					fileEntry.setCanWrite(true);
					fileEntry.setSubdirectory();
					fileEntry.setHeaderPointer(blockNumber);
					fileEntry.setFilename(name);
					fileEntry.setFiletype(0x0f); // Filetype = subdirectory
					directory.incrementFileCount();
					return fileEntry;
				}
				offset+= ProdosCommonEntry.ENTRY_LENGTH;
				entryNum++;
			}
			int nextBlockNumber = AppleUtil.getWordValue(block, NEXT_BLOCK_POINTER);
			if (nextBlockNumber == 0 && directory instanceof ProdosSubdirectoryHeader) {
				byte[] volumeBitmap = readVolumeBitMap();
				nextBlockNumber = findFreeBlock(volumeBitmap);
				setBlockUsed(volumeBitmap, nextBlockNumber);
				writeVolumeBitMap(volumeBitmap);
				byte[] oldBlock = readBlock(blockNumber);
				AppleUtil.setWordValue(oldBlock, NEXT_BLOCK_POINTER, nextBlockNumber);
				writeBlock(blockNumber, oldBlock);
				byte[] nextBlock = new byte[BLOCK_SIZE];
				AppleUtil.setWordValue(nextBlock, PREV_BLOCK_POINTER, blockNumber);
				writeBlock(nextBlockNumber, nextBlock);
				ProdosSubdirectoryHeader header = (ProdosSubdirectoryHeader) directory;
				int blockCount = header.getProdosDirectoryEntry().getBlocksUsed();
				blockCount++;
				header.getProdosDirectoryEntry().setBlocksUsed(blockCount);
				header.getProdosDirectoryEntry().setEofPosition(blockCount * BLOCK_SIZE);
			}
			blockNumber = nextBlockNumber;
		}
		if (directory instanceof ProdosSubdirectoryHeader) {
    		throw new DiskFullException(
    				textBundle.get("ProdosFormatDisk.UnableToAllocateSpaceError") //$NON-NLS-1$
    				, this.getFilename());
		} else {
		    throw new DiskFullException(textBundle.get("ProdosFormatDisk.UnableToAllocateFileEntry"), this.getFilename());
		}
	}

    /**
     * Gives an indication on how this disk's geometry should be handled.
     */
    public DiskGeometry getDiskGeometry() {
        return DiskGeometry.BLOCK;
    }

	/**
	 * Provides conversation from a given ProDOS file type since as it is common across
	 * many archiving tools.
	 */
	@Override
	public String toNativeFiletype(String prodosFiletype) {
		return prodosFiletype;
	};
	/**
	 * Provides conversation to a given ProDOS file type since as it is common across
	 * many archiving tools.
	 */
	@Override
	public String toProdosFiletype(String nativeFiletype) {
		return nativeFiletype;
	};
}
