/**
 * Copyright (c) 2010-2020 Contributors to the openHAB project
 *
 * See the NOTICE file(s) distributed with this work for additional
 * information.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0
 *
 * SPDX-License-Identifier: EPL-2.0
 */
package org.openhab.binding.bluetooth;

import java.time.ZonedDateTime;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The {@link BaseBluetoothDevice} implements parts of the BluetoothDevice functionality that is
 * shared to all concrete BluetoothDevice implementations.
 *
 * @author Connor Petty - Initial Contribution
 */
@NonNullByDefault
public abstract class BaseBluetoothDevice extends BluetoothDevice {

    private final Logger logger = LoggerFactory.getLogger(BaseBluetoothDevice.class);

    /**
     * Current connection state
     */
    protected ConnectionState connectionState = ConnectionState.DISCOVERING;

    /**
     * Manufacturer id
     */
    protected @Nullable Integer manufacturer = null;

    /**
     * Device name.
     * <p>
     * Uses the devices long name if known, otherwise the short name if known
     */
    protected @Nullable String name;

    /**
     * List of supported services
     */
    protected final Map<UUID, BluetoothService> supportedServices = new ConcurrentHashMap<>();

    /**
     * Last known RSSI
     */
    protected @Nullable Integer rssi = null;

    /**
     * Last reported transmitter power
     */
    protected @Nullable Integer txPower = null;

    protected final transient ZonedDateTime createTime = ZonedDateTime.now();

    /**
     * Last time when activity occurred on this device.
     */
    protected @Nullable ZonedDateTime lastSeenTime = null;

    /**
     * The event listeners will be notified of device updates
     */
    private final Set<BluetoothDeviceListener> eventListeners = new CopyOnWriteArraySet<>();

    /**
     * Construct a Bluetooth device taking the Bluetooth address
     *
     * @param adapter
     * @param sender
     */
    public BaseBluetoothDevice(BluetoothAdapter adapter, BluetoothAddress address) {
        super(adapter, address);
    }

    /**
     * Returns the last time this device was active
     *
     * @return The last time this device was active
     */
    public @Nullable ZonedDateTime getLastSeenTime() {
        return lastSeenTime;
    }

    /**
     * Updates the last activity timestamp for this device.
     * Should be called whenever activity occurs on this device.
     *
     */
    public void updateLastSeenTime() {
        lastSeenTime = ZonedDateTime.now();
    }

    /**
     * Returns the name of the Bluetooth device.
     *
     * @return The devices name
     */
    @Override
    public @Nullable String getName() {
        return name;
    }

    /**
     * Sets the manufacturer id for the device
     *
     * @param manufacturer the manufacturer id
     */
    public void setManufacturerId(int manufacturer) {
        this.manufacturer = manufacturer;
    }

    /**
     * Returns the manufacturer ID of the device
     *
     * @return an integer with manufacturer ID of the device, or null if not known
     */
    @Override
    public @Nullable Integer getManufacturerId() {
        return manufacturer;
    }

    /**
     * Returns a {@link BluetoothService} if the requested service is supported
     *
     * @return the {@link BluetoothService} or null if the service is not supported.
     */
    @Override
    public @Nullable BluetoothService getServices(UUID uuid) {
        return supportedServices.get(uuid);
    }

    /**
     * Returns a list of supported service UUIDs
     *
     * @return list of supported {@link BluetoothService}s.
     */
    @Override
    public Collection<BluetoothService> getServices() {
        return supportedServices.values();
    }

    /**
     * Sets the device transmit power
     *
     * @param power the current transmitter power in dBm
     */
    public void setTxPower(int txPower) {
        this.txPower = txPower;
    }

    /**
     * Returns the last Transmit Power value or null if no transmit power has been received
     *
     * @return the last reported transmitter power value in dBm
     */
    @Override
    public @Nullable Integer getTxPower() {
        return txPower;
    }

    /**
     * Sets the current Receive Signal Strength Indicator (RSSI) value
     *
     * @param rssi the current RSSI value in dBm
     * @return true if the RSSI has changed, false if it was the same as previous
     */
    public boolean setRssi(int rssi) {
        boolean changed = (this.rssi == null || this.rssi != rssi);
        this.rssi = rssi;

        return changed;
    }

    /**
     * Returns the last Receive Signal Strength Indicator (RSSI) value or null if no RSSI has been received
     *
     * @return the last RSSI value in dBm
     */
    @Override
    public @Nullable Integer getRssi() {
        return rssi;
    }

    /**
     * Set the name of the device
     *
     * @param name a {@link String} defining the device name
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * Check if the device supports the specified service
     *
     * @param uuid the service {@link UUID}
     * @return true if the service is supported
     */
    @Override
    public boolean supportsService(UUID uuid) {
        return supportedServices.containsKey(uuid);
    }

    /**
     * Get the current connection state for this device
     *
     * @return the current {@link ConnectionState}
     */
    @Override
    public ConnectionState getConnectionState() {
        return connectionState;
    }

    /**
     * Adds a service to the device.
     *
     * @param service the new {@link BluetoothService} to add
     * @return true if the service was added or false if the service was already supported
     */
    @Override
    protected boolean addService(BluetoothService service) {
        BluetoothService oldValue = supportedServices.putIfAbsent(service.getUuid(), service);
        if (oldValue == null) {
            logger.trace("Adding new service to device {}: {}", address, service);
            return true;
        }
        return false;
    }

    @Override
    protected Collection<BluetoothDeviceListener> getListeners() {
        return eventListeners;
    }

    /**
     * Releases resources that this device is using.
     *
     */
    @Override
    protected void dispose() {
    }

    @Override
    protected void notifyListeners(BluetoothEventType event, Object... args) {
        switch (event) {
            case SCAN_RECORD:
            case CHARACTERISTIC_UPDATED:
            case DESCRIPTOR_UPDATED:
            case SERVICES_DISCOVERED:
                updateLastSeenTime();
                break;
            default:
                break;
        }
        super.notifyListeners(event, args);
    }

    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder();
        builder.append("BluetoothDevice [address=");
        builder.append(address);
        builder.append(", manufacturer=");
        builder.append(manufacturer);
        if (BluetoothCompanyIdentifiers.get(manufacturer) != null) {
            builder.append(" (");
            builder.append(BluetoothCompanyIdentifiers.get(manufacturer));
            builder.append(')');
        }
        builder.append(", name=");
        builder.append(name);
        builder.append(", rssi=");
        builder.append(rssi);
        builder.append(']');
        return builder.toString();
    }
}
