/*
 * 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.bluetooth.le;

import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.SystemApi;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.os.Parcel;
import android.os.Parcelable;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 * The {@link AdvertisingSetParameters} provide a way to adjust advertising
 * preferences for each
 * Bluetooth LE advertising set. Use {@link AdvertisingSetParameters.Builder} to
 * create an
 * instance of this class.
 */
public final class AdvertisingSetParameters implements Parcelable {

    /**
     * Advertise on low frequency, around every 1000ms. This is the default and
     * preferred advertising mode as it consumes the least power.
     */
    public static final int INTERVAL_HIGH = 1600;

    /**
     * Advertise on medium frequency, around every 250ms. This is balanced
     * between advertising frequency and power consumption.
     */
    public static final int INTERVAL_MEDIUM = 400;

    /**
     * Perform high frequency, low latency advertising, around every 100ms. This
     * has the highest power consumption and should not be used for continuous
     * background advertising.
     */
    public static final int INTERVAL_LOW = 160;

    /**
     * Minimum value for advertising interval.
     */
    public static final int INTERVAL_MIN = 160;

    /**
     * Maximum value for advertising interval.
     */
    public static final int INTERVAL_MAX = 16777215;

    /**
     * Advertise using the lowest transmission (TX) power level. Low transmission
     * power can be used to restrict the visibility range of advertising packets.
     */
    public static final int TX_POWER_ULTRA_LOW = -21;

    /**
     * Advertise using low TX power level.
     */
    public static final int TX_POWER_LOW = -15;

    /**
     * Advertise using medium TX power level.
     */
    public static final int TX_POWER_MEDIUM = -7;

    /**
     * Advertise using high TX power level. This corresponds to largest visibility
     * range of the advertising packet.
     */
    public static final int TX_POWER_HIGH = 1;

    /**
     * Minimum value for TX power.
     */
    public static final int TX_POWER_MIN = -127;

    /**
     * Maximum value for TX power.
     */
    public static final int TX_POWER_MAX = 1;

    /**
     * The maximum limited advertisement duration as specified by the Bluetooth
     * SIG
     */
    private static final int LIMITED_ADVERTISING_MAX_MILLIS = 180 * 1000;

    /** @hide */
    @IntDef(prefix = "ADDRESS_TYPE_", value = {
        ADDRESS_TYPE_DEFAULT,
        ADDRESS_TYPE_PUBLIC,
        ADDRESS_TYPE_RANDOM,
        ADDRESS_TYPE_RANDOM_NON_RESOLVABLE,
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface AddressTypeStatus {}

    /**
     * Advertise own address type that corresponds privacy settings of the device.
     *
     * @hide
     */
    @SystemApi
    public static final int ADDRESS_TYPE_DEFAULT = -1;

    /**
     * Advertise own public address type.
     *
     * @hide
     */
    @SystemApi
    public static final int ADDRESS_TYPE_PUBLIC = 0;

    /**
     * Generate and adverise own resolvable private address.
     *
     * @hide
     */
    @SystemApi
    public static final int ADDRESS_TYPE_RANDOM = 1;

    /**
     * Generate and advertise on non-resolvable private address.
     *
     * @hide
     */
    @SystemApi
    public static final int ADDRESS_TYPE_RANDOM_NON_RESOLVABLE = 2;

    private final boolean mIsLegacy;
    private final boolean mIsAnonymous;
    private final boolean mIncludeTxPower;
    private final int mPrimaryPhy;
    private final int mSecondaryPhy;
    private final boolean mConnectable;
    private final boolean mDiscoverable;
    private final boolean mScannable;
    private final int mInterval;
    private final int mTxPowerLevel;
    private final int mOwnAddressType;

    private AdvertisingSetParameters(
            boolean connectable,
            boolean discoverable,
            boolean scannable,
            boolean isLegacy,
            boolean isAnonymous,
            boolean includeTxPower,
            int primaryPhy,
            int secondaryPhy,
            int interval,
            int txPowerLevel,
            @AddressTypeStatus int ownAddressType) {
        mConnectable = connectable;
        mDiscoverable = discoverable;
        mScannable = scannable;
        mIsLegacy = isLegacy;
        mIsAnonymous = isAnonymous;
        mIncludeTxPower = includeTxPower;
        mPrimaryPhy = primaryPhy;
        mSecondaryPhy = secondaryPhy;
        mInterval = interval;
        mTxPowerLevel = txPowerLevel;
        mOwnAddressType = ownAddressType;
    }

    private AdvertisingSetParameters(Parcel in) {
        mConnectable = in.readInt() != 0;
        mScannable = in.readInt() != 0;
        mIsLegacy = in.readInt() != 0;
        mIsAnonymous = in.readInt() != 0;
        mIncludeTxPower = in.readInt() != 0;
        mPrimaryPhy = in.readInt();
        mSecondaryPhy = in.readInt();
        mInterval = in.readInt();
        mTxPowerLevel = in.readInt();
        mOwnAddressType = in.readInt();
        mDiscoverable = in.readInt() != 0;
    }

    /**
     * Returns whether the advertisement will be connectable.
     */
    public boolean isConnectable() {
        return mConnectable;
    }

    /**
     * Returns whether the advertisement will be discoverable.
     */
    public boolean isDiscoverable() {
        return mDiscoverable;
    }

    /**
     * Returns whether the advertisement will be scannable.
     */
    public boolean isScannable() {
        return mScannable;
    }

    /**
     * Returns whether the legacy advertisement will be used.
     */
    public boolean isLegacy() {
        return mIsLegacy;
    }

    /**
     * Returns whether the advertisement will be anonymous.
     */
    public boolean isAnonymous() {
        return mIsAnonymous;
    }

    /**
     * Returns whether the TX Power will be included.
     */
    public boolean includeTxPower() {
        return mIncludeTxPower;
    }

    /**
     * Returns the primary advertising phy.
     */
    public int getPrimaryPhy() {
        return mPrimaryPhy;
    }

    /**
     * Returns the secondary advertising phy.
     */
    public int getSecondaryPhy() {
        return mSecondaryPhy;
    }

    /**
     * Returns the advertising interval.
     */
    public int getInterval() {
        return mInterval;
    }

    /**
     * Returns the TX power level for advertising.
     */
    public int getTxPowerLevel() {
        return mTxPowerLevel;
    }

    /**
     * @return the own address type for advertising
     *
     * @hide
     */
    @SystemApi
    public @AddressTypeStatus int getOwnAddressType() {
        return mOwnAddressType;
    }

    @Override
    public String toString() {
        return "AdvertisingSetParameters [connectable=" + mConnectable
                + ", discoverable=" + mDiscoverable
                + ", isLegacy=" + mIsLegacy
                + ", isAnonymous=" + mIsAnonymous
                + ", includeTxPower=" + mIncludeTxPower
                + ", primaryPhy=" + mPrimaryPhy
                + ", secondaryPhy=" + mSecondaryPhy
                + ", interval=" + mInterval
                + ", txPowerLevel=" + mTxPowerLevel
                + ", ownAddressType=" + mOwnAddressType + "]";
    }

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

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(mConnectable ? 1 : 0);
        dest.writeInt(mScannable ? 1 : 0);
        dest.writeInt(mIsLegacy ? 1 : 0);
        dest.writeInt(mIsAnonymous ? 1 : 0);
        dest.writeInt(mIncludeTxPower ? 1 : 0);
        dest.writeInt(mPrimaryPhy);
        dest.writeInt(mSecondaryPhy);
        dest.writeInt(mInterval);
        dest.writeInt(mTxPowerLevel);
        dest.writeInt(mOwnAddressType);
        dest.writeInt(mDiscoverable ? 1 : 0);
    }

    public static final @android.annotation.NonNull Parcelable.Creator<AdvertisingSetParameters> CREATOR =
            new Creator<AdvertisingSetParameters>() {
                @Override
                public AdvertisingSetParameters[] newArray(int size) {
                    return new AdvertisingSetParameters[size];
                }

                @Override
                public AdvertisingSetParameters createFromParcel(Parcel in) {
                    return new AdvertisingSetParameters(in);
                }
            };

    /**
     * Builder class for {@link AdvertisingSetParameters}.
     */
    public static final class Builder {
        private boolean mConnectable = false;
        private boolean mDiscoverable = true;
        private boolean mScannable = false;
        private boolean mIsLegacy = false;
        private boolean mIsAnonymous = false;
        private boolean mIncludeTxPower = false;
        private int mPrimaryPhy = BluetoothDevice.PHY_LE_1M;
        private int mSecondaryPhy = BluetoothDevice.PHY_LE_1M;
        private int mInterval = INTERVAL_LOW;
        private int mTxPowerLevel = TX_POWER_MEDIUM;
        private int mOwnAddressType = ADDRESS_TYPE_DEFAULT;

        /**
         * Set whether the advertisement type should be connectable or
         * non-connectable.
         * Legacy advertisements can be both connectable and scannable. Non-legacy
         * advertisements can be only scannable or only connectable.
         *
         * @param connectable Controls whether the advertisement type will be connectable (true) or
         * non-connectable (false).
         */
        public Builder setConnectable(boolean connectable) {
            mConnectable = connectable;
            return this;
        }

        /**
         * Set whether the advertisement type should be discoverable or non-discoverable. By
         * default, advertisements will be discoverable. Devices connecting to non-discoverable
         * advertisements cannot initiate bonding.
         *
         * @param discoverable Controls whether the advertisement type will be discoverable
         * ({@code true}) or non-discoverable ({@code false}).
         */
        public @NonNull Builder setDiscoverable(boolean discoverable) {
            mDiscoverable = discoverable;
            return this;
        }

        /**
         * Set whether the advertisement type should be scannable.
         * Legacy advertisements can be both connectable and scannable. Non-legacy
         * advertisements can be only scannable or only connectable.
         *
         * @param scannable Controls whether the advertisement type will be scannable (true) or
         * non-scannable (false).
         */
        public Builder setScannable(boolean scannable) {
            mScannable = scannable;
            return this;
        }

        /**
         * When set to true, advertising set will advertise 4.x Spec compliant
         * advertisements.
         *
         * @param isLegacy whether legacy advertising mode should be used.
         */
        public Builder setLegacyMode(boolean isLegacy) {
            mIsLegacy = isLegacy;
            return this;
        }

        /**
         * Set whether advertiser address should be ommited from all packets. If this
         * mode is used, periodic advertising can't be enabled for this set.
         *
         * This is used only if legacy mode is not used.
         *
         * @param isAnonymous whether anonymous advertising should be used.
         */
        public Builder setAnonymous(boolean isAnonymous) {
            mIsAnonymous = isAnonymous;
            return this;
        }

        /**
         * Set whether TX power should be included in the extended header.
         *
         * This is used only if legacy mode is not used.
         *
         * @param includeTxPower whether TX power should be included in extended header
         */
        public Builder setIncludeTxPower(boolean includeTxPower) {
            mIncludeTxPower = includeTxPower;
            return this;
        }

        /**
         * Set the primary physical channel used for this advertising set.
         *
         * This is used only if legacy mode is not used.
         *
         * Use {@link BluetoothAdapter#isLeCodedPhySupported} to determine if LE Coded PHY is
         * supported on this device.
         *
         * @param primaryPhy Primary advertising physical channel, can only be {@link
         * BluetoothDevice#PHY_LE_1M} or {@link BluetoothDevice#PHY_LE_CODED}.
         * @throws IllegalArgumentException If the primaryPhy is invalid.
         */
        public Builder setPrimaryPhy(int primaryPhy) {
            if (primaryPhy != BluetoothDevice.PHY_LE_1M
                    && primaryPhy != BluetoothDevice.PHY_LE_CODED) {
                throw new IllegalArgumentException("bad primaryPhy " + primaryPhy);
            }
            mPrimaryPhy = primaryPhy;
            return this;
        }

        /**
         * Set the secondary physical channel used for this advertising set.
         *
         * This is used only if legacy mode is not used.
         *
         * Use {@link BluetoothAdapter#isLeCodedPhySupported} and
         * {@link BluetoothAdapter#isLe2MPhySupported} to determine if LE Coded PHY or 2M PHY is
         * supported on this device.
         *
         * @param secondaryPhy Secondary advertising physical channel, can only be one of {@link
         * BluetoothDevice#PHY_LE_1M}, {@link BluetoothDevice#PHY_LE_2M} or {@link
         * BluetoothDevice#PHY_LE_CODED}.
         * @throws IllegalArgumentException If the secondaryPhy is invalid.
         */
        public Builder setSecondaryPhy(int secondaryPhy) {
            if (secondaryPhy != BluetoothDevice.PHY_LE_1M
                    && secondaryPhy != BluetoothDevice.PHY_LE_2M
                    && secondaryPhy != BluetoothDevice.PHY_LE_CODED) {
                throw new IllegalArgumentException("bad secondaryPhy " + secondaryPhy);
            }
            mSecondaryPhy = secondaryPhy;
            return this;
        }

        /**
         * Set advertising interval.
         *
         * @param interval Bluetooth LE Advertising interval, in 0.625ms unit. Valid range is from
         * 160 (100ms) to 16777215 (10,485.759375 s). Recommended values are: {@link
         * AdvertisingSetParameters#INTERVAL_LOW}, {@link AdvertisingSetParameters#INTERVAL_MEDIUM},
         * or {@link AdvertisingSetParameters#INTERVAL_HIGH}.
         * @throws IllegalArgumentException If the interval is invalid.
         */
        public Builder setInterval(int interval) {
            if (interval < INTERVAL_MIN || interval > INTERVAL_MAX) {
                throw new IllegalArgumentException("unknown interval " + interval);
            }
            mInterval = interval;
            return this;
        }

        /**
         * Set the transmission power level for the advertising.
         *
         * @param txPowerLevel Transmission power of Bluetooth LE Advertising, in dBm. The valid
         * range is [-127, 1] Recommended values are:
         * {@link AdvertisingSetParameters#TX_POWER_ULTRA_LOW},
         * {@link AdvertisingSetParameters#TX_POWER_LOW},
         * {@link AdvertisingSetParameters#TX_POWER_MEDIUM},
         * or {@link AdvertisingSetParameters#TX_POWER_HIGH}.
         * @throws IllegalArgumentException If the {@code txPowerLevel} is invalid.
         */
        public Builder setTxPowerLevel(int txPowerLevel) {
            if (txPowerLevel < TX_POWER_MIN || txPowerLevel > TX_POWER_MAX) {
                throw new IllegalArgumentException("unknown txPowerLevel " + txPowerLevel);
            }
            mTxPowerLevel = txPowerLevel;
            return this;
        }

        /**
         * Set own address type for advertising to control public or privacy mode. If used to set
         * address type anything other than {@link AdvertisingSetParameters#ADDRESS_TYPE_DEFAULT},
         * then it will require BLUETOOTH_PRIVILEGED permission and will be checked at the
         * time of starting advertising.
         *
         * @throws IllegalArgumentException If the {@code ownAddressType} is invalid
         *
         * @hide
         */
        @SystemApi
        public @NonNull Builder setOwnAddressType(@AddressTypeStatus int ownAddressType) {
            if (ownAddressType < AdvertisingSetParameters.ADDRESS_TYPE_DEFAULT
                    || ownAddressType
                            > AdvertisingSetParameters.ADDRESS_TYPE_RANDOM_NON_RESOLVABLE) {
                throw new IllegalArgumentException("unknown address type " + ownAddressType);
            }
            mOwnAddressType = ownAddressType;
            return this;
        }

        /**
         * Build the {@link AdvertisingSetParameters} object.
         *
         * @throws IllegalStateException if invalid combination of parameters is used.
         */
        public AdvertisingSetParameters build() {
            if (mIsLegacy) {
                if (mIsAnonymous) {
                    throw new IllegalArgumentException("Legacy advertising can't be anonymous");
                }

                if (mConnectable && !mScannable) {
                    throw new IllegalStateException(
                            "Legacy advertisement can't be connectable and non-scannable");
                }

                if (mIncludeTxPower) {
                    throw new IllegalStateException(
                            "Legacy advertising can't include TX power level in header");
                }
            } else {
                if (mConnectable && mScannable) {
                    throw new IllegalStateException(
                            "Advertising can't be both connectable and scannable");
                }

                if (mIsAnonymous && mConnectable) {
                    throw new IllegalStateException(
                            "Advertising can't be both connectable and anonymous");
                }
            }

            return new AdvertisingSetParameters(
                    mConnectable,
                    mDiscoverable,
                    mScannable,
                    mIsLegacy,
                    mIsAnonymous,
                    mIncludeTxPower,
                    mPrimaryPhy,
                    mSecondaryPhy,
                    mInterval,
                    mTxPowerLevel,
                    mOwnAddressType);
        }
    }
}
