package jp.sourceforge.nicoro;

import static jp.sourceforge.nicoro.Log.LOG_TAG;

import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.os.Handler;
//import android.os.Message;
import android.os.SystemClock;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

import java.lang.ref.WeakReference;
import java.nio.ByteBuffer;
import java.util.LinkedList;
//import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;


public class SurfaceVideoDrawer extends Thread
implements SurfaceHolder.Callback, SurfaceVideoDrawerInterface {
    private static final boolean DEBUG_LOGV = Release.IS_DEBUG & false;
    private static final boolean DEBUG_LOGD = Release.IS_DEBUG & true;

    private static final int FRAMESKIP_GAP_MAX = 1000;
//    private static final long THREAD_JOIN_TIME = 1000L * 3;

    public interface DrawMessage {
        void drawMessage(Canvas canvas, int vpos);
    }
    public interface VideoDrawBufferPool {
        void addToFill(VideoDrawBuffer<?> videoBuffer);
        void addToEmpty(VideoDrawBuffer<?> videoBuffer);
    }

    private SurfaceView mSurfaceView;
    private SurfaceHolder mSurfaceHolder;

    private int mWidthSurface = 0;
    private int mHeightSurface = 0;
    private int mDrawOffsetY = 0;

    private int mWidthVideo = 0;
    private int mHeightVideo = 0;

    private int mFrameRateNum = 0;
    private int mFrameRateDen = 0;

    private final AtomicLong mPlayStartTime = new AtomicLong();
    private int mFrameCurrent;
    private final AtomicLong mWhenLastDraw = new AtomicLong();

    private Matrix mMatrixScale = new Matrix();
    private Paint mPaint = new Paint();

    private boolean mUse16bitColor;
    private Bitmap mDrawBuffer16bit;

    private WeakReference<Handler> mRefEventHandler;
    private int mWhatReady;
    private int mWhatDestroyed;
    private int mWhatPlayFinished;

    private DrawMessage mDrawMessage;
    private VideoDrawBufferPool mVideoBufferPool;

    private int mFrameDurationMs;

    private final LinkedList<VideoDrawBuffer<?>> mVideoBuffer =
        new LinkedList<VideoDrawBuffer<?>>();
    private volatile boolean mIsFinish = false;
    private boolean mDrawAtOnce;
    private volatile boolean mIsPause = false;
    private volatile boolean mIsIdlePlay = false;
    private final ReentrantLock mLock = new ReentrantLock();
    private final Condition mCondition = mLock.newCondition();
    private final Condition mConditionIsIdlePlay = mLock.newCondition();
    private boolean mSurfaceOk = false;
    private volatile boolean mIsDecodeFinished = false;
    private boolean mWasFinishNotified = false;
    private boolean mWasSetViewSize = false;

    public SurfaceVideoDrawer(SurfaceView surfaceView) {
        super("SurfaceVideoDrawer");
        mSurfaceView = surfaceView;
        mSurfaceHolder = surfaceView.getHolder();
        mSurfaceHolder.addCallback(this);

        Context context = surfaceView.getContext();
        SharedPreferences sharedPreferences = Util.getDefaultSharedPreferencesMultiProcess(
                context);
        boolean bitmapFilter = sharedPreferences.getBoolean(
                context.getString(R.string.pref_key_ffmpeg_bitmap_filter), false);
        mPaint.setFilterBitmap(bitmapFilter);
        mUse16bitColor = sharedPreferences.getBoolean(
                context.getString(R.string.pref_key_ffmpeg_16bit_color), true);
        if (mUse16bitColor) {
            mSurfaceHolder.setFormat(PixelFormat.RGB_565);
        }
    }

    // Thread

    @Override
    public void run() {
        android.os.Process.setThreadPriority(
                android.os.Process.THREAD_PRIORITY_DISPLAY);

        VideoDrawBuffer<?> videoBufferCurrent = null;
        VideoDrawBuffer<?> videoBufferNext = null;
        final ReentrantLock lock = mLock;
        MAIN_LOOP : while (!mIsFinish) {
            if (mIsPause) {
                lock.lock();
                try {
                    mIsIdlePlay = true;
                    mConditionIsIdlePlay.signal();
                    mCondition.await(1000L, TimeUnit.MILLISECONDS);
                } catch (InterruptedException e) {
                } finally {
                    try {
                        mIsIdlePlay = false;
                    } finally {
                        lock.unlock();
                    }
                }
                continue;
            }

            videoBufferCurrent = videoBufferNext;
            assert mIsIdlePlay == false;
            lock.lock();
            try {
                if (videoBufferCurrent == null) {
                    videoBufferCurrent = mVideoBuffer.poll();
                    if (videoBufferCurrent == null) {
                        mIsIdlePlay = true;
                        if (mIsDecodeFinished && !mWasFinishNotified) {
                            mWasFinishNotified = true;
                            Handler handler = mRefEventHandler == null ? null : mRefEventHandler.get();
                            if (handler != null) {
                                handler.sendEmptyMessage(mWhatPlayFinished);
                            }
                        }
                        mConditionIsIdlePlay.signal();
                        mCondition.await(1000L, TimeUnit.MILLISECONDS);
                        continue;
                    }
                }
                videoBufferNext = mVideoBuffer.poll();
            } catch (InterruptedException e) {
            } finally {
                try {
                    mIsIdlePlay = false;
                } finally {
                    lock.unlock();
                }
            }

            long when = videoBufferCurrent.when;
            long now = SystemClock.uptimeMillis();

            if (DEBUG_LOGV) {
                Log.v(LOG_TAG, Log.buf().append("SurfaceVideoDrawer#run() when=")
                        .append(when).append(" now=").append(now)
                        .append(" videoBuffer.frame=").append(videoBufferCurrent.frame)
                        .append(" videoBufferNext.when=").append(
                                videoBufferNext == null
                                ? "(null)" : videoBufferNext.when)
                                .toString());
            }

            Object drawBuffer = videoBufferCurrent.strengthenBuffer();
            if (drawBuffer == null) {
                mVideoBufferPool.addToEmpty(videoBufferCurrent);
                continue;
            }

            boolean frameSkip = false;
            long gap = now - when;
//            final long waitGap = 0;
//            final long waitGap = mFrameDurationMs*4;
            final long waitGap = mFrameDurationMs*1;
            if (gap < -waitGap) {
                // 待つ
                if (!mDrawAtOnce) {
                    do {
                        lock.lock();
                        try {
                            mCondition.await(-gap - waitGap, TimeUnit.MILLISECONDS);
                        } catch (InterruptedException e) {
                        } finally {
                            lock.unlock();
                        }
                        if (mIsFinish) {
                            break MAIN_LOOP;
                        }
                        if (mDrawAtOnce) {
                            break;
                        }
                        now = SystemClock.uptimeMillis();
                        gap = now - when;
                    } while (gap < -waitGap);
                }
//            } else if (gap > mFrameDurationMs * 3) {
//                // TODO ここで仕切り直しすると微妙に音ずれする？
//                frameSkip = true;
////                updatePlayStartTime(videoBuffer.frame);
            } else if (gap > mFrameDurationMs * 2) {
                if (videoBufferNext != null) {
                    if ((now - videoBufferNext.when) >= -waitGap) {
                        frameSkip = true;
                    }
                }
            } else {
            }
            mDrawAtOnce = false;
            if (now - mWhenLastDraw.get() > FRAMESKIP_GAP_MAX) {
                frameSkip = false;
            }

            mFrameCurrent = videoBufferCurrent.frame;

            assert drawBuffer != null;
            if (mSurfaceOk) {
                if (!frameSkip) {
                    draw(drawBuffer);
                    mWhenLastDraw.set(now);
                } else {
                    if (DEBUG_LOGV) {
                        Log.v(LOG_TAG, "SurfaceVideoDrawer frameSkip");
                    }
                }
            }
            assert videoBufferCurrent.hasBufferRef();
            videoBufferCurrent.weakenBuffer();
            mVideoBufferPool.addToFill(videoBufferCurrent);
        }

        // 終了処理
        lock.lock();
        try {
            mConditionIsIdlePlay.signal();
            // 一応開放
            mVideoBuffer.clear();
//            mVideoBuffer = null;
        } finally {
            lock.unlock();
        }
        // 弱参照でしばらく持っているようなので、明示的に開放
        mSurfaceHolder.removeCallback(this);
        mSurfaceHolder = null;
//        mSurfaceView = null;
    }

    // SurfaceHolder.Callback

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        if (DEBUG_LOGD) {
            Log.d(LOG_TAG, Log.buf().append("surfaceChanged: format=").append(format)
                    .append(" width=").append(width).append(" height=").append(height).toString());
        }
        if (width == 0 || height == 0) {
            // 正しいサイズの変更が来るまで待つ
            return;
        }

        mWidthSurface = width;
        mHeightSurface = height;
        setScaleMatrix();

        if (mWasSetViewSize) {
            Handler handler = mRefEventHandler == null ? null : mRefEventHandler.get();
            if (handler != null) {
                handler.sendEmptyMessage(mWhatReady);
            }
        }
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        if (DEBUG_LOGD) {
            Log.d(LOG_TAG, "surfaceCreated");
        }
        mSurfaceOk = true;
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        if (DEBUG_LOGD) {
            Log.d(LOG_TAG, "surfaceDestroyed");
        }
        mSurfaceOk = false;
        Handler handler = mRefEventHandler == null ? null : mRefEventHandler.get();
        if (handler != null) {
            handler.sendEmptyMessage(mWhatDestroyed);
        }
//        quit();
    }

    @Override
    public boolean isNull() {
        return false;
    }

    @Override
    public void quit() {
        mIsFinish = true;
        mLock.lock();
        try {
            mCondition.signal();
        } finally {
            mLock.unlock();
        }
        try {
            // Thread止まらないとそれはそれで不具合出るので、タイムアウト無しに
            join();
        } catch (InterruptedException e) {
            Log.d(LOG_TAG, e.toString(), e);
        }
    }

    @Override
    public void quitAsync(ExecutorService executorService,
            final CountDownLatch latch) {
        if (mIsFinish) {
            latch.countDown();
        } else {
            mIsFinish = true;
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    quit();
                    latch.countDown();
                }
            });
        }
    }

    /**
     * 描画をポスト
     * @param videoBuffer
     */
    @Override
    public void postDraw(VideoDrawBuffer<?> videoBuffer) {
        if (mIsFinish) {
            if (DEBUG_LOGD) {
                Log.d(LOG_TAG, "SurfaceVideoDrawer#postDraw: already quit");
            }
            return;
        }

        long when = mPlayStartTime.get() + ((long) videoBuffer.frame * 1000L * mFrameRateDen / mFrameRateNum);
        videoBuffer.when = when;
        mLock.lock();
        try {
            mVideoBuffer.add(videoBuffer);
            mCondition.signal();
        } finally {
            mLock.unlock();
        }
    }

    /**
     * 描画待機中の先頭バッファを、時間を無視して直ちに描画させる
     */
    @Override
    public void drawAtOnce() {
        if (DEBUG_LOGV) {
            Log.v(LOG_TAG, "SurfaceVideoDrawer#drawAtOnce()");
        }
        mDrawAtOnce = true;
        mLock.lock();
        try {
            mCondition.signal();
        } finally {
            mLock.unlock();
        }
    }

    /**
     * 先頭バッファを強制的に取り返す
     * @return 強参照状態で返す
     */
    @Override
    public VideoDrawBuffer<?> getBackVideoBufferForcibly() {
        if (mIsFinish) {
            if (DEBUG_LOGD) {
                Log.d(LOG_TAG, "SurfaceVideoDrawer#getBackVideoBufferForcibly: already quit");
            }
            return null;
        }
        long now = SystemClock.uptimeMillis();
        if (now - mWhenLastDraw.get() > FRAMESKIP_GAP_MAX) {
            return null;
        }
//        if (mVideoBuffer.size() < 2) {
//            return null;
//        }
        while (true) {
            VideoDrawBuffer<?> videoBuffer;
            mLock.lock();
            try {
                videoBuffer = mVideoBuffer.poll();
            } finally {
                mLock.unlock();
            }
            if (videoBuffer == null) {
                return null;
            }
            if (videoBuffer.strengthenBuffer() != null) {
                return videoBuffer;
            }
        }
    }

    @Override
    public boolean hasLongFrameskip() {
        long now = SystemClock.uptimeMillis();
        return (now - mWhenLastDraw.get() > FRAMESKIP_GAP_MAX);
    }

    /**
     * {@link android.view.SurfaceView} のサイズを設定する
     * @param width
     * @param height
     */
    @Override
    public void setViewSize(int width, int height) {
        mWasSetViewSize = true;
        mSurfaceView.getLayoutParams().width = width;
        mSurfaceView.getLayoutParams().height = height;
        mSurfaceView.requestLayout();
        if (DEBUG_LOGD) {
            Log.d(LOG_TAG, Log.buf().append("SurfaceVideoDrawer: View width=").append(width)
                    .append(" height=").append(height).toString());
        }

        if (mWidthSurface == width && mHeightSurface == height) {
            Handler handler = mRefEventHandler == null ? null : mRefEventHandler.get();
            if (handler != null) {
                handler.sendEmptyMessage(mWhatReady);
            }
        }
    }

    /**
     * 動画のサイズを設定する
     * @param width
     * @param height
     */
    @Override
    public void setVideoSize(int width, int height) {
        mWidthVideo = width;
        mHeightVideo = height;
        setScaleMatrix();

        if (mUse16bitColor) {
            for (int i = 0;; ++i) {
                try {
                    mDrawBuffer16bit = Bitmap.createBitmap(width, height,
                            Bitmap.Config.RGB_565);
                    break;
                } catch (OutOfMemoryError e) {
                    Log.e(LOG_TAG, e.toString(), e);
                    if (i >= 3) {
                        throw e;
                    }
                    Util.gcStrong();
                    SystemClock.sleep(10);
                }
            }
        }
    }

    private void setScaleMatrix() {
        assert mMatrixScale != null;
        mMatrixScale.reset();
        if (mWidthVideo == 0 || mHeightVideo == 0
                || mWidthSurface == 0 || mHeightSurface == 0) {
            return;
        }
        float scaleWidth = (float) mWidthSurface / (float) mWidthVideo;
        float scaleHeight = (float) mHeightSurface / (float) mHeightVideo;
        float scale;
        if (scaleWidth < scaleHeight) {
            scale = scaleWidth;
            mDrawOffsetY = (int) ((mHeightSurface / scale - mHeightVideo) / 2);
        } else {
            scale = scaleHeight;
            mDrawOffsetY = 0;
        }
        mMatrixScale.postScale(scale, scale);
        if (DEBUG_LOGD) {
            Log.d(LOG_TAG, Log.buf().append("SurfaceVideoDrawer#setScaleMatrix: mMatrixScale=")
                    .append(mMatrixScale.toShortString()).toString());
        }
    }

    @Override
    public void registerEventHandler(Handler handler, int whatReady,
            int whatDestroyed, int whatPlayFinished) {
        mRefEventHandler = new WeakReference<Handler>(handler);
        mWhatReady = whatReady;
        mWhatDestroyed = whatDestroyed;
        mWhatPlayFinished = whatPlayFinished;
    }

    @Override
    public void setDrawMessage(DrawMessage drawMessage) {
        mDrawMessage = drawMessage;
    }
    @Override
    public void setVideoBufferPool(VideoDrawBufferPool videoBufferPool) {
        mVideoBufferPool = videoBufferPool;
    }

//    private void draw(int[] drawBuffer) {
    private void draw(Object drawBuffer) {
        assert mSurfaceHolder != null;
        Canvas canvas = mSurfaceHolder.lockCanvas();
        if (canvas == null) {
            if (DEBUG_LOGD) {
                Log.d(LOG_TAG, "lockCanvas NG");
            }
            return;
        }

        try {
            // 一応外側消去
            canvas.drawColor(0xFF000000);

            canvas.save(Canvas.MATRIX_SAVE_FLAG);
            canvas.concat(mMatrixScale);

            if (mUse16bitColor) {
                ByteBuffer byteBuffer = (ByteBuffer) drawBuffer;
                byteBuffer.rewind();
                mDrawBuffer16bit.copyPixelsFromBuffer(byteBuffer);

                canvas.drawBitmap(mDrawBuffer16bit,
                        0, mDrawOffsetY,
                        mPaint);
            } else {
                canvas.drawBitmap((int[]) drawBuffer, 0, mWidthVideo,
                        0, mDrawOffsetY,
                        mWidthVideo, mHeightVideo,
                        false, mPaint);
            }
            canvas.restore();

            // TODO 将来的にはたぶん別のViewでコメント表示
            int vpos = getVpos();
            mDrawMessage.drawMessage(canvas, vpos);
        } finally {
            mSurfaceHolder.unlockCanvasAndPost(canvas);
        }
    }

    @Override
    public void setFrameDurationMs(int duration) {
        mFrameDurationMs = duration;
    }

    @Override
    public void setFrameRate(int num, int den) {
        mFrameRateNum = num;
        mFrameRateDen = den;
    }

    @Override
    public void pause() {
        mIsPause = true;
    }
    @Override
    public void restart() {
        // 新しい時間で更新
        long now = SystemClock.uptimeMillis();
        updatePlayStartTime();
        long startTime = mPlayStartTime.get();
        mLock.lock();
        try {
            for (VideoDrawBuffer<?> videoBuffer : mVideoBuffer) {
                videoBuffer.when = startTime + ((long) videoBuffer.frame * 1000L * mFrameRateDen / mFrameRateNum);
                assert videoBuffer.when >= now;
            }
            mIsPause = false;
            mCondition.signal();
        } finally {
            mLock.unlock();
        }
    }

    @Override
    public void startSeek() {
        mFrameCurrent = 0;
        // 不要になったバッファを戻す
        while (true) {
            VideoDrawBuffer<?> videoBuffer;
            mLock.lock();
            try {
                videoBuffer = mVideoBuffer.poll();
            } finally {
                mLock.unlock();
            }
            if (videoBuffer == null) {
                break;
            }
            Object drawBuffer = videoBuffer.strengthenBuffer();
            if (drawBuffer == null) {
                mVideoBufferPool.addToEmpty(videoBuffer);
            } else {
                assert videoBuffer.hasBufferRef();
                videoBuffer.weakenBuffer();
                mVideoBufferPool.addToFill(videoBuffer);
            }
        }
    }

    @Override
    public void updatePlayStartTime() {
        if (mFrameRateNum == 0) {
            Log.w(LOG_TAG, "SurfaceVideoDrawer#updatePlayStartTime is called before frame rate is set");
            return;
        }

        long playStartTime = SystemClock.uptimeMillis();
        long now = playStartTime;
     // 既に進んでいるフレーム数ぶんだけ調整
        playStartTime -= ((long) mFrameCurrent * 1000L * mFrameRateDen / mFrameRateNum);
        mPlayStartTime.set(playStartTime);

        mLock.lock();
        try {
            for (VideoDrawBuffer<?> videoBuffer : mVideoBuffer) {
                videoBuffer.when = playStartTime + ((long) videoBuffer.frame * 1000L * mFrameRateDen / mFrameRateNum);
                assert videoBuffer.when >= now : "videoBuffer.when=" + videoBuffer.when + " now=" + now;
            }
        } finally {
            mLock.unlock();
        }
    }

    @Override
    public void updatePlayStartTime(int frameCurrent) {
        mFrameCurrent = frameCurrent;
        updatePlayStartTime();
    }

    @Override
    public void fixPlayStartTime(int diff) {
        long playStartTime = mPlayStartTime.addAndGet(diff);
        long now = SystemClock.uptimeMillis();

        mLock.lock();
        try {
            for (VideoDrawBuffer<?> videoBuffer : mVideoBuffer) {
                videoBuffer.when = playStartTime + ((long) videoBuffer.frame * 1000L * mFrameRateDen / mFrameRateNum);
                assert videoBuffer.when >= now;
            }
        } finally {
            mLock.unlock();
        }
    }

    @Override
    public void getCurrentPosition(Rational rational) {
        rational.num = (long) mFrameCurrent * mFrameRateDen;
        rational.den = mFrameRateNum;
    }

    @Override
    public void waitIdlePlay() {
        while (!mIsFinish) {
            mLock.lock();
            try {
                if (mIsIdlePlay) {
                    return;
                }
                mConditionIsIdlePlay.await(3000L, TimeUnit.MILLISECONDS);
            } catch (InterruptedException e) {
            } finally {
                mLock.unlock();
            }
        }
        if (DEBUG_LOGD) {
            Log.d(LOG_TAG, "SurfaceVideoDrawer#waitIdlePlay: already quit");
        }
    }

    /**
     * 描画待機中のバッファの個数を取得
     * @return
     */
    @Override
    public int getStandbyBufferSize() {
        mLock.lock();
        try {
            return mVideoBuffer.size();
        } finally {
            mLock.unlock();
        }
    }

    @Override
    public Handler getViewHandler() {
        return mSurfaceView.getHandler();
    }

    @Override
    public void setDecodeFinished(boolean isFinished) {
        mIsDecodeFinished = isFinished;
        if (!isFinished) {
            mWasFinishNotified = false;
        }
    }

    @Override
    public int getVpos() {
//        return (int) ((SystemClock.uptimeMillis() - mPlayStartTime) / 10L);
        if (mFrameRateNum == 0) {
            // 未設定の場合はとりあえず0を返す
            return 0;
        } else {
            return (int) ((long) mFrameCurrent * 100L * mFrameRateDen / mFrameRateNum);
        }
    }

    @Override
    public int getViewWidth() {
        int width = mSurfaceView.getLayoutParams().width;
        if (width >= 0) {
            return width;
        } else {
            return 0;
        }
    }

    @Override
    public int getViewHeight() {
        int height = mSurfaceView.getLayoutParams().height;
        if (height >= 0) {
            return height;
        } else {
            return 0;
        }
    }
}
