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

// DECODING PRIMITIVES: packet streaming layer
import java.util.logging.Logger;

/**
 * This has two layers to place more of the multi-serialno and paging
 * control in the application's hands.  First, we expose a data buffer
 * using ogg_decode_buffer().  The app either copies into the
 * buffer, or passes it directly to read(), etc.  We then call
 * ogg_decode_wrote() to tell how many bytes we just added.
 *
 * Pages are returned (pointers into the buffer in ogg_sync_state)
 * by ogg_decode_stream().  The page is then submitted to
 * ogg_decode_page() along with the appropriate
 * ogg_stream_state* (ie, matching serialno).  We then get raw
 * packets out calling ogg_stream_packet() with a
 * ogg_stream_state.  See the 'frame-prog.txt' docs for details and
 * example code.
 */
public class SyncState {

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

    /**
     *
     */
    public byte[] data;
    int storage;
    int fill;
    int returned;

    int unsynced;
    int headerbytes;
    int bodybytes;

    /**
     *
     * @return
     */
    public int clear() {
        data = null;
        return (0);
    }

    /**
     *
     * @param size
     * @return
     */
    public int buffer(int size) {
        // first, clear out any space that has been previously returned
        if (returned != 0) {
            fill -= returned;
            if (fill > 0) {
                System.arraycopy(data, returned, data, 0, fill);
            }
            returned = 0;
        }

        if (size > storage - fill) {
            // We need to extend the internal buffer
            int newsize = size + fill + 4096; // an extra page to be nice
            if (data != null) {
                byte[] foo = new byte[newsize];
                System.arraycopy(data, 0, foo, 0, data.length);
                data = foo;
            } else {
                data = new byte[newsize];
            }
            storage = newsize;
        }

        return (fill);
    }

    /**
     *
     * @param bytes
     * @return
     */
    public int wrote(int bytes) {
        if (fill + bytes > storage) {
            return (-1);
        }
        fill += bytes;
        return (0);
    }

    // sync the stream.  This is meant to be useful for finding page
    // boundaries.
    //
    // return values for this:
    // -n) skipped n bytes
    //  0) page not ready; more data (no bytes skipped)
    //  n) page synced at current location; page length n bytes
    private final Page pageseek = new Page();
    private final byte[] chksum = new byte[4];

    /**
     *
     * @param og
     * @return
     */
    public int pageseek(Page og) {
        int page = returned;
        int next;
        int bytes = fill - returned;

        if (headerbytes == 0) {
            int _headerbytes, i;
            if (bytes < 27) {
                return (0); // not enough for a header
            }
            /* verify capture pattern */
            if (data[page] != 'O' || data[page + 1] != 'g' || data[page + 2] != 'g'
                    || data[page + 3] != 'S') {
                headerbytes = 0;
                bodybytes = 0;

                // search for possible capture
                next = 0;
                for (int ii = 0; ii < bytes - 1; ii++) {
                    if (data[page + 1 + ii] == 'O') {
                        next = page + 1 + ii;
                        break;
                    }
                }
                //next=memchr(page+1,'O',bytes-1);
                if (next == 0) {
                    next = fill;
                }

                returned = next;
                return (-(next - page));
            }
            _headerbytes = (data[page + 26] & 0xff) + 27;
            if (bytes < _headerbytes) {
                return (0); // not enough for header + seg table
            }
            // count up body length in the segment table

            for (i = 0; i < (data[page + 26] & 0xff); i++) {
                bodybytes += (data[page + 27 + i] & 0xff);
            }
            headerbytes = _headerbytes;
        }

        if (bodybytes + headerbytes > bytes) {
            return (0);
        }

        // The whole test page is buffered.  Verify the checksum
        synchronized (chksum) {
            // Grab the checksum bytes, set the header field to zero

            System.arraycopy(data, page + 22, chksum, 0, 4);
            data[page + 22] = 0;
            data[page + 23] = 0;
            data[page + 24] = 0;
            data[page + 25] = 0;

            // set up a temp page struct and recompute the checksum
            Page log = pageseek;
            log.header_base = data;
            log.header = page;
            log.header_len = headerbytes;

            log.body_base = data;
            log.body = page + headerbytes;
            log.body_len = bodybytes;
            log.checksum();

            // Compare
            if (chksum[0] != data[page + 22] || chksum[1] != data[page + 23]
                    || chksum[2] != data[page + 24] || chksum[3] != data[page + 25]) {
                // D'oh.  Mismatch! Corrupt page (or miscapture and not a page at all)
                // replace the computed checksum with the one actually read in
                System.arraycopy(chksum, 0, data, page + 22, 4);
                // Bad checksum. Lose sync */

                headerbytes = 0;
                bodybytes = 0;
                // search for possible capture
                next = 0;
                for (int ii = 0; ii < bytes - 1; ii++) {
                    if (data[page + 1 + ii] == 'O') {
                        next = page + 1 + ii;
                        break;
                    }
                }
                //next=memchr(page+1,'O',bytes-1);
                if (next == 0) {
                    next = fill;
                }
                returned = next;
                return (-(next - page));
            }
        }

        // yes, have a whole page all ready to go
        {
            page = returned;

            if (og != null) {
                og.header_base = data;
                og.header = page;
                og.header_len = headerbytes;
                og.body_base = data;
                og.body = page + headerbytes;
                og.body_len = bodybytes;
            }

            unsynced = 0;
            returned += (bytes = headerbytes + bodybytes);
            headerbytes = 0;
            bodybytes = 0;
            return (bytes);
        }
    }

    // sync the stream and get a page.  Keep trying until we find a page.
    // Supress 'sync errors' after reporting the first.
    //
    // return values:
    //  -1) recapture (hole in data)
    //   0) need more data
    //   1) page returned
    //
    // Returns pointers into buffered data; invalidated by next call to
    // _stream, _clear, _init, or _buffer
    /**
     *
     * @param og
     * @return
     */
    public int pageout(Page og) {
        // all we need to do is verify a page at the head of the stream
        // buffer.  If it doesn't verify, we look for the next potential
        // frame

        while (true) {
            int ret = pageseek(og);
            if (ret > 0) {
                // have a page
                return (1);
            }
            if (ret == 0) {
                // need more data
                return (0);
            }

            // head did not start a synced page... skipped some bytes
            if (unsynced == 0) {
                unsynced = 1;
                return (-1);
            }
            // loop. keep looking
        }
    }

    // clear things to an initial state.  Good to call, eg, before seeking
    /**
     *
     * @return
     */
    public int reset() {
        fill = 0;
        returned = 0;
        unsynced = 0;
        headerbytes = 0;
        bodybytes = 0;
        return (0);
    }

    /**
     *
     */
    public void init() {
    }

    /**
     *
     * @return
     */
    public int getDataOffset() {
        return returned;
    }

    /**
     *
     * @return
     */
    public int getBufferOffset() {
        return fill;
    }
}
