/*
 * Copyright 2019 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.camera2.internal.compat.params;

import android.hardware.camera2.CameraCaptureSession;
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.params.SessionConfiguration;
import android.os.Build;

import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
import androidx.camera.camera2.internal.compat.CameraDeviceCompat;

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.Objects;
import java.util.concurrent.Executor;

/**
 * Helper for accessing features in SessionConfiguration in a backwards compatible fashion.
 */
@SuppressWarnings("unused")
@RequiresApi(21)
public final class SessionConfigurationCompat {

    /**
     * A regular session type containing instances of {@link OutputConfigurationCompat} running
     * at regular non high speed FPS ranges and optionally {@link InputConfigurationCompat} for
     * reprocessable sessions.
     *
     * @see CameraDevice#createCaptureSession
     * @see CameraDevice#createReprocessableCaptureSession
     */
    public static final int SESSION_REGULAR = CameraDeviceCompat.SESSION_OPERATION_MODE_NORMAL;
    /**
     * A high speed session type that can only contain instances of
     * {@link OutputConfigurationCompat}.
     * The outputs can run using high speed FPS ranges. Calls to {@link #setInputConfiguration}
     * are not supported.
     *
     * @see CameraDevice#createConstrainedHighSpeedCaptureSession
     */
    public static final int SESSION_HIGH_SPEED =
            CameraDeviceCompat.SESSION_OPERATION_MODE_CONSTRAINED_HIGH_SPEED;
    private final SessionConfigurationCompatImpl mImpl;

    /**
     * Create a new {@link SessionConfigurationCompat}.
     *
     * @param sessionType   The session type.
     * @param outputsCompat 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
     */
    public SessionConfigurationCompat(@SessionMode int sessionType,
            @NonNull List<OutputConfigurationCompat> outputsCompat,
            @NonNull /* @CallbackExecutor */ Executor executor,
            @NonNull CameraCaptureSession.StateCallback cb) {
        if (Build.VERSION.SDK_INT < 28) {
            mImpl = new SessionConfigurationCompatBaseImpl(sessionType, outputsCompat, executor,
                    cb);
        } else {
            mImpl = new SessionConfigurationCompatApi28Impl(sessionType, outputsCompat, executor,
                    cb);
        }
    }

    private SessionConfigurationCompat(@NonNull SessionConfigurationCompatImpl impl) {
        mImpl = impl;
    }

    /**
     * Creates an instance from a framework android.hardware.camera2.params.SessionConfiguration
     * object.
     *
     * <p>This method always returns {@code null} on API &lt;= 27.</p>
     *
     * @param sessionConfiguration an android.hardware.camera2.params.SessionConfiguration object,
     *                             or {@code null} if none.
     * @return an equivalent {@link SessionConfigurationCompat} object, or {@code null} if not
     * supported.
     */
    @Nullable
    public static SessionConfigurationCompat wrap(@Nullable Object sessionConfiguration) {
        if (sessionConfiguration == null) {
            return null;
        }
        if (Build.VERSION.SDK_INT < 28) {
            return null;
        }

        return new SessionConfigurationCompat(
                new SessionConfigurationCompatApi28Impl(sessionConfiguration));
    }

    @RequiresApi(24)
    static List<OutputConfigurationCompat> transformToCompat(
            @NonNull List<OutputConfiguration> outputConfigurations) {
        ArrayList<OutputConfigurationCompat> outList = new ArrayList<>(outputConfigurations.size());
        for (OutputConfiguration outputConfiguration : outputConfigurations) {
            outList.add(OutputConfigurationCompat.wrap(outputConfiguration));
        }

        return outList;
    }

    @NonNull
    @RequiresApi(24)
    @RestrictTo(Scope.LIBRARY)
    public static List<OutputConfiguration> transformFromCompat(
            @NonNull List<OutputConfigurationCompat> outputConfigurations) {
        ArrayList<OutputConfiguration> outList = new ArrayList<>(outputConfigurations.size());
        for (OutputConfigurationCompat outputConfiguration : outputConfigurations) {
            outList.add((OutputConfiguration) outputConfiguration.unwrap());
        }

        return outList;
    }

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

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

    /**
     * Retrieve the {@link CameraCaptureSession.StateCallback} for the capture session.
     *
     * @return A state callback interface implementation. May be {@code null} when created from
     * parcel.
     */
    @NonNull
    public CameraCaptureSession.StateCallback getStateCallback() {
        return mImpl.getStateCallback();
    }

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

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

    /**
     * Sets the {@link InputConfigurationCompat} 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 InputConfigurationCompat input) {
        mImpl.setInputConfiguration(input);
    }

    /**
     * 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.
     */
    @Nullable
    public CaptureRequest getSessionParameters() {
        return mImpl.getSessionParameters();
    }

    /**
     * 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 {@code 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
     *               {@code 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(@NonNull CaptureRequest params) {
        mImpl.setSessionParameters(params);
    }

    /**
     * Gets the underlying framework android.hardware.camera2.params.SessionConfiguration object.
     *
     * <p>This method always returns {@code null} on API &lt;= 27.</p>
     *
     * @return an equivalent android.hardware.camera2.params.SessionConfiguration object, or
     * {@code null} if not supported.
     */
    @Nullable
    public Object unwrap() {
        return mImpl.getSessionConfiguration();
    }

    @Override
    public boolean equals(@Nullable Object obj) {
        if (!(obj instanceof SessionConfigurationCompat)) {
            return false;
        }

        return mImpl.equals(((SessionConfigurationCompat) obj).mImpl);
    }

    @Override
    public int hashCode() {
        return mImpl.hashCode();
    }

    @RestrictTo(Scope.LIBRARY)
    @Retention(RetentionPolicy.SOURCE)
    @IntDef(value =
            {SESSION_REGULAR, SESSION_HIGH_SPEED})
    public @interface SessionMode {
    }

    private interface SessionConfigurationCompatImpl {
        @SessionMode
        int getSessionType();

        @NonNull
        List<OutputConfigurationCompat> getOutputConfigurations();

        @NonNull
        CameraCaptureSession.StateCallback getStateCallback();

        @NonNull
        Executor getExecutor();

        @Nullable
        InputConfigurationCompat getInputConfiguration();

        void setInputConfiguration(@NonNull InputConfigurationCompat input);

        @Nullable
        CaptureRequest getSessionParameters();

        void setSessionParameters(@NonNull CaptureRequest params);

        @Nullable
        Object getSessionConfiguration();
    }

    @RequiresApi(21)
    private static final class SessionConfigurationCompatBaseImpl implements
            SessionConfigurationCompatImpl {

        private final List<OutputConfigurationCompat> mOutputConfigurations;
        private final CameraCaptureSession.StateCallback mStateCallback;
        private final Executor mExecutor;
        private final int mSessionType;
        private InputConfigurationCompat mInputConfig = null;
        private CaptureRequest mSessionParameters = null;

        SessionConfigurationCompatBaseImpl(@SessionMode int sessionType,
                @NonNull List<OutputConfigurationCompat> outputs,
                @NonNull /* @CallbackExecutor */ Executor executor,
                @NonNull CameraCaptureSession.StateCallback cb) {
            mSessionType = sessionType;
            mOutputConfigurations = Collections.unmodifiableList(new ArrayList<>(outputs));
            mStateCallback = cb;
            mExecutor = executor;
        }

        @Override
        public int getSessionType() {
            return mSessionType;
        }

        @NonNull
        @Override
        public List<OutputConfigurationCompat> getOutputConfigurations() {
            return mOutputConfigurations;
        }

        @NonNull
        @Override
        public CameraCaptureSession.StateCallback getStateCallback() {
            return mStateCallback;
        }

        @NonNull
        @Override
        public Executor getExecutor() {
            return mExecutor;
        }

        @Nullable
        @Override
        public InputConfigurationCompat getInputConfiguration() {
            return mInputConfig;
        }

        @Override
        public void setInputConfiguration(@NonNull InputConfigurationCompat input) {
            if (mSessionType != SESSION_HIGH_SPEED) {
                mInputConfig = input;
            } else {
                throw new UnsupportedOperationException(
                        "Method not supported for high speed session types");
            }
        }

        @Override
        public CaptureRequest getSessionParameters() {
            return mSessionParameters;
        }

        @Override
        public void setSessionParameters(@NonNull CaptureRequest params) {
            mSessionParameters = params;
        }

        @Nullable
        @Override
        public Object getSessionConfiguration() {
            return null;
        }

        @Override
        public boolean equals(@Nullable Object obj) {
            if (this == obj) {
                return true;
            } else if (obj instanceof SessionConfigurationCompatBaseImpl) {
                SessionConfigurationCompatBaseImpl other = (SessionConfigurationCompatBaseImpl) obj;
                if (!Objects.equals(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;
        }

        @Override
        public int hashCode() {
            int h = 1;
            // Strength reduction; in case the compiler has illusions about divisions being faster
            h = ((h << 5) - h)
                    ^ mOutputConfigurations.hashCode(); // (h * 31) XOR mOutputConfigurations
            // .hashCode()
            h = ((h << 5) - h) ^ (mInputConfig == null ? 0
                    : mInputConfig.hashCode()); // (h * 31) XOR mInputConfig.hashCode()
            h = ((h << 5) - h) ^ mSessionType; // (h * 31) XOR mSessionType

            return h;
        }
    }

    @RequiresApi(28)
    private static final class SessionConfigurationCompatApi28Impl implements
            SessionConfigurationCompatImpl {

        private final SessionConfiguration mObject;
        private final List<OutputConfigurationCompat> mOutputConfigurations;

        SessionConfigurationCompatApi28Impl(@NonNull Object sessionConfiguration) {
            mObject = (SessionConfiguration) sessionConfiguration;
            mOutputConfigurations = Collections.unmodifiableList(transformToCompat(
                    ((SessionConfiguration) sessionConfiguration).getOutputConfigurations()));
        }

        SessionConfigurationCompatApi28Impl(@SessionMode int sessionType,
                @NonNull List<OutputConfigurationCompat> outputs,
                @NonNull /* @CallbackExecutor */ Executor executor,
                @NonNull CameraCaptureSession.StateCallback cb) {
            this(new SessionConfiguration(sessionType, transformFromCompat(outputs), executor, cb));
        }

        @Override
        public int getSessionType() {
            return mObject.getSessionType();
        }

        @NonNull
        @Override
        public List<OutputConfigurationCompat> getOutputConfigurations() {
            // Return cached compat version of list
            return mOutputConfigurations;
        }

        @NonNull
        @Override
        public CameraCaptureSession.StateCallback getStateCallback() {
            return mObject.getStateCallback();
        }

        @NonNull
        @Override
        public Executor getExecutor() {
            return mObject.getExecutor();
        }

        @Override
        public InputConfigurationCompat getInputConfiguration() {
            return InputConfigurationCompat.wrap(mObject.getInputConfiguration());
        }

        @Override
        public void setInputConfiguration(@NonNull InputConfigurationCompat input) {
            mObject.setInputConfiguration((InputConfiguration) input.unwrap());
        }

        @Override
        public CaptureRequest getSessionParameters() {
            return mObject.getSessionParameters();
        }

        @Override
        public void setSessionParameters(@NonNull CaptureRequest params) {
            mObject.setSessionParameters(params);
        }

        @Nullable
        @Override
        public Object getSessionConfiguration() {
            return mObject;
        }

        @Override
        public boolean equals(@Nullable Object obj) {
            if (!(obj instanceof SessionConfigurationCompatApi28Impl)) {
                return false;
            }

            return Objects.equals(mObject, ((SessionConfigurationCompatApi28Impl) obj).mObject);
        }

        @Override
        public int hashCode() {
            return mObject.hashCode();
        }
    }
}
