/*
 * Copyright (C) 2018 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.params;

import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.graphics.ImageFormat;
import android.graphics.PixelFormat;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CameraMetadata;
import android.hardware.camera2.CaptureRequest;
import android.util.ArraySet;
import android.util.Range;
import android.util.Size;
import android.view.Surface;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

import java.util.Arrays;
import java.util.Collections;
import java.util.Set;

/**
 * Immutable class to store the recommended stream configurations to set up
 * {@link android.view.Surface Surfaces} for creating a
 * {@link android.hardware.camera2.CameraCaptureSession capture session} with
 * {@link android.hardware.camera2.CameraDevice#createCaptureSession}.
 *
 * <p>The recommended list does not replace or deprecate the exhaustive complete list found in
 * {@link StreamConfigurationMap}. It is a suggestion about available power and performance
 * efficient stream configurations for a specific use case. Per definition it is only a subset
 * of {@link StreamConfigurationMap} and can be considered by developers for optimization
 * purposes.</p>
 *
 * <p>This also duplicates the minimum frame durations and stall durations from the
 * {@link StreamConfigurationMap} for each format/size combination that can be used to calculate
 * effective frame rate when submitting multiple captures.
 * </p>
 *
 * <p>An instance of this object is available by invoking
 * {@link CameraCharacteristics#getRecommendedStreamConfigurationMap} and passing a respective
 * usecase id. For more information about supported use case constants see
 * {@link #USECASE_PREVIEW}.</p>
 *
 * <pre><code>{@code
 * CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraId);
 * RecommendedStreamConfigurationMap configs = characteristics.getRecommendedStreamConfigurationMap(
 *         RecommendedStreamConfigurationMap.USECASE_PREVIEW);
 * }</code></pre>
 *
 * @see CameraCharacteristics#getRecommendedStreamConfigurationMap
 * @see CameraDevice#createCaptureSession
 */
public final class RecommendedStreamConfigurationMap {

    private static final String TAG = "RecommendedStreamConfigurationMap";
    private int mUsecase;
    private boolean mSupportsPrivate;
    private StreamConfigurationMap mRecommendedMap;

    /** @hide */
    public static final int MAX_USECASE_COUNT = 32;

    /**
     * The recommended stream configuration map for use case preview must contain a subset of
     * efficient, non-stalling configurations that must include both
     * {@link android.graphics.ImageFormat#PRIVATE} and
     * {@link android.graphics.ImageFormat#YUV_420_888} output formats. Even if available for the
     * camera device, high speed or input configurations will be absent.
     */
    public static final int USECASE_PREVIEW = 0x0;

    /**
     * The recommended stream configuration map for recording must contain a subset of efficient
     * video configurations that include {@link android.graphics.ImageFormat#PRIVATE}
     * output format for at least all supported {@link android.media.CamcorderProfile profiles}.
     * High speed configurations if supported will be available as well. Even if available for the
     * camera device, input configurations will be absent.
     */
    public static final int USECASE_RECORD = 0x1;

    /**
     * The recommended stream configuration map for use case video snapshot must only contain a
     * subset of efficient liveshot configurations that include
     * {@link android.graphics.ImageFormat#JPEG} output format. The sizes will match at least
     * the maximum resolution of usecase record and will not cause any preview glitches. Even
     * if available for the camera device, high speed or input configurations will be absent.
     */
    public static final int USECASE_VIDEO_SNAPSHOT = 0x2;

    /**
     * The recommended stream configuration map for use case snapshot must contain a subset of
     * efficient still capture configurations that must include
     * {@link android.graphics.ImageFormat#JPEG} output format and at least one configuration with
     * size approximately equal to the sensor pixel array size
     * {@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE}.
     * Even if available for the camera device, high speed or input configurations will be absent.
     */
    public static final int USECASE_SNAPSHOT = 0x3;

    /**
     * In case the device supports
     * {@link CameraMetadata#REQUEST_AVAILABLE_CAPABILITIES_YUV_REPROCESSING} and/or
     * {@link CameraMetadata#REQUEST_AVAILABLE_CAPABILITIES_PRIVATE_REPROCESSING},
     * the recommended stream configuration map for use case ZSL must contain a subset of efficient
     * configurations that include the suggested input and output format mappings. Even if
     * available for the camera device, high speed configurations will be absent.
     */
    public static final int USECASE_ZSL = 0x4;

    /**
     * In case the device supports
     * {@link android.hardware.camera2.CameraMetadata#REQUEST_AVAILABLE_CAPABILITIES_RAW}, the
     * recommended stream configuration map for use case RAW must contain a subset of efficient
     * configurations that include the {@link android.graphics.ImageFormat#RAW_SENSOR} and other
     * RAW output formats. Even if available for the camera device, high speed and input
     * configurations will be absent.
     */
    public static final int USECASE_RAW = 0x5;

    /**
     * Device specific use cases.
     * @hide
     */
    public static final int USECASE_VENDOR_START = 0x18;

    /** @hide */
    @Retention(RetentionPolicy.SOURCE)
    @IntDef(prefix = {"USECASE_"}, value =
        {USECASE_PREVIEW,
        USECASE_RECORD,
        USECASE_VIDEO_SNAPSHOT,
        USECASE_SNAPSHOT,
        USECASE_ZSL,
        USECASE_RAW })
     public @interface RecommendedUsecase {};

    /**
     * Create a new {@link RecommendedStreamConfigurationMap}.
     *
     * @param recommendedMap stream configuration map that contains for the specific use case
     * @param usecase Recommended use case
     * @param supportsPrivate Flag indicating private format support.
     *
     * @hide
     */
    public RecommendedStreamConfigurationMap(StreamConfigurationMap recommendedMap, int usecase,
            boolean supportsPrivate) {
        mRecommendedMap = recommendedMap;
        mUsecase = usecase;
        mSupportsPrivate = supportsPrivate;
    }

    /**
     * Get the use case value for the recommended stream configurations.
     *
     * @return Use case id.
     */
    public int getRecommendedUseCase() {
        return mUsecase;
    }

    private Set<Integer> getUnmodifiableIntegerSet(int[] intArray) {
        if ((intArray != null) && (intArray.length > 0)) {
            ArraySet<Integer> integerSet = new ArraySet<Integer>();
            integerSet.ensureCapacity(intArray.length);
            for (int intEntry : intArray) {
                integerSet.add(intEntry);
            }

            return Collections.unmodifiableSet(integerSet);
        }

        return null;
    }

    /**
     * Get the image {@code format} output formats in this stream configuration.
     *
     * <p>
     * For more information refer to {@link StreamConfigurationMap#getOutputFormats}.
     * </p>
     *
     * @return a non-modifiable set of Integer formats
     */
    public @NonNull Set<Integer> getOutputFormats() {
        return getUnmodifiableIntegerSet(mRecommendedMap.getOutputFormats());
    }

    /**
     * Get the image {@code format} output formats for a reprocessing input format.
     *
     * <p>
     * For more information refer to {@link StreamConfigurationMap#getValidOutputFormatsForInput}.
     * </p>
     *
     * @return a non-modifiable set of Integer formats
     */
    public @Nullable Set<Integer> getValidOutputFormatsForInput(int inputFormat) {
        return getUnmodifiableIntegerSet(mRecommendedMap.getValidOutputFormatsForInput(
                    inputFormat));
    }

    /**
     * Get the image {@code format} input formats in this stream configuration.
     *
     * <p>All image formats returned by this function will be defined in either {@link ImageFormat}
     * or in {@link PixelFormat} (and there is no possibility of collision).</p>
     *
     * @return a non-modifiable set of Integer formats
     */
    public @Nullable Set<Integer> getInputFormats() {
        return getUnmodifiableIntegerSet(mRecommendedMap.getInputFormats());
    }

    private Set<Size> getUnmodifiableSizeSet(Size[] sizeArray) {
        if ((sizeArray != null) && (sizeArray.length > 0)) {
            ArraySet<Size> sizeSet = new ArraySet<Size>();
            sizeSet.addAll(Arrays.asList(sizeArray));
            return Collections.unmodifiableSet(sizeSet);
        }

        return  null;
    }

    /**
     * Get the supported input sizes for this input format.
     *
     * <p>The format must have come from {@link #getInputFormats}; otherwise
     * {@code null} is returned.</p>
     *
     * @param format a format from {@link #getInputFormats}
     * @return a non-modifiable set of sizes, or {@code null} if the format was not available.
     */
    public @Nullable Set<Size> getInputSizes(int format) {
        return getUnmodifiableSizeSet(mRecommendedMap.getInputSizes(format));
    }

    /**
     * Determine whether or not output surfaces with a particular user-defined format can be passed
     * {@link CameraDevice#createCaptureSession createCaptureSession}.
     *
     * <p>
     * For further information refer to {@link StreamConfigurationMap#isOutputSupportedFor}.
     * </p>
     *
     *
     * @param format an image format from either {@link ImageFormat} or {@link PixelFormat}
     * @return
     *          {@code true} if using a {@code surface} with this {@code format} will be
     *          supported with {@link CameraDevice#createCaptureSession}
     *
     * @throws IllegalArgumentException
     *          if the image format was not a defined named constant
     *          from either {@link ImageFormat} or {@link PixelFormat}
     */
    public boolean isOutputSupportedFor(int format) {
        return mRecommendedMap.isOutputSupportedFor(format);
    }

    /**
     * Get a list of sizes compatible with the requested image {@code format}.
     *
     * <p>
     * For more information refer to {@link StreamConfigurationMap#getOutputSizes}.
     * </p>
     *
     *
     * @param format an image format from {@link ImageFormat} or {@link PixelFormat}
     * @return  a non-modifiable set of supported sizes,
     *          or {@code null} if the {@code format} is not a supported output
     */
    public @Nullable Set<Size> getOutputSizes(int format) {
        return getUnmodifiableSizeSet(mRecommendedMap.getOutputSizes(format));
    }

    /**
     * Get a list of supported high speed video recording sizes.
     * <p>
     * For more information refer to {@link StreamConfigurationMap#getHighSpeedVideoSizes}.
     * </p>
     *
     * @return a non-modifiable set of supported high speed video recording sizes
     */
    public @Nullable Set<Size> getHighSpeedVideoSizes() {
        return getUnmodifiableSizeSet(mRecommendedMap.getHighSpeedVideoSizes());
    }

    private Set<Range<Integer>> getUnmodifiableRangeSet(Range<Integer>[] rangeArray) {
        if ((rangeArray != null) && (rangeArray.length > 0)) {
            ArraySet<Range<Integer>> rangeSet = new ArraySet<Range<Integer>>();
            rangeSet.addAll(Arrays.asList(rangeArray));
            return Collections.unmodifiableSet(rangeSet);
        }

        return null;
    }

    /**
     * Get the frame per second ranges (fpsMin, fpsMax) for input high speed video size.
     *
     * <p>
     * For further information refer to
     * {@link StreamConfigurationMap#getHighSpeedVideoFpsRangesFor}.
     * </p>
     * @param size one of the sizes returned by {@link #getHighSpeedVideoSizes()}
     * @return a non-modifiable set of supported high speed video recording FPS ranges The upper
     *         bound of returned ranges is guaranteed to be greater than or equal to 120.
     * @throws IllegalArgumentException if input size does not exist in the return value of
     *             getHighSpeedVideoSizes
     */
    public @Nullable Set<Range<Integer>> getHighSpeedVideoFpsRangesFor(Size size) {
        return getUnmodifiableRangeSet(mRecommendedMap.getHighSpeedVideoFpsRangesFor(size));
    }

    /**
     * Get a list of supported high speed video recording FPS ranges.
     * <p>
     * For further information refer to {@link StreamConfigurationMap#getHighSpeedVideoFpsRanges}.
     * </p>
     * @return a non-modifiable set of supported high speed video recording FPS ranges The upper
     *         bound of returned ranges is guaranteed to be larger or equal to 120.
     */
    public @Nullable Set<Range<Integer>> getHighSpeedVideoFpsRanges() {
        return getUnmodifiableRangeSet(mRecommendedMap.getHighSpeedVideoFpsRanges());
    }

    /**
     * Get the supported video sizes for an input high speed FPS range.
     *
     * <p>
     * For further information refer to {@link StreamConfigurationMap#getHighSpeedVideoSizesFor}.
     * </p>
     *
     * @param fpsRange one of the FPS range returned by {@link #getHighSpeedVideoFpsRanges()}
     * @return A non-modifiable set of video sizes to create high speed capture sessions for high
     *         speed streaming use cases.
     *
     * @throws IllegalArgumentException if input FPS range does not exist in the return value of
     *         getHighSpeedVideoFpsRanges
     */
    public @Nullable Set<Size> getHighSpeedVideoSizesFor(Range<Integer> fpsRange) {
        return getUnmodifiableSizeSet(mRecommendedMap.getHighSpeedVideoSizesFor(fpsRange));
    }

    /**
     * Get a list of supported high resolution sizes, which cannot operate at full BURST_CAPTURE
     * rate.
     *
     * <p>
     * For further information refer to {@link StreamConfigurationMap#getHighResolutionOutputSizes}.
     * </p>
     *
     * @return a non-modifiable set of supported slower high-resolution sizes, or {@code null} if
     *         the BURST_CAPTURE capability is not supported
     */
    public @Nullable Set<Size> getHighResolutionOutputSizes(int format) {
        return getUnmodifiableSizeSet(mRecommendedMap.getHighResolutionOutputSizes(format));
    }

    /**
     * Get the minimum
     * {@link android.hardware.camera2.CaptureRequest#SENSOR_FRAME_DURATION frame duration}
     * for the format/size combination (in nanoseconds).
     *
     * <p>
     * For further information refer to {@link StreamConfigurationMap#getOutputMinFrameDuration}.
     * </p>
     *
     * @param format an image format from {@link ImageFormat} or {@link PixelFormat}
     * @param size an output-compatible size
     * @return a minimum frame duration {@code >} 0 in nanoseconds, or
     *          0 if the minimum frame duration is not available.
     *
     * @throws IllegalArgumentException if {@code format} or {@code size} was not supported
     * @throws NullPointerException if {@code size} was {@code null}
     *
     */
    public long getOutputMinFrameDuration(int format, Size size) {
        return mRecommendedMap.getOutputMinFrameDuration(format, size);
    }

    /**
     * Get the stall duration for the format/size combination (in nanoseconds).
     *
     * <p>
     * For further information refer to {@link StreamConfigurationMap#getOutputStallDuration}.
     * </p>
     *
     * @param format an image format from {@link ImageFormat} or {@link PixelFormat}
     * @param size an output-compatible size
     * @return a stall duration {@code >=} 0 in nanoseconds
     *
     * @throws IllegalArgumentException if {@code format} or {@code size} was not supported
     * @throws NullPointerException if {@code size} was {@code null}
     */
    public long getOutputStallDuration(int format, Size size) {
        return mRecommendedMap.getOutputStallDuration(format, size);
    }

    /**
     * Get a list of sizes compatible with {@code klass} to use as an output.
     *
     * <p>For further information refer to {@link StreamConfigurationMap#getOutputSizes(Class)}.
     * </p>
     *
     * @param klass
     *          a non-{@code null} {@link Class} object reference
     * @return
     *          a non-modifiable set of supported sizes for {@link ImageFormat#PRIVATE} format,
     *          or {@code null} if the {@code klass} is not a supported output.
     *
     *
     * @throws NullPointerException if {@code klass} was {@code null}
     *
     */
    public <T> @Nullable Set<Size> getOutputSizes(Class<T> klass) {
        if (mSupportsPrivate) {
            return getUnmodifiableSizeSet(mRecommendedMap.getOutputSizes(klass));
        }

        return null;
    }

    /**
     * Get the minimum {@link CaptureRequest#SENSOR_FRAME_DURATION frame duration}
     * for the class/size combination (in nanoseconds).
     *
     * <p>For more information refer to
     * {@link StreamConfigurationMap#getOutputMinFrameDuration(Class, Size)}.</p>
     *
     * @param klass
     *          a class which  has a non-empty array returned by {@link #getOutputSizes(Class)}
     * @param size an output-compatible size
     * @return a minimum frame duration {@code >} 0 in nanoseconds, or
     *          0 if the minimum frame duration is not available.
     *
     * @throws IllegalArgumentException if {@code klass} or {@code size} was not supported
     * @throws NullPointerException if {@code size} or {@code klass} was {@code null}
     *
     */
    public <T> long getOutputMinFrameDuration(final Class<T> klass, final Size size) {
        if (mSupportsPrivate) {
            return mRecommendedMap.getOutputMinFrameDuration(klass, size);
        }

        return 0;
    }

    /**
     * Get the stall duration for the class/size combination (in nanoseconds).
     *
     * <p>For more information refer to
     * {@link StreamConfigurationMap#getOutputStallDuration(Class, Size)}.
     *
     * @param klass
     *          a class which has a non-empty array returned by {@link #getOutputSizes(Class)}.
     * @param size an output-compatible size
     * @return a minimum frame duration {@code >=} 0 in nanoseconds, or 0 if the stall duration is
     *         not available.
     *
     * @throws IllegalArgumentException if {@code klass} or {@code size} was not supported
     * @throws NullPointerException if {@code size} or {@code klass} was {@code null}
     *
     */
    public <T> long getOutputStallDuration(final Class<T> klass, final Size size) {
        if (mSupportsPrivate) {
            return mRecommendedMap.getOutputStallDuration(klass, size);
        }

        return 0;
    }

    /**
     * Determine whether or not the {@code surface} in its current state is suitable to be included
     * in a {@link CameraDevice#createCaptureSession capture session} as an output.
     *
     * <p>For more information refer to {@link StreamConfigurationMap#isOutputSupportedFor}.
     * </p>
     *
     * @param surface a non-{@code null} {@link Surface} object reference
     * @return {@code true} if this is supported, {@code false} otherwise
     *
     * @throws NullPointerException if {@code surface} was {@code null}
     * @throws IllegalArgumentException if the Surface endpoint is no longer valid
     *
     */
    public boolean isOutputSupportedFor(Surface surface) {
        return mRecommendedMap.isOutputSupportedFor(surface);
    }

}
