/**
 * Copyright (c) 2010-2014, openHAB.org and others.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 */
package org.openhab.binding.maxcube.internal;

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Dictionary;

import org.apache.commons.lang.StringUtils;
import org.openhab.binding.maxcube.MaxCubeBindingProvider;
import org.openhab.binding.maxcube.internal.message.C_Message;
import org.openhab.binding.maxcube.internal.message.Configuration;
import org.openhab.binding.maxcube.internal.message.Device;
import org.openhab.binding.maxcube.internal.message.DeviceInformation;
import org.openhab.binding.maxcube.internal.message.H_Message;
import org.openhab.binding.maxcube.internal.message.HeatingThermostat;
import org.openhab.binding.maxcube.internal.message.L_Message;
import org.openhab.binding.maxcube.internal.message.M_Message;
import org.openhab.binding.maxcube.internal.message.Message;
import org.openhab.binding.maxcube.internal.message.MessageType;
import org.openhab.binding.maxcube.internal.message.S_Command;
import org.openhab.binding.maxcube.internal.message.ShutterContact;
import org.openhab.binding.maxcube.internal.message.ThermostatModeType;
import org.openhab.binding.maxcube.internal.message.WallMountedThermostat;
import org.openhab.core.binding.AbstractActiveBinding;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.types.Command;
import org.osgi.service.cm.ConfigurationException;
import org.osgi.service.cm.ManagedService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The RefreshService polls the MAX!Cube frequently and updates the list of
 * configurations and devices. The refresh interval can be changed via
 * openhab.cfg.
 * 
 * Note that the MAX Cube has a lock out that only allows a maximum of 36s of
 * transmissions (1%) in total in 1 hour. This means that if too many S messages
 * are sent then the cube no longer sends the data out.
 * 
 * @author Andreas Heil (info@aheil.de)
 * @since 1.4.0
 */
public class MaxCubeBinding extends AbstractActiveBinding<MaxCubeBindingProvider> implements ManagedService {

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

	/** The IP address of the MAX!Cube LAN gateway */
	private static String ip;

	/**
	 * The port of the MAX!Cube LAN gateway as provided at
	 * http://www.elv.de/controller.aspx?cid=824&detail=10&detail2=3484
	 */
	private static int port = 62910;

	/** The refresh interval which is used to poll given MAX!Cube */
	private static long refreshInterval = 10000;

	/** MaxCube's default off temperature */
	private static final DecimalType DEFAULT_OFF_TEMPERATURE = new DecimalType(4.5);

	/** MaxCubes default on temperature */
	private static final DecimalType DEFAULT_ON_TEMPERATURE = new DecimalType(30.5);
	
	/**
	 * Configuration and device lists, kept during the overall lifetime of the
	 * binding
	 */
	private ArrayList<Configuration> configurations = new ArrayList<Configuration>();
	private ArrayList<Device> devices = new ArrayList<Device>();

	/**
	 * {@inheritDoc}
	 */
	@Override
	protected String getName() {
		return "MAX!Cube Refresh Service";
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	protected long getRefreshInterval() {
		return refreshInterval;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void activate() {
		super.activate();
		setProperlyConfigured(false);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void execute() {
		Socket socket = null;
		BufferedReader reader = null;

		if (ip == null) {
			logger.debug("Update prior to completion of interface IP configuration");
			return;
		}
		try {
			String raw = null;

			socket = new Socket(ip, port);
			reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));

			boolean cont = true;
			while (cont) {
				raw = reader.readLine();
				if (raw == null) {
					cont = false;
					continue;
				}

				Message message;
				try {
					message = processRawMessage(raw);

					message.debug(logger);

					if (message != null) {
						if (message.getType() == MessageType.M) {
							M_Message msg = (M_Message) message;
							for (DeviceInformation di : msg.devices) {
								Configuration c = null;
								for (Configuration conf : configurations) {
									if (conf.getSerialNumber().equalsIgnoreCase(di.getSerialNumber())) {
										c = conf;
										break;
									}
								}

								if (c != null) {
									configurations.remove(c);
								}

								c = Configuration.create(di);
								configurations.add(c);

								c.setRoomId(di.getRoomId());
							}
						} else if (message.getType() == MessageType.C) {
							Configuration c = null;
							for (Configuration conf : configurations) {
								if (conf.getSerialNumber().equalsIgnoreCase(((C_Message) message).getSerialNumber())) {
									c = conf;
									break;
								}
							}

							if (c == null) {
								configurations.add(Configuration.create(message));
							} else {
								c.setValues((C_Message) message);
							}
						} else if (message.getType() == MessageType.L) {
							Collection<? extends Device> tempDevices = ((L_Message) message).getDevices(configurations);

							for (Device d : tempDevices) {
								Device existingDevice = findDevice(d.getSerialNumber(), devices);
								if (existingDevice == null) {
									devices.add(d);
								} else {
									devices.remove(existingDevice);
									devices.add(d);
								}
							}

							logger.debug("{} devices found.", devices.size());

							// the L message is the last one, while the reader
							// would hang trying to read a new line and
							// eventually the
							// cube will fail to establish
							// new connections for some time
							cont = false;
						}
					}
				} catch (Exception e) {
					logger.info("Failed to process message received by MAX! protocol.");
					logger.debug(Utils.getStackTrace(e));
				}
			}

			socket.close();

			for (MaxCubeBindingProvider provider : providers) {
				for (String itemName : provider.getItemNames()) {
					String serialNumber = provider.getSerialNumber(itemName);

					Device device = findDevice(serialNumber, devices);

					if (device == null) {
						logger.info("Cannot find MAX!cube device with serial number '{}'", serialNumber);

						if (logger.isDebugEnabled()) {
							StringBuilder sb = new StringBuilder();
							sb.append("Available MAX! devices are:");
							for (Device d : devices) {
								sb.append("\n\t");
								sb.append(d.getSerialNumber());
							}
							logger.debug(sb.toString());
						}
						continue;
					}

					switch (device.getType()) {
					case HeatingThermostatPlus:
					case HeatingThermostat:
						if (provider.getBindingType(itemName) == BindingType.VALVE) {
							eventPublisher.postUpdate(itemName, ((HeatingThermostat) device).getValvePosition());
						} else if (provider.getBindingType(itemName) == BindingType.MODE) {
							eventPublisher.postUpdate(itemName, ((HeatingThermostat) device).getModeString());
						} else if (provider.getBindingType(itemName) == BindingType.BATTERY) {
							eventPublisher.postUpdate(itemName, ((HeatingThermostat) device).getBatteryLow());
						} else if (provider.getBindingType(itemName) == BindingType.ACTUAL) {
							eventPublisher.postUpdate(itemName, ((HeatingThermostat) device).getTemperatureActual());
						} else {
							eventPublisher.postUpdate(itemName, ((HeatingThermostat) device).getTemperatureSetpoint());
						}
						break;
					case ShutterContact:
						if (provider.getBindingType(itemName) == BindingType.BATTERY) {
							eventPublisher.postUpdate(itemName, ((ShutterContact) device).getBatteryLow());
						} else {
							eventPublisher.postUpdate(itemName, ((ShutterContact) device).getShutterState());
						}
						break;
					case WallMountedThermostat:
						if (provider.getBindingType(itemName) == BindingType.ACTUAL) {
							eventPublisher.postUpdate(itemName, ((WallMountedThermostat) device).getTemperatureActual());
						} else if (provider.getBindingType(itemName) == BindingType.MODE) {
							eventPublisher.postUpdate(itemName, ((WallMountedThermostat) device).getModeString());
						} else if (provider.getBindingType(itemName) == BindingType.BATTERY) {
							eventPublisher.postUpdate(itemName, ((WallMountedThermostat) device).getBatteryLow());
						} else {
						eventPublisher.postUpdate(itemName, ((WallMountedThermostat) device).getTemperatureSetpoint());
						}
						break;
					default:
						// no further devices supported yet
					}
				}
			}
		} catch (UnknownHostException e) {
			logger.info("Cannot establish connection with MAX!Cube lan gateway while connecting to '{}'", ip);
			logger.debug(Utils.getStackTrace(e));
		} catch (IOException e) {
			logger.info("Cannot read data from MAX!Cube lan gateway while connecting to '{}'", ip);
			logger.debug(Utils.getStackTrace(e));
		}
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void internalReceiveCommand(String itemName, Command command) {
		logger.debug("Received command from {}", itemName);

		// resolve serial number for item
		String serialNumber = null;

		for (MaxCubeBindingProvider provider : providers) {
			serialNumber = provider.getSerialNumber(itemName);

			if (serialNumber.equals(null)) {
				continue;
			}

			// send command to MAX!Cube LAN Gateway
			Device device = findDevice(serialNumber, devices);

			if (device == null) {
				logger.debug("Cannot send command to device with serial number {}, device not listed.", serialNumber);
				continue;
			}

			String rfAddress = device.getRFAddress();
			String commandString = null;

			if (command instanceof DecimalType || command instanceof OnOffType) {
				DecimalType decimalType = DEFAULT_OFF_TEMPERATURE;
				if (command instanceof DecimalType) {
					decimalType = (DecimalType) command;
				} else if (command instanceof OnOffType) {
					decimalType = OnOffType.ON.equals(command) ? DEFAULT_ON_TEMPERATURE : DEFAULT_OFF_TEMPERATURE;
				}

				S_Command cmd = new S_Command(rfAddress, device.getRoomId(), decimalType.doubleValue());
				commandString = cmd.getCommandString();
			} else if (command instanceof StringType) {
				String commandContent = command.toString().trim().toUpperCase();
				ThermostatModeType commandThermoType = null;
				if (commandContent.contentEquals(ThermostatModeType.AUTOMATIC.toString())) {
					commandThermoType = ThermostatModeType.AUTOMATIC;
				} else if (commandContent.contentEquals(ThermostatModeType.BOOST.toString())) {
					commandThermoType = ThermostatModeType.BOOST;
				} else {
					logger.debug("Only updates to AUTOMATIC & BOOST supported, received value ;'{}'", commandContent);
					continue;
				}

				S_Command cmd = new S_Command(rfAddress, device.getRoomId(), commandThermoType);
				commandString = cmd.getCommandString();
			}

			if (commandString != null) {
				Socket socket = null;
				try {
					socket = new Socket(ip, port);
					DataOutputStream stream = new DataOutputStream(socket.getOutputStream());

					byte[] b = commandString.getBytes();
					stream.write(b);
					socket.close();

				} catch (UnknownHostException e) {
					logger.warn("Cannot establish connection with MAX!cube lan gateway while sending command to '{}'", ip);
					logger.debug(Utils.getStackTrace(e));
				} catch (IOException e) {
					logger.warn("Cannot write data from MAX!Cube lan gateway while connecting to '{}'", ip);
					logger.debug(Utils.getStackTrace(e));
				}
				logger.debug("Command Sent to {}", ip);
			} else {
				logger.debug("Null Command not sent to {}", ip);
			}
		}
	}

	private Device findDevice(String serialNumber, ArrayList<Device> devices) {
		for (Device device : devices) {
			if (device.getSerialNumber().toUpperCase().equals(serialNumber)) {
				return device;
			}
		}
		return null;
	}

	/**
	 * Processes the raw TCP data read from the MAX protocol, returning the
	 * corresponding Message.
	 * 
	 * @param raw
	 *            the raw data provided read from the MAX protocol
	 * @return the correct message for the given raw data
	 */
	private Message processRawMessage(String raw) {

		if (raw.startsWith("H:")) {
			return new H_Message(raw);
		} else if (raw.startsWith("M:")) {
			return new M_Message(raw);
		} else if (raw.startsWith("C:")) {
			return new C_Message(raw);
		} else if (raw.startsWith("L:")) {
			return new L_Message(raw);
		} else {
			logger.debug("Unknown message block: '{}'",raw);
		}
			return null;
	}

	/**
	 * {@inheritDoc}
	 */
	@SuppressWarnings("rawtypes")
	public void updated(Dictionary config) throws ConfigurationException {
		if (config != null) {
			ip = (String) config.get("ip");
			if (StringUtils.isBlank(ip)) {
				ip = discoveryGatewayIp();
			}

			String portString = (String) config.get("port");
			if (portString != null && !portString.isEmpty()) {
				if (port > 0 && port <= 65535) {
					port = Integer.parseInt(portString);
				}
			}

			String refreshIntervalString = (String) config.get("refreshInterval");
			if (refreshIntervalString != null && !refreshIntervalString.isEmpty()) {
				refreshInterval = Long.parseLong(refreshIntervalString);
			}
		} else {
			ip = discoveryGatewayIp();
		}
		
		setProperlyConfigured(ip != null);
	}

	/**
	 * Discovers the MAX!CUbe LAN Gateway IP address.
	 * 
	 * @return the cube IP if available, a blank string otherwise.
	 * @throws ConfigurationException
	 */
	private String discoveryGatewayIp() throws ConfigurationException {
		String ip = MaxCubeDiscover.discoverIp();
		if (ip == null) {
			throw new ConfigurationException("maxcube:ip", "IP address for MAX!Cube must be set");
		} else {
			logger.info("Discovered MAX!Cube lan gateway at '{}'", ip);
		}
		return ip;
	}
}
