package net.pms.network;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.ByteArrayInputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;

import java.net.URL;
import java.util.*;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import net.pms.PMS;
import static net.pms.network.UPNPHelper.sleep;
//import net.pms.util.BasicPlayer;
import net.pms.util.StringUtil;
import org.apache.commons.lang.StringUtils;
import org.fourthline.cling.DefaultUpnpServiceConfiguration;
import org.fourthline.cling.UpnpService;
import org.fourthline.cling.UpnpServiceImpl;
import org.fourthline.cling.controlpoint.ActionCallback;
import org.fourthline.cling.controlpoint.SubscriptionCallback;
import org.fourthline.cling.model.ServerClientTokens;
import org.fourthline.cling.model.action.*;
import org.fourthline.cling.model.gena.*;
import org.fourthline.cling.model.message.UpnpHeaders;
import org.fourthline.cling.model.message.UpnpResponse;
import org.fourthline.cling.model.message.header.DeviceTypeHeader;
import org.fourthline.cling.model.message.header.UpnpHeader;
import org.fourthline.cling.model.meta.*;
import org.fourthline.cling.model.types.DeviceType;
import org.fourthline.cling.model.types.ServiceId;
import org.fourthline.cling.model.types.UDADeviceType;
import org.fourthline.cling.model.types.UDN;
import org.fourthline.cling.registry.DefaultRegistryListener;
import org.fourthline.cling.registry.Registry;
import org.fourthline.cling.registry.RegistryListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/*---- regzam memo ----------
UPNPControl
	public static Device getDevice(InetAddress socket)    //type=Renderer
	public static Device getAnyDevice(InetAddress socket) //Type=Any
	public static String getUUID(Device d) 
	public static String getFriendlyName(String uuid)
	public static String getFriendlyName(Device d)

*///-------------------------

public class UPNPControl {
	// Logger ids to write messages to the logs.
	private static final Logger LOGGER = LoggerFactory.getLogger(UPNPControl.class);

	public static final DeviceType[] mediaRendererTypes = new DeviceType[] {
		new UDADeviceType("MediaRenderer", 1),
		// Older Sony Blurays provide only 'Basic' service
		new UDADeviceType("Basic", 1)
	};

	private static UpnpService upnpService;
	private static UpnpHeaders UMSHeaders;
	private static DocumentBuilder db;

	public static final int ACTIVE = 0;
	public static final int CONTROLS = 1;
	public static final int RENEW = 2;
	//public static final int AVT = BasicPlayer.PLAYCONTROL;
	//public static final int RC = BasicPlayer.VOLUMECONTROL;
	public static final int ANY = 0xff;

	private static final boolean DEBUG = true; // log upnp state vars

	protected static Map<String, Renderer> socketMap = new HashMap<>();

	public static class DeviceMap<T extends Renderer> extends HashMap<String, HashMap<String, T>> {
		private static final long serialVersionUID = 1510675619549915489L;

		private Class<T> TClass;

		public DeviceMap(Class<T> t) {
			TClass = t;
		}

		public T get(String uuid, String id) {
			if (!containsKey(uuid)) {
				put(uuid, new HashMap<String, T>());
			}
			HashMap<String, T> m = get(uuid);
			if (!m.containsKey(id)) {
				try {
					T newitem = TClass.newInstance();
					newitem.uuid = uuid;
					m.put(id, newitem);
				} catch (Exception e) {
					LOGGER.debug("Error instantiating item " + uuid + "[" + id + "]: " + e);
				}
			}
			return m.get(id);
		}

		public String get(String uuid, String id, String key) {
			return get(uuid, id).data.get(key);
		}

		public boolean containsKey(String uuid, String id) {
			return containsKey(uuid) && get(uuid).containsKey(id);
		}

		public HashMap<String, String> getData(String uuid, String id) {
			if (containsKey(uuid, id)) {
				return get(uuid, id).data;
			}
			return null;
		}

		public T put(String uuid, String id, T item) {
			item.uuid = uuid;
			if (!containsKey(uuid)) {
				get(uuid, "0");
			}
			return get(uuid).put(id, item);
		}

		public String put(String uuid, String id, String key, String value) {
			return get(uuid, id).data.put(key, value);
		}

		public void mark(String uuid, int property, Object value) {
			HashMap<String, T> m = get(uuid);
			if (m != null) {
				for (T i : m.values()) {
					switch (property) {
						case ACTIVE:
							i.setActive((boolean) value);
							break;
						case RENEW:
							i.renew = (boolean) value;
							break;
						case CONTROLS:
							i.controls = (int) value;
							break;
						default:
							break;
					}
				}
			}
		}
	}
	protected static DeviceMap rendererMap;

	public static class Renderer {
		public int controls;
		protected ActionEvent event;
		public String uuid;
		public String instanceID = "0"; // FIXME: unclear in what precise context a media renderer's instanceID != 0
		public final HashMap<String, String> data;
		public Map<String, String> details;
		public LinkedHashSet<ActionListener> listeners;
		private Thread monitor;
		public volatile boolean active, renew;

		public Renderer(String uuid) {
			this();
			this.uuid = uuid;
		}

		public Renderer() {
			controls = 0;
			active = false;
			data = new HashMap<>();
			details = null;
			listeners = new LinkedHashSet<>();
			event = new ActionEvent(this, 0, null);
			monitor = null;
			renew = false;
			data.put("TransportState", "STOPPED");
		}

		public void alert() {
			if (isUpnpDevice(uuid) && (monitor == null || !monitor.isAlive()) && !"STOPPED".equals(data.get("TransportState"))) {
				monitor();
			}
			for (ActionListener l : listeners) {
				l.actionPerformed(event);
			}
		}

		public Map<String, String> connect(ActionListener listener) {
			listeners.add(listener);
			return data;
		}

		public void disconnect(ActionListener listener) {
			listeners.remove(listener);
		}

		public void monitor() {
			final Device d = getDevice(uuid);
			monitor = new Thread(new Runnable() {
				@Override
				public void run() {
					String id = data.get("InstanceID");
					while (active && !"STOPPED".equals(data.get("TransportState"))) {
						sleep(1000);
//						if (DEBUG) LOGGER.debug("InstanceID: " + id);
						for (ActionArgumentValue o : getPositionInfo(d, id)) {
							data.put(o.getArgument().getName(), o.toString());
//							if (DEBUG) LOGGER.debug(o.getArgument().getName() + ": " + o.toString());
						}
						alert();
					}
					if (!active) {
						data.put("TransportState", "STOPPED");
						alert();
					}
				}
			}, "UPNP-" + d.getDetails().getFriendlyName());
			monitor.start();
		}

		public boolean hasPlayControls() {
			//return (controls & BasicPlayer.PLAYCONTROL) != 0;
			return false;  //dummy
		}

		public boolean hasVolumeControls() {
			//return (controls & BasicPlayer.VOLUMECONTROL) != 0;
			return false;  //dummy
		}

		public boolean isActive() {
			return active;
		}

		public void setActive(boolean b) {
			active = b;
		}

		public boolean needsRenewal() {
			return !active || renew;
		}
	}

	public static Device getDevice(String uuid) {
		return uuid != null && upnpService != null ? upnpService.getRegistry().getDevice(UDN.valueOf(uuid), false) : null;
	}

	public static synchronized void xml2d(String uuid, String xml, Renderer item) {
		try {
			Document doc = db.parse(new ByteArrayInputStream(xml.getBytes()));
//			doc.getDocumentElement().normalize();
			NodeList ids = doc.getElementsByTagName("InstanceID");
			for (int i = 0; i < ids.getLength(); i++) {
				NodeList c = ids.item(i).getChildNodes();
				String id = ((Element) ids.item(i)).getAttribute("val");
//				if (DEBUG) LOGGER.debug("InstanceID: " + id);
				if (item == null) {
					item = rendererMap.get(uuid, id);
				}
				item.data.put("InstanceID", id);
				for (int n = 0; n < c.getLength(); n++) {
					if (c.item(n).getNodeType() != Node.ELEMENT_NODE) {
//						LOGGER.debug("skip this " + c.item(n));
						continue;
					}
					Element e = (Element) c.item(n);
					String name = e.getTagName();
					String val = e.getAttribute("val");
					if (DEBUG) {
						LOGGER.debug(name + ": " + val);
					}
					item.data.put(name, val);
				}
				item.alert();
			}
		} catch (Exception e) {
			LOGGER.debug("Error parsing xml: " + e);
		}
	}

	public UPNPControl() {
		rendererMap = new DeviceMap<>(Renderer.class);
	}

	public void init() {
		LOGGER.debug("UPnPController: init() Start UPnP Listning...");
		try {
			//PMS.dbg("UPnPController: pos-1");
			db = DocumentBuilderFactory.newInstance().newDocumentBuilder();
			UMSHeaders = new UpnpHeaders();
			
			//PMS.dbg("UPnPController: pos-2");
			
			//regzam, set dummy
			//UMSHeaders.add(UpnpHeader.Type.USER_AGENT.getHttpName(), "UMS/" + PMS.getVersion() + " " + new ServerClientTokens());
			String uah="PMS_FRZ/" + PMS.getVersion() + " " + new ServerClientTokens();
			PMS.get().setMyServerUAH(uah);
			UMSHeaders.add(UpnpHeader.Type.USER_AGENT.getHttpName(), uah);

			//PMS.dbg("UPnPController: pos-3");
			DefaultUpnpServiceConfiguration sc = new DefaultUpnpServiceConfiguration() {
				@Override
				public UpnpHeaders getDescriptorRetrievalHeaders(RemoteDeviceIdentity identity) {
					return UMSHeaders;
				}
			};

			//PMS.dbg("UPnPController: pos-4");
			RegistryListener rl = new DefaultRegistryListener() {
				@Override
				public void remoteDeviceAdded(Registry registry, RemoteDevice d) {
					super.remoteDeviceAdded(registry, d);
					LOGGER.debug("UPNPControl: remoteDeviceAdded: called: device="+d);
					boolean ignore=false;
					if (isBlocked(getUUID(d)) || !addRenderer(d)) {
						LOGGER.debug("Ignoring device: {} {}", d.getType().getType(), d.toString());
						ignore=true;
					}
					// This may be unnecessary, but we might as well be thorough
					if (d.hasEmbeddedDevices()) {
						for (Device e : d.getEmbeddedDevices()) {
							if (isBlocked(getUUID(e)) || !addRenderer(e)) {
								LOGGER.debug("Ignoring embedded device: {} {}", e.getType(), e.toString());
								//ignore=true;
							}
						}
					}
					if(true || !ignore) PMS.get().deviceAdded(d);
				}

				@Override
				public void remoteDeviceRemoved(Registry registry, RemoteDevice d) {
					super.remoteDeviceRemoved(registry, d);
					
					LOGGER.debug("UPNPControl: remoteDeviceRemoved: called: device="+d);
					String uuid = getUUID(d);
					if (rendererMap.containsKey(uuid)) {
						rendererMap.mark(uuid, ACTIVE, false);
						rendererRemoved(d);
					}
				}

				@Override
				public void remoteDeviceUpdated(Registry registry, RemoteDevice d) {
					super.remoteDeviceUpdated(registry, d);
					rendererUpdated(d);
				}
			};
			
			//PMS.dbg("UPnPController: pos-10");
			// Runtime error will occur, if lib (org/seamless/util) is not exist 
			upnpService = new UpnpServiceImpl(sc, rl);
			
			//PMS.dbg("UPnPController: pos-11");
			
			for (DeviceType t : mediaRendererTypes) {
				upnpService.getControlPoint().search(new DeviceTypeHeader(t));
			}
			//PMS.dbg("UPnPController: pos-12");

			LOGGER.debug("UPNP Services are online, listening for media renderers");
		} catch (Exception ex) {
			LOGGER.debug("UPNP startup Error", ex);
		}
	}

	public void shutdown() {
		new Thread(new Runnable() {
			@Override
			public void run() {
				if (upnpService != null) {
					LOGGER.debug("Stopping UPNP Services...");
					upnpService.shutdown();
				}
			}
		}).start();
	}

	public static boolean isMediaRenderer(Device d) {
		String t = d.getType().getType();
		for (DeviceType r : mediaRendererTypes) {
			if (r.getType().equals(t)) {
				return true;
			}
		}
		return false;
	}

	public static boolean isUpnpDevice(String uuid) {
		return getDevice(uuid) != null;
	}

	public static boolean isActive(String uuid, String id) {
		if (rendererMap.containsKey(uuid, id)) {
			return rendererMap.get(uuid, id).active;
		}
		return false;
	}

	public static boolean isUpnpControllable(String uuid) {
		// Disable manually for Panasonic TVs since they lie
		if (rendererMap.containsKey(uuid) && !getDeviceDetailsString(getDevice(uuid)).contains("VIERA")) {
			return rendererMap.get(uuid, "0").controls != 0;
		}
		return false;
	}

	public static String getFriendlyName(String uuid) {
		return getFriendlyName(getDevice(uuid));
	}

	public static String getFriendlyName(Device d) {
		return d != null ? d.getDetails().getFriendlyName() : null;
	}

	public static String getUUID(Device d) {
		return d.getIdentity().getUdn().toString();
	}

	public static URL getURL(Device d) {
		return d instanceof RemoteDevice ? ((RemoteDevice) d).getIdentity().getDescriptorURL() :
			d.getDetails().getBaseURL();
	}

	public static List<String> getServiceNames(Device d) {
		ArrayList<String> services = new ArrayList<>();
		for (Service s : d.getServices()) {
			services.add(s.getServiceId().getId());
		}
		return services;
	}

	public static Map<String, String> getDeviceDetails(Device d) {
		if (d == null) {
			return null;
		}
		DeviceDetails dev = d.getDetails();
		ManufacturerDetails man = dev.getManufacturerDetails();
		ModelDetails model = dev.getModelDetails();
		LinkedHashMap<String, String> details = new LinkedHashMap<>();
		details.put("friendlyName", dev.getFriendlyName());
		details.put("address", getURL(d).getHost());
		details.put("udn", getUUID(d));
		Object detail;
		if ((detail = man.getManufacturer()) != null) {
			details.put("manufacturer", (String) detail);
		}
		if ((detail = model.getModelName()) != null) {
			details.put("modelName", (String) detail);
		}
		if ((detail = model.getModelNumber()) != null) {
			details.put("modelNumber", (String) detail);
		}
		if ((detail = model.getModelDescription()) != null) {
			details.put("modelDescription", (String) detail);
		}
		if ((detail = man.getManufacturerURI()) != null) {
			details.put("manufacturerURL", detail.toString());
		}
		if ((detail = model.getModelURI()) != null) {
			details.put("modelURL", detail.toString());
		}
		return details;
	}

	public static String getDeviceDetailsString(Device d) {
		return StringUtils.join(getDeviceDetails(d).values(), " ");
	}

	public static String getDeviceIcon(Renderer r, int maxHeight) {
		if (isUpnpDevice(r.uuid)) {
			return getDeviceIcon(getDevice(r.uuid), maxHeight);
		}
		return null;
	}

	public static String getDeviceIcon(Device d, int maxHeight) {
		URL base = getURL(d);
		Icon icon = null;
		String url = null;
		int maxH = maxHeight == 0 ? 99999 : maxHeight, height = 0;
		for (Icon i : d.getIcons()) {
			int h = i.getHeight();
			if (h < maxH && h > height) {
				icon = i;
				height = h;
			}
		}
		try {
			url = icon != null ? new URL(base, icon.getUri().toString()).toString() : null;
		} catch (Exception e) {}
		LOGGER.debug("Device icon: " + url);
		return url;
	}

	protected boolean addRenderer(String uuid) {
		Device d = getDevice(uuid);
		return d != null ? addRenderer(d) : false;
	}

	protected synchronized boolean addRenderer(Device d) {
		if (d != null) {
			String uuid = getUUID(d);
			if (isMediaRenderer(d) && rendererFound(d, uuid) != null) {
				LOGGER.debug("Adding device: {} {}", d.getType(), d.toString());
				rendererMap.mark(uuid, ACTIVE, true);
				subscribeAll(d, uuid);
				rendererReady(uuid);
				return true;
			}
		}
		return false;
	}

	protected void subscribeAll(Device d, String uuid) {
		String name = getFriendlyName(d);
		int ctrl = 0;
		for (Service s : d.getServices()) {
			String sid = s.getServiceId().getId();
			LOGGER.debug("Subscribing to " + sid + " service on " + name);
			if (sid.contains("AVTransport")) {
				//ctrl |= AVT;  //regzam, set dummy
			} else if (sid.contains("RenderingControl")) {
				//ctrl |= RC;	//regzam, set dummy
			}
			upnpService.getControlPoint().execute(new SubscriptionCB(s));
		}
		rendererMap.mark(uuid, RENEW, false);
		rendererMap.mark(uuid, CONTROLS, ctrl);
	}

	protected Renderer rendererFound(Device d, String uuid) {
		// Create an instance
		return rendererMap.get(uuid, "0");
	}

	protected void rendererReady(String uuid) {
	}

	protected boolean isBlocked(String uuid) {
		return false;
	}

	protected void rendererUpdated(Device d) {
		String uuid = getUUID(d);
		if (rendererMap.containsKey(uuid)) {
			if (rendererMap.get(uuid, "0").needsRenewal()) {
				LOGGER.debug("Renewing subscriptions to ", getFriendlyName(d));
				subscribeAll(d, uuid);
			}
			rendererMap.mark(uuid, ACTIVE, true);
		}
	}

	protected void rendererRemoved(Device d) {
		LOGGER.debug(getFriendlyName(d) + " is now offline.");
	}

	public static String getUUID(String addr) {
		try {
			return getUUID(InetAddress.getByName(addr));
		} catch (Exception e) {
			return null;
		}
	}

	public static String getUUID(InetAddress socket) {
		Device d = getDevice(socket);
		if (d != null) {
			return getUUID(d);
		}
		return null;
	}

	// Returns the first device regardless of type at the given address(+port), if any
	public static Device getAnyDevice(InetSocketAddress socket) {
		if (upnpService != null) {
			
			InetAddress iadr=socket.getAddress();
			int port=socket.getPort();
			//PMS.dbg("UPNPControl.getAnyDevice: Given addr="+iadr+", port="+port);
			
			int cnt=0;
			for (Device d : upnpService.getRegistry().getDevices()) {
				try {
					
					//get port No.
					cnt++;
					InetAddress devsocket = InetAddress.getByName(getURL(d).getHost());
					int devport=getURL(d).getPort();
					
					//PMS.dbg("UPNPControl.getAnyDevice No"+cnt+", addr="+devsocket+", port="+devport);
					
					//if (devsocket.equals(iadr) && devport==port) {
					//Renderer real port doesn't match with DeviceInfo gathered M-SEARCH
					if (devsocket.equals(iadr)) {
						return d;
					}
				} catch (Exception e) {}
			}
		}
		return null;
	}
	
	// Returns the first device regardless of type at the given address, if any
	public static Device getAnyDevice(InetAddress socket) {
		if (upnpService != null) {
			for (Device d : upnpService.getRegistry().getDevices()) {
				try {
					InetAddress devsocket = InetAddress.getByName(getURL(d).getHost());
					if (devsocket.equals(socket)) {
						return d;
					}
				} catch (Exception e) {}
			}
		}
		return null;
	}

	// Returns the first renderer at the given address, if any
	public static Device getDevice(InetAddress socket) {
		if (upnpService != null) {
			for (DeviceType r : mediaRendererTypes) {
				for (Device d : upnpService.getRegistry().getDevices(r)) {
					try {
						InetAddress devsocket = InetAddress.getByName(getURL(d).getHost());
						if (devsocket.equals(socket)) {
							return d;
						}
					} catch (Exception e) {}
				}
			}
		}
		return null;
	}

	public static Renderer getRenderer(String uuid) {
		if (rendererMap.containsKey(uuid)) {
			return rendererMap.get(uuid, "0");
		}
		return null;
	}

	public static boolean isNonRenderer(InetAddress socket) {
		Device d = getDevice(socket);
		boolean b = (d != null && !isMediaRenderer(d));
		if (b) {
			//regzam
			//LOGGER.debug("Device at {} is {}: {}", socket, d.getType(), d.toString());
			LOGGER.debug("Device at socket("+socket+") is Type="+d.getType()+"["+d.toString()+"]");
		}
		return b;
	}

	public static void connect(String uuid, String instanceID, ActionListener listener) {
		rendererMap.get(uuid, instanceID).connect(listener);
	}

	public static Map<String, String> getData(String uuid, String instanceID) {
		return rendererMap.get(uuid, instanceID).data;
	}

	public UpnpService getService() {
		return upnpService;
	}

	public static class SubscriptionCB extends SubscriptionCallback {
		private String uuid;

		public SubscriptionCB(Service s) {
			super(s);
			uuid = getUUID(s.getDevice());
		}

		@Override
		public void eventReceived(GENASubscription subscription) {
			rendererMap.mark(uuid, ACTIVE, true);
			if (subscription.getCurrentValues().containsKey("LastChange")) {
				xml2d(uuid, subscription.getCurrentValues().get("LastChange").toString(), null);
			}
		}

		@Override
		public void established(GENASubscription sub) {
			LOGGER.debug("Subscription established: " + sub.getService().getServiceId().getId() +
				" on " + getFriendlyName(uuid));
		}

		@Override
		public void failed(GENASubscription sub, UpnpResponse response, Exception ex, String defaultMsg) {
			LOGGER.debug("Subscription failed: " + sub.getService().getServiceId().getId() +
				" on " + getFriendlyName(uuid) + ": " + defaultMsg.split(": ", 2)[1]);
		}

		@Override
		public void failed(GENASubscription sub, UpnpResponse response, Exception ex) {
			LOGGER.debug("Subscription failed: " + sub.getService().getServiceId().getId() +
				" on " + getFriendlyName(uuid) + ": " + createDefaultFailureMessage(response, ex).split(": ", 2)[1]);
		}

		@Override
		public void ended(GENASubscription sub, CancelReason reason, UpnpResponse response) {
			// Reason should be null, or it didn't end regularly
			if (reason != null) {
				LOGGER.trace("Subscription cancelled: " + sub.getService().getServiceId().getId() +
					" on " + uuid + ": " + reason);
			}
			rendererMap.mark(uuid, RENEW, true);
		}

		@Override
		public void eventsMissed(GENASubscription sub, int numberOfMissedEvents) {
			LOGGER.debug("Missed events: " + numberOfMissedEvents + " for subscription " + sub.getService().getServiceId().getId() + " on " + getFriendlyName(uuid));
		}
	}

	// Convenience functions for sending various upnp service requests
	@SuppressWarnings("unchecked") //regzam, for Java7 wornings at <1> :danger: to be fixed
	public static ActionInvocation send(final Device dev, String instanceID, String service, String action, String... args) {
		Service svc = dev.findService(ServiceId.valueOf("urn:upnp-org:serviceId:" + service));
		final String uuid = getUUID(dev);
		if (svc != null) {
			Action x = svc.getAction(action);
			String name = getFriendlyName(dev);
			boolean log = !action.equals("GetPositionInfo");
			if (x != null) {
				//ActionInvocation a = new ActionInvocation(x); //<1>regzam, cause unchecked call warning
				ActionInvocation a = new ActionInvocation(x); 	// how to fix?
				a.setInput("InstanceID", instanceID);
				for (int i = 0; i < args.length; i += 2) {
					a.setInput(args[i], args[i + 1]);
				}
				if (log) {
					//regzam, set dummy
					//LOGGER.debug("Sending upnp {}.{} {} to {}[{}]", service, action, args, name, instanceID);
				}
				new ActionCallback(a, upnpService.getControlPoint()) {
					@Override
					public void success(ActionInvocation invocation) {
						rendererMap.mark(uuid, ACTIVE, true);
					}

					@Override
					public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) {
						//regzam, set dummy
						//LOGGER.debug("Action failed: {}", defaultMsg);
						rendererMap.mark(uuid, ACTIVE, false);
					}
				}.run();

				if (log) {
					for (ActionArgumentValue arg : a.getOutput()) {
						//regzam, set dummy
						//LOGGER.debug("Received from {}[{}]: {}={}", name, instanceID, arg.getArgument().getName(), arg.toString());
					}
				}
				return a;
			}
		}
		return null;
	}

	// ConnectionManager
	public static String getProtocolInfo(Device dev, String instanceID, String dir) {
		return send(dev, instanceID, "ConnectionManager", "GetProtocolInfo")
			.getOutput(dir).toString();
	}

	// AVTransport
	// Play modes
	public final static String NORMAL = "NORMAL";
	public final static String REPEAT_ONE = "REPEAT_ONE";
	public final static String REPEAT_ALL = "REPEAT_ALL";
	public final static String RANDOM = "RANDOM";
	// Seek modes
	public final static String REL_BYTE = "X_DLNA_REL_BYTE";
	public final static String REL_TIME = "REL_TIME";
	public final static String TRACK_NR = "TRACK_NR";

	public static void play(Device dev, String instanceID) {
		send(dev, instanceID, "AVTransport", "Play", "Speed", "1");
	}

	public static void pause(Device dev, String instanceID) {
		send(dev, instanceID, "AVTransport", "Pause");
	}

	public static void next(Device dev, String instanceID) {
		send(dev, instanceID, "AVTransport", "Next");
	}

	public static void previous(Device dev, String instanceID) {
		send(dev, instanceID, "AVTransport", "Previous");
	}

	public static void seek(Device dev, String instanceID, String mode, String target) {
		// REL_TIME target format is "hh:mm:ss"
		send(dev, instanceID, "AVTransport", "Seek", "Unit", mode, "Target", target);
	}

	public static void stop(Device dev, String instanceID) {
		send(dev, instanceID, "AVTransport", "Stop");
	}

	public static String getCurrentTransportState(Device dev, String instanceID) {
		return send(dev, instanceID, "AVTransport", "GetTransportInfo")
			.getOutput("CurrentTransportState").toString();
	}

	public static String getCurrentTransportActions(Device dev, String instanceID) {
		return send(dev, instanceID, "AVTransport", "GetCurrentTransportActions")
			.getOutput("CurrentTransportActions").toString();
	}

	public static String getDeviceCapabilities(Device dev, String instanceID) {
		return send(dev, instanceID, "AVTransport", "GetDeviceCapabilities")
			.getOutput("DeviceCapabilities").toString();
	}

	public static String getMediaInfo(Device dev, String instanceID) {
		return send(dev, instanceID, "AVTransport", "GetMediaInfo")
			.getOutput("MediaInfo").toString();
	}

	public static ActionArgumentValue[] getPositionInfo(Device dev, String instanceID) {
		return send(dev, instanceID, "AVTransport", "GetPositionInfo").getOutput();
	}

	public static String getTransportInfo(Device dev, String instanceID) {
		return send(dev, instanceID, "AVTransport", "GetTransportInfo")
			.getOutput("TransportInfo").toString();
	}

	public static String getTransportSettings(Device dev, String instanceID) {
		return send(dev, instanceID, "AVTransport", "GetTransportSettings")
			.getOutput("TransportSettings").toString();
	}

	public static void setAVTransportURI(Device dev, String instanceID, String uri, String metaData) {
		//send(dev, instanceID, "AVTransport", "SetAVTransportURI", "CurrentURI", uri,
		//	"CurrentURIMetaData", metaData != null ? StringUtil.unEncodeXML(metaData) : null);
		//regzam, set dummy
		send(dev, instanceID, "AVTransport", "SetAVTransportURI", "CurrentURI", uri,
			"CurrentURIMetaData", metaData != null ? "to be unEncodeXML(metadata)" : null);
	}

	public static void setPlayMode(Device dev, String instanceID, String mode) {
		send(dev, instanceID, "AVTransport", "SetPlayMode", "NewPlayMode", mode);
	}

	public static String X_DLNA_GetBytePositionInfo(Device dev, String instanceID, String trackSize) {
		return send(dev, instanceID, "AVTransport", "X_DLNA_GetBytePositionInfo", "TrackSize", trackSize)
			.getOutput("BytePositionInfo").toString();
	}

	// RenderingControl
	// Audio channels
	public final static String MASTER = "Master";
	public final static String LF = "LF";
	public final static String RF = "RF";

	public static String getMute(Device dev, String instanceID) {
		return getMute(dev, instanceID, MASTER);
	}

	public static String getMute(Device dev, String instanceID, String channel) {
		return send(dev, instanceID, "RenderingControl", "GetMute", "Channel", channel)
			.getOutput("Mute").toString();
	}

	public static String getVolume(Device dev, String instanceID) {
		return getVolume(dev, instanceID, MASTER);
	}

	public static String getVolume(Device dev, String instanceID, String channel) {
		return send(dev, instanceID, "RenderingControl", "GetVolume", "Channel", channel)
			.getOutput("Volume").toString();
	}

	public static void setMute(Device dev, String instanceID, boolean on) {
		setMute(dev, instanceID, on, MASTER);
	}

	public static void setMute(Device dev, String instanceID, boolean on, String channel) {
		send(dev, instanceID, "RenderingControl", "SetMute", "DesiredMute", on ? "1" : "0", "Channel", channel);
	}

	public static void setVolume(Device dev, String instanceID, int volume) {
		setVolume(dev, instanceID, volume, MASTER);
	}

	public static void setVolume(Device dev, String instanceID, int volume, String channel) {
		// volume = 1 to 100
		send(dev, instanceID, "RenderingControl", "SetVolume", "DesiredVolume", String.valueOf(volume), "Channel", channel);
	}

}
