package org.ourfilesystem.filehander;

/*
OurFileSystem is a peer2peer file sharing program.
Copyright (C) 2012  Robert Gass

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

*/

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import org.ourfilesystem.com.ConnectionUpdateInterface;
import org.ourfilesystem.core.CoreUserInterface;
import org.ourfilesystem.core.EventInterface;
import org.ourfilesystem.db.LocalFileReference;
import org.ourfilesystem.db.LocalPost;
import org.ourfilesystem.db.Peer;
import org.ourfilesystem.postcodec.Codec;
import org.ourfilesystem.postcodec.PostDecoded;
import org.ourfilesystem.utilities.FileUtils;
import org.ourfilesystem.utilities.BBytes;

import com.db4o.Db4oEmbedded;
import com.db4o.ObjectContainer;
import com.db4o.ObjectSet;
import com.db4o.config.EmbeddedConfiguration;
import com.db4o.query.Predicate;
import com.db4o.query.Query;

public class FileHandler implements FileHandlerInterface, EventInterface, Runnable {

	//TODO: Fix so that if we save a new file with the same SaveAs we catch it.
	//TODO: Fix so that we dispatch status for pending requests upon startup.
	
	/**
	 * Choose this wisely.  Once set never change it, or you'll end up with
	 * duplicate files as the digests of files split differently will be different.
	 * Current logic:
	 * 1) 2MB is reasonable size to download within a few minutes even over SOCKS5 proxy such as TOR
	 * 2) 2MB can hold 2097152 / (64(SHA512) + 4(int)) = 30840 digests.  If each digest represents a 2MB file
	 *    fragment then the total file size would be: 30840*2MB = 60GB.  This seems more than reasonable.
	 *    If someone is dealing with a file larger than 60GB then they can manage splitting it on their own.
	 */
	private static long SPLITSIZE = 2L * 1024L * 1024L;
	
	public static String LARGEFILEKEY = "<<!~>Large_$Split$_File<~!>>";
	public static String FILENAME = "FILENAME";
	public static String FILESIZE = "FILESIZE";
	
	private ObjectContainer DB;
	private FileReturnInterface FR;
	private CoreUserInterface Core;
	private int MaxDown = 20;
	private File TempDir;
	
	private int CurrentPending;
	
	public FileHandler(String file, String tmpdir, CoreUserInterface core, FileReturnInterface fi) {
		CurrentPending = 0;
		FR = fi;
		Core = core;
		EmbeddedConfiguration db4oconfig = Db4oEmbedded.newConfiguration();
		db4oconfig.common().activationDepth(4);
		db4oconfig.common().updateDepth(4);
		DB = Db4oEmbedded.openFile(db4oconfig, file);
		Runtime.getRuntime().addShutdownHook(new Thread() {
			public void run() {
				Close();
				DB.close();
			}
		});
		TempDir = new File(tmpdir);
		if (TempDir.exists()) {
			if (!TempDir.isDirectory()) {
				throw new RuntimeException(tmpdir + " exists, but is not a directory.  Please rename the file.");
			}
		}
		else {
			TempDir.mkdirs();
		}
	}
	
	public void Go() {
		ResetAll();
		updateAllStatus();
		Thread t = new Thread(this);
		t.start();
	}

	private synchronized void ResetAll() {
		Query q = DB.query();
		q.constrain(PendingMultiDownload.class);
		ObjectSet<PendingMultiDownload> os = q.execute();
		Iterator<PendingMultiDownload> i = os.iterator();
		while (i.hasNext()) {
			PendingMultiDownload p = i.next();
			p.ResetRequests();
			DB.store(p);
			DB.commit();
		}
		q = DB.query();
		q.constrain(PendingSingleDownload.class);
		ObjectSet<PendingSingleDownload> os2 = q.execute();
		Iterator<PendingSingleDownload> i2 = os2.iterator();
		while (i2.hasNext()) {
			PendingSingleDownload pd = i2.next();
			pd.ResetRequests();
			DB.store(pd);
			DB.commit();
		}
	}
	
	/**
	 * The higher the number the higher the priority.
	 */
	private synchronized void Process() {
		boolean hasprocessed = true;
		while (CurrentPending < MaxDown && hasprocessed) {
			hasprocessed = false;
			//First build a map that maps the priorities to a list of downloads at that priority.
			//For both single and multi-downloads.
			HashMap<Integer,List<PendingMultiDownload>> multimap = new HashMap<Integer,List<PendingMultiDownload>>();
			HashMap<Integer,List<PendingSingleDownload>> singmap = new HashMap<Integer,List<PendingSingleDownload>>();
			Query q = DB.query();
			q.constrain(PendingMultiDownload.class);
			ObjectSet<PendingMultiDownload> os = q.execute();
			Iterator<PendingMultiDownload> i = os.iterator();
			while (i.hasNext()) {
				PendingMultiDownload p = i.next();
				if (!p.isPaused()) {
					List<PendingMultiDownload> l = multimap.get(p.getPriority());
					if (l == null) {
						l = new LinkedList<PendingMultiDownload>();
						multimap.put(p.getPriority(), l);
					}
					l.add(p);
				}
			}
			q = DB.query();
			q.constrain(PendingSingleDownload.class);
			ObjectSet<PendingSingleDownload> os2 = q.execute();
			Iterator<PendingSingleDownload> i2 = os2.iterator();
			while (i2.hasNext()) {
				PendingSingleDownload pd = i2.next();
				if (!pd.isPaused()) {
					List<PendingSingleDownload> l = singmap.get(pd.getPriority());
					if (l == null) {
						l = new LinkedList<PendingSingleDownload>();
						singmap.put(pd.getPriority(), l);
					}
					l.add(pd);
				}
			}
			//Merge the priorities from both single and multi into one list.  Then
			//sort the array of those priorities.  Then loop through them in reverse
			//submitting first the single downloads at that priority, then the multi
			//downloads at that priority.
			HashSet<Integer> prioritylist = new HashSet<Integer>();
			prioritylist.addAll(singmap.keySet());
			prioritylist.addAll(multimap.keySet());
			if (prioritylist.size() > 0) {
				Object[] priorities = prioritylist.toArray();
				Arrays.sort(priorities);
				for (int p = priorities.length-1; p >= 0 && CurrentPending < MaxDown; p--) {
					List<PendingSingleDownload> l = singmap.get(priorities[p]);
					if (l != null) {
						Iterator<PendingSingleDownload> is = l.iterator();
						while (is.hasNext() && CurrentPending < MaxDown) {
							PendingSingleDownload ps = is.next();
							if (!ps.isPaused()) {
								BBytes b = ps.getRequest();
								if (b != null) {
									Core.downloadFile(b);
									DB.store(ps);
									DB.commit();
									CurrentPending++;
									hasprocessed = true;
								}
							}
						}	
					}
					List<PendingMultiDownload> l2 = multimap.get(priorities[p]);
					if (l2 != null) {
						Iterator<PendingMultiDownload> im = l2.iterator();
						while (im.hasNext() && CurrentPending < MaxDown) {
							PendingMultiDownload pm = im.next();
							if (!pm.isPaused()) {
								BBytes b = pm.nextPeiceToDownload();
								if (b != null) {
									Core.downloadFile(b);
									DB.store(pm);
									DB.commit();
									CurrentPending++;
									hasprocessed = true;
								}
							}
						}
					}
				}
			}
		}
	}

	
	@Override
	public void setFileReturnInterface(FileReturnInterface ri) {
		FR = ri;
	}

	@Override
	public void setCoreUserInterface(CoreUserInterface ui) {
		Core = ui;
	}

	@Override
	public void setMaxPendingPeicesDown(int number) {
		MaxDown = number;
	}

	@Override
	public int getMaxPendingPeicesDown() {
		return MaxDown;
	}
	
	@SuppressWarnings({ "serial" })
	private PendingMultiDownload getMultiDownload(final File saveas) {
		ObjectSet<PendingMultiDownload> o = DB.query(new Predicate<PendingMultiDownload>() {
			@Override
			public boolean match(PendingMultiDownload a) {
				if (a.getSaveAs().equals(saveas)) {
					return true;
				}
				return false;
			}
		});
		Iterator<PendingMultiDownload> i = o.iterator();
		while (i.hasNext()) {
			PendingMultiDownload pmd = i.next();
			return pmd;
		}
		return null;
	}
	
	@SuppressWarnings({ "serial" })
	private PendingSingleDownload getSingleDownload(final File saveas) {
		ObjectSet<PendingSingleDownload> o = DB.query(new Predicate<PendingSingleDownload>() {
			@Override
			public boolean match(PendingSingleDownload a) {
				if (a.getSaveAs().equals(saveas)) {
					return true;
				}
				return false;
			}
		});
		Iterator<PendingSingleDownload> i = o.iterator();
		while (i.hasNext()) {
			PendingSingleDownload pmd = i.next();
			return pmd;
		}
		return null;
	}
	
	@Override
	public synchronized void removeFile(final File saveas) {
		PendingMultiDownload md = getMultiDownload(saveas);
		if (md != null) {
			DB.delete(md);
		}
		PendingSingleDownload sd = getSingleDownload(saveas);
		if (sd != null) {
			DB.delete(sd);
		}
	}
	
	@Override
	public synchronized void pauseFile(final File saveas) {
		PendingMultiDownload md = getMultiDownload(saveas);
		if (md != null) {
			md.setPaused(true);
			DB.store(md);
			DB.commit();
			FR.downloadStatusUpdate(saveas, 
					md.getDig(),
					md.getCompleteSize(), 
					md.getDownloadedSize(), 
					md.getNumberRequested(),
					md.isPaused(), 
					md.isInProgress(), 
					md.getPriority());
		}
		PendingSingleDownload sd = getSingleDownload(saveas);
		if (sd != null) {
			sd.setPaused(true);
			DB.store(sd);
			DB.commit();
			FR.downloadStatusUpdate(saveas, 
					sd.getDig(),
					sd.getTotalSize(), 
					sd.getAmountComplete(), 
					sd.getNumberRequested(),
					sd.isPaused(), 
					sd.isInProgress(), 
					sd.getPriority());
		}
	}

	@Override
	public synchronized void resumeFile(final File saveas) {
		PendingMultiDownload md = getMultiDownload(saveas);
		if (md != null) {
			md.setPaused(false);
			DB.store(md);
			DB.commit();
			FR.downloadStatusUpdate(saveas, 
					md.getDig(),
					md.getCompleteSize(), 
					md.getDownloadedSize(), 
					md.getNumberRequested(),
					md.isPaused(), 
					md.isInProgress(), 
					md.getPriority());
		}
		PendingSingleDownload sd = getSingleDownload(saveas);
		if (sd != null) {
			sd.setPaused(false);
			DB.store(sd);
			DB.commit();
			FR.downloadStatusUpdate(saveas, 
					sd.getDig(),
					sd.getTotalSize(), 
					sd.getAmountComplete(), 
					sd.getNumberRequested(),
					sd.isPaused(), 
					sd.isInProgress(), 
					sd.getPriority());
		}
		notifyAll();
		//Process();
		//updateAllStatus();
	}

	@Override
	public synchronized void setPriority(final File saveas, int priority) {
		PendingMultiDownload md = getMultiDownload(saveas);
		if (md != null) {
			md.setPriority(priority);
			DB.store(md);
			DB.commit();
			FR.downloadStatusUpdate(saveas, 
					md.getDig(),
					md.getCompleteSize(), 
					md.getDownloadedSize(), 
					md.getNumberRequested(),
					md.isPaused(), 
					md.isInProgress(), 
					md.getPriority());
		}
		PendingSingleDownload sd = getSingleDownload(saveas);
		if (sd != null) {
			sd.setPriority(priority);
			DB.store(sd);
			DB.commit();
			FR.downloadStatusUpdate(saveas, 
					sd.getDig(),
					sd.getTotalSize(), 
					sd.getAmountComplete(), 
					sd.getNumberRequested(),
					sd.isPaused(), 
					sd.isInProgress(), 
					sd.getPriority());
		}
	}
	
	public int getCurrentPending() {
		return CurrentPending;
	}

	private void insertLargeFile(File file, PostDecoded dec) {
		try {
			List<File> sl = FileSplitter.split(file, TempDir, SPLITSIZE);
			List<LocalFileReference> refs = new LinkedList<LocalFileReference>();
			Iterator<File> i = sl.iterator();
			while (i.hasNext()) {
				File f = i.next();
				LocalFileReference ref = Core.addLocalFile(f, null);
				refs.add(ref);
			}
			File primary = File.createTempFile("primary", ".dat", TempDir);
			FileOutputStream fos = new FileOutputStream(primary);
			fos.write(PendingMultiDownload.CURRENT_FORMAT_VERSION);
			FileUtils.writeLong(file.length(), fos);
			FileUtils.writeInt(refs.size(), fos);
			Iterator<LocalFileReference> i2 = refs.iterator();
			while (i2.hasNext()) {
				LocalFileReference lf = i2.next();
				FileUtils.writeBytes(fos, ((BBytes)lf.getFileReference().getUnsignedDigest()).getBytes());
			}
			fos.close();
			//-- add large file identifier to the post.
			Set<String> ks = dec.getStringKeySet(FILENAME);
			if (ks != null) ks.clear();
			dec.pushStringValue(FILENAME, file.getName());
			dec.getNumberValues().put(FILESIZE, file.length());
			dec.getNumberValues().put(LARGEFILEKEY, (long)refs.size());
			dec.getPost().getPost().setMessage(File.createTempFile("message", ".dat", TempDir));
			//-- here he encode the post into the temp message file we just created.
			Codec.ecnode(dec);
			//-- insert the primary file with the temp message file we just encoded.
			Core.addLocalFile(primary, dec.getPost().getPost().getMessage());
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	private void insertSmallFile(File file, PostDecoded dec) {
		try {
			File mf = File.createTempFile("message", ".dat", TempDir);
			Set<String> ks = dec.getStringKeySet(FILENAME);
			if (ks != null) ks.clear();
			Set<String> sks = dec.getStringKeySet(FILENAME);
			if (sks != null) sks.clear();
			dec.pushStringValue(FILENAME, file.getName());
			dec.getNumberValues().put(FILESIZE, file.length());
			dec.getNumberValues().remove(LARGEFILEKEY);
			dec.getPost().getPost().setMessage(mf);
			Codec.ecnode(dec);
			//- Copy the file because the database tries to move the file.
			//- obveously we don't want to move a file someone has referenced or
			//- They'll think it's gone.  Also this fails if the user has the
			//- file open already.
			File tmpfile = File.createTempFile("reffile", ".dat", TempDir);
			FileUtils.copyFile(file, tmpfile, false);
			Core.addLocalFile(tmpfile, dec.getPost().getPost().getMessage());
		}
		catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	@Override
	public synchronized void insertFile(File file, PostDecoded dec) {
		removeFile(file);
		if (file.length() > SPLITSIZE) {
			insertLargeFile(file, dec);
		}
		else {
			insertSmallFile(file, dec);
		}
	}

	@Override
	public synchronized void requestFile(File saveas, PostDecoded dec, boolean paused, int priority) {
		Long parts = dec.getNumberValues().get(LARGEFILEKEY);
		Long lfs = dec.getNumberValues().get(FILESIZE);
		long filesize = 0L;
		if (lfs != null) {
			filesize = lfs;
		}
		BBytes bb = (BBytes)dec.getPost().getPost().getFileReferenceDigest();
		if (bb != null) {
			removeFile(saveas);
			/*
			 * Versions <= 0.3 would not properly add the filesize when
			 * replying to another post with a file.
			 */
			if (parts != null && (filesize > SPLITSIZE || filesize == 0)) {
				PendingMultiDownload pd = new PendingMultiDownload();
				try {
					pd.Init(saveas, bb, priority, paused);
					DB.store(pd);
					DB.commit();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
			else {
				PendingSingleDownload pd = new PendingSingleDownload();
				pd.Init(saveas, bb, priority, paused);
				DB.store(pd);
				DB.commit();
			}
			notifyAll();
			//Process();
		}
		//updateAllStatus();
	}

	@Override
	public synchronized void updateAllStatus() {
		Query q = DB.query();
		q.constrain(PendingMultiDownload.class);
		ObjectSet<PendingMultiDownload> r = q.execute();
		Iterator<PendingMultiDownload> i = r.iterator();
		while (i.hasNext()) {
			PendingMultiDownload p = i.next();
			FR.downloadStatusUpdate(p.getSaveAs(), 
									p.getDig(),
									p.getCompleteSize(), 
									p.getDownloadedSize(), 
									p.getNumberRequested(),
									p.isPaused(), 
									p.isInProgress(), 
									p.getPriority());
		}
		Query q2 = DB.query();
		q2.constrain(PendingSingleDownload.class);
		ObjectSet<PendingSingleDownload> r2 = q2.execute();
		Iterator<PendingSingleDownload> i2 = r2.iterator();
		while (i2.hasNext()) {
			PendingSingleDownload p = i2.next();
			FR.downloadStatusUpdate(p.getSaveAs(), 
								    p.getDig(),
									p.getTotalSize(),
									p.getAmountComplete(),
									p.getNumberRequested(),
									p.isPaused(), 
									p.isInProgress(), 
									p.getPriority());
		}
	}

	//==========================================================================================
	/**
	 * Process data returned from the node.
	 */
	
	@Override
	public void newPostReceived(LocalPost postlist) {
		//I don't care about posts.
	}

	@Override
	public void newPeerReceived(Peer peerlist) {
		//I don't care about peers.
	}

	@Override
	public synchronized void newFileDownloaded(LocalFileReference filelist) {
		if (filelist != null) {
			if (filelist.getFileReference() != null) {
				if (filelist.getFileReference().getFile() != null) {
					if (filelist.getFileReference().getFile().exists()) {
						Query q = DB.query();
						q.constrain(PendingMultiDownload.class);
						ObjectSet<PendingMultiDownload> os = q.execute();
						Iterator<PendingMultiDownload> i = os.iterator();
						while (i.hasNext()) {
							PendingMultiDownload p = i.next();
							if (p.ProcessPiece(filelist)) {
								CurrentPending--;
								FR.downloadStatusUpdate(p.getSaveAs(), 
														p.getDig(),
														p.getCompleteSize(), 
														p.getDownloadedSize(), 
														p.getNumberRequested(),
														p.isPaused(), 
														p.isInProgress(), 
														p.getPriority());
								DB.store(p);
								DB.commit();
							}
						}
						q = DB.query();
						q.constrain(PendingSingleDownload.class);
						ObjectSet<PendingSingleDownload> os2 = q.execute();
						Iterator<PendingSingleDownload> i2 = os2.iterator();
						while (i2.hasNext()) {
							PendingSingleDownload pd = i2.next();
							if (pd.ProcessPiece(filelist)) {
								CurrentPending--;
								FR.downloadStatusUpdate(pd.getSaveAs(), 
													pd.getDig(),
													pd.getTotalSize(), 
													pd.getAmountComplete(), 
													pd.getNumberRequested(),
													pd.isPaused(), 
													pd.isInProgress(), 
													pd.getPriority());
								DB.store(pd);
								DB.commit();
							}
						}
						notifyAll();
						//Process();
					}
				}
			}
		}
	}

	@Override
	public synchronized void downloadFailed(Object dig) {
		System.out.println("Download failed: " + dig + " :: " + (new Date()).getTime());
		//Find out who needed this file and set it no longer as pending.
		Query q = DB.query();
		q.constrain(PendingMultiDownload.class);
		ObjectSet<PendingMultiDownload> os = q.execute();
		Iterator<PendingMultiDownload> i = os.iterator();
		while (i.hasNext()) {
			PendingMultiDownload p = i.next();
			p.clearPending((BBytes)dig);
			DB.store(p);
			DB.commit();
		}
		q = DB.query();
		q.constrain(PendingSingleDownload.class);
		ObjectSet<PendingSingleDownload> os2 = q.execute();
		Iterator<PendingSingleDownload> i2 = os2.iterator();
		while (i2.hasNext()) {
			PendingSingleDownload pd = i2.next();
			if (pd.getDigest().equals(dig)) {
				pd.ResetRequests();
				DB.store(pd);
				DB.commit();
			}
		}
		//updateAllStatus();	
		notifyAll();
	}

	@Override
	public void connectionFailure(Peer p) {
		//I don't care about connections.
	}

	@Override
	public void connectionEvent(ConnectionUpdateInterface con) {
		//I don't care.
		
	}

	private boolean Run;
	
	public synchronized void Close() {
		Run = false;
		notifyAll();
	}
	
	private synchronized void doWait() {
		try {
			wait(60L * 1000L);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
	
	@Override
	public void run() {
		Run = true;
		while (Run) {
			Process();
			updateAllStatus();
			doWait();
		}
	}

}
