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

import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.os.Parcel;
import android.os.Parcelable;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;

/**
 * @hide
 * Class to represent the attributes of an audio device: its type (speaker, headset...), address
 * (if known) and role (input, output).
 * <p>Unlike {@link AudioDeviceInfo}, the device
 * doesn't need to be connected to be uniquely identified, it can
 * for instance represent a specific A2DP headset even after a
 * disconnection, whereas the corresponding <code>AudioDeviceInfo</code>
 * would then be invalid.
 * <p>While creating / obtaining an instance is not protected by a
 * permission, APIs using one rely on MODIFY_AUDIO_ROUTING.
 */
@SystemApi
public final class AudioDeviceAttributes implements Parcelable {

    /**
     * A role identifying input devices, such as microphones.
     */
    public static final int ROLE_INPUT = AudioPort.ROLE_SOURCE;
    /**
     * A role identifying output devices, such as speakers or headphones.
     */
    public static final int ROLE_OUTPUT = AudioPort.ROLE_SINK;

    /** @hide */
    @IntDef(flag = false, prefix = "ROLE_", value = {
            ROLE_INPUT, ROLE_OUTPUT }
    )
    @Retention(RetentionPolicy.SOURCE)
    public @interface Role {}

    /**
     * The audio device type, as defined in {@link AudioDeviceInfo}
     */
    private final @AudioDeviceInfo.AudioDeviceType int mType;
    /**
     * The unique address of the device. Some devices don't have addresses, only an empty string.
     */
    private @NonNull String mAddress;
    /**
     * The non-unique name of the device. Some devices don't have names, only an empty string.
     * Should not be used as a unique identifier for a device.
     */
    private final @NonNull String mName;
    /**
     * Is input or output device
     */
    private final @Role int mRole;
    /**
     * The internal audio device type
     */
    private final int mNativeType;
    /**
     * List of AudioProfiles supported by the device
     */
    private final @NonNull List<AudioProfile> mAudioProfiles;
    /**
     * List of AudioDescriptors supported by the device
     */
    private final @NonNull List<AudioDescriptor> mAudioDescriptors;

    /**
     * @hide
     * Constructor from a valid {@link AudioDeviceInfo}
     * @param deviceInfo the connected audio device from which to obtain the device-identifying
     *                   type and address.
     */
    @SystemApi
    public AudioDeviceAttributes(@NonNull AudioDeviceInfo deviceInfo) {
        Objects.requireNonNull(deviceInfo);
        mRole = deviceInfo.isSink() ? ROLE_OUTPUT : ROLE_INPUT;
        mType = deviceInfo.getType();
        mAddress = deviceInfo.getAddress();
        mName = String.valueOf(deviceInfo.getProductName());
        mNativeType = deviceInfo.getInternalType();
        mAudioProfiles = deviceInfo.getAudioProfiles();
        mAudioDescriptors = deviceInfo.getAudioDescriptors();
    }

    /**
     * @hide
     * Constructor from role, device type and address
     * @param role indicates input or output role
     * @param type the device type, as defined in {@link AudioDeviceInfo}
     * @param address the address of the device, or an empty string for devices without one
     */
    @SystemApi
    public AudioDeviceAttributes(@Role int role, @AudioDeviceInfo.AudioDeviceType int type,
            @NonNull String address) {
        this(role, type, address, "", new ArrayList<>(), new ArrayList<>());
    }

    /**
     * @hide
     * Constructor with specification of all attributes
     * @param role indicates input or output role
     * @param type the device type, as defined in {@link AudioDeviceInfo}
     * @param address the address of the device, or an empty string for devices without one
     * @param name the name of the device, or an empty string for devices without one
     * @param profiles the list of AudioProfiles supported by the device
     * @param descriptors the list of AudioDescriptors supported by the device
     */
    @SystemApi
    public AudioDeviceAttributes(@Role int role, @AudioDeviceInfo.AudioDeviceType int type,
            @NonNull String address, @NonNull String name, @NonNull List<AudioProfile> profiles,
            @NonNull List<AudioDescriptor> descriptors) {
        Objects.requireNonNull(address);
        if (role != ROLE_OUTPUT && role != ROLE_INPUT) {
            throw new IllegalArgumentException("Invalid role " + role);
        }
        if (role == ROLE_OUTPUT) {
            AudioDeviceInfo.enforceValidAudioDeviceTypeOut(type);
            mNativeType = AudioDeviceInfo.convertDeviceTypeToInternalDevice(type);
        } else if (role == ROLE_INPUT) {
            AudioDeviceInfo.enforceValidAudioDeviceTypeIn(type);
            mNativeType = AudioDeviceInfo.convertDeviceTypeToInternalInputDevice(type, address);
        } else {
            mNativeType = AudioSystem.DEVICE_NONE;
        }

        mRole = role;
        mType = type;
        mAddress = address;
        mName = name;
        mAudioProfiles = profiles;
        mAudioDescriptors = descriptors;
    }

    /**
     * @hide
     * Constructor called from AudioSystem JNI when creating an AudioDeviceAttributes from a native
     * AudioDeviceTypeAddr instance.
     * @param nativeType the internal device type, as defined in {@link AudioSystem}
     * @param address the address of the device, or an empty string for devices without one
     */
    public AudioDeviceAttributes(int nativeType, @NonNull String address) {
        this(nativeType, address, "");
    }

    /**
     * @hide
     * Constructor called from BtHelper to connect or disconnect a Bluetooth device.
     * @param nativeType the internal device type, as defined in {@link AudioSystem}
     * @param address the address of the device, or an empty string for devices without one
     * @param name the name of the device, or an empty string for devices without one
     */
    public AudioDeviceAttributes(int nativeType, @NonNull String address, @NonNull String name) {
        mRole = AudioSystem.isInputDevice(nativeType) ? ROLE_INPUT : ROLE_OUTPUT;
        mType = AudioDeviceInfo.convertInternalDeviceToDeviceType(nativeType);
        mAddress = address;
        mName = name;
        mNativeType = nativeType;
        mAudioProfiles = new ArrayList<>();
        mAudioDescriptors = new ArrayList<>();
    }

    /**
     * @hide
     * Copy Constructor.
     * @param ada the copied AudioDeviceAttributes
     */
    public AudioDeviceAttributes(AudioDeviceAttributes ada) {
        mRole = ada.getRole();
        mType = ada.getType();
        mAddress = ada.getAddress();
        mName = ada.getName();
        mNativeType = ada.getInternalType();
        mAudioProfiles = ada.getAudioProfiles();
        mAudioDescriptors = ada.getAudioDescriptors();
    }

    /**
     * @hide
     * Returns the role of a device
     * @return the role
     */
    @SystemApi
    public @Role int getRole() {
        return mRole;
    }

    /**
     * @hide
     * Returns the audio device type of a device
     * @return the type, as defined in {@link AudioDeviceInfo}
     */
    @SystemApi
    public @AudioDeviceInfo.AudioDeviceType int getType() {
        return mType;
    }

    /**
     * @hide
     * Returns the address of the audio device, or an empty string for devices without one
     * @return the device address
     */
    @SystemApi
    public @NonNull String getAddress() {
        return mAddress;
    }

    /**
     * @hide
     * Sets the device address. Only used by audio service.
     */
    public void setAddress(@NonNull String address) {
        Objects.requireNonNull(address);
        mAddress = address;
    }

    /**
     * @hide
     * Returns the name of the audio device, or an empty string for devices without one
     * @return the device name
     */
    @SystemApi
    public @NonNull String getName() {
        return mName;
    }

    /**
     * @hide
     * Returns the internal device type of a device
     * @return the internal device type
     */
    public int getInternalType() {
        return mNativeType;
    }

    /**
     * @hide
     * Returns the list of AudioProfiles supported by the device
     * @return the list of AudioProfiles
     */
    @SystemApi
    public @NonNull List<AudioProfile> getAudioProfiles() {
        return mAudioProfiles;
    }

    /**
     * @hide
     * Returns the list of AudioDescriptors supported by the device
     * @return the list of AudioDescriptors
     */
    @SystemApi
    public @NonNull List<AudioDescriptor> getAudioDescriptors() {
        return mAudioDescriptors;
    }

    @Override
    public int hashCode() {
        return Objects.hash(mRole, mType, mAddress, mName, mAudioProfiles, mAudioDescriptors);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        AudioDeviceAttributes that = (AudioDeviceAttributes) o;
        return ((mRole == that.mRole)
                && (mType == that.mType)
                && mAddress.equals(that.mAddress)
                && mName.equals(that.mName)
                && mAudioProfiles.equals(that.mAudioProfiles)
                && mAudioDescriptors.equals(that.mAudioDescriptors));
    }

    /**
     * Returns true if the role, type and address are equal. Called to compare with an
     * AudioDeviceAttributes that was created from a native AudioDeviceTypeAddr instance.
     * @param o object to compare with
     * @return whether role, type and address are equal
     */
    public boolean equalTypeAddress(@Nullable Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        AudioDeviceAttributes that = (AudioDeviceAttributes) o;
        return ((mRole == that.mRole)
                && (mType == that.mType)
                && mAddress.equals(that.mAddress));
    }

    /** @hide */
    public static String roleToString(@Role int role) {
        return (role == ROLE_OUTPUT ? "output" : "input");
    }

    @Override
    public String toString() {
        return new String("AudioDeviceAttributes:"
                + " role:" + roleToString(mRole)
                + " type:" + (mRole == ROLE_OUTPUT ? AudioSystem.getOutputDeviceName(mNativeType)
                        : AudioSystem.getInputDeviceName(mNativeType))
                + " addr:" + Utils.anonymizeBluetoothAddress(mNativeType, mAddress)
                + " name:" + mName
                + " profiles:" + mAudioProfiles.toString()
                + " descriptors:" + mAudioDescriptors.toString());
    }

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

    @Override
    public void writeToParcel(@NonNull Parcel dest, int flags) {
        dest.writeInt(mRole);
        dest.writeInt(mType);
        dest.writeString(mAddress);
        dest.writeString(mName);
        dest.writeInt(mNativeType);
        dest.writeParcelableArray(
                mAudioProfiles.toArray(new AudioProfile[mAudioProfiles.size()]), flags);
        dest.writeParcelableArray(
                mAudioDescriptors.toArray(new AudioDescriptor[mAudioDescriptors.size()]), flags);
    }

    private AudioDeviceAttributes(@NonNull Parcel in) {
        mRole = in.readInt();
        mType = in.readInt();
        mAddress = in.readString();
        mName = in.readString();
        mNativeType = in.readInt();
        AudioProfile[] audioProfilesArray =
                in.readParcelableArray(AudioProfile.class.getClassLoader(), AudioProfile.class);
        mAudioProfiles = new ArrayList<AudioProfile>(Arrays.asList(audioProfilesArray));
        AudioDescriptor[] audioDescriptorsArray = in.readParcelableArray(
                AudioDescriptor.class.getClassLoader(), AudioDescriptor.class);
        mAudioDescriptors = new ArrayList<AudioDescriptor>(Arrays.asList(audioDescriptorsArray));
    }

    public static final @NonNull Parcelable.Creator<AudioDeviceAttributes> CREATOR =
            new Parcelable.Creator<AudioDeviceAttributes>() {
        /**
         * Rebuilds an AudioDeviceAttributes previously stored with writeToParcel().
         * @param p Parcel object to read the AudioDeviceAttributes from
         * @return a new AudioDeviceAttributes created from the data in the parcel
         */
        public AudioDeviceAttributes createFromParcel(Parcel p) {
            return new AudioDeviceAttributes(p);
        }

        public AudioDeviceAttributes[] newArray(int size) {
            return new AudioDeviceAttributes[size];
        }
    };
}
