/*
 * 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.jogg;

import java.nio.charset.Charset;
import java.util.logging.Logger;

/**
 *
 */
public class StreamState {

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

    byte[] body_data; /* bytes from packet bodies */

    int body_storage; /* storage elements allocated */

    int body_fill; /* elements stored; fill mark */

    private int body_returned; /* elements of fill returned */

    int[] lacing_vals; /* The values that will go to the segment table */

    long[] granule_vals; /* pcm_pos values for headers. Not compact
     this way, but it is simple coupled to the
     lacing fifo */

    int lacing_storage;
    int lacing_fill;
    int lacing_packet;
    int lacing_returned;

    byte[] header = new byte[282]; /* working space for header encode */

    /**
     *
     */
    public int e_o_s; /* set when we have buffered the last packet in the
     logical bitstream */

    int b_o_s; /* set after we've written the initial page
     of a logical bitstream */

    int serialno;
    int pageno;
    long packetno; /* sequence number for decode; the framing
     knows where there's a hole in the data,
     but we need coupling so that the codec
     (which is in a seperate abstraction
     layer) also knows about the gap */

    long granulepos;

    /**
     *
     */
    public StreamState() {
        init();
    }

    StreamState(int serialno) {
        this();
        init(serialno);
    }

    /**
     *
     */
    public final void init() {
        body_storage = 16 * 1024;
        body_data = new byte[body_storage];
        lacing_storage = 1024;
        lacing_vals = new int[lacing_storage];
        granule_vals = new long[lacing_storage];
    }

    /**
     *
     * @param serialno
     */
    public final void init(int serialno) {
        if (body_data == null) {
            init();
        } else {
            for (int i = 0; i < body_data.length; i++) {
                body_data[i] = 0;
            }
            for (int i = 0; i < lacing_vals.length; i++) {
                lacing_vals[i] = 0;
            }
            for (int i = 0; i < granule_vals.length; i++) {
                granule_vals[i] = 0;
            }
        }
        this.serialno = serialno;
    }

    /**
     *
     */
    public void clear() {
        body_data = null;
        lacing_vals = null;
        granule_vals = null;
    }

    void destroy() {
        clear();
    }

    void body_expand(int needed) {
        if (body_storage <= body_fill + needed) {
            body_storage += (needed + 1024);
            byte[] foo = new byte[body_storage];
            System.arraycopy(body_data, 0, foo, 0, body_data.length);
            body_data = foo;
        }
    }

    void lacing_expand(int needed) {
        if (lacing_storage <= lacing_fill + needed) {
            lacing_storage += (needed + 32);
            int[] foo = new int[lacing_storage];
            System.arraycopy(lacing_vals, 0, foo, 0, lacing_vals.length);
            lacing_vals = foo;

            long[] bar = new long[lacing_storage];
            System.arraycopy(granule_vals, 0, bar, 0, granule_vals.length);
            granule_vals = bar;
        }
    }

    /* submit data to the internal buffer of the framing engine */
    /**
     *
     * @param op
     * @return
     */
    public int packetin(Packet op) {
        int lacing_val = op.bytes / 255 + 1;

        if (body_returned != 0) {
            /* advance packet data according to the body_returned pointer. We
             had to keep it around to return a pointer into the buffer last
             call */

            body_fill -= body_returned;
            if (body_fill != 0) {
                System.arraycopy(body_data, body_returned, body_data, 0, body_fill);
            }
            body_returned = 0;
        }

        /* make sure we have the buffer storage */
        body_expand(op.bytes);
        lacing_expand(lacing_val);

        /* Copy in the submitted packet.  Yes, the copy is a waste; this is
         the liability of overly clean abstraction for the time being.  It
         will actually be fairly easy to eliminate the extra copy in the
         future */
        System.arraycopy(op.packet_base, op.packet, body_data, body_fill, op.bytes);
        body_fill += op.bytes;

        /* Store lacing vals for this packet */
        int j;
        for (j = 0; j < lacing_val - 1; j++) {
            lacing_vals[lacing_fill + j] = 255;
            granule_vals[lacing_fill + j] = granulepos;
        }
        lacing_vals[lacing_fill + j] = (op.bytes) % 255;
        granulepos = granule_vals[lacing_fill + j] = op.granulepos;

        /* flag the first segment as the beginning of the packet */
        lacing_vals[lacing_fill] |= 0x100;

        lacing_fill += lacing_val;

        /* for the sake of completeness */
        packetno++;

        if (op.e_o_s != 0) {
            e_o_s = 1;
        }
        return (0);
    }

    /**
     *
     * @param op
     * @return
     */
    public int packetout(Packet op) {

        /* The last part of decode. We have the stream broken into packet
         segments.  Now we need to group them into packets (or return the
         out of sync markers) */
        int ptr = lacing_returned;

        if (lacing_packet <= ptr) {
            return (0);
        }

        if ((lacing_vals[ptr] & 0x400) != 0) {
            /* We lost sync here; let the app know */
            lacing_returned++;

            /* we need to tell the codec there's a gap; it might need to
             handle previous packet dependencies. */
            packetno++;
            return (-1);
        }

        /* Gather the whole packet. We'll have no holes or a partial packet */
        {
            int size = lacing_vals[ptr] & 0xff;
            int bytes = 0;

            op.packet_base = body_data;
            op.packet = body_returned;
            op.e_o_s = lacing_vals[ptr] & 0x200; /* last packet of the stream? */

            op.b_o_s = lacing_vals[ptr] & 0x100; /* first packet of the stream? */

            bytes += size;

            while (size == 255) {
                int val = lacing_vals[++ptr];
                size = val & 0xff;
                if ((val & 0x200) != 0) {
                    op.e_o_s = 0x200;
                }
                bytes += size;
            }

            op.packetno = packetno;
            op.granulepos = granule_vals[ptr];
            op.bytes = bytes;

            body_returned += bytes;

            lacing_returned = ptr + 1;
        }
        packetno++;
        return (1);
    }

    // add the incoming page to the stream state; we decompose the page
    // into packet segments here as well.
    /**
     *
     * @param og
     * @return
     */
    public int pagein(Page og) {
        byte[] header_base = og.header_base;
        int header = og.header;
        byte[] body_base = og.body_base;
        int body = og.body;
        int bodysize = og.body_len;
        int segptr = 0;

        int version = og.version();
        int continued = og.continued();
        int bos = og.bos();
        int eos = og.eos();
        long granulepos = og.granulepos();
        int _serialno = og.serialno();
        int _pageno = og.pageno();
        int segments = header_base[header + 26] & 0xff;

        // clean up 'returned data'
        {
            int lr = lacing_returned;
            int br = body_returned;

            // body data
            if (br != 0) {
                body_fill -= br;
                if (body_fill != 0) {
                    System.arraycopy(body_data, br, body_data, 0, body_fill);
                }
                body_returned = 0;
            }

            if (lr != 0) {
                // segment table
                if ((lacing_fill - lr) != 0) {
                    System.arraycopy(lacing_vals, lr, lacing_vals, 0, lacing_fill - lr);
                    System.arraycopy(granule_vals, lr, granule_vals, 0, lacing_fill - lr);
                }
                lacing_fill -= lr;
                lacing_packet -= lr;
                lacing_returned = 0;
            }
        }

        // check the serial number
        if (_serialno != serialno) {
            return (-1);
        }
        if (version > 0) {
            return (-1);
        }

        lacing_expand(segments + 1);

        // are we in sequence?
        if (_pageno != pageno) {
            int i;

            // unroll previous partial packet (if any)
            for (i = lacing_packet; i < lacing_fill; i++) {
                body_fill -= lacing_vals[i] & 0xff;
                //System.out.println("??");
            }
            lacing_fill = lacing_packet;

            // make a note of dropped data in segment table
            if (pageno != -1) {
                lacing_vals[lacing_fill++] = 0x400;
                lacing_packet++;
            }

            // are we a 'continued packet' page?  If so, we'll need to skip
            // some segments
            if (continued != 0) {
                bos = 0;
                for (; segptr < segments; segptr++) {
                    int val = (header_base[header + 27 + segptr] & 0xff);
                    body += val;
                    bodysize -= val;
                    if (val < 255) {
                        segptr++;
                        break;
                    }
                }
            }
        }

        if (bodysize != 0) {
            body_expand(bodysize);
            System.arraycopy(body_base, body, body_data, body_fill, bodysize);
            body_fill += bodysize;
        }

        {
            int saved = -1;
            while (segptr < segments) {
                int val = (header_base[header + 27 + segptr] & 0xff);
                lacing_vals[lacing_fill] = val;
                granule_vals[lacing_fill] = -1;

                if (bos != 0) {
                    lacing_vals[lacing_fill] |= 0x100;
                    bos = 0;
                }

                if (val < 255) {
                    saved = lacing_fill;
                }

                lacing_fill++;
                segptr++;

                if (val < 255) {
                    lacing_packet = lacing_fill;
                }
            }

            /* set the granulepos on the last pcmval of the last full packet */
            if (saved != -1) {
                granule_vals[saved] = granulepos;
            }
        }

        if (eos != 0) {
            e_o_s = 1;
            if (lacing_fill > 0) {
                lacing_vals[lacing_fill - 1] |= 0x200;
            }
        }

        pageno = _pageno + 1;
        return (0);
    }

    /* This will flush remaining packets into a page (returning nonzero),
     even if there is not enough data to trigger a flush normally
     (undersized page). If there are no packets or partial packets to
     flush, ogg_stream_flush returns 0.  Note that ogg_stream_flush will
     try to flush a normal sized page like ogg_stream_pageout; a call to
     ogg_stream_flush does not gurantee that all packets have flushed.
     Only a return value of 0 from ogg_stream_flush indicates all packet
     data is flushed into pages.

     ogg_stream_page will flush the last page in a stream even if it's
     undersized; you almost certainly want to use ogg_stream_pageout
     (and *not* ogg_stream_flush) unless you need to flush an undersized
     page in the middle of a stream for some reason. */
    /**
     *
     * @param og
     * @return
     */
    public int flush(Page og) {

        int i;
        int vals = 0;
        int maxvals = (lacing_fill > 255 ? 255 : lacing_fill);
        int bytes = 0;
        int acc = 0;
        long granule_pos = granule_vals[0];

        if (maxvals == 0) {
            return (0);
        }

        /* construct a page */
        /* decide how many segments to include */

        /* If this is the initial header case, the first page must only include
         the initial header packet */
        if (b_o_s == 0) { /* 'initial header page' case */

            granule_pos = 0;
            for (vals = 0; vals < maxvals; vals++) {
                if ((lacing_vals[vals] & 0x0ff) < 255) {
                    vals++;
                    break;
                }
            }
        } else {
            for (vals = 0; vals < maxvals; vals++) {
                if (acc > 4096) {
                    break;
                }
                acc += (lacing_vals[vals] & 0x0ff);
                granule_pos = granule_vals[vals];
            }
        }

        /* construct the header in temp storage */
        System.arraycopy("OggS".getBytes(Charset.forName("US-ASCII")), 0, header, 0, 4);

        /* stream structure version */
        header[4] = 0x00;

        /* continued packet flag? */
        header[5] = 0x00;
        if ((lacing_vals[0] & 0x100) == 0) {
            header[5] |= 0x01;
        }
        /* first page flag? */
        if (b_o_s == 0) {
            header[5] |= 0x02;
        }
        /* last page flag? */
        if (e_o_s != 0 && lacing_fill == vals) {
            header[5] |= 0x04;
        }
        b_o_s = 1;

        /* 64 bits of PCM position */
        for (i = 6; i < 14; i++) {
            header[i] = (byte) granule_pos;
            granule_pos >>>= 8;
        }

        /* 32 bits of stream serial number */
        {
            int _serialno = serialno;
            for (i = 14; i < 18; i++) {
                header[i] = (byte) _serialno;
                _serialno >>>= 8;
            }
        }

        /* 32 bits of page counter (we have both counter and page header
         because this val can roll over) */
        if (pageno == -1) {
            pageno = 0; /* because someone called
             stream_reset; this would be a
             strange thing to do in an
             encode stream, but it has
             plausible uses */

        }
        {
            int _pageno = pageno++;
            for (i = 18; i < 22; i++) {
                header[i] = (byte) _pageno;
                _pageno >>>= 8;
            }
        }

        /* zero for computation; filled in later */
        header[22] = 0;
        header[23] = 0;
        header[24] = 0;
        header[25] = 0;

        /* segment table */
        header[26] = (byte) vals;
        for (i = 0; i < vals; i++) {
            header[i + 27] = (byte) lacing_vals[i];
            bytes += (header[i + 27] & 0xff);
        }

        /* set pointers in the ogg_page struct */
        og.header_base = header;
        og.header = 0;
        og.header_len = vals + 27;
        og.body_base = body_data;
        og.body = body_returned;
        og.body_len = bytes;

        /* advance the lacing data and set the body_returned pointer */
        lacing_fill -= vals;
        System.arraycopy(lacing_vals, vals, lacing_vals, 0, lacing_fill * 4);
        System.arraycopy(granule_vals, vals, granule_vals, 0, lacing_fill * 8);
        body_returned += bytes;

        /* calculate the checksum */
        og.checksum();

        /* done */
        return (1);
    }

    /* This constructs pages from buffered packet segments.  The pointers
     returned are to static buffers; do not free. The returned buffers are
     good only until the next call (using the same ogg_stream_state) */
    /**
     *
     * @param og
     * @return
     */
    public int pageout(Page og) {
        if ((e_o_s != 0 && lacing_fill != 0)
                || /* 'were done, now flush' case */ body_fill - body_returned > 4096
                || /* 'page nominal size' case */ lacing_fill >= 255
                || /* 'segment table full' case */ (lacing_fill != 0 && b_o_s == 0)) { /* 'initial header page' case */

            return flush(og);
        }
        return 0;
    }

    /**
     *
     * @return
     */
    public int eof() {
        return e_o_s;
    }

    /**
     *
     * @return
     */
    public int reset() {
        body_fill = 0;
        body_returned = 0;

        lacing_fill = 0;
        lacing_packet = 0;
        lacing_returned = 0;

        e_o_s = 0;
        b_o_s = 0;
        pageno = -1;
        packetno = 0;
        granulepos = 0;
        return (0);
    }
}
