/**
 * Copyright (c) 2010-2019 Contributors to the openHAB project
 *
 * See the NOTICE file(s) distributed with this work for additional
 * information.
 *
 * 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.openhab.binding.zoneminder.internal.handler;

import java.io.IOException;
import java.math.BigDecimal;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

import javax.security.auth.login.FailedLoginException;

import org.apache.commons.lang.StringUtils;
import org.eclipse.smarthome.core.library.types.DecimalType;
import org.eclipse.smarthome.core.library.types.OnOffType;
import org.eclipse.smarthome.core.thing.Bridge;
import org.eclipse.smarthome.core.thing.Channel;
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.ThingStatusInfo;
import org.eclipse.smarthome.core.thing.ThingTypeUID;
import org.eclipse.smarthome.core.thing.binding.BaseBridgeHandler;
import org.eclipse.smarthome.core.thing.binding.ThingHandlerService;
import org.eclipse.smarthome.core.types.Command;
import org.eclipse.smarthome.core.types.RefreshType;
import org.eclipse.smarthome.core.types.State;
import org.eclipse.smarthome.core.types.UnDefType;
import org.openhab.binding.zoneminder.internal.DataRefreshPriorityEnum;
import org.openhab.binding.zoneminder.internal.ZoneMinderConstants;
import org.openhab.binding.zoneminder.internal.ZoneMinderProperties;
import org.openhab.binding.zoneminder.internal.config.ZoneMinderBridgeServerConfig;
import org.openhab.binding.zoneminder.internal.discovery.ZoneMinderDiscoveryService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import name.eskildsen.zoneminder.IZoneMinderConnectionInfo;
import name.eskildsen.zoneminder.IZoneMinderDaemonStatus;
import name.eskildsen.zoneminder.IZoneMinderDiskUsage;
import name.eskildsen.zoneminder.IZoneMinderHostLoad;
import name.eskildsen.zoneminder.IZoneMinderHostVersion;
import name.eskildsen.zoneminder.IZoneMinderMonitorData;
import name.eskildsen.zoneminder.IZoneMinderServer;
import name.eskildsen.zoneminder.IZoneMinderSession;
import name.eskildsen.zoneminder.ZoneMinderFactory;
import name.eskildsen.zoneminder.api.config.ZoneMinderConfig;
import name.eskildsen.zoneminder.api.config.ZoneMinderConfigEnum;
import name.eskildsen.zoneminder.exception.ZoneMinderUrlNotFoundException;

/**
 * Handler for a ZoneMinder Server.
 *
 * @author Martin S. Eskildsen - Initial contribution
 */
public class ZoneMinderServerBridgeHandler extends BaseBridgeHandler implements ZoneMinderHandler {

    public static final int TELNET_TIMEOUT = 5000;

    public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections
            .singleton(ZoneMinderConstants.THING_TYPE_BRIDGE_ZONEMINDER_SERVER);

    /**
     * Logger
     */
    private final Logger logger = LoggerFactory.getLogger(getClass());

    private ZoneMinderDiscoveryService discoveryService;

    private ScheduledFuture<?> taskWatchDog;
    private int refreshFrequency;
    private int refreshCycleCount;

    /** Connection status for the bridge. */
    private boolean connected;
    private ThingStatus curBridgeStatus = ThingStatus.UNKNOWN;

    protected boolean online;

    private Runnable watchDogRunnable = new Runnable() {
        private int watchDogCount = -1;

        @Override
        public void run() {
            try {
                updateAvaliabilityStatus(zoneMinderConnection);

                if ((discoveryService != null) && (getBridgeConfig().getAutodiscoverThings())) {
                    watchDogCount++;
                    // Run every two minutes
                    if ((watchDogCount % 8) == 0) {
                        discoveryService.startBackgroundDiscovery();
                        watchDogCount = 0;
                    }
                }
            } catch (Exception exception) {
                logger.error("[WATCHDOG]: Server run(): Exception: {}", exception.getMessage());
            }
        }
    };

    /**
     * Local copies of last fetched values from ZM
     */
    private String channelCpuLoad = "";
    private String channelDiskUsage = "";

    private Boolean isInitialized = false;

    private IZoneMinderSession zoneMinderSession;
    private IZoneMinderConnectionInfo zoneMinderConnection;

    private ScheduledFuture<?> taskRefreshData;
    private ScheduledFuture<?> taskPriorityRefreshData;

    private Runnable refreshDataRunnable = () -> {
        try {
            boolean fetchDiskUsage = false;

            if (!isOnline()) {
                logger.debug("{}: Bridge '{}' is noit online skipping refresh", getLogIdentifier(), thing.getUID());
            }

            refreshCycleCount++;

            int iMaxCycles;
            boolean resetCount = false;
            boolean doRefresh = false;

            // Disk Usage is disabled
            if (getBridgeConfig().getRefreshIntervalLowPriorityTask() == 0) {
                iMaxCycles = getBridgeConfig().getRefreshInterval();
                resetCount = true;
                doRefresh = true;
            } else {
                iMaxCycles = getBridgeConfig().getRefreshIntervalLowPriorityTask() * 60;
                doRefresh = true;
                if ((refreshCycleCount * refreshFrequency) >= (getBridgeConfig().getRefreshIntervalLowPriorityTask()
                        * 60)) {
                    fetchDiskUsage = true;
                    resetCount = true;
                }
            }

            logger.debug(
                    "{}: Running Refresh data task count='{}', freq='{}', max='{}', interval='{}', intervalLow='{}'",
                    getLogIdentifier(), refreshCycleCount, refreshFrequency, iMaxCycles,
                    getBridgeConfig().getRefreshInterval(), getBridgeConfig().getRefreshIntervalLowPriorityTask());

            if (doRefresh) {
                if (resetCount) {
                    refreshCycleCount = 0;
                }

                logger.debug("{}: 'refreshDataRunnable()': (diskUsage='{}')", getLogIdentifier(), fetchDiskUsage);

                refreshThing(zoneMinderSession, fetchDiskUsage);
            }
        } catch (Exception exception) {
            logger.error("{}: monitorRunnable::run(): Exception: ", getLogIdentifier(), exception);
        }
    };

    private Runnable refreshPriorityDataRunnable = () -> {
        try {
            // Make sure priority updates is done
            for (Thing thing : getThing().getThings()) {
                try {
                    if (thing.getThingTypeUID().equals(ZoneMinderConstants.THING_TYPE_THING_ZONEMINDER_MONITOR)) {
                        ZoneMinderBaseThingHandler thingHandler = (ZoneMinderBaseThingHandler) thing.getHandler();
                        if (thingHandler != null) {
                            if (thingHandler.getRefreshPriority() == DataRefreshPriorityEnum.HIGH_PRIORITY) {
                                logger.debug("[MONITOR-{}]: RefreshPriority is High Priority",
                                        thingHandler.getZoneMinderId());
                                thingHandler.refreshThing(zoneMinderSession, DataRefreshPriorityEnum.HIGH_PRIORITY);
                            }
                        } else {
                            logger.debug(
                                    "[MONITOR]: refreshThing not called for monitor, since thingHandler is 'null'");
                        }
                    }
                } catch (NullPointerException ex) {
                    // This isn't critical (unless it comes over and over). There seems to be a bug so that a
                    // null
                    // pointer exception is coming every now and then.
                    // HAve to find the reason for that. Until thenm, don't Spamm
                    logger.error("[MONITOR]: Method 'refreshThing()' for Bridge failed for thing='{}' - Exception: ",
                            thing.getUID(), ex);
                } catch (Exception ex) {
                    logger.error("[MONITOR]: Method 'refreshThing()' for Bridge failed for thing='{}' - Exception: ",
                            thing.getUID(), ex);
                }
            }
        } catch (Exception exception) {
            logger.error("[MONITOR]: monitorRunnable::run(): Exception: ", exception);
        }
    };

    /**
     * Constructor
     *
     * @param bridge Bridge object representing a ZoneMinder Server
     */
    public ZoneMinderServerBridgeHandler(Bridge bridge) {
        super(bridge);

        logger.info("{}: Starting ZoneMinder Server Bridge Handler (Bridge='{}')", getLogIdentifier(),
                bridge.getBridgeUID());
    }

    /**
     * Initializes the bridge.
     */
    @Override
    public void initialize() {
        logger.debug("[BRIDGE]: About to initialize bridge " + ZoneMinderConstants.BRIDGE_ZONEMINDER_SERVER);
        try {
            updateStatus(ThingStatus.OFFLINE);
            logger.info("BRIDGE: ZoneMinder Server Bridge Handler Initialized");
            logger.debug("BRIDGE:    HostName:           {}", getBridgeConfig().getHostName());
            logger.debug("BRIDGE:    Protocol:           {}", getBridgeConfig().getProtocol());
            logger.debug("BRIDGE:    Port HTTP(S)        {}", getBridgeConfig().getHttpPort());
            logger.debug("BRIDGE:    Port Telnet         {}", getBridgeConfig().getTelnetPort());
            logger.debug("BRIDGE:    Server Path         {}", getBridgeConfig().getServerBasePath());
            logger.debug("BRIDGE:    User:               {}", getBridgeConfig().getUserName());
            logger.debug("BRIDGE:    Refresh interval:   {}", getBridgeConfig().getRefreshInterval());
            logger.debug("BRIDGE:    Low  prio. refresh: {}", getBridgeConfig().getRefreshIntervalLowPriorityTask());
            logger.debug("BRIDGE:    Autodiscovery:      {}", getBridgeConfig().getAutodiscoverThings());

            closeConnection();

            zoneMinderConnection = ZoneMinderFactory.CreateConnection(getBridgeConfig().getProtocol(),
                    getBridgeConfig().getHostName(), getBridgeConfig().getHttpPort(), getBridgeConfig().getTelnetPort(),
                    getBridgeConfig().getServerBasePath(), getBridgeConfig().getUserName(),
                    getBridgeConfig().getPassword(), 3000);

            taskRefreshData = null;
            taskPriorityRefreshData = null;
        } catch (Exception ex) {
            logger.error("[BRIDGE]: 'ZoneMinderServerBridgeHandler' failed to initialize. Exception='{}'",
                    ex.getMessage());
            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR);
        } finally {
            startWatchDogTask();
            isInitialized = true;
        }
    }

    public void setDiscoveryService(ZoneMinderDiscoveryService discoveryService) {
        this.discoveryService = discoveryService;
    }

    /**
     * Method to find the lowest possible refresh rate (based on configuration)
     *
     * @param refreshRate
     * @return
     */
    protected int calculateCommonRefreshFrequency(int refreshRate) {
        // Check if 30, 15, 10 or 5 seconds is possible
        if ((refreshRate % 30) == 0) {
            return 30;
        } else if ((refreshRate % 15) == 0) {
            return 15;
        } else if ((refreshRate % 10) == 0) {
            return 10;
        } else if ((refreshRate % 5) == 0) {
            return 5;
        }

        // Hmm, didn't find a obvious shared value. Run every second...
        return 1;
    }

    protected void startWatchDogTask() {
        taskWatchDog = startTask(watchDogRunnable, 0, 15, TimeUnit.SECONDS);
    }

    protected void stopWatchDogTask() {
        stopTask(taskWatchDog);
        taskWatchDog = null;
    }

    /**
     */
    @Override
    public void dispose() {
        try {
            logger.debug("{}: Stop polling of ZoneMinder Server API", getLogIdentifier());

            logger.info("{}: Stopping Discovery service", getLogIdentifier());
            // Remove the discovery service
            if (discoveryService != null) {
                discoveryService.deactivate();
                discoveryService = null;
            }

            logger.info("{}: Stopping WatchDog task", getLogIdentifier());
            stopWatchDogTask();

            logger.info("{}: Stopping refresh data task", getLogIdentifier());
            stopTask(taskRefreshData);
        } catch (Exception ex) {
        }
    }

    protected String getThingId() {
        return getThing().getUID().getId();
    }

    @Override
    public String getZoneMinderId() {
        return getThing().getUID().getAsString();
    }

    protected ArrayList<IZoneMinderMonitorData> getMonitors(IZoneMinderSession session) {
        if (isConnected()) {
            return ZoneMinderFactory.getServerProxy(session).getMonitors();
        }

        return new ArrayList<>();
    }

    protected ZoneMinderBridgeServerConfig getBridgeConfig() {
        return this.getConfigAs(ZoneMinderBridgeServerConfig.class);
    }

    /**
    *
    */
    public ZoneMinderBaseThingHandler getZoneMinderThingHandlerFromZoneMinderId(ThingTypeUID thingTypeUID,
            String zoneMinderId) {
        // Inform thing handlers of connection
        List<Thing> things = getThing().getThings();

        for (Thing thing : things) {
            ZoneMinderBaseThingHandler thingHandler = (ZoneMinderBaseThingHandler) thing.getHandler();

            if ((thingHandler != null) && (thingHandler.getZoneMinderId().equals(zoneMinderId))
                    && (thing.getThingTypeUID().equals(thingTypeUID))) {
                return thingHandler;
            }
        }
        return null;
    }

    @Override
    public Collection<Class<? extends ThingHandlerService>> getServices() {
        return Collections.singleton(ZoneMinderDiscoveryService.class);
    }

    @Override
    public void handleCommand(ChannelUID channelUID, Command command) {
        logger.debug("{}: Update '{}' with '{}'", getLogIdentifier(), channelUID.getAsString(), command.toString());
    }

    protected synchronized void refreshThing(IZoneMinderSession session, boolean fetchDiskUsage) {
        logger.debug("{}: 'refreshThing()': Thing='{}'!", getLogIdentifier(), this.getThing().getUID());

        List<Channel> channels = getThing().getChannels();
        List<Thing> things = getThing().getThings();

        IZoneMinderServer zoneMinderServerProxy = ZoneMinderFactory.getServerProxy(session);
        if (zoneMinderServerProxy == null) {
            logger.warn("{}:  Could not obtain ZonerMinderServerProxy ", getLogIdentifier());

            // Make sure old data is cleared
            channelCpuLoad = "";
            channelDiskUsage = "";
        } else if (isConnected()) {
            /*
             * Fetch data for Bridge
             */
            IZoneMinderHostLoad hostLoad = null;
            try {
                hostLoad = zoneMinderServerProxy.getHostCpuLoad();
                logger.debug("{}: URL='{}' ResponseCode='{}' ResponseMessage='{}'", getLogIdentifier(),
                        zoneMinderServerProxy.getHttpUrl(), zoneMinderServerProxy.getHttpResponseCode(),
                        zoneMinderServerProxy.getHttpResponseMessage());
            } catch (FailedLoginException | ZoneMinderUrlNotFoundException | IOException ex) {
                logger.error("{}: Exception thrown in call to ZoneMinderHostLoad: ", getLogIdentifier(), ex);
            }

            if (hostLoad == null) {
                logger.warn("{}: ZoneMinderHostLoad dataset could not be obtained (received 'null')",
                        getLogIdentifier());
            } else if (hostLoad.getHttpResponseCode() != 200) {
                logger.warn(
                        "BRIDGE [{}]: ZoneMinderHostLoad dataset could not be obtained (HTTP Response: Code='{}', Message='{}')",
                        getThingId(), hostLoad.getHttpResponseCode(), hostLoad.getHttpResponseMessage());
            } else {
                channelCpuLoad = hostLoad.getCpuLoad().toString();
            }

            if (fetchDiskUsage) {
                IZoneMinderDiskUsage diskUsage = null;
                try {
                    diskUsage = zoneMinderServerProxy.getHostDiskUsage();
                    logger.debug("{}: URL='{}' ResponseCode='{}' ResponseMessage='{}'", getLogIdentifier(),
                            zoneMinderServerProxy.getHttpUrl(), zoneMinderServerProxy.getHttpResponseCode(),
                            zoneMinderServerProxy.getHttpResponseMessage());
                } catch (Exception ex) {
                    logger.error("{}: Exception thrown in call to ZoneMinderDiskUsage: ", getLogIdentifier(), ex);
                }

                if (diskUsage == null) {
                    logger.warn("{}: ZoneMinderDiskUsage dataset could not be obtained (received 'null')",
                            getLogIdentifier());
                } else if (diskUsage.getHttpResponseCode() != 200) {
                    logger.warn(
                            "{}: ZoneMinderDiskUsage dataset could not be obtained (HTTP Response: Code='{}', Message='{}')",
                            getLogIdentifier(), diskUsage.getHttpResponseCode(), diskUsage.getHttpResponseMessage());
                } else {
                    channelDiskUsage = diskUsage.getDiskUsage();
                }
            }
        } else {
            online = false;
            // Make sure old data is cleared
            channelCpuLoad = "";
            channelDiskUsage = "";
        }

        /*
         * Update all channels on Bridge
         */
        for (Channel channel : channels) {
            updateChannel(channel.getUID());
        }

        /*
         * Request Things attached to Bridge to refresh
         */
        for (Thing thing : things) {
            try {
                if (thing.getThingTypeUID().equals(ZoneMinderConstants.THING_TYPE_THING_ZONEMINDER_MONITOR)) {
                    ZoneMinderBaseThingHandler thingHandler = (ZoneMinderBaseThingHandler) thing.getHandler();
                    if (thingHandler == null) {
                        continue;
                    }
                    thingHandler.refreshThing(session, DataRefreshPriorityEnum.SCHEDULED);
                }
            } catch (Exception ex) { // Other exceptions has to be shown as errors
                logger.warn("{}: Method 'refreshThing()' for Bridge {} failed for thing='{}' - Exception='{}'",
                        getLogIdentifier(), this.getZoneMinderId(), thing.getUID(), ex.getMessage());
            }
        }
    }

    /**
     * Returns connection status.
     */
    public synchronized Boolean isConnected() {
        return connected;
    }

    public boolean isOnline() {
        return online;
    }

    /**
     * Set connection status.
     *
     * @param connected
     */
    private synchronized void setConnected(boolean connected) {
        if (this.connected != connected) {
            if (connected) {
                try {
                    zoneMinderSession = ZoneMinderFactory.CreateSession(zoneMinderConnection);
                } catch (FailedLoginException | IllegalArgumentException | IOException
                        | ZoneMinderUrlNotFoundException e) {
                    logger.error("BRIDGE [{}]: Call to setConencted failed with exception '{}'", getThingId(),
                            e.getMessage());
                }
            } else {
                zoneMinderSession = null;
            }
            this.connected = connected;
        }
    }

    /**
     * Set channel 'bridge_connection'.
     *
     * @param connected
     */
    private void setBridgeConnectionStatus(boolean connected) {
        logger.debug(" {}: setBridgeConnection(): Set Bridge to {}", getLogIdentifier(),
                connected ? ThingStatus.ONLINE : ThingStatus.OFFLINE);

        Bridge bridge = getBridge();
        if (bridge != null) {
            ThingStatus status = bridge.getStatus();
            logger.debug("{}: Bridge ThingStatus is: {}", getLogIdentifier(), status);
        }

        setConnected(connected);
    }

    /**
     * Runs when connection established.
     *
     * @throws ZoneMinderUrlNotFoundException
     * @throws IOException
     * @throws GeneralSecurityException
     * @throws IllegalArgumentException
     */
    public void onConnected() {
        logger.debug("BRIDGE [{}]: onConnected(): Bridge Connected!", getThingId());
        setConnected(true);
        onBridgeConnected(this, zoneMinderConnection);

        // Inform thing handlers of connection
        List<Thing> things = getThing().getThings();

        for (Thing thing : things) {
            ZoneMinderBaseThingHandler thingHandler = (ZoneMinderBaseThingHandler) thing.getHandler();

            if (thingHandler != null) {
                try {
                    thingHandler.onBridgeConnected(this, zoneMinderConnection);
                } catch (IllegalArgumentException | GeneralSecurityException | IOException
                        | ZoneMinderUrlNotFoundException e) {
                    logger.error("{}: onConnected() failed - Exceprion: {}", getLogIdentifier(), e.getMessage());
                }
                logger.debug("{}: onConnected(): Bridge - {}, Thing - {}, Thing Handler - {}", getLogIdentifier(),
                        thing.getBridgeUID(), thing.getUID(), thingHandler);
            }
        }
    }

    /**
     * Runs when disconnected.
     */
    private void onDisconnected() {
        logger.debug("{}: onDisconnected(): Bridge Disconnected!", getLogIdentifier());
        setConnected(false);
        onBridgeDisconnected(this);

        // Inform thing handlers of disconnection
        List<Thing> things = getThing().getThings();

        for (Thing thing : things) {
            ZoneMinderBaseThingHandler thingHandler = (ZoneMinderBaseThingHandler) thing.getHandler();

            if (thingHandler != null) {
                thingHandler.onBridgeDisconnected(this);
                logger.debug("{}: onDisconnected(): Bridge - {}, Thing - {}, Thing Handler - {}", getLogIdentifier(),
                        thing.getBridgeUID(), thing.getUID(), thingHandler);
            }
        }
    }

    @Override
    public void updateAvaliabilityStatus(IZoneMinderConnectionInfo connection) {
        ThingStatus newStatus = ThingStatus.OFFLINE;
        ThingStatusDetail statusDetail = ThingStatusDetail.NONE;
        String statusDescription = "";

        boolean isOnline = false;

        ThingStatus prevStatus = getThing().getStatus();

        try {
            // Just perform a health check to see if we are still connected
            if (prevStatus == ThingStatus.ONLINE) {
                if (zoneMinderSession == null) {
                    newStatus = ThingStatus.ONLINE;
                    statusDetail = ThingStatusDetail.NONE;
                    statusDescription = "";
                    updateBridgeStatus(newStatus, statusDetail, statusDescription);
                    return;
                } else if (!zoneMinderSession.isConnected()) {
                    newStatus = ThingStatus.OFFLINE;
                    statusDetail = ThingStatusDetail.COMMUNICATION_ERROR;
                    statusDescription = "Session lost connection to ZoneMinder Server";
                    updateBridgeStatus(newStatus, statusDetail, statusDescription);

                    return;
                }

                IZoneMinderServer serverProxy = ZoneMinderFactory.getServerProxy(zoneMinderSession);
                IZoneMinderDaemonStatus daemonStatus = serverProxy.getHostDaemonCheckState();

                // If service isn't running OR we revceived a http responsecode other than 200, assume we are offline
                if ((!daemonStatus.getStatus()) || (daemonStatus.getHttpResponseCode() != 200)) {
                    newStatus = ThingStatus.OFFLINE;
                    statusDetail = ThingStatusDetail.COMMUNICATION_ERROR;
                    statusDescription = "ZoneMinder Server Daemon not running";

                    logger.debug("{}: {} (state='{}' and ResponseCode='{}')", getLogIdentifier(), statusDescription,
                            daemonStatus.getStatus(), daemonStatus.getHttpResponseCode());
                    updateBridgeStatus(newStatus, statusDetail, statusDescription);
                    return;
                }

                // TODO:: Check other things without being harsh????

                newStatus = ThingStatus.ONLINE;
                statusDetail = ThingStatusDetail.NONE;
                statusDescription = "";
            }
            // If we are OFFLINE, check everything
            else if (prevStatus == ThingStatus.OFFLINE) {
                // Just wait until we are finished initializing
                if (!isInitialized) {
                    online = isOnline;
                    return;
                }

                ZoneMinderBridgeServerConfig config = getBridgeConfig();

                // Check if server Bridge configuration is valid
                if (config == null) {
                    newStatus = ThingStatus.OFFLINE;
                    statusDetail = ThingStatusDetail.CONFIGURATION_ERROR;
                    statusDescription = "Configuration not found";
                    updateBridgeStatus(newStatus, statusDetail, statusDescription);
                    return;
                } else if (config.getHostName() == null) {
                    newStatus = ThingStatus.OFFLINE;
                    statusDetail = ThingStatusDetail.CONFIGURATION_ERROR;
                    statusDescription = "Host not found in configuration";
                    updateBridgeStatus(newStatus, statusDetail, statusDescription);
                    return;
                } else if (config.getProtocol() == null) {
                    newStatus = ThingStatus.OFFLINE;
                    statusDetail = ThingStatusDetail.CONFIGURATION_ERROR;
                    statusDescription = "Unknown protocol in configuration";
                    updateBridgeStatus(newStatus, statusDetail, statusDescription);
                    return;
                }

                else if (config.getHttpPort() == null) {
                    newStatus = ThingStatus.OFFLINE;
                    statusDetail = ThingStatusDetail.CONFIGURATION_ERROR;
                    statusDescription = "Invalid HTTP port";
                    updateBridgeStatus(newStatus, statusDetail, statusDescription);
                    return;
                }

                else if (config.getTelnetPort() == null) {
                    newStatus = ThingStatus.OFFLINE;
                    statusDetail = ThingStatusDetail.CONFIGURATION_ERROR;
                    statusDescription = "Invalid telnet port";
                    updateBridgeStatus(newStatus, statusDetail, statusDescription);
                    return;
                } else if (!ZoneMinderFactory.isZoneMinderUrl(connection)) {
                    newStatus = ThingStatus.OFFLINE;
                    statusDetail = ThingStatusDetail.CONFIGURATION_ERROR;
                    statusDescription = "URL not a ZoneMinder Server";
                    updateBridgeStatus(newStatus, statusDetail, statusDescription);
                    return;
                }

                if (!isZoneMinderLoginValid(connection)) {
                    newStatus = ThingStatus.OFFLINE;
                    statusDetail = ThingStatusDetail.CONFIGURATION_ERROR;
                    statusDescription = "Cannot access ZoneMinder Server. Check provided usercredentials";
                    updateBridgeStatus(newStatus, statusDetail, statusDescription);
                    return;
                }

                /*
                 * Now we will try to establish a session
                 */

                IZoneMinderSession curSession = null;
                try {
                    curSession = ZoneMinderFactory.CreateSession(connection);
                } catch (FailedLoginException | IllegalArgumentException | IOException
                        | ZoneMinderUrlNotFoundException ex) {
                    logger.error("{}: Create Session failed with exception {}", getLogIdentifier(), ex.getMessage());

                    newStatus = ThingStatus.OFFLINE;
                    statusDetail = ThingStatusDetail.COMMUNICATION_ERROR;
                    statusDescription = "Failed to connect. (Check Log)";
                    if (curBridgeStatus != ThingStatus.OFFLINE) {
                        logger.error("{}: Bridge OFFLINE because of '{}' Exception='{}'", getLogIdentifier(),
                                statusDescription, ex.getMessage());
                    }
                    updateBridgeStatus(newStatus, statusDetail, statusDescription);
                    return;
                }
                IZoneMinderServer serverProxy = ZoneMinderFactory.getServerProxy(curSession);

                // Check if server API can be accessed
                if (!serverProxy.isApiEnabled()) {
                    newStatus = ThingStatus.OFFLINE;
                    statusDetail = ThingStatusDetail.CONFIGURATION_ERROR;
                    statusDescription = "ZoneMinder Server 'OPT_USE_API' not enabled";
                    updateBridgeStatus(newStatus, statusDetail, statusDescription);
                    return;
                } else if (!serverProxy.getHostDaemonCheckState().getStatus()) {
                    newStatus = ThingStatus.OFFLINE;
                    statusDetail = ThingStatusDetail.CONFIGURATION_ERROR;
                    statusDescription = "ZoneMinder Server Daemon not running";
                    updateBridgeStatus(newStatus, statusDetail, statusDescription);
                    return;
                }
                // Verify that 'OPT_TRIGGER' is set to true in ZoneMinder
                else if (!serverProxy.isTriggerOptionEnabled()) {
                    newStatus = ThingStatus.OFFLINE;
                    statusDetail = ThingStatusDetail.CONFIGURATION_ERROR;
                    statusDescription = "ZoneMinder Server option 'OPT_TRIGGERS' not enabled";
                    updateBridgeStatus(newStatus, statusDetail, statusDescription);
                    return;
                } else {
                    // Seems like everything is as we want it :-)
                    isOnline = true;
                }

                if (isOnline) {
                    zoneMinderSession = curSession;
                    online = isOnline;
                    newStatus = ThingStatus.ONLINE;
                    statusDetail = ThingStatusDetail.NONE;
                    statusDescription = "";
                } else {
                    zoneMinderSession = null;
                    online = isOnline;
                    newStatus = ThingStatus.OFFLINE;
                }
            }
        } catch (Exception ex) {
            newStatus = ThingStatus.OFFLINE;
            statusDetail = ThingStatusDetail.COMMUNICATION_ERROR;
            logger.error("{}: Exception occurred in updateAvailabilityStatus Exception='{}'", getLogIdentifier(),
                    ex.getMessage());
            statusDescription = "Error occurred (Check log)";
        }
        updateBridgeStatus(newStatus, statusDetail, statusDescription);

        // Ask child things to update their Availability Status
        for (Thing thing : getThing().getThings()) {
            ZoneMinderBaseThingHandler thingHandler = (ZoneMinderBaseThingHandler) thing.getHandler();
            if (thingHandler instanceof ZoneMinderThingMonitorHandler) {
                try {
                    thingHandler.updateAvaliabilityStatus(connection);
                } catch (Exception ex) {
                    logger.debug("{}: Failed to call 'updateAvailabilityStatus()' for '{}'", getLogIdentifier(),
                            thingHandler.getThing().getUID());
                }
            }
        }
    }

    @SuppressWarnings("null")
    protected void updateBridgeStatus(ThingStatus newStatus, ThingStatusDetail statusDetail, String statusDescription) {
        ThingStatusInfo curStatusInfo = thing.getStatusInfo();

        String curDescription = curStatusInfo.getDescription();
        if (StringUtils.isBlank(curStatusInfo.getDescription())) {
            curDescription = "";
        }

        // Status changed, curDescription is null checked
        if ((curStatusInfo.getStatus() != newStatus) || (curStatusInfo.getStatusDetail() != statusDetail)
                || (!curDescription.equals(statusDescription))) {
            // if (thing.getStatus() != newStatus) {
            logger.info("{}: Bridge status changed from '{}' to '{}'", getLogIdentifier(), thing.getStatus(),
                    newStatus);

            if ((newStatus == ThingStatus.ONLINE) && (curStatusInfo.getStatus() != ThingStatus.ONLINE)) {
                try {
                    setBridgeConnectionStatus(true);
                    onConnected();
                } catch (IllegalArgumentException e) {
                    // Just ignore that here
                }
            } else if ((newStatus == ThingStatus.OFFLINE) && (curStatusInfo.getStatus() != ThingStatus.OFFLINE)) {
                try {
                    setBridgeConnectionStatus(false);
                    onDisconnected();
                } catch (IllegalArgumentException e) {
                    // Just ignore that here
                }
            }
            // Update Status correspondingly
            if ((newStatus == ThingStatus.OFFLINE) && (statusDetail != ThingStatusDetail.NONE)) {
                updateStatus(newStatus, statusDetail, statusDescription);
            } else {
                updateStatus(newStatus);
            }

            curBridgeStatus = newStatus;
        }
    }

    protected boolean isZoneMinderLoginValid(IZoneMinderConnectionInfo connection) {
        try {
            return ZoneMinderFactory.validateLogin(connection);
        } catch (Exception e) {
            return false;
        }
    }

    @Override
    public void updateChannel(ChannelUID channel) {
        State state = null;
        try {
            switch (channel.getId()) {
                case ZoneMinderConstants.CHANNEL_ONLINE:
                    updateState(channel, (isOnline() ? OnOffType.ON : OnOffType.OFF));
                    break;

                case ZoneMinderConstants.CHANNEL_SERVER_DISKUSAGE:
                    state = getServerDiskUsageState();
                    break;

                case ZoneMinderConstants.CHANNEL_SERVER_CPULOAD:
                    state = getServerCpuLoadState();
                    break;

                default:
                    logger.warn("{}: updateChannel(): Server '{}': No handler defined for channel='{}'",
                            getLogIdentifier(), thing.getLabel(), channel.getAsString());
                    break;
            }

            if (state != null) {
                logger.debug("{}: BridgeHandler.updateChannel(): Updating channel '{}' to state='{}'",
                        getLogIdentifier(), channel.getId(), state.toString());
                updateState(channel.getId(), state);
            }
        } catch (Exception ex) {
            logger.error("{}: Error when 'updateChannel()' was called for thing='{}' (Exception='{}'",
                    getLogIdentifier(), channel.getId(), ex.getMessage());
        }
    }

    protected boolean openConnection() {
        boolean connected = false;
        if (!isConnected()) {
            logger.debug("{}: Connecting Bridge to ZoneMinder Server", getLogIdentifier());

            try {
                if (isConnected()) {
                    closeConnection();
                }
                setConnected(connected);

                logger.info("{}: Connecting to ZoneMinder Server (result='{}'", getLogIdentifier(), connected);

            } catch (Exception exception) {
                logger.error("{}: openConnection(): Exception: ", getLogIdentifier(), exception);
                setConnected(false);
            } finally {
                if (!isConnected()) {
                    closeConnection();
                }
            }

        }
        return isConnected();
    }

    synchronized void closeConnection() {
        try {
            logger.debug("{}: closeConnection(): Closed HTTP Connection!", getLogIdentifier());
            setConnected(false);

        } catch (Exception exception) {
            logger.error("{}: closeConnection(): Error closing connection - {}", getLogIdentifier(),
                    exception.getMessage());
        }

    }

    protected State getServerCpuLoadState() {
        State state = UnDefType.UNDEF;

        try {
            if ((channelCpuLoad != "") && (isConnected())) {
                state = new DecimalType(new BigDecimal(channelCpuLoad));
            }

        } catch (Exception ex) {
            // Deliberately kept as debug info!
            logger.debug("{}: Exception='{}'", getLogIdentifier(), ex.getMessage());
        }

        return state;
    }

    protected State getServerDiskUsageState() {
        State state = UnDefType.UNDEF;

        try {
            if ((channelDiskUsage != "") && (isConnected())) {
                state = new DecimalType(new BigDecimal(channelDiskUsage));
            }
        } catch (Exception ex) {
            // Deliberately kept as debug info!
            logger.debug("{}: Exception {}", getLogIdentifier(), ex.getMessage());
        }

        return state;
    }

    @Override
    public void onBridgeConnected(ZoneMinderServerBridgeHandler bridge, IZoneMinderConnectionInfo connection) {
        logger.info("{}: Brigde went ONLINE", getLogIdentifier());

        if (taskRefreshData == null) {
            // Perform first refresh manually (we want to force update of DiskUsage)
            boolean updateDiskUsage = (getBridgeConfig().getRefreshIntervalLowPriorityTask() > 0) ? true : false;
            refreshThing(zoneMinderSession, updateDiskUsage);

            if (getBridgeConfig().getRefreshIntervalLowPriorityTask() != 0) {
                refreshFrequency = calculateCommonRefreshFrequency(getBridgeConfig().getRefreshInterval());
            } else {
                refreshFrequency = getBridgeConfig().getRefreshInterval();
            }
            logger.info("BRIDGE [{}]: Calculated refresh inetrval to '{}'", getThingId(), refreshFrequency);

            if (taskRefreshData != null) {
                taskRefreshData.cancel(true);
                taskRefreshData = null;
            }

            // Start job to handle next updates
            taskRefreshData = startTask(refreshDataRunnable, refreshFrequency, refreshFrequency, TimeUnit.SECONDS);

            if (taskPriorityRefreshData != null) {
                taskPriorityRefreshData.cancel(true);
                taskPriorityRefreshData = null;
            }

            // Only start if Priority Frequency is higher than ordinary
            if (refreshFrequency > 1) {
                taskPriorityRefreshData = startTask(refreshPriorityDataRunnable, 0, 1, TimeUnit.SECONDS);
            }
        }

        // Update properties
        updateMonitorProperties(zoneMinderSession);
    }

    @Override
    public void onBridgeDisconnected(ZoneMinderServerBridgeHandler bridge) {
        logger.info("{}: Brigde went OFFLINE", getLogIdentifier());

        // Deactivate discovery service
        discoveryService.deactivate();

        // Stopping refresh thread while OFFLINE
        if (taskRefreshData != null) {
            taskRefreshData.cancel(true);
            taskRefreshData = null;
            logger.debug("{}: Stopping DataRefresh task", getLogIdentifier());
        }

        // Stopping High priority thread while OFFLINE
        if (taskPriorityRefreshData != null) {
            taskPriorityRefreshData.cancel(true);
            taskPriorityRefreshData = null;
            logger.debug("{}: Stopping Priority DataRefresh task", getLogIdentifier());
        }

        // Make sure everything gets refreshed
        for (Channel ch : getThing().getChannels()) {
            handleCommand(ch.getUID(), RefreshType.REFRESH);
        }

        // Inform thing handlers of disconnection
        for (Thing thing : getThing().getThings()) {
            ZoneMinderBaseThingHandler thingHandler = (ZoneMinderBaseThingHandler) thing.getHandler();

            if (thingHandler != null) {
                thingHandler.onBridgeDisconnected(this);
                logger.debug("{}: onDisconnected(): Bridge - {}, Thing - {}, Thing Handler - {}", getLogIdentifier(),
                        thing.getBridgeUID(), thing.getUID(), thingHandler);
            }
        }

    }

    /**
     * Method to start a data refresh task.
     */
    protected ScheduledFuture<?> startTask(Runnable command, long delay, long interval, TimeUnit unit) {
        logger.debug("BRIDGE [{}]: Starting ZoneMinder Bridge Monitor Task. Command='{}'", getThingId(),
                command.toString());
        if (interval == 0) {
            return null;
        }

        return scheduler.scheduleWithFixedDelay(command, delay, interval, unit);
    }

    /**
     * Method to stop the datarefresh task.
     */
    protected void stopTask(ScheduledFuture<?> task) {
        try {
            if (task != null && !task.isCancelled()) {
                logger.debug("{}: Stopping ZoneMinder Bridge Monitor Task. Task='{}'", getLogIdentifier(),
                        task.toString());
                task.cancel(true);
            }
        } catch (Exception ex) {
        }

    }

    public ArrayList<IZoneMinderMonitorData> getMonitors() {
        if (isOnline()) {
            IZoneMinderServer serverProxy = ZoneMinderFactory.getServerProxy(zoneMinderSession);
            ArrayList<IZoneMinderMonitorData> result = serverProxy.getMonitors();

            return result;
        }
        return new ArrayList<>();
    }

    /*
     * This is experimental
     * Try to add different properties
     */
    private void updateMonitorProperties(IZoneMinderSession session) {
        // Update property information about this device
        Map<String, String> properties = editProperties();
        IZoneMinderServer serverProxy = ZoneMinderFactory.getServerProxy(session);

        IZoneMinderHostVersion hostVersion = null;
        try {
            hostVersion = serverProxy.getHostVersion();
            logger.debug("{}: URL='{}' ResponseCode='{}' ResponseMessage='{}'", getLogIdentifier(),
                    serverProxy.getHttpUrl(), serverProxy.getHttpResponseCode(), serverProxy.getHttpResponseMessage());

            ZoneMinderConfig configUseApi = serverProxy.getConfig(ZoneMinderConfigEnum.ZM_OPT_USE_API);
            logger.debug("{}: URL='{}' ResponseCode='{}' ResponseMessage='{}'", getLogIdentifier(),
                    serverProxy.getHttpUrl(), serverProxy.getHttpResponseCode(), serverProxy.getHttpResponseMessage());

            ZoneMinderConfig configUseAuth = serverProxy.getConfig(ZoneMinderConfigEnum.ZM_OPT_USE_AUTH);
            logger.debug("{}: URL='{}' ResponseCode='{}' ResponseMessage='{}'", getLogIdentifier(),
                    serverProxy.getHttpUrl(), serverProxy.getHttpResponseCode(), serverProxy.getHttpResponseMessage());

            ZoneMinderConfig configTrigerrs = serverProxy.getConfig(ZoneMinderConfigEnum.ZM_OPT_TRIGGERS);
            logger.debug("{}: URL='{}' ResponseCode='{}' ResponseMessage='{}'", getLogIdentifier(),
                    configUseApi.getHttpUrl(), configUseApi.getHttpResponseCode(),
                    configUseApi.getHttpResponseMessage());

            properties.put(ZoneMinderProperties.PROPERTY_SERVER_VERSION, hostVersion.getVersion());
            properties.put(ZoneMinderProperties.PROPERTY_SERVER_API_VERSION, hostVersion.getApiVersion());
            properties.put(ZoneMinderProperties.PROPERTY_SERVER_USE_API, configUseApi.getValueAsString());
            properties.put(ZoneMinderProperties.PROPERTY_SERVER_USE_AUTHENTIFICATION, configUseAuth.getValueAsString());
            properties.put(ZoneMinderProperties.PROPERTY_SERVER_TRIGGERS_ENABLED, configTrigerrs.getValueAsString());
        } catch (FailedLoginException | ZoneMinderUrlNotFoundException | IOException e) {
            logger.warn("{}: Exception occurred when updating monitor properties (Exception='{}'", getLogIdentifier(),
                    e.getMessage());
        }

        // Must loop over the new properties since we might have added data
        boolean update = false;
        Map<String, String> originalProperties = editProperties();
        for (String property : properties.keySet()) {
            if ((originalProperties.get(property) == null
                    || !originalProperties.get(property).equals(properties.get(property)))) {
                update = true;
                break;
            }
        }

        if (update) {
            logger.info("{}: Properties synchronised, Thing id: {}", getLogIdentifier(), getThingId());
            updateProperties(properties);
        }
    }

    @Override
    public String getLogIdentifier() {
        String result = "[BRIDGE]";
        try {
            result = String.format("[BRIDGE (%s)]", getThingId());
        } catch (Exception e) {
            result = "[BRIDGE (?)]";
        }
        return result;
    }
}
