/*
 * Copyright (C) 2014 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.example.android.hdrviewfinder;

import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCaptureSession;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CameraManager;
import android.hardware.camera2.CaptureRequest;
import android.os.ConditionVariable;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.Log;
import android.view.Surface;

import java.util.List;

/**
 * Simple interface for operating the camera, with major camera operations
 * all performed on a background handler thread.
 */
public class CameraOps {

    private static final String TAG = "CameraOps";

    public static final long CAMERA_CLOSE_TIMEOUT = 2000; // ms

    private final CameraManager mCameraManager;
    private CameraDevice mCameraDevice;
    private CameraCaptureSession mCameraSession;
    private List<Surface> mSurfaces;

    private final ConditionVariable mCloseWaiter = new ConditionVariable();

    private HandlerThread mCameraThread;
    private Handler mCameraHandler;

    private final ErrorDisplayer mErrorDisplayer;

    private final CameraReadyListener mReadyListener;
    private final Handler mReadyHandler;

    /**
     * Create a new camera ops thread.
     *
     * @param errorDisplayer listener for displaying error messages
     * @param readyListener  listener for notifying when camera is ready for requests
     * @param readyHandler   the handler for calling readyListener methods on
     */
    CameraOps(CameraManager manager, ErrorDisplayer errorDisplayer,
              CameraReadyListener readyListener, Handler readyHandler) {
        mCameraThread = new HandlerThread("CameraOpsThread");
        mCameraThread.start();

        if (manager == null || errorDisplayer == null ||
                readyListener == null || readyHandler == null) {
            throw new IllegalArgumentException("Need valid displayer, listener, handler");
        }

        mCameraManager = manager;
        mErrorDisplayer = errorDisplayer;
        mReadyListener = readyListener;
        mReadyHandler = readyHandler;
    }

    /**
     * Open the first back-facing camera listed by the camera manager.
     * Displays a dialog if it cannot open a camera.
     */
    public void openCamera(final String cameraId) {
        mCameraHandler = new Handler(mCameraThread.getLooper());

        mCameraHandler.post(new Runnable() {
            public void run() {
                if (mCameraDevice != null) {
                    throw new IllegalStateException("Camera already open");
                }
                try {
                    mCameraManager.openCamera(cameraId, mCameraDeviceListener, mCameraHandler);
                } catch (CameraAccessException e) {
                    String errorMessage = mErrorDisplayer.getErrorString(e);
                    mErrorDisplayer.showErrorDialog(errorMessage);
                }
            }
        });
    }

    /**
     * Close the camera and wait for the close callback to be called in the camera thread.
     * Times out after @{value CAMERA_CLOSE_TIMEOUT} ms.
     */
    public void closeCameraAndWait() {
        mCloseWaiter.close();
        mCameraHandler.post(mCloseCameraRunnable);
        boolean closed = mCloseWaiter.block(CAMERA_CLOSE_TIMEOUT);
        if (!closed) {
            Log.e(TAG, "Timeout closing camera");
        }
    }

    private Runnable mCloseCameraRunnable = new Runnable() {
        public void run() {
            if (mCameraDevice != null) {
                mCameraDevice.close();
            }
            mCameraDevice = null;
            mCameraSession = null;
            mSurfaces = null;
        }
    };

    /**
     * Set the output Surfaces, and finish configuration if otherwise ready.
     */
    public void setSurfaces(final List<Surface> surfaces) {
        mCameraHandler.post(new Runnable() {
            public void run() {
                mSurfaces = surfaces;
                startCameraSession();
            }
        });
    }

    /**
     * Get a request builder for the current camera.
     */
    public CaptureRequest.Builder createCaptureRequest(int template) throws CameraAccessException {
        CameraDevice device = mCameraDevice;
        if (device == null) {
            throw new IllegalStateException("Can't get requests when no camera is open");
        }
        return device.createCaptureRequest(template);
    }

    /**
     * Set a repeating request.
     */
    public void setRepeatingRequest(final CaptureRequest request,
                                    final CameraCaptureSession.CaptureCallback listener,
                                    final Handler handler) {
        mCameraHandler.post(new Runnable() {
            public void run() {
                try {
                    mCameraSession.setRepeatingRequest(request, listener, handler);
                } catch (CameraAccessException e) {
                    String errorMessage = mErrorDisplayer.getErrorString(e);
                    mErrorDisplayer.showErrorDialog(errorMessage);
                }
            }
        });
    }

    /**
     * Set a repeating request.
     */
    public void setRepeatingBurst(final List<CaptureRequest> requests,
                                  final CameraCaptureSession.CaptureCallback listener,
                                  final Handler handler) {
        mCameraHandler.post(new Runnable() {
            public void run() {
                try {
                    mCameraSession.setRepeatingBurst(requests, listener, handler);
                } catch (CameraAccessException e) {
                    String errorMessage = mErrorDisplayer.getErrorString(e);
                    mErrorDisplayer.showErrorDialog(errorMessage);
                }
            }
        });
    }

    /**
     * Configure the camera session.
     */
    private void startCameraSession() {
        // Wait until both the camera device is open and the SurfaceView is ready
        if (mCameraDevice == null || mSurfaces == null) return;

        try {
            mCameraDevice.createCaptureSession(
                    mSurfaces, mCameraSessionListener, mCameraHandler);
        } catch (CameraAccessException e) {
            String errorMessage = mErrorDisplayer.getErrorString(e);
            mErrorDisplayer.showErrorDialog(errorMessage);
            mCameraDevice.close();
            mCameraDevice = null;
        }
    }

    /**
     * Main listener for camera session events
     * Invoked on mCameraThread
     */
    private CameraCaptureSession.StateCallback mCameraSessionListener =
            new CameraCaptureSession.StateCallback() {

                @Override
                public void onConfigured(CameraCaptureSession session) {
                    mCameraSession = session;
                    mReadyHandler.post(new Runnable() {
                        public void run() {
                            // This can happen when the screen is turned off and turned back on.
                            if (null == mCameraDevice) {
                                return;
                            }

                            mReadyListener.onCameraReady();
                        }
                    });

                }

                @Override
                public void onConfigureFailed(CameraCaptureSession session) {
                    mErrorDisplayer.showErrorDialog("Unable to configure the capture session");
                    mCameraDevice.close();
                    mCameraDevice = null;
                }
            };

    /**
     * Main listener for camera device events.
     * Invoked on mCameraThread
     */
    private CameraDevice.StateCallback mCameraDeviceListener = new CameraDevice.StateCallback() {

        @Override
        public void onOpened(CameraDevice camera) {
            mCameraDevice = camera;
            startCameraSession();
        }

        @Override
        public void onClosed(CameraDevice camera) {
            mCloseWaiter.open();
        }

        @Override
        public void onDisconnected(CameraDevice camera) {
            mErrorDisplayer.showErrorDialog("The camera device has been disconnected.");
            camera.close();
            mCameraDevice = null;
        }

        @Override
        public void onError(CameraDevice camera, int error) {
            mErrorDisplayer.showErrorDialog("The camera encountered an error:" + error);
            camera.close();
            mCameraDevice = null;
        }

    };

    /**
     * Simple listener for main code to know the camera is ready for requests, or failed to
     * start.
     */
    public interface CameraReadyListener {
        public void onCameraReady();
    }

    /**
     * Simple listener for displaying error messages
     */
    public interface ErrorDisplayer {
        public void showErrorDialog(String errorMessage);

        public String getErrorString(CameraAccessException e);
    }

}
