package de.tu_darmstadt.tk.SmartHomeNetworkSim.core;

import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.function.Predicate;

/**
 * A packet collector allows collection of {@link Packet}s from one or multiple {@link Link}s or {@link SmartDevice}s.<br>
 * The Packets are stored per Link.<br>
 * All packets which are sent from/to one of the devices can be filtered by the
 * {@link #getFilter() getFilter()}.<br>
 * Packets should be collected by adding them via the
 * {@link #addPackets(Link, Collection)} method.<br>
 * 
 * @author Andreas T. Meyer-Berg
 */
public class PacketCollector {
	/**
	 * Devices which packets should be collected. Just devices from/to the
	 * device will be collected.
	 */
	private LinkedList<SmartDevice> devices = new LinkedList<SmartDevice>();

	/**
	 * All packets which are sent via this link are collected.
	 */
	private LinkedList<Link> links = new LinkedList<Link>();

	/**
	 * Packets which were collected by this packet collector
	 */
	private HashMap<Link,LinkedList<Packet>> collectedPackets = new HashMap<Link, LinkedList<Packet>>();
	
	/**
	 * Packet Sniffing Algorithm, which 
	 */
	private PacketSniffer packetAlgorithm = null;
	
	/**
	 * Mode of the Collector, whether it should train or test the packetSniffer algorithm
	 */
	private boolean mode = false;
	
	/**
	 * True if it should collect packages
	 */
	private boolean active = true;
	
	/**
	 * Creates a PacketCollector without assigning and algorithm to process the packets
	 */
	public PacketCollector() {
		packetAlgorithm = null;
	}
	
	/**
	 * Creates a PacketCollector and assigns an algorithm to process the packets
	 * @param sniffer PacketSniffer to process the packets collected by this collector
	 */
	public PacketCollector(PacketSniffer sniffer) {
		packetAlgorithm = sniffer;
	}
	
	/**
	 * Adds a new Link, which packets should be collected. All packets send via
	 * the link will be collected from now on.
	 * 
	 * @param link
	 *            link, which packets should be collected.
	 */
	public void addLink(Link link) {
		if(!links.contains(link)){
			links.add(link);
			collectedPackets.put(link, new LinkedList<Packet>());
		}
	}

	/**
	 * Remove link from Links that should be collected. Packets sent via this
	 * Link will no longer be collected. Just if a separate Device which
	 * contains the link should be collected.
	 * 
	 * @param link link which packets should no longer be collected
	 */
	public void removeLink(Link link) {
		links.remove(link);
		collectedPackets.remove(link).clear();
	}
	
	/**
	 * Returns all links, which should be collected by this PacketCollector
	 * @return links, which packets should be collected
	 */
	public Collection<Link> getLinks() {
		return links;
	}

	/**
	 * Adds device, which packets should be collected. All packets sent to or received from this device will be collected.
	 * @param device device, which packets should be collected
	 */
	public void addDevice(SmartDevice device) {
		if(!devices.contains(device))
			devices.add(device);
	}

	/**
	 * Remove device. Packets sent to or from this device will no longer be collected. Unless it participates in one of the links.
	 * @param device device to be removed
	 */
	public void removeDevice(SmartDevice device) {
		devices.remove(device);
	}

	/**
	 * Returns all devices which packets are being collected by this Packet Collector.
	 * @return devices, which packets are being collected
	 */
	public Collection<SmartDevice> getDevices() {
		return devices;
	}

	/**
	 * Predicate which is true if the given packet was sent from or to one of
	 * the devices, stored in this PacketCollector
	 * 
	 * @return true if it was sent from or to one of the devices stored in this
	 *         packet collector
	 */
	public Predicate<? super Packet> getFilter() {
		return p -> /* filter devices where source or destination is null */
		(p.getSource() != null && p.getDestination() != null && p.getSource().getOwner() != null
				&& p.getDestination().getOwner() != null)
				/* return true if */
				/*&& (devices.contains(p.getSource().getOwner())||devices.contains(p.getDestination().getOwner()))*/
				 && devices.stream().anyMatch(d -> d == p.getSource().getOwner() || d == p.getDestination().getOwner());
	}

	/**
	 * Adds packets which should be collected by this PacketCollector.
	 * @param link link, which the packets were sent on
	 * @param packets packets which were sent
	 */
	public void addPackets(Link link, Collection<Packet> packets) {
		if(link == null)
			return;
		LinkedList<Packet> packetsOfLink = collectedPackets.get(link);
		if(packetsOfLink !=null)
			packetsOfLink.addAll(packets);
		
		collectedPackets.put(link, new LinkedList<>(packets));
			
	}
	
	/**
	 * Returns the packets which were collected
	 * @return collected packets
	 */
	public HashMap<Link,LinkedList<Packet>> getPackets(){
		return collectedPackets;
	}
	
	/**
	 * Return the collected Packets of the link
	 * @param link Link, which packets should be returned
	 * @return packets, collected on the given link
	 */
	public LinkedList<Packet> getPacketsOfLink(Link link){
		return collectedPackets.get(link);
	}
	
	/**
	 * Resets the collected Packets list, by clearing the list.
	 */
	public void resetPackets(){
		collectedPackets.clear();
	}

	/**
	 * @return the packetSnifferAlgorithm
	 */
	public PacketSniffer getPacketAlgorithm() {
		return packetAlgorithm;
	}

	/**
	 * @param packetAlgorithm the packetSnifferAlgorithm to set
	 */
	public void setPacketAlgorithm(PacketSniffer packetAlgorithm) {
		this.packetAlgorithm = packetAlgorithm;
	}

	/**
	 * Returns training or testing mode of the algorithm.
	 * @return true if testing or false if training the algorithm.
	 */
	public boolean getMode() {
		if(packetAlgorithm!=null)
			mode = packetAlgorithm.getMode();
		return mode;
	}

	/**
	 * Sets the training or testing mode
	 * @param mode true if testing or false if training the algorithm.
	 */
	public void setMode(boolean mode) {
		this.mode = mode;
		if(packetAlgorithm != null)
			packetAlgorithm.setMode(mode);
	}

	/**
	 * Whether the algorithm should run
	 * @return true if algorithm will be executed
	 */
	public boolean isActive() {
		return active;
	}

	/**
	 * Set to true, if it should run
	 * @param active true if it should be active
	 */
	public void setActive(boolean active) {
		this.active = active;
	}
}
