package com.idevicesinc.sweetblue;


import android.annotation.TargetApi;
import android.bluetooth.le.AdvertiseData;
import android.bluetooth.le.AdvertiseSettings;
import android.os.Build;
import android.os.ParcelUuid;

import com.idevicesinc.sweetblue.utils.BitwiseEnum;
import com.idevicesinc.sweetblue.utils.Interval;
import com.idevicesinc.sweetblue.BleAdvertisingSettings.BleAdvertisingMode;
import com.idevicesinc.sweetblue.BleAdvertisingSettings.BleTransmissionPower;

import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

/**
 * Class to used for advertising Bluetooth services, used with {@link com.idevicesinc.sweetblue.BleServer#startAdvertising(BleAdvertisingPacket)}
 */
public final class BleAdvertisingPacket {


    private final UUID[] serviceUuids;
    private final Map<UUID, byte[]> serviceData;
    private final int m_options;
    private final int m_manufacturerId;
    private final byte[] m_manData;


    /**
     * Base constructor which all other constructors in this class overload. This sets all the packet information to be included
     * in your advertisement.
     */
    public BleAdvertisingPacket(UUID[] serviceUuids, Map<UUID, byte[]> serviceData, int options, int manufacturerId, byte[] manufacturerData)
    {
        this.serviceUuids = serviceUuids;
        this.serviceData = serviceData;
        m_options = options;
        m_manufacturerId = manufacturerId;
        m_manData = manufacturerData;
    }

    /**
     * Overload of {@link #BleAdvertisingPacket(UUID[], Map, int, int, byte[])}, which sets the {@link BleAdvertisingPacket.Option#CONNECTABLE},
     * and {@link BleAdvertisingPacket.Option#INCLUDE_NAME} flags.
     */
    public BleAdvertisingPacket(UUID serviceUuid)
    {
        this(new UUID[] { serviceUuid }, null, Option.CONNECTABLE.or(Option.INCLUDE_NAME), 0, null);
    }

    /**
     * Overload of {@link #BleAdvertisingPacket(UUID[], Map, int, int, byte[])}
     */
    public BleAdvertisingPacket(UUID serviceUuid, Option... options)
    {
        this(new UUID[] { serviceUuid }, null, Option.getFlags(options), 0, null);
    }

    /**
     * Overload of {@link #BleAdvertisingPacket(UUID[], Map, int, int, byte[])}, which sets the {@link BleAdvertisingPacket.Option#CONNECTABLE},
     * and {@link BleAdvertisingPacket.Option#INCLUDE_NAME} flags.
     */
    public BleAdvertisingPacket(UUID serviceUuid, int manufacturerId)
    {
        this(new UUID[] { serviceUuid }, null, Option.CONNECTABLE.or(Option.INCLUDE_NAME), manufacturerId, null);
    }

    /**
     * Overload of {@link #BleAdvertisingPacket(UUID[], Map, int, int, byte[])}
     */
    public BleAdvertisingPacket(UUID serviceUuid, int manufacturerId, Option... options)
    {
        this(new UUID[] { serviceUuid }, null, Option.getFlags(options), manufacturerId, null);
    }

    /**
     * Overload of {@link #BleAdvertisingPacket(UUID[], Map, int, int, byte[])}, which sets the {@link BleAdvertisingPacket.Option#CONNECTABLE},
     * and {@link BleAdvertisingPacket.Option#INCLUDE_NAME} flags.
     */
    public BleAdvertisingPacket(UUID serviceUuid, int manufacturerId, byte[] manufacturerData)
    {
        this(new UUID[] { serviceUuid }, null, Option.CONNECTABLE.or(Option.INCLUDE_NAME), manufacturerId, manufacturerData);
    }

    /**
     * Overload of {@link #BleAdvertisingPacket(UUID[], Map, int, int, byte[])}
     */
    public BleAdvertisingPacket(UUID serviceUuid, int manufacturerId, byte[] manufacturerData, Option... options)
    {
        this(new UUID[] { serviceUuid }, null, Option.getFlags(options), manufacturerId, manufacturerData);
    }

    /**
     * Overload of {@link #BleAdvertisingPacket(UUID[], Map, int, int, byte[])}, which sets the {@link BleAdvertisingPacket.Option#CONNECTABLE},
     * and {@link BleAdvertisingPacket.Option#INCLUDE_NAME} flags.
     */
    public BleAdvertisingPacket(UUID[] serviceUuids)
    {
        this(serviceUuids, null, Option.CONNECTABLE.or(Option.INCLUDE_NAME), 0, null);
    }

    /**
     * Overload of {@link #BleAdvertisingPacket(UUID[], Map, int, int, byte[])}
     */
    public BleAdvertisingPacket(UUID[] serviceUuids, Option... options) {
        this(serviceUuids, null, Option.getFlags(options), 0, null);
    }

    /**
     * Overload of {@link #BleAdvertisingPacket(UUID[], Map, int, int, byte[])}, which sets the {@link BleAdvertisingPacket.Option#CONNECTABLE},
     * and {@link BleAdvertisingPacket.Option#INCLUDE_NAME} flags.
     */
    public BleAdvertisingPacket(UUID[] serviceUuids, int manufacturerId)
    {
        this(serviceUuids, null, Option.CONNECTABLE.or(Option.INCLUDE_NAME), manufacturerId, null);
    }

    /**
     * Overload of {@link #BleAdvertisingPacket(UUID[], Map, int, int, byte[])}
     */
    public BleAdvertisingPacket(UUID[] serviceUuids, int manufacturerId, Option... options)
    {
        this(serviceUuids, null, Option.getFlags(options), manufacturerId, null);
    }

    /**
     * Overload of {@link #BleAdvertisingPacket(UUID[], Map, int, int, byte[])}, which sets the {@link BleAdvertisingPacket.Option#CONNECTABLE},
     * and {@link BleAdvertisingPacket.Option#INCLUDE_NAME} flags.
     */
    public BleAdvertisingPacket(UUID[] serviceUuids, int manufacturerId, byte[] manufacturerData)
    {
        this(serviceUuids, null, Option.CONNECTABLE.or(Option.INCLUDE_NAME), manufacturerId, manufacturerData);
    }

    /**
     * Overload of {@link #BleAdvertisingPacket(UUID[], Map, int, int, byte[])}
     */
    public BleAdvertisingPacket(UUID[] serviceUuids, int manufacturerId, byte[] manufacturerData, Option... options)
    {
        this(serviceUuids, null, Option.getFlags(options), manufacturerId, manufacturerData);
    }

    /**
     * Overload of {@link #BleAdvertisingPacket(UUID[], Map, int, int, byte[])}, which sets the {@link BleAdvertisingPacket.Option#CONNECTABLE},
     * and {@link BleAdvertisingPacket.Option#INCLUDE_NAME} flags.
     */
    public BleAdvertisingPacket(UUID serviceUuid, final UUID serviceDataUuid, final byte[] serviceData)
    {
        this(new UUID[] { serviceUuid }, new HashMap<UUID, byte[]>(1) {{ put(serviceDataUuid, serviceData); }}, Option.CONNECTABLE.or(Option.INCLUDE_NAME), 0, null);
    }

    /**
     * Overload of {@link #BleAdvertisingPacket(UUID[], Map, int, int, byte[])}
     */
    public BleAdvertisingPacket(UUID serviceUuid, final UUID serviceDataUuid, final byte[] serviceData, Option... options)
    {
        this(new UUID[] { serviceUuid }, new HashMap<UUID, byte[]>(1) {{ put(serviceDataUuid, serviceData); }}, Option.getFlags(options), 0, null);
    }

    /**
     * Overload of {@link #BleAdvertisingPacket(UUID[], Map, int, int, byte[])}, which sets the {@link BleAdvertisingPacket.Option#CONNECTABLE},
     * and {@link BleAdvertisingPacket.Option#INCLUDE_NAME} flags.
     */
    public BleAdvertisingPacket(UUID[] serviceUuids, final UUID serviceDataUuid, final byte[] serviceData)
    {
        this(serviceUuids, new HashMap<UUID, byte[]>(1) {{ put(serviceDataUuid, serviceData); }}, Option.CONNECTABLE.or(Option.INCLUDE_NAME), 0, null);
    }

    /**
     * Overload of {@link #BleAdvertisingPacket(UUID[], Map, int, int, byte[])}
     */
    public BleAdvertisingPacket(UUID[] serviceUuids, final UUID serviceDataUuid, final byte[] serviceData, Option... options)
    {
        this(serviceUuids, new HashMap<UUID, byte[]>(1) {{ put(serviceDataUuid, serviceData); }}, Option.getFlags(options), 0, null);
    }

    /**
     * Overload of {@link #BleAdvertisingPacket(UUID[], Map, int, int, byte[])}, which sets the {@link BleAdvertisingPacket.Option#CONNECTABLE},
     * and {@link BleAdvertisingPacket.Option#INCLUDE_NAME} flags.
     */
    public BleAdvertisingPacket(UUID serviceUuid, Map<UUID, byte[]> serviceUuidsAndData)
    {
        this(new UUID[] { serviceUuid }, serviceUuidsAndData, Option.CONNECTABLE.or(Option.INCLUDE_NAME), 0, null);
    }

    /**
     * Overload of {@link #BleAdvertisingPacket(UUID[], Map, int, int, byte[])}
     */
    public BleAdvertisingPacket(UUID serviceUuid, Map<UUID, byte[]> serviceUuidsAndData, Option... options)
    {
        this(new UUID[] { serviceUuid }, serviceUuidsAndData, Option.getFlags(options), 0, null);
    }

    /**
     * Overload of {@link #BleAdvertisingPacket(UUID[], Map, int, int, byte[])}, which sets the {@link BleAdvertisingPacket.Option#CONNECTABLE},
     * and {@link BleAdvertisingPacket.Option#INCLUDE_NAME} flags.
     */
    public BleAdvertisingPacket(UUID serviceUuid, Map<UUID, byte[]> serviceUuidsAndData, int manufacturerId, byte[] manufacturerData)
    {
        this(new UUID[] { serviceUuid }, serviceUuidsAndData, Option.CONNECTABLE.or(Option.INCLUDE_NAME), manufacturerId, manufacturerData);
    }

    /**
     * Overload of {@link #BleAdvertisingPacket(UUID[], Map, int, int, byte[])}
     */
    public BleAdvertisingPacket(UUID serviceUuid, Map<UUID, byte[]> serviceUuidsAndData, int manufacturerId, byte[] manufacturerData, Option... options)
    {
        this(new UUID[] { serviceUuid }, serviceUuidsAndData, Option.getFlags(options), manufacturerId, manufacturerData);
    }

    /**
     * Overload of {@link #BleAdvertisingPacket(UUID[], Map, int, int, byte[])}, which sets the {@link BleAdvertisingPacket.Option#CONNECTABLE},
     * and {@link BleAdvertisingPacket.Option#INCLUDE_NAME} flags.
     */
    public BleAdvertisingPacket(final UUID serviceDataUuid, final byte[] serviceData)
    {
        this(new UUID[0], new HashMap<UUID, byte[]>(1) {{ put(serviceDataUuid, serviceData); }}, Option.CONNECTABLE.or(Option.INCLUDE_NAME), 0, null);
    }

    /**
     * Overload of {@link #BleAdvertisingPacket(UUID[], Map, int, int, byte[])}
     */
    public BleAdvertisingPacket(final UUID serviceDataUuid, final byte[] serviceData, Option... options)
    {
        this(new UUID[0], new HashMap<UUID, byte[]>(1) {{ put(serviceDataUuid, serviceData); }}, Option.getFlags(options), 0, null);
    }

    /**
     * Overload of {@link #BleAdvertisingPacket(UUID[], Map, int, int, byte[])}, which sets the {@link BleAdvertisingPacket.Option#CONNECTABLE},
     * and {@link BleAdvertisingPacket.Option#INCLUDE_NAME} flags.
     */
    public BleAdvertisingPacket(final UUID serviceDataUuid, final byte[] serviceData, int manufacturerId, byte[] manufacturerData)
    {
        this(new UUID[0], new HashMap<UUID, byte[]>(1) {{ put(serviceDataUuid, serviceData); }}, Option.CONNECTABLE.or(Option.INCLUDE_NAME), manufacturerId, manufacturerData);
    }

    /**
     * Overload of {@link #BleAdvertisingPacket(UUID[], Map, int, int, byte[])}
     */
    public BleAdvertisingPacket(final UUID serviceDataUuid, final byte[] serviceData, int manufacturerId, byte[] manufacturerData, Option... options)
    {
        this(new UUID[0], new HashMap<UUID, byte[]>(1) {{ put(serviceDataUuid, serviceData); }}, Option.getFlags(options), manufacturerId, manufacturerData);
    }

    /**
     * Returns the manufacturer Id being used
     */
    public int getManufacturerId()
    {
        return m_manufacturerId;
    }

    /**
     * Returns the manufacturer data
     */
    public byte[] getManufacturerData()
    {
        return m_manData;
    }

    /**
     * Whether or not this advertisement is connectable
     */
    public boolean isConnectable()
    {
        return (m_options & Option.CONNECTABLE.bit()) == Option.CONNECTABLE.bit();
    }

    /**
     * Whether or not this advertisement includes the device name
     */
    public boolean includeDeviceName()
    {
        return (m_options & Option.INCLUDE_NAME.bit()) == Option.INCLUDE_NAME.bit();
    }

    /**
     * Whether or not this advertisement includes the Tx power level in the packet
     */
    public boolean includeTxPowerLevel()
    {
        return (m_options & Option.INCLUDE_TX_POWER.bit()) == Option.INCLUDE_TX_POWER.bit();
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    /*package*/ AdvertiseSettings getNativeSettings(BleAdvertisingMode mode, BleTransmissionPower power, Interval timeout) {
        AdvertiseSettings.Builder settings = new AdvertiseSettings.Builder();
        settings.setAdvertiseMode(mode.getNativeMode());
        settings.setTxPowerLevel(power.getNativeMode());
        settings.setConnectable(isConnectable());
        settings.setTimeout((int) timeout.millis());
        return settings.build();
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    /*package*/ AdvertiseData getNativeData() {
        AdvertiseData.Builder data = new AdvertiseData.Builder();
        for (UUID id : serviceUuids)
        {
            data.addServiceUuid(new ParcelUuid(id));
        }
        if (m_manufacturerId != 0 && m_manData != null)
        {
            data.addManufacturerData(m_manufacturerId, m_manData);
        }
        if (serviceData != null && serviceData.size() > 0)
        {
            for (UUID dataUuid : serviceData.keySet())
            {
                data.addServiceData(new ParcelUuid(dataUuid), serviceData.get(dataUuid));
            }
        }
        data.setIncludeDeviceName(includeDeviceName());
        data.setIncludeTxPowerLevel(includeTxPowerLevel());
        return data.build();
    }

    /**
     * Returns true if this advertising packet contains the uuid given.
     */
    public boolean hasUuid(UUID uuid)
    {
        if (serviceUuids != null && serviceUuids.length > 0)
        {
            for (UUID id : serviceUuids)
            {
                if (id.equals(uuid))
                {
                    return true;
                }
            }
        }
        if (serviceData != null && serviceData.size() > 0)
        {
            for (UUID id : serviceData.keySet())
            {
                if (id.equals(uuid))
                {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Returns a HashSet of UUIDS that will be advertised
     */
    public UUID[] getUuids()
    {
        return serviceUuids;
    }

    /**
     * Returns a Map of 16bit service UUIDs, along with the associated byte arrays.
     */
    public Map<UUID, byte[]> getServiceData()
    {
        return serviceData;
    }


    /**
     * Enumeration for advertising options
     */
    public static enum Option implements BitwiseEnum
    {

        CONNECTABLE(1),
        INCLUDE_NAME(2),
        INCLUDE_TX_POWER(4);

        private final int m_bit;

        private Option(int bit)
        {
            m_bit = bit;
        }

        public static int getFlags(Option[] options) {
            if (options == null || options.length == 0) {
                return 0;
            }
            int flags = 0;
            for (Option o : options) {
                flags |= o.bit();
            }
            return flags;
        }

        @Override public int or(BitwiseEnum state)
        {
            return m_bit | state.bit();
        }

        @Override public int or(int bits)
        {
            return m_bit | bits;
        }

        @Override public int bit()
        {
            return m_bit;
        }

        @Override public boolean overlaps(int mask)
        {
            return (m_bit & mask) != 0x0;
        }
    }


}