package protocolP2P;
import localException.InternalError;
import localException.ProtocolError;
import localException.SizeError;
import localException.TransmissionError;
import localException.VersionError;
import localException.SocketClosed;
import remoteException.NotATracker;
import remoteException.EmptyDirectory;
import remoteException.InternalRemoteError;
import remoteException.NotFound;
import remoteException.ProtocolRemoteError;
import remoteException.VersionRemoteError;
import remoteException.EmptyFile;
import remoteException.UnknownHost;
import tools.BytesArrayTools;
import tools.HostItem;
import protocolP2P.Payload;
import protocolP2P.RequestResponseCode;
import protocolP2P.LoadRequest;
import protocolP2P.FileList;
import protocolP2P.FilePart;
import protocolP2P.RatioRequest;
import protocolP2P.RatioResponse;
import protocolP2P.UpdateRatio;
import protocolP2P.Denied;
import protocolP2P.SizeRequest;
import protocolP2P.SizeResponse;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketAddress;
import java.io.IOException;
import java.util.Arrays;

/** Representation of packet.
 * @author	Louis Royer
 * @author	Flavien Haas
 * @author	JS Auge
 * @version	1.0
 */
public class ProtocolP2PPacketUDP < T extends Payload> extends ProtocolP2PPacket < T > {

	private final static int CHECKSUM_POSITION = 2;
	private HostItem remoteHost;
	private SocketAddress responseSocketAddress; // socket address used when receptionning request and to sending response
	private DatagramSocket responseSocket; // socket used to recept request and send response
	private DatagramSocket requestSocket; // socket used to send request and to reception response

	/** Constructor with payload parameter (typically used when sending packet).
	 * @param payload	the payload associated with the packet to send
	 */
	public ProtocolP2PPacketUDP(T payload) {
		super(payload);
	}

	/** Send a Packet. Socket must be set and connected.
	 * @param socket	DatagramSocket used to send Packet.
	 * @throws	InternalError
	 * @throws	IOException
	 */
	protected void send(DatagramSocket socket) throws InternalError, IOException {
		send(socket, null);
	}

	/** Send a Packet. Socket must be set and connected, or an addr can be given.
	 * @param socket	DatagramSocket used to send Packet.
	 * @param addr	SocketAddress used to send Packet.
	 * @throws	InternalError
	 * @throws	IOException
	 */
	protected void send(DatagramSocket socket, SocketAddress addr) throws InternalError, IOException {
		assert socket != null : "Trying to send a Packet but no socket defined";
		assert socket.isConnected() || addr != null : "Trying to send a Packet but socket not connected, and no socketAddress given";
		if (socket == null || ((!socket.isConnected()) && (addr == null))) {
			throw new InternalError();
		}
		// generate DatagramPacket
		byte[] packet = toPacket();
		DatagramPacket datagramPacket = new DatagramPacket(packet, packet.length);
		if (addr != null) {
			datagramPacket.setSocketAddress(addr);
		}
		// send it
		socket.send(datagramPacket);
	}

	/** Send a Request throught socket. Socket must be connected (typically used from client).
	 * @param socket	DatagramSocket. Must be connected.
	 * @throws	InternalError
	 * @throws	IOException
	 */
	public void sendRequest(Object socket) throws InternalError, IOException {
		assert socket instanceof DatagramSocket: "Wrong socket type";
		if (socket instanceof DatagramSocket) {
			requestSocket = (DatagramSocket)socket;
			send(requestSocket);
		} else {
			throw new InternalError();
		}
	}

	/** Receive Request (typically used from server).
	 * @param socket	socket used to receive request
	 * @throws	TransmissionError
	 * @throws	ProtocolError
	 * @throws	VersionError
	 * @throws	InternalError
	 * @throws	SizeError
	 * @throws	IOException
	 * @throws	SocketClosed
	 * @return	ProtocolP2PPacket received.
	 */
	public ProtocolP2PPacketUDP(Object socket) throws TransmissionError, ProtocolError, VersionError, InternalError, SizeError, IOException, SocketClosed {
		super(socket);
		assert socket instanceof DatagramSocket : "Wrong socket type";
		if (!(socket instanceof DatagramSocket)) {
			throw new InternalError();
		}
		DatagramSocket ss = (DatagramSocket)socket;
		byte[] packetTmp = new byte[0xFFFF];
		DatagramPacket reception = new DatagramPacket(packetTmp, packetTmp.length);
		ss.receive(reception);
		int payloadSize = Payload.getPayloadSize(packetTmp);
		byte[] packet = Arrays.copyOf(packetTmp, Payload.PAYLOAD_START_POSITION + payloadSize);
		responseSocketAddress = reception.getSocketAddress();
		remoteHost = new HostItem(reception.getAddress().getHostName(), reception.getPort());
		// contruction
		boolean protocolError = false;
		try {
			constructPacket(packet, ss);
			Payload payload = getPayload();
			switch (payload.getRequestResponseCode()) {
				case PROTOCOL_ERROR :
					// we do not want to create an infinite loop of protocolError message exchange.
					protocolError = true;
					break;
				case VERSION_ERROR :
				case INTERNAL_ERROR :
				case EMPTY_DIRECTORY :
				case NOT_FOUND :
				case EMPTY_FILE:
				case LOAD_RESPONSE:
				case LIST_RESPONSE:
				case HASH_RESPONSE:
				case DISCOVER_RESPONSE:
				case NOT_A_TRACKER:
				case RATIO_RESPONSE:
				case DENIED:
				case SIZE_RESPONSE:
					// we were expecting a request, but we are receiving a response
					throw new ProtocolError();
				default :
					break;
			}
		} catch (TransmissionError e) {
			(new ProtocolP2PPacketUDP<Payload>(new Payload(RequestResponseCode.INTERNAL_ERROR))).send(ss, responseSocketAddress);
			throw e;
		} catch (ProtocolError e) {
			(new ProtocolP2PPacketUDP<Payload>(new Payload(RequestResponseCode.PROTOCOL_ERROR))).send(ss, responseSocketAddress);
			throw e;
		} catch (VersionError e) {
			(new ProtocolP2PPacketUDP<Payload>(new Payload(RequestResponseCode.VERSION_ERROR))).send(ss, responseSocketAddress);
			throw e;
		} catch (InternalError e) {
			(new ProtocolP2PPacketUDP<Payload>(new Payload(RequestResponseCode.INTERNAL_ERROR))).send(ss, responseSocketAddress);
			throw e;
		} catch (SizeError e) {
			(new ProtocolP2PPacketUDP<Payload>(new Payload(RequestResponseCode.INTERNAL_ERROR))).send(ss, responseSocketAddress);
			throw e;
		}
		if (protocolError) {
			throw new ProtocolError();
		}
	}

	/** Send a Response to a Request (typically used from server).
	 * @param response	Packet to send as a response.
	 * @throws	InternalError
	 * @throws	IOException
	 */
	public <U extends ProtocolP2PPacket<?>>void sendResponse(U response) throws InternalError, IOException {
		assert response instanceof ProtocolP2PPacketUDP: "Wrong Packet type";
		if (response instanceof ProtocolP2PPacketUDP) {
			ProtocolP2PPacketUDP<?> r = (ProtocolP2PPacketUDP<?>) response;
			assert responseSocket != null : "Cannot send response to a packet not received";
			if (responseSocket == null) {
				throw new InternalError();
			}
			assert responseSocketAddress != null : "Cannot send response because address of client has not been saved";
			if (responseSocketAddress == null) {
				throw new InternalError();
			}
			r.send(responseSocket, responseSocketAddress);
		} else {
			throw new InternalError();
		}
	}

	/** Receive response (typically used by client).
	 * @return	ProtocolP2PPacket received
	 * @throws	EmptyFile
	 * @throws	NotFound
	 * @throws	NotATracker
	 * @throws	EmptyDirectory
	 * @throws	InternalRemoteError
	 * @throws	VersionRemoteError
	 * @throws	ProtocolRemoteError
	 * @throws	TransmissionError
	 * @throws	ProtocolError
	 * @throws	VersionError
	 * @throws	InternalError
	 * @throws	SizeError
	 * @throws	IOException
	 * @throws	UnknownHost
	 */
	public ProtocolP2PPacket<?> receiveResponse() throws EmptyFile, NotFound, NotATracker, EmptyDirectory, InternalRemoteError, VersionRemoteError, ProtocolRemoteError, TransmissionError, ProtocolError, VersionError, InternalError, SizeError, IOException, UnknownHost {
		assert requestSocket != null : "Cannot receive response because request packet not sent.";
		if (requestSocket == null) {
			throw new InternalError();
		}
		// reception
		byte[] packetTmp = new byte[0xFFFF];
		DatagramPacket reception = new DatagramPacket(packetTmp, packetTmp.length);
		requestSocket.receive(reception);
		int payloadSize = Payload.getPayloadSize(packetTmp);
		byte[] packet = Arrays.copyOf(packetTmp, Payload.PAYLOAD_START_POSITION + payloadSize);
		
		// contruction
		try {
			ProtocolP2PPacketUDP<?> p = new ProtocolP2PPacketUDP<>(packet);
			Payload payload = p.getPayload();
			switch (payload.getRequestResponseCode()) {
				case PROTOCOL_ERROR :
					throw new ProtocolRemoteError();
				case VERSION_ERROR :
					throw new VersionRemoteError();
				case INTERNAL_ERROR :
					throw new InternalRemoteError();
				case EMPTY_DIRECTORY :
					throw new EmptyDirectory();
				case NOT_FOUND :
					throw new NotFound();
				case EMPTY_FILE:
					throw new EmptyFile();
				case NOT_A_TRACKER:
					throw new NotATracker();
				case UNKNOWN_HOST:
					throw new UnknownHost();
				default :
					return (ProtocolP2PPacket)p;
			}
		} catch (TransmissionError e) {
			(new ProtocolP2PPacketUDP<Payload>(new Payload(RequestResponseCode.INTERNAL_ERROR))).send(requestSocket);
			throw e;
		} catch (ProtocolError e) {
			(new ProtocolP2PPacketUDP<Payload>(new Payload(RequestResponseCode.PROTOCOL_ERROR))).send(requestSocket);
			throw e;
		} catch (VersionError e) {
			(new ProtocolP2PPacketUDP<Payload>(new Payload(RequestResponseCode.VERSION_ERROR))).send(requestSocket);
			throw e;
		} catch (InternalError e) {
			(new ProtocolP2PPacketUDP<Payload>(new Payload(RequestResponseCode.INTERNAL_ERROR))).send(requestSocket);
			throw e;
		} catch (SizeError e) {
			(new ProtocolP2PPacketUDP<Payload>(new Payload(RequestResponseCode.INTERNAL_ERROR))).send(requestSocket);
			throw e;
		}
	}

	/** Private constructor with packet as byte[] parameter (typically used when receiving Packet response).
	 * @param packet	the full Packet received
	 * @throws	TransmissionError
	 * @throws	ProtocolError
	 * @throws	VersionError
	 * @throws	InternalError
	 * @throws	SizeError
	 */
	private ProtocolP2PPacketUDP(byte[] packet) throws TransmissionError, ProtocolError, VersionError, InternalError, SizeError {
		super(packet);
		constructPacket(packet);
	}

	/** Private constructor helper with packet as byte[] parameter (typically used when receiving Packet response/request).
	 * @param packet	the full Packet received
	 * @throws	TransmissionError
	 * @throws	ProtocolError
	 * @throws	VersionError
	 * @throws	InternalError
	 * @throws	SizeError
	 */
	private void constructPacket(byte[] packet) throws TransmissionError, ProtocolError, VersionError, InternalError, SizeError {
		// unwrap version
		version = packet[VERSION_POSITION];
		checkProtocolVersion(); // this can throw VersionError
		checkCheckSum(packet);
		RequestResponseCode r = RequestResponseCode.fromCode(packet[RequestResponseCode.RRCODE_POSITION]); // this can throw ProtocolError
		switch (r) {
			case LIST_RESPONSE:
				payload = (Payload) new FileList(packet);
				break;
			case LOAD_RESPONSE:
				payload = (Payload) new FilePart(packet);
				break;
			case LOAD_REQUEST:
				payload = (Payload) new LoadRequest(packet);
				break;
			case HASH_REQUEST:
				payload = (Payload) new HashRequest(packet);
				break;
			case HASH_RESPONSE:
				payload = (Payload) new HashResponse(packet);
				break;
			case REGISTER:
				payload = (Payload) new Register(packet);
				break;
			case UNREGISTER:
				payload = (Payload) new Unregister(packet);
				break;
			case DISCOVER_REQUEST:
				payload = (Payload) new DiscoverRequest(packet);
				break;
			case DISCOVER_RESPONSE:
				payload = (Payload) new DiscoverResponse(packet);
				break;
			case RATIO_REQUEST:
				payload = (Payload) new RatioRequest(packet);
				break;
			case RATIO_RESPONSE:
				payload = (Payload) new RatioResponse(packet);
				break;
			case UPDATE_RATIO:
				payload = (Payload) new UpdateRatio(packet);
				break;
			case DENIED:
				payload = (Payload) new Denied(packet);
				break;
			case SIZE_REQUEST:
				payload = (Payload) new SizeRequest(packet);
				break;
			case SIZE_RESPONSE:
				payload = (Payload) new SizeResponse(packet);
				break;
			default:
				payload = new Payload(packet); // this can throw TransmissionError
				break;
		}
	}

	/** Private constructor helper with packet as byte[] parameter and (typically used when receiving Packet request).
	 * @param packet	the full Packet received
	 * @param responseSocket	socket address used to reception this request (use this one to respond)
	 * @throws	TransmissionError
	 * @throws	ProtocolError
	 * @throws	VersionError
	 * @throws	InternalError
	 * @throws	SizeError
	 */
	private void constructPacket(byte[] packet, DatagramSocket responseSocket) throws TransmissionError, ProtocolError, VersionError, InternalError, SizeError {
		constructPacket(packet);
		this.responseSocket = responseSocket;
	}

	/** Returns a byte[] containing full packet (typically used when sending packet).
	 * This packet is complete and ready to be send.
	 * @return the full packet to send
	 * @throws	InternalError
	 */
	protected byte[] toPacket() throws InternalError {
		byte[] packet = payload.toPacket();
		packet[VERSION_POSITION] = version;
		setCheckSum(packet);
		return packet;
	}

	/** Compute checksum associated to packet.
	 * @param packet	the full packet received
	 * @throws	SizeError
	 */
	private int computeCheckSum(byte [] packet) throws SizeError {
		/*
		* The checksum field is the 16 bit one’s complement of the one’s complement sum of all 16-bit words
		* in the header and text. If a segment contains an odd number of header and text octets to be checksummed,
		* the last octet is padded on the right with zeros to form a 16-bit word for checksum purposes.
		* The pad is not transmitted as part of the segment. While computing the checksum, the checksum field
		* itself is replaced with zeros.
		*/
		int checksum = 0;
		for (int i=CHECKSUM_POSITION+2; i<CHECKSUM_POSITION+2+4+Payload.getPayloadSize(packet); ++i) {
			checksum += BytesArrayTools.readInt16Bits(packet, i);
			checksum &= 0xffff;
		}
		return checksum ^ 0xffff;
	}

	/** Used to set checksum into packet
	* @param packet	full packet
	* @throws InternalError
	*/
	private void setCheckSum(byte [] packet) throws InternalError {
		try {
			int checksum = computeCheckSum(packet);
			BytesArrayTools.write16Bits(packet, CHECKSUM_POSITION, checksum);
		} catch (SizeError e) {
			throw new InternalError();
		}
	}

	/** Used to check if the checksum is correct
	* @param packet	full packet
	* @throws TransmissionError
	*/
	private void checkCheckSum(byte [] packet) throws TransmissionError {
		try {
			int checksum = BytesArrayTools.readInt16Bits(packet, CHECKSUM_POSITION);
			int computedCheckSum = computeCheckSum(packet);
			if (computedCheckSum != checksum){
				throw new TransmissionError();
			}
		} catch(SizeError e) {
			throw new TransmissionError();
		}
	}

	/** Get hostItem of the sender
	 * @return	hostItem of the sender
	 * @throws	InternalError
	 */
	public HostItem getHostItem() throws InternalError {
		if (remoteHost == null) {
			throw new InternalError();
		}
		return remoteHost;
	}
}
