/*
 * 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 android.hardware.camera2.legacy;

import android.graphics.ImageFormat;
import android.hardware.Camera;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.impl.CaptureResultExtras;
import android.hardware.camera2.ICameraDeviceCallbacks;
import android.hardware.camera2.utils.LongParcelable;
import android.hardware.camera2.impl.CameraMetadataNative;
import android.hardware.camera2.utils.CameraBinderDecorator;
import android.hardware.camera2.utils.CameraRuntimeException;
import android.os.ConditionVariable;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.RemoteException;
import android.util.Log;
import android.view.Surface;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * This class emulates the functionality of a Camera2 device using a the old Camera class.
 *
 * <p>
 * There are two main components that are used to implement this:
 * - A state machine containing valid Camera2 device states ({@link CameraDeviceState}).
 * - A message-queue based pipeline that manages an old Camera class, and executes capture and
 *   configuration requests.
 * </p>
 */
public class LegacyCameraDevice implements AutoCloseable {
    public static final String DEBUG_PROP = "HAL1ShimLogging";

    private final String TAG;

    private final int mCameraId;
    private final ICameraDeviceCallbacks mDeviceCallbacks;
    private final CameraDeviceState mDeviceState = new CameraDeviceState();

    private final ConditionVariable mIdle = new ConditionVariable(/*open*/true);
    private final AtomicInteger mRequestIdCounter = new AtomicInteger(0);

    private final HandlerThread mCallbackHandlerThread = new HandlerThread("ResultThread");
    private final Handler mCallbackHandler;
    private static final int ILLEGAL_VALUE = -1;

    private CaptureResultExtras getExtrasFromRequest(RequestHolder holder) {
        if (holder == null) {
            return new CaptureResultExtras(ILLEGAL_VALUE, ILLEGAL_VALUE, ILLEGAL_VALUE,
                    ILLEGAL_VALUE, ILLEGAL_VALUE);
        }
        return new CaptureResultExtras(holder.getRequestId(), holder.getSubsequeceId(),
                /*afTriggerId*/0, /*precaptureTriggerId*/0, holder.getFrameNumber());
    }

    /**
     * Listener for the camera device state machine.  Calls the appropriate
     * {@link ICameraDeviceCallbacks} for each state transition.
     */
    private final CameraDeviceState.CameraDeviceStateListener mStateListener =
            new CameraDeviceState.CameraDeviceStateListener() {
        @Override
        public void onError(final int errorCode, RequestHolder holder) {
            mIdle.open();
            final CaptureResultExtras extras = getExtrasFromRequest(holder);
            try {
                mDeviceCallbacks.onCameraError(errorCode, extras);
            } catch (RemoteException e) {
                Log.e(TAG, "Received remote exception during onCameraError callback: ", e);
            }

        }

        @Override
        public void onConfiguring() {
            // Do nothing
        }

        @Override
        public void onIdle() {
            mIdle.open();

            try {
                mDeviceCallbacks.onCameraIdle();
            } catch (RemoteException e) {
                Log.e(TAG, "Received remote exception during onCameraIdle callback: ", e);
            }
        }

        @Override
        public void onCaptureStarted(RequestHolder holder) {
            final CaptureResultExtras extras = getExtrasFromRequest(holder);

            try {
                // TODO: Don't fake timestamp
                mDeviceCallbacks.onCaptureStarted(extras, System.nanoTime());
            } catch (RemoteException e) {
                Log.e(TAG, "Received remote exception during onCameraError callback: ", e);
            }

        }

        @Override
        public void onCaptureResult(CameraMetadataNative result, RequestHolder holder) {
            final CaptureResultExtras extras = getExtrasFromRequest(holder);

            try {
                // TODO: Don't fake metadata
                mDeviceCallbacks.onResultReceived(result, extras);
            } catch (RemoteException e) {
                Log.e(TAG, "Received remote exception during onCameraError callback: ", e);
            }
        }
    };

    private final RequestThreadManager mRequestThreadManager;

    /**
     * Check if a given surface uses {@link ImageFormat#YUV_420_888} format.
     *
     * @param s the surface to check.
     * @return {@code true} if the surfaces uses {@link ImageFormat#YUV_420_888}.
     */
    static boolean needsConversion(Surface s) {
        return LegacyCameraDevice.nativeDetectSurfaceType(s) == ImageFormat.YUV_420_888;
    }

    /**
     * Create a new emulated camera device from a given Camera 1 API camera.
     *
     * <p>
     * The {@link Camera} provided to this constructor must already have been successfully opened,
     * and ownership of the provided camera is passed to this object.  No further calls to the
     * camera methods should be made following this constructor.
     * </p>
     *
     * @param cameraId the id of the camera.
     * @param camera an open {@link Camera} device.
     * @param callbacks {@link ICameraDeviceCallbacks} callbacks to call for Camera2 API operations.
     */
    public LegacyCameraDevice(int cameraId, Camera camera, ICameraDeviceCallbacks callbacks) {
        mCameraId = cameraId;
        mDeviceCallbacks = callbacks;
        TAG = String.format("CameraDevice-%d-LE", mCameraId);

        mCallbackHandlerThread.start();
        mCallbackHandler = new Handler(mCallbackHandlerThread.getLooper());
        mDeviceState.setCameraDeviceCallbacks(mCallbackHandler, mStateListener);
        mRequestThreadManager =
                new RequestThreadManager(cameraId, camera, mDeviceState);
        mRequestThreadManager.start();
    }

    /**
     * Configure the device with a set of output surfaces.
     *
     * @param outputs a list of surfaces to set.
     * @return an error code for this binder operation, or {@link CameraBinderDecorator.NO_ERROR}
     *          on success.
     */
    public int configureOutputs(List<Surface> outputs) {
        int error = mDeviceState.setConfiguring();
        if (error == CameraBinderDecorator.NO_ERROR) {
            mRequestThreadManager.configure(outputs);
            error = mDeviceState.setIdle();
        }
        return error;
    }

    /**
     * Submit a burst of capture requests.
     *
     * @param requestList a list of capture requests to execute.
     * @param repeating {@code true} if this burst is repeating.
     * @param frameNumber an output argument that contains either the frame number of the last frame
     *                    that will be returned for this request, or the frame number of the last
     *                    frame that will be returned for the current repeating request if this
     *                    burst is set to be repeating.
     * @return the request id.
     */
    public int submitRequestList(List<CaptureRequest> requestList, boolean repeating,
            /*out*/LongParcelable frameNumber) {
        // TODO: validate request here
        mIdle.close();
        return mRequestThreadManager.submitCaptureRequests(requestList, repeating,
                frameNumber);
    }

    /**
     * Submit a single capture request.
     *
     * @param request the capture request to execute.
     * @param repeating {@code true} if this request is repeating.
     * @param frameNumber an output argument that contains either the frame number of the last frame
     *                    that will be returned for this request, or the frame number of the last
     *                    frame that will be returned for the current repeating request if this
     *                    request is set to be repeating.
     * @return the request id.
     */
    public int submitRequest(CaptureRequest request, boolean repeating,
            /*out*/LongParcelable frameNumber) {
        ArrayList<CaptureRequest> requestList = new ArrayList<CaptureRequest>();
        requestList.add(request);
        return submitRequestList(requestList, repeating, frameNumber);
    }

    /**
     * Cancel the repeating request with the given request id.
     *
     * @param requestId the request id of the request to cancel.
     * @return the last frame number to be returned from the HAL for the given repeating request, or
     *          {@code INVALID_FRAME} if none exists.
     */
    public long cancelRequest(int requestId) {
        return mRequestThreadManager.cancelRepeating(requestId);
    }

    /**
     * Block until the {@link ICameraDeviceCallbacks#onCameraIdle()} callback is received.
     */
    public void waitUntilIdle()  {
        mIdle.block();
    }

    @Override
    public void close() {
        mRequestThreadManager.quit();
        mCallbackHandlerThread.quitSafely();
        // TODO: throw IllegalStateException in every method after close has been called
    }

    @Override
    protected void finalize() throws Throwable {
        try {
            close();
        } catch (CameraRuntimeException e) {
            Log.e(TAG, "Got error while trying to finalize, ignoring: " + e.getMessage());
        } finally {
            super.finalize();
        }
    }

    protected static native int nativeDetectSurfaceType(Surface surface);

    protected static native void nativeDetectSurfaceDimens(Surface surface, int[] dimens);

    protected static native void nativeConfigureSurface(Surface surface, int width, int height,
                                                        int pixelFormat);

    protected static native void nativeProduceFrame(Surface surface, byte[] pixelBuffer, int width,
                                                    int height, int pixelFormat);

    protected static native void nativeSetSurfaceFormat(Surface surface, int pixelFormat);

    protected static native void nativeSetSurfaceDimens(Surface surface, int width, int height);

}
