package clientP2P;
import java.util.List;
import java.util.ArrayList;
import java.net.DatagramSocket;
import protocolP2P.ProtocolP2PPacketUDP;
import protocolP2P.Payload;
import protocolP2P.LoadRequest;
import protocolP2P.FilePart;
import localException.InternalError;
import remoteException.EmptyDirectory;
import remoteException.EmptyFile;
import localException.ProtocolError;
import remoteException.InternalRemoteError;
import remoteException.VersionRemoteError;
import localException.TransmissionError;
import remoteException.ProtocolRemoteError;
import localException.VersionError;
import localException.SizeError;
import remoteException.NotFound;
import java.nio.file.Files;
import java.io.File;
import java.nio.file.Paths;
import java.io.IOException;
import tools.Logger;
import tools.LogLevel;

/** Class to download file parts on udp.
* @author	Louis Royer
* @author	Flavien Haas
* @author	JS Auge
* @version	1.0
*/
public class ClientDownloadPartUDP implements Runnable {

	private List<Long> toDoTasks;
	private List<Long> pendingTasks;
	private List<Long> tasksDone;
	private volatile boolean tasksListsLock;
	private volatile boolean stop;
	private volatile boolean failed;
	private String filename;
	private DatagramSocket socket;
	private volatile boolean noTask;
	private String partsSubdir;
	private static final long MAX_PARTIAL_SIZE = 4096;
	private ClientDownloadUDP manager;
	private Logger logger;

	/** Constructor with filename, socket, and part subdir
	 * @param filename	name of file to download
	 * @param socket	socket to use
	 * @param partsSubdir	directory to store .part files
	 */
	public ClientDownloadPartUDP(ClientDownloadUDP manager, String filename, DatagramSocket socket, String partsSubdir, Logger logger) {
		this.manager = manager;
		this.partsSubdir = partsSubdir;
		this.filename = filename;
		this.socket = socket;
		this.logger = logger;
		stop = false;
		failed = false;
		pendingTasks = new ArrayList<>();
		toDoTasks = new ArrayList<>();
		tasksDone = new ArrayList<>();
		noTask = true;
		tasksListsLock = false;
	}

	/** True if thread has failed to get a file.
	 * @return	true if thread has failed to get a file
	 */
	public boolean hasFailed() {
		return failed;
	}

	/** Asks to stop thread.
	 * @throws	InterruptedException
	 */
	public synchronized void setStop() throws InterruptedException {
		stop = true;
		this.notifyAll();
	}

	/** Runnable implementation */
	public void run() {
		while(!stop) {
			try {
				doTasks();
				synchronized(manager) {
					manager.notify();
				}
			} catch(InterruptedException e) {
				try {
					setStop();
					synchronized(manager) {
						manager.notify();
					}
				} catch (InterruptedException e2) {
				}
			}
		}
		System.err.println("Closing socket");
		logger.writeUDP("Closing socket", LogLevel.Info);
		socket.close();
	}

	/** Get list of offsets that have not be downloaded if failed, else
	 * empty list.
	 * @return list of offsets
	 */
	public List<Long> getFailed() {
		List<Long> ret = new ArrayList<>();
		if (failed) {
			ret.addAll(pendingTasks);
			ret.addAll(toDoTasks);
		}
		return ret;
	}

	/** Get list of downloaded file parts offset, then clear this list.
	 * @return	list of offsets
	 * @throws	InterruptedException
	 */
	public List<Long> getDone() throws InterruptedException {
		if (tasksDone.size() == 0) {
			return new ArrayList<>();
		} else {
			synchronized (this) {
				while(tasksListsLock) {
					this.wait();
				}
				tasksListsLock = true;
				List<Long> ret = new ArrayList<>(tasksDone);
				tasksDone.clear();
				tasksListsLock = false;
				this.notifyAll();
				return ret;
			}
		}
	}

	/** Adds offset of files parts to download.
	 * @param task	offset to download
	 * @throws	InterruptedException
	 */
	public synchronized void assignTask(Long task) throws InterruptedException {
		synchronized(this) {
			while(tasksListsLock) {
				this.wait();
			}
			tasksListsLock = true;
			toDoTasks.add(task);
			noTask = false;
			tasksListsLock = false;
			this.notifyAll();
		}
	}

	/** Send one request and wait for one response. Blocks when no task.
	 * @throws InterruptedException
	 */
	public synchronized void doTasks() throws InterruptedException {
		while(noTask && !stop) {
			this.wait();
		}
		if (!stop) {
			try {
				Long offset = toDoTasks.get(0);
				ProtocolP2PPacketUDP p = reqPart(offset);
				if (p == null) {
					stop = true;
				}

				failed = downloadPart(p);
				if (failed) {
					System.err.println("Error: DownloadPart failed.");
					stop = true;
				} else if (toDoTasks.isEmpty()) {
						noTask = true;
				}
			} catch (IndexOutOfBoundsException e) {
				logger.writeUDP(e, LogLevel.Error);
				noTask = true;
			}

		}
	}

	/** Send a request for a specific offset.
	 * @param offset	Offset of the file part to download
	 * @return	ProtocolP2PPacketTCP used to send request
	 */
	private ProtocolP2PPacketUDP reqPart(Long offset) {
		System.err.println("New request: "+ offset);
		logger.writeUDP("New request: "+ offset, LogLevel.Info);
		// maintain tracking of tasks
		if (toDoTasks.contains(offset)) {
			try {
				synchronized (this) {
					while(tasksListsLock) {
						this.wait();
					}
					tasksListsLock = true;
					toDoTasks.remove(offset);
					pendingTasks.add(offset);
					tasksListsLock = false;
					this.notifyAll();
				}
			} catch(InterruptedException e) {
				System.err.println("Error: reqPart interruptedException");
				logger.writeUDP("reqPart interruptedException", LogLevel.Error);
				return null;
			}
		} else {
			System.err.println("Error: reqPart (offset " + offset + " not in toDoTasks)");
			logger.writeUDP("reqPart (offset " + offset + " not in toDoTasks)", LogLevel.Error);
			return null;
		}
		// send request
		try {
			ProtocolP2PPacketUDP d = new ProtocolP2PPacketUDP((Payload) new LoadRequest(filename, offset.longValue(), MAX_PARTIAL_SIZE));
			d.sendRequest((Object)socket);
			return d;
		} catch (InternalError e) {
			System.err.println("Error: reqPart internalError");
			logger.writeUDP("reqPart internalError", LogLevel.Error);
			return null;
		} catch (IOException e) {
			e.printStackTrace();
			System.err.println("Error: reqPart ioexception");
			logger.writeUDP("reqPart ioexception", LogLevel.Error);
			return null;
		}
	}

	/** Download file part associated to the request send (d).
	 * @param d	request packet
	 * @return	true on failure, else false
	 */
	public boolean downloadPart(ProtocolP2PPacketUDP d) {
		if (d == null) {
			System.err.println("Error: downloadPart -> d is null.");
			logger.writeUDP("downloadPart -> d is null.", LogLevel.Error);
			return true;
		}
		try {
			Payload p = d.receiveResponse().getPayload();
			assert p instanceof FilePart : "This payload must be instance of FilePart";
			if (!(p instanceof FilePart)) {
				System.err.println("Error: cannot get size.");
				logger.writeUDP("cannot get size.", LogLevel.Error);
				return true;
			} else {
				FilePart fp = (FilePart)p;
				if (!fp.getFilename().equals(filename)) {
					System.err.println("Error: wrong file received: `" + fp.getFilename() + "`");
					logger.writeUDP("wrong file received: `" + fp.getFilename() + "`", LogLevel.Error);
					return true;
				}
				Long offset = Long.valueOf(fp.getOffset());
				if (pendingTasks.contains(offset)) {
					try {
						Files.write(new File(partsSubdir + filename + "_" + offset + ".part").toPath(), fp.getPartialContent());
					} catch (IOException e) {
						System.err.println("Error: cannot write file (" + partsSubdir + filename + "_" + offset + ".part)");
						logger.writeUDP("cannot write file (" + partsSubdir + filename + "_" + offset + ".part)", LogLevel.Error);
					}
				} else {
					System.err.println("Error: wrong file part received.");
					logger.writeUDP("wrong file part received.", LogLevel.Error);
					return true;
				}
				try {
					synchronized(this) {
						while(tasksListsLock) {
							this.wait();
						}
						tasksListsLock = true;
						pendingTasks.remove(offset);
						tasksDone.add(offset);
						tasksListsLock = false;
						this.notifyAll();
					}
				} catch(InterruptedException e) {
					System.err.println("Error: DownloadPart Interrupted exception");
					logger.writeUDP("DownloadPart Interrupted exception", LogLevel.Error);
					return true;
				}
			}
		} catch (EmptyDirectory e) {
			System.err.println("Error: empty directory.");
			logger.writeUDP("empty directory.", LogLevel.Error);
			return true;
		} catch (EmptyFile e) {
			System.err.println("Error: downloadPart emptyFile");
			logger.writeUDP("downloadPart emptyFile", LogLevel.Error);
			// TODO: use more specific errors
			return true;
		} catch (ProtocolError e) {
			System.err.println("Error: downloadPart protocolError");
			logger.writeUDP("downloadPart protocolError", LogLevel.Error);
			return true;
		} catch (InternalRemoteError e) {
			System.err.println("Error: downloadPart internalRemoteError");
			logger.writeUDP("downloadPart internalRemoteError", LogLevel.Error);
			return true;
		} catch (VersionRemoteError e) {
			System.err.println("Error: downloadPart versionRemoteError");
			logger.writeUDP("downloadPart versionRemoteError", LogLevel.Error);
			return true;
		} catch (ProtocolRemoteError e) {
			System.err.println("Error: downloadPart protocolRemoteError");
			logger.writeUDP("downloadPart protocolRemoteError", LogLevel.Error);
			return true;
		} catch (TransmissionError e) {
			System.err.println("Error: downloadPart transmissionError");
			logger.writeUDP("downloadPart transmissionError", LogLevel.Error);
			return true;
		} catch (VersionError e) {
			System.err.println("Error: downloadPart versionError");
			logger.writeUDP("downloadPart versionError", LogLevel.Error);
			return true;
		} catch (SizeError e) {
			System.err.println("Error: downloadPart sizeError");
			logger.writeUDP("downloadPart sizeError", LogLevel.Error);
			return true;
		} catch (NotFound e) {
			System.err.println("Error: downloadPart notFound");
			logger.writeUDP("downloadPart notFound", LogLevel.Error);
			return true;
		} catch (IOException e) {
			System.err.println("Error: downloadPart ioexception");
			logger.writeUDP("downloadPart ioexception", LogLevel.Error);
			return true;
		} catch (InternalError e) {
			System.err.println("Error: downloadPart internalError");
			logger.writeUDP("downloadPart internalError", LogLevel.Error);
			return true;
		}
		return false;
	}

}
