/*
 * Copyright (C) 2013 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.accessorydisplay.sink;

import com.android.accessorydisplay.common.Protocol;
import com.android.accessorydisplay.common.Service;
import com.android.accessorydisplay.common.Transport;

import android.content.Context;
import android.graphics.Rect;
import android.media.MediaCodec;
import android.media.MediaCodec.BufferInfo;
import android.media.MediaFormat;
import android.os.Handler;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

import java.nio.ByteBuffer;

public class DisplaySinkService extends Service implements SurfaceHolder.Callback {
    private final ByteBuffer mBuffer = ByteBuffer.allocate(12);
    private final Handler mTransportHandler;
    private final int mDensityDpi;

    private SurfaceView mSurfaceView;

    // These fields are guarded by the following lock.
    // This is to ensure that the surface lifecycle is respected.  Although decoding
    // happens on the transport thread, we are not allowed to access the surface after
    // it is destroyed by the UI thread so we need to stop the codec immediately.
    private final Object mSurfaceAndCodecLock = new Object();
    private Surface mSurface;
    private int mSurfaceWidth;
    private int mSurfaceHeight;
    private MediaCodec mCodec;
    private ByteBuffer[] mCodecInputBuffers;
    private BufferInfo mCodecBufferInfo;

    public DisplaySinkService(Context context, Transport transport, int densityDpi) {
        super(context, transport, Protocol.DisplaySinkService.ID);
        mTransportHandler = transport.getHandler();
        mDensityDpi = densityDpi;
    }

    public void setSurfaceView(final SurfaceView surfaceView) {
        if (mSurfaceView != surfaceView) {
            final SurfaceView oldSurfaceView = mSurfaceView;
            mSurfaceView = surfaceView;

            if (oldSurfaceView != null) {
                oldSurfaceView.post(new Runnable() {
                    @Override
                    public void run() {
                        final SurfaceHolder holder = oldSurfaceView.getHolder();
                        holder.removeCallback(DisplaySinkService.this);
                        updateSurfaceFromUi(null);
                    }
                });
            }

            if (surfaceView != null) {
                surfaceView.post(new Runnable() {
                    @Override
                    public void run() {
                        final SurfaceHolder holder = surfaceView.getHolder();
                        holder.addCallback(DisplaySinkService.this);
                        updateSurfaceFromUi(holder);
                    }
                });
            }
        }
    }

    @Override
    public void onMessageReceived(int service, int what, ByteBuffer content) {
        switch (what) {
            case Protocol.DisplaySinkService.MSG_QUERY: {
                getLogger().log("Received MSG_QUERY.");
                sendSinkStatus();
                break;
            }

            case Protocol.DisplaySinkService.MSG_CONTENT: {
                decode(content);
                break;
            }
        }
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        // Ignore.  Wait for surface changed event that follows.
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        updateSurfaceFromUi(holder);
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        updateSurfaceFromUi(null);
    }

    private void updateSurfaceFromUi(SurfaceHolder holder) {
        Surface surface = null;
        int width = 0, height = 0;
        if (holder != null && !holder.isCreating()) {
            surface = holder.getSurface();
            if (surface.isValid()) {
                final Rect frame = holder.getSurfaceFrame();
                width = frame.width();
                height = frame.height();
            } else {
                surface = null;
            }
        }

        synchronized (mSurfaceAndCodecLock) {
            if (mSurface == surface &&  mSurfaceWidth == width && mSurfaceHeight == height) {
                return;
            }

            mSurface = surface;
            mSurfaceWidth = width;
            mSurfaceHeight = height;

            if (mCodec != null) {
                mCodec.stop();
                mCodec = null;
                mCodecInputBuffers = null;
                mCodecBufferInfo = null;
            }

            if (mSurface != null) {
                MediaFormat format = MediaFormat.createVideoFormat(
                        "video/avc", mSurfaceWidth, mSurfaceHeight);
                mCodec = MediaCodec.createDecoderByType("video/avc");
                mCodec.configure(format, mSurface, null, 0);
                mCodec.start();
                mCodecBufferInfo = new BufferInfo();
            }

            mTransportHandler.post(new Runnable() {
                @Override
                public void run() {
                    sendSinkStatus();
                }
            });
        }
    }

    private void decode(ByteBuffer content) {
        if (content == null) {
            return;
        }
        synchronized (mSurfaceAndCodecLock) {
            if (mCodec == null) {
                return;
            }

            while (content.hasRemaining()) {
                if (!provideCodecInputLocked(content)) {
                    getLogger().log("Dropping content because there are no available buffers.");
                    return;
                }

                consumeCodecOutputLocked();
            }
        }
    }

    private boolean provideCodecInputLocked(ByteBuffer content) {
        final int index = mCodec.dequeueInputBuffer(0);
        if (index < 0) {
            return false;
        }
        if (mCodecInputBuffers == null) {
            mCodecInputBuffers = mCodec.getInputBuffers();
        }
        final ByteBuffer buffer = mCodecInputBuffers[index];
        final int capacity = buffer.capacity();
        buffer.clear();
        if (content.remaining() <= capacity) {
            buffer.put(content);
        } else {
            final int limit = content.limit();
            content.limit(content.position() + capacity);
            buffer.put(content);
            content.limit(limit);
        }
        buffer.flip();
        mCodec.queueInputBuffer(index, 0, buffer.limit(), 0, 0);
        return true;
    }

    private void consumeCodecOutputLocked() {
        for (;;) {
            final int index = mCodec.dequeueOutputBuffer(mCodecBufferInfo, 0);
            if (index >= 0) {
                mCodec.releaseOutputBuffer(index, true /*render*/);
            } else if (index != MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED
                    && index != MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                break;
            }
        }
    }

    private void sendSinkStatus() {
        synchronized (mSurfaceAndCodecLock) {
            if (mCodec != null) {
                mBuffer.clear();
                mBuffer.putInt(mSurfaceWidth);
                mBuffer.putInt(mSurfaceHeight);
                mBuffer.putInt(mDensityDpi);
                mBuffer.flip();
                getTransport().sendMessage(Protocol.DisplaySourceService.ID,
                        Protocol.DisplaySourceService.MSG_SINK_AVAILABLE, mBuffer);
            } else {
                getTransport().sendMessage(Protocol.DisplaySourceService.ID,
                        Protocol.DisplaySourceService.MSG_SINK_NOT_AVAILABLE, null);
            }
        }
    }
}
