/*
 * Copyright 2022 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 androidx.camera.core.imagecapture;

import static androidx.camera.core.impl.utils.Threads.checkMainThread;
import static androidx.core.util.Preconditions.checkArgument;

import static java.util.Objects.requireNonNull;

import android.graphics.Matrix;
import android.graphics.Rect;
import android.os.Build;

import androidx.annotation.IntRange;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageCaptureException;
import androidx.camera.core.ImageProxy;
import androidx.camera.core.impl.CameraCaptureCallback;
import androidx.camera.core.impl.ImageCaptureConfig;
import androidx.camera.core.impl.ImageOutputConfig;
import androidx.camera.core.impl.SessionConfig;
import androidx.camera.core.internal.compat.workaround.CaptureFailedRetryEnabler;

import com.google.auto.value.AutoValue;

import java.util.List;
import java.util.concurrent.Executor;

/**
 * A {@link ImageCapture#takePicture} request.
 *
 * <p> It contains app provided data and a snapshot of {@link ImageCapture} properties.
 */
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@SuppressWarnings("AutoValueImmutableFields")
@AutoValue
public abstract class TakePictureRequest {

    /**
     * By default, ImageCapture does not retry requests. For some problematic devices, the
     * capture request can become success after retrying. The allowed retry count will be
     * provided by the {@link CaptureFailedRetryEnabler}.
     */
    private int mRemainingRetires = new CaptureFailedRetryEnabler().getRetryCount();

    /**
     * Gets the callback {@link Executor} provided by the app.
     */
    @NonNull
    abstract Executor getAppExecutor();

    /**
     * Gets the app provided callback for in-memory capture.
     */
    @Nullable
    abstract ImageCapture.OnImageCapturedCallback getInMemoryCallback();

    /**
     * Gets the app provided callback for on-disk capture.
     */
    @Nullable
    abstract ImageCapture.OnImageSavedCallback getOnDiskCallback();

    /**
     * Gets the app provided options for on-disk capture.
     */
    @Nullable
    abstract ImageCapture.OutputFileOptions getOutputFileOptions();

    /**
     * A snapshot of {@link ImageCapture#getViewPortCropRect()} when
     * {@link ImageCapture#takePicture} is called.
     */
    @NonNull
    abstract Rect getCropRect();

    /**
     * A snapshot of {@link ImageCapture#getSensorToBufferTransformMatrix()} when
     * {@link ImageCapture#takePicture} is called.
     */
    @NonNull
    abstract Matrix getSensorToBufferTransform();

    /**
     * A snapshot of rotation degrees when {@link ImageCapture#takePicture} is called.
     */
    @ImageOutputConfig.RotationValue
    abstract int getRotationDegrees();

    /**
     * A snapshot of {@link ImageCaptureConfig#getJpegQuality()} when
     * {@link ImageCapture#takePicture} is called.
     */
    @IntRange(from = 1, to = 100)
    abstract int getJpegQuality();

    /**
     * Gets the capture mode of the request.
     *
     * <p>When there are software JPEG encoding/decoding, the value of {@link #getJpegQuality()}
     * is used for the software encoding. The capture mode value is for calculating the JPEG
     * quality for camera hardware encoding.
     */
    @ImageCapture.CaptureMode
    abstract int getCaptureMode();

    /**
     * Gets the {@link CameraCaptureCallback}s set on the {@link SessionConfig}.
     *
     * <p>This is for calling back to Camera2InterOp. See: aosp/947197.
     */
    @NonNull
    abstract List<CameraCaptureCallback> getSessionConfigCameraCaptureCallbacks();

    /**
     * Decrements retry counter.
     *
     * @return true if there is still remaining retries at the time of calling. In that case, the
     * request should be retried. False when there is no retry left. The caller needs to fail the
     * request.
     */
    @MainThread
    boolean decrementRetryCounter() {
        checkMainThread();
        if (mRemainingRetires > 0) {
            mRemainingRetires--;
            return true;
        } else {
            return false;
        }
    }

    /**
     * Increments retry counter.
     */
    @MainThread
    void incrementRetryCounter() {
        checkMainThread();
        mRemainingRetires++;
    }

    /**
     * Gets the current retry count for testing.
     */
    @MainThread
    @VisibleForTesting
    int getRemainingRetries() {
        checkMainThread();
        return mRemainingRetires;
    }

    /**
     * Delivers {@link ImageCaptureException} to the app.
     */
    void onError(@NonNull ImageCaptureException imageCaptureException) {
        getAppExecutor().execute(() -> {
            boolean hasInMemory = getInMemoryCallback() != null;
            boolean hasOnDisk = getOnDiskCallback() != null;
            if (hasInMemory && !hasOnDisk) {
                requireNonNull(getInMemoryCallback()).onError(imageCaptureException);
            } else if (hasOnDisk && !hasInMemory) {
                requireNonNull(getOnDiskCallback()).onError(imageCaptureException);
            } else {
                throw new IllegalStateException("One and only one callback is allowed.");
            }
        });
    }

    /**
     * Delivers on-disk capture result to the app.
     */
    void onResult(@Nullable ImageCapture.OutputFileResults outputFileResults) {
        getAppExecutor().execute(() -> requireNonNull(getOnDiskCallback()).onImageSaved(
                requireNonNull(outputFileResults)));
    }

    /**
     * Delivers in-memory capture result to the app.
     */
    void onResult(@Nullable ImageProxy imageProxy) {
        getAppExecutor().execute(() -> requireNonNull(getInMemoryCallback()).onCaptureSuccess(
                requireNonNull(imageProxy)));
    }

    /**
     * Creates a {@link TakePictureRequest} instance.
     */
    @NonNull
    public static TakePictureRequest of(@NonNull Executor appExecutor,
            @Nullable ImageCapture.OnImageCapturedCallback inMemoryCallback,
            @Nullable ImageCapture.OnImageSavedCallback onDiskCallback,
            @Nullable ImageCapture.OutputFileOptions outputFileOptions,
            @NonNull Rect cropRect,
            @NonNull Matrix sensorToBufferTransform,
            int rotationDegrees,
            int jpegQuality,
            @ImageCapture.CaptureMode int captureMode,
            @NonNull List<CameraCaptureCallback> sessionConfigCameraCaptureCallbacks) {
        checkArgument((onDiskCallback == null) == (outputFileOptions == null),
                "onDiskCallback and outputFileOptions should be both null or both non-null.");
        checkArgument((onDiskCallback == null) ^ (inMemoryCallback == null),
                "One and only one on-disk or in-memory callback should be present.");
        return new AutoValue_TakePictureRequest(appExecutor, inMemoryCallback,
                onDiskCallback, outputFileOptions, cropRect, sensorToBufferTransform,
                rotationDegrees, jpegQuality, captureMode, sessionConfigCameraCaptureCallbacks);
    }

    /**
     * Interface for retrying a {@link TakePictureRequest}.
     */
    interface RetryControl {

        /**
         * Retries the given {@link TakePictureRequest}.
         *
         * <p>The request should be injected to the front of the queue.
         */
        void retryRequest(@NonNull TakePictureRequest takePictureRequest);
    }
}
