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

import android.annotation.IntDef;
import android.annotation.NonNull;
import android.compat.annotation.UnsupportedAppUsage;
import android.os.Parcel;
import android.os.ParcelUuid;
import android.os.Parcelable;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

/**
 * Represents a Bluetooth GATT Characteristic
 *
 * <p>A GATT characteristic is a basic data element used to construct a GATT service, {@link
 * BluetoothGattService}. The characteristic contains a value as well as additional information and
 * optional GATT descriptors, {@link BluetoothGattDescriptor}.
 */
public class BluetoothGattCharacteristic implements Parcelable {

    /** Characteristic property: Characteristic is broadcastable. */
    public static final int PROPERTY_BROADCAST = 0x01;

    /** Characteristic property: Characteristic is readable. */
    public static final int PROPERTY_READ = 0x02;

    /** Characteristic property: Characteristic can be written without response. */
    public static final int PROPERTY_WRITE_NO_RESPONSE = 0x04;

    /** Characteristic property: Characteristic can be written. */
    public static final int PROPERTY_WRITE = 0x08;

    /** Characteristic property: Characteristic supports notification */
    public static final int PROPERTY_NOTIFY = 0x10;

    /** Characteristic property: Characteristic supports indication */
    public static final int PROPERTY_INDICATE = 0x20;

    /** Characteristic property: Characteristic supports write with signature */
    public static final int PROPERTY_SIGNED_WRITE = 0x40;

    /** Characteristic property: Characteristic has extended properties */
    public static final int PROPERTY_EXTENDED_PROPS = 0x80;

    /** Characteristic read permission */
    public static final int PERMISSION_READ = 0x01;

    /** Characteristic permission: Allow encrypted read operations */
    public static final int PERMISSION_READ_ENCRYPTED = 0x02;

    /** Characteristic permission: Allow reading with person-in-the-middle protection */
    public static final int PERMISSION_READ_ENCRYPTED_MITM = 0x04;

    /** Characteristic write permission */
    public static final int PERMISSION_WRITE = 0x10;

    /** Characteristic permission: Allow encrypted writes */
    public static final int PERMISSION_WRITE_ENCRYPTED = 0x20;

    /** Characteristic permission: Allow encrypted writes with person-in-the-middle protection */
    public static final int PERMISSION_WRITE_ENCRYPTED_MITM = 0x40;

    /** Characteristic permission: Allow signed write operations */
    public static final int PERMISSION_WRITE_SIGNED = 0x80;

    /**
     * Characteristic permission: Allow signed write operations with person-in-the-middle protection
     */
    public static final int PERMISSION_WRITE_SIGNED_MITM = 0x100;

    /** @hide */
    @Retention(RetentionPolicy.SOURCE)
    @IntDef(
            prefix = "WRITE_TYPE_",
            value = {WRITE_TYPE_DEFAULT, WRITE_TYPE_NO_RESPONSE, WRITE_TYPE_SIGNED})
    public @interface WriteType {}

    /** Write characteristic, requesting acknowledgement by the remote device */
    public static final int WRITE_TYPE_DEFAULT = 0x02;

    /** Write characteristic without requiring a response by the remote device */
    public static final int WRITE_TYPE_NO_RESPONSE = 0x01;

    /** Write characteristic including authentication signature */
    public static final int WRITE_TYPE_SIGNED = 0x04;

    /** Characteristic value format type uint8 */
    public static final int FORMAT_UINT8 = 0x11;

    /** Characteristic value format type uint16 */
    public static final int FORMAT_UINT16 = 0x12;

    /** Characteristic value format type uint32 */
    public static final int FORMAT_UINT32 = 0x14;

    /** Characteristic value format type sint8 */
    public static final int FORMAT_SINT8 = 0x21;

    /** Characteristic value format type sint16 */
    public static final int FORMAT_SINT16 = 0x22;

    /** Characteristic value format type sint32 */
    public static final int FORMAT_SINT32 = 0x24;

    /** Characteristic value format type sfloat (16-bit float) */
    public static final int FORMAT_SFLOAT = 0x32;

    /** Characteristic value format type float (32-bit float) */
    public static final int FORMAT_FLOAT = 0x34;

    /**
     * The UUID of this characteristic.
     *
     * @hide
     */
    protected UUID mUuid;

    /**
     * Instance ID for this characteristic.
     *
     * @hide
     */
    @UnsupportedAppUsage protected int mInstance;

    /**
     * Characteristic properties.
     *
     * @hide
     */
    protected int mProperties;

    /**
     * Characteristic permissions.
     *
     * @hide
     */
    protected int mPermissions;

    /**
     * Key size (default = 16).
     *
     * @hide
     */
    protected int mKeySize = 16;

    /**
     * Write type for this characteristic. See WRITE_TYPE_* constants.
     *
     * @hide
     */
    protected int mWriteType;

    /**
     * Back-reference to the service this characteristic belongs to.
     *
     * @hide
     */
    @UnsupportedAppUsage protected BluetoothGattService mService;

    /**
     * The cached value of this characteristic.
     *
     * @hide
     */
    protected byte[] mValue;

    /** List of descriptors included in this characteristic. */
    protected List<BluetoothGattDescriptor> mDescriptors;

    /**
     * Create a new BluetoothGattCharacteristic.
     *
     * @param uuid The UUID for this characteristic
     * @param properties Properties of this characteristic
     * @param permissions Permissions for this characteristic
     */
    public BluetoothGattCharacteristic(UUID uuid, int properties, int permissions) {
        initCharacteristic(null, uuid, 0, properties, permissions);
    }

    /**
     * Create a new BluetoothGattCharacteristic
     *
     * @hide
     */
    /*package*/ BluetoothGattCharacteristic(
            BluetoothGattService service,
            UUID uuid,
            int instanceId,
            int properties,
            int permissions) {
        initCharacteristic(service, uuid, instanceId, properties, permissions);
    }

    /**
     * Create a new BluetoothGattCharacteristic
     *
     * @hide
     */
    public BluetoothGattCharacteristic(UUID uuid, int instanceId, int properties, int permissions) {
        initCharacteristic(null, uuid, instanceId, properties, permissions);
    }

    private void initCharacteristic(
            BluetoothGattService service,
            UUID uuid,
            int instanceId,
            int properties,
            int permissions) {
        mUuid = uuid;
        mInstance = instanceId;
        mProperties = properties;
        mPermissions = permissions;
        mService = service;
        mValue = null;
        mDescriptors = new ArrayList<BluetoothGattDescriptor>();

        if ((mProperties & PROPERTY_WRITE_NO_RESPONSE) != 0) {
            mWriteType = WRITE_TYPE_NO_RESPONSE;
        } else {
            mWriteType = WRITE_TYPE_DEFAULT;
        }
    }

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

    @Override
    public void writeToParcel(Parcel out, int flags) {
        out.writeParcelable(new ParcelUuid(mUuid), 0);
        out.writeInt(mInstance);
        out.writeInt(mProperties);
        out.writeInt(mPermissions);
        out.writeInt(mKeySize);
        out.writeInt(mWriteType);
        out.writeTypedList(mDescriptors);
    }

    public static final @NonNull Creator<BluetoothGattCharacteristic> CREATOR =
            new Creator<>() {
                public BluetoothGattCharacteristic createFromParcel(Parcel in) {
                    return new BluetoothGattCharacteristic(in);
                }

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

    private BluetoothGattCharacteristic(Parcel in) {
        mUuid = ((ParcelUuid) in.readParcelable(null)).getUuid();
        mInstance = in.readInt();
        mProperties = in.readInt();
        mPermissions = in.readInt();
        mKeySize = in.readInt();
        mWriteType = in.readInt();

        mDescriptors = new ArrayList<BluetoothGattDescriptor>();

        ArrayList<BluetoothGattDescriptor> descs =
                in.createTypedArrayList(BluetoothGattDescriptor.CREATOR);
        if (descs != null) {
            for (BluetoothGattDescriptor desc : descs) {
                desc.setCharacteristic(this);
                mDescriptors.add(desc);
            }
        }
    }

    /**
     * Returns the desired key size.
     *
     * @hide
     */
    public int getKeySize() {
        return mKeySize;
    }

    /**
     * Adds a descriptor to this characteristic.
     *
     * @param descriptor Descriptor to be added to this characteristic.
     * @return true, if the descriptor was added to the characteristic
     */
    public boolean addDescriptor(BluetoothGattDescriptor descriptor) {
        mDescriptors.add(descriptor);
        descriptor.setCharacteristic(this);
        return true;
    }

    /**
     * Get a descriptor by UUID and instance id.
     *
     * @hide
     */
    /*package*/ BluetoothGattDescriptor getDescriptor(UUID uuid, int instanceId) {
        for (BluetoothGattDescriptor descriptor : mDescriptors) {
            if (descriptor.getUuid().equals(uuid) && descriptor.getInstanceId() == instanceId) {
                return descriptor;
            }
        }
        return null;
    }

    /**
     * Returns the service this characteristic belongs to.
     *
     * @return The associated service
     */
    public BluetoothGattService getService() {
        return mService;
    }

    /**
     * Sets the service associated with this device.
     *
     * @hide
     */
    @UnsupportedAppUsage
    /*package*/ void setService(BluetoothGattService service) {
        mService = service;
    }

    /**
     * Returns the UUID of this characteristic
     *
     * @return UUID of this characteristic
     */
    public UUID getUuid() {
        return mUuid;
    }

    /**
     * Returns the instance ID for this characteristic.
     *
     * <p>If a remote device offers multiple characteristics with the same UUID, the instance ID is
     * used to distuinguish between characteristics.
     *
     * @return Instance ID of this characteristic
     */
    public int getInstanceId() {
        return mInstance;
    }

    /**
     * Force the instance ID.
     *
     * @hide
     */
    public void setInstanceId(int instanceId) {
        mInstance = instanceId;
    }

    /**
     * Returns the properties of this characteristic.
     *
     * <p>The properties contain a bit mask of property flags indicating the features of this
     * characteristic.
     *
     * @return Properties of this characteristic
     */
    public int getProperties() {
        return mProperties;
    }

    /**
     * Returns the permissions for this characteristic.
     *
     * @return Permissions of this characteristic
     */
    public int getPermissions() {
        return mPermissions;
    }

    /**
     * Gets the write type for this characteristic.
     *
     * @return Write type for this characteristic
     */
    public int getWriteType() {
        return mWriteType;
    }

    /**
     * Set the write type for this characteristic
     *
     * <p>Setting the write type of a characteristic determines how the {@link
     * BluetoothGatt#writeCharacteristic(BluetoothGattCharacteristic, byte[], int)} function write
     * this characteristic.
     *
     * @param writeType The write type to for this characteristic. Can be one of: {@link
     *     #WRITE_TYPE_DEFAULT}, {@link #WRITE_TYPE_NO_RESPONSE} or {@link #WRITE_TYPE_SIGNED}.
     */
    public void setWriteType(int writeType) {
        mWriteType = writeType;
    }

    /**
     * Set the desired key size.
     *
     * @hide
     */
    @UnsupportedAppUsage
    public void setKeySize(int keySize) {
        mKeySize = keySize;
    }

    /**
     * Returns a list of descriptors for this characteristic.
     *
     * @return Descriptors for this characteristic
     */
    public List<BluetoothGattDescriptor> getDescriptors() {
        return mDescriptors;
    }

    /**
     * Returns a descriptor with a given UUID out of the list of descriptors for this
     * characteristic.
     *
     * @return GATT descriptor object or null if no descriptor with the given UUID was found.
     */
    public BluetoothGattDescriptor getDescriptor(UUID uuid) {
        for (BluetoothGattDescriptor descriptor : mDescriptors) {
            if (descriptor.getUuid().equals(uuid)) {
                return descriptor;
            }
        }
        return null;
    }

    /**
     * Get the stored value for this characteristic.
     *
     * <p>This function returns the stored value for this characteristic as retrieved by calling
     * {@link BluetoothGatt#readCharacteristic}. The cached value of the characteristic is updated
     * as a result of a read characteristic operation or if a characteristic update notification has
     * been received.
     *
     * @return Cached value of the characteristic
     * @deprecated Use {@link BluetoothGatt#readCharacteristic(BluetoothGattCharacteristic)} instead
     */
    @Deprecated
    public byte[] getValue() {
        return mValue;
    }

    /**
     * Return the stored value of this characteristic.
     *
     * <p>The formatType parameter determines how the characteristic value is to be interpreted. For
     * example, setting formatType to {@link #FORMAT_UINT16} specifies that the first two bytes of
     * the characteristic value at the given offset are interpreted to generate the return value.
     *
     * @param formatType The format type used to interpret the characteristic value.
     * @param offset Offset at which the integer value can be found.
     * @return Cached value of the characteristic or null of offset exceeds value size.
     * @deprecated Use {@link BluetoothGatt#readCharacteristic(BluetoothGattCharacteristic)} to get
     *     the characteristic value
     */
    @Deprecated
    public Integer getIntValue(int formatType, int offset) {
        if ((offset + getTypeLen(formatType)) > mValue.length) return null;

        switch (formatType) {
            case FORMAT_UINT8:
                return unsignedByteToInt(mValue[offset]);

            case FORMAT_UINT16:
                return unsignedBytesToInt(mValue[offset], mValue[offset + 1]);

            case FORMAT_UINT32:
                return unsignedBytesToInt(
                        mValue[offset], mValue[offset + 1], mValue[offset + 2], mValue[offset + 3]);
            case FORMAT_SINT8:
                return unsignedToSigned(unsignedByteToInt(mValue[offset]), 8);

            case FORMAT_SINT16:
                return unsignedToSigned(unsignedBytesToInt(mValue[offset], mValue[offset + 1]), 16);

            case FORMAT_SINT32:
                return unsignedToSigned(
                        unsignedBytesToInt(
                                mValue[offset],
                                mValue[offset + 1],
                                mValue[offset + 2],
                                mValue[offset + 3]),
                        32);
        }

        return null;
    }

    /**
     * Return the stored value of this characteristic.
     *
     * <p>See {@link #getValue} for details.
     *
     * @param formatType The format type used to interpret the characteristic value.
     * @param offset Offset at which the float value can be found.
     * @return Cached value of the characteristic at a given offset or null if the requested offset
     *     exceeds the value size.
     * @deprecated Use {@link BluetoothGatt#readCharacteristic(BluetoothGattCharacteristic)} to get
     *     the characteristic value
     */
    @Deprecated
    public Float getFloatValue(int formatType, int offset) {
        if ((offset + getTypeLen(formatType)) > mValue.length) return null;

        switch (formatType) {
            case FORMAT_SFLOAT:
                return bytesToFloat(mValue[offset], mValue[offset + 1]);

            case FORMAT_FLOAT:
                return bytesToFloat(
                        mValue[offset], mValue[offset + 1], mValue[offset + 2], mValue[offset + 3]);
        }

        return null;
    }

    /**
     * Return the stored value of this characteristic.
     *
     * <p>See {@link #getValue} for details.
     *
     * @param offset Offset at which the string value can be found.
     * @return Cached value of the characteristic
     * @deprecated Use {@link BluetoothGatt#readCharacteristic(BluetoothGattCharacteristic)} to get
     *     the characteristic value
     */
    @Deprecated
    public String getStringValue(int offset) {
        if (mValue == null || offset > mValue.length) return null;
        byte[] strBytes = new byte[mValue.length - offset];
        for (int i = 0; i != (mValue.length - offset); ++i) strBytes[i] = mValue[offset + i];
        return new String(strBytes);
    }

    /**
     * Updates the locally stored value of this characteristic.
     *
     * <p>This function modifies the locally stored cached value of this characteristic. To send the
     * value to the remote device, call {@link BluetoothGatt#writeCharacteristic} to send the value
     * to the remote device.
     *
     * @param value New value for this characteristic
     * @return true if the locally stored value has been set, false if the requested value could not
     *     be stored locally.
     * @deprecated Pass the characteristic value directly into {@link
     *     BluetoothGatt#writeCharacteristic(BluetoothGattCharacteristic, byte[], int)}
     */
    @Deprecated
    public boolean setValue(byte[] value) {
        mValue = value;
        return true;
    }

    /**
     * Set the locally stored value of this characteristic.
     *
     * <p>See {@link #setValue(byte[])} for details.
     *
     * @param value New value for this characteristic
     * @param formatType Integer format type used to transform the value parameter
     * @param offset Offset at which the value should be placed
     * @return true if the locally stored value has been set
     * @deprecated Pass the characteristic value directly into {@link
     *     BluetoothGatt#writeCharacteristic(BluetoothGattCharacteristic, byte[], int)}
     */
    @Deprecated
    public boolean setValue(int value, int formatType, int offset) {
        int len = offset + getTypeLen(formatType);
        if (mValue == null) mValue = new byte[len];
        if (len > mValue.length) return false;

        switch (formatType) {
            case FORMAT_SINT8:
                value = intToSignedBits(value, 8);
                // Fall-through intended
            case FORMAT_UINT8:
                mValue[offset] = (byte) (value & 0xFF);
                break;

            case FORMAT_SINT16:
                value = intToSignedBits(value, 16);
                // Fall-through intended
            case FORMAT_UINT16:
                mValue[offset++] = (byte) (value & 0xFF);
                mValue[offset] = (byte) ((value >> 8) & 0xFF);
                break;

            case FORMAT_SINT32:
                value = intToSignedBits(value, 32);
                // Fall-through intended
            case FORMAT_UINT32:
                mValue[offset++] = (byte) (value & 0xFF);
                mValue[offset++] = (byte) ((value >> 8) & 0xFF);
                mValue[offset++] = (byte) ((value >> 16) & 0xFF);
                mValue[offset] = (byte) ((value >> 24) & 0xFF);
                break;

            default:
                return false;
        }
        return true;
    }

    /**
     * Set the locally stored value of this characteristic.
     *
     * <p>See {@link #setValue(byte[])} for details.
     *
     * @param mantissa Mantissa for this characteristic
     * @param exponent exponent value for this characteristic
     * @param formatType Float format type used to transform the value parameter
     * @param offset Offset at which the value should be placed
     * @return true if the locally stored value has been set
     * @deprecated Pass the characteristic value directly into {@link
     *     BluetoothGatt#writeCharacteristic(BluetoothGattCharacteristic, byte[], int)}
     */
    @Deprecated
    public boolean setValue(int mantissa, int exponent, int formatType, int offset) {
        int len = offset + getTypeLen(formatType);
        if (mValue == null) mValue = new byte[len];
        if (len > mValue.length) return false;

        switch (formatType) {
            case FORMAT_SFLOAT:
                mantissa = intToSignedBits(mantissa, 12);
                exponent = intToSignedBits(exponent, 4);
                mValue[offset++] = (byte) (mantissa & 0xFF);
                mValue[offset] = (byte) ((mantissa >> 8) & 0x0F);
                mValue[offset] += (byte) ((exponent & 0x0F) << 4);
                break;

            case FORMAT_FLOAT:
                mantissa = intToSignedBits(mantissa, 24);
                exponent = intToSignedBits(exponent, 8);
                mValue[offset++] = (byte) (mantissa & 0xFF);
                mValue[offset++] = (byte) ((mantissa >> 8) & 0xFF);
                mValue[offset++] = (byte) ((mantissa >> 16) & 0xFF);
                mValue[offset] += (byte) (exponent & 0xFF);
                break;

            default:
                return false;
        }

        return true;
    }

    /**
     * Set the locally stored value of this characteristic.
     *
     * <p>See {@link #setValue(byte[])} for details.
     *
     * @param value New value for this characteristic
     * @return true if the locally stored value has been set
     * @deprecated Pass the characteristic value directly into {@link
     *     BluetoothGatt#writeCharacteristic(BluetoothGattCharacteristic, byte[], int)}
     */
    @Deprecated
    public boolean setValue(String value) {
        mValue = value.getBytes();
        return true;
    }

    /** Returns the size of a give value type. */
    private int getTypeLen(int formatType) {
        return formatType & 0xF;
    }

    /** Convert a signed byte to an unsigned int. */
    private int unsignedByteToInt(byte b) {
        return b & 0xFF;
    }

    /** Convert signed bytes to a 16-bit unsigned int. */
    private int unsignedBytesToInt(byte b0, byte b1) {
        return (unsignedByteToInt(b0) + (unsignedByteToInt(b1) << 8));
    }

    /** Convert signed bytes to a 32-bit unsigned int. */
    private int unsignedBytesToInt(byte b0, byte b1, byte b2, byte b3) {
        return (unsignedByteToInt(b0) + (unsignedByteToInt(b1) << 8))
                + (unsignedByteToInt(b2) << 16)
                + (unsignedByteToInt(b3) << 24);
    }

    /** Convert signed bytes to a 16-bit short float value. */
    private float bytesToFloat(byte b0, byte b1) {
        int mantissa =
                unsignedToSigned(unsignedByteToInt(b0) + ((unsignedByteToInt(b1) & 0x0F) << 8), 12);
        int exponent = unsignedToSigned(unsignedByteToInt(b1) >> 4, 4);
        return (float) (mantissa * Math.pow(10, exponent));
    }

    /** Convert signed bytes to a 32-bit short float value. */
    private float bytesToFloat(byte b0, byte b1, byte b2, byte b3) {
        int mantissa =
                unsignedToSigned(
                        unsignedByteToInt(b0)
                                + (unsignedByteToInt(b1) << 8)
                                + (unsignedByteToInt(b2) << 16),
                        24);
        return (float) (mantissa * Math.pow(10, b3));
    }

    /** Convert an unsigned integer value to a two's-complement encoded signed value. */
    private int unsignedToSigned(int unsigned, int size) {
        if ((unsigned & (1 << (size - 1))) != 0) {
            unsigned = -1 * ((1 << (size - 1)) - (unsigned & ((1 << (size - 1)) - 1)));
        }
        return unsigned;
    }

    /** Convert an integer into the signed bits of a given length. */
    private int intToSignedBits(int i, int size) {
        if (i < 0) {
            i = (1 << (size - 1)) + (i & ((1 << (size - 1)) - 1));
        }
        return i;
    }
}
