/*
 * 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.Matrix;
import android.graphics.Rect;
import android.graphics.RectF;
import android.hardware.Camera.Area;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;

import com.android.camera.app.AppController;
import com.android.camera.app.MotionManager;
import com.android.camera.debug.Log;
import com.android.camera.one.Settings3A;
import com.android.camera.settings.Keys;
import com.android.camera.settings.SettingsManager;
import com.android.camera.ui.PreviewStatusListener;
import com.android.camera.ui.TouchCoordinate;
import com.android.camera.util.CameraUtil;
import com.android.camera.util.UsageStatistics;
import com.android.ex.camera2.portability.CameraCapabilities;

import java.util.ArrayList;
import java.util.List;

/* A class that handles everything about focus in still picture mode.
 * This also handles the metering area because it is the same as focus area.
 *
 * The test cases:
 * (1) The camera has continuous autofocus. Move the camera. Take a picture when
 *     CAF is not in progress.
 * (2) The camera has continuous autofocus. Move the camera. Take a picture when
 *     CAF is in progress.
 * (3) The camera has face detection. Point the camera at some faces. Hold the
 *     shutter. Release to take a picture.
 * (4) The camera has face detection. Point the camera at some faces. Single tap
 *     the shutter to take a picture.
 * (5) The camera has autofocus. Single tap the shutter to take a picture.
 * (6) The camera has autofocus. Hold the shutter. Release to take a picture.
 * (7) The camera has no autofocus. Single tap the shutter and take a picture.
 * (8) The camera has autofocus and supports focus area. Touch the screen to
 *     trigger autofocus. Take a picture.
 * (9) The camera has autofocus and supports focus area. Touch the screen to
 *     trigger autofocus. Wait until it times out.
 * (10) The camera has no autofocus and supports metering area. Touch the screen
 *     to change metering area.
 */
public class FocusOverlayManager implements PreviewStatusListener.PreviewAreaChangedListener,
        MotionManager.MotionListener {
    private static final Log.Tag TAG = new Log.Tag("FocusOverlayMgr");

    private static final int RESET_TOUCH_FOCUS = 0;

    private static final int RESET_TOUCH_FOCUS_DELAY_MILLIS = Settings3A.getFocusHoldMillis();

    public static final float AF_REGION_BOX = Settings3A.getAutoFocusRegionWidth();
    public static final float AE_REGION_BOX = Settings3A.getMeteringRegionWidth();

    private int mState = STATE_IDLE;
    private static final int STATE_IDLE = 0; // Focus is not active.
    private static final int STATE_FOCUSING = 1; // Focus is in progress.
    // Focus is in progress and the camera should take a picture after focus finishes.
    private static final int STATE_FOCUSING_SNAP_ON_FINISH = 2;
    private static final int STATE_SUCCESS = 3; // Focus finishes and succeeds.
    private static final int STATE_FAIL = 4; // Focus finishes and fails.

    private boolean mInitialized;
    private boolean mFocusAreaSupported;
    private boolean mMeteringAreaSupported;
    private boolean mLockAeAwbNeeded;
    private boolean mAeAwbLock;
    private final Matrix mMatrix;

    private boolean mMirror; // true if the camera is front-facing.
    private int mDisplayOrientation;
    private List<Area> mFocusArea; // focus area in driver format
    private List<Area> mMeteringArea; // metering area in driver format
    private CameraCapabilities.FocusMode mFocusMode;
    private final List<CameraCapabilities.FocusMode> mDefaultFocusModes;
    private CameraCapabilities.FocusMode mOverrideFocusMode;
    private CameraCapabilities mCapabilities;
    private final AppController mAppController;
    private final SettingsManager mSettingsManager;
    private final Handler mHandler;
    Listener mListener;
    private boolean mPreviousMoving;
    private final FocusUI mUI;
    private final Rect mPreviewRect = new Rect(0, 0, 0, 0);
    private boolean mFocusLocked;

    /** Manual tap to focus parameters */
    private TouchCoordinate mTouchCoordinate;
    private long mTouchTime;

    public  interface FocusUI {
        public boolean hasFaces();
        public void clearFocus();
        public void setFocusPosition(int x, int y, boolean isPassiveScan, int aFsize, int aEsize);
        public void setFocusPosition(int x, int y, boolean isPassiveScan);
        public void onFocusStarted();
        public void onFocusSucceeded();
        public void onFocusFailed();
        public void setPassiveFocusSuccess(boolean success);
        public void showDebugMessage(String message);
        public void pauseFaceDetection();
        public void resumeFaceDetection();
    }

    public interface Listener {
        public void autoFocus();
        public void cancelAutoFocus();
        public boolean capture();
        public void startFaceDetection();
        public void stopFaceDetection();
        public void setFocusParameters();
    }

    private class MainHandler extends Handler {
        public MainHandler(Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case RESET_TOUCH_FOCUS: {
                    cancelAutoFocus();
                    mListener.startFaceDetection();
                    break;
                }
            }
        }
    }

    public FocusOverlayManager(AppController appController,
            List<CameraCapabilities.FocusMode> defaultFocusModes, CameraCapabilities capabilities,
            Listener listener, boolean mirror, Looper looper, FocusUI ui) {
        mAppController = appController;
        mSettingsManager = appController.getSettingsManager();
        mHandler = new MainHandler(looper);
        mMatrix = new Matrix();
        mDefaultFocusModes = new ArrayList<CameraCapabilities.FocusMode>(defaultFocusModes);
        updateCapabilities(capabilities);
        mListener = listener;
        setMirror(mirror);
        mUI = ui;
        mFocusLocked = false;
    }

    public void updateCapabilities(CameraCapabilities capabilities) {
        // capabilities can only be null when onConfigurationChanged is called
        // before camera is open. We will just return in this case, because
        // capabilities will be set again later with the right capabilities after
        // camera is open.
        if (capabilities == null) {
            return;
        }
        mCapabilities = capabilities;
        mFocusAreaSupported = mCapabilities.supports(CameraCapabilities.Feature.FOCUS_AREA);
        mMeteringAreaSupported = mCapabilities.supports(CameraCapabilities.Feature.METERING_AREA);
        mLockAeAwbNeeded = (mCapabilities.supports(CameraCapabilities.Feature.AUTO_EXPOSURE_LOCK)
                || mCapabilities.supports(CameraCapabilities.Feature.AUTO_WHITE_BALANCE_LOCK));
    }

    /** This setter should be the only way to mutate mPreviewRect. */
    public void setPreviewRect(Rect previewRect) {
        if (!mPreviewRect.equals(previewRect)) {
            mPreviewRect.set(previewRect);
            setMatrix();
        }
    }

    @Override
    public void onPreviewAreaChanged(RectF previewArea) {
        setPreviewRect(CameraUtil.rectFToRect(previewArea));
    }

    /** Returns a copy of mPreviewRect so that outside class cannot modify preview
     *  rect except deliberately doing so through the setter. */
    public Rect getPreviewRect() {
        return new Rect(mPreviewRect);
    }

    public void setMirror(boolean mirror) {
        mMirror = mirror;
        setMatrix();
    }

    public void setDisplayOrientation(int displayOrientation) {
        mDisplayOrientation = displayOrientation;
        setMatrix();
    }

    private void setMatrix() {
        if (mPreviewRect.width() != 0 && mPreviewRect.height() != 0) {
            Matrix matrix = new Matrix();
            CameraUtil.prepareMatrix(matrix, mMirror, mDisplayOrientation, getPreviewRect());
            // In face detection, the matrix converts the driver coordinates to UI
            // coordinates. In tap focus, the inverted matrix converts the UI
            // coordinates to driver coordinates.
            matrix.invert(mMatrix);
            mInitialized = true;
        }
    }

    private void lockAeAwbIfNeeded() {
        if (mLockAeAwbNeeded && !mAeAwbLock) {
            mAeAwbLock = true;
            mListener.setFocusParameters();
        }
    }

    private void unlockAeAwbIfNeeded() {
        if (mLockAeAwbNeeded && mAeAwbLock && (mState != STATE_FOCUSING_SNAP_ON_FINISH)) {
            mAeAwbLock = false;
            mListener.setFocusParameters();
        }
    }

    public void onShutterUp(CameraCapabilities.FocusMode currentFocusMode) {
        if (!mInitialized) {
            return;
        }

        if (needAutoFocusCall(currentFocusMode)) {
            // User releases half-pressed focus key.
            if (mState == STATE_FOCUSING || mState == STATE_SUCCESS
                    || mState == STATE_FAIL) {
                cancelAutoFocus();
            }
        }

        // Unlock AE and AWB after cancelAutoFocus. Camera API does not
        // guarantee setParameters can be called during autofocus.
        unlockAeAwbIfNeeded();
    }

    public void focusAndCapture(CameraCapabilities.FocusMode currentFocusMode) {
        if (!mInitialized) {
            return;
        }

        if (!needAutoFocusCall(currentFocusMode)) {
            // Focus is not needed.
            capture();
        } else if (mState == STATE_SUCCESS || mState == STATE_FAIL) {
            // Focus is done already.
            capture();
        } else if (mState == STATE_FOCUSING) {
            // Still focusing and will not trigger snap upon finish.
            mState = STATE_FOCUSING_SNAP_ON_FINISH;
        } else if (mState == STATE_IDLE) {
            autoFocusAndCapture();
        }
    }

    public void onAutoFocus(boolean focused, boolean shutterButtonPressed) {
        if (mState == STATE_FOCUSING_SNAP_ON_FINISH) {
            // Take the picture no matter focus succeeds or fails. No need
            // to play the AF sound if we're about to play the shutter
            // sound.
            if (focused) {
                mState = STATE_SUCCESS;
            } else {
                mState = STATE_FAIL;
            }
            updateFocusUI();
            capture();
        } else if (mState == STATE_FOCUSING) {
            // This happens when (1) user is half-pressing the focus key or
            // (2) touch focus is triggered. Play the focus tone. Do not
            // take the picture now.
            if (focused) {
                mState = STATE_SUCCESS;
            } else {
                mState = STATE_FAIL;
            }
            updateFocusUI();
            // If this is triggered by touch focus, cancel focus after a
            // while.
            if (mFocusArea != null) {
                mFocusLocked = true;
                mHandler.sendEmptyMessageDelayed(RESET_TOUCH_FOCUS, RESET_TOUCH_FOCUS_DELAY_MILLIS);
            }
            if (shutterButtonPressed) {
                // Lock AE & AWB so users can half-press shutter and recompose.
                lockAeAwbIfNeeded();
            }
        } else if (mState == STATE_IDLE) {
            // User has released the focus key before focus completes.
            // Do nothing.
        }
    }

    public void onAutoFocusMoving(boolean moving) {
        if (!mInitialized) {
            return;
        }


        // Ignore if the camera has detected some faces.
        if (mUI.hasFaces()) {
            mUI.clearFocus();
            return;
        }

        // Ignore if we have requested autofocus. This method only handles
        // continuous autofocus.
        if (mState != STATE_IDLE) {
            return;
        }

        // animate on false->true trasition only b/8219520
        if (moving && !mPreviousMoving) {
            // Auto focus at the center of the preview.
            mUI.setFocusPosition(mPreviewRect.centerX(), mPreviewRect.centerY(), true,
                    getAFRegionEdge(), getAERegionEdge());
            mUI.onFocusStarted();
        } else if (!moving) {
            mUI.onFocusSucceeded();
        }
        mPreviousMoving = moving;
    }

    /** Returns width of auto focus region in pixels. */
    private int getAFRegionEdge() {
        return (int) (Math.min(mPreviewRect.width(), mPreviewRect.height()) * AF_REGION_BOX);
    }

    /** Returns width of metering region in pixels. */
    private int getAERegionEdge() {
        return (int) (Math.min(mPreviewRect.width(), mPreviewRect.height()) * AE_REGION_BOX);
    }

    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    private void initializeFocusAreas(int x, int y) {
        if (mFocusArea == null) {
            mFocusArea = new ArrayList<Area>();
            mFocusArea.add(new Area(new Rect(), 1));
        }

        // Convert the coordinates to driver format.
        calculateTapArea(x, y, getAFRegionEdge(), mFocusArea.get(0).rect);
    }

    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    private void initializeMeteringAreas(int x, int y) {
        if (mMeteringArea == null) {
            mMeteringArea = new ArrayList<Area>();
            mMeteringArea.add(new Area(new Rect(), 1));
        }

        // Convert the coordinates to driver format.
        calculateTapArea(x, y, getAERegionEdge(), mMeteringArea.get(0).rect);
    }

    public void onSingleTapUp(int x, int y) {
        if (!mInitialized || mState == STATE_FOCUSING_SNAP_ON_FINISH) {
            return;
        }

        // Let users be able to cancel previous touch focus.
        if ((mFocusArea != null) && (mState == STATE_FOCUSING ||
                    mState == STATE_SUCCESS || mState == STATE_FAIL)) {
            cancelAutoFocus();
        }
        if (mPreviewRect.width() == 0 || mPreviewRect.height() == 0) {
            return;
        }
        // Initialize variables.
        // Initialize mFocusArea.
        if (mFocusAreaSupported) {
            initializeFocusAreas(x, y);
        }
        // Initialize mMeteringArea.
        if (mMeteringAreaSupported) {
            initializeMeteringAreas(x, y);
        }

        // Use margin to set the focus indicator to the touched area.
        mUI.setFocusPosition(x, y, false, getAFRegionEdge(), getAERegionEdge());
        // Log manual tap to focus.
        mTouchCoordinate = new TouchCoordinate(x, y, mPreviewRect.width(), mPreviewRect.height());
        mTouchTime = System.currentTimeMillis();

        // Stop face detection because we want to specify focus and metering area.
        mListener.stopFaceDetection();

        // Set the focus area and metering area.
        mListener.setFocusParameters();
        if (mFocusAreaSupported) {
            autoFocus();
        } else {  // Just show the indicator in all other cases.
            updateFocusUI();
            // Reset the metering area in 4 seconds.
            mHandler.removeMessages(RESET_TOUCH_FOCUS);
            mHandler.sendEmptyMessageDelayed(RESET_TOUCH_FOCUS, RESET_TOUCH_FOCUS_DELAY_MILLIS);
        }
    }

    public void onPreviewStarted() {
        mState = STATE_IDLE;
        resetTouchFocus();
    }

    public void onPreviewStopped() {
        // If auto focus was in progress, it would have been stopped.
        mState = STATE_IDLE;
        updateFocusUI();
    }

    public void onCameraReleased() {
        onPreviewStopped();
    }

    @Override
    public void onMoving() {
        if (mFocusLocked) {
            Log.d(TAG, "onMoving: Early focus unlock.");
            cancelAutoFocus();
        }
    }

    /**
     * Triggers the autofocus and sets the specified state.
     *
     * @param focusingState The state to use when focus is in progress.
     */
    private void autoFocus(int focusingState) {
        mListener.autoFocus();
        mState = focusingState;
        // Pause the face view because the driver will keep sending face
        // callbacks after the focus completes.
        mUI.pauseFaceDetection();
        updateFocusUI();
        mHandler.removeMessages(RESET_TOUCH_FOCUS);
    }

    /**
     * Triggers the autofocus and set the state to indicate the focus is in
     * progress.
     */
    private void autoFocus() {
        autoFocus(STATE_FOCUSING);
    }

    /**
     * Triggers the autofocus and set the state to which a capture will happen
     * in the following autofocus callback.
     */
    private void autoFocusAndCapture() {
        autoFocus(STATE_FOCUSING_SNAP_ON_FINISH);
    }

    private void cancelAutoFocus() {
        Log.v(TAG, "Cancel autofocus.");
        // Reset the tap area before calling mListener.cancelAutofocus.
        // Otherwise, focus mode stays at auto and the tap area passed to the
        // driver is not reset.
        resetTouchFocus();
        mListener.cancelAutoFocus();
        mUI.resumeFaceDetection();
        mState = STATE_IDLE;
        mFocusLocked = false;
        updateFocusUI();
        mHandler.removeMessages(RESET_TOUCH_FOCUS);
    }

    private void capture() {
        if (mListener.capture()) {
            mState = STATE_IDLE;
            mHandler.removeMessages(RESET_TOUCH_FOCUS);
        }
    }

    public CameraCapabilities.FocusMode getFocusMode(
            final CameraCapabilities.FocusMode currentFocusMode) {
        if (mOverrideFocusMode != null) {
            Log.v(TAG, "returning override focus: " + mOverrideFocusMode);
            return mOverrideFocusMode;
        }
        if (mCapabilities == null) {
            Log.v(TAG, "no capabilities, returning default AUTO focus mode");
            return CameraCapabilities.FocusMode.AUTO;
        }

        if (mFocusAreaSupported && mFocusArea != null) {
            Log.v(TAG, "in tap to focus, returning AUTO focus mode");
            // Always use autofocus in tap-to-focus.
            mFocusMode = CameraCapabilities.FocusMode.AUTO;
        } else {
            String focusSetting = mSettingsManager.getString(mAppController.getCameraScope(),
                    Keys.KEY_FOCUS_MODE);
            Log.v(TAG, "stored focus setting for camera: " + focusSetting);
            // The default is continuous autofocus.
            mFocusMode = mCapabilities.getStringifier().focusModeFromString(focusSetting);
            Log.v(TAG, "focus mode resolved from setting: " + mFocusMode);
            // Try to find a supported focus mode from the default list.
            if (mFocusMode == null) {
                for (CameraCapabilities.FocusMode mode : mDefaultFocusModes) {
                    if (mCapabilities.supports(mode)) {
                        mFocusMode = mode;
                        Log.v(TAG, "selected supported focus mode from default list" + mode);
                        break;
                    }
                }
            }
        }
        if (!mCapabilities.supports(mFocusMode)) {
            // For some reasons, the driver does not support the current
            // focus mode. Fall back to auto.
            if (mCapabilities.supports(CameraCapabilities.FocusMode.AUTO)) {
                Log.v(TAG, "no supported focus mode, falling back to AUTO");
                mFocusMode = CameraCapabilities.FocusMode.AUTO;
            } else {
                Log.v(TAG, "no supported focus mode, falling back to current: " + currentFocusMode);
                mFocusMode = currentFocusMode;
            }
        }
        return mFocusMode;
    }

    public List<Area> getFocusAreas() {
        return mFocusArea;
    }

    public List<Area> getMeteringAreas() {
        return mMeteringArea;
    }

    public void updateFocusUI() {
        if (!mInitialized) {
            // Show only focus indicator or face indicator.
            return;
        }
        if (mState == STATE_IDLE) {
            if (mFocusArea == null) {
                mUI.clearFocus();
            } else {
                // Users touch on the preview and the indicator represents the
                // metering area. Either focus area is not supported or
                // autoFocus call is not required.
                mUI.onFocusStarted();
            }
        } else if (mState == STATE_FOCUSING) {
            mUI.onFocusStarted();
        } else {
            if (mFocusMode == CameraCapabilities.FocusMode.CONTINUOUS_PICTURE) {
                // TODO: check HAL behavior and decide if this can be removed.
                mUI.onFocusSucceeded();
            } else if (mState == STATE_SUCCESS) {
                mUI.onFocusSucceeded();
            } else if (mState == STATE_FAIL) {
                mUI.onFocusFailed();
            }
        }
    }

    public void resetTouchFocus() {
        if (!mInitialized) {
            return;
        }

        // Put focus indicator to the center. clear reset position
        mUI.clearFocus();
        // Initialize mFocusArea.
        mFocusArea = null;
        mMeteringArea = null;
        // This will cause current module to call getFocusAreas() and
        // getMeteringAreas() and send updated regions to camera.
        mListener.setFocusParameters();

        if (mTouchCoordinate != null) {
            UsageStatistics.instance().tapToFocus(mTouchCoordinate,
                    0.001f * (System.currentTimeMillis() - mTouchTime));
            mTouchCoordinate = null;
        }
    }

    private void calculateTapArea(int x, int y, int size, Rect rect) {
        int left = CameraUtil.clamp(x - size / 2, mPreviewRect.left,
                mPreviewRect.right - size);
        int top = CameraUtil.clamp(y - size / 2, mPreviewRect.top,
                mPreviewRect.bottom - size);

        RectF rectF = new RectF(left, top, left + size, top + size);
        mMatrix.mapRect(rectF);
        CameraUtil.rectFToRect(rectF, rect);
    }

    /* package */ int getFocusState() {
        return mState;
    }

    public boolean isFocusCompleted() {
        return mState == STATE_SUCCESS || mState == STATE_FAIL;
    }

    public boolean isFocusingSnapOnFinish() {
        return mState == STATE_FOCUSING_SNAP_ON_FINISH;
    }

    public void removeMessages() {
        mHandler.removeMessages(RESET_TOUCH_FOCUS);
    }

    public void overrideFocusMode(CameraCapabilities.FocusMode focusMode) {
        mOverrideFocusMode = focusMode;
    }

    public void setAeAwbLock(boolean lock) {
        mAeAwbLock = lock;
    }

    public boolean getAeAwbLock() {
        return mAeAwbLock;
    }

    private boolean needAutoFocusCall(CameraCapabilities.FocusMode focusMode) {
        return !(focusMode == CameraCapabilities.FocusMode.INFINITY
                || focusMode == CameraCapabilities.FocusMode.FIXED
                || focusMode == CameraCapabilities.FocusMode.EXTENDED_DOF);
    }
}
