/*
 * Digital Media Server, for streaming digital media to UPnP AV or DLNA
 * compatible devices based on PS3 Media Server and Universal Media Server.
 * Copyright (C) 2016 Digital Media Server developers.
 *
 * This program is free software: you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later
 * version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program. If not, see http://www.gnu.org/licenses/.
 */
package net.pms.dlna.protocolinfo;

import static org.apache.commons.lang3.StringUtils.isBlank;
import java.io.Serializable;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.text.translate.CharSequenceTranslator;
import org.apache.commons.text.translate.LookupTranslator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.pms.dlna.DLNAImageProfile;
import net.pms.exception.ParseException;

/**
 * This class represents a device's {@code ProtocolInfo} elements, typically
 * {@code Source} or {@code Sink} from {@code GetProtocolInfo}.
 * <p>
 * This class is thread-safe.
 *
 * @author Nadahar
 */
public class DeviceProtocolInfo implements Serializable {

	private static final long serialVersionUID = 1L;

	/** The logger. */
	private static final Logger LOGGER = LoggerFactory.getLogger(DeviceProtocolInfo.class);

	/** The static singleton {@code GetProtocolInfo} {@code Source} identifier */
	public static final DeviceProtocolInfoSource<DeviceProtocolInfo> GET_PROTOCOLINFO_SOURCE = new GetProtocolInfoType() {

		private static final long serialVersionUID = 1L;

		@Override
		public String getType() {
			return "GetProtocolInfo Source";
		}
	};

	/** The static singleton {@code GetProtocolInfo} {@code Sink} identifier */
	public static final DeviceProtocolInfoSource<DeviceProtocolInfo> GET_PROTOCOLINFO_SINK = new GetProtocolInfoType() {

		private static final long serialVersionUID = 1L;

		@Override
		public String getType() {
			return "GetProtocolInfo Sink";
		}
	};

	/**
	 * A regex for splitting a comma separated list of {@code protocolInfo}
	 * entries returned from {@code GetProtocolInfo} while taking DLNA comma
	 * escaping rules into account.
	 */
	public static final String COMMA_SPLIT_REGEX = "\\s*(?:(?<!\\\\),|(?<!\\\\)\\\\\\\\,)\\s*";

	/**
	 * A {@link CharSequenceTranslator} for unescaping individual
	 * {@code GetProtocolInfo} elements.
	 */
	public static final CharSequenceTranslator PROTOCOLINFO_UNESCAPE;

	/**
	 * A {@link CharSequenceTranslator} for escaping individual
	 * {@code GetProtocolInfo} elements.
	 */
	public static final CharSequenceTranslator PROTOCOLINFO_ESCAPE;

	static {
		HashMap<CharSequence, CharSequence> escapeMap = new HashMap<>();
		escapeMap.put(",", "\\,");   // , - comma
		escapeMap.put("\\", "\\\\"); // \ - backslash
		PROTOCOLINFO_ESCAPE = new LookupTranslator(escapeMap);

		HashMap<CharSequence, CharSequence> unescapeMap = new HashMap<>();
		unescapeMap.put("\\\\", "\\"); // \ - backslash
		unescapeMap.put("\\,", ",");   // , - comma
		PROTOCOLINFO_UNESCAPE = new LookupTranslator(unescapeMap);
	}

	/** The sets lock. */
	protected final ReentrantReadWriteLock setsLock = new ReentrantReadWriteLock();

	/** The {@link Map} of {@link ProtocolInfo} {@link Set}s. */
	protected final HashMap<DeviceProtocolInfoSource<?>, SortedSet<ProtocolInfo>> protocolInfoSets = new HashMap<>();

	/** The image profile set. */
	protected final SortedSet<DLNAImageProfile> imageProfileSet = new TreeSet<>();

	/**
	 * Creates a new empty instance.
	 */
	public DeviceProtocolInfo() {
	}

	/**
	 * Creates a new instance containing the content from the parsing of
	 * {@code protocolInfoString}.
	 *
	 * @param type The {@link DeviceProtocolInfoSource} of
	 *            {@code protocolInfoString}, must be either
	 *            {@link #GET_PROTOCOLINFO_SINK} or
	 *            {@link #GET_PROTOCOLINFO_SOURCE}.
	 * @param protocolInfoString a comma separated string of
	 *            {@code protocolInfo} representations.
	 */
	public DeviceProtocolInfo(GetProtocolInfoType type, String protocolInfoString) {
		add(type, protocolInfoString);
	}

	/**
	 * Tries to parse {@code protocolInfoString} and add the resulting
	 * {@link ProtocolInfo} instances.
	 *
	 * @param type The {@link DeviceProtocolInfoSource} that identifies the
	 *            source of these {@code protocolInfo}s.
	 * @param protocolInfoString a comma separated string of
	 *            {@code protocolInfo} representations whose presence is to be
	 *            ensured.
	 * @return {@code true} if this changed as a result of the call. Returns
	 *         {@code false} this already contains the specified element(s).
	 */
	public boolean add(DeviceProtocolInfoSource<?> type, String protocolInfoString) {
		if (StringUtils.isBlank(protocolInfoString)) {
			return false;
		}

		String[] elements = protocolInfoString.trim().split(COMMA_SPLIT_REGEX);
		boolean result = false;
		setsLock.writeLock().lock();
		try {
			SortedSet<ProtocolInfo> currentSet;
			if (protocolInfoSets.containsKey(type)) {
				currentSet = protocolInfoSets.get(type);
			} else {
				currentSet = new TreeSet<ProtocolInfo>();
				protocolInfoSets.put(type, currentSet);
			}

			SortedSet<ProtocolInfo> tempSet = null;
			for (String element : elements) {
				try {
					tempSet = handleSpecialCaseString(element);
					if (tempSet == null) {
						// No special handling
						result |= currentSet.add(new ProtocolInfo(unescapeString(element)));
					} else {
						// Add the special handling results
						result |= currentSet.addAll(tempSet);
						tempSet = null;
					}
				} catch (ParseException e) {
					LOGGER.warn(
						"Unable to parse protocolInfo from \"{}\", this profile will not be registered: {}",
						element,
						e.getMessage()
					);
					LOGGER.trace("", e);
				}
			}
			updateImageProfiles();
		} finally {
			setsLock.writeLock().unlock();
		}
		return result;
	}

	/**
	 * Re-parses {@code protocolInfoSet} and stores the results in
	 * {@code imageProfileSet}.
	 */
	protected void updateImageProfiles() {
		setsLock.writeLock().lock();
		try {
			imageProfileSet.clear();
			for (SortedSet<ProtocolInfo> set : protocolInfoSets.values()) {
				for (ProtocolInfo protocolInfo : set) {
					imageProfileSet.addAll(DLNAImageProfile.toDLNAImageProfiles(protocolInfo));
				}
			}
		} finally {
			setsLock.writeLock().unlock();
		}
	}


	// Standard java.util.Collection methods.


	/**
	 * Returns the number of elements of the given
	 * {@link DeviceProtocolInfoSource} type. If this contains more than
	 * {@link Integer#MAX_VALUE} elements, returns {@link Integer#MAX_VALUE}.
	 *
	 * @param type the {@link DeviceProtocolInfoSource} type to get the number
	 *            of elements for.
	 * @return The number of elements in the {@link Set} for {@code type}.
	 */
	public int size(DeviceProtocolInfoSource<?> type) {
		setsLock.readLock().lock();
		try {
			SortedSet<ProtocolInfo> set = protocolInfoSets.get(type);
			return set == null ? 0 : set.size();
		} finally {
			setsLock.readLock().unlock();
		}
	}

	/**
	 * Returns the total number of elements of all
	 * {@link DeviceProtocolInfoSource} types. If the result is greater than
	 * {@link Integer#MAX_VALUE} elements, returns {@link Integer#MAX_VALUE}.
	 *
	 * @return The number of elements.
	 */
	public int size() {
		long result = 0;
		setsLock.readLock().lock();
		try {
			for (SortedSet<ProtocolInfo> set : protocolInfoSets.values()) {
				if (set != null) {
					result += set.size();
				}
			}
		} finally {
			setsLock.readLock().unlock();
		}
		return result > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) result;
	}

	/**
	 * Checks if the {@link Set} for the given {@link DeviceProtocolInfoSource}
	 * type is empty.
	 *
	 * @param type the {@link DeviceProtocolInfoSource} type to check.
	 * @return {@code true} if {@code protocolInfoSets} contains no elements of
	 *         {@code type}.
	 */
	public boolean isEmpty(DeviceProtocolInfoSource<?> type) {
		setsLock.readLock().lock();
		try {
			SortedSet<ProtocolInfo> set = protocolInfoSets.get(type);
			return set == null ? true : set.isEmpty();
		} finally {
			setsLock.readLock().unlock();
		}
	}

	/**
	 * Checks if all the {@link DeviceProtocolInfoSource} {@link Set}s are
	 * empty.
	 *
	 * @return {@code true} if neither of the {@link DeviceProtocolInfoSource}
	 *         {@link Set}s contain any elements, {@code false} otherwise.
	 */
	public boolean isEmpty() {
		setsLock.readLock().lock();
		try {
			for (SortedSet<ProtocolInfo> set : protocolInfoSets.values()) {
				if (set != null && !set.isEmpty()) {
					return false;
				}
			}
		} finally {
			setsLock.readLock().unlock();
		}
		return true;
	}

	/**
	 * Returns {@code true} if the {@link Set} for the given
	 * {@link DeviceProtocolInfoSource} contains the specified element.
	 *
	 * @param type the {@link DeviceProtocolInfoSource} type to check.
	 * @param protocolInfo the element whose presence is to be tested.
	 * @return {@code true} if the {@link Set} for the given
	 *         {@link DeviceProtocolInfoSource} contains the specified element,
	 *         {@code false} otherwise.
	 */
	public boolean contains(DeviceProtocolInfoSource<?> type, ProtocolInfo protocolInfo) {
		setsLock.readLock().lock();
		try {
			SortedSet<ProtocolInfo> set = protocolInfoSets.get(type);
			return set == null ? false : set.contains(protocolInfo);
		} finally {
			setsLock.readLock().unlock();
		}
	}

	/**
	 * Returns {@code true} if any of the {@link DeviceProtocolInfoSource}
	 * {@link Set}s contains the specified element.
	 *
	 * @param protocolInfo the element whose presence is to be tested.
	 * @return {@code true} if any of the {@link DeviceProtocolInfoSource}
	 *         {@link Set}s contains the specified element {@code false}
	 *         otherwise.
	 */
	public boolean contains(ProtocolInfo protocolInfo) {
		setsLock.readLock().lock();
		try {
			for (SortedSet<ProtocolInfo> set : protocolInfoSets.values()) {
				if (set != null && set.contains(protocolInfo)) {
					return true;
				}
			}
		} finally {
			setsLock.readLock().unlock();
		}
		return false;
	}

	/**
	 * Returns a sorted array containing all of the elements in the {@link Set}
	 * for the given {@link DeviceProtocolInfoSource}.
	 * <p>
	 * The returned array will be "safe" in that no reference to it is
	 * maintained. (In other words, this method must allocate a new array). The
	 * caller is thus free to modify the returned array.
	 *
	 * @param type the {@link DeviceProtocolInfoSource} type whose elements to
	 *            convert to an {@code array}.
	 * @return An array containing all the {@link ProtocolInfo} instances in the
	 *         {@link Set} for {@code type}.
	 */
	public ProtocolInfo[] toArray(DeviceProtocolInfoSource<?> type) {
		setsLock.readLock().lock();
		try {
			SortedSet<ProtocolInfo> set = protocolInfoSets.get(type);
			return set == null ? null : set.toArray(new ProtocolInfo[protocolInfoSets.size()]);
		} finally {
			setsLock.readLock().unlock();
		}
	}

	/**
	 * Returns a sorted array containing all of the elements for all of the
	 * {@link DeviceProtocolInfoSource} {@link Set}s.
	 * <p>
	 * The returned array will be "safe" in that no reference to it is
	 * maintained. (In other words, this method must allocate a new array). The
	 * caller is thus free to modify the returned array.
	 *
	 * @return An array containing all the {@link ProtocolInfo} instances.
	 */
	public ProtocolInfo[] toArray() {
		SortedSet<ProtocolInfo> result = new TreeSet<>();
		setsLock.readLock().lock();
		try {
			for (SortedSet<ProtocolInfo> set : protocolInfoSets.values()) {
				if (set != null) {
					result.addAll(set);
				}
			}
		} finally {
			setsLock.readLock().unlock();
		}
		return result.toArray(new ProtocolInfo[result.size()]);
	}

	/**
	 * Returns {@code true} if the {@link Set} for the given
	 * {@link DeviceProtocolInfoSource} contains all of the elements in the
	 * specified collection.
	 *
	 * @param type the {@link DeviceProtocolInfoSource} type to check.
	 * @param collection a {@link Collection} to be checked for containment.
	 * @return {@code true} if {@link Set} for {@code type} contains all of the
	 *         elements in {@code collection}.
	 *
	 * @see #contains(DeviceProtocolInfoSource, ProtocolInfo)
	 * @see #contains(ProtocolInfo)
	 * @see #containsAll(Collection)
	 */

	public boolean containsAll(DeviceProtocolInfoSource<?> type, Collection<ProtocolInfo> collection) {
		setsLock.readLock().lock();
		try {
			SortedSet<ProtocolInfo> set = protocolInfoSets.get(type);
			return set == null ? false : set.containsAll(collection);
		} finally {
			setsLock.readLock().unlock();
		}
	}

	/**
	 * Returns {@code true} if any of the {@link DeviceProtocolInfoSource}
	 * {@link Set}s contains the elements in the specified collection.
	 *
	 * @param collection a {@link Collection} to be checked for containment.
	 * @return {@code true} if any of the {@link DeviceProtocolInfoSource}
	 *         {@link Set}s contains the elements in {@code collection}.
	 *
	 * @see #contains(DeviceProtocolInfoSource, ProtocolInfo)
	 * @see #contains(ProtocolInfo)
	 * @see #containsAll(DeviceProtocolInfoSource, Collection)
	 */
	public boolean containsAll(Collection<ProtocolInfo> collection) {
		setsLock.readLock().lock();
		try {
			for (ProtocolInfo protocolInfo : collection) {
				if (!contains(protocolInfo)) {
					return false;
				}
			}
		} finally {
			setsLock.readLock().unlock();
		}
		return true;
	}

	/**
	 * Removes all elements in the {@link Set} for the given
	 * {@link DeviceProtocolInfoSource}.
	 *
	 * @param type The {@link DeviceProtocolInfoSource} type to clear.
	 */
	public void clear(DeviceProtocolInfoSource<?> type) {
		setsLock.writeLock().lock();
		try {
			SortedSet<ProtocolInfo> set = protocolInfoSets.get(type);
			if (set != null) {
				set.clear();
				updateImageProfiles();
			}
		} finally {
			setsLock.writeLock().unlock();
		}
	}

	/**
	 * Removes all elements in all of the {@link DeviceProtocolInfoSource}
	 * {@link Set}s.
	 */
	public void clear() {
		setsLock.writeLock().lock();
		try {
			protocolInfoSets.clear();
			imageProfileSet.clear();
		} finally {
			setsLock.writeLock().unlock();
		}
	}

	/**
	 * Ensures that the {@link Set} for the given
	 * {@link DeviceProtocolInfoSource} contains the specified element. Returns
	 * {@code true} if {@code protocolInfo} was added a result of the call, or
	 * {@code false} the {@link Set} for the given
	 * {@link DeviceProtocolInfoSource} already contains the specified element.
	 *
	 * @param type the {@link DeviceProtocolInfoSource} type.
	 * @param protocolInfo element whose presence is to be ensured.
	 * @return {@code true} if the {@link Set} for {@code type} changed as a
	 *         result of the call, {@code false} otherwise.
	 */
	public boolean add(DeviceProtocolInfoSource<?> type, ProtocolInfo protocolInfo) {
		setsLock.writeLock().lock();
		try {
			SortedSet<ProtocolInfo> currentSet;
			if (protocolInfoSets.containsKey(type)) {
				currentSet = protocolInfoSets.get(type);
			} else {
				currentSet = new TreeSet<ProtocolInfo>();
				protocolInfoSets.put(type, currentSet);
			}

			if (currentSet.add(protocolInfo)) {
				updateImageProfiles();
				return true;
			}
			return false;
		} finally {
			setsLock.writeLock().unlock();
		}
	}

	/**
	 * Ensures that the {@link Set} for the given
	 * {@link DeviceProtocolInfoSource} contains all of the elements in the
	 * specified collection.
	 *
	 * @param type the {@link DeviceProtocolInfoSource} type.
	 * @param collection a {@link Collection} containing elements to be added.
	 * @return {@code true} if the {@link Set} for {@code type} changed as a
	 *         result of the call, {@code false} otherwise.
	 *
	 * @see #add(DeviceProtocolInfoSource, ProtocolInfo)
	 */
	public boolean addAll(DeviceProtocolInfoSource<?> type, Collection<? extends ProtocolInfo> collection) {
		setsLock.writeLock().lock();
		try {
			SortedSet<ProtocolInfo> currentSet;
			if (protocolInfoSets.containsKey(type)) {
				currentSet = protocolInfoSets.get(type);
			} else {
				currentSet = new TreeSet<ProtocolInfo>();
				protocolInfoSets.put(type, currentSet);
			}

			if (currentSet.addAll(collection)) {
				updateImageProfiles();
				return true;
			}
			return false;
		} finally {
			setsLock.writeLock().unlock();
		}
	}

	/**
	 * Removes a given instance of {@link ProtocolInfo}, if it is present in the
	 * {@link Set} for the given {@link DeviceProtocolInfoSource}. Returns
	 * {@code true} if the {@link Set} for the given
	 * {@link DeviceProtocolInfoSource} contained the specified element (or
	 * equivalently, if the {@link Set} for the given
	 * {@link DeviceProtocolInfoSource} changed as a result of the call).
	 *
	 * @param type the {@link DeviceProtocolInfoSource} type.
	 * @param protocolInfo element to be removed, if present.
	 * @return {@code true} if an element was removed from the {@link Set} for
	 *         {@code type} as a result of this call, {@code false} otherwise.
	 */
	public boolean remove(DeviceProtocolInfoSource<?> type, ProtocolInfo protocolInfo) {
		setsLock.writeLock().lock();
		try {
			SortedSet<ProtocolInfo> set = protocolInfoSets.get(type);
			if (set != null) {
				if (set.remove(protocolInfo)) {
					updateImageProfiles();
					return true;
				}
			}
			return false;
		} finally {
			setsLock.writeLock().unlock();
		}
	}

	/**
	 * Removes all instances of {@code protocolInfo} from all the
	 * {@link DeviceProtocolInfoSource} {@link Set}s, if it is present. Returns
	 * {@code true} if any of the {@link DeviceProtocolInfoSource} {@link Set}s
	 * contained the specified element (or equivalently, if any of the
	 * {@link DeviceProtocolInfoSource} {@link Set}s changed as a result of the
	 * call).
	 *
	 * @param protocolInfo element to be removed, if present.
	 * @return {@code true} if an element was removed as a result of this call,
	 *         {@code false} otherwise.
	 */
	public boolean remove(ProtocolInfo protocolInfo) {
		boolean result = false;
		setsLock.writeLock().lock();
		try {
			for (SortedSet<ProtocolInfo> set : protocolInfoSets.values()) {
				result |= set != null && set.remove(protocolInfo);
			}
			if (result) {
				updateImageProfiles();
			}
		} finally {
			setsLock.writeLock().unlock();
		}
		return result;
	}

	/**
	 * Removes all elements from the {@link Set} for the given
	 * {@link DeviceProtocolInfoSource} that are also contained in
	 * {@code collection}.
	 *
	 * @param type the {@link DeviceProtocolInfoSource} type.
	 * @param collection a {@link Collection} containing the elements to be
	 *            removed.
	 * @return {@code true} if this call resulted in a change.
	 *
	 * @see #remove(DeviceProtocolInfoSource, ProtocolInfo)
	 * @see #remove(ProtocolInfo)
	 * @see #contains(DeviceProtocolInfoSource, ProtocolInfo)
	 * @see #contains(ProtocolInfo)
	 * @see #removeAll(Collection)
	 */
	public boolean removeAll(DeviceProtocolInfoSource<?> type, Collection<ProtocolInfo> collection) {
		setsLock.writeLock().lock();
		try {
			SortedSet<ProtocolInfo> set = protocolInfoSets.get(type);
			if (set != null && set.removeAll(collection)) {
				updateImageProfiles();
				return true;
			}
			return false;
		} finally {
			setsLock.writeLock().unlock();
		}
	}

	/**
	 * Removes all elements from all the {@link DeviceProtocolInfoSource}
	 * {@link Set}s that are also contained in {@code collection}.
	 *
	 * @param collection a {@link Collection} containing the elements to be
	 *            removed.
	 * @return {@code true} if this call resulted in a change.
	 *
	 * @see #remove(DeviceProtocolInfoSource, ProtocolInfo)
	 * @see #remove(ProtocolInfo)
	 * @see #contains(DeviceProtocolInfoSource, ProtocolInfo)
	 * @see #contains(ProtocolInfo)
	 * @see #removeAll(DeviceProtocolInfoSource, Collection)
	 */
	public boolean removeAll(Collection<ProtocolInfo> collection) {
		boolean result = false;
		setsLock.writeLock().lock();
		try {
			for (SortedSet<ProtocolInfo> set : protocolInfoSets.values()) {
				result |= set != null && set.removeAll(collection);
			}
			if (result) {
				updateImageProfiles();
			}
		} finally {
			setsLock.writeLock().unlock();
		}
		return result;
	}

	/**
	 * Retains only the elements that are contained in {@code collection} from
	 * the {@link Set} for the given {@link DeviceProtocolInfoSource}. In other
	 * words, removes all elements that are not contained in {@code collection}
	 * from the {@link Set} for the given {@link DeviceProtocolInfoSource}.
	 *
	 * @param type the {@link DeviceProtocolInfoSource} type.
	 * @param collection a {@link Collection} containing elements to be
	 *            retained.
	 * @return {@code true} if this call resulted in a change.
	 *
	 * @see #remove(ProtocolInfo)
	 * @see #remove(DeviceProtocolInfoSource, ProtocolInfo)
	 * @see #contains(ProtocolInfo)
	 * @see #contains(DeviceProtocolInfoSource, ProtocolInfo)
	 * @see #retainAll(Collection)
	 */
	public boolean retainAll(DeviceProtocolInfoSource<?> type, Collection<ProtocolInfo> collection) {
		setsLock.writeLock().lock();
		try {
			SortedSet<ProtocolInfo> set = protocolInfoSets.get(type);
			if (set != null && set.retainAll(collection)) {
				updateImageProfiles();
				return true;
			}
			return false;
		} finally {
			setsLock.writeLock().unlock();
		}
	}

	/**
	 * Retains only the elements that are contained in {@code collection} in all
	 * the {@link DeviceProtocolInfoSource} {@link Set}s. In other words,
	 * removes all elements from all the {@link DeviceProtocolInfoSource}
	 * {@link Set}s that are not contained in {@code collection}.
	 *
	 * @param collection a {@link Collection} containing elements to be
	 *            retained.
	 * @return {@code true} if this call resulted in a change.
	 *
	 * @see #remove(ProtocolInfo)
	 * @see #remove(DeviceProtocolInfoSource, ProtocolInfo)
	 * @see #contains(ProtocolInfo)
	 * @see #contains(DeviceProtocolInfoSource, ProtocolInfo)
	 * @see #retainAll(DeviceProtocolInfoSource, Collection)
	 */
	public boolean retainAll(Collection<ProtocolInfo> collection) {
		boolean result = false;
		setsLock.writeLock().lock();
		try {
			for (SortedSet<ProtocolInfo> set : protocolInfoSets.values()) {
				result |= set != null && set.retainAll(collection);
			}
			if (result) {
				updateImageProfiles();
			}
		} finally {
			setsLock.writeLock().unlock();
		}
		return result;
	}


	// imageProfileSet "java.util.Collection methods" getters


	/**
	 * Returns the number of {@link DLNAImageProfile} elements. If this contains
	 * more than {@link Integer#MAX_VALUE} elements, returns
	 * {@link Integer#MAX_VALUE}.
	 *
	 * @return The number of {@link DLNAImageProfile} elements.
	 */
	public int imageProfilesSize() {
		setsLock.readLock().lock();
		try {
			return imageProfileSet.size();
		} finally {
			setsLock.readLock().unlock();
		}
	}

	/**
	 * Checks if is image profiles empty.
	 *
	 * @return {@code true} if this contains no {@link DLNAImageProfile}
	 *         elements.
	 */
	public boolean isImageProfilesEmpty() {
		setsLock.readLock().lock();
		try {
			return imageProfileSet.isEmpty();
		} finally {
			setsLock.readLock().unlock();
		}
	}

	/**
	 * Returns {@code true} if this contains the specified
	 * {@link DLNAImageProfile} instance.
	 *
	 * @param imageProfile the {@link DLNAImageProfile} instance whose presence
	 *            is to be tested.
	 * @return {@code true} if this contains the specified
	 *         {@link DLNAImageProfile} instance.
	 */
	public boolean imageProfilesContains(DLNAImageProfile imageProfile) {
		setsLock.readLock().lock();
		try {
			return imageProfileSet.contains(imageProfile);
		} finally {
			setsLock.readLock().unlock();
		}
	}

	/**
	 * Returns an array containing all the {@link DLNAImageProfile} instances in
	 * an unspecified order.
	 * <p>
	 * The returned array will be "safe" in that no references to it are
	 * maintained. (In other words, this method must allocate a new array). The
	 * caller is thus free to modify the returned array.
	 *
	 * @return An array containing all the {@link DLNAImageProfile} instances.
	 */
	public DLNAImageProfile[] imageProfilesToArray() {
		setsLock.readLock().lock();
		try {
			return imageProfileSet.toArray(new DLNAImageProfile[imageProfileSet.size()]);
		} finally {
			setsLock.readLock().unlock();
		}
	}


	/**
	 * Returns {@code true} if this contains all the {@link DLNAImageProfile} instances in the
	 * specified collection.
	 *
	 * @param collection a {@link Collection} to be checked for containment.
	 * @return {@code true} if this collection contains all of the {@link DLNAImageProfile} instances in
	 *         {@code collection}.
	 *
	 * @see #imageProfilesContains(DLNAImageProfile)
	 */
	public boolean imageProfilesContainsAll(Collection<DLNAImageProfile> collection) {
		setsLock.readLock().lock();
		try {
			return imageProfileSet.containsAll(collection);
		} finally {
			setsLock.readLock().unlock();
		}
	}

	@Override
	public String toString() {
		return toString(null, false);
	}

	/**
	 * Returns a string representation of this {@link DeviceProtocolInfo}
	 * instance showing only the {@link ProtocolInfo} instances of the given
	 * {@link DeviceProtocolInfoSource} type. If {@code debug} is {@code true},
	 * verbose output is returned.
	 *
	 * @param type the {@link DeviceProtocolInfoSource} type to include.
	 * @return A string representation of this {@link DeviceProtocolInfo}.
	 */
	public String toString(DeviceProtocolInfoSource<?> type) {
		return toString(type, false);
	}

	/**
	 * Returns a string representation of this {@link DeviceProtocolInfo}
	 * instance. If {@code debug} is {@code true}, verbose output is returned.
	 *
	 * @param debug whether or not verbose output should be generated.
	 * @return A string representation of this {@link DeviceProtocolInfo}.
	 */
	public String toString(boolean debug) {
		return toString(null, debug);
	}

	/**
	 * Returns a string representation of this {@link DeviceProtocolInfo}
	 * instance showing only the {@link ProtocolInfo} instances of the given
	 * {@link DeviceProtocolInfoSource} type. If {@code debug} is {@code true},
	 * verbose output is returned.
	 *
	 * @param type the {@link DeviceProtocolInfoSource} type to include. Use
	 *            {@code null} for all types.
	 * @param debug whether or not verbose output should be generated.
	 * @return A string representation of this {@link DeviceProtocolInfo}.
	 */
	public String toString(DeviceProtocolInfoSource<?> type, boolean debug) {
		StringBuilder sb = new StringBuilder();
		setsLock.readLock().lock();
		try {
			if (protocolInfoSets != null && !protocolInfoSets.isEmpty()) {
				for (Entry<DeviceProtocolInfoSource<?>, SortedSet<ProtocolInfo>>  entry : protocolInfoSets.entrySet()) {
					if (type == null || type.equals(entry.getKey())) {
						if (!entry.getValue().isEmpty()) {
							sb.append(entry.getKey().getType()).append(" entries:\n");
							for (ProtocolInfo protocolInfo : entry.getValue()) {
								if (protocolInfo != null) {
									sb.append("  ").append(debug ?
										protocolInfo.toDebugString() :
										protocolInfo.toString()
									).append("\n");
								}
							}
							sb.append("\n");
						}
					}
				}
			}
			if (imageProfileSet != null && !imageProfileSet.isEmpty()) {
				sb.append("DLNAImageProfile entries:\n");
				for (DLNAImageProfile imageProfile : imageProfileSet) {
					if (imageProfile != null) {
						sb.append("  ").append(imageProfile).append("\n");
					}
				}
			}
		} finally {
			setsLock.readLock().unlock();
		}
		return sb.toString();
	}


	// Static methods


	/**
	 * Escapes {@code protocolInfo} strings for use in {@code GetProtocolInfo}
	 * in accordance with DLNA comma escaping rules.
	 *
	 * @param unescapedString the {@code protocolInfo} string to escape.
	 * @return The escaped {@link String};
	 */
	public static String escapeString(String unescapedString) {
		return PROTOCOLINFO_ESCAPE.translate(unescapedString);
	}

	/**
	 * Unescapes {@code protocolInfo} strings after splitting a string from
	 * {@code GetProtocolInfo} into individual elements in accordance with DLNA
	 * comma escaping rules.
	 *
	 * @param escapedString the {@code protocolInfo} string to unescape.
	 * @return The unescaped {@link String};
	 */
	public static String unescapeString(String escapedString) {
		return PROTOCOLINFO_UNESCAPE.translate(escapedString);
	}

	/**
	 * Handles known special cases, i.e bugs in renderers' {@code protocolInfo}
	 * output so that we are able to parse them despite them being broken.
	 *
	 * @param element the {@code protocolInfo} element to handle if needed.
	 * @return {@code null} if {@code element} doesn't match a known special
	 *         case, or a {@link SortedSet} of {@link ProtocolInfo} instances
	 *         with the result of the parsed special case.
	 * @throws ParseException If {@code element} is a known special case but the
	 *             parsing fails.
	 */
	public static SortedSet<ProtocolInfo> handleSpecialCaseString(String element) throws ParseException {
		if (isBlank(element)) {
			return null;
		}
		switch (element) {
			/*
			 * Seen on a LG-BP550-1, missing comma between elements
			 */
			case "http-get:*:audio/sonyoma:*http-get:*:audio/ogg:*":
				SortedSet<ProtocolInfo> currentSet = new TreeSet<>();
				currentSet.add(new ProtocolInfo("http-get:*:audio/sonyoma:*"));
				currentSet.add(new ProtocolInfo("http-get:*:audio/ogg:*"));
				return currentSet;
		}
		return null;
	}

	/**
	 * This is an abstract implementation of {@link DeviceProtocolInfoSource}
	 * where {@link DeviceProtocolInfo} is the parsing class.
	 *
	 * @author Nadahar
	 */
	public abstract static class GetProtocolInfoType extends DeviceProtocolInfoSource<DeviceProtocolInfo> {

		private static final long serialVersionUID = 1L;

		@Override
		public Class<DeviceProtocolInfo> getClazz() {
			return DeviceProtocolInfo.class;
		}
	}
}
