/*
 * DeviceS20.java
 * 
 * This class was written by Jens Schröder.
 * Webpage: https://server47.de
 * 
 * You may use this code in your own - non commercial - code as long as you don't remove this header. 
 */


import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InterfaceAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Observable;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.bind.DatatypeConverter;



public class DeviceS20
{    
    protected static final String broadcastAddress = "255.255.255.255";    
    protected final static int s20Port = 10000;
    protected final static String magicKey = "6864";
    protected final static int queueTTL = 2; // Incoming packets will be removed after x seconds if they are not taken
    protected final static int maxSleepTime = 4000;    // Maximum total waiting time in milliseconds
    protected final static int sleepStep = 10;    // Time between checks if response has arrived in milliseconds
    protected final static int querySwitchTollerance = 30;    // seconds
    protected final static int switchAttempts = 3;
    
    protected InetAddress ipAddress;
    protected String macAddress;
    
    protected static long timestampLastDiscovery;
    
    protected static boolean stopRequested = false;
    protected static boolean listenerRunning = false;
    
    protected static CopyOnWriteArrayList queue = new CopyOnWriteArrayList();
//    protected static Listener listener;
    protected static Thread listenerThread;
    
    static DatagramSocket datagramSendSocket = null;

    protected static synchronized CopyOnWriteArrayList getQueue()
    {
        return queue;
    }

    public void setIpAddress(InetAddress ipAddress)
    {
        this.ipAddress = ipAddress;
    }

    public InetAddress getIpAddress()
    {
        return ipAddress;
    }

    String name;
    public boolean state;

    public String getName()
    {
        return name;
    }
    public void setName(String name)
    {
        this.name = name;
    }
    public String getMacAddress()
    {
        return macAddress;
    }
    public void setMacAddress(String macAddress)
    {
        this.macAddress = macAddress.toLowerCase();
    }
    public boolean getState()
    {
        return state;
    }
    public void setState(boolean state)
    {
        this.state = state;
    }
    
    public static DeviceS20 getByIpAddress(String singleIpAddress) throws UnknownHostException
    {
        InetAddress singleAddress = InetAddress.getByName(singleIpAddress);
        return queryDevice(singleAddress).get(0);
    }
    
    public String getReverseMacAddress()
    {
        String mac = this.getMacAddress();
        StringBuilder revMac = new StringBuilder();
        int pos = mac.length() - 2;
//        ACCF238342EE
        do
        {
            revMac.append(mac.substring(pos, pos+2));
            pos = pos - 2;
        }
        while(pos >= 0);
        
//        log("Reverse Mac: " + revMac.toString().toLowerCase());
        return revMac.toString().toLowerCase();
    }

    public static ArrayList discoverAllDevices()
    {        
        // TODO: Replace by a more generic address:
        
        try
        {
//            for (InetAddress bcAddress : listOfBroadcasts)
//            {
//                https://en.wikipedia.org/wiki/Broadcast_address
                InetAddress broadcastAddressForUse = InetAddress.getByName(broadcastAddress);
//                InetAddress broadcastAddressForUse = InetAddress.getByName(bcAddress);
//            }
            return queryDevice(broadcastAddressForUse);
        }
        catch(UnknownHostException e)
        {
            return null;
        }
    }
    
    protected static ArrayList queryDevice(InetAddress singleIpOrBroadcastAddress)
    {
        /*
         * get1() and queryDevice() send the same data out. Just for performance reasons get1() gives a shit.
         * If real data is required queryDevice() has to be used.
         */

        
        /*
         * Gerät einstecken, blinkt jetzt schnell rot
         * Taste gedrückt halten, blinkt jetzt schnell blau
         * Gerät hat jetzt einen Access Point aufgemacht, da reingehen (siehe Bilder)
         * Jetzt in die Anwendung -> Wifi Config -> fertig
         */

        
        /*
         * Send this to 255.255.255.255
         * 68:64:00:06:71:61
         */

        
        log("Sending discovery to " + singleIpOrBroadcastAddress.toString(), 5);
        
        verifyListenerIsRunning();
                
        final String discoverCommand = "00067161";
        
//        DatagramSocket datagramSendSocket = null;
                
        try
        {
            byte[] buffer = hexStringToByteArray(magicKey + discoverCommand);
            
            DatagramPacket packet = new DatagramPacket(buffer, buffer.length, singleIpOrBroadcastAddress, s20Port);
//            datagramSendSocket = new DatagramSocket();
//            datagramSendSocket.send(packet);
            getDatagramSendSocket().send(packet);
            
            /*
             * Als Antwort kommt jetzt:
             * Aus:
             * 6864002A716100ACCF2383BEE6202020202020E6BE8323CFAC202020202020534F43303035ADB029DA00
             * An:                                                                         ---------- Unterschiede
             * 6864002A716100ACCF2383BEE6202020202020E6BE8323CFAC202020202020534F4330303572B129DA01
             * 
             * 68:64:00:2a:71:61:00:ac:cf:23:83:be:e6:20:20:20:20:20:20:e6:be:83:23:cf:ac:20:20:20:20:20:20:53:4f:43:30:30:35:e6:f5:28:da:00
             * 68:64:00:2A:71:61:00:AC:CF:23:83:BE:E6:20:20:20:20:20:20:E6:BE:83:23:CF:AC:20:20:20:20:20:20:53:4F:43:30:30:35:BE:41:3E:DA:00
             * ----- Magic Key
             *       -- Message length
             *          ----------- ? (evtl. Command code)
             *                      ------------------ MAC address
             *                                         ----------------- MAC padding
             *                                                          ------------------ reverse mac (blocks, not characters)
             *                                                                mac padding -----------------
             *                                                                                                ------------------------------- ?
             *                                                                                                                               - State on/off
             */

            
            ArrayList respondingDevices = new ArrayList();
            
            Thread.sleep(maxSleepTime);
            
            Iterator iter = getQueue().iterator();
            while(iter.hasNext())
//            int queueSize = getQueue().size();
//            for(int i=0; i < queueSize; i++)
            {
//                QueuePacket currentPacket = getQueue().get(i);
                QueuePacket currentPacket = iter.next();
                                
                if(currentPacket.getSender().equals(singleIpOrBroadcastAddress) && currentPacket.getValue().length() >= "6864002A716100ACCF2383BEE6202020202020E6BE8323CFAC202020202020534F43303035ADB029DA00".length())
                {                
                    currentPacket.takeFromQueue();
                    DeviceS20 newDevice = new DeviceS20();
                    newDevice.setIpAddress(currentPacket.sender);
                    String macAddress = currentPacket.getValue().substring(14, 14+12);
                    newDevice.setMacAddress(macAddress);
                    newDevice.setIpAddress(currentPacket.getSender());
                    
                    boolean state = false;
//                    log(currentPacket.getValue().substring(83, 84));
                    if(Integer.parseInt(currentPacket.getValue().substring(83, 84)) == 1)
                        state = true;
                    
                    newDevice.setState(state);
                                        
                    boolean found = false;
                    for(DeviceS20 oneDevice : respondingDevices)
                    {
                        if(oneDevice.getMacAddress().equalsIgnoreCase(macAddress))
                        {
                            found = true;
                            break;
                        }
                    }
                    
                    if(!found)
                        respondingDevices.add(newDevice);
                    
    //                686400067161
    //                6864002A716100
    //                ACCF238342EE
                    
                    // Compensate for the packet being taken from the queue
//                    i--;
//                    queueSize--;
                }
                
            }
            
            ArrayList threads = new ArrayList();
            
            for(DeviceS20 respondedDevice : respondingDevices)
            {
                Thread t = new Thread(new DeviceQuerrier(respondedDevice));
                t.setName("Thread-DeviceQuerrier-respondedDevice-" + respondedDevice.getName());
                threads.add(t);
                t.start();
            }
            
            // Wait for all threads to finish.
            log("Waiting for all query threads to finish...", 5);
            for(Thread t : threads)
                t.join();
            log("All query threads finished.", 5);
            
//            datagramSendSocket.close();
            
            timestampLastDiscovery = Calendar.getInstance().getTimeInMillis();
            
            return respondingDevices;
        }
        catch(Exception e)
        {
            e.printStackTrace();
            return null;
        }
//        finally
//        {
//            try
//            {
//                datagramSendSocket.close();
//            }
//            catch(Exception e)
//            {}
//        }
    }
    
    protected static void queryDevice(DeviceS20 device)
    {
        try
        {
            get2(device);
        }
        catch (Exception e)
        {
            // give a shit
        }
        try
        {
            get3(device);
        }
        catch (Exception e)
        {
            // give a shit
        }
        try
        {
            get4(device);
        }
        catch (Exception e)
        {
            // give a shit
        }
        try
        {
            get5(device);
        }
        catch (Exception e)
        {
            // give a shit
        }
        try
        {
            get6(device);
        }
        catch (Exception e)
        {
            // give a shit
        }
        
        try
        {
            String response = get7DeviceName(device);
            device.setName(response);
        }
        catch (Exception e)
        {
            // give a shit
        }
        
        try
        {
            get8(device);
        }
        catch (Exception e)
        {
            // give a shit
        }
    }

    static class DeviceQuerrier implements Runnable
    {
        DeviceS20 device;
        
        public DeviceQuerrier(DeviceS20 device)
        {
            this.device = device;
        }

        @Override
        public void run()
        {
            if(this.device != null)
                queryDevice(this.device);
            else
                log("No device specified.", 5);
        }                
    }
    
    protected static synchronized DatagramSocket getDatagramSendSocket() throws SocketException
    {
        if(datagramSendSocket == null || datagramSendSocket.isClosed())
            datagramSendSocket = new DatagramSocket();
        
        return datagramSendSocket;
    }
    
    protected static void get1(DeviceS20 dev)
    {
        /*
         * get1() and queryDevice() send the same data out. Just for performance reasons get1() gives a shit.
         * If real data is required queryDevice() has to be used.
         */

        
        log("get1() for " + dev.toString(), 5);
                
        verifyListenerIsRunning();

        final String discoverCommand = "00067161";
        
//        DatagramSocket datagramSendSocket = null;
//        if(datagramSendSocket == null)
//            datagramSendSocket = null;
        
        try
        {
            log("Sending discovery to " + dev.getIpAddress().toString(), 4);
            
            byte[] buffer = hexStringToByteArray(magicKey + discoverCommand);
            
            DatagramPacket packet = new DatagramPacket(buffer, buffer.length, dev.getIpAddress(), s20Port);
//            datagramSendSocket = new DatagramSocket();
//            datagramSendSocket.send(packet);
            getDatagramSendSocket().send(packet);
            
            /*
             * Als Antwort kommt jetzt:
             * Aus:
             * 6864002A716100ACCF2383BEE6202020202020E6BE8323CFAC202020202020534F43303035ADB029DA00
             * An:                                                                         ---------- Unterschiede
             * 6864002A716100ACCF2383BEE6202020202020E6BE8323CFAC202020202020534F4330303572B129DA01
             * 
             * 68:64:00:2a:71:61:00:ac:cf:23:83:be:e6:20:20:20:20:20:20:e6:be:83:23:cf:ac:20:20:20:20:20:20:53:4f:43:30:30:35:e6:f5:28:da:00
             * 68:64:00:2A:71:61:00:AC:CF:23:83:BE:E6:20:20:20:20:20:20:E6:BE:83:23:CF:AC:20:20:20:20:20:20:53:4F:43:30:30:35:BE:41:3E:DA:00
             * ----- Magic Key
             *       -- Message length
             *          ----------- ? (evtl. Command code)
             *                      ------------------ MAC address
             *                                         ----------------- MAC padding
             *                                                          ------------------ reverse mac (blocks, not characters)
             *                                                                mac padding -----------------
             *                                                                                                ------------------------------- ?
             *                                                                                                                               - State on/off
             */

            
            int currentSleepTime = 0;
            
            do
            {
                Thread.sleep(sleepStep);
            
                Iterator iter = getQueue().iterator();
                while(iter.hasNext())
//                int queueSize = getQueue().size();
//                for(int i=0; i < queueSize; i++)
                {
//                    Miscellaneous.logEvent("Checking packet " + String.valueOf(i+1), 5);
                    
//                    QueuePacket currentPacket = getQueue().get(i);
                    QueuePacket currentPacket = iter.next();
                                    
                    if(currentPacket.getSender().equals(dev.getIpAddress()) && currentPacket.getValue().length() >= "6864002A716100ACCF2383BEE6202020202020E6BE8323CFAC202020202020534F43303035ADB029DA00".length())
                    {                
                        log("Found my packet.", 5);
                        currentPacket.takeFromQueue();

                        String macAddress = currentPacket.getValue().substring(14, 14+12);
                                                
                        if(dev.getMacAddress().equalsIgnoreCase(macAddress))
                            return;
                        
                        // Compensate for the packet being taken from the queue
//                        i--;
//                        queueSize--;
                    }
//                    else
//                        Miscellaneous.logEvent("Not my packet.", 5);
                }
                
                currentSleepTime += sleepStep;
            }
            while(currentSleepTime <= maxSleepTime);
            
            log("Expected packet not received.", 4);
        }
        catch(Exception e)
        {
            e.printStackTrace();
            return;
        }
//        finally
//        {
//            try
//            {
//                datagramSendSocket.close();
//            }
//            catch(Exception e)
//            {}
//        }
    }
    
    private static String get2(DeviceS20 dev) throws InterruptedException, IOException
    {
        log("get2() for " + dev.toString(), 5);
//        Send this as command 2:
//        68:64:00:1e:63:6c:ac:cf:23:83:42:ee:20:20:20:20:20:20:ee:42:83:23:cf:ac:20:20:20:20:20:20

//        Example response2: 6864001e636caccf238342ee202020202020ee428323cfac202020202020
        String discoverCommand2 = magicKey + "001e636c" + dev.getMacAddress() + "202020202020" + dev.getReverseMacAddress() + "202020202020";
        byte[] buffer2 = hexStringToByteArray(discoverCommand2);
        DatagramPacket packet = new DatagramPacket(buffer2, buffer2.length, dev.getIpAddress(), s20Port);
//        DatagramSocket datagramSendSocket = new DatagramSocket();
//        datagramSendSocket.send(packet);
        getDatagramSendSocket().send(packet);

        Thread.sleep(500);    // just to be on the save side
//        datagramSendSocket.close();
        return null;
        
        /*            
//            response 2:
//            68:64:00:2a:71:61:00:ac:cf:23:83:42:ee:20:20:20:20:20:20:ee:42:83:23:cf:ac:20:20:20:20:20:20:53:4f:43:30:30:35:c1:0e:61:da:00
//            6864002a716100accf238342ee202020202020ee428323cfac202020202020534f43303035c10e61da00;    //Windows-Programm
//            68640018636CACCF238342EE202020202020000000000000AC202020202020534F43303035937173DA00    //Meins
//            68:64:00:2a:71:61:00:ac:cf:23:83:42:ee:20:20:20:20:20:20:ee:42:83:23:cf:ac:20:20:20:20:20:20:53:4f:43:30:30:35:e6:d5:64:da:00
            String expectedResult = magicKey + "[a-zA-Z0-9]{10}" + dev.getMacAddress().toLowerCase() + "202020202020" + dev.getReverseMacAddress() + "202020202020" + "[a-zA-Z0-9]{12}";
            
            int currentSleepTime = 0;
            do
            {
                Thread.sleep(sleepStep);
                
                for(QueuePacket onePacket : getQueue())
                {
//                    log("Checking packet...");
                    if(onePacket.getSender().equals(dev.getIpAddress()))
                    {
                        if(onePacket.getValue().toLowerCase().matches(expectedResult.toLowerCase()))
                        {
                            log("Found packet...");
                            datagramSendSocket.close();
                            return onePacket.getValue();
                        }
                    }
                }
//                log("Done with packet check round...");
                
                currentSleepTime += sleepStep;
//                log("waiting loop");
            }
            while(currentSleepTime <= maxSleepTime);
            
            log("Couldn't find packet...");
            
            datagramSendSocket.close();
            return expectedResult;
            */

    }

    private static String get3(DeviceS20 respondedDevice) throws IOException, InterruptedException
    {
        log("get3() for " + respondedDevice.toString(), 5);
        
//        command 3: 68:64:00:1e:63:6c:ac:cf:23:83:42:ee:20:20:20:20:20:20:ee:42:83:23:cf:ac:20:20:20:20:20:20
//        6864001e636caccf238342ee202020202020ee428323cfac202020202020
        String command3 = magicKey + "001e636c" + respondedDevice.getMacAddress() + "202020202020" + respondedDevice.getReverseMacAddress() + "202020202020";
        byte[] buffer = hexStringToByteArray(command3);
        DatagramPacket packet = new DatagramPacket(buffer, buffer.length, respondedDevice.getIpAddress(), s20Port);
//        DatagramSocket datagramSendSocket = new DatagramSocket();
//        datagramSendSocket.send(packet);
        getDatagramSendSocket().send(packet);

        int currentSleepTime = 0;
        do
        {
            Thread.sleep(sleepStep);
            
            Iterator iter = getQueue().iterator();
            while(iter.hasNext())
//            for(QueuePacket response3 : getQueue())
            {
                QueuePacket response3 = iter.next();
                if(response3.getSender().equals(respondedDevice.getIpAddress()))
                {
//                    if(response3.getValue().toLowerCase().startsWith(expectedResult.toLowerCase()))
//                    {
//                    datagramSendSocket.close();
                    return null;
                }
            }
            
            currentSleepTime += sleepStep;
        }
        while(currentSleepTime <= maxSleepTime);
        
//        datagramSendSocket.close();
        return command3;
    }

    private static String get4(DeviceS20 respondedDevice) throws IOException, InterruptedException
    {
        log("get4() for " + respondedDevice.toString(), 5);
        
//        68:64:00:1e:63:6c:ac:cf:23:83:42:ee:20:20:20:20:20:20:ee:42:83:23:cf:ac:20:20:20:20:20:20
//        6864001e636caccf238342ee202020202020ee428323cfac202020202020
        String command4 = magicKey + "001e636c" + respondedDevice.getMacAddress() + "202020202020" + respondedDevice.getReverseMacAddress() + "202020202020";
        byte[] buffer = hexStringToByteArray(command4);
        DatagramPacket packet = new DatagramPacket(buffer, buffer.length, respondedDevice.getIpAddress(), s20Port);
//        DatagramSocket datagramSendSocket = new DatagramSocket();
//        datagramSendSocket.send(packet);
        getDatagramSendSocket().send(packet);
            
//        Example response 4: 68:64:00:2a:71:61:00:ac:cf:23:83:42:ee:20:20:20:20:20:20:ee:42:83:23:cf:ac:20:20:20:20:20:20:53:4f:43:30:30:35:e2:1a:61:da:00
//        6864002a716100accf238342ee202020202020ee428323cfac202020202020534f43303035e21a61da00
        
        String expectedResult = magicKey + "002a716100" + respondedDevice.getMacAddress() + "202020202020" + respondedDevice.getReverseMacAddress() + "202020202020";
        
        int currentSleepTime = 0;
        do
        {
            Thread.sleep(sleepStep);

            Iterator iter = getQueue().iterator();
            while(iter.hasNext())
//            for(QueuePacket response4 : getQueue())
            {
                QueuePacket response4 = iter.next();
                if(response4.getSender().equals(respondedDevice.getIpAddress()))
                {
                    if(response4.getValue().toLowerCase().startsWith(expectedResult.toLowerCase()))
                    {
//                        datagramSendSocket.close();
                        return null;
                    }
                }
            }
            
            currentSleepTime += sleepStep;
        }
        while(currentSleepTime <= maxSleepTime);

//        datagramSendSocket.close();
        return expectedResult;
    }

    private static String get5(DeviceS20 respondedDevice) throws InterruptedException, IOException
    {
        log("get5() for " + respondedDevice.toString(), 5);
        
//        68:64:00:1e:63:6c:ac:cf:23:83:42:ee:20:20:20:20:20:20:ee:42:83:23:cf:ac:20:20:20:20:20:20
//        6864001e636caccf238342ee202020202020ee428323cfac202020202020
        String command5 = magicKey + "001e636c" + respondedDevice.getMacAddress() + "202020202020" + respondedDevice.getReverseMacAddress() + "202020202020";
        byte[] buffer = hexStringToByteArray(command5);
        DatagramPacket packet = new DatagramPacket(buffer, buffer.length, respondedDevice.getIpAddress(), s20Port);
//        DatagramSocket datagramSendSocket = new DatagramSocket();
//        datagramSendSocket.send(packet);
        getDatagramSendSocket().send(packet);
        
//        Response 5: 68:64:00:2a:71:61:00:ac:cf:23:83:42:ee:20:20:20:20:20:20:ee:42:83:23:cf:ac:20:20:20:20:20:20:53:4f:43:30:30:35:e2:1a:61:da:00
//        6864002a716100accf238342ee202020202020ee428323cfac202020202020534f43303035e21a61da00
        
        String expectedResult = magicKey + "002a716100" + respondedDevice.getMacAddress() + "202020202020" + respondedDevice.getReverseMacAddress() + "202020202020";
        
        int currentSleepTime = 0;
        do
        {
            Thread.sleep(sleepStep);

            Iterator iter = getQueue().iterator();
            while(iter.hasNext())
//            for(QueuePacket response5 : getQueue())
            {
                QueuePacket response5 = iter.next();
                if(response5.getSender().equals(respondedDevice.getIpAddress()))
                {
                    if(response5.getValue().toLowerCase().startsWith(expectedResult.toLowerCase()))
                    {
//                        datagramSendSocket.close();
                        return null;
                    }
                }
            }
            
            currentSleepTime += sleepStep;
        }
        while(currentSleepTime <= maxSleepTime);
        
//        datagramSendSocket.close();
        return expectedResult;
    }

    private static String get6(DeviceS20 respondedDevice) throws IOException, InterruptedException
    {
        log("get6() for " + respondedDevice.toString(), 5);
        
//        Command 5: 68:64:00:1d:72:74:ac:cf:23:83:42:ee:20:20:20:20:20:20:72:00:00:00:01:00:00:00:00:00:00
//        6864001d7274accf238342ee2020202020207200000001000000000000
        String command6 = magicKey + "001d7274" + respondedDevice.getMacAddress() + "2020202020207200000001000000000000";
        byte[] buffer = hexStringToByteArray(command6);
        DatagramPacket packet = new DatagramPacket(buffer, buffer.length, respondedDevice.getIpAddress(), s20Port);
//        DatagramSocket datagramSendSocket = new DatagramSocket();
//        datagramSendSocket.send(packet);
        getDatagramSendSocket().send(packet);

//        Response 6: 68:64:00:24:72:74:ac:cf:23:83:42:ee:20:20:20:20:20:20:01:00:00:00:00:01:00:01:00:00:06:00:04:00:04:00:04:00
//        686400247274accf238342ee202020202020010000000001000100000600040004000400

        String expectedResult = magicKey + "00247274" + respondedDevice.getMacAddress() + "202020202020";
        
        int currentSleepTime = 0;
        do
        {
            Thread.sleep(sleepStep);

            Iterator iter = getQueue().iterator();
            while(iter.hasNext())
//            for(QueuePacket response6 : getQueue())
            {
                QueuePacket response6 = iter.next();
                if(response6.getSender().equals(respondedDevice.getIpAddress()))
                {
                    if(response6.getValue().toLowerCase().startsWith(expectedResult.toLowerCase()))
                    {
//                        datagramSendSocket.close();
                        return null;
                    }
                }
            }
            
            currentSleepTime += sleepStep;
        }
        while(currentSleepTime <= maxSleepTime);

//        datagramSendSocket.close();
        return expectedResult;
    }

    private static String get7DeviceName(DeviceS20 dev) throws IOException, InterruptedException
    {
        log("get7DeviceName() for " + dev.toString(), 5);
        
//        68:64:00:1d:72:74:ac:cf:23:83:42:ee:20:20:20:20:20:20:00:00:00:00:04:00:00:00:00:00:00
//        6864001d7274accf238342ee2020202020200000000004000000000000
        String command7 = magicKey + "001d7274" + dev.getMacAddress() + "2020202020200000000004000000000000";
        byte[] buffer = hexStringToByteArray(command7);
        DatagramPacket packet = new DatagramPacket(buffer, buffer.length, dev.getIpAddress(), s20Port);
//        DatagramSocket datagramSendSocket = new DatagramSocket();
        
//        datagramSendSocket.send(packet);
        getDatagramSendSocket().send(packet);
        Thread.sleep(maxSleepTime);
        
//        68:64:00:dc:72:74:ac:cf:23:83:42:ee:20:20:20:20:20:20:01:00:00:00:00:04:00:01:00:00:be:00:01:00:43:25:ac:cf:23:83:42:ee:20:20:20:20:20:20:ee:42:83:23:cf:ac:20:20:20:20:20:20:38:38:38:38:38:38:20:20:20:20:20:20:4c:69:63:68:74:65:72:6b:65:74:74:65:20:20:20:20:04:00:20:00:00:00:1a:00:00:00:05:00:00:00:10:27:34:1c:19:ff:10:27:76:69:63:65:6e:74:65:72:2e:6f:72:76:69:62:6f:2e:63:6f:6d:20:20:20:20:20:20:20:20:20:20:20:20:20:20:20:20:20:20:20:20:20:c0:a8:01:c8:c0:a8:01:01:ff:ff:ff:00:01:01:00:01:00:ff:00:00:00:00:00:00:00:00:00:00:00:00:00:00:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30
//        686400dc7274accf238342ee20202020202001000000000400010000be0001004325accf238342ee202020202020ee428323cfac2020202020203838383838382020202020204c6963687465726b65747465202020200400200000001a000000050000001027341c19ff1027766963656e7465722e6f727669626f2e636f6d202020202020202020202020202020202020202020c0a801c8c0a80101ffffff000101000100ff000000000000000000000000000030303030303030303030303030303030303030303030303030303030303030303030303030303030
        String expectedResult = magicKey + "00dc7274" + dev.getMacAddress() + "202020202020" + "[a-zA-Z0-9]{32}" + dev.getMacAddress() + "202020202020" + dev.getReverseMacAddress() + "202020202020383838383838202020202020";
//        expectedResult = expectedResult.toLowerCase();
        
        int currentSleepTime = 0;
        do
        {
            Thread.sleep(sleepStep);
            
            Iterator iter = getQueue().iterator();
            while(iter.hasNext())
//            for(QueuePacket responsePacket : getQueue())
            {
                QueuePacket responsePacket = iter.next();
                if(responsePacket.getSender().equals(dev.getIpAddress()))
                {
                    log(responsePacket.getValue().toLowerCase(), 5);
                    Pattern p = Pattern.compile("^" + expectedResult + "*");
                    Matcher m = p.matcher(responsePacket.getValue().toLowerCase());
                    if(m.find())
                    {
                        int pos1 = "686400dc7274accf238342ee20202020202001000000000400010000be0001004325accf238342ee202020202020ee428323cfac202020202020383838383838202020202020".length();
                        int pos2 = responsePacket.getValue().indexOf("20202020", pos1);
                        String deviceNameHex = responsePacket.getValue().substring(pos1, pos2);
                        String converted = convertHexToString(deviceNameHex);
                        log("Found device name: " + deviceNameHex + ", converted: " + converted, 5);
//                        dev.setName(converted);
//                        datagramSendSocket.close();
                        return converted;
                    }
                }
            }            
            currentSleepTime += sleepStep;
        }
        while(currentSleepTime <= maxSleepTime);
        
//        datagramSendSocket.close();
        return expectedResult;
    }

    private static void get8(DeviceS20 respondedDevice) throws InterruptedException, IOException
    {
        log("get8() for " + respondedDevice.toString(), 5);
        
//        68:64:00:1d:72:74:ac:cf:23:83:42:ee:20:20:20:20:20:20:00:00:00:00:03:00:00:00:00:00:00
//        6864001d7274accf238342ee2020202020200000000003000000000000
        String command8 = magicKey + "001d7274" + respondedDevice.getMacAddress() + "2020202020200000000004000000000000";
        byte[] buffer = hexStringToByteArray(command8);
        DatagramPacket packet = new DatagramPacket(buffer, buffer.length, respondedDevice.getIpAddress(), s20Port);
//        DatagramSocket datagramSendSocket = new DatagramSocket();
//        datagramSendSocket.send(packet);
        getDatagramSendSocket().send(packet);

//        68:64:00:1c:72:74:ac:cf:23:83:42:ee:20:20:20:20:20:20:01:00:00:00:00:03:00:01:00:00
//        6864001c7274accf238342ee20202020202001000000000300010000
        
        String expectedResult = magicKey + "001c7274" + respondedDevice.getMacAddress() + "20202020202001000000000300010000";
        
        int currentSleepTime = 0;
        do
        {
            Thread.sleep(sleepStep);
            
            Iterator iter = getQueue().iterator();
            while(iter.hasNext())
//            for(QueuePacket response8 : getQueue())
            {
                QueuePacket response8 = iter.next();
                if(response8.getSender().equals(respondedDevice.getIpAddress()))
                {
                    if(response8.getValue().toLowerCase().startsWith(expectedResult.toLowerCase()))
                    {
                        log("Query complete for device.", 4);
                    }
                }
            }    
            currentSleepTime += sleepStep;
        }
        while(currentSleepTime <= maxSleepTime);        
        
//        datagramSendSocket.close();
    }
    
    protected void prepForSwitchCommand() throws IOException, InterruptedException
    {    
        log("prepForSwitchCommand()", 5);
                
        if(Calendar.getInstance().getTimeInMillis() - ((long)querySwitchTollerance)*1000 <= timestampLastDiscovery)
        {
            log("still in tollerance", 5);
            return;
        }
        else
        {
            log("not in tollerance", 5);
            
            try
            {
                get1(this);
            }
            catch (Exception e)
            {
                // give a shit
            }
            log("get1() finish", 5);
            
            Thread.sleep(300);
            
            try
            {
                get2(this);
            }
            catch (Exception e)
            {
                // give a shit
            }
            Thread.sleep(300);
            log("get2() finish", 5);
        }
    }
    
    public boolean switchDeviceOrRoom(boolean desiredState, boolean useForce, boolean shouldCommandBeRelayed)
    {            
        String desStateString;
        if(desiredState)
            desStateString = "on";
        else
            desStateString = "off";
        
        for(int i = 1; i <= switchAttempts; i++)
        {
            verifyListenerIsRunning();
            
            try
            {
                prepForSwitchCommand();
    //            Thread.sleep(3000);
            }
            catch (IOException e1)
            {
                e1.printStackTrace();
            }
            catch (InterruptedException e1)
            {
                e1.printStackTrace();
            }
            
    //        String macAddress = "accf2383bee6";
            String macAddress = this.getMacAddress();
            macAddress = macAddress.toLowerCase();
            
            log("Switching device " + macAddress + " " + desStateString, 3);
            
            StringBuilder commandBuilder = new StringBuilder();
            commandBuilder.append(magicKey);
            commandBuilder.append("00");    //length, always seems to be zeroes
            commandBuilder.append("176463");
            commandBuilder.append(macAddress);    //mac
            commandBuilder.append("202020202020");    //mac padding
    //            commandBuilder.append(new StringBuilder(macAddress).reverse().toString());
    //            commandBuilder.append("202020202020");    //reverse mac padding
            commandBuilder.append("000000000");
            commandBuilder.append(String.valueOf(boolToInt(desiredState)));
            
            DatagramSocket datagramSendSocket = null;
            
            log("Sending switch packet with content \"" + commandBuilder.toString() + "\" to socket " + this.getIpAddress().getHostAddress(), 4);
            
            try
            {
                byte[] buffer = hexStringToByteArray(commandBuilder.toString());
        
                DatagramPacket packet = new DatagramPacket(buffer, buffer.length, this.getIpAddress(), s20Port);
                datagramSendSocket = new DatagramSocket();
                
                datagramSendSocket.send(packet);
                
                try
                {
                    datagramSendSocket.close();
                }
                catch(Exception e)
                {}
                
    //            68:64:00:17:64:63:ac:cf:23:83:42:ee:20:20:20:20:20:20:01:00:00:00:00
    //            68:64:00:17:73:66:ac:cf:23:83:42:ee:20:20:20:20:20:20:00:00:00:00:01
    //            68:64:00:17:73:66:ac:cf:23:83:42:ee:20:20:20:20:20:20:00:00:00:00:01
                
                /*
                 *  Wait for the response to come, however we don't give a fuck as the
                 *  response comes several times with different states.
                 */

    
    //            Example: 686400177366accf238342ee2020202020200000000001
                
                String expectedResult = magicKey + "00177366" + this.getMacAddress() + "202020202020" + "000000000";
    
                if(desiredState)
                    expectedResult += "1";
                else
                    expectedResult += "0";
                
                log("Waiting for confirmation packet...", 4);
                
                int currentSleepTime = 0;
                do
                {
                    Thread.sleep(sleepStep);
                    
                    
                    Iterator iter = getQueue().iterator();
                    while(iter.hasNext())
//                    for(QueuePacket onePacket : getQueue())
                    {
                        QueuePacket onePacket = iter.next();
                        if(onePacket.getSender().equals(ipAddress))
                        {
                            if(onePacket.getValue().toLowerCase().startsWith(expectedResult))
                            {
                                log("Device switched successfully.", 3);
                                
                                this.setState(desiredState);
                                
                                return true;
                            }
                        }
                    }
                    
                    /*for(QueuePacket onePacket : getQueue())
                    {
                        if(onePacket.getSender().equals(ipAddress))
                        {
                            if(onePacket.getValue().toLowerCase().startsWith(expectedResult))
                            {
                                log("Device switched successfully.", 3);
                                
                                this.setState(desiredState);
                                
                                if(!Settings.serverRelay | shouldCommandBeRelayed)    // Either relaying is inactive or this node is the one relaying
                                    saveState();
                                
                                return true;
                            }
                        }
                    }*/

    
                    currentSleepTime += sleepStep;
                }
                while(currentSleepTime <= maxSleepTime);    
                
                log("Confirmation packet not received.", 4);
                
                log("Forcibly checking status to compensate for missing confirmation packet.", 4);
                this.prepForSwitchCommand();
                
                if(this.getState() == desiredState)        // We didn't get a confirmation packet, but switch worked.
                {
                    return true;
                }
            }
            catch(Exception e)
            {
                log("Error switching device:", 3);
                e.printStackTrace();
                return false;
            }
            
            log("Device " + macAddress + " could not be switched " + desStateString + ". This was attempt " + String.valueOf(i) + " / " + String.valueOf(switchAttempts), 4);
        }

        return false;
    }
    
    public static byte[] hexStringToByteArray(String s)
    {
        return DatatypeConverter.parseHexBinary(s);
    }
    
    public static String bytesToHex(byte[] bytes, int bundleSize)
    {
        String packetMessage = javax.xml.bind.DatatypeConverter.printHexBinary(bytes);
        int beginOfZeroes;
        for(beginOfZeroes = packetMessage.length() -1 ; beginOfZeroes >= 0; beginOfZeroes--)
        {
            if(!String.valueOf(packetMessage.charAt(beginOfZeroes)).equals("0"))
            {
                beginOfZeroes++;
                break;
            }
        }
        return packetMessage.substring(0, beginOfZeroes+2);
    }
    
    public static String convertHexToString(String hex)
    {
          StringBuilder sb = new StringBuilder();
          StringBuilder temp = new StringBuilder();
          
          //49204c6f7665204a617661 split into two characters 49, 20, 4c...
          forint i=0; i          {              
              //grab the hex in pairs
              String output = hex.substring(i, (i + 2));
              //convert hex to decimal
              int decimal = Integer.parseInt(output, 16);
              //convert the decimal to character
              sb.append((char)decimal);
              
              temp.append(decimal);
          }
          log("Decimal : " + temp.toString(), 5);
          
          return sb.toString();
    }

    /*
     * SUPER:
     * id
     * name
     * room
     * state
     * unitcode
     * */

    
    
    public static int boolToInt(boolean in)
    {
        if(in)
            return 1;
        else
            return 0;
    }

    public static class Listener implements Runnable
    {    
        static TimerTask queueCleaner;
        static Timer queueCleanerTimer;
        static boolean queueCleanerRunning = false;
        
        static HashSet listOfBroadcasts;
        static HashSet listOfOwnAddresses;
        
        public static Timer getQueueCleanerTimer()
        {
            return queueCleanerTimer;
        }
        
        protected synchronized static void startQueueCleaner()
        {
            if(!queueCleanerRunning && queue.size() > 0)
            {
                queueCleaner = new CleanUpTask();
                queueCleanerTimer = new Timer();
                log("Starting queue cleanup background task.", 5);
                queueCleanerTimer.scheduleAtFixedRate(queueCleaner, 1000, (long)queueTTL * 1500);
                queueCleanerRunning = true;
            }
        }
        
        protected void discoverBroadcastAddresses()
        {
            listOfBroadcasts = new HashSet();
            listOfOwnAddresses = new HashSet();
            Enumeration list;
            try
            {
                list = NetworkInterface.getNetworkInterfaces();

                while(list.hasMoreElements())
                {
                    NetworkInterface iface = (NetworkInterface) list.nextElement();

                    if(iface == null)
                        continue;

                    if(!iface.isLoopback() && iface.isUp())
                    {
                        Iterator it = iface.getInterfaceAddresses().iterator();
                        while (it.hasNext())
                        {
                            InterfaceAddress address = (InterfaceAddress) it.next();
                            //log("Found address: " + address);
                            if(address == null)
                                continue;
                            InetAddress broadcast = address.getBroadcast();
                            if(broadcast != null
                            {
//                                log("Found broadcast: " + broadcast);
                                listOfBroadcasts.add(broadcast);
                            }
                            
                            InetAddress ownAddress = address.getAddress();
                            if(ownAddress != null
                            {
//                                log("Found broadcast: " + broadcast);
                                listOfOwnAddresses.add(ownAddress);
                            }
                        }
                    }
                }
            }
            catch (SocketException ex)
            {
                System.err.println("Error while getting network interfaces");
                ex.printStackTrace();
            }
        }
        
        protected synchronized static void stopQueueCleaner()
        {
            if(queueCleanerRunning)
            {
                log("Stopping queue cleanup background task.", 5);
                getQueueCleanerTimer().cancel();
                queueCleanerRunning = false;
            }
        }

        @Override
        public void run()
        {
            log("Starting UDP listener on port " + String.valueOf(s20Port) + "...", 3);                        
            
            if(listOfBroadcasts == null)
                discoverBroadcastAddresses();
            
            try
            {
                DatagramSocket datagramReceiveSocket = null;
                byte[] buffer = new byte[100];
                DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
                datagramReceiveSocket = new DatagramSocket(s20Port);
                
                listenerRunning = true;
            
                while(!stopRequested)
                {
//                    log("Waiting for package...", 5);
                    datagramReceiveSocket.receive(packet);
                        
                    for (InetAddress ownAddress : listOfOwnAddresses)
                    {
                        if(ownAddress.getAddress().equals(packet.getAddress().getAddress()))
                        {
                            break;
                        }
                    }
                    boolean foundBroadcastPacket = false;
                    for (InetAddress broadcastAddress : listOfBroadcasts)
                    {
                        if(broadcastAddress.getAddress().equals(packet.getAddress().getAddress()))
                        {
                            foundBroadcastPacket = true;
                            break;
                        }
                    }
                    if(!foundBroadcastPacket && !packet.getAddress().getHostAddress().endsWith("255"))
                    {
                        byte[] result = packet.getData();
                        String message = bytesToHex(result, packet.getLength());
                        
                        if(message.startsWith(magicKey))
                        {
                            QueuePacket queuePacket = new QueuePacket();
                            queuePacket.setTimestamp(Calendar.getInstance());
                            queuePacket.setValue(extractRelevantPart(message));

                            queuePacket.setSender(packet.getSocketAddress().toString());
        //                    Example for SocketAddress:
        //                    /192.168.xxx.xxx:10000
                            queuePacket.setSender(packet.getAddress());
                            getQueue().add(queuePacket);
                            
                            startQueueCleaner();
                            
                            log("Received, Num-Elements " + String.valueOf(getQueue().size()) + ", Origin: " + queuePacket.getSender().getHostName() + ", Content: " + message, 5);
                    }
                    else
                        log("Received invalid packet. Dropping this:" + message, 5);
                    }
                }
                datagramReceiveSocket.close();
            }
            catch(SocketException e)
            {
                e.printStackTrace();
            }
            catch (IOException e)
            {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            finally
            {
                log("Exiting UDP listener on port " + String.valueOf(s20Port) + "...", 3);
                listenerRunning = false;
            }
        }
                
        
        static class CleanUpTask extends TimerTask
        {
            @Override
            public void run()
            {
                if(queue.size() > 0)
                    cleanUpQueue();
            }
            
            void cleanUpQueue()
            {
                log("Running queue cleanup sweep. Amount of queue elements now: " + String.valueOf(getQueue().size()), 5);
                
                Calendar now = Calendar.getInstance();
                
                Iterator iter = getQueue().iterator();
                while(iter.hasNext())
//                int queueSize = queue.size();
//                for(int i=0; i < queueSize; i++)
                {
                    QueuePacket currentPacket = iter.next();
                    if(currentPacket.getTimestamp().getTimeInMillis() < (now.getTimeInMillis() - ((long)queueTTL * 1000)))
                    {
                        log("Packet " + currentPacket.getValue() + " was not used. It will be removed from the incoming queue.", 5);
                        queue.remove(currentPacket);
                    }
                    
                    // Compensate for the packet being taken from the queue
//                    i--;
//                    queueSize--;
                }
                
                log("Queue cleanup sweep finished. Amount of queue elements now: " + String.valueOf(getQueue().size()), 5);
                
                if(getQueue().size() == 0)
                {
                    log("Queue empty.", 5);
                    stopQueueCleaner();
                }
            }
        }
    }
    
    /*
     * Discovery
     * Broadcast Paket an 255.255.255.255
     * UDP, Source 52802, Destination Port 10000
     * 68:64:00:06:71:61                            variiert nicht bei weiteren Aufrufen
     * ----- Magic Key
     *       -- Message length
     *       
     * Absender MAC in dem Fall: 00-25-22-D2-42-CD
     * 
     * Antwort:
     * 68:64:00:2a:71:61:00:ac:cf:23:83:be:e6:20:20:20:20:20:20:e6:be:83:23:cf:ac:20:20:20:20:20:20:53:4f:43:30:30:35:e6:f5:28:da:00
     * ----- Magic Key
     *       -- Message length
     *          ----------- ?
     *                      ------------------ MAC address
     *                                         ----------------- MAC padding
     *                                                          ------------------ reverse mac
     *                                                                mac padding -----------------
     * UDP, Source: 10000, Destination 10000
     * 
     * 
     * 
     * 
     * Port ist immer 10000
     * Standard Passwort: 888888
     * 
     * 68:64:00:1e:63:6c:ac:cf:23:83:be:e6:20:20:20:20:20:20:e6:be:83:23:cf:ac:20:20:20:20:20:20
     *                   ----------------- MAC
     *                                      ----------------- MAC padding
     *                                                       ------------------ reverse mac
     *                                                                         ----------------- mac padding
     * Magic Key is 68 64 (hexadecimals) and is used to distinguish these UDP packets
     * from any other packets that are send over UDP port 10000
     * 
     * Auschalten:
     * 68:64:00:17:64:63:ac:cf:23:83:be:e6:20:20:20:20:20:20:00:00:00:00:00
     * Einschalten:
     * 68:64:00:17:64:63:ac:cf:23:83:be:e6:20:20:20:20:20:20:00:00:00:00:01
     * 
     * 192.168.198.29        ac-cf-23-83-be-e6     dynamic
     * 
     * 68:64:00:1e:63:6c:ac:cf:23:83:be:e6:20:20:20:20:20:20:e6:be:83:23:cf:ac:20:20:20:20:20:20
     * ----- Magic Key
     *       -- Message length
     *          -------- ?
     *          
     *                   ------------------ MAC address
     *                                      ----------------- MAC padding
     *                                                       ------------------ reverse mac
     *                                                             mac padding -----------------
     *                               
     *                   
     *                                               
     * MAC address ac:cf:23:83:be:e6
     * 
     * 
     * 
     * 
     * 
     * 
     * 
     * 
     * 
     * 
     */


    public static class QueuePacket extends Observable
    {
        Calendar timestamp;
        InetAddress sender;
        String value;
        
        public Calendar getTimestamp()
        {
            return timestamp;
        }
        public void setTimestamp(Calendar timestamp)
        {
            this.timestamp = timestamp;
        }
        public String getValue()
        {
            return value;
        }
        public void setValue(String value)
        {
            this.value = value;
        }
        public InetAddress getSender()
        {
            return sender;
        }
        public void setSender(InetAddress sender)
        {
            this.sender = sender;
        }
        public void setSender(String sender)
        {
//            StringBuilder stripped = new StringBuilder();
//            
//            for(int i = 0; i < sender.length(); i++)
//            {
//                if(sender.charAt(i). .matches("^[a-zA-Z0-9]+$"))
//                    stripped.append(sender.charAt(i));
//            }
//            
//            this.setSender(InetAddress.getByName(stripped.toString()));
            
            InetAddress address = null;
            
            try
            {
//                String temp = sender.substring(1, sender.lastIndexOf(':'));
                address = InetAddress.getByName(sender.substring(1, sender.lastIndexOf(':')));
            }
            catch(UnknownHostException e)
            {}
            
            this.setSender(address);
        }
        
        public synchronized void takeFromQueue()
        {
//            log("Packet about to be taken from queue. Current size: " + String.valueOf(queue.size()));
            getQueue().remove(this);
//            log("Packet taken from queue. New size: " + String.valueOf(queue.size()));
        }
    }
    
    static String extractRelevantPart(String message)
    {
        return message;
        /*int pos1 = 0;
        int pos2 = 0;
        
        if(message.contains(" "))
            pos2 = message.indexOf(' ') - 1;
        
        for(int i=0; i < message.length(); i++)
        {
            if(!Character.toString(message.charAt(i)).matches("^[a-zA-Z0-9]+$"))
            {
                pos2 = i;
                break;
            }
        }
        
        return message.substring(pos1, pos2);*/

    }
    
    static void verifyListenerIsRunning()
    {
        log("verifyListenerIsRunning()", 5);
        
        if(!listenerRunning)
        {
            /*
             * This used to be a global var. Testing local if consuming less memory.
             */

            Listener listener = new Listener();
            listenerThread = new Thread(listener);
            listenerThread.setName("Thread-listenerThread");
            listenerThread.start();
            
            while(!listenerRunning)
            {
                try
                {
                    log("Waiting for listener to start...", 4);
                    Thread.sleep(10);
                }
                catch (InterruptedException e)
                {
                    e.printStackTrace();
                }
            }
            
            log("Listener started.", 3);
        }
        

        log("verifyListenerIsRunning() finished", 5);
    }
    
    static void stopListener()
    {
        if(listenerRunning)
        {
            try
            {
                Thread.sleep(5000);
                log("Killing listener...", 4);
                Listener.stopQueueCleaner();
                listenerThread.interrupt();
                if(listenerThread.isInterrupted())
                    log("Listener killed.", 4);
            }
            catch(Exception e)
            {
    //            log("Error killing listener thread.");
            }
        }
    }
    
    @Override
    public String toString()
    {        
        return "Device responded - IP address: " + this.getIpAddress().getHostAddress() + ", MAC address: " + this.getMacAddress() + ", Name: " + getName() + ", State: " + getStringForBoolean(state);
    }

    static void log(String text, int logLevel)
    {
        System.out.println(text);
    }
    
    public static String getStringForBoolean(boolean state)
    {
        if(state)
            return "on";
        else
            return "off";
    }
    
    
}