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

import static android.companion.BluetoothDeviceFilterUtils.getDeviceDisplayNameInternal;
import static android.companion.BluetoothDeviceFilterUtils.patternFromString;
import static android.companion.BluetoothDeviceFilterUtils.patternToString;

import static com.android.internal.util.Preconditions.checkArgument;
import static com.android.internal.util.Preconditions.checkState;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UnsupportedAppUsage;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.le.ScanFilter;
import android.bluetooth.le.ScanRecord;
import android.bluetooth.le.ScanResult;
import android.os.Parcel;
import android.provider.OneTimeUseBuilder;
import android.text.TextUtils;
import android.util.Log;

import com.android.internal.util.BitUtils;
import com.android.internal.util.ObjectUtils;
import com.android.internal.util.Preconditions;

import java.nio.ByteOrder;
import java.util.Arrays;
import java.util.Objects;
import java.util.regex.Pattern;

/**
 * A filter for Bluetooth LE devices
 *
 * @see ScanFilter
 */
public final class BluetoothLeDeviceFilter implements DeviceFilter<ScanResult> {

    private static final boolean DEBUG = false;
    private static final String LOG_TAG = "BluetoothLeDeviceFilter";

    private static final int RENAME_PREFIX_LENGTH_LIMIT = 10;

    private final Pattern mNamePattern;
    private final ScanFilter mScanFilter;
    private final byte[] mRawDataFilter;
    private final byte[] mRawDataFilterMask;
    private final String mRenamePrefix;
    private final String mRenameSuffix;
    private final int mRenameBytesFrom;
    private final int mRenameBytesLength;
    private final int mRenameNameFrom;
    private final int mRenameNameLength;
    private final boolean mRenameBytesReverseOrder;

    private BluetoothLeDeviceFilter(Pattern namePattern, ScanFilter scanFilter,
            byte[] rawDataFilter, byte[] rawDataFilterMask, String renamePrefix,
            String renameSuffix, int renameBytesFrom, int renameBytesLength,
            int renameNameFrom, int renameNameLength, boolean renameBytesReverseOrder) {
        mNamePattern = namePattern;
        mScanFilter = ObjectUtils.firstNotNull(scanFilter, ScanFilter.EMPTY);
        mRawDataFilter = rawDataFilter;
        mRawDataFilterMask = rawDataFilterMask;
        mRenamePrefix = renamePrefix;
        mRenameSuffix = renameSuffix;
        mRenameBytesFrom = renameBytesFrom;
        mRenameBytesLength = renameBytesLength;
        mRenameNameFrom = renameNameFrom;
        mRenameNameLength = renameNameLength;
        mRenameBytesReverseOrder = renameBytesReverseOrder;
    }

    /** @hide */
    @Nullable
    public Pattern getNamePattern() {
        return mNamePattern;
    }

    /** @hide */
    @NonNull
    @UnsupportedAppUsage
    public ScanFilter getScanFilter() {
        return mScanFilter;
    }

    /** @hide */
    @Nullable
    public byte[] getRawDataFilter() {
        return mRawDataFilter;
    }

    /** @hide */
    @Nullable
    public byte[] getRawDataFilterMask() {
        return mRawDataFilterMask;
    }

    /** @hide */
    @Nullable
    public String getRenamePrefix() {
        return mRenamePrefix;
    }

    /** @hide */
    @Nullable
    public String getRenameSuffix() {
        return mRenameSuffix;
    }

    /** @hide */
    public int getRenameBytesFrom() {
        return mRenameBytesFrom;
    }

    /** @hide */
    public int getRenameBytesLength() {
        return mRenameBytesLength;
    }

    /** @hide */
    public boolean isRenameBytesReverseOrder() {
        return mRenameBytesReverseOrder;
    }

    /** @hide */
    @Override
    @Nullable
    public String getDeviceDisplayName(ScanResult sr) {
        if (mRenameBytesFrom < 0 && mRenameNameFrom < 0) {
            return getDeviceDisplayNameInternal(sr.getDevice());
        }
        final StringBuilder sb = new StringBuilder(TextUtils.emptyIfNull(mRenamePrefix));
        if (mRenameBytesFrom >= 0) {
            final byte[] bytes = sr.getScanRecord().getBytes();
            int startInclusive = mRenameBytesFrom;
            int endInclusive = mRenameBytesFrom + mRenameBytesLength -1;
            int initial = mRenameBytesReverseOrder ? endInclusive : startInclusive;
            int step = mRenameBytesReverseOrder ? -1 : 1;
            for (int i = initial; startInclusive <= i && i <= endInclusive; i += step) {
                sb.append(Byte.toHexString(bytes[i], true));
            }
        } else {
            sb.append(
                    getDeviceDisplayNameInternal(sr.getDevice())
                            .substring(mRenameNameFrom, mRenameNameFrom + mRenameNameLength));
        }
        return sb.append(TextUtils.emptyIfNull(mRenameSuffix)).toString();
    }

    /** @hide */
    @Override
    public boolean matches(ScanResult device) {
        boolean result = matches(device.getDevice())
                && (mRawDataFilter == null
                    || BitUtils.maskedEquals(device.getScanRecord().getBytes(),
                            mRawDataFilter, mRawDataFilterMask));
        if (DEBUG) Log.i(LOG_TAG, "matches(this = " + this + ", device = " + device +
                ") -> " + result);
        return result;
    }

    private boolean matches(BluetoothDevice device) {
        return BluetoothDeviceFilterUtils.matches(getScanFilter(), device)
                && BluetoothDeviceFilterUtils.matchesName(getNamePattern(), device);
    }

    /** @hide */
    @Override
    public int getMediumType() {
        return DeviceFilter.MEDIUM_TYPE_BLUETOOTH_LE;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        BluetoothLeDeviceFilter that = (BluetoothLeDeviceFilter) o;
        return mRenameBytesFrom == that.mRenameBytesFrom &&
                mRenameBytesLength == that.mRenameBytesLength &&
                mRenameNameFrom == that.mRenameNameFrom &&
                mRenameNameLength == that.mRenameNameLength &&
                mRenameBytesReverseOrder == that.mRenameBytesReverseOrder &&
                Objects.equals(mNamePattern, that.mNamePattern) &&
                Objects.equals(mScanFilter, that.mScanFilter) &&
                Arrays.equals(mRawDataFilter, that.mRawDataFilter) &&
                Arrays.equals(mRawDataFilterMask, that.mRawDataFilterMask) &&
                Objects.equals(mRenamePrefix, that.mRenamePrefix) &&
                Objects.equals(mRenameSuffix, that.mRenameSuffix);
    }

    @Override
    public int hashCode() {
        return Objects.hash(mNamePattern, mScanFilter, mRawDataFilter, mRawDataFilterMask,
                mRenamePrefix, mRenameSuffix, mRenameBytesFrom, mRenameBytesLength,
                mRenameNameFrom, mRenameNameLength, mRenameBytesReverseOrder);
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(patternToString(getNamePattern()));
        dest.writeParcelable(mScanFilter, flags);
        dest.writeByteArray(mRawDataFilter);
        dest.writeByteArray(mRawDataFilterMask);
        dest.writeString(mRenamePrefix);
        dest.writeString(mRenameSuffix);
        dest.writeInt(mRenameBytesFrom);
        dest.writeInt(mRenameBytesLength);
        dest.writeInt(mRenameNameFrom);
        dest.writeInt(mRenameNameLength);
        dest.writeBoolean(mRenameBytesReverseOrder);
    }

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

    @Override
    public String toString() {
        return "BluetoothLEDeviceFilter{" +
                "mNamePattern=" + mNamePattern +
                ", mScanFilter=" + mScanFilter +
                ", mRawDataFilter=" + Arrays.toString(mRawDataFilter) +
                ", mRawDataFilterMask=" + Arrays.toString(mRawDataFilterMask) +
                ", mRenamePrefix='" + mRenamePrefix + '\'' +
                ", mRenameSuffix='" + mRenameSuffix + '\'' +
                ", mRenameBytesFrom=" + mRenameBytesFrom +
                ", mRenameBytesLength=" + mRenameBytesLength +
                ", mRenameNameFrom=" + mRenameNameFrom +
                ", mRenameNameLength=" + mRenameNameLength +
                ", mRenameBytesReverseOrder=" + mRenameBytesReverseOrder +
                '}';
    }

    public static final Creator<BluetoothLeDeviceFilter> CREATOR
            = new Creator<BluetoothLeDeviceFilter>() {
        @Override
        public BluetoothLeDeviceFilter createFromParcel(Parcel in) {
            Builder builder = new Builder()
                    .setNamePattern(patternFromString(in.readString()))
                    .setScanFilter(in.readParcelable(null));
            byte[] rawDataFilter = in.createByteArray();
            byte[] rawDataFilterMask = in.createByteArray();
            if (rawDataFilter != null) {
                builder.setRawDataFilter(rawDataFilter, rawDataFilterMask);
            }
            String renamePrefix = in.readString();
            String suffix = in.readString();
            int bytesFrom = in.readInt();
            int bytesTo = in.readInt();
            int nameFrom = in.readInt();
            int nameTo = in.readInt();
            boolean bytesReverseOrder = in.readBoolean();
            if (renamePrefix != null) {
                if (bytesFrom >= 0) {
                    builder.setRenameFromBytes(renamePrefix, suffix, bytesFrom, bytesTo,
                            bytesReverseOrder ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN);
                } else {
                    builder.setRenameFromName(renamePrefix, suffix, nameFrom, nameTo);
                }
            }
            return builder.build();
        }

        @Override
        public BluetoothLeDeviceFilter[] newArray(int size) {
            return new BluetoothLeDeviceFilter[size];
        }
    };

    public static int getRenamePrefixLengthLimit() {
        return RENAME_PREFIX_LENGTH_LIMIT;
    }

    /**
     * Builder for {@link BluetoothLeDeviceFilter}
     */
    public static final class Builder extends OneTimeUseBuilder<BluetoothLeDeviceFilter> {
        private ScanFilter mScanFilter;
        private Pattern mNamePattern;
        private byte[] mRawDataFilter;
        private byte[] mRawDataFilterMask;
        private String mRenamePrefix;
        private String mRenameSuffix;
        private int mRenameBytesFrom = -1;
        private int mRenameBytesLength;
        private int mRenameNameFrom = -1;
        private int mRenameNameLength;
        private boolean mRenameBytesReverseOrder = false;

        /**
         * @param regex if set, only devices with {@link BluetoothDevice#getName name} matching the
         *              given regular expression will be shown
         * @return self for chaining
         */
        public Builder setNamePattern(@Nullable Pattern regex) {
            checkNotUsed();
            mNamePattern = regex;
            return this;
        }

        /**
         * @param scanFilter a {@link ScanFilter} to filter devices by
         *
         * @return self for chaining
         * @see ScanFilter for specific details on its various fields
         */
        @NonNull
        public Builder setScanFilter(@Nullable ScanFilter scanFilter) {
            checkNotUsed();
            mScanFilter = scanFilter;
            return this;
        }

        /**
         * Filter devices by raw advertisement data, as obtained by {@link ScanRecord#getBytes}
         *
         * @param rawDataFilter bit values that have to match against advertized data
         * @param rawDataFilterMask bits that have to be matched
         * @return self for chaining
         */
        @NonNull
        public Builder setRawDataFilter(@NonNull byte[] rawDataFilter,
                @Nullable byte[] rawDataFilterMask) {
            checkNotUsed();
            Preconditions.checkNotNull(rawDataFilter);
            checkArgument(rawDataFilterMask == null ||
                    rawDataFilter.length == rawDataFilterMask.length,
                    "Mask and filter should be the same length");
            mRawDataFilter = rawDataFilter;
            mRawDataFilterMask = rawDataFilterMask;
            return this;
        }

        /**
         * Rename the devices shown in the list, using specific bytes from the raw advertisement
         * data ({@link ScanRecord#getBytes}) in hexadecimal format, as well as a custom
         * prefix/suffix around them
         *
         * Note that the prefix length is limited to {@link #getRenamePrefixLengthLimit} characters
         * to ensure that there's enough space to display the byte data
         *
         * The range of bytes to be displayed cannot be empty
         *
         * @param prefix to be displayed before the byte data
         * @param suffix to be displayed after the byte data
         * @param bytesFrom the start byte index to be displayed (inclusive)
         * @param bytesLength the number of bytes to be displayed from the given index
         * @param byteOrder whether the given range of bytes is big endian (will be displayed
         *                   in same order) or little endian (will be flipped before displaying)
         * @return self for chaining
         */
        @NonNull
        public Builder setRenameFromBytes(@NonNull String prefix, @NonNull String suffix,
                int bytesFrom, int bytesLength, ByteOrder byteOrder) {
            checkRenameNotSet();
            checkRangeNotEmpty(bytesLength);
            mRenameBytesFrom = bytesFrom;
            mRenameBytesLength = bytesLength;
            mRenameBytesReverseOrder = byteOrder == ByteOrder.LITTLE_ENDIAN;
            return setRename(prefix, suffix);
        }

        /**
         * Rename the devices shown in the list, using specific characters from the advertised name,
         * as well as a custom prefix/suffix around them
         *
         * Note that the prefix length is limited to {@link #getRenamePrefixLengthLimit} characters
         * to ensure that there's enough space to display the byte data
         *
         * The range of name characters to be displayed cannot be empty
         *
         * @param prefix to be displayed before the byte data
         * @param suffix to be displayed after the byte data
         * @param nameFrom the start name character index to be displayed (inclusive)
         * @param nameLength the number of characters to be displayed from the given index
         * @return self for chaining
         */
        @NonNull
        public Builder setRenameFromName(@NonNull String prefix, @NonNull String suffix,
                int nameFrom, int nameLength) {
            checkRenameNotSet();
            checkRangeNotEmpty(nameLength);
            mRenameNameFrom = nameFrom;
            mRenameNameLength = nameLength;
            mRenameBytesReverseOrder = false;
            return setRename(prefix, suffix);
        }

        private void checkRenameNotSet() {
            checkState(mRenamePrefix == null, "Renaming rule can only be set once");
        }

        private void checkRangeNotEmpty(int length) {
            checkArgument(length > 0, "Range must be non-empty");
        }

        @NonNull
        private Builder setRename(@NonNull String prefix, @NonNull String suffix) {
            checkNotUsed();
            checkArgument(TextUtils.length(prefix) <= getRenamePrefixLengthLimit(),
                    "Prefix is too long");
            mRenamePrefix = prefix;
            mRenameSuffix = suffix;
            return this;
        }

        /** @inheritDoc */
        @Override
        @NonNull
        public BluetoothLeDeviceFilter build() {
            markUsed();
            return new BluetoothLeDeviceFilter(mNamePattern, mScanFilter,
                    mRawDataFilter, mRawDataFilterMask,
                    mRenamePrefix, mRenameSuffix,
                    mRenameBytesFrom, mRenameBytesLength,
                    mRenameNameFrom, mRenameNameLength,
                    mRenameBytesReverseOrder);
        }
    }
}
