/* /////////////////////////////////////////////////////////////////////////////////////////////////
//
//  IMPORTANT: READ BEFORE DOWNLOADING, COPYING, INSTALLING OR USING.
//
//  By downloading, copying, installing or using the software you agree to this license.
//  If you do not agree to this license, do not download, install,
//  copy or use the software.
//
//                                 MIT License
//                            Copyright (c) 2019 VIA, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software
// and associated documentation files (the "Software"), to deal in the Software without restriction,
// including without limitation the rights to use, copy, modify, merge, publish, distribute,
// sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
// NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
//
// ////////////////////////////////////////////////////////////////////////////////////////////// */

package com.viatech.utility.video;

import android.annotation.TargetApi;
import android.media.Image;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaExtractor;
import android.media.MediaFormat;
import android.util.Log;

import com.viatech.utility.tool.Helper;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;

import static android.media.MediaFormat.KEY_COLOR_FORMAT;
import static android.media.MediaFormat.KEY_STRIDE;

@TargetApi(21)
public class MediaPlayerGrabber implements Runnable {
    private static final String TAG = "MediaPlayerGrabber";
    private static final boolean VERBOSE = false;
    private static final long DEFAULT_TIMEOUT_US = 10000;

    public static final int decodeColorFormat = MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar;

    private boolean stopDecode = false;
    private boolean stopDisplay = false;

    private String videoFilePath;
    private Throwable throwable;
    private Thread childThread;

    private Callback callback;

    boolean sawOutputEOS = false;

    boolean ready = false;

    long displayCounter = 0;


    public void interruptThread() {
        childThread.interrupt();
    }

    public interface Callback {
        void onFrameReady(ByteBuffer buffer, int offset, int width, int height, int colorFormat, int stride);
        void onImageReady(Image image);
    }

    public void setCallback(Callback callback) {
        this.callback = callback;
    }

    public void stop() {
        try {
            stopDisplay = true;
            if(displayThread!=null) displayThread.join();

            stopDecode = true;
            if(childThread!=null) childThread.join();

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void start() {
        try {
            decode();
        } catch (Throwable throwable1) {
            throwable1.printStackTrace();
        }
    }

    public void setDataSource(String file) {
        this.videoFilePath = file;
    }

    public void prepare() throws FileNotFoundException, IOException {
        File f = new File(videoFilePath);
        if(f.exists() && f.canRead()) {

        } else {
            if(!f.exists()) new FileNotFoundException("File not found");
            if(!f.canRead()) new IOException("File permission deined");
        }
    }

    private void decode() throws Throwable {
        if (childThread == null) {
            childThread = new Thread(this, "decode");
            childThread.start();
            if (throwable != null) {
                throw throwable;
            }
        }
    }

    public void run() {
        try {
            videoDecode(videoFilePath);
        } catch (Throwable t) {
            throwable = t;
        }
    }

    private void videoDecode(String videoFilePath) throws IOException {
        MediaExtractor extractor = null;
        MediaCodec decoder = null;
        try {
            File videoFile = new File(videoFilePath);
            extractor = new MediaExtractor();
            extractor.setDataSource(videoFile.toString());
            int trackIndex = selectTrack(extractor);
            if (trackIndex < 0) {
                throw new RuntimeException("No video track found in " + videoFilePath);
            }
            extractor.selectTrack(trackIndex);
            MediaFormat mediaFormat = extractor.getTrackFormat(trackIndex);
            String mime = mediaFormat.getString(MediaFormat.KEY_MIME);
            decoder = MediaCodec.createDecoderByType(mime);
            showSupportedColorFormat(decoder.getCodecInfo().getCapabilitiesForType(mime));
            if (isColorFormatSupported(decodeColorFormat, decoder.getCodecInfo().getCapabilitiesForType(mime))) {
                mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, decodeColorFormat);
                Log.i(TAG, "set decode color format to type " + decodeColorFormat);
            } else {
                Log.i(TAG, "unable to set decode color format, color format type " + decodeColorFormat + " not supported");
            }

            width = mediaFormat.getInteger(MediaFormat.KEY_WIDTH);
            height = mediaFormat.getInteger(MediaFormat.KEY_HEIGHT);
            decodeFrames(decoder, extractor, mediaFormat);
            decoder.stop();
        } finally {
            Log.d(TAG, "finally");
            if (decoder != null) {
                decoder.stop();
                decoder.release();
                decoder = null;
            }
            if (extractor != null) {
                extractor.release();
                extractor = null;
            }
        }
    }

    public static ArrayList<Integer> showSupportedColorFormat(MediaCodecInfo.CodecCapabilities caps) {
        System.out.print("supported color format: ");
        ArrayList<Integer> ret = new ArrayList<>();

        int i =0;
        for (int c : caps.colorFormats) {
            ret.add(c);
            System.out.print(c + "\t");
        }
        System.out.println();
        return ret;
    }

    private boolean isColorFormatSupported(int colorFormat, MediaCodecInfo.CodecCapabilities caps) {
        for (int c : caps.colorFormats) {
            if (c == colorFormat) {
                return true;
            }
        }
        return false;
    }

    int outputFrameCount = 0;
    int width = 0;//mediaFormat.getInteger(MediaFormat.KEY_WIDTH);
    int height = 0;//mediaFormat.getInteger(MediaFormat.KEY_HEIGHT);
    ByteBuffer[] inputByteBuffers = null;
    ByteBuffer[] outputByteBuffers = null;

    private void decodeFrames(final MediaCodec decoder, MediaExtractor extractor, MediaFormat mediaFormat) {
        boolean sawInputEOS = false;
        sawOutputEOS = false;
        decoder.configure(mediaFormat, null, null, 0);
        decoder.start();

        if(!Helper.isUpperThanAPI21()) {
            inputByteBuffers = decoder.getInputBuffers();
            outputByteBuffers = decoder.getOutputBuffers();
        }

        while (!sawOutputEOS && !stopDecode) {
            if (!sawInputEOS) {
                int inputBufferId = decoder.dequeueInputBuffer(DEFAULT_TIMEOUT_US);
                if (inputBufferId >= 0) {
                    ByteBuffer inputBuffer = null;
                    if (Helper.isUpperThanAPI21()) {
                        inputBuffer = decoder.getInputBuffer(inputBufferId);
                    } else {
                        inputBuffer = inputByteBuffers[inputBufferId];
                    }

                    int sampleSize = extractor.readSampleData(inputBuffer, 0);
                    if (sampleSize < 0) {
                        decoder.queueInputBuffer(inputBufferId, 0, 0, 0L, 0);
                        sawInputEOS = false;
                        extractor.seekTo(0, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);
                    } else {
                        long presentationTimeUs = extractor.getSampleTime();
                        decoder.queueInputBuffer(inputBufferId, 0, sampleSize, presentationTimeUs, 0);
                        extractor.advance();
                    }
                }

            }

            if(displayThread==null) {
                displayThread = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        while (!sawOutputEOS && !stopDisplay) {
                            frameDisplay(decoder);
                        }
                    }
                });
                displayThread.start();
            }
        }

    }

    Thread displayThread = null;
    long time = -1;

    private void frameDisplay(MediaCodec decoder) {
        MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();

        int srcColorFormat = 0;
        int srcStride = 0;
        int outputBufferId = decoder.dequeueOutputBuffer(info, DEFAULT_TIMEOUT_US);
        if (outputBufferId >= 0) {
            if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                sawOutputEOS = false;
            }

            boolean doRender = (info.size != 0);
            if (doRender) {
                outputFrameCount++;

                ByteBuffer b = null;
                Image image = null;

                if (Helper.isUpperThanAPI21()) {
                    image = decoder.getOutputImage(outputBufferId);
                    if(image!=null) {
//                        Log.d(TAG, "image:" + image.getWidth() + "," + image.getHeight() + "," + image.getFormat());
                    } else {
                        b = decoder.getOutputBuffer(outputBufferId);
                        MediaFormat format = decoder.getOutputFormat(outputBufferId);
                        srcColorFormat = format.getInteger(KEY_COLOR_FORMAT);
                        srcStride = format.getInteger(KEY_STRIDE);
                    }
                } else {
                    b = outputByteBuffers[outputBufferId];
                }

                if(time==-1) time = System.currentTimeMillis();
                else {
                    long diff = (System.currentTimeMillis()-time);
                    if(diff<33) {
                        //waitMs((33*1000-diff)/1000);
                        waitMs((33-diff));
                    }
                    time = System.currentTimeMillis();
                }

                if(callback!=null) {
                    if(image!=null) callback.onImageReady(image);
                    else if (b!=null) callback.onFrameReady(b,info.offset, width, height, srcColorFormat , srcStride);
                }

                decoder.releaseOutputBuffer(outputBufferId, false);
            }
        }
    }

    private void waitMs(long time) {
        try {
            Thread.sleep(time);
        } catch (Exception e) {

        }
    }


    private static int selectTrack(MediaExtractor extractor) {
        int numTracks = extractor.getTrackCount();
        for (int i = 0; i < numTracks; i++) {
            MediaFormat format = extractor.getTrackFormat(i);
            String mime = format.getString(MediaFormat.KEY_MIME);
            if (mime.startsWith("video/")) {
                if (VERBOSE) {
                    Log.d(TAG, "Extractor selected track " + i + " (" + mime + "): " + format);
                }
                return i;
            }
        }
        return -1;
    }
}