/*
 * Copyright (c) 2018, Nordic Semiconductor
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the
 * documentation and/or other materials provided with the distribution.
 *
 * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
 * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package no.nordicsemi.android.support.v18.scanner;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.os.Parcel;
import android.os.ParcelUuid;
import android.os.Parcelable;

import java.util.Arrays;
import java.util.List;
import java.util.UUID;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

/**
 * Criteria for filtering result from Bluetooth LE scans. A {@link ScanFilter} allows clients to
 * restrict scan results to only those that are of interest to them.
 * <p>Current filtering on the following fields are supported:</p>
 * <ul>
 * <li>Service UUIDs which identify the bluetooth gatt services running on the device.</li>
 * <li>Name of remote Bluetooth LE device.</li>
 * <li>Mac address of the remote device.</li>
 * <li>Service data which is the data associated with a service.</li>
 * <li>Manufacturer specific data which is the data associated with a particular manufacturer.</li>
 * </ul>
 *
 * @see ScanResult
 * @see BluetoothLeScannerCompat
 */
@SuppressWarnings("WeakerAccess")
public final class ScanFilter implements Parcelable {

	@Nullable
	private final String deviceName;
	@Nullable
	private final String deviceAddress;

	@Nullable
	private final ParcelUuid serviceUuid;
	@Nullable
	private final ParcelUuid serviceUuidMask;

	@Nullable
	private final ParcelUuid serviceDataUuid;
	@Nullable
	private final byte[] serviceData;
	@Nullable
	private final byte[] serviceDataMask;

	private final int manufacturerId;
	@Nullable
	private final byte[] manufacturerData;
	@Nullable
	private final byte[] manufacturerDataMask;

	private static final ScanFilter EMPTY = new ScanFilter.Builder().build() ;

	private ScanFilter(@Nullable final String name, @Nullable final String deviceAddress,
					   @Nullable final ParcelUuid uuid, @Nullable final ParcelUuid uuidMask,
					   @Nullable final ParcelUuid serviceDataUuid, @Nullable final byte[] serviceData,
					   @Nullable final byte[] serviceDataMask, final int manufacturerId,
					   @Nullable final byte[] manufacturerData,
					   @Nullable final byte[] manufacturerDataMask) {
		this.deviceName = name;
		this.serviceUuid = uuid;
		this.serviceUuidMask = uuidMask;
		this.deviceAddress = deviceAddress;
		this.serviceDataUuid = serviceDataUuid;
		this.serviceData = serviceData;
		this.serviceDataMask = serviceDataMask;
		this.manufacturerId = manufacturerId;
		this.manufacturerData = manufacturerData;
		this.manufacturerDataMask = manufacturerDataMask;
	}

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

	@Override
	public void writeToParcel(final Parcel dest, final int flags) {
		dest.writeInt(deviceName == null ? 0 : 1);
		if (deviceName != null) {
			dest.writeString(deviceName);
		}
		dest.writeInt(deviceAddress == null ? 0 : 1);
		if (deviceAddress != null) {
			dest.writeString(deviceAddress);
		}
		dest.writeInt(serviceUuid == null ? 0 : 1);
		if (serviceUuid != null) {
			dest.writeParcelable(serviceUuid, flags);
			dest.writeInt(serviceUuidMask == null ? 0 : 1);
			if (serviceUuidMask != null) {
				dest.writeParcelable(serviceUuidMask, flags);
			}
		}
		dest.writeInt(serviceDataUuid == null ? 0 : 1);
		if (serviceDataUuid != null) {
			dest.writeParcelable(serviceDataUuid, flags);
			dest.writeInt(serviceData == null ? 0 : 1);
			if (serviceData != null) {
				dest.writeInt(serviceData.length);
				dest.writeByteArray(serviceData);

				dest.writeInt(serviceDataMask == null ? 0 : 1);
				if (serviceDataMask != null) {
					dest.writeInt(serviceDataMask.length);
					dest.writeByteArray(serviceDataMask);
				}
			}
		}
		dest.writeInt(manufacturerId);
		dest.writeInt(manufacturerData == null ? 0 : 1);
		if (manufacturerData != null) {
			dest.writeInt(manufacturerData.length);
			dest.writeByteArray(manufacturerData);

			dest.writeInt(manufacturerDataMask == null ? 0 : 1);
			if (manufacturerDataMask != null) {
				dest.writeInt(manufacturerDataMask.length);
				dest.writeByteArray(manufacturerDataMask);
			}
		}
	}

	/**
	 * A {@link android.os.Parcelable.Creator} to create {@link ScanFilter} from parcel.
	 */
	public static final Creator<ScanFilter> CREATOR = new Creator<ScanFilter>() {

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

		@Override
		public ScanFilter createFromParcel(final Parcel in) {
			final Builder builder = new Builder();
			if (in.readInt() == 1) {
				builder.setDeviceName(in.readString());
			}
			if (in.readInt() == 1) {
				builder.setDeviceAddress(in.readString());
			}
			if (in.readInt() == 1) {
				ParcelUuid uuid = in.readParcelable(ParcelUuid.class.getClassLoader());
				builder.setServiceUuid(uuid);
				if (in.readInt() == 1) {
					ParcelUuid uuidMask = in.readParcelable(
							ParcelUuid.class.getClassLoader());
					builder.setServiceUuid(uuid, uuidMask);
				}
			}
			if (in.readInt() == 1) {
				ParcelUuid serviceDataUuid = in.readParcelable(ParcelUuid.class.getClassLoader());
				if (in.readInt() == 1) {
					final int serviceDataLength = in.readInt();
					final byte[] serviceData = new byte[serviceDataLength];
					in.readByteArray(serviceData);
					if (in.readInt() == 0) {
						//noinspection ConstantConditions
						builder.setServiceData(serviceDataUuid, serviceData);
					} else {
						final int serviceDataMaskLength = in.readInt();
						final byte[] serviceDataMask = new byte[serviceDataMaskLength];
						in.readByteArray(serviceDataMask);
						//noinspection ConstantConditions
						builder.setServiceData(serviceDataUuid, serviceData, serviceDataMask);
					}
				}
			}

			final int manufacturerId = in.readInt();
			if (in.readInt() == 1) {
				final int manufacturerDataLength = in.readInt();
				final byte[] manufacturerData = new byte[manufacturerDataLength];
				in.readByteArray(manufacturerData);
				if (in.readInt() == 0) {
					builder.setManufacturerData(manufacturerId, manufacturerData);
				} else {
					final int manufacturerDataMaskLength = in.readInt();
					final byte[] manufacturerDataMask = new byte[manufacturerDataMaskLength];
					in.readByteArray(manufacturerDataMask);
					builder.setManufacturerData(manufacturerId, manufacturerData,
							manufacturerDataMask);
				}
			}

			return builder.build();
		}
	};

	/**
	 * Returns the filter set the device name field of Bluetooth advertisement data.
	 */
	@Nullable
	public String getDeviceName() {
		return deviceName;
	}

	/**
	 * Returns the filter set on the service uuid.
	 */
	@Nullable
	public ParcelUuid getServiceUuid() {
		return serviceUuid;
	}

	@Nullable
	public ParcelUuid getServiceUuidMask() {
		return serviceUuidMask;
	}

	@Nullable
	public String getDeviceAddress() {
		return deviceAddress;
	}

	@Nullable
	public byte[] getServiceData() {
		return serviceData;
	}

	@Nullable
	public byte[] getServiceDataMask() {
		return serviceDataMask;
	}

	@Nullable
	public ParcelUuid getServiceDataUuid() {
		return serviceDataUuid;
	}

	/**
	 * Returns the manufacturer id. -1 if the manufacturer filter is not set.
	 */
	public int getManufacturerId() {
		return manufacturerId;
	}

	@Nullable
	public byte[] getManufacturerData() {
		return manufacturerData;
	}

	@Nullable
	public byte[] getManufacturerDataMask() {
		return manufacturerDataMask;
	}

	/**
	 * Check if the scan filter matches a {@code scanResult}. A scan result is considered as a match
	 * if it matches all the field filters.
	 */
	public boolean matches(@Nullable final ScanResult scanResult) {
		if (scanResult == null) {
			return false;
		}
		final BluetoothDevice device = scanResult.getDevice();
		// Device match.
		if (deviceAddress != null && !deviceAddress.equals(device.getAddress())) {
			return false;
		}

		final ScanRecord scanRecord = scanResult.getScanRecord();

		// Scan record is null but there exist filters on it.
		if (scanRecord == null
				&& (deviceName != null || serviceUuid != null || manufacturerData != null
				|| serviceData != null)) {
			return false;
		}

		// Local name match.
		if (deviceName != null && !deviceName.equals(scanRecord.getDeviceName())) {
			return false;
		}

		// UUID match.
		if (serviceUuid != null && !matchesServiceUuids(serviceUuid, serviceUuidMask,
				scanRecord.getServiceUuids())) {
			return false;
		}

		// Service data match
		if (serviceDataUuid != null && scanRecord != null) {
			if (!matchesPartialData(serviceData, serviceDataMask,
					scanRecord.getServiceData(serviceDataUuid))) {
				return false;
			}
		}

		// Manufacturer data match.
		if (manufacturerId >= 0 && scanRecord != null) {
			//noinspection RedundantIfStatement
			if (!matchesPartialData(manufacturerData, manufacturerDataMask,
					scanRecord.getManufacturerSpecificData(manufacturerId))) {
				return false;
			}
		}
		// All filters match.
		return true;
	}

	/**
	 * Check if the uuid pattern is contained in a list of parcel uuids.
	 */
	private static boolean matchesServiceUuids(@Nullable final ParcelUuid uuid,
											   @Nullable final ParcelUuid parcelUuidMask,
											   @Nullable final List<ParcelUuid> uuids) {
		if (uuid == null) {
			return true;
		}
		if (uuids == null) {
			return false;
		}

		for (final ParcelUuid parcelUuid : uuids) {
			final UUID uuidMask = parcelUuidMask == null ? null : parcelUuidMask.getUuid();
			if (matchesServiceUuid(uuid.getUuid(), uuidMask, parcelUuid.getUuid())) {
				return true;
			}
		}
		return false;
	}

	// Check if the uuid pattern matches the particular service uuid.
	private static boolean matchesServiceUuid(@NonNull  final UUID uuid,
											  @Nullable final UUID mask,
											  @NonNull  final UUID data) {
		if (mask == null) {
			return uuid.equals(data);
		}
		if ((uuid.getLeastSignificantBits() & mask.getLeastSignificantBits()) !=
				(data.getLeastSignificantBits() & mask.getLeastSignificantBits())) {
			return false;
		}
		return ((uuid.getMostSignificantBits() & mask.getMostSignificantBits()) ==
				(data.getMostSignificantBits() & mask.getMostSignificantBits()));
	}

	// Check whether the data pattern matches the parsed data.
	@SuppressWarnings("BooleanMethodIsAlwaysInverted")
	private boolean matchesPartialData(@Nullable final byte[] data,
									   @Nullable final byte[] dataMask,
									   @Nullable final byte[] parsedData) {
		if (data == null) {
			// If filter data is null it means it doesn't matter.
			// We return true if any data matching the manufacturerId were found.
			return parsedData != null;
		}
		if (parsedData == null || parsedData.length < data.length) {
			return false;
		}
		if (dataMask == null) {
			for (int i = 0; i < data.length; ++i) {
				if (parsedData[i] != data[i]) {
					return false;
				}
			}
			return true;
		}
		for (int i = 0; i < data.length; ++i) {
			if ((dataMask[i] & parsedData[i]) != (dataMask[i] & data[i])) {
				return false;
			}
		}
		return true;
	}

	@Override
	public String toString() {
		return "BluetoothLeScanFilter [deviceName=" + deviceName + ", deviceAddress="
				+ deviceAddress
				+ ", mUuid=" + serviceUuid + ", uuidMask=" + serviceUuidMask
				+ ", serviceDataUuid=" + Objects.toString(serviceDataUuid) + ", serviceData="
				+ Arrays.toString(serviceData) + ", serviceDataMask="
				+ Arrays.toString(serviceDataMask) + ", manufacturerId=" + manufacturerId
				+ ", manufacturerData=" + Arrays.toString(manufacturerData)
				+ ", manufacturerDataMask=" + Arrays.toString(manufacturerDataMask) + "]";
	}

	@Override
	public int hashCode() {
		return Objects.hash(deviceName, deviceAddress, manufacturerId,
				Arrays.hashCode(manufacturerData),
				Arrays.hashCode(manufacturerDataMask),
				serviceDataUuid,
				Arrays.hashCode(serviceData),
				Arrays.hashCode(serviceDataMask),
				serviceUuid, serviceUuidMask);
	}

	@Override
	public boolean equals(final Object obj) {
		if (this == obj) {
			return true;
		}
		if (obj == null || getClass() != obj.getClass()) {
			return false;
		}
		final ScanFilter other = (ScanFilter) obj;
		return Objects.equals(deviceName, other.deviceName) &&
				Objects.equals(deviceAddress, other.deviceAddress) &&
				manufacturerId == other.manufacturerId &&
				Objects.deepEquals(manufacturerData, other.manufacturerData) &&
				Objects.deepEquals(manufacturerDataMask, other.manufacturerDataMask) &&
				Objects.equals(serviceDataUuid, other.serviceDataUuid) &&
				Objects.deepEquals(serviceData, other.serviceData) &&
				Objects.deepEquals(serviceDataMask, other.serviceDataMask) &&
				Objects.equals(serviceUuid, other.serviceUuid) &&
				Objects.equals(serviceUuidMask, other.serviceUuidMask);
	}

	/**
	 * Checks if the scan filter is empty.
	 */
	@SuppressWarnings("unused")
	/* package */ boolean isAllFieldsEmpty() {
		return EMPTY.equals(this);
	}

	/**
	 * Builder class for {@link ScanFilter}.
	 */
	public static final class Builder {

		private String deviceName;
		private String deviceAddress;

		private ParcelUuid serviceUuid;
		private ParcelUuid uuidMask;

		private ParcelUuid serviceDataUuid;
		private byte[] serviceData;
		private byte[] serviceDataMask;

		private int manufacturerId = -1;
		private byte[] manufacturerData;
		private byte[] manufacturerDataMask;

		/**
		 * Set filter on device name.
		 */
		public Builder setDeviceName(@Nullable final String deviceName) {
			this.deviceName = deviceName;
			return this;
		}

		/**
		 * Set filter on device address.
		 *
		 * @param deviceAddress The device Bluetooth address for the filter. It needs to be in the
		 *            format of "01:02:03:AB:CD:EF". The device address can be validated using
		 *            {@link BluetoothAdapter#checkBluetoothAddress}.
		 * @throws IllegalArgumentException If the {@code deviceAddress} is invalid.
		 */
		public Builder setDeviceAddress(@Nullable final String deviceAddress) {
			if (deviceAddress != null && !BluetoothAdapter.checkBluetoothAddress(deviceAddress)) {
				throw new IllegalArgumentException("invalid device address " + deviceAddress);
			}
			this.deviceAddress = deviceAddress;
			return this;
		}

		/**
		 * Set filter on service uuid.
		 */
		public Builder setServiceUuid(@Nullable final ParcelUuid serviceUuid) {
			this.serviceUuid = serviceUuid;
			this.uuidMask = null; // clear uuid mask
			return this;
		}

		/**
		 * Set filter on partial service uuid. The {@code uuidMask} is the bit mask for the
		 * {@code serviceUuid}. Set any bit in the mask to 1 to indicate a match is needed for the
		 * bit in {@code serviceUuid}, and 0 to ignore that bit.
		 *
		 * @throws IllegalArgumentException If {@code serviceUuid} is {@code null} but
		 *             {@code uuidMask} is not {@code null}.
		 */
		public Builder setServiceUuid(@Nullable final ParcelUuid serviceUuid,
									  @Nullable final ParcelUuid uuidMask) {
			if (uuidMask != null && serviceUuid == null) {
				throw new IllegalArgumentException("uuid is null while uuidMask is not null!");
			}
			this.serviceUuid = serviceUuid;
			this.uuidMask = uuidMask;
			return this;
		}

		/**
		 * Set filtering on service data.
		 *
		 * @throws IllegalArgumentException If {@code serviceDataUuid} is null.
		 */
		public Builder setServiceData(@NonNull final ParcelUuid serviceDataUuid,
									  @Nullable final byte[] serviceData) {
			//noinspection ConstantConditions
			if (serviceDataUuid == null) {
				throw new IllegalArgumentException("serviceDataUuid is null!");
			}
			this.serviceDataUuid = serviceDataUuid;
			this.serviceData = serviceData;
			this.serviceDataMask = null; // clear service data mask
			return this;
		}

		/**
		 * Set partial filter on service data. For any bit in the mask, set it to 1 if it needs to
		 * match the one in service data, otherwise set it to 0 to ignore that bit.
		 * <p>
		 * The {@code serviceDataMask} must have the same length of the {@code serviceData}.
		 *
		 * @throws IllegalArgumentException If {@code serviceDataUuid} is null or
		 *             {@code serviceDataMask} is {@code null} while {@code serviceData} is not or
		 *             {@code serviceDataMask} and {@code serviceData} has different length.
		 */
		public Builder setServiceData(@NonNull final ParcelUuid serviceDataUuid,
									  @Nullable final byte[] serviceData,
									  @Nullable final byte[] serviceDataMask) {
			//noinspection ConstantConditions
			if (serviceDataUuid == null) {
				throw new IllegalArgumentException("serviceDataUuid is null");
			}
			if (serviceDataMask != null) {
				if (serviceData == null) {
					throw new IllegalArgumentException(
							"serviceData is null while serviceDataMask is not null");
				}
				// Since the serviceDataMask is a bit mask for serviceData, the lengths of the two
				// byte array need to be the same.
				if (serviceData.length != serviceDataMask.length) {
					throw new IllegalArgumentException(
							"size mismatch for service data and service data mask");
				}
			}
			this.serviceDataUuid = serviceDataUuid;
			this.serviceData = serviceData;
			this.serviceDataMask = serviceDataMask;
			return this;
		}

		/**
		 * Set filter on on manufacturerData. A negative manufacturerId is considered as invalid id.
		 * <p>
		 * Note the first two bytes of the {@code manufacturerData} is the manufacturerId.
		 *
		 * @throws IllegalArgumentException If the {@code manufacturerId} is invalid.
		 */
		public Builder setManufacturerData(final int manufacturerId,
										   @Nullable final byte[] manufacturerData) {
			if (manufacturerData != null && manufacturerId < 0) {
				throw new IllegalArgumentException("invalid manufacture id");
			}
			this.manufacturerId = manufacturerId;
			this.manufacturerData = manufacturerData;
			this.manufacturerDataMask = null; // clear manufacturer data mask
			return this;
		}

		/**
		 * Set filter on partial manufacture data. For any bit in the mask, set it the 1 if it needs
		 * to match the one in manufacturer data, otherwise set it to 0.
		 * <p>
		 * The {@code manufacturerDataMask} must have the same length of {@code manufacturerData}.
		 *
		 * @throws IllegalArgumentException If the {@code manufacturerId} is invalid, or
		 *             {@code manufacturerData} is null while {@code manufacturerDataMask} is not,
		 *             or {@code manufacturerData} and {@code manufacturerDataMask} have different
		 *             length.
		 */
		public Builder setManufacturerData(final int manufacturerId,
										   @Nullable final byte[] manufacturerData,
										   @Nullable final byte[] manufacturerDataMask) {
			if (manufacturerData != null && manufacturerId < 0) {
				throw new IllegalArgumentException("invalid manufacture id");
			}
			if (manufacturerDataMask != null) {
				if (manufacturerData == null) {
					throw new IllegalArgumentException(
							"manufacturerData is null while manufacturerDataMask is not null");
				}
				// Since the manufacturerDataMask is a bit mask for manufacturerData, the lengths
				// of the two byte array need to be the same.
				if (manufacturerData.length != manufacturerDataMask.length) {
					throw new IllegalArgumentException(
							"size mismatch for manufacturerData and manufacturerDataMask");
				}
			}
			this.manufacturerId = manufacturerId;
			this.manufacturerData = manufacturerData;
			this.manufacturerDataMask = manufacturerDataMask;
			return this;
		}

		/**
		 * Build {@link ScanFilter}.
		 *
		 * @throws IllegalArgumentException If the filter cannot be built.
		 */
		public ScanFilter build() {
			return new ScanFilter(deviceName, deviceAddress, serviceUuid, uuidMask,
					serviceDataUuid, serviceData, serviceDataMask,
					manufacturerId, manufacturerData, manufacturerDataMask);
		}
	}
}