/*
   Copyright 2008-2013 CNR-ISTI, http://isti.cnr.it
   Institute of Information Science and Technologies
   of the Italian National Research Council


   See the NOTICE file distributed with this work for additional
   information regarding copyright ownership

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

     http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
 */
package org.bubblecloud.zigbee.network.impl;

import org.bouncycastle.util.encoders.Hex;
import org.bubblecloud.zigbee.network.*;
import org.bubblecloud.zigbee.network.packet.*;
import org.bubblecloud.zigbee.network.packet.af.*;
import org.bubblecloud.zigbee.network.packet.simple.*;
import org.bubblecloud.zigbee.network.packet.system.SYS_RESET;
import org.bubblecloud.zigbee.network.packet.system.SYS_RESET_RESPONSE;
import org.bubblecloud.zigbee.network.packet.zdo.*;
import org.bubblecloud.zigbee.util.DoubleByte;
import org.bubblecloud.zigbee.util.Integers;
import org.bubblecloud.zigbee.network.model.*;
import org.bubblecloud.zigbee.v3.SerialPort;
import org.bubblecloud.zigbee.v3.ZigBeeException;
import org.bubblecloud.zigbee.v3.model.ZToolAddress16;
import org.bubblecloud.zigbee.v3.model.ZToolAddress64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;

/**
 * The zigbee network manager port implementation.
 *
 * @author <a href="mailto:stefano.lenzi@isti.cnr.it">Stefano "Kismet" Lenzi</a>
 * @author <a href="mailto:michele.girolami@isti.cnr.it">Michele Girolami</a>
 * @author <a href="mailto:manlio.bacco@isti.cnr.it">Manlio Bacco</a>
 * @author <a href="mailto:tommi.s.e.laukkanen@gmail.com">Tommi S.E. Laukkanen</a>
 * @author <a href="mailto:christopherhattonuk@gmail.com">Chris Hatton</a>
 * @author <a href="mailto:chris@cd-jackson.com">Chris Jackson</a>
 */
public class ZigBeeNetworkManagerImpl implements ZigBeeNetworkManager {

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

    public static final int DEFAULT_TIMEOUT = 8000;
    public static final String TIMEOUT_KEY = "zigbee.driver.cc2530.timeout";

    public static final int RESET_TIMEOUT_DEFAULT = 5000;
    public static final String RESET_TIMEOUT_KEY = "zigbee.driver.cc2530.reset.timeout";

    public static final int STARTUP_TIMEOUT_DEFAULT = 5000;
    public static final String STARTUP_TIMEOUT_KEY = "zigbee.driver.cc2530.startup.timeout";

    public static final int RESEND_TIMEOUT_DEFAULT = 1000;
    public static final String RESEND_TIMEOUT_KEY = "zigbee.driver.cc2530.resend.timeout";

    public static final int RESEND_MAX_RESEND_DEFAULT = 3;
    public static final String RESEND_MAX_RESEND_KEY = "zigbee.driver.cc2530.resend.max";

    public static final boolean RESEND_ONLY_EXCEPTION_DEFAULT = true;
    public static final String RESEND_ONLY_EXCEPTION_KEY = "zigbee.driver.cc2530.resend.exceptionally";
    
    public static final int BOOTLOADER_MAGIC_BYTE_DEFAULT = 0xef;
    public static final String BOOTLOADER_MAGIC_BYTE_KEY = "zigbee.driver.cc2530.bl.magic.byte";

    private final int TIMEOUT;
    private final int RESET_TIMEOUT;
    private final int STARTUP_TIMEOUT;
    private final int RESEND_TIMEOUT;
    private final int RESEND_MAX_RETRY;
    private final boolean RESEND_ONLY_EXCEPTION;
    private final int BOOTLOADER_MAGIC_BYTE;
    
    // Dongle startup options
    private final int STARTOPT_CLEAR_CONFIG = 0x00000001;
    private final int STARTOPT_CLEAR_STATE = 0x00000002;
    
    // The dongle will automatically pickup a random, not conflicting PAN ID
    private final short AUTO_PANID = (short) 0xffff;

    private CommandInterface commandInterface;
    private SerialPort port;
    private DriverStatus state;
    private NetworkMode mode;
    private short pan = AUTO_PANID;
    private byte channel;
    private long extendedPanId; // do not initialize to use dongle defaults (the IEEE address)
    private byte[] networkKey;  // 16 byte network key
    private boolean distributeNetworkKey = true; // distribute network key in clear (be careful)
    private int securityMode = 1; // int for future extensibility

    private final Set<AnnounceListener> announceListeners = new HashSet<AnnounceListener>();
    private final AnnounceListenerFilter announceListenerFilter = new AnnounceListenerFilter(announceListeners);

    private final ArrayList<ApplicationFrameworkMessageListener> messageListeners = new ArrayList<ApplicationFrameworkMessageListener>();
    private final AFMessageListenerFilter afMessageListenerFilter = new AFMessageListenerFilter(messageListeners);

    /**
     * The cached current IEEE address read from dongle.
     */
    private long ieeeAddress = -1;
    /**
     * The cached current PAN id read from dongle.
     */
    private int currentPanId = -1;

    private final HashMap<Class<?>, Thread> conversation3Way = new HashMap<Class<?>, Thread>();

    public ZigBeeNetworkManagerImpl(SerialPort port, NetworkMode mode, int pan, int channel, byte[] networkKey, long timeout) {

        int aux = RESEND_TIMEOUT_DEFAULT;
        try {
            aux = Integer.parseInt(System.getProperty(RESEND_TIMEOUT_KEY));
            logger.trace("Using RESEND_TIMEOUT set from enviroment {}", aux);
        } catch (NumberFormatException ex) {
            logger.trace("Using RESEND_TIMEOUT set as DEFAULT {}", aux);
        }
        RESEND_TIMEOUT = aux;

        aux = (int) Math.max(DEFAULT_TIMEOUT, timeout);
        try {
            aux = Integer.parseInt(System.getProperty(TIMEOUT_KEY));
            logger.trace("Using TIMEOUT set from enviroment {}", aux);
        } catch (NumberFormatException ex) {
            logger.trace("Using TIMEOUT set as DEFAULT {}ms", aux);
        }
        TIMEOUT = aux;

        aux = (int) Math.max(RESET_TIMEOUT_DEFAULT, timeout);
        try {
            aux = Integer.parseInt(System.getProperty(RESET_TIMEOUT_KEY));
            logger.trace("Using RESET_TIMEOUT set from enviroment {}", aux);
        } catch (NumberFormatException ex) {
            logger.trace("Using RESET_TIMEOUT set as DEFAULT {}ms", aux);
        }
        RESET_TIMEOUT = aux;

        aux = (int) Math.max(STARTUP_TIMEOUT_DEFAULT, timeout);
        try {
            aux = Integer.parseInt(System.getProperty(STARTUP_TIMEOUT_KEY));
            logger.trace("Using STARTUP_TIMEOUT set from enviroment {}", aux);
        } catch (NumberFormatException ex) {
            logger.trace("Using STARTUP_TIMEOUT set as DEFAULT {}ms", aux);
        }
        STARTUP_TIMEOUT = aux;

        aux = RESEND_MAX_RESEND_DEFAULT;
        try {
            aux = Integer.parseInt(System.getProperty(RESEND_MAX_RESEND_KEY));
            logger.trace("Using RESEND_MAX_RETRY set from enviroment {}", aux);
        } catch (NumberFormatException ex) {
            logger.trace("Using RESEND_MAX_RETRY set as DEFAULT {}", aux);
        }
        RESEND_MAX_RETRY = aux;

        boolean b = RESEND_ONLY_EXCEPTION_DEFAULT;
        try {
            b = Boolean.parseBoolean(System.getProperty(RESEND_ONLY_EXCEPTION_KEY));
            logger.trace("Using RESEND_MAX_RETRY set from enviroment {}", aux);
        } catch (NumberFormatException ex) {
            logger.trace("Using RESEND_MAX_RETRY set as DEFAULT {}", aux);
        }
        RESEND_ONLY_EXCEPTION = b;
        
        aux = BOOTLOADER_MAGIC_BYTE_DEFAULT;
        try {
            aux = Integer.parseInt(System.getProperty(BOOTLOADER_MAGIC_BYTE_KEY));
            logger.trace("Using BOOTLOADER_MAGIC_BYTE set from enviroment {}", aux);
        } catch (NumberFormatException ex) {
            logger.trace("Using BOOTLOADER_MAGIC_BYTE set as DEFAULT {}", aux);
        }
        BOOTLOADER_MAGIC_BYTE = aux;
        
        state = DriverStatus.CLOSED;
        setSerialPort(port);
        setZigBeeNetwork((byte) channel, (short) pan);
        setZigBeeNodeMode(mode);
        setZigBeeNetworkKey(networkKey);
    }

    public boolean startup() {
        if (state == DriverStatus.CLOSED) {
            state = DriverStatus.CREATED;
            logger.trace("Initializing hardware.");

            // Open the hardware port
            setState(DriverStatus.HARDWARE_INITIALIZING);
            if (!initializeHardware()) {
                shutdown();
                return false;
            }
            
            // Now reset the dongle
            setState(DriverStatus.HARDWARE_OPEN);
            if (!dongleReset()) {
            	logger.warn("Dongle reset failed. Assuming bootloader is running and sending magic byte {}.",
            			String.format("0x%02x", BOOTLOADER_MAGIC_BYTE));
            	if (!bootloaderGetOut(BOOTLOADER_MAGIC_BYTE)) {
            		logger.warn("Attempt to get out from bootloader failed.");
            		shutdown();
            		return false;
            	}
            }

            setState(DriverStatus.HARDWARE_READY);
            
            return true;
        } else {
            throw new IllegalStateException("Driver already opened, current status is:" + state);
        }
    }

    public void shutdown() {
        if (state == DriverStatus.CLOSED) {
            logger.debug("Already CLOSED");
            return;
        }
        if (state == DriverStatus.NETWORK_READY) {
            logger.trace("Closing NETWORK");
            setState(DriverStatus.HARDWARE_READY);
        }
        if (state == DriverStatus.HARDWARE_OPEN || state == DriverStatus.HARDWARE_READY || 
        		state == DriverStatus.NETWORK_INITIALIZING) {
            logger.trace("Closing HARDWARE");
            commandInterface.close();
            setState(DriverStatus.CREATED);
        }
        setState(DriverStatus.CLOSED);
    }

    private boolean initializeHardware() {
        commandInterface = new CommandInterfaceImpl(port);
        if (!commandInterface.open()) {
            logger.error("Failed to initialize the dongle on port {}.", port);
            return false;
        }

        return true;
    }

    public boolean initializeZigBeeNetwork(boolean cleanStatus) {
        logger.trace("Initializing network.");

        setState(DriverStatus.NETWORK_INITIALIZING);

        if (cleanStatus) {
            if (!configureZigBeeNetwork()) {
                shutdown();
                return false;
            }
        }
        if (!createZigBeeNetwork()) {
            logger.error("Failed to start zigbee network.");
            shutdown();
            return false;
        }
        if (checkZigBeeNetworkConfiguration()) {
            logger.error("Dongle configuration does not match the specified configuration.");
            shutdown();
            return false;
        }
        return true;
    }

    private boolean createZigBeeNetwork() {
        createCustomDevicesOnDongle();
        logger.debug("Creating network as {}", mode.toString());

        final int ALL_CLUSTERS = 0xFFFF;

        logger.trace("Reset seq: Trying MSG_CB_REGISTER");
        ZDO_MSG_CB_REGISTER_SRSP responseCb = (ZDO_MSG_CB_REGISTER_SRSP) sendSynchrouns(
                commandInterface, new ZDO_MSG_CB_REGISTER(new DoubleByte(ALL_CLUSTERS))
        );
        if (responseCb == null) {
            return false;
        }

        final int instantStartup = 0;

        ZDO_STARTUP_FROM_APP_SRSP response = (ZDO_STARTUP_FROM_APP_SRSP) sendSynchrouns(
                commandInterface, new ZDO_STARTUP_FROM_APP(instantStartup), STARTUP_TIMEOUT
        );
        if (response == null) {
        	return false;
        }
        
        switch (response.Status) {
            case 0: {
                logger.info("Initialized ZigBee network with existing network state.");
                return true;
            }
            case 1: {
                logger.info("Initialized ZigBee network with new or reset network state.");
                return true;
            }
            case 2: {
                logger.warn("Initializing ZigBee network failed.");
                return false;
            }
            default: {
                logger.error("Unexpected response state for ZDO_STARTUP_FROM_APP {}", response.Status);
                return false;
            }
        }


    }

    private boolean checkZigBeeNetworkConfiguration() {
        int value;
        long longValue;
        boolean mismatch = false;
        if ((value = getCurrentChannel()) != channel) {
            logger.warn(
                    "The channel configuration differ from the channel configuration in use: " +
                            "in use {}, while the configured is {}.\n" +
                            "The ZigBee network should be reconfigured or configuration corrected.",
                    value, channel
            );
            mismatch = true;
        }
        // Do not check the current PAN ID if a random one is generated
        // by the dongle.
        if (pan != AUTO_PANID && (value = getCurrentPanId()) != pan) {
            logger.warn(
                    "The PanId configuration differ from the PanId configuration in use: " +
                            "in use {}, while the configured is {}.\n" +
                            "The ZigBee network should be reconfigured or configuration corrected.",
                    value, pan
            );
            mismatch = true;
        }
        if (extendedPanId != 0 && (longValue = getExtendedPanId()) != extendedPanId) {
            logger.warn(
                    "The ExtendedPanId configuration differ from the ExtendedPanId configuration in use: " +
                            "in use {}, while the configured is {}.\n" +
                            "The ZigBee network should be reconfigured or configuration corrected.",
                    longValue, extendedPanId
            );
            mismatch = true;
        }
        if ((value = getZigBeeNodeMode()) != mode.ordinal()) {
            logger.warn(
                    "The NetworkMode configuration differ from the NetworkMode configuration in use: " +
                            "in use {}, while the configured is {}.\n" +
                            "The ZigBee network should be reconfigured or configuration corrected.",
                    value, mode.ordinal()
            );
            mismatch = true;
        }

        if (networkKey != null) {
            final byte[] readNetworkKey = getZigBeeNetworkKey();
            if (readNetworkKey == null) {
                logger.warn("Could not read preconfigured network key.");
                mismatch = true;
            } else {
                boolean networkKeyMismatch = false;
                for (int i = 0; i < 16; i++) {
                    if (networkKey[i] != readNetworkKey[i]) {
                        networkKeyMismatch = true;
                        break;
                    }
                }
                if (networkKeyMismatch) {
                    logger.warn(
                            "The NetworkKey configuration differ from the NetworkKey configuration in use.",
                            Hex.toHexString(networkKey), Hex.toHexString(readNetworkKey)
                    );
                    mismatch = true;
                }
            }
        }

        return mismatch;
    }

    private boolean configureZigBeeNetwork() {
        logger.debug("Resetting network stack.");

        // Make sure we start clearing configuration and state
        logger.info("Setting clean state.");
        if (!dongleSetStartupOption(STARTOPT_CLEAR_CONFIG | STARTOPT_CLEAR_STATE)) {
            logger.error("Unable to set clean state for dongle");
            return false;
        }
        logger.debug("Changing the Network Mode to {}.", mode);
        if (!dongleSetNetworkMode()) {
            logger.error("Unable to set NETWORK_MODE for ZigBee Network");
            return false;
        } else {
            logger.trace("NETWORK_MODE set");
        }
        // A dongle reset is needed to put into effect
        // configuration clear and network mode.
        logger.info("Resetting dongle.");
        if (!dongleReset()) {
            logger.error("Unable to reset dongle");
            return false;
        }
        
        logger.debug("Setting channel to {}.", channel);
        if (!dongleSetChannel()) {
            logger.error("Unable to set CHANNEL for ZigBee Network");
            return false;
        } else {
            logger.trace("CHANNEL set");
        }
        logger.debug("Setting PAN to {}.", String.format("%04X", pan & 0x0000ffff));
        if (!dongleSetPanId()) {
            logger.error("Unable to set PANID for ZigBee Network");
            return false;
        } else {
            logger.trace("PANID set");
        }
        if (extendedPanId != 0) {
        	logger.debug("Setting Extended PAN ID to {}.", String.format("%08X", extendedPanId));
        	if (!dongleSetExtendedPanId()) {
        		logger.error("Unable to set EXT_PANID for ZigBee Network");
        		return false;
        	} else {
        		logger.trace("EXT_PANID set");
        	}
        }
        if (networkKey != null) {
        	logger.debug("Setting NETWORK_KEY.");
        	if (!dongleSetNetworkKey()) {
        		logger.error("Unable to set NETWORK_KEY for ZigBee Network");
        		return false;
        	} else {
        		logger.trace("NETWORK_KEY set");
        	}
        }
        logger.debug("Setting Distribute Network Key to {}.", distributeNetworkKey);
        if (!dongleSetDistributeNetworkKey()) {
        	logger.error("Unable to set DISTRIBUTE_NETWORK_KEY for ZigBee Network");
        	return false;
        } else {
        	logger.trace("DISTRIBUTE_NETWORK_KEY set");
        }
        logger.debug("Setting Security Mode to {}.", securityMode);
        if (!dongleSetSecurityMode()) {
        	logger.error("Unable to set SECURITY_MODE for ZigBee Network");
        	return false;
        } else {
        	logger.trace("SECURITY_MODE set");
        }
        return true;
    }

    private void setState(DriverStatus value) {
        logger.trace("{} -> {}", this.state, value);
        synchronized (this) {
            state = value;
            notifyAll();
        }
        if (state == DriverStatus.HARDWARE_READY) {
            postHardwareEnabled();
        }
    }

    private void postHardwareEnabled() {
        if (!messageListeners.contains(afMessageListenerFilter)) {
            commandInterface.addAsynchronousCommandListener(afMessageListenerFilter);
        }
        if (!announceListeners.contains(announceListenerFilter)) {
            commandInterface.addAsynchronousCommandListener(announceListenerFilter);
        }
    }

    private boolean waitForHardware() {
        synchronized (this) {
            while (state == DriverStatus.CREATED || state == DriverStatus.CLOSED) {
                logger.debug("Waiting for hardware to become ready");
                try {
                    wait();
                } catch (InterruptedException ignored) {
                }
            }
            return isHardwareReady();
        }
    }

    private boolean waitForNetwork() {
    	long before = System.currentTimeMillis();
    	boolean timedOut = false;
        synchronized (this) {
            while (state != DriverStatus.NETWORK_READY && state != DriverStatus.CLOSED && !timedOut) {
                logger.debug("Waiting for network to become ready");
                try {
                	long now = System.currentTimeMillis();
                	long timeout = STARTUP_TIMEOUT - (now - before);
                	if (timeout > 0) {
                		wait(timeout);
                	} else {
                		timedOut = true;
                	}
                } catch (InterruptedException ignored) {
                }
                
            }
            return isNetworkReady();
        }
    }

    public void setZigBeeNodeMode(NetworkMode networkMode) {
        if (state != DriverStatus.CLOSED) {
            throw new IllegalStateException("Network mode can be changed only " +
                    "if driver is CLOSED while it is:" + state);
        }
        mode = networkMode;
    }

    public void setZigBeeNetworkKey(byte[] networkKey) {
        if (state != DriverStatus.CLOSED) {
            throw new IllegalStateException("Network key can be changed only " +
                    "if driver is CLOSED while it is:" + state);
        }
        this.networkKey = networkKey;
    }

    public void setZigBeeNetwork(byte ch, short panId) {
        if (state != DriverStatus.CLOSED) {
            throw new IllegalStateException("Network mode can be changed only " +
                    "if driver is CLOSED while it is:" + state);
        }
        channel = ch;
        pan = panId;
    }

    public void setSerialPort(SerialPort port) {
        if (state != DriverStatus.CLOSED) {
            throw new IllegalStateException("Serial port can be changed only " +
                    "if driver is CLOSED while it is:" + state);
        }
        this.port = port;
    }

    @Override
    public Set<AnnounceListener> getAnnounceListeners() {
        return announceListeners;
    }

    @Override
    public void addAsynchronousCommandListener(final AsynchronousCommandListener asynchronousCommandListener) {
        commandInterface.addAsynchronousCommandListener(asynchronousCommandListener);
    }

    public <REQUEST extends ZToolPacket, RESPONSE extends ZToolPacket> RESPONSE sendLocalRequest(REQUEST request) {
        if (!waitForNetwork()) {
        	return null;
        }
        RESPONSE result = (RESPONSE) sendSynchrouns(commandInterface, request);
        if (result == null) {
            logger.error("{} timed out waiting for synchronous local response.", request.getClass().getSimpleName());
        }
        return result;
    }

    public <REQUEST extends ZToolPacket, RESPONSE extends ZToolPacket> RESPONSE sendRemoteRequest(REQUEST request) {
        if (!waitForNetwork()) {
        	return null;
        }
        RESPONSE result;

        waitAndLock3WayConversation(request);
        final BlockingCommandReceiver waiter = new BlockingCommandReceiver(ZToolCMD.ZDO_MGMT_PERMIT_JOIN_RSP, commandInterface);

        logger.trace("Sending {}", request);
        ZToolPacket response = sendSynchrouns(commandInterface, request);
        if (response == null) {
            logger.error("{} timed out waiting for synchronous local response.", request.getClass().getSimpleName());
            waiter.cleanup();
            return null;
        } else {
            logger.error("{} timed out waiting for asynchronous remote response.", request.getClass().getSimpleName());
            result = (RESPONSE) waiter.getCommand(TIMEOUT);
            unLock3WayConversation(request);
            return result;
        }
    }

    public ZDO_MGMT_LQI_RSP sendLQIRequest(ZDO_MGMT_LQI_REQ request) {

        if (!waitForNetwork()) {
        	return null;
        }
        ZDO_MGMT_LQI_RSP result = null;

        waitAndLock3WayConversation(request);
        final BlockingCommandReceiver waiter = new BlockingCommandReceiver(ZToolCMD.ZDO_MGMT_LQI_RSP, commandInterface);

        logger.trace("Sending ZDO_MGMT_LQI_REQ {}", request);
        ZDO_MGMT_LQI_REQ_SRSP response = (ZDO_MGMT_LQI_REQ_SRSP) sendSynchrouns(commandInterface, request);
        if (response == null || response.Status != 0) {
            logger.trace("ZDO_MGMT_LQI_REQ failed, received {}", response);
            waiter.cleanup();
        } else {
            result = (ZDO_MGMT_LQI_RSP) waiter.getCommand(TIMEOUT);
        }
        unLock3WayConversation(request);
        return result;
    }

    public ZDO_IEEE_ADDR_RSP sendZDOIEEEAddressRequest(ZDO_IEEE_ADDR_REQ request) {
        if (!waitForNetwork()) {
        	return null;
        }
        ZDO_IEEE_ADDR_RSP result = null;

        waitAndLock3WayConversation(request);
        final BlockingCommandReceiver waiter = new BlockingCommandReceiver(ZToolCMD.ZDO_IEEE_ADDR_RSP, commandInterface);

        logger.trace("Sending ZDO_IEEE_ADDR_REQ {}", request);
        ZDO_IEEE_ADDR_REQ_SRSP response = (ZDO_IEEE_ADDR_REQ_SRSP) sendSynchrouns(commandInterface, request);
        if (response == null || response.Status != 0) {
            logger.warn("ZDO_IEEE_ADDR_REQ failed, received {}", response);
            waiter.cleanup();
        } else {
            result = (ZDO_IEEE_ADDR_RSP) waiter.getCommand(TIMEOUT);
        }
        unLock3WayConversation(request);
        return result;
    }

    public ZDO_NODE_DESC_RSP sendZDONodeDescriptionRequest(ZDO_NODE_DESC_REQ request) {
        if (!waitForNetwork()) {
        	return null;
        }
        ZDO_NODE_DESC_RSP result = null;

        waitAndLock3WayConversation(request);
        final BlockingCommandReceiver waiter = new BlockingCommandReceiver(ZToolCMD.ZDO_NODE_DESC_RSP, commandInterface);

        ZDO_NODE_DESC_REQ_SRSP response = (ZDO_NODE_DESC_REQ_SRSP) sendSynchrouns(commandInterface, request);
        if (response == null || response.Status != 0) {
            waiter.cleanup();
        } else {
            result = (ZDO_NODE_DESC_RSP) waiter.getCommand(TIMEOUT);
        }

        unLock3WayConversation(request);
        return result;
    }

    public ZDO_POWER_DESC_RSP sendZDOPowerDescriptionRequest(ZDO_POWER_DESC_REQ request) {
        if (!waitForNetwork()) {
        	return null;
        }
        ZDO_POWER_DESC_RSP result = null;

        waitAndLock3WayConversation(request);
        final BlockingCommandReceiver waiter = new BlockingCommandReceiver(ZToolCMD.ZDO_POWER_DESC_RSP, commandInterface);

        ZDO_POWER_DESC_REQ_SRSP response = (ZDO_POWER_DESC_REQ_SRSP) sendSynchrouns(commandInterface, request);
        if (response == null || response.Status != 0) {
            waiter.cleanup();
        } else {
            result = (ZDO_POWER_DESC_RSP) waiter.getCommand(TIMEOUT);
        }

        unLock3WayConversation(request);
        return result;
    }

    public ZDO_ACTIVE_EP_RSP sendZDOActiveEndPointRequest(ZDO_ACTIVE_EP_REQ request) {
        if (!waitForNetwork()) {
        	return null;
        }
        ZDO_ACTIVE_EP_RSP result = null;

        waitAndLock3WayConversation(request);
        final BlockingCommandReceiver waiter = new BlockingCommandReceiver(ZToolCMD.ZDO_ACTIVE_EP_RSP, commandInterface);

        logger.trace("Sending ZDO_ACTIVE_EP_REQ {}", request);
        ZDO_ACTIVE_EP_REQ_SRSP response = (ZDO_ACTIVE_EP_REQ_SRSP) sendSynchrouns(commandInterface, request);
        if (response == null || response.Status != 0) {
            logger.trace("ZDO_ACTIVE_EP_REQ failed, received {}", response);
            waiter.cleanup();
        } else {
            result = (ZDO_ACTIVE_EP_RSP) waiter.getCommand(TIMEOUT);
        }
        unLock3WayConversation(request);
        return result;
    }

    public ZDO_MGMT_PERMIT_JOIN_RSP sendPermitJoinRequest(ZDO_MGMT_PERMIT_JOIN_REQ request, boolean waitForCommand) {
        if (!waitForNetwork()) {
        	return null;
        }
        ZDO_MGMT_PERMIT_JOIN_RSP result = null;

        waitAndLock3WayConversation(request);
        final BlockingCommandReceiver waiter = new BlockingCommandReceiver(ZToolCMD.ZDO_MGMT_PERMIT_JOIN_RSP, commandInterface);

        logger.trace("Sending ZDO_MGMT_PERMIT_JOIN_REQ {}", request);
        ZDO_MGMT_PERMIT_JOIN_REQ_SRSP response = (ZDO_MGMT_PERMIT_JOIN_REQ_SRSP) sendSynchrouns(commandInterface, request);
        if (response == null || response.Status != 0) {
            logger.trace("ZDO_MGMT_PERMIT_JOIN_REQ failed, received {}", response);
            waiter.cleanup();
        } else if (!waitForCommand) {
            result = (ZDO_MGMT_PERMIT_JOIN_RSP) waiter.getCommand(TIMEOUT);
        }
        unLock3WayConversation(request);
        return result;
    }

    public boolean sendZDOLeaveRequest(ZToolAddress16[] addresses) {
        //---------ZDO GET IEEE ADDR
        logger.trace("Reset seq: Trying GETIEEEADDR");
        ZToolAddress64[] longAddresses = new ZToolAddress64[addresses.length];
        for (int k = 0; k < addresses.length; k++) {
//			ZDO_IEEE_ADDR_RSP responseA4 = sendZDOIEEEAddressRequest(new ZDO_IEEE_ADDR_REQ(addresses[k],ZDO_IEEE_ADDR_REQ.REQ_TYPE.EXTENDED.getValue(),0));

            ZDO_IEEE_ADDR_RSP responseA4 = null;
            BlockingCommandReceiver waiter = new BlockingCommandReceiver(ZToolCMD.ZDO_IEEE_ADDR_RSP, commandInterface);
            logger.trace("Sending ZDO_IEEE_ADDR_REQ");
            ZDO_IEEE_ADDR_REQ_SRSP response = (ZDO_IEEE_ADDR_REQ_SRSP) sendSynchrouns(commandInterface, new ZDO_IEEE_ADDR_REQ(addresses[k], ZDO_IEEE_ADDR_REQ.REQ_TYPE.EXTENDED.getValue(), 0));
            if (response == null || response.Status != 0) {
                logger.trace("ZDO_IEEE_ADDR_REQ failed, received {}", response);
                waiter.cleanup();
            } else {
                responseA4 = (ZDO_IEEE_ADDR_RSP) waiter.getCommand(TIMEOUT);
            }

            if (responseA4 != null) {
                longAddresses[k] = responseA4.getIeeeAddress();
            } else {
                longAddresses[k] = null;
            }
        }
        //---------ZDO MGMT LEAVE
        logger.debug("Reset seq: Trying LEAVE");
        long start = System.currentTimeMillis();
        for (int k = 0; k < longAddresses.length; k++) {
            if (longAddresses[k] != null) {
                BlockingCommandReceiver waiter3 = new BlockingCommandReceiver(ZToolCMD.ZDO_MGMT_LEAVE_RSP, commandInterface);

                ZDO_MGMT_LEAVE_REQ_SRSP response = (ZDO_MGMT_LEAVE_REQ_SRSP) sendSynchrouns(commandInterface, new ZDO_MGMT_LEAVE_REQ(addresses[k], longAddresses[k], 0));
                if (response == null) {
                    logger.error("Leave request time out.");
                    return false;
                }
                if (response.Status != 0) {
                    waiter3.cleanup();
                    logger.error("Leave SRSP error status: " + response.Status);
                    return false;
                }
                ZDO_MGMT_LEAVE_RSP responseA5 = (ZDO_MGMT_LEAVE_RSP) waiter3.getCommand(TIMEOUT);
                if (responseA5 == null) {
                    logger.error("Leave request got no RSP.");
                    return false;
                }
                if (responseA5.Status != 0) {
                    logger.error("Leave request RSP error status: " + responseA5.Status);
                    return false;
                }
            }
            if ((System.currentTimeMillis() - start) > TIMEOUT) {
                logger.error("Reset seq: Failed LEAVE");
                return false;
            }
        }

        return true;
    }


    /**
     * @param request
     */
    private void waitAndLock3WayConversation(ZToolPacket request) {
        synchronized (conversation3Way) {
            Class<?> clz = request.getClass();
            Thread requestor;
            while ((requestor = conversation3Way.get(clz)) != null) {
                if (!requestor.isAlive()) {
                    logger.error("Thread {} whom requested {} DIED before unlocking the conversation");
                    logger.debug("The thread {} who was waiting for {} to complete DIED, so we have to remove the lock");
                    conversation3Way.put(clz, null);
                    break;
                }
                logger.trace(
                        "{} is waiting for {} to complete which was issued by {} to complete",
                        new Object[]{Thread.currentThread(), clz, requestor}
                );
                try {
                    conversation3Way.wait();
                } catch (InterruptedException ex) {
                } catch (IllegalMonitorStateException ex) {
                    logger.error("Error in 3 way conversation.", ex);
                }
            }
            conversation3Way.put(clz, Thread.currentThread());
        }
    }

    /**
     * Release the lock held for the 3-way communication
     *
     * @param request
     */
    private void unLock3WayConversation(ZToolPacket request) {
        Class<?> clz = request.getClass();
        Thread requestor;
        synchronized (conversation3Way) {
