package clientP2P;
import java.util.List;
import java.util.ArrayList;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import protocolP2P.ProtocolP2PPacket;
import protocolP2P.Payload;
import protocolP2P.LoadRequest;
import protocolP2P.FilePart;
import protocolP2P.Denied;
import localException.InternalError;
import localException.ProtocolError;
import localException.TransmissionError;
import localException.VersionError;
import localException.SizeError;
import localException.SocketClosed;
import remoteException.EmptyDirectory;
import remoteException.EmptyFile;
import remoteException.InternalRemoteError;
import remoteException.VersionRemoteError;
import remoteException.ProtocolRemoteError;
import remoteException.NotFound;
import remoteException.NotATracker;
import exception.LocalException;
import exception.RemoteException;
import tools.Logger;
import tools.LogLevel;
import tools.ServeErrors;
import tools.HostItem;

/** Class to download file parts.
* @author	Louis Royer
* @author	Flavien Haas
* @author	JS Auge
* @version	1.0
*/
public abstract class ClientDownloadPart extends ServeErrors implements Runnable {
	protected long receivedBytesCount;
	protected List<Long> toDoTasks;
	protected List<Long> pendingTasks;
	protected List<Long> tasksDone;
	protected volatile boolean tasksListsLock;
	protected volatile boolean stop;
	protected volatile boolean failed;
	protected String filename;
	protected volatile boolean noTask;
	protected String partsSubdir;
	protected static final long MAX_PARTIAL_SIZE = 4096;
	protected ClientDownload manager;
	protected Logger logger;
	protected HostItem client;
	private HostItem server;

	/** 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
	 * @param logger	Logger
	 * @param client	HostItem of the application
	 * @param server	HostItem of the server application
	 */
	public ClientDownloadPart(ClientDownload manager, String filename, String partsSubdir, Logger logger, HostItem client, HostItem server) {
		this.manager = manager;
		this.partsSubdir = partsSubdir;
		this.filename = filename;
		this.logger = logger;
		this.client = client;
		this.server = server;
		stop = false;
		failed = false;
		pendingTasks = new ArrayList<>();
		toDoTasks = new ArrayList<>();
		tasksDone = new ArrayList<>();
		noTask = true;
		tasksListsLock = false;
		receivedBytesCount = 0;
	}


	/** receivedBytesCount getter
	 * @return	receivedBytesCount
	 */
	public Long getReceivedBytesCount() {
		return Long.valueOf(receivedBytesCount);
	}
	
	/** Server getter
	 * @return	server
	 */
	public HostItem getServer() {
		return server;
	}

	/** 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) {
				}
			}
		}
		writeLog("Closing socket", LogLevel.Info);
		try{
			closeSocket();
		} catch(IOException e){
			writeLog("can't close socket", LogLevel.Error);
		}
	}

	/** Close the socket
	 */
	protected abstract void closeSocket() throws IOException;

	/** 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);
				ProtocolP2PPacket<?> p = reqPart(offset);
				if (p == null) {
					stop = true;
				}
				failed = downloadPart(p);
				if (failed) {
					System.err.println("Error: DownloadPart failed.");
					writeLog("DownloadPart failed.", LogLevel.Error);
					stop = true;
				} else if (toDoTasks.isEmpty()) {
						noTask = true;
				}
			} catch (IndexOutOfBoundsException e) {
				writeLog(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
	 */
	protected ProtocolP2PPacket<?> reqPart(Long offset) {
		writeLog("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) {
				writeLog("reqPart interruptedException", LogLevel.Error);
				return null;
			}
		} else {
			writeLog("reqPart (offset " + offset + " not in toDoTasks)", LogLevel.Error);
			return null;
		}
		// send request
		try {
			ProtocolP2PPacket<?> d = createProtocolP2PPacket(new LoadRequest(filename, offset.longValue(), MAX_PARTIAL_SIZE, client));
			d.sendRequest(getSocket());
			return d;
		} catch (InternalError e) {
			writeLog("reqPart internalError", LogLevel.Error);
			return null;
		} catch (IOException e) {
			writeLog("reqPart ioexception", LogLevel.Error);
			writeLog(e, LogLevel.Error);
			return null;
		} catch (SocketClosed e){
			writeLog("reqPart SocketClosed", LogLevel.Error);
			return null;
		}
	}

	/** Get the socket */
	protected abstract Object getSocket();


	/** Download file part associated to the request send (d).
	 * @param d	request packet
	 * @return	true on failure, else false
	 */
	public < T extends ProtocolP2PPacket<?> > boolean downloadPart(T d) {
		if (d == null) {
			writeLog("downloadPart -> d is null.", LogLevel.Error);
			return true;
		}
		try {
			Payload p = d.receiveResponse().getPayload();
			if (p instanceof Denied) {
				Denied denied = (Denied)p;
				if (!denied.getFilename().equals(filename)) {
					writeLog("wrong file deny response received: `" + denied.getFilename() + "`", LogLevel.Error);
					return true;
				}
				Long offset = Long.valueOf(denied.getOffset());
				if (pendingTasks.contains(offset)) {
					pendingTasks.remove(offset);
					toDoTasks.add(offset);
					return false;
				} else {
					writeLog("wrong file offset deny received: " + offset, LogLevel.Error);
					return true;
				}
			}
			assert p instanceof FilePart : "This payload must be instance of FilePart";
			if (!(p instanceof FilePart)) {
				writeLog("cannot get size.", LogLevel.Error);
				return true;
			} else {
				FilePart fp = (FilePart)p;
				if (!fp.getFilename().equals(filename)) {
					writeLog("wrong file received: `" + fp.getFilename() + "`", LogLevel.Error);
					return true;
				}
				Long offset = Long.valueOf(fp.getOffset());
				if (pendingTasks.contains(offset)) {
					byte[] partialContent = fp.getPartialContent();
					try {
						Files.write(new File(partsSubdir + filename + "_" + offset + ".part").toPath(), partialContent);
						receivedBytesCount += partialContent.length;
					} catch (IOException e) {
						writeLog("cannot write file (" + partsSubdir + filename + "_" + offset + ".part)", LogLevel.Error);
					}
				} else {
					writeLog("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) {
					writeLog("DownloadPart Interrupted exception", LogLevel.Error);
					return true;
				}
			}
		} catch (LocalException e) {
			writeLog(e, LogLevel.Error);
			return true;
		} catch (RemoteException e) {
			writeLog(e, LogLevel.Error);
			return true;
		} catch (IOException e) {
			System.err.println("Error: downloadPart ioexception");
			writeLog("downloadPart ioexception", LogLevel.Error);
			return true;
		}
		return false;
	}


}
