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.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.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Arrays;

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

	private Socket responseSocket; // socket used to recept request and send response
	private Socket 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 ProtocolP2PPacketTCP(T payload) {
		super(payload);
	}


	/** Send a Packet. Socket must be set and connected.
	 * @param socket	Socket used to send Packet.
	 * @throws	InternalError
	 * @throws	SocketClosed
	 * @throws	IOException
	 */
	protected void send(Socket socket) throws InternalError, SocketClosed, IOException {
		assert socket != null : "Trying to send a Packet but no socket defined";
		assert socket.isConnected() : "Trying to send a Packet but socket not connected";
		if (socket == null || (!socket.isConnected())) {
			throw new InternalError();
		}
		// generate Packet
		byte[] packet = toPacket();
		// send it
		try {
			OutputStream outputStream = socket.getOutputStream();
			outputStream.write(packet);
			outputStream.flush();
		} catch (IOException e) {
			// Error: cannot send response, closing socket
			try {
				socket.close();
			} catch (IOException e2) {
				System.err.println("Cannot close socket");
				throw new SocketClosed();
			}
			
		}
	}

	/** Send a Request throught socket. Socket must be connected (typically used from client).
	 * @param socket	Socket. Must be connected.
	 * @throws	InternalError
	 * @throws	SocketClosed
	 * @throws	IOException
	 */
	public void sendRequest(Object socket) throws InternalError, IOException, SocketClosed {
		assert socket instanceof Socket: "Wrong socket type";
		if (socket instanceof Socket) {
			requestSocket = (Socket)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	SocketClosed
	 * @throws	IOException
	 * @return	ProtocolP2PPacket received.
	 */
	public ProtocolP2PPacketTCP(Object socket) throws TransmissionError, ProtocolError, VersionError, InternalError, SizeError, SocketClosed, IOException {
		super(socket);
		assert socket instanceof Socket : "Wrong socket type";
		if (!(socket instanceof Socket)) {
			throw new InternalError();
		}
		Socket ss = (Socket)socket;
		byte[] packetHeader = new byte[Payload.PAYLOAD_START_POSITION];
		try {
			if (-1 == ss.getInputStream().read(packetHeader, 0, Payload.PAYLOAD_START_POSITION)) {
				throw new IOException();
			}
			int payloadSize = Payload.getPayloadSize(packetHeader);
			byte[] packet = Arrays.copyOf(packetHeader, Payload.PAYLOAD_START_POSITION + payloadSize);
			if (-1 == ss.getInputStream().read(packet, Payload.PAYLOAD_START_POSITION, payloadSize)) {
				throw new IOException();
			}
			// 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 ProtocolP2PPacketTCP<Payload>(new Payload(RequestResponseCode.INTERNAL_ERROR))).send(ss);
				throw e;
			} catch (ProtocolError e) {
				(new ProtocolP2PPacketTCP<Payload>(new Payload(RequestResponseCode.PROTOCOL_ERROR))).send(ss);
				throw e;
			} catch (VersionError e) {
				(new ProtocolP2PPacketTCP<Payload>(new Payload(RequestResponseCode.VERSION_ERROR))).send(ss);
				throw e;
			} catch (InternalError e) {
				(new ProtocolP2PPacketTCP<Payload>(new Payload(RequestResponseCode.INTERNAL_ERROR))).send(ss);
				throw e;
			} catch (SizeError e) {
				(new ProtocolP2PPacketTCP<Payload>(new Payload(RequestResponseCode.INTERNAL_ERROR))).send(ss);
				throw e;
			}
			if (protocolError) {
				throw new ProtocolError();
			}
		} catch (IOException e) {
			// Error: cannot read request, closing socket
			try {
				ss.close();
				throw new SocketClosed();
			} catch (IOException e2) {
				System.err.println("Cannot close socket");
				throw new SocketClosed();
			}
		}
	}

	/** Send a Response to a Request (typically used from server).
	 * @param response	Packet to send as a response.
	 * @throws	InternalError
	 * @throws	IOException
	 * @throws	SocketClosed
	 */
	public <U extends ProtocolP2PPacket<?>> void sendResponse(U response) throws InternalError, IOException, SocketClosed {
		assert response instanceof ProtocolP2PPacketTCP: "Wrong Packet type";
		if (response instanceof ProtocolP2PPacketTCP) {
			ProtocolP2PPacketTCP<?> r = (ProtocolP2PPacketTCP<?>) response;
			assert responseSocket != null : "Cannot send response to a packet not received";
			if (responseSocket == null) {
				throw new InternalError();
			}
			r.send(responseSocket);
		} 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	SocketClosed
	 * @throws	UnknownHost
	 */
	public ProtocolP2PPacket<?> receiveResponse() throws EmptyFile, NotFound, NotATracker, EmptyDirectory, InternalRemoteError, VersionRemoteError, ProtocolRemoteError, TransmissionError, ProtocolError, VersionError, InternalError, SizeError, IOException, SocketClosed, UnknownHost {
		assert requestSocket != null : "Cannot receive response because request packet not sent.";
		if (requestSocket == null) {
			throw new InternalError();
		}
		// reception
		byte[] packetHeader = new byte[Payload.PAYLOAD_START_POSITION];
		try {
			if (-1 == requestSocket.getInputStream().read(packetHeader, 0, Payload.PAYLOAD_START_POSITION)) {
				throw new IOException();
			}
			int payloadSize = Payload.getPayloadSize(packetHeader);
			byte[] packet = Arrays.copyOf(packetHeader, Payload.PAYLOAD_START_POSITION + payloadSize);
			if (-1 == requestSocket.getInputStream().read(packet, Payload.PAYLOAD_START_POSITION, payloadSize)) {
				throw new IOException();
			}
			// contruction
			try {
				ProtocolP2PPacketTCP<?> p = new ProtocolP2PPacketTCP<>(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 ProtocolP2PPacketTCP<Payload>(new Payload(RequestResponseCode.INTERNAL_ERROR))).send(requestSocket);
				throw e;
			} catch (ProtocolError e) {
				(new ProtocolP2PPacketTCP<Payload>(new Payload(RequestResponseCode.PROTOCOL_ERROR))).send(requestSocket);
				throw e;
			} catch (VersionError e) {
				(new ProtocolP2PPacketTCP<Payload>(new Payload(RequestResponseCode.VERSION_ERROR))).send(requestSocket);
				throw e;
			} catch (InternalError e) {
				(new ProtocolP2PPacketTCP<Payload>(new Payload(RequestResponseCode.INTERNAL_ERROR))).send(requestSocket);
				throw e;
			} catch (SizeError e) {
				(new ProtocolP2PPacketTCP<Payload>(new Payload(RequestResponseCode.INTERNAL_ERROR))).send(requestSocket);
				throw e;
			}
		} catch (IOException e) {
			// Error: cannot read request, closing socket
			try {
				requestSocket.close();
				throw new SocketClosed();
			} catch (IOException e2) {
				System.err.println("Cannot close socket");
				throw new SocketClosed();
			}
		}
	}

	/** 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 ProtocolP2PPacketTCP(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
		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, Socket 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;
		return packet;
	}

	/** Get hostItem of the sender
	 * @return	hostItem of the sender
	 * @throws	InternalError
	 */
	public HostItem getHostItem() throws InternalError {
		if (responseSocket == null) {
			throw new InternalError();
		}
		return new HostItem(responseSocket.getInetAddress().getHostName(), responseSocket.getPort());
	}

}
