/**
 * Copyright (c) 2014,2019 Contributors to the Eclipse Foundation
 *
 * See the NOTICE file(s) distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0
 *
 * SPDX-License-Identifier: EPL-2.0
 */
package org.eclipse.smarthome.binding.digitalstrom.handler;

import static org.eclipse.smarthome.binding.digitalstrom.DigitalSTROMBindingConstants.*;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

import org.apache.commons.lang.StringUtils;
import org.eclipse.smarthome.binding.digitalstrom.DigitalSTROMBindingConstants;
import org.eclipse.smarthome.binding.digitalstrom.internal.lib.climate.jsonresponsecontainer.impl.TemperatureControlStatus;
import org.eclipse.smarthome.binding.digitalstrom.internal.lib.config.Config;
import org.eclipse.smarthome.binding.digitalstrom.internal.lib.event.EventListener;
import org.eclipse.smarthome.binding.digitalstrom.internal.lib.listener.ConnectionListener;
import org.eclipse.smarthome.binding.digitalstrom.internal.lib.listener.DeviceStatusListener;
import org.eclipse.smarthome.binding.digitalstrom.internal.lib.listener.ManagerStatusListener;
import org.eclipse.smarthome.binding.digitalstrom.internal.lib.listener.SceneStatusListener;
import org.eclipse.smarthome.binding.digitalstrom.internal.lib.listener.TemperatureControlStatusListener;
import org.eclipse.smarthome.binding.digitalstrom.internal.lib.listener.TotalPowerConsumptionListener;
import org.eclipse.smarthome.binding.digitalstrom.internal.lib.listener.stateenums.ManagerStates;
import org.eclipse.smarthome.binding.digitalstrom.internal.lib.listener.stateenums.ManagerTypes;
import org.eclipse.smarthome.binding.digitalstrom.internal.lib.manager.ConnectionManager;
import org.eclipse.smarthome.binding.digitalstrom.internal.lib.manager.DeviceStatusManager;
import org.eclipse.smarthome.binding.digitalstrom.internal.lib.manager.SceneManager;
import org.eclipse.smarthome.binding.digitalstrom.internal.lib.manager.StructureManager;
import org.eclipse.smarthome.binding.digitalstrom.internal.lib.manager.impl.ConnectionManagerImpl;
import org.eclipse.smarthome.binding.digitalstrom.internal.lib.manager.impl.DeviceStatusManagerImpl;
import org.eclipse.smarthome.binding.digitalstrom.internal.lib.manager.impl.SceneManagerImpl;
import org.eclipse.smarthome.binding.digitalstrom.internal.lib.manager.impl.StructureManagerImpl;
import org.eclipse.smarthome.binding.digitalstrom.internal.lib.manager.impl.TemperatureControlManager;
import org.eclipse.smarthome.binding.digitalstrom.internal.lib.structure.devices.Circuit;
import org.eclipse.smarthome.binding.digitalstrom.internal.lib.structure.devices.Device;
import org.eclipse.smarthome.binding.digitalstrom.internal.lib.structure.devices.deviceparameters.DeviceStateUpdate;
import org.eclipse.smarthome.binding.digitalstrom.internal.lib.structure.devices.deviceparameters.constants.MeteringTypeEnum;
import org.eclipse.smarthome.binding.digitalstrom.internal.lib.structure.devices.deviceparameters.constants.MeteringUnitsEnum;
import org.eclipse.smarthome.binding.digitalstrom.internal.lib.structure.scene.InternalScene;
import org.eclipse.smarthome.binding.digitalstrom.internal.providers.DsChannelTypeProvider;
import org.eclipse.smarthome.config.core.Configuration;
import org.eclipse.smarthome.core.library.types.DecimalType;
import org.eclipse.smarthome.core.thing.Bridge;
import org.eclipse.smarthome.core.thing.ChannelUID;
import org.eclipse.smarthome.core.thing.Thing;
import org.eclipse.smarthome.core.thing.ThingStatus;
import org.eclipse.smarthome.core.thing.ThingStatusDetail;
import org.eclipse.smarthome.core.thing.ThingTypeUID;
import org.eclipse.smarthome.core.thing.binding.BaseBridgeHandler;
import org.eclipse.smarthome.core.thing.binding.ThingHandler;
import org.eclipse.smarthome.core.thing.binding.builder.ThingStatusInfoBuilder;
import org.eclipse.smarthome.core.types.Command;
import org.eclipse.smarthome.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The {@link BridgeHandler} is the handler for a digitalSTROM-Server and connects it to
 * the framework.<br>
 * All {@link DeviceHandler}s and {@link SceneHandler}s use the {@link BridgeHandler} to execute the actual
 * commands.<br>
 * <br>
 * The {@link BridgeHandler} also:
 * <ul>
 * <li>manages the {@link DeviceStatusManager} (starts, stops, register {@link DeviceStatusListener},
 * register {@link SceneStatusListener} and so on)</li>
 * <li>creates and load the configurations in the {@link Config}.</li>
 * <li>implements {@link ManagerStatusListener} to manage the expiration of the Thing initializations</li>
 * <li>implements the {@link ConnectionListener} to manage the {@link ThingStatus} of this {@link BridgeHandler}</li>
 * <li>and implements the {@link TotalPowerConsumptionListener} to update his Channels.</li>
 * </ul>
 *
 * @author Michael Ochel - Initial contribution
 * @author Matthias Siegele - Initial contribution
 */
public class BridgeHandler extends BaseBridgeHandler
        implements ConnectionListener, TotalPowerConsumptionListener, ManagerStatusListener {

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

    /**
     * Contains all supported thing types of this handler
     */
    public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_DSS_BRIDGE);

    private static final long RECONNECT_TRACKER_INTERVAL = 15;

    /* DS-Manager */
    private ConnectionManager connMan;
    private StructureManager structMan;
    private SceneManager sceneMan;
    private DeviceStatusManager devStatMan;
    private TemperatureControlManager tempContMan;

    private EventListener eventListener;
    private ScheduledFuture<?> reconnectTracker;

    private DeviceStatusListener deviceDiscovery;
    private SceneStatusListener sceneDiscovery;
    private TemperatureControlStatusListener temperatureControlDiscovery;
    private Config config;

    List<SceneStatusListener> unregisterSceneStatusListeners;
    private short connectionTimeoutCounter = 0;
    private final short ignoredTimeouts = 5;

    private class Initializer implements Runnable {

        BridgeHandler bridge;
        Config config;

        public Initializer(BridgeHandler bridge, Config config) {
            this.bridge = bridge;
            this.config = config;
        }

        @Override
        public void run() {
            logger.debug("Checking connection");
            if (connMan == null) {
                connMan = new ConnectionManagerImpl(config, bridge, true);
            } else {
                connMan.registerConnectionListener(bridge);
                connMan.configHasBeenUpdated();
            }

            logger.debug("Initializing digitalSTROM Manager ");
            if (eventListener == null) {
                eventListener = new EventListener(connMan);
            }
            if (structMan == null) {
                structMan = new StructureManagerImpl();
            }
            if (sceneMan == null) {
                sceneMan = new SceneManagerImpl(connMan, structMan, bridge, eventListener);
            }
            if (devStatMan == null) {
                devStatMan = new DeviceStatusManagerImpl(connMan, structMan, sceneMan, bridge, eventListener);
            } else {
                devStatMan.registerStatusListener(bridge);
            }

            devStatMan.registerTotalPowerConsumptionListener(bridge);

            if (connMan.checkConnection()) {
                logger.debug("connection established, start services");
                if (TemperatureControlManager.isHeatingControllerInstallated(connMan)) {
                    if (tempContMan == null) {
                        tempContMan = new TemperatureControlManager(connMan, eventListener,
                                temperatureControlDiscovery);
                        temperatureControlDiscovery = null;
                    } else {
                        if (temperatureControlDiscovery != null) {
                            tempContMan.registerTemperatureControlStatusListener(temperatureControlDiscovery);
                        }
                    }
                }
                structMan.generateZoneGroupNames(connMan);
                devStatMan.start();
                eventListener.start();
            }

            boolean configChanged = false;
            Configuration configuration = bridge.getConfig();
            if (connMan.getApplicationToken() != null) {
                configuration.remove(USER_NAME);
                configuration.remove(PASSWORD);
                logger.debug("Application-Token is: {}", connMan.getApplicationToken());
                configuration.put(APPLICATION_TOKEN, connMan.getApplicationToken());
                configChanged = true;
            }
            Map<String, String> properties = editProperties();
            String dSSname = connMan.getDigitalSTROMAPI().getInstallationName(connMan.getSessionToken());
            if (dSSname != null) {
                properties.put(DS_NAME, dSSname);
            }
            Map<String, String> dsidMap = connMan.getDigitalSTROMAPI().getDSID(connMan.getSessionToken());
            if (dsidMap != null) {
                logger.debug("{}", dsidMap);
                properties.putAll(dsidMap);
            }
            Map<String, String> versions = connMan.getDigitalSTROMAPI().getSystemVersion();
            if (versions != null) {
                properties.putAll(versions);
            }
            if (StringUtils.isBlank(getThing().getProperties().get(DigitalSTROMBindingConstants.SERVER_CERT))
                    && StringUtils.isNotBlank(config.getCert())) {
                properties.put(DigitalSTROMBindingConstants.SERVER_CERT, config.getCert());
            }
            logger.debug("update properties");
            updateProperties(properties);

            if (configChanged) {
                updateConfiguration(configuration);
            }
        }
    };

    /**
     * Creates a new {@link BridgeHandler}.
     *
     * @param bridge must not be null
     */
    public BridgeHandler(Bridge bridge) {
        super(bridge);
    }

    @Override
    public void initialize() {
        logger.debug("Initializing digitalSTROM-BridgeHandler");
        updateStatus(ThingStatus.ONLINE, ThingStatusDetail.CONFIGURATION_PENDING, "Checking configuration...");
        // Start an extra thread to readout the configuration and check the connection, because it takes sometimes more
        // than 5000 milliseconds and the handler will suspend (ThingStatus.UNINITIALIZED).
        Config config = loadAndCheckConfig();

        if (config != null) {
            logger.debug("{}", config.toString());
            scheduler.execute(new Initializer(this, config));
        }
    }

    private boolean checkLoginConfig(Config config) {
        if ((StringUtils.isNotBlank(config.getUserName()) && StringUtils.isNotBlank(config.getPassword()))
                || StringUtils.isNotBlank(config.getAppToken())) {
            return true;
        }
        onConnectionStateChange(CONNECTION_LOST, NO_USER_PASSWORD);
        return false;
    }

    private Config loadAndCheckConfig() {
        Configuration thingConfig = super.getConfig();
        Config config = loadAndCheckConnectionData(thingConfig);
        if (config == null) {
            return null;
        }
        logger.debug("Loading configuration");
        ArrayList<String> numberExc = new ArrayList<String>();
        // Parameters can't be null, because of an existing default value.
        if (thingConfig.get(DigitalSTROMBindingConstants.SENSOR_DATA_UPDATE_INTERVAL) instanceof BigDecimal) {
            config.setSensordataRefreshInterval(
                    ((BigDecimal) thingConfig.get(DigitalSTROMBindingConstants.SENSOR_DATA_UPDATE_INTERVAL)).intValue()
                            * 1000);
        } else {
            numberExc.add("\"Sensor update interval\" ( "
                    + thingConfig.get(DigitalSTROMBindingConstants.SENSOR_DATA_UPDATE_INTERVAL) + ")");
        }
        if (thingConfig.get(DigitalSTROMBindingConstants.TOTAL_POWER_UPDATE_INTERVAL) instanceof BigDecimal) {
            config.setTotalPowerUpdateInterval(
                    ((BigDecimal) thingConfig.get(DigitalSTROMBindingConstants.TOTAL_POWER_UPDATE_INTERVAL)).intValue()
                            * 1000);
        } else {
            numberExc.add("\"Total power update interval\" ("
                    + thingConfig.get(DigitalSTROMBindingConstants.TOTAL_POWER_UPDATE_INTERVAL) + ")");
        }
        if (thingConfig.get(DigitalSTROMBindingConstants.SENSOR_WAIT_TIME) instanceof BigDecimal) {
            config.setSensorReadingWaitTime(
                    ((BigDecimal) thingConfig.get(DigitalSTROMBindingConstants.SENSOR_WAIT_TIME)).intValue() * 1000);
        } else {
            numberExc.add("\"Wait time sensor reading\" ("
                    + thingConfig.get(DigitalSTROMBindingConstants.SENSOR_WAIT_TIME) + ")");
        }
        if (thingConfig.get(DigitalSTROMBindingConstants.DEFAULT_TRASH_DEVICE_DELETE_TIME_KEY) instanceof BigDecimal) {
            config.setTrashDeviceDeleteTime(
                    ((BigDecimal) thingConfig.get(DigitalSTROMBindingConstants.DEFAULT_TRASH_DEVICE_DELETE_TIME_KEY))
                            .intValue());
        } else {
            numberExc.add("\"Days to be slaked trash bin devices\" ("
                    + thingConfig.get(DigitalSTROMBindingConstants.DEFAULT_TRASH_DEVICE_DELETE_TIME_KEY) + ")");
        }
        if (!numberExc.isEmpty()) {
            String excText = "The field ";
            for (int i = 0; i < numberExc.size(); i++) {
                excText = excText + numberExc.get(i);
                if (i < numberExc.size() - 2) {
                    excText = excText + ", ";
                } else if (i < numberExc.size() - 1) {
                    excText = excText + " and ";
                }
            }
            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, excText + " have to be a number.");
            return null;
        }
        if (StringUtils.isNotBlank(getThing().getProperties().get(DigitalSTROMBindingConstants.SERVER_CERT))) {
            config.setCert(getThing().getProperties().get(DigitalSTROMBindingConstants.SERVER_CERT));
        }
        return config;
    }

    private Config loadAndCheckConnectionData(Configuration thingConfig) {
        if (this.config == null) {
            this.config = new Config();
        }
        // load and check connection and authorization data
        if (StringUtils.isNotBlank((String) thingConfig.get(HOST))) {
            config.setHost(thingConfig.get(HOST).toString());
        } else {
            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
                    "The connection to the digitalSTROM-Server can't established, because the host address is missing. Please set the host address.");
            return null;
        }
        if (thingConfig.get(USER_NAME) != null) {
            config.setUserName(thingConfig.get(USER_NAME).toString());
        } else {
            config.setUserName(null);
        }
        if (thingConfig.get(PASSWORD) != null) {
            config.setPassword(thingConfig.get(PASSWORD).toString());
        } else {
            config.setPassword(null);
        }
        if (thingConfig.get(APPLICATION_TOKEN) != null) {
            config.setAppToken(thingConfig.get(APPLICATION_TOKEN).toString());
        } else {
            config.setAppToken(null);
        }

        if (!checkLoginConfig(config)) {
            return null;
        }
        return config;
    }

    @Override
    public void dispose() {
        logger.debug("Handler disposed");
        if (reconnectTracker != null && !reconnectTracker.isCancelled()) {
            reconnectTracker.cancel(true);
        }
        if (eventListener != null) {
            eventListener.stop();
        }
        if (devStatMan != null) {
            devStatMan.unregisterTotalPowerConsumptionListener();
            devStatMan.unregisterStatusListener();
            this.devStatMan.stop();
        }
        if (connMan != null) {
            connMan.unregisterConnectionListener();
        }
    }

    @Override
    public void handleCommand(ChannelUID channelUID, Command command) {
        if (command instanceof RefreshType) {
            channelLinked(channelUID);
        } else {
            logger.debug("Command {} is not supported for channel: {}", command, channelUID.getId());
        }
    }

    @Override
    public void handleRemoval() {
        for (Thing thing : getThing().getThings()) {
            // Inform Thing-Child's about removed bridge.
            final ThingHandler thingHandler = thing.getHandler();
            if (thingHandler != null) {
                thingHandler.bridgeStatusChanged(ThingStatusInfoBuilder.create(ThingStatus.REMOVED).build());
            }
        }
        if (StringUtils.isNotBlank((String) super.getConfig().get(APPLICATION_TOKEN))) {
            if (connMan == null) {
                Config config = loadAndCheckConnectionData(this.getConfig());
                if (config != null) {
                    this.connMan = new ConnectionManagerImpl(config, null, false);
                } else {
                    updateStatus(ThingStatus.REMOVED);
                    return;
                }
            }
            if (connMan.removeApplicationToken()) {
                logger.debug("Application-Token deleted");
            }
        }
        updateStatus(ThingStatus.REMOVED);
    }

    /* methods to store listener */

    /**
     * Registers a new {@link DeviceStatusListener} on the {@link DeviceStatusManager}.
     *
     * @param deviceStatusListener (must not be null)
     */
    public synchronized void registerDeviceStatusListener(DeviceStatusListener deviceStatusListener) {
        if (this.devStatMan != null) {
            if (deviceStatusListener == null) {
                throw new IllegalArgumentException("It's not allowed to pass null.");
            }

            if (deviceStatusListener.getDeviceStatusListenerID() != null) {
                if (devStatMan.getManagerState().equals(ManagerStates.RUNNING)) {
                    devStatMan.registerDeviceListener(deviceStatusListener);
                } else if (deviceStatusListener.getDeviceStatusListenerID()
                        .equals(DeviceStatusListener.DEVICE_DISCOVERY)) {
                    devStatMan.registerDeviceListener(deviceStatusListener);
                }
            } else {
                throw new IllegalArgumentException("It's not allowed to pass a DeviceStatusListener with ID = null.");
            }
        } else {
            if (deviceStatusListener.getDeviceStatusListenerID().equals(DeviceStatusListener.DEVICE_DISCOVERY)) {
                deviceDiscovery = deviceStatusListener;
            }
        }
    }

    /**
     * Unregisters a new {@link DeviceStatusListener} on the {@link BridgeHandler}.
     *
     * @param deviceStatusListener (must not be null)
     */
    public void unregisterDeviceStatusListener(DeviceStatusListener deviceStatusListener) {
        if (this.devStatMan != null) {
            if (deviceStatusListener.getDeviceStatusListenerID() != null) {
                this.devStatMan.unregisterDeviceListener(deviceStatusListener);
            } else {
                throw new IllegalArgumentException("It's not allowed to pass a DeviceStatusListener with ID = null.");
            }
        }
    }

    /**
     * Registers a new {@link SceneStatusListener} on the {@link BridgeHandler}.
     *
     * @param sceneStatusListener (must not be null)
     */
    public synchronized void registerSceneStatusListener(SceneStatusListener sceneStatusListener) {
        if (this.sceneMan != null) {
            if (sceneStatusListener == null) {
                throw new IllegalArgumentException("It's not allowed to pass null.");
            }

            if (sceneStatusListener.getSceneStatusListenerID() != null) {
                this.sceneMan.registerSceneListener(sceneStatusListener);
            } else {
                throw new IllegalArgumentException("It's not allowed to pass a SceneStatusListener with ID = null.");
            }
        } else {
            if (sceneStatusListener.getSceneStatusListenerID().equals(SceneStatusListener.SCENE_DISCOVERY)) {
                sceneDiscovery = sceneStatusListener;
            }
        }
    }

    /**
     * Unregisters a new {@link SceneStatusListener} on the {@link DeviceStatusManager}.
     *
     * @param sceneStatusListener (must not be null)
     */
    public void unregisterSceneStatusListener(SceneStatusListener sceneStatusListener) {
        if (this.sceneMan != null) {
            if (sceneStatusListener.getSceneStatusListenerID() != null) {
                this.sceneMan.unregisterSceneListener(sceneStatusListener);
            } else {
                throw new IllegalArgumentException("It's not allowed to pass a SceneStatusListener with ID = null..");
            }
        }
    }

    /**
     * Has to be called from a removed Thing-Child to rediscovers the Thing.
     *
     * @param id = scene or device id (must not be null)
     */
    public void childThingRemoved(String id) {
        if (id != null && id.split("-").length == 3) {
            InternalScene scene = sceneMan.getInternalScene(id);
            if (scene != null) {
                sceneMan.removeInternalScene(id);
                sceneMan.addInternalScene(scene);
            }
        } else {
            devStatMan.removeDevice(id);
        }
    }

    /**
     * Delegate a stop command from a Thing to the {@link DeviceStatusManager#sendStopComandsToDSS(Device)}.
     *
     * @param device can be null
     */
    public void stopOutputValue(Device device) {
        this.devStatMan.sendStopComandsToDSS(device);
    }

    @Override
    public void channelLinked(ChannelUID channelUID) {
        if (devStatMan != null) {
            MeteringTypeEnum meteringType = DsChannelTypeProvider.getMeteringType(channelUID.getId());
            if (meteringType != null) {
                if (meteringType.equals(MeteringTypeEnum.ENERGY)) {
                    onEnergyMeterValueChanged(devStatMan.getTotalEnergyMeterValue());
                } else {
                    onTotalPowerConsumptionChanged(devStatMan.getTotalPowerConsumption());
                }
            } else {
                logger.warn("Channel with id {} is not known for the thing with id {}.", channelUID.getId(),
                        getThing().getUID());
            }
        }
    }

    @Override
    public void onTotalPowerConsumptionChanged(int newPowerConsumption) {
        updateChannelState(MeteringTypeEnum.CONSUMPTION, MeteringUnitsEnum.WH, newPowerConsumption);
    }

    @Override
    public void onEnergyMeterValueChanged(int newEnergyMeterValue) {
        updateChannelState(MeteringTypeEnum.ENERGY, MeteringUnitsEnum.WH, newEnergyMeterValue * 0.001);
    }

    @Override
    public void onEnergyMeterWsValueChanged(int newEnergyMeterValue) {
        // not needed
    }

    private void updateChannelState(MeteringTypeEnum meteringType, MeteringUnitsEnum meteringUnit, double value) {
        String channelID = DsChannelTypeProvider.getMeteringChannelID(meteringType, meteringUnit, true);
        if (getThing().getChannel(channelID) != null) {
            updateState(channelID, new DecimalType(value));
        }
    }

    @Override
    public void onConnectionStateChange(String newConnectionState) {
        switch (newConnectionState) {
            case CONNECTION_LOST:
                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
                        "The connection to the digitalSTROM-Server cannot be established.");
                startReconnectTracker();
                return;
            case CONNECTION_RESUMED:
                if (connectionTimeoutCounter > 0) {
                    if (connMan.checkConnection()) {
                        restartServices();
                        setStatus(ThingStatus.ONLINE);
                    }
                }
                // reset connection timeout counter
                connectionTimeoutCounter = 0;
                return;
            case APPLICATION_TOKEN_GENERATED:
                if (connMan != null) {
                    Configuration config = this.getConfig();
                    config.remove(USER_NAME);
                    config.remove(PASSWORD);
                    config.put(APPLICATION_TOKEN, connMan.getApplicationToken());
                    this.updateConfiguration(config);
                }
                return;
            default:
                return;
        }
    }

    private void setStatus(ThingStatus status) {
        logger.debug("set status to: {}", status);
        updateStatus(status);
        for (Thing thing : getThing().getThings()) {
            ThingHandler handler = thing.getHandler();
            if (handler != null) {
                handler.bridgeStatusChanged(getThing().getStatusInfo());
            }
        }
    }

    private void startReconnectTracker() {
        if (reconnectTracker == null || reconnectTracker.isCancelled()) {
            logger.debug("Connection lost, stop all services and start reconnectTracker.");
            stopServices();
            reconnectTracker = scheduler.scheduleWithFixedDelay(new Runnable() {

                @Override
                public void run() {
                    if (connMan != null) {
                        boolean conStat = connMan.checkConnection();
                        logger.debug("check connection = {}", conStat);
                        if (conStat) {
                            restartServices();
                            reconnectTracker.cancel(false);
                        }
                    }
                }
            }, RECONNECT_TRACKER_INTERVAL, RECONNECT_TRACKER_INTERVAL, TimeUnit.SECONDS);
        }
    }

    private void stopServices() {
        if (devStatMan != null && !devStatMan.getManagerState().equals(ManagerStates.STOPPED)) {
            devStatMan.stop();
        }
        if (eventListener != null && eventListener.isStarted()) {
            eventListener.stop();
        }
    }

    private void restartServices() {
        logger.debug("reconnect, stop reconnection tracker and restart services");
        if (reconnectTracker != null && !reconnectTracker.isCancelled()) {
            reconnectTracker.cancel(true);
        }
        stopServices();
        if (devStatMan != null) {
            devStatMan.start();
        }
        if (eventListener != null) {
            eventListener.start();
        }
    }

    @Override
    public void onConnectionStateChange(String newConnectionState, String reason) {
        if (newConnectionState.equals(NOT_AUTHENTICATED) || newConnectionState.equals(CONNECTION_LOST)) {
            switch (reason) {
                case WRONG_APP_TOKEN:
                    updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
                            "User defined Application-Token is wrong. "
                                    + "Please set user name and password to generate an Application-Token or set an valid Application-Token.");
                    stopServices();
                    return;
                case WRONG_USER_OR_PASSWORD:
                    updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
                            "The set username or password is wrong.");
                    stopServices();
                    return;
                case NO_USER_PASSWORD:
                    updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
                            "No username or password is set to generate Application-Token. Please set user name and password or Application-Token.");
                    stopServices();
                    return;
                case CONNECTON_TIMEOUT:
                    // ignore the first connection timeout
                    if (connectionTimeoutCounter++ > ignoredTimeouts) {
                        updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
                                "Connection lost because connection timeout to Server.");
                        break;
                    } else {
                        return;
                    }
                case HOST_NOT_FOUND:
                    updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
                            "Server not found! Please check these points:\n" + " - Is digitalSTROM-Server turned on?\n"
                                    + " - Is the host address correct?\n"
                                    + " - Is the ethernet cable connection established?");
                    break;
                case UNKNOWN_HOST:
                    updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
                            "Unknown host name, please check the set host name!");
                    break;
                case INVALID_URL:
                    updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Invalid URL is set.");
                    break;
                default:
            }
            // reset connection timeout counter
            connectionTimeoutCounter = 0;
            startReconnectTracker();
        }
    }

    /**
     * Returns a list of all {@link Device}'s.
     *
     * @return device list (cannot be null)
     */
    public List<Device> getDevices() {
        return this.structMan != null && this.structMan.getDeviceMap() != null
                ? new LinkedList<Device>(this.structMan.getDeviceMap().values())
                : null;
    }

    /**
     * Returns the {@link StructureManager}.
     *
     * @return StructureManager
     */
    public StructureManager getStructureManager() {
        return this.structMan;
    }

    /**
     * Delegates a scene command of a Thing to the
     * {@link DeviceStatusManager#sendSceneComandsToDSS(InternalScene, boolean)}
     *
     * @param scene the called scene
     * @param call_undo (true = call scene | false = undo scene)
     */
    public void sendSceneComandToDSS(InternalScene scene, boolean call_undo) {
        if (devStatMan != null) {
            devStatMan.sendSceneComandsToDSS(scene, call_undo);
        }
    }

    /**
     * Delegates a device command of a Thing to the
     * {@link DeviceStatusManager#sendComandsToDSS(Device, DeviceStateUpdate)}
     *
     * @param device can be null
     * @param deviceStateUpdate can be null
     */
    public void sendComandsToDSS(Device device, DeviceStateUpdate deviceStateUpdate) {
        if (devStatMan != null) {
            devStatMan.sendComandsToDSS(device, deviceStateUpdate);
        }
    }

    /**
     * Returns a list of all {@link InternalScene}'s.
     *
     * @return Scene list (cannot be null)
     */
    public List<InternalScene> getScenes() {
        return sceneMan != null ? sceneMan.getScenes() : new LinkedList<InternalScene>();
    }

    /**
     * Returns the {@link ConnectionManager}.
     *
     * @return ConnectionManager
     */
    public ConnectionManager getConnectionManager() {
        return this.connMan;
    }

    @Override
    public void onStatusChanged(ManagerTypes managerType, ManagerStates state) {
        if (managerType.equals(ManagerTypes.DEVICE_STATUS_MANAGER)) {
            switch (state) {
                case INITIALIZING:
                    if (deviceDiscovery != null) {
                        devStatMan.registerDeviceListener(deviceDiscovery);
                        deviceDiscovery = null;
                    }
                    logger.debug("Building digitalSTROM model");
                    break;
                case RUNNING:
                    updateStatus(ThingStatus.ONLINE);
                    break;
                case STOPPED:
                    if (!getThing().getStatusInfo().getStatusDetail().equals(ThingStatusDetail.COMMUNICATION_ERROR)
                            && !getThing().getStatusInfo().getStatusDetail()
                                    .equals(ThingStatusDetail.CONFIGURATION_ERROR)) {
                        updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "DeviceStatusManager is stopped.");
                        devStatMan.start();
                    }
                    break;
                default:
                    break;
            }
        }
        if (managerType.equals(ManagerTypes.SCENE_MANAGER)) {
            switch (state) {
                case GENERATING_SCENES:
                    logger.debug("SceneManager reports that he is generating scenes");
                    if (sceneDiscovery != null) {
                        sceneMan.registerSceneListener(sceneDiscovery);
                        sceneDiscovery = null;
                    }
                    break;
                case RUNNING:
                    logger.debug("SceneManager reports that he is running");
                    break;
                default:
                    break;
            }
        }
    }

    /**
     * Returns a {@link List} of all {@link Circuit}'s.
     *
     * @return circuit list
     */
    public List<Circuit> getCircuits() {
        logger.debug("circuits: {}", structMan.getCircuitMap().values().toString());
        return structMan != null && structMan.getCircuitMap() != null
                ? new LinkedList<Circuit>(structMan.getCircuitMap().values())
                : null;
    }

    /**
     * Returns the {@link TemperatureControlManager} or null if no one exist.
     *
     * @return {@link TemperatureControlManager}
     */
    public TemperatureControlManager getTemperatureControlManager() {
        return tempContMan;
    }

    /**
     * Registers the given {@link TemperatureControlStatusListener} to the {@link TemperatureControlManager}.
     *
     * @param temperatureControlStatusListener can be null
     */
    public void registerTemperatureControlStatusListener(
            TemperatureControlStatusListener temperatureControlStatusListener) {
        if (tempContMan != null) {
            tempContMan.registerTemperatureControlStatusListener(temperatureControlStatusListener);
        } else if (TemperatureControlStatusListener.DISCOVERY
                .equals(temperatureControlStatusListener.getTemperationControlStatusListenrID())) {
            this.temperatureControlDiscovery = temperatureControlStatusListener;
        }
    }

    /**
     * Unregisters the given {@link TemperatureControlStatusListener} from the {@link TemperatureControlManager}.
     *
     * @param temperatureControlStatusListener can be null
     */
    public void unregisterTemperatureControlStatusListener(
            TemperatureControlStatusListener temperatureControlStatusListener) {
        if (tempContMan != null) {
            tempContMan.unregisterTemperatureControlStatusListener(temperatureControlStatusListener);
        }
    }

    /**
     * see {@link TemperatureControlManager#getTemperatureControlStatusFromAllZones()}
     *
     * @return all temperature control status objects
     */
    public Collection<TemperatureControlStatus> getTemperatureControlStatusFromAllZones() {
        return tempContMan != null ? tempContMan.getTemperatureControlStatusFromAllZones()
                : new LinkedList<TemperatureControlStatus>();
    }
}