/*
 * Copyright (C) 2000 ymnk<ymnk@jcraft.com>
 *               2015 Trilarion
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser 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 Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.github.trilarion.sound.vorbis.jcraft.jorbis;

import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.github.trilarion.sound.util.SoundException;
import com.github.trilarion.sound.vorbis.jcraft.jogg.Packet;
import com.github.trilarion.sound.vorbis.jcraft.jogg.Page;
import com.github.trilarion.sound.vorbis.jcraft.jogg.StreamState;
import com.github.trilarion.sound.vorbis.jcraft.jogg.SyncState;

/**
 *
 */
public class VorbisFile {

    private static final Logger LOG = Logger.getLogger(VorbisFile.class.getName());

    static final int CHUNKSIZE = 8500;
    static final int SEEK_SET = 0;
    static final int SEEK_CUR = 1;
    static final int SEEK_END = 2;

    static final int OV_FALSE = -1;
    static final int OV_EOF = -2;
    static final int OV_HOLE = -3;

    static final int OV_EREAD = -128;
    static final int OV_EFAULT = -129;
    static final int OV_EIMPL = -130;
    static final int OV_EINVAL = -131;
    static final int OV_ENOTVORBIS = -132;
    static final int OV_EBADHEADER = -133;
    static final int OV_EVERSION = -134;
    static final int OV_ENOTAUDIO = -135;
    static final int OV_EBADPACKET = -136;
    static final int OV_EBADLINK = -137;
    static final int OV_ENOSEEK = -138;

    InputStream datasource;
    boolean seekable = false;
    long offset;
    long end;

    SyncState oy = new SyncState();

    int links;
    long[] offsets;
    long[] dataoffsets;
    int[] serialnos;
    long[] pcmlengths;
    Info[] vi;
    Comment[] vc;

    // Decoding working state local storage
    long pcm_offset;
    boolean decode_ready = false;

    int current_serialno;
    int current_link;

    float bittrack;
    float samptrack;

    StreamState os = new StreamState(); // take physical pages, weld into a logical
    // stream of packets
    DspState vd = new DspState(); // central working state for 
    // the packet->PCM decoder
    Block vb = new Block(vd); // local working space for packet->PCM decode

    //ov_callbacks callbacks;
    /**
     *
     * @param file
     * @throws SoundException
     */
    public VorbisFile(String file) throws SoundException {
        super();
        InputStream is = null;
        try {
            is = new SeekableInputStream(file);
            int ret = open(is, null, 0);
            if (ret == -1) {
                throw new SoundException("VorbisFile: open return -1");
            }
        } catch (IOException | SoundException e) {
            throw new SoundException("VorbisFile: " + e.toString());
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    LOG.log(Level.FINE, e.getMessage());
                }
            }
        }
    }

    /**
     *
     * @param is
     * @param initial
     * @param ibytes
     * @throws SoundException
     */
    public VorbisFile(InputStream is, byte[] initial, int ibytes)
            throws SoundException {
        super();
        int ret = open(is, initial, ibytes);
        if (ret == -1) {
        }
    }

    private int get_data() {
        int index = oy.buffer(CHUNKSIZE);
        byte[] buffer = oy.data;
        int bytes = 0;
        try {
            bytes = datasource.read(buffer, index, CHUNKSIZE);
        } catch (Exception e) {
            return OV_EREAD;
        }
        oy.wrote(bytes);
        if (bytes == -1) {
            bytes = 0;
        }
        return bytes;
    }

    private void seek_helper(long offst) {
        fseek(datasource, offst, SEEK_SET);
        this.offset = offst;
        oy.reset();
    }

    private int get_next_page(Page page, long boundary) {
        if (boundary > 0) {
            boundary += offset;
        }
        while (true) {
            int more;
            if (boundary > 0 && offset >= boundary) {
                return OV_FALSE;
            }
            more = oy.pageseek(page);
            if (more < 0) {
                offset -= more;
            } else {
                if (more == 0) {
                    if (boundary == 0) {
                        return OV_FALSE;
                    }
                    int ret = get_data();
                    if (ret == 0) {
                        return OV_EOF;
                    }
                    if (ret < 0) {
                        return OV_EREAD;
                    }
                } else {
                    int ret = (int) offset; //!!!
                    offset += more;
                    return ret;
                }
            }
        }
    }

    private int get_prev_page(Page page) throws SoundException {
        long begin = offset; //!!!
        int ret;
        int offst = -1;
        while (offst == -1) {
            begin -= CHUNKSIZE;
            if (begin < 0) {
                begin = 0;
            }
            seek_helper(begin);
            while (offset < begin + CHUNKSIZE) {
                ret = get_next_page(page, begin + CHUNKSIZE - offset);
                if (ret == OV_EREAD) {
                    return OV_EREAD;
                }
                if (ret < 0) {
                    if (offst == -1) {
                        throw new SoundException("");
                    }
                    break;
                } else {
                    offst = ret;
                }
            }
        }
        seek_helper(offst); //!!!
        ret = get_next_page(page, CHUNKSIZE);
        if (ret < 0) {
            return OV_EFAULT;
        }
        return offst;
    }

    int bisect_forward_serialno(long begin, long searched, long end,
            int currentno, int m) {
        long endsearched = end;
        long next = end;
        Page page = new Page();
        int ret;

        while (searched < endsearched) {
            long bisect;
            if (endsearched - searched < CHUNKSIZE) {
                bisect = searched;
            } else {
                bisect = (searched + endsearched) / 2;
            }

            seek_helper(bisect);
            ret = get_next_page(page, -1);
            if (ret == OV_EREAD) {
                return OV_EREAD;
            }
            if (ret < 0 || page.serialno() != currentno) {
                endsearched = bisect;
                if (ret >= 0) {
                    next = ret;
                }
            } else {
                searched = ret + page.header_len + page.body_len;
            }
        }
        seek_helper(next);
        ret = get_next_page(page, -1);
        if (ret == OV_EREAD) {
            return OV_EREAD;
        }

        if (searched >= end || ret == -1) {
            links = m + 1;
            offsets = new long[m + 2];
            offsets[m + 1] = searched;
        } else {
            ret = bisect_forward_serialno(next, offset, end, page.serialno(), m + 1);
            if (ret == OV_EREAD) {
                return OV_EREAD;
            }
        }
        offsets[m] = begin;
        return 0;
    }

    // uses the local ogg_stream storage in vf; this is important for
    // non-streaming input sources
    int fetch_headers(Info vi, Comment vc, int[] serialno, Page og_ptr) {
        Page og = new Page();
        Packet op = new Packet();
        int ret;

        if (og_ptr == null) {
            ret = get_next_page(og, CHUNKSIZE);
            if (ret == OV_EREAD) {
                return OV_EREAD;
            }
            if (ret < 0) {
                return OV_ENOTVORBIS;
            }
            og_ptr = og;
        }

        if (serialno != null) {
            serialno[0] = og_ptr.serialno();
        }

        os.init(og_ptr.serialno());

        // extract the initial header from the first page and verify that the
        // Ogg bitstream is in fact Vorbis data
        vi.init();
        vc.init();

        int i = 0;
        while (i < 3) {
            os.pagein(og_ptr);
            while (i < 3) {
                int result = os.packetout(op);
                if (result == 0) {
                    break;
                }
                if (result == -1) {
                    vi.clear();
                    vc.clear();
                    os.clear();
                    return -1;
                }
                if (vi.synthesis_headerin(vc, op) != 0) {
                    vi.clear();
                    vc.clear();
                    os.clear();
                    return -1;
                }
                i++;
            }
            if (i < 3) {
                if (get_next_page(og_ptr, 1) < 0) {
                    vi.clear();
                    vc.clear();
                    os.clear();
                    return -1;
                }
            }
        }
        return 0;
    }

    // last step of the OggVorbis_File initialization; get all the
    // vorbis_info structs and PCM positions.  Only called by the seekable
    // initialization (local stream storage is hacked slightly; pay
    // attention to how that's done)
    void prefetch_all_headers(Info first_i, Comment first_c, int dataoffset)
            throws SoundException {
        Page og = new Page();
        int ret;

        vi = new Info[links];
        vc = new Comment[links];
        dataoffsets = new long[links];
        pcmlengths = new long[links];
        serialnos = new int[links];

        for (int i = 0; i < links; i++) {
            if (first_i != null && first_c != null && i == 0) {
                // we already grabbed the initial header earlier.  This just
                // saves the waste of grabbing it again
                vi[i] = first_i;
                vc[i] = first_c;
                dataoffsets[i] = dataoffset;
            } else {
                // seek to the location of the initial header
                seek_helper(offsets[i]); //!!!
                vi[i] = new Info();
                vc[i] = new Comment();
                if (fetch_headers(vi[i], vc[i], null, null) == -1) {
                    dataoffsets[i] = -1;
                } else {
                    dataoffsets[i] = offset;
                    os.clear();
                }
            }

            // get the serial number and PCM length of this link. To do this,
            // get the last page of the stream
            {
                long end = offsets[i + 1]; //!!!
                seek_helper(end);

                while (true) {
                    ret = get_prev_page(og);
                    if (ret == -1) {
                        // this should not be possible
                        vi[i].clear();
                        vc[i].clear();
                        break;
                    }
                    if (og.granulepos() != -1) {
                        serialnos[i] = og.serialno();
                        pcmlengths[i] = og.granulepos();
                        break;
                    }
                }
            }
        }
    }

    private int make_decode_ready() {
        if (decode_ready) {
            throw new RuntimeException("");
        }
        vd.synthesis_init(vi[0]);
        vb.init(vd);
        decode_ready = true;
        return 0;
    }

    int open_seekable() throws SoundException {
        Info initial_i = new Info();
        Comment initial_c = new Comment();
        int serialno;
        long end;
        int ret;
        int dataoffset;
        Page og = new Page();
        // is this even vorbis...?
        int[] foo = new int[1];
        ret = fetch_headers(initial_i, initial_c, foo, null);
        serialno = foo[0];
        dataoffset = (int) offset; //!!
        os.clear();
        if (ret == -1) {
            return (-1);
        }
        if (ret < 0) {
            return (ret);
        }
        // we can seek, so set out learning all about this file
        seekable = true;
        fseek(datasource, 0, SEEK_END);
        offset = ftell(datasource);
        end = offset;
        // We get the offset for the last page of the physical bitstream.
        // Most OggVorbis files will contain a single logical bitstream
        end = get_prev_page(og);
        // moer than one logical bitstream?
        if (og.serialno() != serialno) {
            // Chained bitstream. Bisect-search each logical bitstream
            // section.  Do so based on serial number only
            if (bisect_forward_serialno(0, 0, end + 1, serialno, 0) < 0) {
                clear();
                return OV_EREAD;
            }
        } else {
            // Only one logical bitstream
            if (bisect_forward_serialno(0, end, end + 1, serialno, 0) < 0) {
                clear();
                return OV_EREAD;
            }
        }
        prefetch_all_headers(initial_i, initial_c, dataoffset);
        return 0;
    }

    int open_nonseekable() {
        // we cannot seek. Set up a 'single' (current) logical bitstream entry
        links = 1;
        vi = new Info[links];
        vi[0] = new Info(); // ??
        vc = new Comment[links];
        vc[0] = new Comment(); // ?? bug?

        // Try to fetch the headers, maintaining all the storage
        int[] foo = new int[1];
        if (fetch_headers(vi[0], vc[0], foo, null) == -1) {
            return (-1);
        }
        current_serialno = foo[0];
        make_decode_ready();
        return 0;
    }

    // clear out the current logical bitstream decoder
    void decode_clear() {
        os.clear();
        vd.clear();
        vb.clear();
        decode_ready = false;
        bittrack = 0.f;
        samptrack = 0.f;
    }

    // fetch and process a packet.  Handles the case where we're at a
    // bitstream boundary and dumps the decoding machine.  If the decoding
    // machine is unloaded, it loads it.  It also keeps pcm_offset up to
    // date (seek and read both use this.  seek uses a special hack with
    // readp). 
    //
    // return: -1) hole in the data (lost packet) 
    //          0) need more date (only if readp==0)/eof
    //          1) got a packet 
    int process_packet(int readp) {
        Page og = new Page();

        // handle one packet.  Try to fetch it from current stream state
        // extract packets from page
        while (true) {
            // process a packet if we can.  If the machine isn't loaded,
            // neither is a page
            if (decode_ready) {
                Packet op = new Packet();
                int result = os.packetout(op);
                long granulepos;
                // if(result==-1)return(-1); // hole in the data. For now, swallow
                // and go. We'll need to add a real
                // error code in a bit.
                if (result > 0) {
                    // got a packet.  process it
                    granulepos = op.granulepos;
                    if (vb.synthesis(op) == 0) { // lazy check for lazy
                        // header handling.  The
                        // header packets aren't
                        // audio, so if/when we
                        // submit them,
                        // vorbis_synthesis will
                        // reject them
                        // suck in the synthesis data and track bitrate
                        {
                            int oldsamples = vd.synthesis_pcmout(null, null);
                            vd.synthesis_blockin(vb);
                            samptrack += vd.synthesis_pcmout(null, null) - oldsamples;
                            bittrack += op.bytes * 8;
                        }

                        // update the pcm offset.
                        if (granulepos != -1 && op.e_o_s == 0) {
                            int link = (seekable ? current_link : 0);
                            int samples;
                            // this packet has a pcm_offset on it (the last packet
                            // completed on a page carries the offset) After processing
                            // (above), we know the pcm position of the *last* sample
                            // ready to be returned. Find the offset of the *first*
                            // 
                            // As an aside, this trick is inaccurate if we begin
                            // reading anew right at the last page; the end-of-stream
                            // granulepos declares the last frame in the stream, and the
                            // last packet of the last page may be a partial frame.
                            // So, we need a previous granulepos from an in-sequence page
                            // to have a reference point.  Thus the !op.e_o_s clause above

                            samples = vd.synthesis_pcmout(null, null);
                            granulepos -= samples;
                            for (int i = 0; i < link; i++) {
                                granulepos += pcmlengths[i];
                            }
                            pcm_offset = granulepos;
                        }
                        return (1);
                    }
                }
            }

            if (readp == 0) {
                return (0);
            }
            if (get_next_page(og, -1) < 0) {
                return (0); // eof. leave unitialized
            }
            // bitrate tracking; add the header's bytes here, the body bytes
            // are done by packet above
            bittrack += og.header_len * 8;

            // has our decoding just traversed a bitstream boundary?
            if (decode_ready) {
                if (current_serialno != og.serialno()) {
                    decode_clear();
                }
            }

            // Do we need to load a new machine before submitting the page?
            // This is different in the seekable and non-seekable cases.  
            // 
            // In the seekable case, we already have all the header
            // information loaded and cached; we just initialize the machine
            // with it and continue on our merry way.
            // 
            // In the non-seekable (streaming) case, we'll only be at a
            // boundary if we just left the previous logical bitstream and
            // we're now nominally at the header of the next bitstream
            if (!decode_ready) {
                int i;
                if (seekable) {
                    current_serialno = og.serialno();

                    // match the serialno to bitstream section.  We use this rather than
                    // offset positions to avoid problems near logical bitstream
                    // boundaries
                    for (i = 0; i < links; i++) {
                        if (serialnos[i] == current_serialno) {
                            break;
                        }
                    }
                    if (i == links) {
                        return (-1); // sign of a bogus stream.  error out,
                    }          // leave machine uninitialized
                    current_link = i;

                    os.init(current_serialno);
                    os.reset();

                } else {
                    // we're streaming
                    // fetch the three header packets, build the info struct
                    int[] foo = new int[1];
                    int ret = fetch_headers(vi[0], vc[0], foo, og);
                    current_serialno = foo[0];
                    if (ret != 0) {
                        return ret;
                    }
                    current_link++;
                    i = 0;
                }
                make_decode_ready();
            }
            os.pagein(og);
        }
    }

    // The helpers are over; it's all toplevel interface from here on out
    // clear out the OggVorbis_File struct
    int clear() {
        vb.clear();
        vd.clear();
        os.clear();

        if (vi != null && links != 0) {
            for (int i = 0; i < links; i++) {
                vi[i].clear();
                vc[i].clear();
            }
            vi = null;
            vc = null;
        }
        if (dataoffsets != null) {
            dataoffsets = null;
        }
        if (pcmlengths != null) {
            pcmlengths = null;
        }
        if (serialnos != null) {
            serialnos = null;
        }
        if (offsets != null) {
            offsets = null;
        }
        oy.clear();

        return (0);
    }

    static int fseek(InputStream fis, long off, int whence) {
        if (fis instanceof SeekableInputStream) {
            SeekableInputStream sis = (SeekableInputStream) fis;
            try {
                if (whence == SEEK_SET) {
                    sis.seek(off);
                } else if (whence == SEEK_END) {
                    sis.seek(sis.getLength() - off);
                } else {
                }
            } catch (Exception e) {
            }
            return 0;
        }
        try {
            if (whence == 0) {
                fis.reset();
            }
            fis.skip(off);
        } catch (Exception e) {
            return -1;
        }
        return 0;
    }

    static long ftell(InputStream fis) {
        try {
            if (fis instanceof SeekableInputStream) {
                SeekableInputStream sis = (SeekableInputStream) fis;
                return (sis.tell());
            }
        } catch (Exception e) {
        }
        return 0;
    }

    // inspects the OggVorbis file and finds/documents all the logical
    // bitstreams contained in it.  Tries to be tolerant of logical
    // bitstream sections that are truncated/woogie. 
    //
    // return: -1) error
    //          0) OK
    final int open(InputStream is, byte[] initial, int ibytes) throws SoundException {
        return open_callbacks(is, initial, ibytes);
    }

    int open_callbacks(InputStream is, byte[] initial, int ibytes//, callbacks callbacks
    ) throws SoundException {
        int ret;
        datasource = is;

        oy.init();

        // perhaps some data was previously read into a buffer for testing
        // against other stream types.  Allow initialization from this
        // previously read data (as we may be reading from a non-seekable
        // stream)
        if (initial != null) {
            int index = oy.buffer(ibytes);
            System.arraycopy(initial, 0, oy.data, index, ibytes);
            oy.wrote(ibytes);
        }
        // can we seek? Stevens suggests the seek test was portable
        if (is instanceof SeekableInputStream) {
            ret = open_seekable();
        } else {
            ret = open_nonseekable();
        }
        if (ret != 0) {
            datasource = null;
            clear();
        }
        return ret;
    }

    // How many logical bitstreams in this physical bitstream?
    /**
     *
     * @return
     */
    public int streams() {
        return links;
    }

    // Is the FILE * associated with vf seekable?
    /**
     *
     * @return
     */
    public boolean seekable() {
        return seekable;
    }

    // returns the bitrate for a given logical bitstream or the entire
    // physical bitstream.  If the file is open for random access, it will
    // find the *actual* average bitrate.  If the file is streaming, it
    // returns the nominal bitrate (if set) else the average of the
    // upper/lower bounds (if set) else -1 (unset).
    // 
    // If you want the actual bitrate field settings, get them from the
    // vorbis_info structs
    /**
     *
     * @param i
     * @return
     */
    public int bitrate(int i) {
        if (i >= links) {
            return (-1);
        }
        if (!seekable && i != 0) {
            return (bitrate(0));
        }
        if (i < 0) {
            long bits = 0;
            for (int j = 0; j < links; j++) {
                bits += (offsets[j + 1] - dataoffsets[j]) * 8;
            }
            return ((int) Math.rint(bits / time_total(-1)));
        } else {
            if (seekable) {
                // return the actual bitrate
                return ((int) Math.rint((offsets[i + 1] - dataoffsets[i]) * 8 / time_total(i)));
            } else {
                // return nominal if set
                if (vi[i].bitrate_nominal > 0) {
                    return vi[i].bitrate_nominal;
                } else {
                    if (vi[i].bitrate_upper > 0) {
                        if (vi[i].bitrate_lower > 0) {
                            return (vi[i].bitrate_upper + vi[i].bitrate_lower) / 2;
                        } else {
                            return vi[i].bitrate_upper;
                        }
                    }
                    return (-1);
                }
            }
        }
    }

    // returns the actual bitrate since last call.  returns -1 if no
    // additional data to offer since last call (or at beginning of stream)
    /**
     *
     * @return
     */
    public int bitrate_instant() {
        int _link = (seekable ? current_link : 0);
        if (samptrack == 0) {
            return (-1);
        }
        int ret = (int) (bittrack / samptrack * vi[_link].rate + .5);
        bittrack = 0.f;
        samptrack = 0.f;
        return (ret);
    }

    /**
     *
     * @param i
     * @return
     */
    public int serialnumber(int i) {
        if (i >= links) {
            return (-1);
        }
        if (!seekable && i >= 0) {
            return (serialnumber(-1));
        }
        if (i < 0) {
            return (current_serialno);
        } else {
            return (serialnos[i]);
        }
    }

    // returns: total raw (compressed) length of content if i==-1
    //          raw (compressed) length of that logical bitstream for i==0 to n
    //          -1 if the stream is not seekable (we can't know the length)
    /**
     *
     * @param i
     * @return
     */
    public long raw_total(int i) {
        if (!seekable || i >= links) {
            return (-1);
        }
        if (i < 0) {
            long acc = 0; // bug?
            for (int j = 0; j < links; j++) {
                acc += raw_total(j);
            }
            return (acc);
        } else {
            return (offsets[i + 1] - offsets[i]);
        }
    }

    // returns: total PCM length (samples) of content if i==-1
    //          PCM length (samples) of that logical bitstream for i==0 to n
    //          -1 if the stream is not seekable (we can't know the length)
    /**
     *
     * @param i
     * @return
     */
    public long pcm_total(int i) {
        if (!seekable || i >= links) {
            return (-1);
        }
        if (i < 0) {
            long acc = 0;
            for (int j = 0; j < links; j++) {
                acc += pcm_total(j);
            }
            return (acc);
        } else {
            return (pcmlengths[i]);
        }
    }

    // returns: total seconds of content if i==-1
    //          seconds in that logical bitstream for i==0 to n
    //          -1 if the stream is not seekable (we can't know the length)
    /**
     *
     * @param i
     * @return
     */
    public float time_total(int i) {
        if (!seekable || i >= links) {
            return (-1);
        }
        if (i < 0) {
            float acc = 0;
            for (int j = 0; j < links; j++) {
                acc += time_total(j);
            }
            return (acc);
        } else {
            return ((float) (pcmlengths[i]) / vi[i].rate);
        }
    }

    // seek to an offset relative to the *compressed* data. This also
    // immediately sucks in and decodes pages to update the PCM cursor. It
    // will cross a logical bitstream boundary, but only if it can't get
    // any packets out of the tail of the bitstream we seek to (so no
    // surprises). 
    // 
    // returns zero on success, nonzero on failure
    /**
     *
     * @param pos
     * @return
     */
    public int raw_seek(int pos) {
        if (!seekable) {
            return (-1); // don't dump machine if we can't seek
        }
        if (pos < 0 || pos > offsets[links]) {
            //goto seek_error;
            pcm_offset = -1;
            decode_clear();
            return -1;
        }

        // clear out decoding machine state
        pcm_offset = -1;
        decode_clear();

        // seek
        seek_helper(pos);

        // we need to make sure the pcm_offset is set.  We use the
        // _fetch_packet helper to process one packet with readp set, then
        // call it until it returns '0' with readp not set (the last packet
        // from a page has the 'granulepos' field set, and that's how the
        // helper updates the offset
        switch (process_packet(1)) {
            case 0:
                // oh, eof. There are no packets remaining.  Set the pcm offset to
                // the end of file
                pcm_offset = pcm_total(-1);
                return (0);
            case -1:
                // error! missing data or invalid bitstream structure
                //goto seek_error;
                pcm_offset = -1;
                decode_clear();
                return -1;
            default:
                // all OK
                break;
        }
        while (true) {
            switch (process_packet(0)) {
                case 0:
                    // the offset is set.  If it's a bogus bitstream with no offset
                    // information, it's not but that's not our fault.  We still run
                    // gracefully, we're just missing the offset
                    return (0);
                case -1:
                    // error! missing data or invalid bitstream structure
                    //goto seek_error;
                    pcm_offset = -1;
                    decode_clear();
                    return -1;
                default:
                    // continue processing packets
                    break;
            }
        }

        // seek_error:
        // dump the machine so we're in a known state
        //pcm_offset=-1;
        //decode_clear();
        //return -1;
    }

    // seek to a sample offset relative to the decompressed pcm stream 
    // returns zero on success, nonzero on failure
    /**
     *
     * @param pos
     * @return
     */
    public int pcm_seek(long pos) {
        int link = -1;
        long total = pcm_total(-1);

        if (!seekable) {
            return (-1); // don't dump machine if we can't seek
        }
        if (pos < 0 || pos > total) {
            //goto seek_error;
            pcm_offset = -1;
            decode_clear();
            return -1;
        }

        // which bitstream section does this pcm offset occur in?
        for (link = links - 1; link >= 0; link--) {
            total -= pcmlengths[link];
            if (pos >= total) {
                break;
            }
        }

        // search within the logical bitstream for the page with the highest
        // pcm_pos preceeding (or equal to) pos.  There is a danger here;
        // missing pages or incorrect frame number information in the
        // bitstream could make our task impossible.  Account for that (it
        // would be an error condition)
        {
            long target = pos - total;
            long end = offsets[link + 1];
            long begin = offsets[link];
            int best = (int) begin;

            Page og = new Page();
            while (begin < end) {
                long bisect;
                int ret;

                if (end - begin < CHUNKSIZE) {
                    bisect = begin;
                } else {
                    bisect = (end + begin) / 2;
                }

                seek_helper(bisect);
                ret = get_next_page(og, end - bisect);

                if (ret == -1) {
                    end = bisect;
                } else {
                    long granulepos = og.granulepos();
                    if (granulepos < target) {
                        best = ret; // raw offset of packet with granulepos
                        begin = offset; // raw offset of next packet
                    } else {
                        end = bisect;
                    }
                }
            }
            // found our page. seek to it (call raw_seek).
            if (raw_seek(best) != 0) {
                //goto seek_error;
                pcm_offset = -1;
                decode_clear();
                return -1;
            }
        }

        // verify result
        if (pcm_offset >= pos) {
            //goto seek_error;
            pcm_offset = -1;
            decode_clear();
            return -1;
        }
        if (pos > pcm_total(-1)) {
            //goto seek_error;
            pcm_offset = -1;
            decode_clear();
            return -1;
        }

        // discard samples until we reach the desired position. Crossing a
        // logical bitstream boundary with abandon is OK.
        while (pcm_offset < pos) {
            int target = (int) (pos - pcm_offset);
            float[][][] _pcm = new float[1][][];
            int[] _index = new int[getInfo(-1).channels];
            int samples = vd.synthesis_pcmout(_pcm, _index);

            if (samples > target) {
                samples = target;
            }
            vd.synthesis_read(samples);
            pcm_offset += samples;

            if (samples < target) {
                if (process_packet(1) == 0) {
                    pcm_offset = pcm_total(-1); // eof
                }
            }
        }
        return 0;

        // seek_error:
        // dump machine so we're in a known state
        //pcm_offset=-1;
        //decode_clear();
        //return -1;
    }

    // seek to a playback time relative to the decompressed pcm stream 
    // returns zero on success, nonzero on failure
    int time_seek(float seconds) {
        // translate time to PCM position and call pcm_seek

        int link = -1;
        long pcm_total = pcm_total(-1);
        float time_total = time_total(-1);

        if (!seekable) {
            return (-1); // don't dump machine if we can't seek
        }
        if (seconds < 0 || seconds > time_total) {
            //goto seek_error;
            pcm_offset = -1;
            decode_clear();
            return -1;
        }

        // which bitstream section does this time offset occur in?
        for (link = links - 1; link >= 0; link--) {
            pcm_total -= pcmlengths[link];
            time_total -= time_total(link);
            if (seconds >= time_total) {
                break;
            }
        }

        // enough information to convert time offset to pcm offset
        {
            long target = (long) (pcm_total + (seconds - time_total) * vi[link].rate);
            return (pcm_seek(target));
        }

        //seek_error:
        // dump machine so we're in a known state
        //pcm_offset=-1;
        //decode_clear();
        //return -1;
    }

    // tell the current stream offset cursor.  Note that seek followed by
    // tell will likely not give the set offset due to caching
    /**
     *
     * @return
     */
    public long raw_tell() {
        return (offset);
    }

    // return PCM offset (sample) of next PCM sample to be read
    /**
     *
     * @return
     */
    public long pcm_tell() {
        return (pcm_offset);
    }

    // return time offset (seconds) of next PCM sample to be read
    /**
     *
     * @return
     */
    public float time_tell() {
        // translate time to PCM position and call pcm_seek

        int link = -1;
        long pcm_total = 0;
        float time_total = 0.f;

        if (seekable) {
            pcm_total = pcm_total(-1);
            time_total = time_total(-1);

            // which bitstream section does this time offset occur in?
            for (link = links - 1; link >= 0; link--) {
                pcm_total -= pcmlengths[link];
                time_total -= time_total(link);
                if (pcm_offset >= pcm_total) {
                    break;
                }
            }
        }

        return (time_total + (float) (pcm_offset - pcm_total) / vi[link].rate);
    }

    //  link:   -1) return the vorbis_info struct for the bitstream section
    //              currently being decoded
    //         0-n) to request information for a specific bitstream section
    //
    // In the case of a non-seekable bitstream, any call returns the
    // current bitstream.  NULL in the case that the machine is not
    // initialized
    /**
     *
     * @param link
     * @return
     */
    public Info getInfo(int link) {
        if (seekable) {
            if (link < 0) {
                if (decode_ready) {
                    return vi[current_link];
                } else {
                    return null;
                }
            } else {
                if (link >= links) {
                    return null;
                } else {
                    return vi[link];
                }
            }
        } else {
            if (decode_ready) {
                return vi[0];
            } else {
                return null;
            }
        }
    }

    /**
     *
     * @param link
     * @return
     */
    public Comment getComment(int link) {
        if (seekable) {
            if (link < 0) {
                if (decode_ready) {
                    return vc[current_link];
                } else {
                    return null;
                }
            } else {
                if (link >= links) {
                    return null;
                } else {
                    return vc[link];
                }
            }
        } else {
            if (decode_ready) {
                return vc[0];
            } else {
                return null;
            }
        }
    }

    int host_is_big_endian() {
        return 1;
        //    short pattern = 0xbabe;
        //    unsigned char *bytewise = (unsigned char *)&pattern;
        //    if (bytewise[0] == 0xba) return 1;
        //    assert(bytewise[0] == 0xbe);
        //    return 0;
    }

    // up to this point, everything could more or less hide the multiple
    // logical bitstream nature of chaining from the toplevel application
    // if the toplevel application didn't particularly care.  However, at
    // the point that we actually read audio back, the multiple-section
    // nature must surface: Multiple bitstream sections do not necessarily
    // have to have the same number of channels or sampling rate.
    // 
    // read returns the sequential logical bitstream number currently
    // being decoded along with the PCM data in order that the toplevel
    // application can take action on channel/sample rate changes.  This
    // number will be incremented even for streamed (non-seekable) streams
    // (for seekable streams, it represents the actual logical bitstream
    // index within the physical bitstream.  Note that the accessor
    // functions above are aware of this dichotomy).
    //
    // input values: buffer) a buffer to hold packed PCM data for return
    //               length) the byte length requested to be placed into buffer
    //               bigendianp) should the data be packed LSB first (0) or
    //                           MSB first (1)
    //               word) word size for output.  currently 1 (byte) or 
    //                     2 (16 bit short)
    // 
    // return values: -1) error/hole in data
    //                 0) EOF
    //                 n) number of bytes of PCM actually returned.  The
    //                    below works on a packet-by-packet basis, so the
    //                    return length is not related to the 'length' passed
    //                    in, just guaranteed to fit.
    // 
    // *section) set to the logical bitstream number
    int read(byte[] buffer, int length, int bigendianp, int word, int sgned,
            int[] bitstream) {
        int host_endian = host_is_big_endian();
        int index = 0;

        while (true) {
            if (decode_ready) {
                float[][] pcm;
                float[][][] _pcm = new float[1][][];
                int[] _index = new int[getInfo(-1).channels];
                int samples = vd.synthesis_pcmout(_pcm, _index);
                pcm = _pcm[0];
                if (samples != 0) {
                    // yay! proceed to pack data into the byte buffer
                    int channels = getInfo(-1).channels;
                    int bytespersample = word * channels;
                    if (samples > length / bytespersample) {
                        samples = length / bytespersample;
                    }

                    // a tight loop to pack each size
                    {
                        int val;
                        if (word == 1) {
                            int off = (sgned != 0 ? 0 : 128);
                            for (int j = 0; j < samples; j++) {
                                for (int i = 0; i < channels; i++) {
                                    val = (int) (pcm[i][_index[i] + j] * 128. + 0.5);
                                    if (val > 127) {
                                        val = 127;
                                    } else if (val < -128) {
                                        val = -128;
                                    }
                                    buffer[index++] = (byte) (val + off);
                                }
                            }
                        } else {
                            int off = (sgned != 0 ? 0 : 32768);

                            if (host_endian == bigendianp) {
                                if (sgned != 0) {
                                    for (int i = 0; i < channels; i++) { // It's faster in this order
                                        int src = _index[i];
                                        int dest = i;
                                        for (int j = 0; j < samples; j++) {
                                            val = (int) (pcm[i][src + j] * 32768. + 0.5);
                                            if (val > 32767) {
                                                val = 32767;
                                            } else if (val < -32768) {
                                                val = -32768;
                                            }
                                            buffer[dest] = (byte) (val >>> 8);
                                            buffer[dest + 1] = (byte) (val);
                                            dest += channels * 2;
                                        }
                                    }
                                } else {
                                    for (int i = 0; i < channels; i++) {
                                        float[] src = pcm[i];
                                        int dest = i;
                                        for (int j = 0; j < samples; j++) {
                                            val = (int) (src[j] * 32768. + 0.5);
                                            if (val > 32767) {
                                                val = 32767;
                                            } else if (val < -32768) {
                                                val = -32768;
                                            }
                                            buffer[dest] = (byte) ((val + off) >>> 8);
                                            buffer[dest + 1] = (byte) (val + off);
                                            dest += channels * 2;
                                        }
                                    }
                                }
                            } else if (bigendianp != 0) {
                                for (int j = 0; j < samples; j++) {
                                    for (int i = 0; i < channels; i++) {
                                        val = (int) (pcm[i][j] * 32768. + 0.5);
                                        if (val > 32767) {
                                            val = 32767;
                                        } else if (val < -32768) {
                                            val = -32768;
                                        }
                                        val += off;
                                        buffer[index++] = (byte) (val >>> 8);
                                        buffer[index++] = (byte) val;
                                    }
                                }
                            } else {
                                //int val;
                                for (int j = 0; j < samples; j++) {
                                    for (int i = 0; i < channels; i++) {
                                        val = (int) (pcm[i][j] * 32768. + 0.5);
                                        if (val > 32767) {
                                            val = 32767;
                                        } else if (val < -32768) {
                                            val = -32768;
                                        }
                                        val += off;
                                        buffer[index++] = (byte) val;
                                        buffer[index++] = (byte) (val >>> 8);
                                    }
                                }
                            }
                        }
                    }

                    vd.synthesis_read(samples);
                    pcm_offset += samples;
                    if (bitstream != null) {
                        bitstream[0] = current_link;
                    }
                    return (samples * bytespersample);
                }
            }

            // suck in another packet
            switch (process_packet(1)) {
                case 0:
                    return (0);
                case -1:
                    return -1;
                default:
                    break;
            }
        }
    }

    /**
     *
     * @return
     */
    public Info[] getInfo() {
        return vi;
    }

    /**
     *
     * @return
     */
    public Comment[] getComment() {
        return vc;
    }

    /**
     *
     * @throws IOException
     */
    public void close() throws java.io.IOException {
        datasource.close();
    }

    static class SeekableInputStream extends InputStream {

        RandomAccessFile raf;
        final String mode = "r";

        SeekableInputStream(String file) throws java.io.IOException {
            this.raf = null;
            raf = new java.io.RandomAccessFile(file, mode);
        }

        @Override
        public int read() throws java.io.IOException {
            return raf.read();
        }

        @Override
        public int read(byte[] buf) throws java.io.IOException {
            return raf.read(buf);
        }

        @Override
        public int read(byte[] buf, int s, int len) throws java.io.IOException {
            return raf.read(buf, s, len);
        }

        @Override
        public long skip(long n) throws java.io.IOException {
            return (long) (raf.skipBytes((int) n));
        }

        public long getLength() throws java.io.IOException {
            return raf.length();
        }

        public long tell() throws java.io.IOException {
            return raf.getFilePointer();
        }

        @Override
        public int available() throws java.io.IOException {
            return (raf.length() == raf.getFilePointer()) ? 0 : 1;
        }

        @Override
        public void close() throws java.io.IOException {
            raf.close();
        }

        @Override
        public synchronized void mark(int m) {
        }

        @Override
        public synchronized void reset() throws java.io.IOException {
        }

        @Override
        public boolean markSupported() {
            return false;
        }

        public void seek(long pos) throws java.io.IOException {
            raf.seek(pos);
        }
    }

}
