package de.toman.milight;

import java.awt.Color;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.HashSet;
import java.util.Set;

import de.toman.milight.events.ChangeBrightnessEvent;
import de.toman.milight.events.ChangeColorEvent;
import de.toman.milight.events.ColoredModeEvent;
import de.toman.milight.events.DiscoModeEvent;
import de.toman.milight.events.DiscoModeFasterEvent;
import de.toman.milight.events.DiscoModeSlowerEvent;
import de.toman.milight.events.LightEvent;
import de.toman.milight.events.LightListener;
import de.toman.milight.events.SwitchOffEvent;
import de.toman.milight.events.SwitchOnEvent;
import de.toman.milight.events.WhiteModeEvent;

/**
 * This class represents a MiLight WiFi box and is able to send commands to a
 * particular box.
 * 
 * @author Stefan Toman ([email protected])
 */
public class WiFiBox {
	/**
	 * The address of the WiFi box
	 */
	private InetAddress address;

	/**
	 * The port of the WiFi box
	 */
	private int port;

	/**
	 * The set of all listeners listening for all groups of lights connected to
	 * this WiFiBox.
	 */
	private Set<LightListener>[] lightListeners;

	/**
	 * The number of the currently active group
	 */
	private int activeGroup;

	/**
	 * An array containing references to Lights instances for all groups of
	 * lights connected to this WiFiBox. They are not created on the fly to
	 * avoid creating duplicate instances. Entry 0 corresponds to group 1 and so
	 * on.
	 */
	private Lights[] lights;

	/**
	 * The default port for unconfigured boxes.
	 */
	public static final int DEFAULT_PORT = 8899;

	/**
	 * The sleep time between both messages for switching lights to the white
	 * mode.
	 */
	public static final int MIN_SLEEP_BETWEEN_MESSAGES = 100;

	/**
	 * The command code for "RGBW COLOR LED ALL OFF".
	 */
	public static final int COMMAND_ALL_OFF = 0x41;

	/**
	 * The command code for "GROUP 1 ALL OFF".
	 */
	public static final int COMMAND_GROUP_1_OFF = 0x46;

	/**
	 * The command code for "GROUP 2 ALL OFF".
	 */
	public static final int COMMAND_GROUP_2_OFF = 0x48;

	/**
	 * The command code for "GROUP 3 ALL OFF".
	 */
	public static final int COMMAND_GROUP_3_OFF = 0x4A;

	/**
	 * The command code for "GROUP 4 ALL OFF".
	 */
	public static final int COMMAND_GROUP_4_OFF = 0x4C;

	/**
	 * The command code for "RGBW COLOR LED ALL ON".
	 */
	public static final int COMMAND_ALL_ON = 0x42;

	/**
	 * The command code for "GROUP 1 ALL ON".
	 */
	public static final int COMMAND_GROUP_1_ON = 0x45;

	/**
	 * The command code for "GROUP 2 ALL ON".
	 */
	public static final int COMMAND_GROUP_2_ON = 0x47;

	/**
	 * The command code for "GROUP 3 ALL ON".
	 */
	public static final int COMMAND_GROUP_3_ON = 0x49;

	/**
	 * The command code for "GROUP 4 ALL ON".
	 */
	public static final int COMMAND_GROUP_4_ON = 0x4B;

	/**
	 * The command code for "SET COLOR TO WHITE (GROUP ALL)". Send an "ON"
	 * command 100ms before.
	 */
	public static final int COMMAND_ALL_WHITE = 0xC2;

	/**
	 * The command code for "SET COLOR TO WHITE (GROUP 1)". Send an "ON" command
	 * 100ms before.
	 */
	public static final int COMMAND_GROUP_1_WHITE = 0xC5;

	/**
	 * The command code for "SET COLOR TO WHITE (GROUP 2)". Send an "ON" command
	 * 100ms before.
	 */
	public static final int COMMAND_GROUP_2_WHITE = 0xC7;

	/**
	 * The command code for "SET COLOR TO WHITE (GROUP 3)". Send an "ON" command
	 * 100ms before.
	 */
	public static final int COMMAND_GROUP_3_WHITE = 0xC9;

	/**
	 * The command code for "SET COLOR TO WHITE (GROUP 4)". Send an "ON" command
	 * 100ms before.
	 */
	public static final int COMMAND_GROUP_4_WHITE = 0xCB;

	/**
	 * The command code for "DISCO MODE".
	 */
	public static final int COMMAND_DISCO = 0x4D;

	/**
	 * The command code for "DISCO SPEED FASTER".
	 */
	public static final int COMMAND_DISCO_FASTER = 0x44;

	/**
	 * The command code for "DISCO SPEED SLOWER".
	 */
	public static final int COMMAND_DISCO_SLOWER = 0x43;

	/**
	 * The command code for "COLOR SETTING" (part of a two-byte command).
	 */

	public static final int COMMAND_COLOR = 0x40;
	/**
	 * The command code for "DIRECT BRIGHTNESS SETTING" (part of a two-byte
	 * command).
	 */
	public static final int COMMAND_BRIGHTNESS = 0x4E;

	/**
	 * A constructor creating a new instance of the WiFi box class.
	 * 
	 * @param address
	 *            is the address of the WiFi box
	 * @param port
	 *            is the port of the WiFi box (omit this if unsure)
	 */
	@SuppressWarnings("unchecked")
	public WiFiBox(InetAddress address, int port) {
		// super call
		super();

		// save attributes
		this.address = address;
		this.port = port;

		// create listener sets
		lightListeners = new HashSet[4];
		for (int i = 0; i < 4; i++) {
			lightListeners[i] = new HashSet<LightListener>();
		}

		// create lights
		lights = new Lights[4];
		for (int group = 1; group <= 4; group++) {
			lights[group - 1] = new Lights(this, group);
		}
	}

	/**
	 * A constructor creating a new instance of the WiFi box class using the
	 * default port number.
	 * 
	 * @param address
	 *            is the address of the WiFi box
	 */
	public WiFiBox(InetAddress address) {
		this(address, DEFAULT_PORT);
	}

	/**
	 * A constructor creating a new instance of the WiFi box class. The address
	 * is resolved from a hostname or ip address.
	 * 
	 * @param host
	 *            is the host given as hostname such as "domain.tld" or string
	 *            repesentation of an ip address
	 * @param port
	 *            is the port of the WiFi box (omit this if unsure)
	 * @throws UnknownHostException
	 *             if the hostname could not be resolved
	 */
	public WiFiBox(String host, int port) throws UnknownHostException {
		this(InetAddress.getByName(host), port);
	}

	/**
	 * A constructor creating a new instance of the WiFi box class using the
	 * default port number. The address is resolved from a hostname or ip
	 * address.
	 * 
	 * @param host
	 *            is the host given as hostname such as "domain.tld" or string
	 *            repesentation of an ip address
	 * @throws UnknownHostException
	 *             if the hostname could not be resolved
	 */
	public WiFiBox(String host) throws UnknownHostException {
		this(host, DEFAULT_PORT);
	}

	/**
	 * Get the group of lights that is controlled by a given group number. The
	 * Lights instance may be used to control the groups of lights individually
	 * and mix different WiFi boxes.
	 * 
	 * @param group
	 *            is the number of the group at the WiFi box (between 1 and 4)
	 * @return the group of lights that is controled by the given group number
	 * @throws IllegalArgumentException
	 *             if the group number is not between 1 and 4
	 */
	public Lights getLights(int group) throws IllegalArgumentException {
		// check group number
		if (1 > group || group > 4) {
			throw new IllegalArgumentException(
					"The group number must be between 1 and 4");
		}

		// create new instance
		return lights[group - 1];
	}

	/**
	 * This function sends an array of bytes to the WiFi box. The bytes should
	 * be a valid command, i.e. the array's length should be three.
	 * 
	 * @param messages
	 *            is an array of message codes to send
	 * @throws IllegalArgumentException
	 *             if the length of the array is not 3
	 * @throws IOException
	 *             if the message could not be sent
	 */
	private void sendMessage(byte[] messages) throws IOException {
		// check arguments
		if (messages.length != 3) {
			throw new IllegalArgumentException(
					"The message to send should consist of exactly 3 bytes.");
		}

		// notify listeners
		notifyLightListeners(messages);

		// send message
		DatagramSocket socket = new DatagramSocket();
		DatagramPacket packet = new DatagramPacket(messages, messages.length,
				address, port);
		socket.send(packet);
		socket.close();

		// adjust currently active group of lights
		switch (messages[0]) {
		case COMMAND_GROUP_1_ON:
		case COMMAND_GROUP_1_OFF:
			activeGroup = 1;
			break;
		case COMMAND_GROUP_2_ON:
		case COMMAND_GROUP_2_OFF:
			activeGroup = 2;
			break;
		case COMMAND_GROUP_3_ON:
		case COMMAND_GROUP_3_OFF:
			activeGroup = 3;
			break;
		case COMMAND_GROUP_4_ON:
		case COMMAND_GROUP_4_OFF:
			activeGroup = 4;
			break;
		}
	}

	/**
	 * This function pads a one-byte message to a three-byte message by adding
	 * the default bytes 0x00 0x55.
	 * 
	 * @param message
	 *            is the message to pad
	 * @return is the padded message
	 */
	private byte[] padMessage(int message) {
		byte[] paddedMessage = { (byte) message, 0x55 & 0x00, 0x55 & 0x55 };
		return paddedMessage;
	}

	/**
	 * This function pads a two-byte message to a three-byte message by adding
	 * the default byte 0x55.
	 * 
	 * @param message1
	 *            is the first byte of the message to pad
	 * @param message2
	 *            is the second byte of the message to pad
	 * @return is the padded message
	 */
	private byte[] padMessage(int message1, int message2) {
		byte[] paddedMessage = { (byte) message1, (byte) message2, 0x55 & 0x55 };
		return paddedMessage;
	}

	/**
	 * This function constructs a three-byte command to switch on a given group
	 * of lights. This array is ready to be sent to the WiFi box.
	 * 
	 * @param group
	 *            is the group of lights to switch on
	 * @throws IllegalArgumentException
	 *             if the group number is not between 1 and 4
	 * @return the message array to send to the WiFi box
	 */
	private byte[] getSwitchOnCommand(int group)
			throws IllegalArgumentException {
		switch (group) {
		case 1:
			return padMessage(COMMAND_GROUP_1_ON);
		case 2:
			return padMessage(COMMAND_GROUP_2_ON);
		case 3:
			return padMessage(COMMAND_GROUP_3_ON);
		case 4:
			return padMessage(COMMAND_GROUP_4_ON);
		default:
			throw new IllegalArgumentException(
					"The group number must be between 1 and 4");
		}
	}

	/**
	 * This function constructs a three-byte command to switch off a given group
	 * of lights. This array is ready to be sent to the WiFi box.
	 * 
	 * @param group
	 *            is the group of lights to switch off
	 * @throws IllegalArgumentException
	 *             if the group number is not between 1 and 4
	 * @return the message array to send to the WiFi box
	 */
	private byte[] getSwitchOffCommand(int group)
			throws IllegalArgumentException {
		switch (group) {
		case 1:
			return padMessage(COMMAND_GROUP_1_OFF);
		case 2:
			return padMessage(COMMAND_GROUP_2_OFF);
		case 3:
			return padMessage(COMMAND_GROUP_3_OFF);
		case 4:
			return padMessage(COMMAND_GROUP_4_OFF);
		default:
			throw new IllegalArgumentException(
					"The group number must be between 1 and 4");
		}
	}

	/**
	 * This function constructs a three-byte command to switch a given group of
	 * lights to the white mode. This array is ready to be sent to the WiFi box.
	 * 
	 * @param group
	 *            is the group of lights to switch to the white mode
	 * @throws IllegalArgumentException
	 *             if the group number is not between 1 and 4
	 * @return the message array to send to the WiFi box
	 */
	private byte[] getWhiteModeCommand(int group)
			throws IllegalArgumentException {
		switch (group) {
		case 1:
			return padMessage(COMMAND_GROUP_1_WHITE);
		case 2:
			return padMessage(COMMAND_GROUP_2_WHITE);
		case 3:
			return padMessage(COMMAND_GROUP_3_WHITE);
		case 4:
			return padMessage(COMMAND_GROUP_4_WHITE);
		default:
			throw new IllegalArgumentException(
					"The group number must be between 1 and 4");
		}
	}

	/**
	 * This function constructs a three-byte command to change the hue of a
	 * light to a given color
	 * 
	 * @param value
	 *            the color value (between MilightColor.MIN_COLOR and
	 *            MilightColor.MAX_COLOR)
	 * @throws IllegalArgumentException
	 *             if the color value is not between MilightColor.MIN_COLOR and
	 *             MilightColor.MAX_COLOR
	 * @return the message array to send to the WiFi box
	 */
	private byte[] getColorCommand(int value) throws IllegalArgumentException {
		// check argument
		if (value < MilightColor.MIN_COLOR || value > MilightColor.MAX_COLOR) {
			throw new IllegalArgumentException(
					"The color value should be between MilightColor.MIN_COLOR and MilightColor.MAX_COLOR");
		}

		// send message to the WiFi box
		return padMessage(COMMAND_COLOR, value);
	}

	/**
	 * This function sends an one-byte control message to the WiFi box. The
	 * message is padded with 0x00 0x55 as given in the documentation.
	 * 
	 * @param message
	 *            is the message code to send
	 * @throws IOException
	 *             if the message could not be sent
	 */
	private void sendMessage(int message) throws IOException {
		// pad the message with 0x00 0x55
		byte[] paddedMessage = padMessage(message);

		// send the padded message
		sendMessage(paddedMessage);
	}

	/**
	 * This function sends a two-byte control message to the WiFi box. The
	 * message is padded with 0x55 as given in the documentation.
	 * 
	 * @param message1
	 *            is the first byte of the message to send
	 * @param message2
	 *            is the second byte of the message to send
	 * @throws IOException
	 *             if the message could not be sent
	 */
	private void sendMessage(int message1, int message2) throws IOException {
		// pad the message with 0x55
		byte[] paddedMessage = padMessage(message1, message2);

		// send the padded message
		sendMessage(paddedMessage);
	}

	/**
	 * This function sends multiple three-byte messages to the WiFi box. All
	 * elements of the message array should be byte arrays with three elements.
	 * Note that the messages are sent in a new thread. Therefore, you should
	 * not send other commands directly after executing this one. Also, there
	 * are no exceptions when sending messages fails since they occur in another
	 * thread.
	 * 
	 * @param messages
	 *            is the messages to send (in order)
	 * @param sleep
	 *            is the time to wait between two message in milliseconds
	 * @throws IllegalArgumentException
	 *             if some of the messages in the array don't consist of exactly
	 *             three bytes
	 */
	private void sendMultipleMessages(final byte[][] messages, final long sleep)
			throws IllegalArgumentException {
		// check arguments
		for (int i = 0; i < messages.length; i++) {
			if (messages[i].length != 3) {
				throw new IllegalArgumentException(
						"All messages should consist of three bytes.");
			}
		}

		// start new thread
		new Thread(new Runnable() {
			public void run() {
				try {
					for (byte[] message : messages) {
						WiFiBox.this.sendMessage(message);
						Thread.sleep(sleep);
					}
				} catch (IOException e) {
					// if the message could not be sent
				} catch (InterruptedException e) {
					// if the thread could not sleep
				}
			}
		}).start();
	}

	/**
	 * This function sends multiple one-byte messages to the WiFi box. All of
	 * the are padded with the corresponding bytes. Note that the messages are
	 * sent in a new thread. Therefore, you should not send other commands
	 * directly after executing this one. Also, there are no exceptions when
	 * sending messages fails since they occur in another thread.
	 * 
	 * @param messages
	 *            is the messages to send (in order)
	 * @param sleep
	 *            is the time to wait between two message in milliseconds
	 */
	private void sendMultipleMessages(final int[] messages, final long sleep) {
		// pad messages
		byte[][] paddedMessages = new byte[messages.length][3];
		for (int i = 0; i < messages.length; i++) {
			paddedMessages[i] = padMessage(messages[i]);
		}

		// send the padded messages
		sendMultipleMessages(paddedMessages, sleep);
	}

	/**
	 * Switch all lights off (all groups).
	 * 
	 * @throws IOException
	 *             if the message could not be sent
	 */
	public void off() throws IOException {
		sendMessage(COMMAND_ALL_OFF);
	}

	/**
	 * Switch all lights of a particular group off.
	 * 
	 * @param group
	 *            the group to switch of (between 1 and 4)
	 * @throws IOException
	 *             if the message could not be sent
	 * @throws IllegalArgumentException
	 *             if the group number is not between 1 and 4
	 */
	public void off(int group) throws IOException, IllegalArgumentException {
		sendMessage(getSwitchOffCommand(group));
	}

	/**
	 * Switch all lights on (all groups).
	 * 
	 * @throws IOException
	 *             if the message could not be sent
	 */
	public void on() throws IOException {
		sendMessage(COMMAND_ALL_ON);
	}

	/**
	 * Switch all lights of a particular group on.
	 * 
	 * @param group
	 *            the group to switch of (between 1 and 4)
	 * @throws IOException
	 *             if the message could not be sent
	 * @throws IllegalArgumentException
	 *             if the group number is not between 1 and 4
	 */
	public void on(int group) throws IOException, IllegalArgumentException {
		sendMessage(getSwitchOnCommand(group));
	}

	/**
	 * Switch all lights in all groups to the white mode. Note that the messages
	 * are sent in a new thread. Therefore, you should not send other commands
	 * directly after executing this one. Also, there are no exceptions when
	 * sending messages fails since they occur in another thread.
	 */
	public void white() {
		int[] messages = { COMMAND_ALL_ON, COMMAND_ALL_WHITE };
		sendMultipleMessages(messages, MIN_SLEEP_BETWEEN_MESSAGES);
	}

	/**
	 * Switch all lights in a particular group to the white mode. Note that the
	 * messages are sent in a new thread. Therefore, you should not send other
	 * commands directly after executing this one. Also, there are no exceptions
	 * when sending messages fails since they occur in another thread.
	 * 
	 * @param group
	 *            the group to switch of (between 1 and 4)
	 * @throws IllegalArgumentException
	 *             if the group number is not between 1 and 4
	 */
	public void white(int group) throws IllegalArgumentException {
		// create message array
		byte[][] messages = new byte[2][3];

		// switch on first
		messages[0] = getSwitchOnCommand(group);

		// switch to white mode
		messages[1] = getWhiteModeCommand(group);

		// send messages
		sendMultipleMessages(messages, MIN_SLEEP_BETWEEN_MESSAGES);
	}

	/**
	 * Trigger the disco mode for the active group of lights (the last one that
	 * was switched on, see {@link WiFiBox#getActiveGroup()}).
	 * 
	 * @throws IOException
	 *             if the message could not be sent
	 */
	public void discoMode() throws IOException {
		sendMessage(COMMAND_DISCO);
	}

	/**
	 * Triggers the disco mode for a particular group of lights. The lights will
	 * be switched on before to activate them.Note that the messages are sent in
	 * a new thread. Therefore, you should not send other commands directly
	 * after executing this one. Also, there are no exceptions when sending
	 * messages fails since they occur in another thread.
	 * 
	 * @param group
	 *            the group to switch of (between 1 and 4)
	 * @throws IllegalArgumentException
	 *             if the group number is not between 1 and 4
	 */
	public void discoMode(int group) throws IllegalArgumentException {
		// create message array
		byte[][] messages = new byte[2][3];

		// switch on first
		messages[0] = getSwitchOnCommand(group);

		// start disco mode
		messages[1] = padMessage(COMMAND_DISCO);

		// send messages
		sendMultipleMessages(messages, MIN_SLEEP_BETWEEN_MESSAGES);
	}

	/**
	 * Increase the disco mode's speed for the active group of lights (the last
	 * one that was switched on, see {@link WiFiBox#getActiveGroup()}).
	 * 
	 * @throws IOException
	 *             if the message could not be sent
	 */
	public void discoModeFaster() throws IOException {
		sendMessage(COMMAND_DISCO_FASTER);
	}

	/**
	 * Decrease the disco mode's speed for the active group of lights (the last
	 * one that was switched on, see {@link WiFiBox#getActiveGroup()}).
	 * 
	 * @throws IOException
	 *             if the message could not be sent
	 */
	public void discoModeSlower() throws IOException {
		sendMessage(COMMAND_DISCO_SLOWER);
	}

	/**
	 * Set the brightness value for the currently active group of lights (the
	 * last one that was switched on, see {@link WiFiBox#getActiveGroup()}).
	 * 
	 * @param value
	 *            is the brightness value to set (between
	 *            MilightColor.MIN_BRIGHTNESS and MilightColor.MAX_BRIGHTNESS)
	 * @throws IOException
	 *             if the message could not be sent
	 * @throws IllegalArgumentException
	 *             if the brightness value is not between
	 *             MilightColor.MIN_BRIGHTNESS and MilightColor.MAX_BRIGHTNESS
	 */
	public void brightness(int value) throws IOException,
			IllegalArgumentException {
		// check argument
		if (value < MilightColor.MIN_BRIGHTNESS
				|| value > MilightColor.MAX_BRIGHTNESS) {
			throw new IllegalArgumentException(
					"The brightness value should be between MilightColor.MIN_BRIGHTNESS and MilightColor.MAX_BRIGHTNESS");
		}

		// send message to the WiFi box
		sendMessage(COMMAND_BRIGHTNESS, value);
	}

	/**
	 * Set the brightness value for a given group of lights.
	 * 
	 * @param group
	 *            is the number of the group to set the brightness for
	 * @param value
	 *            is the brightness value to set (between
	 *            MilightColor.MIN_BRIGHTNESS and MilightColor.MAX_BRIGHTNESS)
	 * @throws IOException
	 *             if the message could not be sent
	 * @throws IllegalArgumentException
	 *             if group is not between 1 and 4 or the brightness value is
	 *             not between MilightColor.MIN_BRIGHTNESS and
	 *             MilightColor.MAX_BRIGHTNESS
	 */
	public void brightness(int group, int value) throws IOException,
			IllegalArgumentException {
		// check arguments
		if (value < MilightColor.MIN_BRIGHTNESS
				|| value > MilightColor.MAX_BRIGHTNESS) {
			throw new IllegalArgumentException(
					"The brightness value should be between MilightColor.MIN_BRIGHTNESS and MilightColor.MAX_BRIGHTNESS");
		}

		// create message array
		byte[][] messages = new byte[2][3];

		// switch on first
		messages[0] = getSwitchOnCommand(group);

		// adjust brightness
		messages[1] = padMessage(COMMAND_BRIGHTNESS, value);

		// send messages
		sendMultipleMessages(messages, MIN_SLEEP_BETWEEN_MESSAGES);
	}

	/**
	 * Set the color value for the currently active group of lights (the last
	 * one that was switched on, see {@link WiFiBox#getActiveGroup()}).
	 * 
	 * @param value
	 *            is the color value to set (between MilightColor.MIN_COLOR and
	 *            MilightColor.MAX_COLOR)
	 * @throws IOException
	 *             if the message could not be sent
	 * @throws IllegalArgumentException
	 *             if the color value is not between MilightColor.MIN_COLOR and
	 *             MilightColor.MAX_COLOR
	 */
	public void color(int value) throws IOException, IllegalArgumentException {
		// send message to the WiFi box
		sendMessage(getColorCommand(value));
	}

	/**
	 * Set the color value for the currently active group of lights (the last
	 * one that was switched on, see {@link WiFiBox#getActiveGroup()}).
	 * 
	 * @param color
	 *            is the color to set
	 * @param forceColoredMode
	 *            true if all colors should be displayed in colored mode, false
	 *            to use white mode for colors with low saturation and else
	 *            colored mode
	 * @throws IOException
	 *             if the message could not be sent
	 */
	public void color(MilightColor color, boolean forceColoredMode)
			throws IOException {
		if (color.isColoredMode() || forceColoredMode) {
			// colored mode
			color(color.getMilightHue());
		} else {
			// white mode
			white();
		}
	}

	/**
	 * Set the color value for the currently active group of lights (the last
	 * one that was switched on, see {@link WiFiBox#getActiveGroup()}). Colors
	 * with low saturation will be displayed in white mode for a better result.
	 * 
	 * @param color
	 *            is the color to set
	 * @throws IOException
	 *             if the message could not be sent
	 */
	public void color(MilightColor color) throws IOException {
		color(color, false);
	}

	/**
	 * Set the color value for the currently active group of lights (the last
	 * one that was switched on, see {@link WiFiBox#getActiveGroup()}).
	 * 
	 * @param color
	 *            is the color to set
	 * @param forceColoredMode
	 *            true if all colors should be displayed in colored mode, false
	 *            to use white mode for colors with low saturation and else
	 *            colored mode
	 * @throws IOException
	 *             if the message could not be sent
	 */
	public void color(Color color, boolean forceColoredMode) throws IOException {
		color(new MilightColor(color), forceColoredMode);
	}

	/**
	 * Set the color value for the currently active group of lights (the last
	 * one that was switched on, see {@link WiFiBox#getActiveGroup()}). Colors
	 * with low saturation will be displayed in white mode for a better result.
	 * 
	 * @param color
	 *            is the color to set
	 * @throws IOException
	 *             if the message could not be sent
	 */
	public void color(Color color) throws IOException {
		color(new MilightColor(color));
	}

	/**
	 * Set the color value for a given group of lights.
	 * 
	 * @param group
	 *            is the number of the group to set the color for
	 * @param value
	 *            is the color value to set (between MilightColor.MIN_COLOR and
	 *            MilightColor.MAX_COLOR)
	 * @throws IOException
	 *             if the message could not be sent
	 * @throws IllegalArgumentException
	 *             if group is not between 1 and 4 or the color value is not
	 *             between MilightColor.MIN_COLOR and MilightColor.MAX_COLOR
	 */
	public void color(int group, int value) throws IOException,
			IllegalArgumentException {
		// create message array
		byte[][] messages = new byte[2][3];

		// switch on first
		messages[0] = getSwitchOnCommand(group);

		// adjust color
		messages[1] = getColorCommand(value);

		// send messages
		sendMultipleMessages(messages, MIN_SLEEP_BETWEEN_MESSAGES);
	}

	/**
	 * Set the color value for a given group of lights.
	 * 
	 * @param group
	 *            is the number of the group to set the color for
	 * @param color
	 *            is the color to set
	 * @param forceColoredMode
	 *            true if all colors should be displayed in colored mode, false
	 *            to use white mode for colors with low saturation and else
	 *            colored mode
	 * @throws IOException
	 *             if the message could not be sent
	 * @throws IllegalArgumentException
	 *             if group is not between 1 and 4
	 */
	public void color(int group, MilightColor color, boolean forceColoredMode)
			throws IOException, IllegalArgumentException {
		if (color.isColoredMode() || forceColoredMode) {
			// colored mode
			color(group, color.getMilightHue());
		} else {
			// white mode
			white(group);
		}
	}

	/**
	 * Set the color value for a given group of lights. Colors with low
	 * saturation will be displayed in white mode for a better result.
	 * 
	 * @param group
	 *            is the number of the group to set the color for
	 * @param color
	 *            is the color to set
	 * @throws IOException
	 *             if the message could not be sent
	 * @throws IllegalArgumentException
	 *             if group is not between 1 and 4
	 */
	public void color(int group, MilightColor color) throws IOException,
			IllegalArgumentException {
		color(group, color, false);
	}

	/**
	 * Set the color value for a given group of lights.
	 * 
	 * @param group
	 *            is the number of the group to set the color for
	 * @param color
	 *            is the color to set
	 * @param forceColoredMode
	 *            true if all colors should be displayed in colored mode, false
	 *            to use white mode for colors with low saturation and else
	 *            colored mode
	 * @throws IOException
	 *             if the message could not be sent
	 * @throws IllegalArgumentException
	 *             if group is not between 1 and 4
	 */
	public void color(int group, Color color, boolean forceColoredMode)
			throws IOException, IllegalArgumentException {
		color(group, new MilightColor(color), forceColoredMode);
	}

	/**
	 * Set the color value for a given group of lights. Colors with low
	 * saturation will be displayed in white mode for a better result.
	 * 
	 * @param group
	 *            is the number of the group to set the color for
	 * @param color
	 *            is the color to set
	 * @throws IOException
	 *             if the message could not be sent
	 * @throws IllegalArgumentException
	 *             if group is not between 1 and 4
	 */
	public void color(int group, Color color) throws IOException,
			IllegalArgumentException {
		color(group, new MilightColor(color));
	}

	/**
	 * Set the color and brightness values for the currently active group of
	 * lights (the last one that was switched on, see
	 * {@link WiFiBox#getActiveGroup()}). Both values are extracted from the
	 * color given to the function by transforming it to an HSB color.
	 * 
	 * @param color
	 *            is the color to extract hue and brightness from
	 */
	public void colorAndBrightness(MilightColor color) {
		// create message array
		byte[][] messages = new byte[2][3];

		// adjust color
		messages[0] = getColorCommand(color.getMilightHue());

		// adjust brightness
		messages[1] = padMessage(COMMAND_BRIGHTNESS,
				color.getMilightBrightness());

		// send messages
		sendMultipleMessages(messages, MIN_SLEEP_BETWEEN_MESSAGES);
	}

	/**
	 * Set the color and brightness values for the currently active group of
	 * lights (the last one that was switched on, see
	 * {@link WiFiBox#getActiveGroup()}). Both values are extracted from the
	 * color given to the function by transforming it to an HSB color.
	 * 
	 * @param color
	 *            is the color to extract hue and brightness from
	 */
	public void colorAndBrightness(Color color) {
		colorAndBrightness(new MilightColor(color));
	}

	/**
	 * Set the color and brightness values for a given group of lights. Both
	 * values are extracted from the color given to the function by transforming
	 * it to an HSB color.
	 * 
	 * @param group
	 *            is the number of the group to set the color for
	 * @param color
	 *            is the color to extract hue and brightness from
	 * @param forceColoredMode
	 *            true if all colors should be displayed in colored mode, false
	 *            to use white mode for colors with low saturation and else
	 *            colored mode
	 * @throws IllegalArgumentException
	 *             if group is not between 1 and 4
	 */
	public void colorAndBrightness(int group, MilightColor color,
			boolean forceColoredMode) {
		// create message array
		byte[][] messages = new byte[3][3];

		// switch on first
		messages[0] = getSwitchOnCommand(group);

		// adjust color
		if (color.isColoredMode() || forceColoredMode) {
			// colored mode
			messages[1] = getColorCommand(color.getMilightHue());
		} else {
			// white mode
			messages[1] = getWhiteModeCommand(group);
		}

		// adjust brightness
		messages[2] = padMessage(COMMAND_BRIGHTNESS,
				color.getMilightBrightness());

		// send messages
		sendMultipleMessages(messages, MIN_SLEEP_BETWEEN_MESSAGES);
	}

	/**
	 * Set the color and brightness values for a given group of lights. Both
	 * values are extracted from the color given to the function by transforming
	 * it to an HSB color. Colors with low saturation will be displayed in white
	 * mode for a better result.
	 * 
	 * @param group
	 *            is the number of the group to set the color for
	 * @param color
	 *            is the color to extract hue and brightness from
	 * @throws IllegalArgumentException
	 *             if group is not between 1 and 4
	 */
	public void colorAndBrightness(int group, MilightColor color) {
		colorAndBrightness(group, color, false);
	}

	/**
	 * Set the color and brightness values for a given group of lights. Both
	 * values are extracted from the color given to the function by transforming
	 * it to an HSB color.
	 * 
	 * @param group
	 *            is the number of the group to set the color for
	 * @param color
	 *            is the color to extract hue and brightness from
	 * @param forceColoredMode
	 *            true if all colors should be displayed in colored mode, false
	 *            to use white mode for colors with low saturation and else
	 *            colored mode
	 * @throws IllegalArgumentException
	 *             if group is not between 1 and 4
	 */
	public void colorAndBrightness(int group, Color color,
			boolean forceColoredMode) {
		colorAndBrightness(group, new MilightColor(color), forceColoredMode);
	}

	/**
	 * Set the color and brightness values for a given group of lights. Both
	 * values are extracted from the color given to the function by transforming
	 * it to an HSB color. Colors with low saturation will be displayed in white
	 * mode for a better result.
	 * 
	 * @param group
	 *            is the number of the group to set the color for
	 * @param color
	 *            is the color to extract hue and brightness from
	 * @throws IllegalArgumentException
	 *             if group is not between 1 and 4
	 */
	public void colorAndBrightness(int group, Color color) {
		colorAndBrightness(group, new MilightColor(color));
	}

	/**
	 * Use this function to add a new listener one group of lights connected to
	 * the WiFiBox. Listeners will be notified when the group of lights is
	 * switched on or off, color or brightness change, white or disco mode is
	 * activated or disco mode is set faster or slower.
	 * 
	 * @param group
	 *            is the number of the group to add the listener to
	 * @param listener
	 *            is the listener to add
	 * @throws IllegalArgumentException
	 *             if group is not between 1 and 4
	 */
	public void addLightListener(int group, LightListener listener) {
		// check group number
		if (1 > group || group > 4) {
			throw new IllegalArgumentException(
					"The group number must be between 1 and 4");
		}

		// add listener
		lightListeners[group - 1].add(listener);
	}

	/**
	 * This function removes a listener from this WiFiBox which was added before
	 * by {@link WiFiBox#addLightListener(int, LightListener)}.
	 * 
	 * @param group
	 *            is the number of the group to remove the listener from
	 * @param listener
	 *            is the listener to remove
	 * @throws IllegalArgumentException
	 *             if group is not between 1 and 4
	 */
	public void removeLightListener(int group, LightListener listener) {
		// check group number
		if (1 > group || group > 4) {
			throw new IllegalArgumentException(
					"The group number must be between 1 and 4");
		}

		// remove listener
		lightListeners[group - 1].remove(listener);
	}

	/**
	 * This function sends a LightEvent to all listeners listening on a certain
	 * group of lights.
	 * 
	 * @param group
	 *            is the number of the group to notify
	 * @param event
	 *            is the LightEvent to send to all listeners
	 * @throws IllegalArgumentException
	 *             if group is not between 1 and 4
	 */
	private void notifyLightListeners(int group, LightEvent event) {
		// check group number
		if (1 > group || group > 4) {
			throw new IllegalArgumentException(
					"The group number must be between 1 and 4");
		}

		// notify listeners
		for (LightListener listener : lightListeners[group - 1]) {
			listener.lightsChanged(event);
		}
	}

	/**
	 * This function sends a LightEvent to all listeners listening on a certain
	 * group of lights. The event's type and the group of lights receiving the
	 * message is obtained from the raw message sent to the WiFiBox.
	 * 
	 * @param message
	 *            is the raw message sent to the WiFiBox
	 */
	private void notifyLightListeners(byte[] message) {
		switch ((int) message[0]) {
		// switch off commands
		case COMMAND_ALL_OFF:
			for (int group = 1; group <= 4; group++) {
				notifyLightListeners(group,
						new SwitchOffEvent(getLights(group)));
			}
			break;
		case COMMAND_GROUP_1_OFF:
			notifyLightListeners(1, new SwitchOffEvent(getLights(1)));
			break;
		case COMMAND_GROUP_2_OFF:
			notifyLightListeners(2, new SwitchOffEvent(getLights(2)));
			break;
		case COMMAND_GROUP_3_OFF:
			notifyLightListeners(3, new SwitchOffEvent(getLights(3)));
			break;
		case COMMAND_GROUP_4_OFF:
			notifyLightListeners(4, new SwitchOffEvent(getLights(4)));
			break;
		// switch on commands
		case COMMAND_ALL_ON:
			for (int group = 1; group <= 4; group++) {
				notifyLightListeners(group, new SwitchOnEvent(getLights(group)));
			}
			break;
		case COMMAND_GROUP_1_ON:
			notifyLightListeners(1, new SwitchOnEvent(getLights(1)));
			break;
		case COMMAND_GROUP_2_ON:
			notifyLightListeners(2, new SwitchOnEvent(getLights(2)));
			break;
		case COMMAND_GROUP_3_ON:
			notifyLightListeners(3, new SwitchOnEvent(getLights(3)));
			break;
		case COMMAND_GROUP_4_ON:
			notifyLightListeners(4, new SwitchOnEvent(getLights(4)));
			break;
		// white mode commands
		case COMMAND_ALL_WHITE:
			for (int group = 1; group <= 4; group++) {
				notifyLightListeners(group,
						new WhiteModeEvent(getLights(group)));
			}
			break;
		case COMMAND_GROUP_1_WHITE:
			notifyLightListeners(1, new WhiteModeEvent(getLights(1)));
			break;
		case COMMAND_GROUP_2_WHITE:
			notifyLightListeners(2, new WhiteModeEvent(getLights(2)));
			break;
		case COMMAND_GROUP_3_WHITE:
			notifyLightListeners(3, new WhiteModeEvent(getLights(3)));
			break;
		case COMMAND_GROUP_4_WHITE:
			notifyLightListeners(4, new WhiteModeEvent(getLights(4)));
			break;
		// disco mode commands
		case COMMAND_DISCO:
			notifyLightListeners(getActiveGroup(), new DiscoModeEvent(
					getLights(getActiveGroup())));
			break;
		case COMMAND_DISCO_FASTER:
			notifyLightListeners(getActiveGroup(), new DiscoModeFasterEvent(
					getLights(getActiveGroup())));
			break;
		case COMMAND_DISCO_SLOWER:
			notifyLightListeners(getActiveGroup(), new DiscoModeSlowerEvent(
					getLights(getActiveGroup())));
			break;
		// change color commands
		case COMMAND_COLOR:
			MilightColor color = new MilightColor(Color.WHITE);
			color.setMilightHue(message[1]);
			notifyLightListeners(getActiveGroup(), new ColoredModeEvent(
					getLights(getActiveGroup())));
			notifyLightListeners(getActiveGroup(), new ChangeColorEvent(
					getLights(getActiveGroup()), color));
			break;
		// change brightness commands
		case COMMAND_BRIGHTNESS:
			MilightColor color2 = new MilightColor(Color.WHITE);
			color2.setMilightBrightness(message[1]);
			notifyLightListeners(getActiveGroup(), new ChangeBrightnessEvent(
					getLights(getActiveGroup()), color2.getBrightness()));
			break;
		}
	}

	/**
	 * This function returns the number of the currently active group of lights.
	 * 
	 * @return the number of the currently active group of lights
	 */
	public int getActiveGroup() {
		return activeGroup;
	}

	/**
	 * This function describes the objet as a string. Use this for debugging.
	 * 
	 * @returns a string description of the instance
	 */
	public String toString() {
		return String.format("[WiFiBox, address: %s, port: %d, activeGroup: %d]",
				address.toString(), port, activeGroup);
	}
}