/*
 * Java port of ffmpeg VOB demultiplexer.
 * Contains some liba52 and Xine code (GPL)
 * Copyright (c) 2003 Jonathan Hueber.
 *
 * Copyright (c) 2000, 2001, 2002 Fabrice Bellard.
 *
 * vobdemux is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * vobdemux 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 General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 * 1a39e335700bec46ae31a38e2156a898
 */
package org.monte.media.jmf.demux.vob;

import java.io.*;
import java.net.URL;

import javax.media.Demultiplexer;
import javax.media.protocol.Positionable;
import javax.media.protocol.Seekable;
import javax.media.Time;
import javax.media.protocol.ContentDescriptor;
import javax.media.protocol.FileTypeDescriptor;
import javax.media.Track;
import javax.media.protocol.DataSource;
import javax.media.protocol.PullDataSource;
import javax.media.protocol.PullSourceStream;
import javax.media.MediaLocator;
import javax.media.BadHeaderException;
import javax.media.Buffer;


import net.sourceforge.jffmpeg.GPLLicense;

/**
 * VOB file demultiplexer.  Effectively this simply maintains a HashMap
 * of data buffers representing the audio and video streams in the VOB file.
 *
 * Some timing information is tracked here
 */
public class VobDemux implements Demultiplexer, Positionable, GPLLicense {
    /**
     * Last time stamp read from Vob file    
     */
    private long timeStamp = 0;
    
    /**
     * Constants for maintaining Lip-sync
     */
    public static final int SYNC_CHANNEL = 0x80;
    public static final long MAX_AUDIO_BUFFER = 4200;
    public static final long MIN_AUDIO_BUFFER = 100;
    public static final boolean debugLipSync = false;
    public static final boolean showAllFrames = false;
    
    /**
     * File size for fast-forward and rewind
     */
    private long totalFileSize = 0;
    private long rateEstimateBase = 0;
    private long rateEstimateTime = 0;
    
    /**
     * Input and output streams
     */
    private InputStream in;
    private DataBuffer[] streams = new DataBuffer[ 512 ];
    
    /** Vob file constants */
    public static final int PACK_START_CODE          = 0x000001ba;
    public static final int SYSTEM_HEADER_START_CODE = 0x000001bb;
    public static final int SEQUENCE_END_CODE        = 0x000001b7;
    public static final int PACKET_START_CODE_MASK   = 0xffffff00;
    public static final int PACKET_START_CODE_PREFIX = 0x00000100;
    public static final int ISO_11172_END_CODE       = 0x000001b9;
  
    /* mpeg2  constants */
    public static final int PROGRAM_STREAM_MAP       = 0x000001bc;
    public static final int PRIVATE_STREAM_1         = 0x000001bd;
    public static final int PADDING_STREAM           = 0x000001be;
    public static final int PRIVATE_STREAM_2         = 0x000001bf;

    public static final int CODEC_TYPE_VIDEO         = 0;
    public static final int CODEC_TYPE_AUDIO         = 1;

    public static final int CODEC_ID_MPEG1VIDEO      = 0;
    public static final int CODEC_ID_MP2             = 1;
    public static final int CODEC_ID_AC3             = 2;
    public static final int CODEC_ID_PCM_S16BE       = 3;

    /** Empty constructor */
    public VobDemux() {
    }
  
    /** Creates a new instance of VobDemux based on an InputStream */
    public VobDemux( InputStream in ) {
        this.in = in;
    }
    
    /**
     * Read until we find a start code 0x000001xx
     */
    private int findStartCode() throws IOException {
        int state = 0xff;

        // BEGIN PATCH W. Randelshofer Abort if value is negative
        int value;

        do {
            state = ((state << 8) | (value=in.read())) & 0xffffffff;
        } while ( (state & 0xffffff00) != 0x100 && value != -1);
        // END PATCH W. Randelshofer
        return state;
    }
    
    /**
     * Read a Presentation Time Stamp
     */
    private long getPts( int c ) throws IOException {
        if (c < 0) c = in.read();
        long pts = (((long)c >> 1) & 0x07) << 30;
        
        long val = (in.read() << 8) | in.read();
        pts |= (val >> 1) << 15;
        val = (in.read() << 8) | in.read();
        pts |= (val >> 1);
        return pts;
    }

    /**
     * This Find and parse packet ID "readPacket"
     */
    protected synchronized void parse( int readPacket ) throws IOException {
        int startCode;
        do {
            /** Find packet ID */
            startCode = findStartCode();
            
            switch ( startCode ) {
                // BEGIN PATCH W. Randelshofer abort if we can't find the packet
                case -1:
                    return;
                // END PATCH W. Randelshofer abort if we can't find the packet

                case PACK_START_CODE: {
                    /* Read timestamp information */
                    int byte4 = in.read();
                    int byte5 = in.read();
                    int byte6 = in.read();
                    int byte7 = in.read();
                    int byte8 = in.read();
                    timeStamp = (( byte4 & 0x08 ) << 27)
                               |(( byte4 & 0x03 ) << 28)
                               |(( byte5        ) << 20)
                               |(( byte6 & 0xf8 ) << 12 )
                               |(( byte6 & 0x03 ) << 13 )
                               |(( byte7        ) << 5 )
                               |(( byte8 & 0xfe ) >> 3 );
                    timeStamp *= 300;
                    timeStamp /= 26900;
//                    timeStamp /= 28000;
                    break;
                }
                case SYSTEM_HEADER_START_CODE: {
                    /** Ignore information packet */
//                    System.out.println( "SystemHeaderStartCode" );
                    break;
                }
                case PADDING_STREAM:
                case PRIVATE_STREAM_2: {
                    /* Skip unsupported packets */
                    int length = (in.read() << 8) | in.read();
                    in.skip( length );
                    break;
                }
                default: {
                    /* Ignore unrecognised packets */
                    if (!((startCode >= 0x1c0 && startCode <= 0x1df) ||
                          (startCode >= 0x1e0 && startCode <= 0x1ef) ||
                          (startCode == 0x1bd))) {
//                        System.out.println( "Unrecognised " + startCode );
                        break;
                    }

                    /* packet length */
                    long pts = 0;
                    long dts = 0;
                    int length = (in.read() << 8) | in.read();
                    
                    /* Read control flags */
                    int c;
                    do {
                        c = in.read();
                        length--;
                    } while ( c == 0xff );

                    /* Escape code */
                    if ( (c & 0xc0) == 0x40 ) {
                        in.read();
                        c = in.read();
                        length -=2;
                    }
                    
                    /* Read Pts/Dts */
                    switch ( c & 0xf0 ) {
                        case 0x20: {
                            pts = getPts(c);
                            length -= 4;
                            break;
                        }
                        case 0x30: {
                            pts = getPts(c);
                            dts = getPts(-1);
                            length -= 9;
                            break;
                        }
                        default: {
                            if ( (c & 0xc0) != 0x80 ) break;
//                            if ( (c & 0x30) != 0) throw new IOException( "Encrypted streams not supported" );

                            /* Escaped header */
                            int flags = in.read();
                            int headerLength = in.read();
                            length -= 2;
                            if ( headerLength > length ) continue;

                            switch ( flags & 0xc0 ) {
                                case 0x80: {
                                    pts = getPts(-1);
                                    headerLength -= 5;
                                    length -= 5;
                                    break;
                                } 
                                case 0xc0: {
                                    pts = getPts(-1);
                                    dts = getPts(-1);
                                    headerLength -= 10;
                                    length -= 10;
                                    break;
                                }
                            }
                            
                            /* Skip remainder of header */
                            length -= headerLength;
                            in.skip( headerLength );
                            break;
                        }
                    }   

//                    System.out.println( "pts " + pts + " dts " + dts );
                    
                    /** Read Audio stream header 0x1bd */
                    if ( startCode == PRIVATE_STREAM_1 ) {
                        startCode = in.read();
                        length--;
                        if (startCode >= 0x80 && startCode <= 0xbf) {
                            /* audio: skip header */
                            in.skip( 3 );
                            length -= 3;
                        }
                    }

                    /* Stream ID is in startCode */
                    DataBuffer out = streams[ startCode ];
                    if ( out == null ) {
//                        System.out.println( "Allocate " + startCode );
                        out = allocateStream( startCode );
                        if ( out == null ) {
                            /* Unknown data */
                            in.skip( length );
                            break;
                        }
                    }
                    out.readData( timeStamp, in, length );
                    break;
                }
            }
        } while ( readPacket != startCode );
    }
    
    private DataBuffer allocateStream( int startCode ) {
        DataBuffer out;
//        int type;
//        int codec_id;
        if (startCode >= 0x1e0 && startCode <= 0x1ef) {
//            type = CODEC_TYPE_VIDEO;
//            codec_id = CODEC_ID_MPEG1VIDEO;
            out = new VideoTrack(this, startCode);
        } else if (startCode >= 0x1c0 && startCode <= 0x1df) {
//            type = CODEC_TYPE_AUDIO;
//            codec_id = CODEC_ID_MP2;
            out = new VideoTrack(this, startCode);
        } else if (startCode >= 0x80 && startCode <= 0x9f) {
//            type = CODEC_TYPE_AUDIO;
//            codec_id = CODEC_ID_AC3;
            out = new AudioTrack(this, startCode);
        } else if (startCode >= 0xa0 && startCode <= 0xbf) {
//            type = CODEC_TYPE_AUDIO;
//            codec_id = CODEC_ID_PCM_S16BE;
            out = new AudioTrack(this, startCode);
        } else {
            return null;
        }
        /* New stream */
        streams[ startCode] = out;
        return out;
    }
 
    /**
     * Read video data into buffer
     */
    protected synchronized void readVideo( int stream, Buffer buffer ) throws IOException {
        DataBuffer bsos;
        do {
            bsos = streams[ stream ];
            if ( bsos == null || bsos.getCurrentSize() == 0 ) {
                parse( SYNC_CHANNEL );
                bsos = null;
            }
        } while ( bsos == null );
        byte[] data = bsos.getBuffer();
        byte[] oldData = (byte[])buffer.getData();


        buffer.setData( data );
        buffer.setLength( bsos.getCurrentSize() );

        if ( oldData != null ) {
            bsos.resetBuffer( oldData );
        } else {
            bsos.resetBuffer( new byte[ data.length ] );
        }
    }
    
    /**
     * Read audio data into buffer
     */
    protected synchronized void readAudio( int stream, Buffer buffer ) throws IOException {
//        System.out.println( "byte rate " + estimateByteRate() );
        DataBuffer bsos;
        do {
            bsos = streams[ stream ];
            if ( bsos == null || (bsos.getCurrentSize() == 0)) {
                parse( SYNC_CHANNEL );
            }   
        } while (bsos == null);
            
        byte[] data = bsos.getBuffer();
        byte[] oldData = (byte[])buffer.getData();

        buffer.setData( data );
        buffer.setLength( bsos.getCurrentSize() );

        if ( oldData != null ) {
            bsos.resetBuffer( oldData );
        } else {
            bsos.resetBuffer( new byte[ data.length ] );
        }
    }
    
    /* Get Start time *
    private long startTime = 0;
    public long getStartTime() {
        return startTime;
    }
    */
    /** Required methods to be a Demultiplexer */    
    @Override
    public void close() {
    }    
    
    @Override
    public Object getControl(String str) {
        return null;
    }
    
    @Override
    public Object[] getControls() {
        return new Object[0];
    }

    Time estimatedDuration = new Time( 1000 );
    @Override
    public Time getDuration() {
        return estimatedDuration;
    }
    
    @Override
    public Time getMediaTime() {
       // System.out.println( "getMediaTime" );
        return new Time( 5000000 );
    }
    
    @Override
    public String getName() {
        return "VOB demux";
    }
    
    @Override
    public ContentDescriptor[] getSupportedInputContentDescriptors() {
//        System.out.println( "Requesting content descriptor" );
        return new ContentDescriptor[] {
            new FileTypeDescriptor( "video.mpeg" ),
            new FileTypeDescriptor( "video.vob" ),
        };
    }
    
    @Override
    public Track[] getTracks() throws IOException, BadHeaderException {
            // BEGIN PATCH Werner Randelshofer enable first audio track, disable the others
         java.util.LinkedList/*<Track>*/ tracks = new java.util.LinkedList/*<Track>*/();
         boolean isFirstAudioTrack = true;
         for (int i=0; i < streams.length; i++) {
             if (streams[i] != null) {
                 if (streams[i] instanceof AudioTrack) {
                     ((AudioTrack) streams[i]).setEnabled(isFirstAudioTrack);
                     isFirstAudioTrack = false;
                 }
                 tracks.add((Track) streams[i]);
             }
         }
         return (Track[]) tracks.toArray(new Track[tracks.size()]);
         /*   
        int length = 0;
        for ( int i = 0; i < streams.length; i++ ) {
            if ( streams[ i ] != null ) length++;
        }
        Track[] tracks = new Track[ length ];
        for ( int i = 0; i < streams.length; i++ ) {
            if ( streams[ i ] != null ) {
                --length;
                tracks[ length ] = (Track)streams[i];
            }
        }
        return tracks;*/
            // BEGIN PATCH Werner Randelshofer enable first audio track, disable the others
    }
    
    @Override
    public boolean isPositionable() {
        return true;
    }
    
    @Override
    public boolean isRandomAccess() {
        return true; 
    }
    
    @Override
    public void open() throws javax.media.ResourceUnavailableException {
    }
    
    @Override
    public void reset() {
    }
    
    @Override
    public Time setPosition(javax.media.Time time, int param) {

        /**
         * Clear pending buffers
         **/
        for ( int i = 0; i < streams.length; i++ ) {
            if ( streams[ i ] != null ) {
                DataBuffer dataBuffer = (DataBuffer)streams[ i ];
                dataBuffer.drop();
            }
        }
        
        try { 
             seekSource.seek( (time.getNanoseconds() * ESTIMATED_BYTE_RATE)/1000000000 );
             timeStamp = 0;
             start();
             System.out.println( "Aim for position " + (time.getNanoseconds()/1000000) + " actual " + timeStamp );
        } catch ( IOException e ) {
            e.printStackTrace();
        }
        startTime = 0;  //System.currentTimeMillis() - timeStamp - MAX_AUDIO_BUFFER;
        
        return time;
    }
    
    private PullSourceStream dataSource;
    private Seekable seekSource;
    
    @Override
    public void setSource( DataSource inputDataSource ) throws java.io.IOException, javax.media.IncompatibleSourceException {
        if ( inputDataSource instanceof PullDataSource ) {
            PullSourceStream pullSource = ((PullDataSource)inputDataSource).getStreams()[0];
            this.dataSource = pullSource;
            if ( pullSource instanceof Seekable ) {
                this.seekSource = (Seekable)pullSource;

//                System.out.println( "Data source " + this.dataSource + (this.dataSource instanceof Seekable) );
                
                /* Find file size */
                estimatedDuration = new Time( 1000 );
                try {
                    MediaLocator locator = inputDataSource.getLocator();
                    if ( locator != null ) {
                        URL url = locator.getURL();
                        if ( url != null ) {
                            File file = new File( url.getFile() );
                            totalFileSize = file.length();
//                System.out.println( "File size " + (totalFileSize * 1000000/ ESTIMATED_BYTE_RATE) );
                            estimatedDuration = new Time( (totalFileSize * 1000000000 / ESTIMATED_BYTE_RATE) );
                        }
                    }
                } catch ( IOException e ) {
                }
            }
            in = new PullSourceInputStream( pullSource );
            start();
            return;
        }
        throw new javax.media.IncompatibleSourceException();
    }
    
    public static final long ESTIMATED_BYTE_RATE = 540000;
    /*
    protected float estimateByteRate() {
        long time  = System.currentTimeMillis() - rateEstimateTime;
        long bytes = (seekSource.tell() - rateEstimateBase) * 1000;
        
        if ( time == 0 || rateEstimateTime == 0 || time > 50000 ) {
            rateEstimateTime = System.currentTimeMillis();
            rateEstimateBase = seekSource.tell();
            if ( time <= 50000 ) return ESTIMATED_BYTE_RATE;
        }
        return bytes / time;
    }
    */
    
    @Override
    public synchronized void start() throws java.io.IOException {
        parse( SYNC_CHANNEL );
        startTime = System.currentTimeMillis() - timeStamp - MAX_AUDIO_BUFFER;
        parse( SYNC_CHANNEL );
        parse( SYNC_CHANNEL );
        parse( SYNC_CHANNEL );
        parse( SYNC_CHANNEL );
        parse( SYNC_CHANNEL );
        parse( SYNC_CHANNEL );
        parse( SYNC_CHANNEL );
        parse( SYNC_CHANNEL );
        parse( SYNC_CHANNEL );
        parse( SYNC_CHANNEL );
        parse( SYNC_CHANNEL );
    }
    
    @Override
    public void stop() {
    }
    
    /**
     * The estimated startTime of the stream timer
     */
    private long startTime = 0;
    
    /**
     * Note the Audio is buffered so this is not exact.
     * We fiddle the startTime so that the video is never more than
     * MAX_AUDIO_BUFFER behind
     */
    public final void setAudioTimeStamp( long timeStamp ) {
        long time = System.currentTimeMillis() - startTime;
        
        /* Video is too far behind audio */
        if ( time + MAX_AUDIO_BUFFER < timeStamp ) {
if (debugLipSync)            System.out.println( "Audio resync " + time + " " + timeStamp );
            startTime -= timeStamp - time - MAX_AUDIO_BUFFER;
        }
        
        if ( time >= timeStamp && timeStamp > MAX_AUDIO_BUFFER ) {
            startTime -= timeStamp - time - MAX_AUDIO_BUFFER;
            return;
        }
        /* Video is too far behind audio */
        if ( time + MIN_AUDIO_BUFFER > timeStamp && timeStamp > MAX_AUDIO_BUFFER  ) {
if (debugLipSync)           System.out.println( "Audio behind " + time + " " + timeStamp );
            startTime -= timeStamp - time - MAX_AUDIO_BUFFER;
        }
    }
    
    /**
     * Returns true if the video is behind target
     */
    public final boolean isVideoSlow( long target ) {
        if ( showAllFrames ) return false;
        
        if ( startTime == 0 ) startTime = System.currentTimeMillis();  //demux.getStartTime();

        long time = System.currentTimeMillis() - startTime;
        
        if ( target < 1000 && time - target > 5000 ) {
            /* change video section */
            startTime = System.currentTimeMillis();
            return false;
        }
        
if (debugLipSync)       System.out.print( "time " + time + " target " + target  );
        if (target <= time) { 
if (debugLipSync)  System.out.println( " slow " + ( time - target ) );
            return true;
        }
if (debugLipSync)  System.out.println();
        if (target > time + 400) {
            synchronized( this ) {
                try {
                    if ( target - time - 15 < 200 ) {
if (debugLipSync)                    System.out.println( "Wait " + (target - time - 15 ) );
                    this.wait( target - time - 15 );
                    } else {
                        this.wait( 100 );
                    }
                } catch ( Exception e ) {}
            }
        }
        return false;
    }
}
