package client.audio;

import java.io.IOException;
import java.io.InputStream;

import client.audio.jogg.Packet;
import client.audio.jogg.Page;
import client.audio.jogg.StreamState;
import client.audio.jogg.SyncState;
import client.audio.jorbis.Block;
import client.audio.jorbis.Comment;
import client.audio.jorbis.DspState;
import client.audio.jorbis.Info;

public class CodecJOrbis
{
    private boolean endOfStream = false;
    private byte[] buffer = null;
    private int bufferSize;
    private int count = 0;
    private int index = 0;
    private short[] convertedBuffer = null;
    private float[][][] pcmInfo;
    private int[] pcmIndex;
    private Packet joggPacket = new Packet();
    private Page joggPage = new Page();
    private StreamState joggStreamState = new StreamState();
    private SyncState joggSyncState = new SyncState();
    private DspState jorbisDspState = new DspState();
    private Block jorbisBlock = new Block(jorbisDspState);
    private Comment jorbisComment = new Comment();
    private Info jorbisInfo = new Info();
    
    private CodecJOrbis() {
    }
    
    private boolean initialize( InputStream in ) throws IOException
    {
//        initialized( SET, false );

        if( joggStreamState != null )
            joggStreamState.clear();
        if( jorbisBlock != null )
            jorbisBlock.clear();
        if( jorbisDspState != null )
            jorbisDspState.clear();
        if( jorbisInfo != null )
            jorbisInfo.clear();
        if( joggSyncState != null )
            joggSyncState.clear();

//        this.url = url;
        //this.bufferSize = SoundSystemConfig.getStreamingBufferSize() / 2;
        this.bufferSize = 4096*2;

        buffer = null;
        count = 0;
        index = 0;

        joggStreamState = new StreamState();
        jorbisBlock = new Block(jorbisDspState);
        jorbisDspState = new DspState();
        jorbisInfo = new Info();
        joggSyncState = new SyncState();

//        URLConnection urlConnection = null;
//        try
//        {
//            urlConnection = url.openConnection();
//        }
//        catch( UnknownServiceException use )
//        {
//            errorMessage( "Unable to create a UrlConnection in method " +
//              "'initialize'." );
//            printStackTrace( use );
//            cleanup();
//            return false;
//        }
//        catch( IOException ioe )
//        {
//            errorMessage( "Unable to create a UrlConnection in method " +
//                          "'initialize'." );
//            printStackTrace( ioe );
//            cleanup();
//            return false;
//        }
//        if( urlConnection != null )
//        {
//            try
//            {
//          inputStream = in; // urlConnection.getInputStream();
//            }
//            catch( IOException ioe )
//            {
//                errorMessage( "Unable to acquire inputstream in method " +
//                              "'initialize'." );
//                printStackTrace( ioe );
//                cleanup();
//                return false;
//            }
//        }

//        endOfStream( SET, false );

        joggSyncState.init();
        joggSyncState.buffer( bufferSize );
        buffer = joggSyncState.data;

        try
        {
            if( !readHeader(in) )
            {
                errorMessage( "Error reading the header" );
                return false;
            }
        }
        catch( IOException ioe )
        {
            errorMessage( "Error reading the header" );
            return false;
        }

//        convertedBufferSize = bufferSize * 2;

        jorbisDspState.synthesis_init( jorbisInfo );
        jorbisBlock.init( jorbisDspState );

        if(jorbisInfo.channels != 1) {
        	errorMessage("Stream muss genau einen Kanal besitzen");
        	return false;
        }
        if(jorbisInfo.rate != 48000) {
        	errorMessage("Stream muss eine Samplerate von 48000 Hz besitzen");
        	return false;
        }

//        audioFormat = ;
        pcmInfo = new float[1][][];
        pcmIndex = new int[ 1 ];

//        initialized( SET, true );

        return true;
    }

    public static short[] readAll(InputStream in) throws IOException
    {
    	CodecJOrbis codec = new CodecJOrbis();
    	if(!codec.initialize(in))
    		return null;
        short[] returnBuffer = null;

        while( !codec.endOfStream )
        {
            if( returnBuffer == null )
                returnBuffer = codec.readBytes(in);
            else
                returnBuffer = appendByteArrays( returnBuffer, codec.readBytes(in) );
        }

//        if( returnBuffer == null )
//            return null;
//        this.cleanup();
        return returnBuffer; // new SoundBuffer( returnBuffer, audioFormat );
    }
    
    private boolean readHeader(InputStream inputStream) throws IOException
    {
        // Update up JOrbis internal buffer:
        index = joggSyncState.buffer( bufferSize );
        // Read in a buffer of data:
        int bytes = inputStream.read( joggSyncState.data, index, bufferSize );
        if( bytes < 0 )
            bytes = 0;
        // Let JOrbis know how many bytes we got:
        joggSyncState.wrote( bytes );

        if( joggSyncState.pageout( joggPage ) != 1 )
        {
            // Finished reading the entire file:
            if( bytes < bufferSize )
                return true;

            errorMessage( "Ogg header not recognized in method 'readHeader'." );
            return false;
        }

        // Initialize JOrbis:
        joggStreamState.init( joggPage.serialno() );

        jorbisInfo.init();
        jorbisComment.init();
        if( joggStreamState.pagein( joggPage ) < 0 )
        {
            errorMessage( "Problem with first Ogg header page in method " +
                          "'readHeader'." );
            return false;
        }

        if( joggStreamState.packetout( joggPacket ) != 1 )
        {
            errorMessage( "Problem with first Ogg header packet in method " +
                          "'readHeader'." );
            return false;
        }

        if( jorbisInfo.synthesis_headerin( jorbisComment, joggPacket ) < 0 )
        {
            errorMessage( "File does not contain Vorbis header in method " +
                          "'readHeader'." );
            return false;
        }

        int i = 0;
        while( i < 2 )
        {
            while( i < 2 )
            {
                int result = joggSyncState.pageout( joggPage );
                if( result == 0 )
                    break;
                if( result == 1 )
                {
                    joggStreamState.pagein( joggPage );
                    while( i < 2 )
                    {
                        result = joggStreamState.packetout( joggPacket );
                        if( result == 0 )
                            break;

                        if( result == -1 )
                        {
                            errorMessage( "Secondary Ogg header corrupt in " +
                                          "method 'readHeader'." );
                            return false;
                        }

                        jorbisInfo.synthesis_headerin( jorbisComment,
                                                       joggPacket );
                        i++;
                    }
                }
            }
            index = joggSyncState.buffer( bufferSize );
            bytes = inputStream.read( joggSyncState.data, index, bufferSize );
            if( bytes < 0 )
                bytes = 0;
            if( bytes == 0 && i < 2 )
            {
                errorMessage( "End of file reached before finished reading" +
                              "Ogg header in method 'readHeader'" );
                return false;
            }

            joggSyncState.wrote( bytes );
        }
        
        index = joggSyncState.buffer( bufferSize );
        buffer = joggSyncState.data;

        return true;
    }

    private short[] readBytes(InputStream inputStream) throws IOException
    {
//        if( !initialized( GET, XXX ) )
//            return null;

        if( endOfStream )
            return null;

        if( convertedBuffer == null )
            convertedBuffer = new short[ bufferSize ];
        short[] returnBuffer = null;

        float[][] pcmf;
        int samples, bout, /* ptr, */ mono, val, i, j;

        switch( joggSyncState.pageout( joggPage ) )
        {
            case( 0 ):
            case( -1 ):
                break;
            default:
            {
                joggStreamState.pagein( joggPage );
                if( joggPage.granulepos() == 0 )
                {
                    endOfStream = true;
                    return null;
                }

                processPackets: while( true )
                {
                    switch( joggStreamState.packetout( joggPacket ) )
                    {
                        case( 0 ):
                            break processPackets;
                        case( -1 ):
                            break;
                        default:
                        {
                            if( jorbisBlock.synthesis( joggPacket ) == 0 )
                                jorbisDspState.synthesis_blockin( jorbisBlock );

                            while( ( samples=jorbisDspState.synthesis_pcmout(
                                                     pcmInfo, pcmIndex ) ) > 0 )
                            {
                                pcmf = pcmInfo[0];
                                bout = ( samples < bufferSize ?
                                                samples : bufferSize );
//                                for( i = 0; i < jorbisInfo.channels; i++ )
//                                {
//                                ptr = 0; // i * 2;
                                mono = pcmIndex[0];
                                for( j = 0; j < bout; j++ )
                                {
                                    val = (int) ( pcmf[0][mono + j] *
                                                                   32767. );
                                    if( val > 32767 )
                                        val = 32767;
                                    if( val < -32768 )
                                        val = -32768;
                                    if( val < 0 )
                                        val = val | 0x8000;
                                    convertedBuffer[j] = (short)val;
//                                    convertedBuffer[ptr] = (byte) (val);
//                                    convertedBuffer[ptr+1] =
//                                                           (byte) (val>>>8);
//                                    ptr += 2 * (jorbisInfo.channels);
                                }
//                                }
                                jorbisDspState.synthesis_read( bout );

                                returnBuffer = appendByteArrays( returnBuffer,
                                               convertedBuffer,
                                               bout );
                            }
                        }
                    }
                }

                if( joggPage.eos() != 0 )
                    endOfStream = true;
            }
        }

        if( !endOfStream )
        {
            index = joggSyncState.buffer( bufferSize );
            buffer = joggSyncState.data;
//            try
//            {
                count = inputStream.read( buffer, index, bufferSize );
//            }
//            catch( Exception e )
//            {
//                printStackTrace( e );
//                return null;
//            }
            if( count == -1 )
                return returnBuffer;

            joggSyncState.wrote( count );
            if( count==0 )
                endOfStream = true;
        }

        return returnBuffer;
    }

/**
 * Creates a new array with the second array appended to the end of the first
 * array.
 * @param arrayOne The first array.
 * @param arrayTwo The second array.
 * @param arrayTwoBytes The number of bytes to append from the second array.
 * @return Byte array containing information from both arrays.
 */
    private static short[] appendByteArrays( short[] arrayOne, short[] arrayTwo,
                                            int arrayTwoBytes )
    {
    	short[] newArray;
        int bytes = arrayTwoBytes;

        // Make sure we aren't trying to append more than is there:
        if( arrayTwo == null || arrayTwo.length == 0 )
            bytes = 0;
        else if( arrayTwo.length < arrayTwoBytes )
            bytes = arrayTwo.length;

        if( arrayOne == null && (arrayTwo == null || bytes <= 0) )
        {
            // no data, just return
            return null;
        }
        else if( arrayOne == null )
        {
            // create the new array, same length as arrayTwo:
            newArray = new short[ bytes ];
            // fill the new array with the contents of arrayTwo:
            System.arraycopy( arrayTwo, 0, newArray, 0, bytes );
            arrayTwo = null;
        }
        else if( arrayTwo == null || bytes <= 0 )
        {
            // create the new array, same length as arrayOne:
            newArray = new short[ arrayOne.length ];
            // fill the new array with the contents of arrayOne:
            System.arraycopy( arrayOne, 0, newArray, 0, arrayOne.length );
            arrayOne = null;
        }
        else
        {
            // create the new array large enough to hold both arrays:
            newArray = new short[ arrayOne.length + bytes ];
            System.arraycopy( arrayOne, 0, newArray, 0, arrayOne.length );
            // fill the new array with the contents of both arrays:
            System.arraycopy( arrayTwo, 0, newArray, arrayOne.length,
                              bytes );
            arrayOne = null;
            arrayTwo = null;
        }

        return newArray;
    }

/**
 * Creates a new array with the second array appended to the end of the first
 * array.
 * @param arrayOne The first array.
 * @param arrayTwo The second array.
 * @return Byte array containing information from both arrays.
 */
    private static short[] appendByteArrays( short[] arrayOne, short[] arrayTwo )
    {
    	short[] newArray;
        if( arrayOne == null && arrayTwo == null )
        {
            // no data, just return
            return null;
        }
        else if( arrayOne == null )
        {
            // create the new array, same length as arrayTwo:
            newArray = new short[ arrayTwo.length ];
            // fill the new array with the contents of arrayTwo:
            System.arraycopy( arrayTwo, 0, newArray, 0, arrayTwo.length );
            arrayTwo = null;
        }
        else if( arrayTwo == null )
        {
            // create the new array, same length as arrayOne:
            newArray = new short[ arrayOne.length ];
            // fill the new array with the contents of arrayOne:
            System.arraycopy( arrayOne, 0, newArray, 0, arrayOne.length );
            arrayOne = null;
        }
        else
        {
            // create the new array large enough to hold both arrays:
            newArray = new short[ arrayOne.length + arrayTwo.length ];
            System.arraycopy( arrayOne, 0, newArray, 0, arrayOne.length );
            // fill the new array with the contents of both arrays:
            System.arraycopy( arrayTwo, 0, newArray, arrayOne.length,
                              arrayTwo.length );
            arrayOne = null;
            arrayTwo = null;
        }

        return newArray;
    }

    private void errorMessage( String message ) throws IOException
    {
        throw new IOException(message);
    }
}
