/*
 * Copyright 1997 Phil Burk, Mobileer Inc
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.jsyn.util.soundfile;

import java.io.EOFException;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;

/**
 * Parse Electronic Arts style IFF File. IFF is a file format that allows "chunks" of data to be
 * placed in a hierarchical file. It was designed by Jerry Morrison at Electronic Arts for the Amiga
 * computer and is now used extensively by Apple Computer and other companies. IFF is an open
 * standard.
 *
 * @see RIFFParser
 * @see AudioSampleAIFF
 * @author (C) 1997 Phil Burk, SoftSynth.com
 */

class IFFParser extends FilterInputStream {

    private long numBytesRead = 0;
    private long totalSize = 0;
    private int fileId;
    static boolean debug = false;

    public static final int RIFF_ID = ('R' << 24) | ('I' << 16) | ('F' << 8) | 'F';
    public static final int LIST_ID = ('L' << 24) | ('I' << 16) | ('S' << 8) | 'T';
    public static final int FORM_ID = ('F' << 24) | ('O' << 16) | ('R' << 8) | 'M';

    IFFParser(InputStream stream) {
        super(stream);
        numBytesRead = 0;
    }

    /**
     * Size of file based on outermost chunk size plus 8. Can be used to report progress when
     * loading samples.
     *
     * @return Number of bytes in outer chunk plus header.
     */
    public long getFileSize() {
        return totalSize;
    }

    /**
     * Since IFF files use chunks with explicit size, it is important to keep track of how many
     * bytes have been read from the file. Can be used to report progress when loading samples.
     *
     * @return Number of bytes read from stream, or skipped.
     */
    public long getOffset() {
        return numBytesRead;
    }

    /** @return Next byte from stream. Increment offset by 1. */
    @Override
    public int read() throws IOException {
        numBytesRead++;
        return super.read();
    }

    /** @return Next byte array from stream. Increment offset by len. */
    @Override
    public int read(byte[] bar) throws IOException {
        return read(bar, 0, bar.length);
    }

    /** @return Next byte array from stream. Increment offset by len. */
    @Override
    public int read(byte[] bar, int off, int len) throws IOException {
        // Reading from a URL can return before all the bytes are available.
        // So we keep reading until we get the whole thing.
        int cursor = off;
        int numLeft = len;
        // keep reading data until we get it all
        while (numLeft > 0) {
            int numRead = super.read(bar, cursor, numLeft);
            if (numRead < 0)
                return numRead;
            cursor += numRead;
            numBytesRead += numRead;
            numLeft -= numRead;
            // LOGGER.debug("read " + numRead + ", cursor = " + cursor +
            // ", len = " + len);
        }
        return cursor - off;
    }

    /** @return Skip forward in stream and add numBytes to offset. */
    @Override
    public long skip(long numBytes) throws IOException {
        numBytesRead += numBytes;
        return super.skip(numBytes);
    }

    /** Read 32 bit signed integer assuming Big Endian byte order. */
    public int readIntBig() throws IOException {
        int result = read() & 0xFF;
        result = (result << 8) | (read() & 0xFF);
        result = (result << 8) | (read() & 0xFF);
        int data = read();
        if (data == -1)
            throw new EOFException("readIntBig() - EOF in middle of word at offset " + numBytesRead);
        result = (result << 8) | (data & 0xFF);
        return result;
    }

    /** Read 32 bit signed integer assuming Little Endian byte order. */
    public int readIntLittle() throws IOException {
        int result = read() & 0xFF; // LSB
        result |= ((read() & 0xFF) << 8);
        result |= ((read() & 0xFF) << 16);
        int data = read();
        if (data == -1)
            throw new EOFException("readIntLittle() - EOF in middle of word at offset "
                    + numBytesRead);
        result |= (data << 24);
        return result;
    }

    /** Read 16 bit signed short assuming Big Endian byte order. */
    public short readShortBig() throws IOException {
        short result = (short) ((read() << 8)); // MSB
        int data = read();
        if (data == -1)
            throw new EOFException("readShortBig() - EOF in middle of word at offset "
                    + numBytesRead);
        result |= data & 0xFF;
        return result;
    }

    /** Read 16 bit signed short assuming Little Endian byte order. */
    public short readShortLittle() throws IOException {
        short result = (short) (read() & 0xFF); // LSB
        int data = read(); // MSB
        if (data == -1)
            throw new EOFException("readShortLittle() - EOF in middle of word at offset "
                    + numBytesRead);
        result |= data << 8;
        return result;
    }

    public int readUShortLittle() throws IOException {
        return (readShortLittle()) & 0x0000FFFF;
    }

    /** Read 8 bit signed byte. */
    public byte readByte() throws IOException {
        return (byte) read();
    }

    /** Read 32 bit signed int assuming IFF order. */
    public int readChunkSize() throws IOException {
        if (isRIFF()) {
            return readIntLittle();
        }
        {
            return readIntBig();
        }
    }

    /** Convert a 4 character IFF ID to a String */
    public static String IDToString(int ID) {
        byte bar[] = new byte[4];
        bar[0] = (byte) (ID >> 24);
        bar[1] = (byte) (ID >> 16);
        bar[2] = (byte) (ID >> 8);
        bar[3] = (byte) ID;
        return new String(bar);
    }

    /**
     * Parse the stream after reading the first ID and pass the forms and chunks to the ChunkHandler
     */
    public void parseAfterHead(ChunkHandler handler) throws IOException {
        int numBytes = readChunkSize();
        totalSize = numBytes + 8;
        parseChunk(handler, fileId, numBytes);
    }

    /**
     * Parse the FORM and pass the chunks to the ChunkHandler The cursor should be positioned right
     * after the type field.
     */
    void parseForm(ChunkHandler handler, int ID, int numBytes, int type) throws IOException {
        while (numBytes > 8) {
            int ckid = readIntBig();
            int size = readChunkSize();
            numBytes -= 8;
            if (size < 0) {
                throw new IOException("Bad IFF chunk Size: " + IDToString(ckid) + " = 0x"
                        + Integer.toHexString(ckid) + ", Size = " + size);
            }
            parseChunk(handler, ckid, size);
            if ((size & 1) == 1)
                size++; // even-up
            numBytes -= size;
        }

        if (numBytes > 0) {
            skip(numBytes);
        }
    }

    /*
     * Parse one chunk from IFF file. After calling handler, make sure stream is positioned at end
     * of chunk.
     */
    void parseChunk(ChunkHandler handler, int ckid, int numBytes) throws IOException {
        long startOffset, endOffset;
        int numRead;
        startOffset = getOffset();
        if (isForm(ckid)) {
            int type = readIntBig();
            handler.handleForm(this, ckid, numBytes - 4, type);
            endOffset = getOffset();
            numRead = (int) (endOffset - startOffset);
            if (numRead < numBytes)
                parseForm(handler, ckid, (numBytes - numRead), type);
        } else {
            handler.handleChunk(this, ckid, numBytes);
        }
        endOffset = getOffset();
        numRead = (int) (endOffset - startOffset);
        if ((numBytes & 1) == 1)
            numBytes++; // even-up
        if (numRead < numBytes)
            skip(numBytes - numRead);
    }

    public void readHead() throws IOException {
        numBytesRead = 0;
        fileId = readIntBig();
    }

    public boolean isRIFF() {
        return (fileId == RIFF_ID);
    }

    public boolean isIFF() {
        return (fileId == FORM_ID);
    }

    /**
     * Does the following chunk ID correspond to a container type like FORM?
     */
    public boolean isForm(int ckid) {
        if (isRIFF()) {
            switch (ckid) {
                case LIST_ID:
                case RIFF_ID:
                    return true;
                default:
                    return false;
            }
        } else {
            switch (ckid) {
                case LIST_ID:
                case FORM_ID:
                    return true;
                default:
                    return false;
            }
        }
    }

}
