/*
 * Copyright (C) 2014 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.Hide;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.RequiresNoPermission;
import android.annotation.SystemApi;
import android.app.compat.CompatChanges;
import android.bluetooth.BluetoothDevice;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledSince;
import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;

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

/**
 * Bluetooth LE scan settings are passed to {@link BluetoothLeScanner#startScan} to define the
 * parameters for the scan.
 */
public final class ScanSettings implements Parcelable {

    /**
     * A special Bluetooth LE scan mode. Applications using this scan mode will passively listen for
     * other scan results without starting BLE scans themselves.
     */
    public static final int SCAN_MODE_OPPORTUNISTIC = -1;

    /**
     * Perform Bluetooth LE scan in low power mode. This is the default scan mode as it consumes the
     * least power. This mode is enforced if the scanning application is not in foreground.
     */
    public static final int SCAN_MODE_LOW_POWER = 0;

    /**
     * Perform Bluetooth LE scan in balanced power mode. Scan results are returned at a rate that
     * provides a good trade-off between scan frequency and power consumption.
     */
    public static final int SCAN_MODE_BALANCED = 1;

    /**
     * Scan using highest duty cycle. It's recommended to only use this mode when the application is
     * running in the foreground.
     */
    public static final int SCAN_MODE_LOW_LATENCY = 2;

    /**
     * Perform Bluetooth LE scan in ambient discovery mode. This mode has lower duty cycle and more
     * aggressive scan interval than balanced mode that provides a good trade-off between scan
     * latency and power consumption.
     */
    @Hide @SystemApi public static final int SCAN_MODE_AMBIENT_DISCOVERY = 3;

    /**
     * Default Bluetooth LE scan mode when the screen is off. This mode has the low duty cycle and
     * long scan interval which results in the lowest power consumption among all modes. It is for
     * the framework internal use only.
     */
    @Hide public static final int SCAN_MODE_SCREEN_OFF = 4;

    /**
     * Balanced Bluetooth LE scan mode for foreground service when the screen is off. It is for the
     * framework internal use only.
     */
    @Hide public static final int SCAN_MODE_SCREEN_OFF_BALANCED = 5;

    /**
     * Trigger a callback for every Bluetooth advertisement found that matches the filter criteria.
     * If no filter is active, all advertisement packets are reported.
     */
    public static final int CALLBACK_TYPE_ALL_MATCHES = 1;

    /**
     * A result callback is only triggered for the first advertisement packet received that matches
     * the filter criteria.
     */
    public static final int CALLBACK_TYPE_FIRST_MATCH = 2;

    /**
     * Receive a callback when advertisements are no longer received from a device that has been
     * previously reported by a first match callback.
     */
    public static final int CALLBACK_TYPE_MATCH_LOST = 4;

    /**
     * A result callback for every Bluetooth advertisement found that matches the filter criteria is
     * only triggered when screen is turned on. While the screen is turned off, the advertisements
     * are batched and the batched result callbacks are triggered every report delay. When the batch
     * scan with this callback type is activated, the batched result callbacks are also triggered
     * while turning on screen or disabling the scan. This callback type must be used with a report
     * delay of {@link ScanSettings#AUTO_BATCH_MIN_REPORT_DELAY_MILLIS} or greater.
     */
    public static final int CALLBACK_TYPE_ALL_MATCHES_AUTO_BATCH = 8;

    /** Minimum report delay for {@link ScanSettings#CALLBACK_TYPE_ALL_MATCHES_AUTO_BATCH}. */
    public static final long AUTO_BATCH_MIN_REPORT_DELAY_MILLIS = 1000 * 60 * 10;

    /**
     * Determines how many advertisements to match per filter, as this is scarce hw resource. Match
     * one advertisement per filter.
     */
    public static final int MATCH_NUM_ONE_ADVERTISEMENT = 1;

    /**
     * Match few advertisement per filter, depends on current capability and availability of the
     * resources in hw.
     */
    public static final int MATCH_NUM_FEW_ADVERTISEMENT = 2;

    /**
     * Match as many advertisement per filter as hw could allow, depends on current capability and
     * availability of the resources in hw.
     */
    public static final int MATCH_NUM_MAX_ADVERTISEMENT = 3;

    /**
     * In Aggressive mode, hw will determine a match sooner even with feeble signal strength and few
     * number of sightings/match in a duration.
     */
    public static final int MATCH_MODE_AGGRESSIVE = 1;

    /**
     * For sticky mode, higher threshold of signal strength and sightings is required before
     * reporting by hw.
     */
    public static final int MATCH_MODE_STICKY = 2;

    /**
     * Request full scan results which contain the device, rssi, advertising data, scan response as
     * well as the scan timestamp.
     */
    @Hide @SystemApi public static final int SCAN_RESULT_TYPE_FULL = 0;

    /**
     * Request abbreviated scan results which contain the device, rssi and scan timestamp.
     *
     * <p><b>Note:</b> It is possible for an application to get more scan results than it asked for,
     * if there are multiple apps using this type.
     */
    @Hide @SystemApi public static final int SCAN_RESULT_TYPE_ABBREVIATED = 1;

    /**
     * Use all supported PHYs for scanning. This will check the controller capabilities, and start
     * the scan on 1Mbit and LE Coded PHYs if supported, or on the 1Mbit PHY only.
     */
    public static final int PHY_LE_ALL_SUPPORTED = 255;

    /** Scan type is unknown. */
    public static final int SCAN_TYPE_UNKNOWN = 0;

    /** Does passive scanning, scan responses are ignored. */
    public static final int SCAN_TYPE_PASSIVE = 1;

    /** Does active scanning, scan results are delivered upon scan responses arrive. */
    public static final int SCAN_TYPE_ACTIVE = 2;

    @Hide
    @IntDef(
            prefix = "SCAN_TYPE_",
            value = {
                SCAN_TYPE_UNKNOWN,
                SCAN_TYPE_PASSIVE,
                SCAN_TYPE_ACTIVE,
            })
    @Retention(RetentionPolicy.SOURCE)
    public @interface ScanType {}

    /**
     * Starting with Android B (Baklava), the default number of trackable advertisements for onFound
     * /onLost scanning is 2 instead of (max hardware allows / 2).
     */
    @ChangeId
    @EnabledSince(targetSdkVersion = Build.VERSION_CODES.BAKLAVA)
    static final long CHANGE_DEFAULT_TRACKABLE_ADV_NUMBER = 386727721L;

    // Bluetooth LE scan mode.
    private final int mScanMode;

    // Bluetooth LE scan callback type.
    private final int mCallbackType;

    // Bluetooth LE scan result type.
    private final int mScanResultType;

    // Time of delay for reporting the scan result.
    private final long mReportDelayMillis;

    private final int mMatchMode;

    private final int mNumOfMatchesPerFilter;

    // Include only legacy advertising results.
    private final boolean mLegacy;

    private final int mPhy;

    private final int mRssiThreshold;

    private final int mScanType;

    @RequiresNoPermission
    public int getScanMode() {
        return mScanMode;
    }

    @RequiresNoPermission
    public int getCallbackType() {
        return mCallbackType;
    }

    @RequiresNoPermission
    public int getScanResultType() {
        return mScanResultType;
    }

    /** Returns report delay timestamp based on the device clock. */
    @RequiresNoPermission
    public long getReportDelayMillis() {
        return mReportDelayMillis;
    }

    @Hide
    @RequiresNoPermission
    public int getMatchMode() {
        return mMatchMode;
    }

    @Hide
    @RequiresNoPermission
    public int getNumOfMatches() {
        return mNumOfMatchesPerFilter;
    }

    /**
     * Returns whether only legacy advertisements will be returned. Legacy advertisements include
     * advertisements as specified by the Bluetooth core specification 4.2 and below.
     */
    @RequiresNoPermission
    public boolean getLegacy() {
        return mLegacy;
    }

    /** Returns the physical layer used during a scan. */
    @RequiresNoPermission
    public int getPhy() {
        return mPhy;
    }

    @RequiresNoPermission
    public int getRssiThreshold() {
        return mRssiThreshold;
    }

    @RequiresNoPermission
    public @ScanType int getScanType() {
        return mScanType;
    }

    private ScanSettings(
            int scanMode,
            int callbackType,
            int scanResultType,
            long reportDelayMillis,
            int matchMode,
            int numOfMatchesPerFilter,
            boolean legacy,
            int phy,
            int rssiThreshold,
            int scanType) {
        mScanMode = scanMode;
        mCallbackType = callbackType;
        mScanResultType = scanResultType;
        mReportDelayMillis = reportDelayMillis;
        mNumOfMatchesPerFilter = numOfMatchesPerFilter;
        mMatchMode = matchMode;
        mLegacy = legacy;
        mPhy = phy;
        mRssiThreshold = rssiThreshold;
        mScanType = scanType;
    }

    private ScanSettings(Parcel in) {
        mScanMode = in.readInt();
        mCallbackType = in.readInt();
        mScanResultType = in.readInt();
        mReportDelayMillis = in.readLong();
        mMatchMode = in.readInt();
        mNumOfMatchesPerFilter = in.readInt();
        mLegacy = in.readInt() != 0;
        mPhy = in.readInt();
        mRssiThreshold = in.readInt();
        mScanType = in.readInt();
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(mScanMode);
        dest.writeInt(mCallbackType);
        dest.writeInt(mScanResultType);
        dest.writeLong(mReportDelayMillis);
        dest.writeInt(mMatchMode);
        dest.writeInt(mNumOfMatchesPerFilter);
        dest.writeInt(mLegacy ? 1 : 0);
        dest.writeInt(mPhy);
        dest.writeInt(mRssiThreshold);
        dest.writeInt(mScanType);
    }

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

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

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

    /** Builder for {@link ScanSettings}. */
    public static final class Builder {
        private int mScanMode = SCAN_MODE_LOW_POWER;
        private int mCallbackType = CALLBACK_TYPE_ALL_MATCHES;
        private int mScanResultType = SCAN_RESULT_TYPE_FULL;
        private long mReportDelayMillis = 0;
        private int mMatchMode = MATCH_MODE_AGGRESSIVE;
        private int mNumOfMatchesPerFilter = MATCH_NUM_MAX_ADVERTISEMENT;
        private boolean mLegacy = true;
        private int mPhy = BluetoothDevice.PHY_LE_1M;
        private int mRssiThreshold = Byte.MIN_VALUE;
        private int mScanType = SCAN_TYPE_ACTIVE;

        // Instance initializer for mNumOfMatchesPerFilter
        {
            if (CompatChanges.isChangeEnabled(CHANGE_DEFAULT_TRACKABLE_ADV_NUMBER)) {
                mNumOfMatchesPerFilter = MATCH_NUM_FEW_ADVERTISEMENT;
            }
        }

        /**
         * Set scan mode for Bluetooth LE scan.
         *
         * @param scanMode The scan mode can be one of {@link ScanSettings#SCAN_MODE_LOW_POWER},
         *     {@link ScanSettings#SCAN_MODE_BALANCED} or {@link
         *     ScanSettings#SCAN_MODE_LOW_LATENCY}.
         * @throws IllegalArgumentException If the {@code scanMode} is invalid.
         */
        @RequiresNoPermission
        public Builder setScanMode(int scanMode) {
            if (!List.of(
                            SCAN_MODE_OPPORTUNISTIC,
                            SCAN_MODE_LOW_POWER,
                            SCAN_MODE_BALANCED,
                            SCAN_MODE_LOW_LATENCY,
                            SCAN_MODE_AMBIENT_DISCOVERY,
                            SCAN_MODE_SCREEN_OFF,
                            SCAN_MODE_SCREEN_OFF_BALANCED)
                    .contains(scanMode)) {
                throw new IllegalArgumentException("invalid scan mode " + scanMode);
            }

            mScanMode = scanMode;
            return this;
        }

        /**
         * Set callback type for Bluetooth LE scan.
         *
         * @param callbackType The callback type flags for the scan.
         * @throws IllegalArgumentException If the {@code callbackType} is invalid.
         */
        @RequiresNoPermission
        public Builder setCallbackType(int callbackType) {
            if (!isValidCallbackType(callbackType)) {
                throw new IllegalArgumentException("invalid callback type - " + callbackType);
            }
            mCallbackType = callbackType;
            return this;
        }

        // Returns true if the callbackType is valid.
        private static boolean isValidCallbackType(int callbackType) {
            if (callbackType == CALLBACK_TYPE_ALL_MATCHES
                    || callbackType == CALLBACK_TYPE_ALL_MATCHES_AUTO_BATCH
                    || callbackType == CALLBACK_TYPE_FIRST_MATCH
                    || callbackType == CALLBACK_TYPE_MATCH_LOST) {
                return true;
            }
            return callbackType == (CALLBACK_TYPE_FIRST_MATCH | CALLBACK_TYPE_MATCH_LOST);
        }

        /**
         * Set scan result type for Bluetooth LE scan.
         *
         * @param scanResultType Type for scan result, could be either {@link
         *     ScanSettings#SCAN_RESULT_TYPE_FULL} or {@link
         *     ScanSettings#SCAN_RESULT_TYPE_ABBREVIATED}.
         * @throws IllegalArgumentException If the {@code scanResultType} is invalid.
         */
        @Hide
        @SystemApi
        @RequiresNoPermission
        public Builder setScanResultType(int scanResultType) {
            if (scanResultType < SCAN_RESULT_TYPE_FULL
                    || scanResultType > SCAN_RESULT_TYPE_ABBREVIATED) {
                throw new IllegalArgumentException("invalid scanResultType - " + scanResultType);
            }
            mScanResultType = scanResultType;
            return this;
        }

        /**
         * Set report delay timestamp for Bluetooth LE scan. If set to 0, you will be notified of
         * scan results immediately. If &gt; 0, scan results are queued up and delivered after the
         * requested delay or 5000 milliseconds (whichever is higher). Note scan results may be
         * delivered sooner if the internal buffers fill up.
         *
         * @param reportDelayMillis how frequently scan results should be delivered in milliseconds
         * @throws IllegalArgumentException if {@code reportDelayMillis} &lt; 0
         */
        @RequiresNoPermission
        public Builder setReportDelay(long reportDelayMillis) {
            if (reportDelayMillis < 0) {
                throw new IllegalArgumentException("reportDelay must be > 0");
            }
            mReportDelayMillis = reportDelayMillis;
            return this;
        }

        /**
         * Set the number of matches for Bluetooth LE scan filters hardware match.
         *
         * @param numOfMatches The num of matches can be one of {@link
         *     ScanSettings#MATCH_NUM_ONE_ADVERTISEMENT} or {@link
         *     ScanSettings#MATCH_NUM_FEW_ADVERTISEMENT} or {@link
         *     ScanSettings#MATCH_NUM_MAX_ADVERTISEMENT}
         * @throws IllegalArgumentException If the {@code matchMode} is invalid.
         */
        @RequiresNoPermission
        public Builder setNumOfMatches(int numOfMatches) {
            if (numOfMatches < MATCH_NUM_ONE_ADVERTISEMENT
                    || numOfMatches > MATCH_NUM_MAX_ADVERTISEMENT) {
                throw new IllegalArgumentException("invalid numOfMatches " + numOfMatches);
            }
            mNumOfMatchesPerFilter = numOfMatches;
            return this;
        }

        /**
         * Set match mode for Bluetooth LE scan filters hardware match.
         *
         * @param matchMode The match mode can be one of {@link ScanSettings#MATCH_MODE_AGGRESSIVE}
         *     or {@link ScanSettings#MATCH_MODE_STICKY}
         * @throws IllegalArgumentException If the {@code matchMode} is invalid.
         */
        @RequiresNoPermission
        public Builder setMatchMode(int matchMode) {
            if (matchMode < MATCH_MODE_AGGRESSIVE || matchMode > MATCH_MODE_STICKY) {
                throw new IllegalArgumentException("invalid matchMode " + matchMode);
            }
            mMatchMode = matchMode;
            return this;
        }

        /**
         * Set whether only legacy advertisements should be returned in scan results. Legacy
         * advertisements include advertisements as specified by the Bluetooth core specification
         * 4.2 and below. This is true by default for compatibility with older apps.
         *
         * @param legacy true if only legacy advertisements will be returned
         */
        @RequiresNoPermission
        public Builder setLegacy(boolean legacy) {
            mLegacy = legacy;
            return this;
        }

        /**
         * Set the Physical Layer to use during this scan. This is used only if {@link
         * ScanSettings.Builder#setLegacy} is set to false. {@link
         * android.bluetooth.BluetoothAdapter#isLeCodedPhySupported} may be used to check whether LE
         * Coded phy is supported by calling {@link
         * android.bluetooth.BluetoothAdapter#isLeCodedPhySupported}. Selecting an unsupported phy
         * will result in failure to start scan.
         *
         * @param phy Can be one of {@link BluetoothDevice#PHY_LE_1M}, {@link
         *     BluetoothDevice#PHY_LE_CODED} or {@link ScanSettings#PHY_LE_ALL_SUPPORTED}
         */
        @RequiresNoPermission
        public Builder setPhy(int phy) {
            mPhy = phy;
            return this;
        }

        /**
         * Sets the RSSI threshold. When filtering by RSSI threshold, an advertisement will pass the
         * filter only if its RSSI value is greater than or equal to the specified threshold.
         *
         * @param rssiThreshold the high threshold of RSSI value. The valid range is [-127, 126].
         * @return this builder
         */
        @RequiresNoPermission
        public @NonNull Builder setRssiThreshold(int rssiThreshold) {
            mRssiThreshold = rssiThreshold;
            return this;
        }

        /**
         * Sets the scan type. Either {@link #SCAN_TYPE_ACTIVE} or {@link #SCAN_TYPE_PASSIVE} can be
         * set. The default value is {@link #SCAN_TYPE_ACTIVE}.
         *
         * @param scanType Either {@link #SCAN_TYPE_ACTIVE} or {@link #SCAN_TYPE_PASSIVE}. If scan
         *     type is {@link #SCAN_TYPE_PASSIVE}, scan results are delivered immediately upon
         *     receiving an advertising report, without waiting for scan responses.
         * @throws IllegalArgumentException if invalid scan type is given.
         */
        @RequiresNoPermission
        public @NonNull Builder setScanType(@ScanType int scanType) {
            if (scanType != SCAN_TYPE_PASSIVE && scanType != SCAN_TYPE_ACTIVE) {
                throw new IllegalArgumentException("invalid scan type");
            }
            mScanType = scanType;
            return this;
        }

        /**
         * Build {@link ScanSettings}.
         *
         * @throws IllegalArgumentException if the settings cannot be built.
         */
        @RequiresNoPermission
        public ScanSettings build() {
            if (mCallbackType == CALLBACK_TYPE_ALL_MATCHES_AUTO_BATCH
                    && mReportDelayMillis < AUTO_BATCH_MIN_REPORT_DELAY_MILLIS) {
                throw new IllegalArgumentException(
                        "report delay for auto batch must be >= "
                                + AUTO_BATCH_MIN_REPORT_DELAY_MILLIS);
            }
            return new ScanSettings(
                    mScanMode,
                    mCallbackType,
                    mScanResultType,
                    mReportDelayMillis,
                    mMatchMode,
                    mNumOfMatchesPerFilter,
                    mLegacy,
                    mPhy,
                    mRssiThreshold,
                    mScanType);
        }
    }
}
