/*
 * Copyright (C) 2015 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.one.v2.photo.zsl;

import android.hardware.camera2.CameraAccessException;
import androidx.core.util.Pair;

import com.android.camera.async.BufferQueue;
import com.android.camera.async.Updatable;
import com.android.camera.debug.Log;
import com.android.camera.debug.Logger;
import com.android.camera.one.v2.camera2proxy.CameraCaptureSessionClosedException;
import com.android.camera.one.v2.camera2proxy.ImageProxy;
import com.android.camera.one.v2.camera2proxy.TotalCaptureResultProxy;
import com.android.camera.one.v2.core.ResourceAcquisitionFailedException;
import com.android.camera.one.v2.imagesaver.ImageSaver;
import com.android.camera.one.v2.photo.ImageCaptureCommand;
import com.android.camera.one.v2.sharedimagereader.metadatasynchronizer.MetadataPool;
import com.google.common.base.Predicate;
import com.google.common.util.concurrent.Futures;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import javax.annotation.Nullable;
import javax.annotation.ParametersAreNonnullByDefault;

/**
 * Captures images by first looking to the zsl ring buffer for acceptable (based
 * on metadata) images. If no such images are available, a fallback
 * ImageCaptureCommand is used instead.
 */
@ParametersAreNonnullByDefault
public class ZslImageCaptureCommand implements ImageCaptureCommand {
    private final Logger mLog;
    private final BufferQueue<ImageProxy> mZslRingBuffer;
    private final MetadataPool mZslMetadataPool;
    private final ImageCaptureCommand mFallbackCommand;
    private final Predicate<TotalCaptureResultProxy> mMetadataFilter;
    private final long mMaxLookBackNanos;

    public ZslImageCaptureCommand(Logger.Factory logFactory,
            BufferQueue<ImageProxy> zslRingBuffer,
            MetadataPool zslMetadataPool,
            ImageCaptureCommand fallbackCommand,
            Predicate<TotalCaptureResultProxy> metadataFilter,
            long maxLookBackNanos) {
        mZslRingBuffer = zslRingBuffer;
        mLog = logFactory.create(new Log.Tag("ZSLImageCaptureCmd"));
        mZslMetadataPool = zslMetadataPool;
        mFallbackCommand = fallbackCommand;
        mMetadataFilter = metadataFilter;
        mMaxLookBackNanos = maxLookBackNanos;
    }

    /**
     * @return All images currently in the ring-buffer, ordered from oldest to
     *         most recent.
     */
    private List<ImageProxy> getAllAvailableImages() throws InterruptedException,
            BufferQueue.BufferQueueClosedException {
        List<ImageProxy> images = new ArrayList<>();
        try {
            // Keep grabbing images until there are no more immediately
            // available in the ring buffer.
            while (true) {
                try {
                    images.add(mZslRingBuffer.getNext(0, TimeUnit.SECONDS));
                } catch (TimeoutException e) {
                    break;
                }
            }
        } catch (Exception e) {
            // Close the images to avoid leaking them, since they will not be
            // returned to the caller.
            for (ImageProxy image : images) {
                image.close();
            }
            throw e;
        }
        return images;
    }

    private List<ImageProxy> filterImagesWithinMaxLookBack(List<ImageProxy> images) {
        if (images.isEmpty()) {
            return Collections.emptyList();
        }
        List<ImageProxy> filtered = new ArrayList<>();
        long mostRecentTimestamp = images.get(images.size() - 1).getTimestamp();
        long timestampThreshold = mostRecentTimestamp - mMaxLookBackNanos;
        for (ImageProxy image : images) {
            if (image.getTimestamp() > timestampThreshold) {
                filtered.add(image);
            } else {
                image.close();
            }
        }
        return filtered;
    }

    @Nullable
    private Pair<ImageProxy, TotalCaptureResultProxy> tryGetZslImage() throws InterruptedException,
            BufferQueue.BufferQueueClosedException {
        List<ImageProxy> images = filterImagesWithinMaxLookBack(getAllAvailableImages());
        ImageProxy imageToSave = null;
        TotalCaptureResultProxy metadata = null;
        try {
            for (ImageProxy image : images) {
                Future<TotalCaptureResultProxy> metadataFuture =
                        mZslMetadataPool.removeMetadataFuture(image.getTimestamp());
                try {
                    if (mMetadataFilter.apply(metadataFuture.get())) {
                        imageToSave = image;
                        metadata = metadataFuture.get();
                    }
                } catch (ExecutionException | CancellationException e) {
                    // If we cannot get metadata for an image, for whatever
                    // reason, assume it is not acceptable for capture.
                }
            }
        } catch (Exception e) {
            if (imageToSave != null) {
                imageToSave.close();
            }
            throw e;
        } finally {
            for (ImageProxy image : images) {
                if (image != imageToSave) {
                    image.close();
                }
            }
        }
        if (imageToSave == null) {
            return null;
        } else {
            return new Pair<>(imageToSave, metadata);
        }
    }

    @Override
    public void run(Updatable<Void> imageExposeCallback, ImageSaver imageSaver)
            throws InterruptedException, CameraAccessException,
            CameraCaptureSessionClosedException, ResourceAcquisitionFailedException {
        boolean mustCloseImageSaver = true;
        try {
            Pair<ImageProxy, TotalCaptureResultProxy> image = tryGetZslImage();
            if (image != null) {
                mLog.i("ZSL image available");
                imageExposeCallback.update(null);
                imageSaver.addFullSizeImage(image.first, Futures.immediateFuture(image.second));
            } else {
                mLog.i("No ZSL image available, using fallback: " + mFallbackCommand);
                mustCloseImageSaver = false;
                mFallbackCommand.run(imageExposeCallback, imageSaver);
            }
        } catch (BufferQueue.BufferQueueClosedException e) {
            // The zsl ring buffer has been closed, so do nothing since the
            // system is shutting down.
        } finally {
            if (mustCloseImageSaver) {
                imageSaver.close();
            }
        }
    }
}
