/*
 * $RCSfile: StructuredStorage.java,v $
 *
 * Copyright (c) 2005 Sun Microsystems, Inc. All rights reserved.
 *
 * Use is subject to license terms.
 *
 * $Revision: 1.1 $
 * $Date: 2005/02/11 04:55:41 $
 * $State: Exp $
 */
package com.lightcrafts.media.jai.codecimpl.fpx;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.StringTokenizer;
import com.lightcrafts.media.jai.codec.ByteArraySeekableStream;
import com.lightcrafts.media.jai.codec.FileSeekableStream;
import com.lightcrafts.media.jai.codec.SeekableStream;
import com.lightcrafts.media.jai.codec.SegmentedSeekableStream;

//
// NOTE -- all 'long' variables are really at most 32 bits,
// corresponding to Microsoft 'ULONG' variables.
//

// Temporary (?) assumptions:
//
// All streams, including the ministream, are shorter than 2GB (size < 2GB)
//
// There are < 2^31 directory entries (#streams < 2^31)
//

class SSDirectoryEntry {

    int index;
    String name;
    long size;
    long startSector;
    long SIDLeftSibling;
    long SIDRightSibling;
    long SIDChild;

    public SSDirectoryEntry(int index,
                            String name,
                            long size,
                            long startSector,
                            long SIDLeftSibling,
                            long SIDRightSibling,
                            long SIDChild) {
        this.name = name;
        this.index = index;
        this.size = size;
        this.startSector = startSector;
        this.SIDLeftSibling = SIDLeftSibling;
        this.SIDRightSibling = SIDRightSibling;
        this.SIDChild = SIDChild;

        // System.out.println("Got a directory entry named " + name +
        //                    " (index " + index + ")");
        // System.out.println("Start sector = " + startSector);
    }

    public String getName() {
        return name;
    }

    public long getSize() {
        return size;
    }

    public long getStartSector() {
        return startSector;
    }

    public long getSIDLeftSibling() {
        return SIDLeftSibling;
    }

    public long getSIDRightSibling() {
        return SIDRightSibling;
    }

    public long getSIDChild() {
        return SIDChild;
    }
}

public class StructuredStorage {

    // Chain terminator
    private static final long FAT_ENDOFCHAIN = 0xFFFFFFFEL;

    // Free sector
    private static final long FAT_FREESECT   = 0xFFFFFFFFL;

    SeekableStream file;

    // Header fields
    private int sectorShift; // ULONG -- must be between 1 and 31
    private int miniSectorShift;
    private long csectFat;
    private long sectDirStart;
    private long miniSectorCutoff;
    private long sectMiniFatStart;
    private long csectMiniFat;
    private long sectDifStart;
    private long csectDif;
    private long[] sectFat;

    // FAT, MiniFAT, and ministream in unrolled format
    // private long[] FAT; // ULONG -- only 2G entries max
    private long[] MINIFAT; // ULONG -- only 2G entries max
    private SSDirectoryEntry[] DIR;

    private SeekableStream miniStream;
    private SeekableStream FATStream;

    // The index of the current directory
    long cwdIndex = -1L;

    public StructuredStorage(SeekableStream file) throws IOException {
        this.file = file;

        // Read fields from the header
        getHeader();

        // Read the FAT
        getFat();

        // Read the MiniFAT
        getMiniFat();

        // Read the directory
        getDirectory();

        // Read the MiniStream
        getMiniStream();
    }

    private void getHeader() throws IOException {
        file.seek(0x1e);
        this.sectorShift = file.readUnsignedShortLE();
        // System.out.println("sectorShift = " + sectorShift);

        file.seek(0x20);
        this.miniSectorShift = file.readUnsignedShortLE();
        // System.out.println("miniSectorShift = " + miniSectorShift);

        file.seek(0x2c);
        this.csectFat = file.readUnsignedIntLE();
        // System.out.println("csectFat = " + csectFat);

        file.seek(0x30);
        this.sectDirStart = file.readUnsignedIntLE();
        // System.out.println("sectDirStart = " + sectDirStart);

        file.seek(0x38);
        this.miniSectorCutoff = file.readUnsignedIntLE();
        // System.out.println("miniSectorCutoff = " + miniSectorCutoff);

        file.seek(0x3c);
        this.sectMiniFatStart = file.readUnsignedIntLE();
        // System.out.println("sectMiniFatStart = " + sectMiniFatStart);

        file.seek(0x40);
        this.csectMiniFat = file.readUnsignedIntLE();
        // System.out.println("csectMiniFat = " + csectMiniFat);

        file.seek(0x44);
        this.sectDifStart = file.readUnsignedIntLE();
        // System.out.println("sectDifStart = " + sectDifStart);

        file.seek(0x48);
        this.csectDif = file.readUnsignedIntLE();
        // System.out.println("csectDif = " + csectDif);

        this.sectFat = new long[109];
        file.seek(0x4c);
        for (int i = 0; i < 109; i++) {
            this.sectFat[i] = file.readUnsignedIntLE();
        }
    }

    private void getFat() throws IOException {
        int size = getSectorSize();
        int sectsPerFat = size/4;
        int fatsPerDif = size/4 - 1;
        // int index = 0;

        // this.FAT =
        // new long[(int)((csectFat + csectDif*fatsPerDif)*sectsPerFat)];

        /*
        System.out.println("FAT has " +
                           ((int)((csectFat + csectDif*fatsPerDif)*sectsPerFat)) + " entries.");

        System.out.println("csectFat = " + csectFat);
        System.out.println("csectDif = " + csectDif);
        System.out.println("fatsPerDif = " + fatsPerDif);
        System.out.println("sectsPerFat = " + sectsPerFat);
        */

        int numFATSectors = (int)(csectFat + csectDif*fatsPerDif);
        long[] FATSectors = new long[numFATSectors];
        int count = 0;

        for (int i = 0; i < 109; i++) {
            long sector = sectFat[i];
            if (sector == FAT_FREESECT) {
                break;
            }

            FATSectors[count++] = getOffsetOfSector(sectFat[i]);
            // readFatSector(sector, index);
            // index += sectsPerFat;
        }

        if (csectDif > 0) {
            long dif = sectDifStart;
            byte[] difBuf = new byte[size];

            for (int i = 0; i < csectDif; i++) {
                readSector(dif, difBuf, 0);
                for (int j = 0; j < fatsPerDif; j++) {
                    int sec = FPXUtils.getIntLE(difBuf, 4*j);
                    FATSectors[count++] = getOffsetOfSector(sec);
                    // readFatSector(sec, index);
                    // index += sectsPerFat;
                }

                dif = FPXUtils.getIntLE(difBuf, size - 4);
            }
        }

        FATStream = new SegmentedSeekableStream(file,
                                                FATSectors,
                                                size,
                                                numFATSectors*size,
                                                true);
    }

    private void getMiniFat() throws IOException {
        int size = getSectorSize();
        int sectsPerFat = size/4;
        int index = 0;

        this.MINIFAT = new long[(int)(csectMiniFat*sectsPerFat)];

        long sector = sectMiniFatStart;
        // System.out.println("minifat start sector = " + sector);
        byte[] buf = new byte[size];
        while (sector != FAT_ENDOFCHAIN) {
            // System.out.println("minifat sector = " + sector);
            readSector(sector, buf, 0);
            for (int j = 0; j < sectsPerFat; j++) {
                MINIFAT[index++] = FPXUtils.getIntLE(buf, 4*j);
            }
            sector = getFATSector(sector);
        }
    }

    private void getDirectory() throws IOException {
        int size = getSectorSize();
        long sector = sectDirStart;

        // Count the length of the directory in sectors
        int numDirectorySectors = 0;
        while (sector != FAT_ENDOFCHAIN) {
            sector = getFATSector(sector);
            ++numDirectorySectors;
        }

        int directoryEntries = 4*numDirectorySectors;
        this.DIR = new SSDirectoryEntry[directoryEntries];

        sector = sectDirStart;
        byte[] buf = new byte[size];
        int index = 0;
        while (sector != FAT_ENDOFCHAIN) {
            readSector(sector, buf, 0);

            int offset = 0;
            for (int i = 0; i < 4; i++) { // 4 dirents per sector
                // We divide the length by 2 for now even though
                // the spec says not to...
                int length = FPXUtils.getShortLE(buf, offset + 0x40);
                // System.out.println("\n\nDirent name length = " + length);

                /*
                FPXUtils.dumpBuffer(buf, offset, 128, 0);

                for (int j = 0; j < 32; j++) {
                    int c = FPXUtils.getShortLE(buf, offset + 2*j);
                    System.out.println("name[" + (2*j) + "] = " + c +
                                       " '" + (char)c + "'");
                }
                */

                String name = FPXUtils.getString(buf, offset + 0x00, length);
                long SIDLeftSibling =
                    FPXUtils.getUnsignedIntLE(buf, offset + 0x44);
                long SIDRightSibling =
                    FPXUtils.getUnsignedIntLE(buf, offset + 0x48);
                long SIDChild = FPXUtils.getUnsignedIntLE(buf, offset + 0x4c);
                long startSector =
                    FPXUtils.getUnsignedIntLE(buf, offset + 0x74);
                long streamSize = FPXUtils.getUnsignedIntLE(buf, offset + 0x78);

                DIR[index] = new SSDirectoryEntry(index,
                                                  name,
                                                  streamSize,
                                                  startSector,
                                                  SIDLeftSibling,
                                                  SIDRightSibling,
                                                  SIDChild);
                ++index;
                offset += 128;
            }

            sector = getFATSector(sector);
        }
    }

    private void getMiniStream() throws IOException {
        int length = getLength(0L);
        int sectorSize = getSectorSize();
        int sectors = (int)((length + sectorSize - 1)/sectorSize);

        long[] segmentPositions = new long[sectors];

        long sector = getStartSector(0);
        // int offset = 0;
        for (int i = 0; i < sectors - 1; i++) {
            segmentPositions[i] = getOffsetOfSector(sector);
            sector = getFATSector(sector);
            if(sector == FAT_ENDOFCHAIN) break;
        }
        segmentPositions[sectors - 1] = getOffsetOfSector(sector);

        miniStream = new SegmentedSeekableStream(file,
                                                 segmentPositions,
                                                 sectorSize,
                                                 length,
                                                 true);
    }

    /*
    private void readFatSector(long sector, int index) throws IOException {
        int sectsPerFat = getSectorSize()/4;
        long offset = getOffsetOfSector(sector);

        file.seek(offset);
        for (int i = 0; i < sectsPerFat; i++) {
            FAT[index] = file.readUnsignedIntLE();
            // System.out.println("FAT[" + index + "] = " + FAT[index]);
            index++;
        }
    }
    */

    private int getSectorSize() {
        return 1 << sectorShift;
    }

    private long getOffsetOfSector(long sector) {
        return sector*getSectorSize() + 512;
    }

    private int getMiniSectorSize() {
        return 1 << miniSectorShift;
    }

    private long getOffsetOfMiniSector(long sector) {
        return sector*getMiniSectorSize();
    }

    private void readMiniSector(long sector, byte[] buf,
                                int offset, int length)
        throws IOException {
        miniStream.seek(getOffsetOfMiniSector(sector));
        miniStream.read(buf, offset, length);
    }

    private void readMiniSector(long sector, byte[] buf, int offset)
        throws IOException {
        readMiniSector(sector, buf, offset, getMiniSectorSize());
    }

    private void readSector(long sector, byte[] buf, int offset, int length)
        throws IOException {
        file.seek(getOffsetOfSector(sector));
        file.read(buf, offset, length);
    }

    private void readSector(long sector, byte[] buf, int offset)
        throws IOException {
        readSector(sector, buf, offset, getSectorSize());
    }

    private SSDirectoryEntry getDirectoryEntry(long index) {
        // Assume #streams < 2^31
        return DIR[(int)index];
    }

    private long getStartSector(long index) {
        // Assume #streams < 2^31
        return DIR[(int)index].getStartSector();
    }

    private int getLength(long index) {
        // Assume #streams < 2^31
        // Assume size < 2GB
        return (int)DIR[(int)index].getSize();
    }

    private long getFATSector(long sector) throws IOException {
        FATStream.seek(4*sector);
        return FATStream.readUnsignedIntLE();
        // return FAT[(int)sector];
    }

    private long getMiniFATSector(long sector) {
        return MINIFAT[(int)sector];
    }

    private int getCurrentIndex() {
        return -1;
    }

    private int getIndex(String name, int index) {
        return -1;
    }

    private long searchDirectory(String name, long index) {
        if (index == FAT_FREESECT) {
            return -1L;
        }

        SSDirectoryEntry dirent = getDirectoryEntry(index);
        /*
        System.out.println("Comparing " + name + " (" + name.length() +
                           ") against " +
                           dirent.getName() + " (" +
                           (dirent.getName()).length() + ") index " +
                           index);
        */

        if (name.equals(dirent.getName())) {
            // System.out.println("Matched!");
            return index;
        } else {
            long lindex =
                searchDirectory(name, dirent.getSIDLeftSibling());
            if (lindex != -1L) {
                return lindex;
            }

            long rindex =
                searchDirectory(name, dirent.getSIDRightSibling());
            if (rindex != -1L) {
                return rindex;
            }
        }

        return -1L;
    }

    // Public methods

    public void changeDirectoryToRoot() {
        cwdIndex = getDirectoryEntry(0L).getSIDChild();
    }

    public boolean changeDirectory(String name) {
        long index = searchDirectory(name, cwdIndex);
        if (index != -1L) {
            cwdIndex = getDirectoryEntry(index).getSIDChild();
            // System.out.println("changeDirectory: setting cwdIndex to " +
            // cwdIndex);
            return true;
        } else {
            return false;
        }
    }

    /*
    public SSDirectory[] getDirectoryEntries() {
    }
    */

    private long getStreamIndex(String name) {
        // Move down the directory hierarchy
        long index = cwdIndex;
        // System.out.println("start index = " + index);

        StringTokenizer st = new StringTokenizer(name, "/");
        boolean firstTime = true;
        while (st.hasMoreTokens()) {
            String tok = st.nextToken();

            // System.out.println("Token = " + tok);
            if (!firstTime) {
                index = getDirectoryEntry(index).getSIDChild();
            } else {
                firstTime = false;
            }
            index = searchDirectory(tok, index);
            // System.out.println("index = " + index);
        }

        return index;
    }

    public byte[] getStreamAsBytes(String name) throws IOException {
        long index = getStreamIndex(name);
        if (index == -1L) {
            return null;
        }

        // Cast index to int (streams < 2^31) and cast stream size to an
        // int (size < 2GB)
        int length = getLength(index);
        byte[] buf = new byte[length];

        if (length > miniSectorCutoff) {
            int sectorSize = getSectorSize();
            int sectors = (int)((length + sectorSize - 1)/sectorSize);

            long sector = getStartSector(index);
            int offset = 0;
            for (int i = 0; i < sectors - 1; i++) {
                readSector(sector, buf, offset, sectorSize);
                offset += sectorSize;
                sector = getFATSector(sector);
                // System.out.println("next sector = " + sector);
                if(sector == FAT_ENDOFCHAIN) break;
            }

            readSector(sector, buf, offset, length - offset);
        } else {
            int sectorSize = getMiniSectorSize();
            int sectors = (int)((length + sectorSize - 1)/sectorSize);

            long sector = getStartSector(index);

            // Assume ministream size < 2GB
            int offset = 0;
            for (int i = 0; i < sectors - 1; i++) {
                long miniSectorOffset = getOffsetOfMiniSector(sector);
                readMiniSector(sector, buf, offset, sectorSize);
                offset += sectorSize;
                sector = getMiniFATSector(sector);
            }
            readMiniSector(sector, buf, offset, length - offset);
        }

        return buf;
    }

    public SeekableStream getStream(String name) throws IOException {
        long index = getStreamIndex(name);
        if (index == -1L) {
            return null;
        }

        // Cast index to int (streams < 2^31) and cast stream size to an
        // int (size < 2GB)
        int length = getLength(index);

        long[] segmentPositions;
        int sectorSize, sectors;

        if (length > miniSectorCutoff) {
            sectorSize = getSectorSize();
            sectors = (int)((length + sectorSize - 1)/sectorSize);
            segmentPositions = new long[sectors];

            long sector = getStartSector(index);
            for (int i = 0; i < sectors - 1; i++) {
                segmentPositions[i] = getOffsetOfSector(sector);
                sector = getFATSector(sector);
                if(sector == FAT_ENDOFCHAIN) break;
            }
            segmentPositions[sectors - 1] = getOffsetOfSector(sector);

            return new SegmentedSeekableStream(file,
                                               segmentPositions,
                                               sectorSize,
                                               length,
                                               true);
        } else {
            sectorSize = getMiniSectorSize();
            sectors = (int)((length + sectorSize - 1)/sectorSize);
            segmentPositions = new long[sectors];

            long sector = getStartSector(index);
            for (int i = 0; i < sectors - 1; i++) {
                segmentPositions[i] = getOffsetOfMiniSector(sector);
                sector = getMiniFATSector(sector);
            }
            segmentPositions[sectors - 1] = getOffsetOfMiniSector(sector);

            return new SegmentedSeekableStream(miniStream,
                                               segmentPositions,
                                               sectorSize,
                                               length,
                                               true);
        }
    }

    public static void main(String[] args) {
        try {
            RandomAccessFile f = new RandomAccessFile(args[0], "r");
            SeekableStream sis = new FileSeekableStream(f);
            StructuredStorage ss = new StructuredStorage(sis);

            ss.changeDirectoryToRoot();

            byte[] s = ss.getStreamAsBytes("SummaryInformation");

            PropertySet ps = new PropertySet(new ByteArraySeekableStream(s));

            // Get the thumbnail property
            byte[] thumb = ps.getBlob(17);

            // Emit it as a BMP file
            System.out.print("BM");
            int fs = (thumb.length - 8) + 14 + 40;
            System.out.print((char)(fs & 0xff));
            System.out.print((char)((fs >>  8) & 0xff));
            System.out.print((char)((fs >> 16) & 0xff));
            System.out.print((char)((fs >> 24) & 0xff));
            System.out.print((char)0);
            System.out.print((char)0);
            System.out.print((char)0);
            System.out.print((char)0);
            System.out.print('6');
            System.out.print((char)0);
            System.out.print((char)0);
            System.out.print((char)0);
            for (int i = 8; i < thumb.length; i++) {
                System.out.print((char)(thumb[i] & 0xff));
            }

            /*
            ss.changeDirectory("Data Object Store 000001");
            SeekableStream imageContents =
                ss.getStream("Image Contents");
            */

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
