/**
 * 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.mpd.internal;

import static org.quartz.JobBuilder.newJob;
import static org.quartz.TriggerBuilder.newTrigger;
import static org.quartz.impl.matchers.GroupMatcher.jobGroupEquals;

import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.lang.StringUtils;
import org.bff.javampd.MPD;
import org.bff.javampd.MPDAdmin;
import org.bff.javampd.MPDDatabase;
import org.bff.javampd.MPDDatabase.ScopeType;
import org.bff.javampd.MPDFile;
import org.bff.javampd.MPDOutput;
import org.bff.javampd.MPDPlayer;
import org.bff.javampd.MPDPlayer.PlayerStatus;
import org.bff.javampd.MPDPlaylist;
import org.bff.javampd.events.OutputChangeEvent;
import org.bff.javampd.events.OutputChangeListener;
import org.bff.javampd.events.PlayerBasicChangeEvent;
import org.bff.javampd.events.PlayerBasicChangeListener;
import org.bff.javampd.events.PlayerChangeEvent;
import org.bff.javampd.events.TrackPositionChangeEvent;
import org.bff.javampd.events.TrackPositionChangeListener;
import org.bff.javampd.events.VolumeChangeEvent;
import org.bff.javampd.events.VolumeChangeListener;
import org.bff.javampd.exception.MPDConnectionException;
import org.bff.javampd.exception.MPDPlayerException;
import org.bff.javampd.exception.MPDResponseException;
import org.bff.javampd.monitor.MPDStandAloneMonitor;
import org.bff.javampd.objects.MPDSong;
import org.openhab.binding.mpd.MpdBindingProvider;
import org.openhab.binding.mpd.internal.MultiClickDetector.MultiClickListener;
import org.openhab.core.binding.AbstractBinding;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.osgi.service.cm.ConfigurationException;
import org.osgi.service.cm.ManagedService;
import org.quartz.CronScheduleBuilder;
import org.quartz.CronTrigger;
import org.quartz.Job;
import org.quartz.JobDetail;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.impl.StdSchedulerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Binding which communicates with (one or many) MPDs. It registers as listener
 * of the MPDs to accomplish real bidirectional communication.
 *
 * @author Thomas.Eichstaedt-Engelen
 * @author Petr.Klus
 * @author Matthew Bowman
 *
 * @since 0.8.0
 */
public class MpdBinding extends AbstractBinding<MpdBindingProvider> implements ManagedService, VolumeChangeListener,
        PlayerBasicChangeListener, TrackPositionChangeListener, MultiClickListener<Command> {

    private static final String MPD_SCHEDULER_GROUP = "MPD";

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

    private Map<String, MpdPlayerConfig> playerConfigCache = new HashMap<String, MpdPlayerConfig>();

    /** RegEx to validate a mpdPlayer config <code>'^(.*?)\\.(host|port|password)$'</code> */
    private static final Pattern EXTRACT_PLAYER_CONFIG_PATTERN = Pattern.compile("^(.*?)\\.(host|port|password)$");

    /** The value by which the volume is changed by each INCREASE or DECREASE-Event */
    private static final int VOLUME_CHANGE_SIZE = 5;

    /** The connection timeout to wait for a MPD connection */
    private static final int CONNECTION_TIMEOUT = 5000;

    private static MultiClickDetector<Command> clickDetector;

    public MpdBinding() {
        playerConfigCache = new HashMap<String, MpdBinding.MpdPlayerConfig>();
        clickDetector = new MultiClickDetector<Command>(this, 300);
    }

    @Override
    public void activate() {
        connectAllPlayersAndMonitors();
    }

    @Override
    public void deactivate() {
        disconnectPlayersAndMonitors();
    }

    /**
     * @{inheritDoc}
     */
    @Override
    public void internalReceiveCommand(String itemName, Command command) {

        MpdBindingProvider provider;
        String matchingPlayerCommand;
        Object params = new Object(); // nothing by default
        if (command instanceof PercentType) {
            // we have received volume adjustment request
            matchingPlayerCommand = "PERCENT";
            params = command;
        } else if (command instanceof DecimalType) {
            // we have received play song id request
            matchingPlayerCommand = "NUMBER";
            params = command;
        } else {
            matchingPlayerCommand = command.toString();
        }

        provider = findFirstMatchingBindingProvider(itemName, matchingPlayerCommand);

        if (provider == null) {
            logger.warn("Cannot find matching binding provider [itemName={}, command={}]", itemName, command);
            return;
        }

        String playerCommand = provider.getPlayerCommand(itemName, matchingPlayerCommand);
        if (StringUtils.isNotBlank(playerCommand)) {
            String playerCommandParam = provider.getPlayerCommandParam(itemName, matchingPlayerCommand);
            if (playerCommandParam != null) {
                params = playerCommandParam;
            }
            executePlayerCommand(playerCommand, params);
        }

    }

    /**
     * Find the first matching {@link MpdBindingProvider} according to
     * <code>itemName</code> and <code>command</code>.
     *
     * @param itemName
     * @param command
     *
     * @return the matching binding provider or <code>null</code> if no binding
     *         provider could be found
     */
    private MpdBindingProvider findFirstMatchingBindingProvider(String itemName, String command) {
        MpdBindingProvider firstMatchingProvider = null;
        for (MpdBindingProvider provider : this.providers) {

            String playerCommand = provider.getPlayerCommand(itemName, command);
            if (playerCommand != null) {
                firstMatchingProvider = provider;
                break;
            }
        }
        return firstMatchingProvider;
    }

    /**
     * Executes the given <code>playerCommandLine</code> on the MPD. The
     * <code>playerCommandLine</code> is split into its properties
     * <code>playerId</code> and <code>playerCommand</code>.
     *
     * @param playerCommandLine the complete commandLine which gets splitted into
     *            its properties.
     */
    private void executePlayerCommand(String playerCommandLine, Object commandParams) {
        String[] commandParts = playerCommandLine.split(":");
        String playerId = commandParts[0];
        String playerCommand = commandParts[1];

        MPD daemon = findMPDInstance(playerId);
        if (daemon == null) {
            // we give that player another chance -> try to reconnect
            reconnect(playerId);
        }

        if (daemon != null) {
            PlayerCommandTypeMapping pCommand = null;
            try {
                pCommand = PlayerCommandTypeMapping.fromString(playerCommand);
                MPDPlayer player = daemon.getMPDPlayer();
                MPDPlaylist playlist = daemon.getMPDPlaylist();
                MPDDatabase db = daemon.getMPDDatabase();

                switch (pCommand) {
                    case PAUSE:
                        player.pause();
                        break;
                    case PLAY:
                        player.play();
                        break;
                    case STOP:
                        player.stop();
                        break;
                    case VOLUME_INCREASE:
                        player.setVolume(player.getVolume() + VOLUME_CHANGE_SIZE);
                        break;
                    case VOLUME_DECREASE:
                        player.setVolume(player.getVolume() - VOLUME_CHANGE_SIZE);
                        break;
                    case NEXT:
                        player.playNext();
                        break;
                    case PREV:
                        player.playPrev();
                        break;
                    case PLAYSONG:
                        logger.debug("Searching for song {}", commandParams);
                        Collection<MPDSong> songs = db.find(ScopeType.TITLE, (String) commandParams);

                        Iterator<MPDSong> it = songs.iterator();
                        if (it.hasNext() == true) {
                            MPDSong song = it.next();

                            logger.debug("Song found: {}", song.getFile());

                            MPDFile file = new MPDFile();
                            file.setPath(song.getFile());

                            playlist.clearPlaylist();
                            playlist.addFileOrDirectory(file);

                        } else {
                            logger.debug("Song not found: {}", commandParams);
                        }

                        break;
                    case PLAYSONGID:
                        logger.debug("Play id {}", ((DecimalType) commandParams).intValue());

                        MPDSong song = new MPDSong();
                        // song.setId(Integer.parseInt((String) commandParams));
                        song.setId(((DecimalType) commandParams).intValue());
                        player.playId(song);

                        break;
                    case ENABLE:
                    case DISABLE:
                        Integer outputId = Integer.valueOf((String) commandParams);
                        MPDAdmin admin = daemon.getMPDAdmin();
                        MPDOutput output = new MPDOutput(outputId - 1); // internally mpd uses 0-based indexing
                        if (pCommand == PlayerCommandTypeMapping.ENABLE) {
                            admin.enableOutput(output);
                        } else {
                            admin.disableOutput(output);
                        }
                        break;
                    case VOLUME:
                        logger.debug("Volume adjustment received: '{}' '{}'", pCommand, commandParams);
                        player.setVolume(((PercentType) commandParams).intValue());
                        break;

                }
            } catch (MPDPlayerException pe) {
                logger.error("Error while executing '{}' command: {}", pCommand, pe.getMessage());
            } catch (Exception e) {
                logger.warn("Unknown player command '{}'", playerCommand);
            }

            logger.info("Executed command '{}' for player '{}'", playerCommand, playerId);
        } else {
            logger.warn("Player '{}' was not found or is not connected", playerId);
        }
    }

    private MPD findMPDInstance(String playerId) {
        MpdPlayerConfig playerConfig = playerConfigCache.get(playerId);
        if (playerConfig != null) {
            return playerConfig.instance;
        }
        return null;
    }

    private String findPlayerId(Object object) {
        for (String playerId : playerConfigCache.keySet()) {
            MpdPlayerConfig playerConfig = playerConfigCache.get(playerId);
            if (playerConfig != null && playerConfig.monitor != null && playerConfig.monitor.equals(object)) {
                return playerId;
            }
        }
        return null;
    }

    /**
     * Implementation of {@link PlayerBasicChangeListener}. Posts the translated
     * type of the {@link PlayerChangeEvent} onto the internal event bus.
     * <code>PLAYER_STARTED</code>-Events are translated to <code>ON</code> whereas
     * <code>PLAYER_PAUSED</code> and <code>PLAYER_STOPPED</code>-Events are
     * translated to <code>OFF</code>.
     *
     * In this case, we use the play state change to trigger full state update, including
     * artist and song name.
     *
     * @param pbce the event which type is translated and posted onto the internal
     *            event bus
     */
    @Override
    public void playerBasicChange(PlayerBasicChangeEvent pbce) {
        String playerId = findPlayerId(pbce.getSource());

        // trigger track name update
        determineSongChange(playerId);

        // trigger our play state change
        determinePlayStateChange(playerId);
    }

    HashMap<String, MPDSong> songInfoCache = new HashMap<String, MPDSong>();
    HashMap<String, PlayerStatus> playerStatusCache = new HashMap<String, PlayerStatus>();

    @Override
    public void trackPositionChanged(TrackPositionChangeEvent tpce) {

        // cache song name internally, we do not want to fire every time
        String playerId = findPlayerId(tpce.getSource());
        // update track name
        determineSongChange(playerId);

        // also update play state
        determinePlayStateChange(playerId);
    }

    private void broadcastPlayerStateChange(String playerId, PlayerCommandTypeMapping reportTo,
            OnOffType reportStatus) {
        String[] itemNames = getItemNamesByPlayerAndPlayerCommand(playerId, reportTo);
        for (String itemName : itemNames) {
            if (StringUtils.isNotBlank(itemName)) {
                eventPublisher.postUpdate(itemName, reportStatus);
            }
        }
    }

    private void determinePlayStateChange(String playerId) {
        MPD daemon = findMPDInstance(playerId);
        if (daemon == null) {
            // we give that player another chance -> try to reconnect
            reconnect(playerId);
        }
        if (daemon != null) {
            try {
                MPDPlayer player = daemon.getMPDPlayer();

                // get the song object here
                PlayerStatus ps = player.getStatus();

                PlayerStatus curPs = playerStatusCache.get(playerId);
                if (curPs != null) {
                    if (ps != curPs) {
                        logger.debug("Play state of player '{}' changed", playerId);
                        playerStatusCache.put(playerId, ps);

                        PlayerCommandTypeMapping reportTo;
                        OnOffType reportStatus;
                        // trigger song update
                        if (ps.equals(PlayerStatus.STATUS_PAUSED) || ps.equals(PlayerStatus.STATUS_STOPPED)) {
                            // stopped
                            reportTo = PlayerCommandTypeMapping.STOP;
                            reportStatus = OnOffType.OFF;
                        } else {
                            // playing
                            reportTo = PlayerCommandTypeMapping.PLAY;
                            reportStatus = OnOffType.ON;
                        }
                        broadcastPlayerStateChange(playerId, reportTo, reportStatus);
                    } else {
                        // nothing, same state
                    }
                } else {
                    playerStatusCache.put(playerId, ps);
                }
            } catch (MPDPlayerException pe) {
                logger.error("Error while updating player status for player '{}': {}", playerId, pe.getMessage());
            } catch (Exception e) {
                logger.warn("Failed to communicate with player '{}'", playerId);
            }
        } else {
            logger.warn("Player '{}' was not found or is not connected", playerId);
        }
    }

    private void determineSongChange(String playerId) {
        MPD daemon = findMPDInstance(playerId);
        if (daemon == null) {
            // we give that player another chance -> try to reconnect
            reconnect(playerId);
        }
        if (daemon != null) {
            try {
                MPDPlayer player = daemon.getMPDPlayer();

                // get the song object here
                MPDSong curSong = player.getCurrentSong();

                MPDSong curSongCache = songInfoCache.get(playerId);
                if (!songsEqual(curSong, curSongCache)) {
                    // song is different (or not in cache), update cache
                    songInfoCache.put(playerId, curSong);
                    // action the song change&notification
                    songChanged(playerId, curSong);
                }
            } catch (MPDPlayerException pe) {
                logger.error("Error while updating player status for player '{}': {}", playerId, pe.getMessage());
            } catch (Exception e) {
                logger.warn("Failed to communicate with player '{}'", playerId);
            }
        } else {
            logger.warn("Player '{}' was not found or is not connected", playerId);
        }
    }

    private boolean songsEqual(MPDSong song1, MPDSong song2) {
        if (!StringUtils.equals(getTitle(song1), getTitle(song2))) {
            return false;
        }

        return StringUtils.equals(getArtist(song1), getArtist(song2));
    }

    private String getTitle(MPDSong song) {
        if (song == null || song.getTitle() == null) {
            return "";
        }

        return song.getTitle().toString();
    }

    private String getArtist(MPDSong song) {
        if (song == null || song.getArtist() == null) {
            return "";
        }

        return song.getArtist().toString();
    }

    private void songChanged(String playerId, MPDSong newSong) {
        String title = getTitle(newSong);
        logger.debug("Current song {}: {}", playerId, title);

        String[] itemNames = getItemNamesByPlayerAndPlayerCommand(playerId, PlayerCommandTypeMapping.TRACKINFO);
        // move to utilities?
        for (String itemName : itemNames) {
            if (StringUtils.isNotBlank(itemName)) {
                eventPublisher.postUpdate(itemName, new StringType(title));
                logger.debug("Updated title: {}, {}", itemName, title);
            }
        }

        String artist = getArtist(newSong);
        itemNames = getItemNamesByPlayerAndPlayerCommand(playerId, PlayerCommandTypeMapping.TRACKARTIST);
        for (String itemName : itemNames) {
            if (StringUtils.isNotBlank(itemName)) {
                eventPublisher.postUpdate(itemName, new StringType(artist));
                logger.debug("Updated artist: {}, {}", itemName, artist);
            }
        }

        int songID = newSong.getId();
        itemNames = getItemNamesByPlayerAndPlayerCommand(playerId, PlayerCommandTypeMapping.PLAYSONGID);
        for (String itemName : itemNames) {
            if (StringUtils.isNotBlank(itemName)) {
                eventPublisher.postUpdate(itemName, new DecimalType(songID));
                logger.debug("Updated song ID: {}, {}", itemName, songID);
            }
        }
    }

    /**
     * More advanced state detection - allows to detect track changes etc.
     * However, the underlying MPD library does not seem to support this well
     */
    public void playerChanged(PlayerChangeEvent pce) {

    }

    /**
     * Implementation of {@link VolumeChangeListener}. Posts the volume value
     * of the {@link VolumeChangeEvent} onto the internal event bus.
     *
     * @param vce the event which volume value is posted onto the internal event bus
     */
    @Override
    public void volumeChanged(VolumeChangeEvent vce) {
        String playerId = findPlayerId(vce.getSource());
        int volume = vce.getVolume();
        if (volumeInvalid(volume)) {
            logger.warn("Ignoring volume change because the volume is invalid: {}", volume);
        } else {
            logger.debug("Volume on player '{}' changed to {}", playerId, volume);
            String[] itemNames = getItemNamesByPlayerAndPlayerCommand(playerId, PlayerCommandTypeMapping.VOLUME);
            for (String itemName : itemNames) {
                if (StringUtils.isNotBlank(itemName)) {
                    eventPublisher.postUpdate(itemName, new PercentType(volume));
                }
            }
        }
    }

    /**
     * Handles MPD output change events.
     *
     * @param playerId the playerId which generated the <code>event</code>
     * @param event the {@link OutputChangeEvent} that occurred
     *
     * @since 1.6.0
     */
    private void outputChanged(String playerId, OutputChangeEvent event) {
        MPDOutput output = (MPDOutput) event.getSource();
        logger.debug("Output {} changed on player '{}'. Enabled = {}", output.getId(), playerId, output.isEnabled());
        PlayerCommandTypeMapping playerCommand = output.isEnabled() ? PlayerCommandTypeMapping.ENABLE
                : PlayerCommandTypeMapping.DISABLE;
        String[] itemNames = getItemsByPlayerCommandAndOutput(playerId, playerCommand, output);
        for (String itemName : itemNames) {
            eventPublisher.postUpdate(itemName, (State) playerCommand.type);
        }
    }

    private String[] getItemsByPlayerCommandAndOutput(String playerId, PlayerCommandTypeMapping playerCommand,
            MPDOutput output) {
        Set<String> itemNames = new HashSet<String>();
        int outputId = output.getId() + 1; // internally mpd uses 0-based indexes
        for (MpdBindingProvider provider : this.providers) {
            itemNames.addAll(
                    Arrays.asList(provider.getItemNamesByPlayerOutputCommand(playerId, playerCommand, outputId)));
        }
        return itemNames.toArray(new String[itemNames.size()]);
    }

    private String[] getItemNamesByPlayerAndPlayerCommand(String playerId, PlayerCommandTypeMapping playerCommand) {
        Set<String> itemNames = new HashSet<String>();
        for (MpdBindingProvider provider : this.providers) {
            itemNames.addAll(Arrays.asList(provider.getItemNamesByPlayerAndPlayerCommand(playerId, playerCommand)));
        }
        return itemNames.toArray(new String[itemNames.size()]);
    }

    protected void addBindingProvider(MpdBindingProvider bindingProvider) {
        super.addBindingProvider(bindingProvider);
    }

    protected void removeBindingProvider(MpdBindingProvider bindingProvider) {
        super.removeBindingProvider(bindingProvider);
    }

    @Override
    @SuppressWarnings("rawtypes")
    public void updated(Dictionary config) throws ConfigurationException {
        if (config != null) {
            disconnectPlayersAndMonitors();
            cancelScheduler();

            Enumeration keys = config.keys();
            while (keys.hasMoreElements()) {

                String key = (String) keys.nextElement();

                // the config-key enumeration contains additional keys that we
                // don't want to process here ...
                if ("service.pid".equals(key)) {
                    continue;
                }

                Matcher matcher = EXTRACT_PLAYER_CONFIG_PATTERN.matcher(key);
                if (!matcher.matches()) {
                    logger.debug(
                            "MPD player config-key '{}' does not follow the expected pattern '<playername>.<host|port>'",
                            key);
                    continue;
                }

                matcher.reset();
                matcher.find();

                String playerId = matcher.group(1);

                MpdPlayerConfig playerConfig = playerConfigCache.get(playerId);
                if (playerConfig == null) {
                    playerConfig = new MpdPlayerConfig();
                    playerConfigCache.put(playerId, playerConfig);
                }

                String configKey = matcher.group(2);
                String value = (String) config.get(key);

                if ("host".equals(configKey)) {
                    playerConfig.host = value;
                } else if ("port".equals(configKey)) {
                    playerConfig.port = Integer.valueOf(value);
                } else if ("password".equals(configKey)) {
                    playerConfig.password = value;
                } else {
                    throw new ConfigurationException(configKey, "the given configKey '" + configKey + "' is unknown");
                }

            }

            connectAllPlayersAndMonitors();
            scheduleReconnect();
        }
    }

    private void scheduleReconnect() {
        Scheduler sched;
        try {
            sched = StdSchedulerFactory.getDefaultScheduler();
            JobDetail job = newJob(ReconnectJob.class).withIdentity("Reconnect", MPD_SCHEDULER_GROUP).build();
            CronTrigger trigger = newTrigger().withIdentity("Reconnect", MPD_SCHEDULER_GROUP)
                    .withSchedule(CronScheduleBuilder.cronSchedule("0 0 0 * * ?")).build();

            sched.scheduleJob(job, trigger);
            logger.debug("Scheduled a daily reconnect of all MPDs");
        } catch (SchedulerException se) {
            logger.warn("Scheduling MPD reconnect failed", se);
        }
    }

    /**
     * Delete all quartz scheduler jobs of the group <code>MPD</code>.
     */
    private void cancelScheduler() {
        try {
            Scheduler sched = StdSchedulerFactory.getDefaultScheduler();
            Set<JobKey> jobKeys = sched.getJobKeys(jobGroupEquals(MPD_SCHEDULER_GROUP));
            if (jobKeys.size() > 0) {
                sched.deleteJobs(new ArrayList<JobKey>(jobKeys));
                logger.debug("Found {} jobs to delete from the DefaultScheduler (keys={})", jobKeys.size(), jobKeys);
            }
        } catch (SchedulerException e) {
            logger.warn("Couldn't remove job(s): {}", e.getMessage());
        }
    }

    /**
     * Connects to all configured {@link MPD}s
     */
    private void connectAllPlayersAndMonitors() {
        logger.debug("MPD binding connecting to players");
        for (String playerId : playerConfigCache.keySet()) {
            connect(playerId);
        }
    }

    /**
     * Connects the player <code>playerId</code> to the given <code>host</code>
     * and <code>port</code> and registers this binding as MPD-Event listener.
     *
     * @param playerId
     * @param host
     * @param port
     */
    private void connect(final String playerId) {
        MpdPlayerConfig config = null;
        try {

            config = playerConfigCache.get(playerId);
            if (config != null && config.instance == null) {

                MPD mpd = new MPD(config.host, config.port, config.password, CONNECTION_TIMEOUT);

                MPDStandAloneMonitor mpdStandAloneMonitor = new MPDStandAloneMonitor(mpd, 500);
                mpdStandAloneMonitor.addVolumeChangeListener(this);
                mpdStandAloneMonitor.addPlayerChangeListener(this);
                mpdStandAloneMonitor.addTrackPositionChangeListener(this);

                final MpdBinding self = this; // 'this' glue for the inner anon instance
                mpdStandAloneMonitor.addOutputChangeListener(new OutputChangeListener() {

                    @Override
                    public void outputChanged(OutputChangeEvent e) {
                        // We have to 'wrap' the OutputChangeEvent listener
                        // callback and add the playerId so we know which
                        // player generated the event. There's not enough
                        // info on just the OutputChangeEvent to derive
                        // the source player. This 'workaround' is necessary
                        // to support output control on multiple MPD players.
                        self.outputChanged(playerId, e);

                    }

                });

                Thread monitorThread = new Thread(mpdStandAloneMonitor, "MPD Monitor (player:" + playerId + ")");
                monitorThread.start();

                config.instance = mpd;
                config.monitor = mpdStandAloneMonitor;

                logger.debug("Connected to player '{}' with config {}", playerId, config);
            }

        } catch (MPDConnectionException ce) {
            logger.error("Error connecting to player '{}' with config {}", playerId, config, ce);
        } catch (UnknownHostException uhe) {
            logger.error("Wrong connection details for player '{}'", playerId, uhe);
        }
    }

    /**
     * Disconnects all available {@link MPD}s and their {@link MPDStandAloneMonitor}-Thread.
     */
    private void disconnectPlayersAndMonitors() {
        for (String playerId : playerConfigCache.keySet()) {
            disconnect(playerId);
        }
    }

    /**
     * Disconnects the player <code>playerId</code>
     *
     * @param playerId the id of the player to disconnect
     */
    private void disconnect(String playerId) {
        try {
            MpdPlayerConfig playerConfig = playerConfigCache.get(playerId);
            MPDStandAloneMonitor monitor = playerConfig.monitor;
            if (monitor != null) {
                monitor.stop();
            }
            MPD mpd = playerConfig.instance;
            if (mpd != null) {
                mpd.close();
            }
        } catch (MPDConnectionException ce) {
            logger.warn("Couldn't disconnect player '{}'", playerId);
        } catch (MPDResponseException re) {
            logger.warn("Received response error: {}", re.getLocalizedMessage());
        }
    }

    /**
     * Checks if MPD's volume is set to an invalid value
     */
    private boolean volumeInvalid(int volume) {
        if (volume < 0 || volume > 100) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * Reconnecting to <code>playerId</code> means disconnect first and try
     * to connect again.
     *
     * @param playerId the id of the player to disconnect
     */
    private void reconnect(String playerId) {
        logger.info("Reconnect player '{}'", playerId);
        disconnect(playerId);
        connect(playerId);
    }

    /**
     * Reconnects all MPDs and Monitors. Meaning disconnect first and try
     * to connect again.
     */
    private void reconnectAllPlayerAndMonitors() {
        disconnectPlayersAndMonitors();
        connectAllPlayersAndMonitors();
    }

    /**
     * Internal data structure which carries the connection details of one
     * MPD (there could be several)
     *
     * @author Thomas.Eichstaedt-Engelen
     */
    static class MpdPlayerConfig {

        String host;
        int port;
        String password;

        MPD instance;
        MPDStandAloneMonitor monitor;

        @Override
        public String toString() {
            return "MPD [host=" + host + ", port=" + port + ", password=" + password + "]";
        }

    }

    @Override
    public void onClick(Command type) {
    }

    @Override
    public void onDoubleClick(Command type) {
    }

    /**
     * A quartz scheduler job to simply do a reconnection to the MPDs.
     */
    public class ReconnectJob implements Job {
        @Override
        public void execute(JobExecutionContext context) throws JobExecutionException {
            reconnectAllPlayerAndMonitors();
        }
    }

}
