/*
 * Copyright (C) 2012 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.camera;

import android.annotation.TargetApi;
import android.graphics.SurfaceTexture;
import android.opengl.Matrix;
import android.util.Log;

import com.android.gallery3d.common.ApiHelper;
import com.android.gallery3d.glrenderer.GLCanvas;
import com.android.gallery3d.glrenderer.RawTexture;
import com.android.gallery3d.ui.SurfaceTextureScreenNail;

/*
 * This is a ScreenNail which can display camera's preview.
 */
@TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
public class CameraScreenNail extends SurfaceTextureScreenNail {
    private static final String TAG = "CAM_ScreenNail";
    private static final int ANIM_NONE = 0;
    // Capture animation is about to start.
    private static final int ANIM_CAPTURE_START = 1;
    // Capture animation is running.
    private static final int ANIM_CAPTURE_RUNNING = 2;
    // Switch camera animation needs to copy texture.
    private static final int ANIM_SWITCH_COPY_TEXTURE = 3;
    // Switch camera animation shows the initial feedback by darkening the
    // preview.
    private static final int ANIM_SWITCH_DARK_PREVIEW = 4;
    // Switch camera animation is waiting for the first frame.
    private static final int ANIM_SWITCH_WAITING_FIRST_FRAME = 5;
    // Switch camera animation is about to start.
    private static final int ANIM_SWITCH_START = 6;
    // Switch camera animation is running.
    private static final int ANIM_SWITCH_RUNNING = 7;

    private boolean mVisible;
    // True if first onFrameAvailable has been called. If screen nail is drawn
    // too early, it will be all white.
    private boolean mFirstFrameArrived;
    private Listener mListener;
    private final float[] mTextureTransformMatrix = new float[16];

    // Animation.
    private CaptureAnimManager mCaptureAnimManager = new CaptureAnimManager();
    private SwitchAnimManager mSwitchAnimManager = new SwitchAnimManager();
    private int mAnimState = ANIM_NONE;
    private RawTexture mAnimTexture;
    // Some methods are called by GL thread and some are called by main thread.
    // This protects mAnimState, mVisible, and surface texture. This also makes
    // sure some code are atomic. For example, requestRender and setting
    // mAnimState.
    private Object mLock = new Object();

    private OnFrameDrawnListener mOneTimeFrameDrawnListener;
    private int mRenderWidth;
    private int mRenderHeight;
    // This represents the scaled, uncropped size of the texture
    // Needed for FaceView
    private int mUncroppedRenderWidth;
    private int mUncroppedRenderHeight;
    private float mScaleX = 1f, mScaleY = 1f;
    private boolean mFullScreen;
    private boolean mEnableAspectRatioClamping = false;
    private boolean mAcquireTexture = false;
    private final DrawClient mDefaultDraw = new DrawClient() {
        @Override
        public void onDraw(GLCanvas canvas, int x, int y, int width, int height) {
            CameraScreenNail.super.draw(canvas, x, y, width, height);
        }

        @Override
        public boolean requiresSurfaceTexture() {
            return true;
        }
    };
    private DrawClient mDraw = mDefaultDraw;

    public interface Listener {
        void requestRender();
        // Preview has been copied to a texture.
        void onPreviewTextureCopied();

        void onCaptureTextureCopied();
    }

    public interface OnFrameDrawnListener {
        void onFrameDrawn(CameraScreenNail c);
    }

    public interface DrawClient {
        void onDraw(GLCanvas canvas, int x, int y, int width, int height);

        boolean requiresSurfaceTexture();
    }

    public CameraScreenNail(Listener listener) {
        mListener = listener;
    }

    public void setFullScreen(boolean full) {
        synchronized (mLock) {
            mFullScreen = full;
        }
    }

    /**
     * returns the uncropped, but scaled, width of the rendered texture
     */
    public int getUncroppedRenderWidth() {
        return mUncroppedRenderWidth;
    }

    /**
     * returns the uncropped, but scaled, width of the rendered texture
     */
    public int getUncroppedRenderHeight() {
        return mUncroppedRenderHeight;
    }

    @Override
    public int getWidth() {
        return mEnableAspectRatioClamping ? mRenderWidth : getTextureWidth();
    }

    @Override
    public int getHeight() {
        return mEnableAspectRatioClamping ? mRenderHeight : getTextureHeight();
    }

    private int getTextureWidth() {
        return super.getWidth();
    }

    private int getTextureHeight() {
        return super.getHeight();
    }

    @Override
    public void setSize(int w, int h) {
        super.setSize(w,  h);
        mEnableAspectRatioClamping = false;
        if (mRenderWidth == 0) {
            mRenderWidth = w;
            mRenderHeight = h;
        }
        updateRenderSize();
    }

    /**
     * Tells the ScreenNail to override the default aspect ratio scaling
     * and instead perform custom scaling to basically do a centerCrop instead
     * of the default centerInside
     *
     * Note that calls to setSize will disable this
     */
    public void enableAspectRatioClamping() {
        mEnableAspectRatioClamping = true;
        updateRenderSize();
    }

    private void setPreviewLayoutSize(int w, int h) {
        Log.i(TAG, "preview layout size: "+w+"/"+h);
        mRenderWidth = w;
        mRenderHeight = h;
        updateRenderSize();
    }

    private void updateRenderSize() {
        if (!mEnableAspectRatioClamping) {
            mScaleX = mScaleY = 1f;
            mUncroppedRenderWidth = getTextureWidth();
            mUncroppedRenderHeight = getTextureHeight();
            Log.i(TAG, "aspect ratio clamping disabled");
            return;
        }

        float aspectRatio;
        if (getTextureWidth() > getTextureHeight()) {
            aspectRatio = (float) getTextureWidth() / (float) getTextureHeight();
        } else {
            aspectRatio = (float) getTextureHeight() / (float) getTextureWidth();
        }
        float scaledTextureWidth, scaledTextureHeight;
        if (mRenderWidth > mRenderHeight) {
            scaledTextureWidth = Math.max(mRenderWidth,
                    (int) (mRenderHeight * aspectRatio));
            scaledTextureHeight = Math.max(mRenderHeight,
                    (int)(mRenderWidth / aspectRatio));
        } else {
            scaledTextureWidth = Math.max(mRenderWidth,
                    (int) (mRenderHeight / aspectRatio));
            scaledTextureHeight = Math.max(mRenderHeight,
                    (int) (mRenderWidth * aspectRatio));
        }
        mScaleX = mRenderWidth / scaledTextureWidth;
        mScaleY = mRenderHeight / scaledTextureHeight;
        mUncroppedRenderWidth = Math.round(scaledTextureWidth);
        mUncroppedRenderHeight = Math.round(scaledTextureHeight);
        Log.i(TAG, "aspect ratio clamping enabled, surfaceTexture scale: " + mScaleX + ", " + mScaleY);
    }

    public void acquireSurfaceTexture() {
        synchronized (mLock) {
            mFirstFrameArrived = false;
            mAnimTexture = new RawTexture(getTextureWidth(), getTextureHeight(), true);
            mAcquireTexture = true;
        }
        mListener.requestRender();
    }

    @Override
    public void releaseSurfaceTexture() {
        synchronized (mLock) {
            if (mAcquireTexture) {
                mAcquireTexture = false;
                mLock.notifyAll();
            } else {
                if (super.getSurfaceTexture() != null) {
                    super.releaseSurfaceTexture();
                }
                mAnimState = ANIM_NONE; // stop the animation
            }
        }
    }

    public void copyTexture() {
        synchronized (mLock) {
            mListener.requestRender();
            mAnimState = ANIM_SWITCH_COPY_TEXTURE;
        }
    }

    public void animateSwitchCamera() {
        Log.v(TAG, "animateSwitchCamera");
        synchronized (mLock) {
            if (mAnimState == ANIM_SWITCH_DARK_PREVIEW) {
                // Do not request render here because camera has been just
                // started. We do not want to draw black frames.
                mAnimState = ANIM_SWITCH_WAITING_FIRST_FRAME;
            }
        }
    }

    public void animateCapture(int displayRotation) {
        synchronized (mLock) {
            mCaptureAnimManager.setOrientation(displayRotation);
            mCaptureAnimManager.animateFlashAndSlide();
            mListener.requestRender();
            mAnimState = ANIM_CAPTURE_START;
        }
    }

    public RawTexture getAnimationTexture() {
        return mAnimTexture;
    }

    public void animateFlash(int displayRotation) {
        synchronized (mLock) {
            mCaptureAnimManager.setOrientation(displayRotation);
            mCaptureAnimManager.animateFlash();
            mListener.requestRender();
            mAnimState = ANIM_CAPTURE_START;
        }
    }

    public void animateSlide() {
        synchronized (mLock) {
            // Ignore the case where animateFlash is skipped but animateSlide is called
            // e.g. Double tap shutter and immediately swipe to gallery, and quickly swipe back
            // to camera. This case only happens in monkey tests, not applicable to normal
            // human beings.
            if (mAnimState != ANIM_CAPTURE_RUNNING) {
                Log.v(TAG, "Cannot animateSlide outside of animateCapture!"
                        + " Animation state = " + mAnimState);
                return;
            }
            mCaptureAnimManager.animateSlide();
            mListener.requestRender();
        }
    }

    private void callbackIfNeeded() {
        if (mOneTimeFrameDrawnListener != null) {
            mOneTimeFrameDrawnListener.onFrameDrawn(this);
            mOneTimeFrameDrawnListener = null;
        }
    }

    @Override
    protected void updateTransformMatrix(float[] matrix) {
        super.updateTransformMatrix(matrix);
        Matrix.translateM(matrix, 0, .5f, .5f, 0);
        Matrix.scaleM(matrix, 0, mScaleX, mScaleY, 1f);
        Matrix.translateM(matrix, 0, -.5f, -.5f, 0);
    }

    public void directDraw(GLCanvas canvas, int x, int y, int width, int height) {
        DrawClient draw;
        synchronized (mLock) {
            draw = mDraw;
        }
        draw.onDraw(canvas, x, y, width, height);
    }

    public void setDraw(DrawClient draw) {
        synchronized (mLock) {
            if (draw == null) {
                mDraw = mDefaultDraw;
            } else {
                mDraw = draw;
            }
        }
        mListener.requestRender();
    }

    @Override
    public void draw(GLCanvas canvas, int x, int y, int width, int height) {
        synchronized (mLock) {
            allocateTextureIfRequested(canvas);
            if (!mVisible) mVisible = true;
            SurfaceTexture surfaceTexture = getSurfaceTexture();
            if (mDraw.requiresSurfaceTexture() && (surfaceTexture == null || !mFirstFrameArrived)) {
                return;
            }

            switch (mAnimState) {
                case ANIM_NONE:
                    directDraw(canvas, x, y, width, height);
                    break;
                case ANIM_SWITCH_COPY_TEXTURE:
                    copyPreviewTexture(canvas);
                    mSwitchAnimManager.setReviewDrawingSize(width, height);
                    mListener.onPreviewTextureCopied();
                    mAnimState = ANIM_SWITCH_DARK_PREVIEW;
                    // The texture is ready. Fall through to draw darkened
                    // preview.
                case ANIM_SWITCH_DARK_PREVIEW:
                case ANIM_SWITCH_WAITING_FIRST_FRAME:
                    // Consume the frame. If the buffers are full,
                    // onFrameAvailable will not be called. Animation state
                    // relies on onFrameAvailable.
                    surfaceTexture.updateTexImage();
                    mSwitchAnimManager.drawDarkPreview(canvas, x, y, width,
                            height, mAnimTexture);
                    break;
                case ANIM_SWITCH_START:
                    mSwitchAnimManager.startAnimation();
                    mAnimState = ANIM_SWITCH_RUNNING;
                    break;
                case ANIM_CAPTURE_START:
                    copyPreviewTexture(canvas);
                    mListener.onCaptureTextureCopied();
                    mCaptureAnimManager.startAnimation(x, y, width, height);
                    mAnimState = ANIM_CAPTURE_RUNNING;
                    break;
            }

            if (mAnimState == ANIM_CAPTURE_RUNNING || mAnimState == ANIM_SWITCH_RUNNING) {
                boolean drawn;
                if (mAnimState == ANIM_CAPTURE_RUNNING) {
                    if (!mFullScreen) {
                        // Skip the animation if no longer in full screen mode
                        drawn = false;
                    } else {
                        drawn = mCaptureAnimManager.drawAnimation(canvas, this, mAnimTexture);
                    }
                } else {
                    drawn = mSwitchAnimManager.drawAnimation(canvas, x, y,
                            width, height, this, mAnimTexture);
                }
                if (drawn) {
                    mListener.requestRender();
                } else {
                    // Continue to the normal draw procedure if the animation is
                    // not drawn.
                    mAnimState = ANIM_NONE;
                    directDraw(canvas, x, y, width, height);
                }
            }
            callbackIfNeeded();
        } // mLock
    }

    private void copyPreviewTexture(GLCanvas canvas) {
        if (!mDraw.requiresSurfaceTexture() && mAnimTexture == null) {
            mAnimTexture = new RawTexture(getTextureWidth(), getTextureHeight(), true);
            mAnimTexture.setIsFlippedVertically(true);
        }
        int width = mAnimTexture.getWidth();
        int height = mAnimTexture.getHeight();
        canvas.beginRenderTarget(mAnimTexture);
        if (!mDraw.requiresSurfaceTexture()) {
            mDraw.onDraw(canvas, 0, 0, width, height);
        } else {
            // Flip preview texture vertically. OpenGL uses bottom left point
            // as the origin (0, 0).
            canvas.translate(0, height);
            canvas.scale(1, -1, 1);
            getSurfaceTexture().getTransformMatrix(mTextureTransformMatrix);
            updateTransformMatrix(mTextureTransformMatrix);
            canvas.drawTexture(mExtTexture, mTextureTransformMatrix, 0, 0, width, height);
        }
        canvas.endRenderTarget();
    }

    @Override
    public void noDraw() {
        synchronized (mLock) {
            mVisible = false;
        }
    }

    @Override
    public void recycle() {
        synchronized (mLock) {
            mVisible = false;
        }
    }

    @Override
    public void onFrameAvailable(SurfaceTexture surfaceTexture) {
        synchronized (mLock) {
            if (getSurfaceTexture() != surfaceTexture) {
                return;
            }
            mFirstFrameArrived = true;
            if (mVisible) {
                if (mAnimState == ANIM_SWITCH_WAITING_FIRST_FRAME) {
                    mAnimState = ANIM_SWITCH_START;
                }
                // We need to ask for re-render if the SurfaceTexture receives a new
                // frame.
                mListener.requestRender();
            }
        }
    }

    // We need to keep track of the size of preview frame on the screen because
    // it's needed when we do switch-camera animation. See comments in
    // SwitchAnimManager.java. This is based on the natural orientation, not the
    // view system orientation.
    public void setPreviewFrameLayoutSize(int width, int height) {
        synchronized (mLock) {
            mSwitchAnimManager.setPreviewFrameLayoutSize(width, height);
            setPreviewLayoutSize(width, height);
        }
    }

    public void setOneTimeOnFrameDrawnListener(OnFrameDrawnListener l) {
        synchronized (mLock) {
            mFirstFrameArrived = false;
            mOneTimeFrameDrawnListener = l;
        }
    }

    @Override
    public SurfaceTexture getSurfaceTexture() {
        synchronized (mLock) {
            SurfaceTexture surfaceTexture = super.getSurfaceTexture();
            if (surfaceTexture == null && mAcquireTexture) {
                try {
                    mLock.wait();
                    surfaceTexture = super.getSurfaceTexture();
                } catch (InterruptedException e) {
                    Log.w(TAG, "unexpected interruption");
                }
            }
            return surfaceTexture;
        }
    }

    private void allocateTextureIfRequested(GLCanvas canvas) {
        synchronized (mLock) {
            if (mAcquireTexture) {
                super.acquireSurfaceTexture(canvas);
                mAcquireTexture = false;
                mLock.notifyAll();
            }
        }
    }
}
