/*
 * Copyright (C) 2017 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 static com.android.internal.util.Preconditions.*;

import android.annotation.CallbackExecutor;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.graphics.ColorSpace;
import android.hardware.camera2.CameraCaptureSession;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.params.InputConfiguration;
import android.hardware.camera2.params.OutputConfiguration;
import android.hardware.camera2.utils.HashCodeHelpers;
import android.media.ImageReader;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Log;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Executor;

/**
 * A helper class that aggregates all supported arguments for capture session initialization.
 */
public final class SessionConfiguration implements Parcelable {
    private static final String TAG = "SessionConfiguration";

    /**
     * A regular session type containing instances of {@link OutputConfiguration} running
     * at regular non high speed FPS ranges and optionally {@link InputConfiguration} for
     * reprocessable sessions.
     *
     * @see CameraDevice#createCaptureSession
     * @see CameraDevice#createReprocessableCaptureSession
     */
    public static final int SESSION_REGULAR = CameraDevice.SESSION_OPERATION_MODE_NORMAL;

    /**
     * A high speed session type that can only contain instances of {@link OutputConfiguration}.
     * The outputs can run using high speed FPS ranges. Calls to {@link #setInputConfiguration}
     * are not supported.
     * <p>
     * When using this type, the CameraCaptureSession returned by
     * {@link android.hardware.camera2.CameraCaptureSession.StateCallback} can be cast to a
     * {@link android.hardware.camera2.CameraConstrainedHighSpeedCaptureSession} to access the extra
     * methods for constrained high speed recording.
     * </p>
     *
     * @see CameraDevice#createConstrainedHighSpeedCaptureSession
     */
    public static final int SESSION_HIGH_SPEED =
        CameraDevice.SESSION_OPERATION_MODE_CONSTRAINED_HIGH_SPEED;

    /**
     * First vendor-specific session mode
     * @hide
     */
    public static final int SESSION_VENDOR_START =
        CameraDevice.SESSION_OPERATION_MODE_VENDOR_START;

     /** @hide */
    @Retention(RetentionPolicy.SOURCE)
    @IntDef(prefix = {"SESSION_"}, value =
            {SESSION_REGULAR,
             SESSION_HIGH_SPEED })
    public @interface SessionMode {};

    // Camera capture session related parameters.
    private List<OutputConfiguration> mOutputConfigurations;
    private CameraCaptureSession.StateCallback mStateCallback;
    private int mSessionType;
    private Executor mExecutor = null;
    private InputConfiguration mInputConfig = null;
    private CaptureRequest mSessionParameters = null;
    private int mColorSpace;

    /**
     * Create a new {@link SessionConfiguration}.
     *
     * @param sessionType The session type.
     * @param outputs A list of output configurations for the capture session.
     * @param executor The executor which should be used to invoke the callback. In general it is
     *                 recommended that camera operations are not done on the main (UI) thread.
     * @param cb A state callback interface implementation.
     *
     * @see #SESSION_REGULAR
     * @see #SESSION_HIGH_SPEED
     * @see CameraDevice#createCaptureSession(List, CameraCaptureSession.StateCallback, Handler)
     * @see CameraDevice#createCaptureSessionByOutputConfigurations
     * @see CameraDevice#createReprocessableCaptureSession
     * @see CameraDevice#createConstrainedHighSpeedCaptureSession
     */
    public SessionConfiguration(@SessionMode int sessionType,
            @NonNull List<OutputConfiguration> outputs,
            @NonNull @CallbackExecutor Executor executor,
            @NonNull CameraCaptureSession.StateCallback cb) {
        mSessionType = sessionType;
        mOutputConfigurations = Collections.unmodifiableList(new ArrayList<>(outputs));
        mStateCallback = cb;
        mExecutor = executor;
    }

    /**
     * Create a SessionConfiguration from Parcel.
     * No support for parcelable 'mStateCallback', 'mExecutor' and 'mSessionParameters' yet.
     */
    private SessionConfiguration(@NonNull Parcel source) {
        int sessionType = source.readInt();
        int inputWidth = source.readInt();
        int inputHeight = source.readInt();
        int inputFormat = source.readInt();
        boolean isInputMultiResolution = source.readBoolean();
        ArrayList<OutputConfiguration> outConfigs = new ArrayList<OutputConfiguration>();
        source.readTypedList(outConfigs, OutputConfiguration.CREATOR);

        if ((inputWidth > 0) && (inputHeight > 0) && (inputFormat != -1)) {
            mInputConfig = new InputConfiguration(inputWidth, inputHeight,
                    inputFormat, isInputMultiResolution);
        }
        mSessionType = sessionType;
        mOutputConfigurations = outConfigs;
    }

    public static final @android.annotation.NonNull Parcelable.Creator<SessionConfiguration> CREATOR =
            new Parcelable.Creator<SessionConfiguration> () {
        @Override
        public SessionConfiguration createFromParcel(Parcel source) {
            return new SessionConfiguration(source);
        }

        @Override
        public SessionConfiguration[] newArray(int size) {
            return new SessionConfiguration[size];
        }
    };

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        if (dest == null) {
            throw new IllegalArgumentException("dest must not be null");
        }
        dest.writeInt(mSessionType);
        if (mInputConfig != null) {
            dest.writeInt(mInputConfig.getWidth());
            dest.writeInt(mInputConfig.getHeight());
            dest.writeInt(mInputConfig.getFormat());
            dest.writeBoolean(mInputConfig.isMultiResolution());
        } else {
            dest.writeInt(/*inputWidth*/ 0);
            dest.writeInt(/*inputHeight*/ 0);
            dest.writeInt(/*inputFormat*/ -1);
            dest.writeBoolean(/*isMultiResolution*/ false);
        }
        dest.writeTypedList(mOutputConfigurations);
    }

    @Override
    public int describeContents() {
        return 0;
    }

    /**
     * Check if this {@link SessionConfiguration} is equal to another {@link SessionConfiguration}.
     *
     * <p>Two output session configurations are only equal if and only if the underlying input
     * configuration, output configurations, and session type are equal. </p>
     *
     * @return {@code true} if the objects were equal, {@code false} otherwise
     */
    @Override
    public boolean equals(@Nullable Object obj) {
        if (obj == null) {
            return false;
        } else if (this == obj) {
            return true;
        } else if (obj instanceof SessionConfiguration) {
            final SessionConfiguration other = (SessionConfiguration) obj;
            if (mInputConfig != other.mInputConfig || mSessionType != other.mSessionType ||
                    mOutputConfigurations.size() != other.mOutputConfigurations.size()) {
                return false;
            }

            for (int i = 0;  i < mOutputConfigurations.size(); i++) {
                if (!mOutputConfigurations.get(i).equals(other.mOutputConfigurations.get(i)))
                    return false;
            }

            return true;
        }

        return false;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int hashCode() {
        return HashCodeHelpers.hashCode(mOutputConfigurations.hashCode(), mInputConfig.hashCode(),
                mSessionType);
    }

    /**
     * Retrieve the type of the capture session.
     *
     * @return The capture session type.
     */
    public @SessionMode int getSessionType() {
        return mSessionType;
    }

    /**
     * Retrieve the {@link OutputConfiguration} list for the capture session.
     *
     * @return A list of output configurations for the capture session.
     */
    public List<OutputConfiguration> getOutputConfigurations() {
        return mOutputConfigurations;
    }

    /**
     * Retrieve the {@link CameraCaptureSession.StateCallback} for the capture session.
     *
     * @return A state callback interface implementation.
     */
    public CameraCaptureSession.StateCallback getStateCallback() {
        return mStateCallback;
    }

    /**
     * Retrieve the {@link java.util.concurrent.Executor} for the capture session.
     *
     * @return The Executor on which the callback will be invoked.
     */
    public Executor getExecutor() {
        return mExecutor;
    }

    /**
     * Sets the {@link InputConfiguration} for a reprocessable session. Input configuration are not
     * supported for {@link #SESSION_HIGH_SPEED}.
     *
     * @param input Input configuration.
     * @throws UnsupportedOperationException In case it is called for {@link #SESSION_HIGH_SPEED}
     *                                       type session configuration.
     */
    public void setInputConfiguration(@NonNull InputConfiguration input) {
        if (mSessionType != SESSION_HIGH_SPEED) {
            mInputConfig = input;
        } else {
            throw new UnsupportedOperationException("Method not supported for high speed session" +
                    " types");
        }
    }

    /**
     * Retrieve the {@link InputConfiguration}.
     *
     * @return The capture session input configuration.
     */
    public InputConfiguration getInputConfiguration() {
        return mInputConfig;
    }

    /**
     * Sets the session wide camera parameters (see {@link CaptureRequest}). This argument can
     * be set for every supported session type and will be passed to the camera device as part
     * of the capture session initialization. Session parameters are a subset of the available
     * capture request parameters (see {@link CameraCharacteristics#getAvailableSessionKeys})
     * and their application can introduce internal camera delays. To improve camera performance
     * it is suggested to change them sparingly within the lifetime of the capture session and
     * to pass their initial values as part of this method.
     *
     * @param params A capture request that includes the initial values for any available
     *               session wide capture keys. Tags (see {@link CaptureRequest.Builder#setTag}) and
     *               output targets (see {@link CaptureRequest.Builder#addTarget}) are ignored if
     *               set. Parameter values not part of
     *               {@link CameraCharacteristics#getAvailableSessionKeys} will also be ignored. It
     *               is recommended to build the session parameters using the same template type as
     *               the initial capture request, so that the session and initial request parameters
     *               match as much as possible.
     */
    public void setSessionParameters(CaptureRequest params) {
        mSessionParameters = params;
    }

    /**
     * Retrieve the session wide camera parameters (see {@link CaptureRequest}).
     *
     * @return A capture request that includes the initial values for any available
     *         session wide capture keys.
     */
    public CaptureRequest getSessionParameters() {
        return mSessionParameters;
    }

    /**
     * Set a specific device-supported color space.
     *
     * <p>Clients can choose from any profile advertised as supported in
     * {@link CameraCharacteristics#REQUEST_AVAILABLE_COLOR_SPACE_PROFILES}
     * queried using {@link ColorSpaceProfiles#getSupportedColorSpaces}.
     * When set, the colorSpace will override the default color spaces of the output targets,
     * or the color space implied by the dataSpace passed into an {@link ImageReader}'s
     * constructor.</p>
     */
    public void setColorSpace(@NonNull ColorSpace.Named colorSpace) {
        mColorSpace = colorSpace.ordinal();
        for (OutputConfiguration outputConfiguration : mOutputConfigurations) {
            outputConfiguration.setColorSpace(colorSpace);
        }
    }

    /**
     * Clear the color space, such that the default color space will be used.
     */
    public void clearColorSpace() {
        mColorSpace = ColorSpaceProfiles.UNSPECIFIED;
        for (OutputConfiguration outputConfiguration : mOutputConfigurations) {
            outputConfiguration.clearColorSpace();
        }
    }

    /**
     * Return the current color space.
     *
     * @return the currently set color space
     */
    @SuppressLint("MethodNameUnits")
    public @Nullable ColorSpace getColorSpace() {
        if (mColorSpace != ColorSpaceProfiles.UNSPECIFIED) {
            return ColorSpace.get(ColorSpace.Named.values()[mColorSpace]);
        } else {
            return null;
        }
    }
}
