/*
 * 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.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.os.Parcel;
import android.os.Parcelable;

/**
 * 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;

    private final boolean isLegacy;
    private final boolean isAnonymous;
    private final boolean includeTxPower;
    private final int primaryPhy;
    private final int secondaryPhy;
    private final boolean connectable;
    private final boolean scannable;
    private final int interval;
    private final int txPowerLevel;

    private AdvertisingSetParameters(boolean connectable, boolean scannable, boolean isLegacy,
                                     boolean isAnonymous, boolean includeTxPower,
                                     int primaryPhy, int secondaryPhy,
                                     int interval, int txPowerLevel) {
        this.connectable = connectable;
        this.scannable = scannable;
        this.isLegacy = isLegacy;
        this.isAnonymous = isAnonymous;
        this.includeTxPower = includeTxPower;
        this.primaryPhy = primaryPhy;
        this.secondaryPhy = secondaryPhy;
        this.interval = interval;
        this.txPowerLevel = txPowerLevel;
    }

    private AdvertisingSetParameters(Parcel in) {
        connectable = in.readInt() != 0 ? true : false;
        scannable = in.readInt() != 0 ? true : false;
        isLegacy = in.readInt() != 0 ? true : false;
        isAnonymous = in.readInt() != 0 ? true : false;
        includeTxPower = in.readInt() != 0 ? true : false;
        primaryPhy = in.readInt();
        secondaryPhy = in.readInt();
        interval = in.readInt();
        txPowerLevel = in.readInt();
    }

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

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

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

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

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

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

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

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

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

    @Override
    public String toString() {
        return "AdvertisingSetParameters [connectable=" + connectable
             + ", isLegacy=" + isLegacy
             + ", isAnonymous=" + isAnonymous
             + ", includeTxPower=" + includeTxPower
             + ", primaryPhy=" + primaryPhy
             + ", secondaryPhy=" + secondaryPhy
             + ", interval=" + interval
             + ", txPowerLevel=" + txPowerLevel + "]";
    }

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

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(connectable ? 1 : 0);
        dest.writeInt(scannable ? 1 : 0);
        dest.writeInt(isLegacy ? 1 : 0);
        dest.writeInt(isAnonymous ? 1 : 0);
        dest.writeInt(includeTxPower ? 1 : 0);
        dest.writeInt(primaryPhy);
        dest.writeInt(secondaryPhy);
        dest.writeInt(interval);
        dest.writeInt(txPowerLevel);
    }

    public static final 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 connectable = false;
        private boolean scannable = false;
        private boolean isLegacy = false;
        private boolean isAnonymous = false;
        private boolean includeTxPower = false;
        private int primaryPhy = BluetoothDevice.PHY_LE_1M;
        private int secondaryPhy = BluetoothDevice.PHY_LE_1M;
        private int interval = INTERVAL_LOW;
        private int txPowerLevel = TX_POWER_MEDIUM;

        /**
         * 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) {
            this.connectable = connectable;
            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) {
            this.scannable = 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) {
            this.isLegacy = 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) {
            this.isAnonymous = 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) {
            this.includeTxPower = 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);
            }
            this.primaryPhy = 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);
            }
            this.secondaryPhy = 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);
            }
            this.interval = 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);
            }
            this.txPowerLevel = txPowerLevel;
            return this;
        }

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

                if (connectable == true && scannable == false) {
                    throw new IllegalStateException(
                        "Legacy advertisement can't be connectable and non-scannable");
                }

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

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

            return new AdvertisingSetParameters(connectable, scannable, isLegacy, isAnonymous,
                                                includeTxPower, primaryPhy,
                                                secondaryPhy, interval, txPowerLevel);
        }
    }
}