/*-
 * Copyright (C) 2006-2009 Erik Larsson
 *
 * 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 3 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, see <http://www.gnu.org/licenses/>.
 */

package org.catacombae.hfs;

import java.util.LinkedList;
import org.catacombae.util.ObjectContainer;
import org.catacombae.hfs.types.hfscommon.CommonHFSExtentDescriptor;
import org.catacombae.hfs.types.hfscommon.CommonHFSVolumeHeader;
import org.catacombae.io.ReadableRandomAccessStream;

/**
 * @author <a href="https://catacombae.org" target="_top">Erik Larsson</a>
 */
public abstract class AllocationFile {
    protected final HFSVolume parentView;
    protected final ReadableRandomAccessStream allocationFileStream;

    protected AllocationFile(HFSVolume parentView,
            ReadableRandomAccessStream allocationFileStream) {
        this.parentView = parentView;
        this.allocationFileStream = allocationFileStream;

        if(this.parentView == null)
            throw new IllegalArgumentException("parentView == null");
    }

    /**
     * Tells if the allocation block addressed by <code>blockNumber</code> is
     * used or not.
     *
     * @param blockNumber the block number to probe for allocation status.
     * @return whether the block <code>blockNumber</code> is used (true) or not
     * (false).
     * @throws java.lang.IllegalArgumentException if <code>blockNumber</code>
     * is out of range.
     */
    public synchronized boolean isAllocationBlockUsed(long blockNumber)
            throws IllegalArgumentException {

        CommonHFSVolumeHeader vh = parentView.getVolumeHeader();
        return isAllocationBlockUsed(blockNumber, vh);
    }

    private synchronized boolean isAllocationBlockUsed(long blockNumber, CommonHFSVolumeHeader vh) {
        long numAllocationBlocks = vh.getTotalBlocks();
        if(blockNumber >= numAllocationBlocks)
            throw new IllegalArgumentException("Block number (" + blockNumber +
                    ") is beyond the highest block of the volume (" +
                    (numAllocationBlocks-1) + ").");


        long byteIndex = blockNumber / 8;
        allocationFileStream.seek(byteIndex);
        int currentByte = allocationFileStream.read();
        if(currentByte >= 0)
            return (currentByte & (1 << 7 - (blockNumber % 8))) != 0;
        else
            throw new RuntimeException("No data left in stream! allocationFileStream.getFilePointer()=" +
                    allocationFileStream.getFilePointer() + " allocationFileStream.length()=" +
                    allocationFileStream.length());
    }

    /**
     * Loops through the entire allocation file and counts the number of blocks
     * that are marked as free.
     *
     * @return the number of free blocks in the allocation file.
     */
    /*
    public synchronized long countFreeBlocks() {
        return countBlocks(false);
    }
    */

    /**
     * Loops through the entire allocation file and counts the number of blocks
     * that are marked as allocated.
     *
     * @return the number of allocated blocks in the allocation file.
     */
    /*
    public synchronized long countAllocatedBlocks() {
        return countBlocks(true);
    }
    */

    /**
     * Loops through the entire allocation file to count the number of used and free blocks on the
     * volume. The output is placed in two <code>ObjectContainer</code>s
     *
     * @param oFreeBlocks (optional) variable where the algorithm stores the free block count.
     * @param oUsedBlocks (optional) variable where the algorithm stores the used block count.
     * @param stop (optional) variable which can be set to abort the block counting process. Must
     * initally be set to <code>false</code> or no work will be done whatsoever.
     * @return the total number of allocation blocks on the volume.
     */
    public long countBlocks(ObjectContainer<Long> oFreeBlocks, ObjectContainer<Long> oUsedBlocks, ObjectContainer<Boolean> stop) {
        CommonHFSVolumeHeader vh = parentView.getVolumeHeader();
        byte[] currentBlock = new byte[128*1024];
        final long totalBlocks = vh.getTotalBlocks();
        long blockCount = 0;
        //long allocatedBlockCount = 0;
        long usedBlockCount = 0;
        //int blockValue = (usedBlocks?0x1:0x0);
        if(stop == null)
            stop = new ObjectContainer<Boolean>(false);

        //System.err.println("countBlocks(): totalBlocks=" + totalBlocks);
        //System.err.println("countBlocks(): allocationFileStream.length()=" + allocationFileStream.length());
        allocationFileStream.seek(0);
        while(blockCount < totalBlocks && !stop.o) {
            //System.err.println("countBlocks():   blockCount=" + blockCount);
            //System.err.println("countBlocks():   allocationFileStream.getFilePointer()=" + allocationFileStream.getFilePointer());
            //System.err.println("countBlocks():   =" + );

            //System.out.println("countBlocks():   Reading a blob (" + currentBlock.length + " bytes)...");
            int bytesRead = allocationFileStream.read(currentBlock);
            //System.out.println("countBlocks():   ..." + bytesRead + " bytes read.");

            if(bytesRead >= 0) {
                for(int i = 0; i < bytesRead && blockCount < totalBlocks && !stop.o; ++i) {
                    byte currentByte = currentBlock[i];
                    for(int j = 0; j < 8 && blockCount < totalBlocks && !stop.o; ++j) {
                        ++blockCount;
                        if(((currentByte >> (7 - j)) & 0x1) == 0x1)
                            ++usedBlockCount;
                    }
                }
            }
            else
                throw new RuntimeException("Could not read all blocks from allocation file!");
        }

        if(blockCount != totalBlocks)
            throw new RuntimeException("[INTERNAL ERROR] blockCount(" + blockCount +
                    ") != totalBlocks(" + totalBlocks + ")");

        if(oFreeBlocks != null)
            oFreeBlocks.o = blockCount-usedBlockCount;
        if(oUsedBlocks != null)
            oUsedBlocks.o = usedBlockCount;

        return totalBlocks;
    }

    /**
     * Creates an implementation specific extent descriptor from the given data.
     *
     * @param startBlock the first block number of the extent.
     * @param blockCount the number of blocks that the extent ranges over.
     * @return an implementation specific representation of an extent descriptor
     * created from the <code>startBlock</code> and <code>allocatedBlockCount</code>
     * parameters.
     * @throws java.lang.IllegalArgumentException if any of the values of <code>
     * startBlock</code> or <code>allocatedBlockCount</code> are out of range for the
     * implementation (16-bit signed integer value for HFS, 32-bit signed
     * integer value for HFS+).
     */
    protected abstract CommonHFSExtentDescriptor createExtentDescriptor(long startBlock,
            long blockCount) throws IllegalArgumentException;

    /**
     * Calculates an array of extents that are currently free, and that matches
     * the supplied file size. Every effort will be made to find the most
     * perfectly fitting match, i.e. with as few extents as possible.<br>
     * If no match is found (the volume does not have enough free blocks to hold
     * the specified size), <code>null</code> is returned.
     *
     * @param fileSize the size of the data region to be allocated, in bytes.
     * @return an array of descriptors of the extents where the data region
     * can be stored on disk.
     */
    public synchronized CommonHFSExtentDescriptor[] findFreeSpace(final long fileSize) {
        if(fileSize < 0)
            throw new IllegalArgumentException("Negative file size: " + fileSize);

        CommonHFSVolumeHeader vh = parentView.getVolumeHeader();
        final long blockSize = vh.getAllocationBlockSize();
        final long totalBlocks = vh.getTotalBlocks();
        final long blocksToAllocate = fileSize / blockSize +
                (fileSize % blockSize != 0 ? 1 : 0);
        long blocksLeft = blocksToAllocate;

        /*
         * Search for the closest matching region, i.e. the smallest region that
         * is larger than or equal to fileSize, or if none can be found, the
         * largest region that is smaller than fileSize.
         */

        // Loop through the entire allocation file
        ByteRegion closestMatchAbove = new ByteRegion();
        ByteRegion closestMatchBelow = new ByteRegion();
        LinkedList<ByteRegion> allocations =
                new LinkedList<ByteRegion>();

        while(blocksLeft > 0) {
            if(closestMatchAbove == null)
                closestMatchAbove = new ByteRegion();
            if(closestMatchBelow == null)
                closestMatchBelow = new ByteRegion();

            closestMatchAbove.reset();
            closestMatchBelow.reset();

            long regionStart = -1;
            for(int i = 0; i < totalBlocks; ++i) {
                // Check that we do not collide with any already allocated regions
                int j = 0;
                for(ByteRegion br : allocations) {
                    if(i >= br.offset && i < (br.offset+br.length))
                        break;
                    ++j;
                }
                if(j != allocations.size())
                    continue;

                if(!isAllocationBlockUsed(i, vh)) {
                    if(regionStart == -1)
                        regionStart = i;
                }
                else {
                    if(regionStart != -1) {
                        long length = i-regionStart;
                        if(length > blocksLeft) {
                            if(closestMatchAbove.length < 0 || closestMatchAbove.length > length) {
                                closestMatchAbove.offset = regionStart;
                                closestMatchAbove.length = length;
                            }
                        }
                        else if(length < blocksLeft) {
                            if(closestMatchBelow.length < 0 || closestMatchBelow.length < length) {
                                closestMatchBelow.offset = regionStart;
                                closestMatchBelow.length = length;
                            }
                        }
                        else {
                            closestMatchAbove.offset = regionStart;
                            closestMatchAbove.length = length;
                            break; // A perfect match, so we don't need to search more.
                        }
                    }
                    regionStart = -1;
                }
            }

            if(closestMatchAbove.isValid()) {
                // We found a fitting region for the rest of the data
                //result.add(createExtentDescriptor(closestMatchAbove.offset,
                //        blocksLeft));
                closestMatchAbove.length = blocksLeft;
                allocations.add(closestMatchAbove);

                blocksLeft = 0;
                closestMatchAbove = null; // It is taken
            }
            else if(closestMatchBelow.isValid()) {
                //result.add(createExtentDescriptor(closestMatchBelow.offset,
                //        closestMatchBelow.length));
                allocations.add(closestMatchBelow);

                blocksLeft -= closestMatchBelow.length;
                closestMatchBelow = null; // It is taken
            }
            else {
                // We're out of free blocks...
                return null;
            }
        }

        if(blocksLeft != 0)
            throw new RuntimeException("[INTERNAL ERROR] blocksLeft(" +
                    blocksLeft + ") != 0 [closestMatchAbove.offset=" +
                    closestMatchAbove.offset + ",closestMatchAbove.length=" +
                    closestMatchAbove.length + "]");

        CommonHFSExtentDescriptor[] result = new CommonHFSExtentDescriptor[allocations.size()];
        int i = 0;
        for(ByteRegion br : allocations)
            result[i++] = createExtentDescriptor(br.offset, br.length);

        return result;
    }

    public void close() {
        allocationFileStream.close();
    }

    private class ByteRegion {
        public long offset;
        public long length;

        public void reset() {
            offset = -1;
            length = -1;
        }

        public boolean isValid() {
            return offset > 0 && length > 0;
        }
    }

}
