/*
 * Copyright (c) 2002 Francisco Javier Cabello
 * Copyright (c) 2004 Guilhem Tardy (www.salyens.com)
 *
 * This library 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 2 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */
/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 *
 * $Id: NativeDecoder.java,v 1.2 2006/11/06 21:38:27 jonathanhueber Exp $
 *
 * Description
 * ============
 * This is a Decoder which takes H263 frames, or H263_RTP(rfc2190) packets
 * and generates either YUV420P or RGB output. This will eventually
 * also support other input format such as MPEG.
 *++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
/*
 * Revision 1.2  2004/02/19 Guilhem Tardy (gravsten@yahoo.com)
 * Major rewrite, now fully supports H.263/RTP (ffmpeg 0.4.7) and conversion to RGB24.
 */

package net.sourceforge.jffmpeg.ffmpegnative;

import javax.media.Buffer;
import javax.media.Codec;
import javax.media.Format;
import javax.media.ResourceUnavailableException;
import javax.media.format.VideoFormat;
import javax.media.format.RGBFormat;
import javax.media.format.YUVFormat;

import java.awt.Dimension;

import net.sourceforge.jffmpeg.JMFCodec;
import net.sourceforge.jffmpeg.CodecManager;

/**
 * A Codec to convert YUVFormat buffer to RGBFormat buffer.
 */
public class NativeDecoder implements JMFCodec {
    private boolean opened;
    private Format[] inputFormats;
    private Format[] outputFormats;
    private Format inputFormat;
    private Format outputFormat;


    private final static String PLUGIN_NAME = "FFMPEG Decoder";

    private final static int DEF_WIDTH = 352;
    private final static int DEF_HEIGHT = 288;

    private final static int INPUT_BUFFER_PADDING_SIZE = 8;

    private int inputH263Length;
    private int outputLength;

    private final static int MAX_PAYLOAD_SIZE = 2048;


    private String encoding;
    /**
     * Video size
     */
    private Dimension videoSize;

    // true if some parameter was changed and a reset is required,
    // false otherwise.
    protected boolean resetRequired = false;

    // If RTP is enabled, some portions of the decoding need to be
    // treated differently. Set this variable to true if we detect
    // an RTP protocol has been selected.
    private boolean rtpActive = false;

    /**
     * Do we need to provide the "Set Truncated" flag to FFMPEG?
     */
    private boolean truncatedFlag = false;

    // Sequence number used for RTP. Gaps between the sequence numbers
    // of incoming buffer indicate a packet loss or reordering.
    private long seqNum = 0;

    // Frame number
    private long frameNum = 0;

    // Timestamp of the last incoming buffer, used in case of packet loss or reordering.
    private long timestamp = 0;

    private float frameRate = 0;

    // Variables used for YUV420P to RGB24 conversion
    private boolean yuv2rgb = false;
    private int depth;
    private int rMask, gMask, bMask;

    //--------------------------------------------------------------------------------
    // NATIVE METHODS
    //--------------------------------------------------------------------------------

    private native boolean open_decoder(String codec, boolean rtp, boolean setTruncted, boolean yuv2rgb, int depth, int rMask, int gMask, int bMask, int width, int height);
    private native boolean close_decoder(int peer);

    // returns the number of bytes consumed, or negative if error
    private native int convert(int peer,
                               Object inData, int inBufSize, int inOffset, int inLength,
                               Object outData, int outLength, int eof);

    private native float extractFrameRate( int peer );

    //--------------------------------------------------------------------------------
    // NATIVE VARIABLES
    //--------------------------------------------------------------------------------

    // Native structure pointer
    public int peer = 0;

    /* Set to true if/when the native library is loaded */
    private static boolean nativeLibraryLoaded = false;
    static {
        CodecManager codecManager = new CodecManager();
        String libraryName = codecManager.getNativeLibraryName();
        if ( libraryName == null ) libraryName = "jffmpeg";
        try {
            System.loadLibrary( libraryName );
            nativeLibraryLoaded = true;
        }
        catch (UnsatisfiedLinkError e) {
            nativeLibraryLoaded = false;
        }
    }

    /**
     * Returns false if the native library failed to load
     */
    public boolean isCodecAvailable() {
        return nativeLibraryLoaded;
    }

    // Constructor
    public NativeDecoder() {
        // Specify the input formats. In this list we can be "general". The
        // framework will narrow down the options by itself and by use of the
        // other methods.
        inputFormats = new Format [] {
            new VideoFormat(VideoFormat.H263),
            new VideoFormat(VideoFormat.H263_RTP),
            new VideoFormat(VideoFormat.MPEG),
            new VideoFormat("mpeg video"),
            new VideoFormat("DIV3"),
            new VideoFormat("DIVX"),
            new VideoFormat("MP42"),
            new VideoFormat("MPG4"),
            new VideoFormat("WMV1"),
            new VideoFormat("WMV2"),
            new VideoFormat("MJPG"),
        };

        // Specify the list of generic output formats. This list will also be
        // "general" in nature.
        outputFormats = new Format[] {
            new YUVFormat(YUVFormat.YUV_420),
            new RGBFormat()
        };

        // Initialize parent members
        inputFormat  = null;
        outputFormat = null;
    }

    public void setYuv2rgb( boolean yuv2rgb ) {
        this.yuv2rgb = yuv2rgb;
    }

    public void setEncoding( String encoding ) {
        this.encoding = encoding;
    }

    public void setVideoSize( Dimension videoSize ) {
        this.videoSize = videoSize;
    }

    public void setIsRtp( boolean isRtp ) {
        this.rtpActive = isRtp;
    }

    public void setIsTruncated( boolean truncatedFlag ) {
        this.truncatedFlag = truncatedFlag;
    }

    /**
     * JMF calls this method when it has decided on an output format. This
     * format will be the result of the call to getSupportedOutputFormats().
     */
    public Format setOutputFormat(Format out) {

        VideoFormat videoOut = (VideoFormat)out;
        Dimension outSize = videoOut.getSize();

        // System.out.println("Decoder:: setOutputFormat(" + out.getEncoding() + ")");
        // System.out.println("\tfrom: " + videoOut.toString() + " size=" + outSize);

        if (outSize == null) {
            Dimension inSize = videoSize;
            if (inSize == null)
                outSize = new Dimension(DEF_WIDTH, DEF_HEIGHT);
            else
                outSize = inSize;
        }

        if (out instanceof YUVFormat) {
            yuv2rgb = false;
            YUVFormat yuv = (YUVFormat) out;

            if (yuv.getYuvType() != YUVFormat.YUV_420)
                return null;

            if (yuv.getOffsetU() > yuv.getOffsetV())
                return null;

            // TODO : Make other safety checks

            int strideY  = outSize.width;
            int strideUV = strideY / 2;
            int offsetU  = strideY * outSize.height;
            int offsetV  = offsetU + strideUV * outSize.height / 2;

            outputLength = (outSize.width * outSize.height * 3) / 2;
            outputFormat = new YUVFormat(outSize,
                outputLength,
                Format.byteArray,
                videoOut.getFrameRate(),
                YUVFormat.YUV_420,
                strideY,
                strideUV,
                0,
                offsetU,
                offsetV);
        } else if (out instanceof RGBFormat) {
            yuv2rgb = true;
            RGBFormat rgb = (RGBFormat) out;

            int bitsPerPixel = rgb.getBitsPerPixel();
            Class dataType = rgb.getDataType();

            int pixelStride = 1;

            switch (bitsPerPixel) {
            case 15:
            case 16:
                if (dataType != Format.byteArray &&
                    dataType != Format.shortArray)
                    return null;
                if (dataType == Format.byteArray)
                    pixelStride = 2;
                break;
            case 24:
                if (dataType != Format.byteArray)
                    return null;
                pixelStride = 3;
                break;
            case 32:
                if (dataType != Format.byteArray &&
                    dataType != Format.intArray)
                    return null;
                if (dataType == Format.byteArray)
                    pixelStride = 4;
                break;
            default:
                return null;
            }

            // TODO : Make other safety checks

            if (dataType == Format.byteArray) {
                rMask = 0x000000ff << ((rgb.getRedMask() - 1) * 8);
                gMask = 0x000000ff << ((rgb.getGreenMask() - 1) * 8);
                bMask = 0x000000ff << ((rgb.getBlueMask() - 1) * 8);
            } else {
                rMask = rgb.getRedMask();
                gMask = rgb.getGreenMask();
                bMask = rgb.getBlueMask();
            }
            depth = bitsPerPixel;

            int bytesPerPixel = (bitsPerPixel + 7) / 8;

            outputLength = outSize.width * outSize.height * bytesPerPixel;
            outputFormat = new RGBFormat(outSize,
                outputLength,
                dataType,
                videoOut.getFrameRate(),
                bitsPerPixel,
                rgb.getRedMask(), rgb.getGreenMask(), rgb.getBlueMask(),
                pixelStride,
                outSize.width * pixelStride,
                Format.FALSE, // flipped
                Format.NOT_SPECIFIED); // endian
        } else
            return null;

// System.out.println("Decoder:: outputFormat is now: " + outputFormat.toString());

        // Return the selected outputFormat
        return outputFormat;
    }

    /**
     * Get frame rate from Codec
     */
    public float getFrameRate( int peer ) {
        if ( frameRate <= 0 ) frameRate = extractFrameRate( peer );
        return frameRate;
    }

    /**
     * This flag can be used to allow data to start
     * in one buffer and end in the next.
     */
    private boolean quirkIncompatibleBuffering = false;
    private byte[] leftOver = null;

    /**
     * The last timestamp and the relative frame number
     */
    private long lastTime = 10;
    private int frames = 0;

    public int process(Buffer inBuffer, Buffer outBuffer) {
        int result;

        /* Do we need to set the time in the output buffer? */
        outBuffer.setFlags( inBuffer.getFlags() );
        if ( (inBuffer.getFlags() & Buffer.FLAG_NO_WAIT) == 0 ) {
            /* Has the input buffer specified a new timestamp */
            if ( lastTime != inBuffer.getTimeStamp() ) {
                lastTime = inBuffer.getTimeStamp();
                frames = 0;
            } else {
                /* Calculate time depending on frame rate */
                outBuffer.setFlags( inBuffer.getFlags() | Buffer.FLAG_RELATIVE_TIME | Buffer.FLAG_NO_DROP );
            }
            float currentFrameRate = ((VideoFormat)inBuffer.getFormat()).getFrameRate();
            if ( currentFrameRate <=0 ) {
                currentFrameRate = getFrameRate(peer);
            }
            outBuffer.setTimeStamp( inBuffer.getTimeStamp() + (long)(((long)1000000000)/currentFrameRate) * frames);
        }

        if ( quirkIncompatibleBuffering && leftOver != null ) {
            byte[] data = new byte[ leftOver.length + inBuffer.getLength() ];
            System.arraycopy( leftOver, 0, data, 0, leftOver.length );
            byte[] in = (byte[])inBuffer.getData();
            System.arraycopy( in, inBuffer.getOffset(), data, leftOver.length,
                              inBuffer.getLength() );
            inBuffer.setData( data );
            inBuffer.setOffset( 0 );
            inBuffer.setLength( data.length );
            leftOver = null;
        }
        // System.out.println("#"+inBuffer.getSequenceNumber()+", timestamp="+inBuffer.getTimeStamp());
        if ((inBuffer.getFlags() & Buffer.FLAG_EOM) != 0) {
            frames = 0;
            outBuffer.setFlags( outBuffer.getFlags() | Buffer.FLAG_EOM );
            reset();
            //System.out.println("return BUFFER_PROCESSED_OK");
            return Codec.BUFFER_PROCESSED_OK;
        }

        if (inBuffer.isDiscard() ) {
            outBuffer.setDiscard(true);
            reset();
            //System.out.println("return BUFFER_PROCESSED_OK");
            return Codec.BUFFER_PROCESSED_OK;
        }

        Format inFormat = inBuffer.getFormat();
//        if (inFormat != inputFormat && !(inFormat.matches(inputFormat))) {
            // System.out.println("format on inBuffer is " + inFormat.toString() + ", updating codec format");
//            setInputFormat(inFormat);
//        }

        if (inBuffer.getLength() < 5) {
            outBuffer.setDiscard(true);
            reset();
            //System.out.println("return BUFFER_PROCESSED_OK");
            return Codec.BUFFER_PROCESSED_OK;
        }

        int inOffset = inBuffer.getOffset();
        int inLength = inBuffer.getLength();

        // The codec might read up to INPUT_BUFFER_PADDING_SIZE additional bytes
        // before checking for EOS, which can cause ArrayOutOfBounds in Java code.
        byte[] curArray = (byte[])inBuffer.getData();
        int inBufSize = curArray.length;

        Object inData = (Object)inBuffer.getData();  //getInputData(inBuffer);  //
        long inDataBytes = 0; //getNativeData(inData);

        /* TODO - check type is correct */
        int[] outData = (int[])outBuffer.getData(); //getOutputData(outBuffer); 

        if (outData == null || outData.length < outputLength ||
            outBuffer.getFormat() != outputFormat ||
            !outBuffer.getFormat().equals(outputFormat)) {
// System.out.println("Decoder:: mismatch: " + (outData == null ? "NULL" : outData.toString()) + ", " + outBuffer.toString());

            outData = new int[ outputLength ];
            outBuffer.setLength(outputLength);
            outBuffer.setFormat(outputFormat);
            outBuffer.setData( outData );
        }

//        outData = validateData(outBuffer, outputLength, true /*allow native*/);
        long outDataBytes = 0; //getNativeData(outData);

        int eof; // end of frame, used only if RTP mode

        if (rtpActive) {
            eof = inBuffer.getFlags() & Buffer.FLAG_RTP_MARKER;

            // Initialize seqNum on first use
            if (seqNum == 0)
                seqNum = inBuffer.getSequenceNumber();

            // Try to detect loss of video packets by seqNum. Skip this
            // whole business if the timestamp is not set on the input
            // buffer.
            /** @todo find out how the timestamp is set on buffer in video decoder case */
            if (seqNum != inBuffer.getSequenceNumber() &&
                inBuffer.getTimeStamp() > 0 && timestamp > 0 ) {

// System.out.println("Decoder:: packet loss, #" + seqNum + " ->#" + inBuffer.getSequenceNumber() + (timestamp != inBuffer.getTimeStamp() ? ", across frames (try to recover)" : ", within a single frame"));

                if (timestamp != inBuffer.getTimeStamp()) {
                    // Try to recover last received frame
                    result = convert(peer, inData, 0, 0, 0, outData, outputLength, 0);
                    if ( result > 0 ) {
//                        outBuffer.setTimeStamp(timestamp);
                        outBuffer.setOffset(0);
                        outBuffer.setLength(outputLength);
                        frameNum++; // increment number of frames decoded
                        // System.out.println("return INPUT_BUFFER_NOT_CONSUMED");
                        seqNum = inBuffer.getSequenceNumber(); // prevent packet loss code from being triggered a second time
                        return Codec.INPUT_BUFFER_NOT_CONSUMED;
                    }
                }
            }

            // Update seqNum to the values from inBuffer
            timestamp = inBuffer.getTimeStamp();
            seqNum = inBuffer.getSequenceNumber();
        } else {
            eof = 1;
        }

        result = convert(peer, inData, inBufSize, inOffset, inLength, 
                         outData, outputLength, eof);

        if ( result > 0 ) {
            inBuffer.setOffset( inOffset + result );
            inBuffer.setLength( inLength - result );
	}

        if ( result < 0 ) {
            //System.out.println("return BUFFER_PROCESSED_FAILED");
            seqNum++;
            return Codec.BUFFER_PROCESSED_FAILED;
        }

        /** Not enough data */
        if ( result == 0 ) {
//            System.out.println("Not enough data " + inLength );
            if ( quirkIncompatibleBuffering ) {
                leftOver = new byte[ inLength ];
                System.arraycopy( inData, inOffset, leftOver, 0, inLength );
	    }
            frames++;
            return Codec.BUFFER_PROCESSED_OK;
        }

        if (eof == 0) {
// System.out.println("Decoder:: packet #" + seqNum + " (" + inLength + " bytes)");

            seqNum++;

            outBuffer.setDiscard(true);
            //System.out.println("return BUFFER_PROCESSED_OK");
            frames++;
            return Codec.BUFFER_PROCESSED_OK;
        }

// System.out.println("Decoder:: packet #" + seqNum + " (" + inLength + " bytes), end of frame #" + frameNum);

        // outBuffer.setTimeStamp(inBuffer.getTimeStamp());
        outBuffer.setOffset(0);
        outBuffer.setLength(outputLength);

        seqNum++;
        frameNum++; // increment number of frames decoded

        // At this point, the input and output frames buffers are done
        if (resetRequired && opened) reset();
        
        frames++;
        if ( inBuffer.getLength() > 0 ) {
            return Codec.INPUT_BUFFER_NOT_CONSUMED;
        }

        //System.out.println("return BUFFER_PROCESSED_OK");
        return Codec.BUFFER_PROCESSED_OK;
    }

    public synchronized void open() throws ResourceUnavailableException {
        if (!opened) {
// System.out.println("Decoder:: open()");

            synchronized (getClass()) {
//                if (inputFormat == null)
//                    throw new ResourceUnavailableException("No input format selected");
                if (outputFormat == null)
                    throw new ResourceUnavailableException("No output format selected");
		Dimension size = videoSize;
                if (!open_decoder(encoding, rtpActive, truncatedFlag, yuv2rgb, depth, rMask, gMask, bMask, 
  (int)size.getWidth(), (int)size.getHeight()))
                    throw new ResourceUnavailableException("Couldn't open codec for " + encoding);
            }

            resetRequired = false;
        }
    }

    public synchronized void close() {
        if (opened) {
// System.out.println("Decoder:: close()");
            close_decoder(peer);
        }
    }

    public synchronized void reset() {
        frames = 0;
        if (resetRequired && opened) {
// System.out.println("Decoder:: reset()");

            try {
                close();
                open();
            }
            catch(Exception e) {
                e.printStackTrace();
            }
        }
    }

    public String getName() {
        return PLUGIN_NAME;
    }

    // Private method for encapsulating the RTP format check
    // Ideally each RTP protocol that we support should be
    // added here.
    private boolean isRTPFormat(Format format) {
        if (format.getEncoding().equals(VideoFormat.H263_RTP))
            return true;

        return false;
    }

    // private method for finding the list of matching output formats
    // based on the input format.
    public Format [] getMatchingOutputFormats(Format in) {
        VideoFormat videoIn = (VideoFormat)in;
        Dimension inSize = videoIn.getSize();

        // System.out.println("Decoder:: getMatchingOutputFormats(" + in.getEncoding() + ", size:" + inSize + ", fps:" + videoIn.getFrameRate() + ")");

        int strideY  = inSize.width;
        int strideUV = strideY / 2;
        int offsetU  = strideY * inSize.height;
        int offsetV  = offsetU + strideUV * inSize.height / 2;

        VideoFormat[] result = new VideoFormat [] {
            new YUVFormat(inSize,
                (strideY + strideUV) * inSize.height,
                Format.byteArray,
                videoIn.getFrameRate(),
                YUVFormat.YUV_420,
                strideY,
                strideUV,
                0,
                offsetU,
                offsetV),
            new RGBFormat(inSize,
                inSize.width * inSize.height,
                Format.shortArray,
                videoIn.getFrameRate(),
                15,
                Format.NOT_SPECIFIED, Format.NOT_SPECIFIED, Format.NOT_SPECIFIED,
                1,
                inSize.width,
                Format.FALSE,
                Format.NOT_SPECIFIED),
            new RGBFormat(inSize,
                inSize.width * inSize.height,
                Format.shortArray,
                videoIn.getFrameRate(),
                16,
                Format.NOT_SPECIFIED, Format.NOT_SPECIFIED, Format.NOT_SPECIFIED,
                1,
                inSize.width,
                Format.FALSE,
                Format.NOT_SPECIFIED),
            new RGBFormat(inSize,
                inSize.width * inSize.height * 3,
                Format.byteArray,
                videoIn.getFrameRate(),
                24,
                Format.NOT_SPECIFIED, Format.NOT_SPECIFIED, Format.NOT_SPECIFIED,
                3,
                inSize.width * 3,
                Format.FALSE,
                Format.NOT_SPECIFIED),
            new RGBFormat(inSize,
                inSize.width * inSize.height,
                Format.intArray,
                videoIn.getFrameRate(),
                32,
                Format.NOT_SPECIFIED, Format.NOT_SPECIFIED, Format.NOT_SPECIFIED,
                1,
                inSize.width,
                Format.FALSE,
                Format.NOT_SPECIFIED),
            new RGBFormat(inSize,
                inSize.width * inSize.height,
                Format.shortArray,
                videoIn.getFrameRate(),
                16,
                0x7c00, 0x3e0, 0x1f,
                1,
                inSize.width,
                Format.FALSE,
                Format.NOT_SPECIFIED),
            new RGBFormat(inSize,
                inSize.width * inSize.height,
                Format.shortArray,
                videoIn.getFrameRate(),
                16,
                0xf800, 0x3e0, 0x1f,
                1,
                inSize.width,
                Format.FALSE,
                Format.NOT_SPECIFIED),
            new RGBFormat(inSize,
                inSize.width * inSize.height * 3,
                Format.byteArray,
                videoIn.getFrameRate(),
                24,
                3, 2, 1,
                3,
                inSize.width * 3,
                Format.FALSE,
                Format.NOT_SPECIFIED),
            new RGBFormat(inSize,
                inSize.width * inSize.height,
                Format.intArray,
                videoIn.getFrameRate(),
                32,
                0xff0000, 0x00ff00, 0x0000ff,
                1,
                inSize.width,
                Format.FALSE,
                Format.NOT_SPECIFIED)
        };
        return result;
    }

    public Format[] getSupportedOutputFormats(Format i ) {
        return null;
    }
}
