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

import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;

/**
 * Immutable class that stores support status for features in the [Device Features] operand.
 * Each feature may be supported, be not supported, or have an unknown support status.
 *
 * @hide
 */
public class DeviceFeatures {

    @IntDef({
            FEATURE_NOT_SUPPORTED,
            FEATURE_SUPPORTED,
            FEATURE_SUPPORT_UNKNOWN
    })
    public @interface FeatureSupportStatus {};

    public static final int FEATURE_NOT_SUPPORTED = 0;
    public static final int FEATURE_SUPPORTED = 1;
    public static final int FEATURE_SUPPORT_UNKNOWN = 2;

    /**
     * Instance representing no knowledge of any feature's support.
     */
    @NonNull
    public static final DeviceFeatures ALL_FEATURES_SUPPORT_UNKNOWN =
            new Builder(FEATURE_SUPPORT_UNKNOWN).build();

    /**
     * Instance representing no support for any feature.
     */
    @NonNull
    public static final DeviceFeatures NO_FEATURES_SUPPORTED =
            new Builder(FEATURE_NOT_SUPPORTED).build();

    @FeatureSupportStatus private final int mRecordTvScreenSupport;
    @FeatureSupportStatus private final int mSetOsdStringSupport;
    @FeatureSupportStatus private final int mDeckControlSupport;
    @FeatureSupportStatus private final int mSetAudioRateSupport;
    @FeatureSupportStatus private final int mArcTxSupport;
    @FeatureSupportStatus private final int mArcRxSupport;
    @FeatureSupportStatus private final int mSetAudioVolumeLevelSupport;

    private DeviceFeatures(@NonNull Builder builder) {
        this.mRecordTvScreenSupport = builder.mRecordTvScreenSupport;
        this.mSetOsdStringSupport = builder.mOsdStringSupport;
        this.mDeckControlSupport = builder.mDeckControlSupport;
        this.mSetAudioRateSupport = builder.mSetAudioRateSupport;
        this.mArcTxSupport = builder.mArcTxSupport;
        this.mArcRxSupport = builder.mArcRxSupport;
        this.mSetAudioVolumeLevelSupport = builder.mSetAudioVolumeLevelSupport;
    }

    /**
     * Converts an instance to a builder.
     */
    public Builder toBuilder() {
        return new Builder(this);
    }

    /**
     * Constructs an instance from a [Device Features] operand.
     *
     * Bit 7 of [Device Features] is currently ignored. It indicates whether the operand spans more
     * than one byte, but only the first byte contains information as of CEC 2.0.
     *
     * @param deviceFeaturesOperand The [Device Features] operand to parse.
     * @return Instance representing the [Device Features] operand.
     */
    @NonNull
    public static DeviceFeatures fromOperand(@NonNull byte[] deviceFeaturesOperand) {
        Builder builder = new Builder(FEATURE_SUPPORT_UNKNOWN);

        // Read feature support status from the bits of [Device Features]
        if (deviceFeaturesOperand.length >= 1) {
            byte b = deviceFeaturesOperand[0];
            builder
                    .setRecordTvScreenSupport(bitToFeatureSupportStatus(((b >> 6) & 1) == 1))
                    .setSetOsdStringSupport(bitToFeatureSupportStatus(((b >> 5) & 1) == 1))
                    .setDeckControlSupport(bitToFeatureSupportStatus(((b >> 4) & 1) == 1))
                    .setSetAudioRateSupport(bitToFeatureSupportStatus(((b >> 3) & 1) == 1))
                    .setArcTxSupport(bitToFeatureSupportStatus(((b >> 2) & 1) == 1))
                    .setArcRxSupport(bitToFeatureSupportStatus(((b >> 1) & 1) == 1))
                    .setSetAudioVolumeLevelSupport(bitToFeatureSupportStatus((b & 1) == 1));
        }
        return builder.build();
    }

    /**
     * Returns the input that is not {@link #FEATURE_SUPPORT_UNKNOWN}. If neither is equal to
     * {@link #FEATURE_SUPPORT_UNKNOWN}, returns the second input.
     */
    private static @FeatureSupportStatus int updateFeatureSupportStatus(
            @FeatureSupportStatus int oldStatus, @FeatureSupportStatus int newStatus) {
        if (newStatus == FEATURE_SUPPORT_UNKNOWN) {
            return oldStatus;
        } else {
            return newStatus;
        }
    }

    /**
     * Returns the [Device Features] operand corresponding to this instance.
     * {@link #FEATURE_SUPPORT_UNKNOWN} maps to 0, indicating no support.
     *
     * As of CEC 2.0, the returned byte array will always be of length 1.
     */
    @NonNull
    public byte[] toOperand() {
        byte result = 0;

        if (mRecordTvScreenSupport == FEATURE_SUPPORTED) result |= (byte) (1 << 6);
        if (mSetOsdStringSupport == FEATURE_SUPPORTED) result |= (byte) (1 << 5);
        if (mDeckControlSupport == FEATURE_SUPPORTED) result |= (byte) (1 << 4);
        if (mSetAudioRateSupport == FEATURE_SUPPORTED) result |= (byte) (1 << 3);
        if (mArcTxSupport == FEATURE_SUPPORTED) result |= (byte) (1 << 2);
        if (mArcRxSupport == FEATURE_SUPPORTED) result |= (byte) (1 << 1);
        if (mSetAudioVolumeLevelSupport == FEATURE_SUPPORTED) result |= (byte) 1;

        return new byte[]{ result };
    }

    @FeatureSupportStatus
    private static int bitToFeatureSupportStatus(boolean bit) {
        return bit ? FEATURE_SUPPORTED : FEATURE_NOT_SUPPORTED;
    }

    /**
     * Returns whether the device is a TV that supports <Record TV Screen>.
     */
    @FeatureSupportStatus
    public int getRecordTvScreenSupport() {
        return mRecordTvScreenSupport;
    }

    /**
     * Returns whether the device is a TV that supports <Set OSD String>.
     */
    @FeatureSupportStatus
    public int getSetOsdStringSupport() {
        return mSetOsdStringSupport;
    }

    /**
     * Returns whether the device supports being controlled by Deck Control.
     */
    @FeatureSupportStatus
    public int getDeckControlSupport() {
        return mDeckControlSupport;
    }

    /**
     * Returns whether the device is a Source that supports <Set Audio Rate>.
     */
    @FeatureSupportStatus
    public int getSetAudioRateSupport() {
        return mSetAudioRateSupport;
    }

    /**
     * Returns whether the device is a Sink that supports ARC Tx.
     */
    @FeatureSupportStatus
    public int getArcTxSupport() {
        return mArcTxSupport;
    }

    /**
     * Returns whether the device is a Source that supports ARC Rx.
     */
    @FeatureSupportStatus
    public int getArcRxSupport() {
        return mArcRxSupport;
    }

    /**
     * Returns whether the device supports <Set Audio Volume Level>.
     */
    @FeatureSupportStatus
    public int getSetAudioVolumeLevelSupport() {
        return mSetAudioVolumeLevelSupport;
    }

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

        DeviceFeatures other = (DeviceFeatures) obj;
        return mRecordTvScreenSupport == other.mRecordTvScreenSupport
                && mSetOsdStringSupport == other.mSetOsdStringSupport
                && mDeckControlSupport == other.mDeckControlSupport
                && mSetAudioRateSupport == other.mSetAudioRateSupport
                && mArcTxSupport == other.mArcTxSupport
                && mArcRxSupport == other.mArcRxSupport
                && mSetAudioVolumeLevelSupport == other.mSetAudioVolumeLevelSupport;
    }

    @Override
    public int hashCode() {
        return java.util.Objects.hash(
                mRecordTvScreenSupport,
                mSetOsdStringSupport,
                mDeckControlSupport,
                mSetAudioRateSupport,
                mArcTxSupport,
                mArcRxSupport,
                mSetAudioVolumeLevelSupport
        );
    }

    @NonNull
    @Override
    public String toString() {
        StringBuilder s = new StringBuilder();
        s.append("Device features: ");
        s.append("record_tv_screen: ")
                .append(featureSupportStatusToString(mRecordTvScreenSupport)).append(" ");
        s.append("set_osd_string: ")
                .append(featureSupportStatusToString(mSetOsdStringSupport)).append(" ");
        s.append("deck_control: ")
                .append(featureSupportStatusToString(mDeckControlSupport)).append(" ");
        s.append("set_audio_rate: ")
                .append(featureSupportStatusToString(mSetAudioRateSupport)).append(" ");
        s.append("arc_tx: ")
                .append(featureSupportStatusToString(mArcTxSupport)).append(" ");
        s.append("arc_rx: ")
                .append(featureSupportStatusToString(mArcRxSupport)).append(" ");
        s.append("set_audio_volume_level: ")
                .append(featureSupportStatusToString(mSetAudioVolumeLevelSupport)).append(" ");
        return s.toString();
    }

    @NonNull
    private static String featureSupportStatusToString(@FeatureSupportStatus int status) {
        switch (status) {
            case FEATURE_SUPPORTED:
                return "Y";
            case FEATURE_NOT_SUPPORTED:
                return "N";
            case FEATURE_SUPPORT_UNKNOWN:
            default:
                return "?";
        }
    }

    /**
     * Builder for {@link DeviceFeatures} instances.
     */
    public static final class Builder {
        @FeatureSupportStatus private int mRecordTvScreenSupport;
        @FeatureSupportStatus private int mOsdStringSupport;
        @FeatureSupportStatus private int mDeckControlSupport;
        @FeatureSupportStatus private int mSetAudioRateSupport;
        @FeatureSupportStatus private int mArcTxSupport;
        @FeatureSupportStatus private int mArcRxSupport;
        @FeatureSupportStatus private int mSetAudioVolumeLevelSupport;

        private Builder(@FeatureSupportStatus int defaultFeatureSupportStatus) {
            mRecordTvScreenSupport = defaultFeatureSupportStatus;
            mOsdStringSupport = defaultFeatureSupportStatus;
            mDeckControlSupport = defaultFeatureSupportStatus;
            mSetAudioRateSupport = defaultFeatureSupportStatus;
            mArcTxSupport = defaultFeatureSupportStatus;
            mArcRxSupport = defaultFeatureSupportStatus;
            mSetAudioVolumeLevelSupport = defaultFeatureSupportStatus;
        }

        private Builder(DeviceFeatures info) {
            mRecordTvScreenSupport = info.getRecordTvScreenSupport();
            mOsdStringSupport = info.getSetOsdStringSupport();
            mDeckControlSupport = info.getDeckControlSupport();
            mSetAudioRateSupport = info.getSetAudioRateSupport();
            mArcTxSupport = info.getArcTxSupport();
            mArcRxSupport = info.getArcRxSupport();
            mSetAudioVolumeLevelSupport = info.getSetAudioVolumeLevelSupport();
        }

        /**
         * Creates a new {@link DeviceFeatures} object.
         */
        public DeviceFeatures build() {
            return new DeviceFeatures(this);
        }

        /**
         * Sets the value for {@link #getRecordTvScreenSupport()}.
         */
        @NonNull
        public Builder setRecordTvScreenSupport(@FeatureSupportStatus int recordTvScreenSupport) {
            mRecordTvScreenSupport = recordTvScreenSupport;
            return this;
        }

        /**
         * Sets the value for {@link #getSetOsdStringSupport()}.
         */
        @NonNull
        public Builder setSetOsdStringSupport(@FeatureSupportStatus int setOsdStringSupport) {
            mOsdStringSupport = setOsdStringSupport;
            return this;
        }

        /**
         * Sets the value for {@link #getDeckControlSupport()}.
         */
        @NonNull
        public Builder setDeckControlSupport(@FeatureSupportStatus int deckControlSupport) {
            mDeckControlSupport = deckControlSupport;
            return this;
        }

        /**
         * Sets the value for {@link #getSetAudioRateSupport()}.
         */
        @NonNull
        public Builder setSetAudioRateSupport(@FeatureSupportStatus int setAudioRateSupport) {
            mSetAudioRateSupport = setAudioRateSupport;
            return this;
        }

        /**
         * Sets the value for {@link #getArcTxSupport()}.
         */
        @NonNull
        public Builder setArcTxSupport(@FeatureSupportStatus int arcTxSupport) {
            mArcTxSupport = arcTxSupport;
            return this;
        }

        /**
         * Sets the value for {@link #getArcRxSupport()}.
         */
        @NonNull
        public Builder setArcRxSupport(@FeatureSupportStatus int arcRxSupport) {
            mArcRxSupport = arcRxSupport;
            return this;
        }

        /**
         * Sets the value for {@link #getSetAudioRateSupport()}.
         */
        @NonNull
        public Builder setSetAudioVolumeLevelSupport(
                @FeatureSupportStatus int setAudioVolumeLevelSupport) {
            mSetAudioVolumeLevelSupport = setAudioVolumeLevelSupport;
            return this;
        }

        /**
         * Updates all fields with those in a 'new' instance of {@link DeviceFeatures}.
         * All fields are replaced with those in the new instance, except when the field is
         * {@link #FEATURE_SUPPORT_UNKNOWN} in the new instance.
         */
        @NonNull
        public Builder update(DeviceFeatures newDeviceFeatures) {
            mRecordTvScreenSupport = updateFeatureSupportStatus(mRecordTvScreenSupport,
                    newDeviceFeatures.getRecordTvScreenSupport());
            mOsdStringSupport = updateFeatureSupportStatus(mOsdStringSupport,
                    newDeviceFeatures.getSetOsdStringSupport());
            mDeckControlSupport = updateFeatureSupportStatus(mDeckControlSupport,
                    newDeviceFeatures.getDeckControlSupport());
            mSetAudioRateSupport = updateFeatureSupportStatus(mSetAudioRateSupport,
                    newDeviceFeatures.getSetAudioRateSupport());
            mArcTxSupport = updateFeatureSupportStatus(mArcTxSupport,
                    newDeviceFeatures.getArcTxSupport());
            mArcRxSupport = updateFeatureSupportStatus(mArcRxSupport,
                    newDeviceFeatures.getArcRxSupport());
            mSetAudioVolumeLevelSupport = updateFeatureSupportStatus(mSetAudioVolumeLevelSupport,
                    newDeviceFeatures.getSetAudioVolumeLevelSupport());
            return this;
        }
    }
}
