package org.ourfilesystem.db;

/*
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.IOException;
import java.nio.ByteBuffer;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import org.ourfilesystem.security.KeySet;
import org.ourfilesystem.utilities.BBytes;
import org.ourfilesystem.utilities.FileUtils;

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 StorageImpl2 implements StorageInterface {

	public static long MAXPOSTSIZE = 10L * 1024L;
	public static long MAXFILESIZE = 3L * 1024L * 1024L;
	
	private File BaseDir;
	private ObjectContainer DB;
	
	public StorageImpl2(String basedir) {
		BaseDir = new File(basedir);
		if (!BaseDir.exists()) {
			BaseDir.mkdirs();
		}
		else if (!BaseDir.isDirectory()) {
			throw new RuntimeException("Expecting a directory: " + basedir);
		}
		String file = basedir + File.separator + "mainstore.dat";
		EmbeddedConfiguration db4oconfig = Db4oEmbedded.newConfiguration();
		db4oconfig.common().activationDepth(10);
		db4oconfig.common().updateDepth(10);

		db4oconfig.common().objectClass(MyPeerData.class).cascadeOnDelete(true);
		db4oconfig.common().objectClass(MyPeerData.class).cascadeOnActivate(true);
		db4oconfig.common().objectClass(MyPeerData.class).cascadeOnUpdate(true);
		
		//db4oconfig.common().automaticShutDown(true);

		DB = Db4oEmbedded.openFile(db4oconfig, file);
		Runtime.getRuntime().addShutdownHook(new Thread() {
			public void run() {
				DB.close();
			}
		});
	}
	
	public class MyPeerData {
		public Peer Peer;
	}
	
	public class LastPostNumber {
		public Object Digest;
		public long PostNumber;
	}
	
	public class BadPeer {
		public Object Digest;
	}
	
	@Override
	public synchronized void saveMyPeerData(Peer peer) {
		Query q = DB.query();
		q.constrain(MyPeerData.class);
		ObjectSet<MyPeerData> os = q.execute();
		if (os.size() > 1) {
			System.out.println("ERROR: There is more than one copy for some reason.");
		}
		Iterator<MyPeerData> i = os.iterator();
		while (i.hasNext()) {
			MyPeerData mydat = i.next();
			DB.delete(mydat);
		}
		DB.commit();
		MyPeerData newpeer = new MyPeerData();
		newpeer.Peer = peer;
		DB.store(newpeer);
		DB.commit();
	}

	@Override
	public synchronized Peer getMyPeerData() {
		Query q = DB.query();
		q.constrain(MyPeerData.class);
		ObjectSet<MyPeerData> os = q.execute();
		if (os.size() > 1) {
			System.out.println("ERROR: There is more than one copy for some reason.");
		}
		Iterator<MyPeerData> i = os.iterator();
		if (i.hasNext()) {
			return i.next().Peer;
		}
		return null;
	}

	@Override
	public synchronized void saveMyKeySet(KeySet keyset) {
		Query q = DB.query();
		q.constrain(KeySet.class);
		ObjectSet<KeySet> os = q.execute();
		if (os.size() > 1) {
			System.out.println("ERROR: There is more than one copy for some reason.");
		}
		Iterator<KeySet> i = os.iterator();
		while (i.hasNext()) {
			KeySet mydat = i.next();
			DB.delete(mydat);
		}
		DB.store(keyset);
		DB.commit();
	}

	@Override
	public synchronized KeySet getMyKeySet() {
		Query q = DB.query();
		q.constrain(KeySet.class);
		ObjectSet<KeySet> os = q.execute();
		if (os.size() > 1) {
			System.out.println("ERROR: There is more than one copy for some reason.");
		}
		Iterator<KeySet> i = os.iterator();
		if (i.hasNext()) {
			return i.next();
		}
		return null;
	}

	@SuppressWarnings("serial")
	@Override
	public synchronized void savePeer(final Peer peer) {
		if (peer == null) {
			System.out.println("ERROR: Peer is null");
			return;
		}
		if (peer.getPeerKeysAndIdentity() == null) {
			System.out.println("ERROR: Identity does not exist.");
			return;
		}
		if (peer.getPeerKeysAndIdentity().getSignature() == null) {
			System.out.println("ERROR: Signature does not exist.");
			return;
		}
		if (peer.getPeerKeysAndIdentity().getSignature().getDigest() == null) {
			System.out.println("ERROR: Digest does not exist.");
			return;
		}
		ObjectSet<Peer> os = DB.query(new Predicate<Peer>() {
			@Override
			public boolean match(Peer ep) {
				return ep.getPeerKeysAndIdentity().getSignature().getDigest().equals(
				     peer.getPeerKeysAndIdentity().getSignature().getDigest());
			}
		});
		if (os.size() > 1) {
			System.out.println("ERROR: There is more than one copy of this peer saved for some reason.");
		}
		Iterator<Peer> i = os.iterator();
		while (i.hasNext()) {
			Peer np = i.next();
			DB.delete(np);
		}
		DB.store(peer);
		DB.commit();
	}

	@SuppressWarnings("serial")
	@Override
	public synchronized Peer getPeer(final Object peerid) {
		ObjectSet<BadPeer> osa = DB.query(new Predicate<BadPeer>() {
			@Override
			public boolean match(BadPeer ep) {
				return ep.Digest.equals(peerid);
			}
		});
		if (osa.size() > 0) return null;
		ObjectSet<Peer> os = DB.query(new Predicate<Peer>() {
			@Override
			public boolean match(Peer ep) {
				return ep.getPeerKeysAndIdentity().getSignature().getDigest().equals(peerid);
			}
		});
		if (os.size() > 1) {
			System.out.println("ERROR: There is more than one copy for some reason.");
		}
		Iterator<Peer> i = os.iterator();
		if (i.hasNext()) {
			return i.next();
		}
		return null;
	}

	@Override
	public synchronized List<Peer> getPeerList() {
		Query q = DB.query();
		q.constrain(Peer.class);
		ObjectSet<Peer> os = q.execute();
		q = DB.query();
		q.constrain(BadPeer.class);
		ObjectSet<BadPeer> osa = q.execute();
		LinkedList<Peer> r = new LinkedList<Peer>();
		Iterator<Peer> i = os.iterator();
		while (i.hasNext()) {
			Peer p = i.next();
			boolean found = false;
			Iterator<BadPeer> i2 = osa.iterator();
			while (i2.hasNext() && !found) {
				BadPeer bp = i2.next();
				if (p.getPeerKeysAndIdentity().getSignature().getDigest().equals(bp.Digest)) {
					found = true;
				}
			}
			if (!found) {
				r.add(p);
			}
		}
		return r;
	}

	@SuppressWarnings("serial")
	@Override
	public synchronized void savePost(final LocalPost post) {
		if (post == null) {
			System.out.println("ERROR: post is null.");
			return;
		}
		if (post.getPost() == null) {
			System.out.println("ERROR: post is null.");
			return;
		}
		if (post.getPost().getSignedDigest() == null) {
			System.out.println("ERROR: Signed digest is null.");
			return;
		}
		if (post.getPost().getSignedDigest().getPeerIdentifier() == null) {
			System.out.println("ERROR: Digest is null.");
			return;
		}
		ObjectSet<LocalPost> os = DB.query(new Predicate<LocalPost>() {
			@Override
			public boolean match(LocalPost ep) {
				return ep.equals(post);
			}
		});
		if (os.size() == 0) {
			boolean savemsg = true;
			File fm = (File)post.getPost().getMessage();
			if (fm != null) {
				if (fm.exists()) {
					if (fm.length() <= MAXPOSTSIZE) {
						File ndir = this.dirFromDig((BBytes)post.getPost().getSignedDigest().getDigest(),
								BaseDir.getPath() + File.separator + "posts");
						File nm = new File(ndir.getPath() + File.separator + "msg.dat");
						if (!fm.renameTo(nm)) {
							savemsg = false;
						}
						post.getPost().setMessage(nm);
					}
					else {
						savemsg = false;
					}
					if (!savemsg) {
						fm.delete();
					}
				}
				else {
					savemsg = false;
				}
			}
			
			
			if (savemsg) {
				//TODO: remove missing posts that have been received.
				LastPostNumber lpn = getLast(post.getPost().getSignedDigest().getPeerIdentifier());
				if (lpn == null) {
					lpn = new LastPostNumber();
					lpn.Digest = post.getPost().getSignedDigest().getPeerIdentifier();
				}
				if (lpn.PostNumber+1 < post.getPost().getPostNumber()) {
					PostHoles ph = new PostHoles(lpn.PostNumber+1, post.getPost().getPostNumber()-1);
					ph.setPeerId(post.getPost().getSignedDigest().getPeerIdentifier());
					DB.store(ph);
					DB.commit();
				}
				if (post.getPost().getPostNumber() > lpn.PostNumber) {
					lpn.PostNumber = post.getPost().getPostNumber();
					DB.store(lpn);
					DB.commit();
				}
				DB.store(post);
				DB.commit();
				List<PostHoles> phl = getPeerPostHoles(post.getPost().getSignedDigest().getPeerIdentifier());
				Iterator<PostHoles> phi = phl.iterator();
				while (phi.hasNext()) {
					PostHoles ph = phi.next();
					ph.newPostReceived(post.getPost().getPostNumber());
					if (ph.isDone()) {
						DB.delete(ph);
					}
					else {
						DB.store(ph);
					}
					DB.commit();
				}
			}
		}
	}

	@SuppressWarnings("serial")
	@Override
	public synchronized List<LocalPost> getPeerPosts(final Peer p, final int page, final int pagesize) {
		if (p == null) return null;
		if (p.getPeerKeysAndIdentity() == null) return null;
		if (p.getPeerKeysAndIdentity().getSignature() == null) return null;
		if (p.getPeerKeysAndIdentity().getSignature().getDigest() == null) return null;
		ObjectSet<LocalPost> os = DB.query(new Predicate<LocalPost>() {
			@Override
			public boolean match(LocalPost ep) {
				return ep.getPost().getSignedDigest().getPeerIdentifier().equals(
						p.getPeerKeysAndIdentity().getSignature().getDigest());
			}
		});
		LinkedList<LocalPost> r = new LinkedList<LocalPost>();
		for (int idx = (page*pagesize); idx < ((page+1)*pagesize) && idx < os.size(); idx++) {
			r.add(os.get(idx));
		}
		return r;
	}

	@SuppressWarnings("serial")
	@Override
	public synchronized List<LocalPost> getPeerPosts(final Peer p, final long start, final long end) {
		if (p == null) return null;
		if (p.getPeerKeysAndIdentity() == null) return null;
		if (p.getPeerKeysAndIdentity().getSignature() == null) return null;
		if (p.getPeerKeysAndIdentity().getSignature().getDigest() == null) return null;
		ObjectSet<LocalPost> os = DB.query(new Predicate<LocalPost>() {
			@Override
			public boolean match(LocalPost ep) {
				if (ep.getPost().getSignedDigest().getPeerIdentifier().equals(
						p.getPeerKeysAndIdentity().getSignature().getDigest())) {
					if (start <= ep.getPost().getPostNumber() &&
							ep.getPost().getPostNumber() <= end) {
						return true;
					}
				}
				return false;
			}
		});
		LinkedList<LocalPost> r = new LinkedList<LocalPost>();
		r.addAll(os);
		return r;
	}

	@SuppressWarnings("serial")
	@Override
	public synchronized List<LocalPost> getFilePosts(final Object dig, int page, int pagesize) {
		if (dig == null) return null;
		ObjectSet<LocalPost> os = DB.query(new Predicate<LocalPost>() {
			@Override
			public boolean match(LocalPost ep) {
				Object ed = ep.getPost().getFileReferenceDigest();
				if (ed != null) {
					return ed.equals(dig);
				}
				return false;
			}
		});
		LinkedList<LocalPost> r = new LinkedList<LocalPost>();
		for (int idx = (page*pagesize); idx < os.size() && idx < ((page+1)*pagesize); idx++) {
			r.add(os.get(idx));
		}
		return r;
	}

	@SuppressWarnings("serial")
	@Override
	public synchronized List<LocalPost> getPosts(final Date fromdate) {
		if (fromdate == null) return null;
		ObjectSet<LocalPost> os = DB.query(new Predicate<LocalPost>() {
			@Override
			public boolean match(LocalPost ep) {
				Date td = ep.getLocalDate();
				if (td != null) {
					return (fromdate.compareTo(td) <= 0);
				}
				return false;
			}
		});
		LinkedList<LocalPost> r = new LinkedList<LocalPost>();
		r.addAll(os);
		return r;
	}
	
	private File dirFromDig(BBytes b, String basedir) {
		byte bt[] = b.getBytes();
		ByteBuffer buf = ByteBuffer.wrap(bt);
		int d0n = (buf.getInt() & 0x3FFF);
		int d1n = 0;
		if (buf.remaining() >= (Integer.SIZE/Byte.SIZE)) { 
			d1n = (buf.getInt() & 0x3FFF);
		}
		int d2n = 0;
		if (buf.remaining() >= (Integer.SIZE/Byte.SIZE)) {
			d2n = (buf.getInt() & 0x3FFF);
		}
		int val = 0;
		File d = new File(basedir + File.separator +
							d0n + File.separator +
							d1n + File.separator + 
							d2n + File.separator + val);
		while (d.exists()) {
			val ++;
			d = new File(basedir + File.separator +
					d0n + File.separator +
					d1n + File.separator + 
					d2n + File.separator + val);
		}
		d.mkdirs();
		return d;
	}

	@SuppressWarnings("serial")
	@Override
	public synchronized void saveFile(final LocalFileReference ref) {
		if (ref == null) {
			System.out.println("ERROR: Ref is null.");
			return;
		}
		if (ref.getFileReference() == null) {
			System.out.println("ERROR: file ref is null.");
			return;
		}
		if (ref.getFileReference().getUnsignedDigest() == null) {
			System.out.println("ERROR: file digest is null.");
			return;
		}
		if (ref.getFileReference().getFile() == null) {
			System.out.println("ERROR: There is no file.");
			return;
		}
		if (!ref.getFileReference().getFile().exists()) {
			System.out.println("ERROR: The file does not exist.");
			return;
		}
		ObjectSet<LocalFileReference> os = DB.query(new Predicate<LocalFileReference>() {
			@Override
			public boolean match(LocalFileReference ep) {
				return ep.getFileReference().getUnsignedDigest().equals(
					  ref.getFileReference().getUnsignedDigest());
			}
		});
		if (os.size() == 0) {
			File f = ref.getFileReference().getFile();
			if (f != null && f.exists() && f.length() <= MAXFILESIZE) {
				File dir = dirFromDig((BBytes)ref.getFileReference().getUnsignedDigest(),
						BaseDir.getPath() + File.separator + "files");
				File sf = new File(dir.getPath() + File.separator + "file.dat");
				try {
					FileUtils.copyFile(f, sf, true);
					ref.getFileReference().setFile(sf);
					DB.store(ref);
					DB.commit();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
			else {
				if (f != null && f.exists()) {
					f.delete();
				}
			}
		}
	}

	@SuppressWarnings("serial")
	@Override
	public synchronized LocalFileReference getFileReference(final Object dig) {
		if (dig == null) return null;
		ObjectSet<LocalFileReference> os = DB.query(new Predicate<LocalFileReference>() {
			@Override
			public boolean match(LocalFileReference ep) {
				return ep.getFileReference().getUnsignedDigest().equals(dig);
			}
		});
		Iterator<LocalFileReference> i = os.iterator();
		if (i.hasNext()) {
			LocalFileReference lfr = i.next();
			return lfr;
		}
		return null;
	}

	@Override
	public synchronized List<LocalFileReference> getFileReferences(int page, int pagesize) {
		Query q = DB.query();
		q.constrain(LocalFileReference.class);
		ObjectSet<LocalFileReference> os = q.execute();
		LinkedList<LocalFileReference> r = new LinkedList<LocalFileReference>();
		for (int idx = (page*pagesize); idx < ((page+1)*pagesize) && idx < os.size(); idx++) {
			r.add(os.get(idx));
		}
		return r;
	}

	@SuppressWarnings("serial")
	@Override
	public synchronized List<LocalFileReference> getFileReferences(final Date fromdate) {
		ObjectSet<LocalFileReference> os = DB.query(new Predicate<LocalFileReference>() {
			@Override
			public boolean match(LocalFileReference ep) {
				return (ep.getLocalDate().compareTo(fromdate) >= 0);
			}
		});
		LinkedList<LocalFileReference> r = new LinkedList<LocalFileReference>();
		r.addAll(os);
		return r;
	}

	@Override
	public synchronized void close() {
		DB.close();
	}
	
	@SuppressWarnings("serial")
	private LastPostNumber getLast(final Object dig) {
		ObjectSet<LastPostNumber> os = DB.query(new Predicate<LastPostNumber>() {
			@Override
			public boolean match(LastPostNumber lp) {
				return lp.Digest.equals(dig);
			}
		});
		Iterator<LastPostNumber> i = os.iterator();
		if (i.hasNext()) {
			return i.next();
		}
		return null;
	}

	@Override
	public synchronized Long getLastPostNumber(Peer p) {
		LastPostNumber lpn = getLast(p.getPeerKeysAndIdentity().getSignature().getDigest());
		if (lpn != null) {
			return lpn.PostNumber;
		}
		return null;
	}

	@Override
	public synchronized List<PostHoles> getPostHoles(final Peer p) {
		if (p == null) return null;
		if (p.getPeerKeysAndIdentity() == null) return null;
		if (p.getPeerKeysAndIdentity().getSignature() == null) return null;
		if (p.getPeerKeysAndIdentity().getSignature().getDigest() == null) return null;
		return getPeerPostHoles(p.getPeerKeysAndIdentity().getSignature().getDigest());
	}

	@SuppressWarnings("serial")
	private synchronized List<PostHoles> getPeerPostHoles(final Object dig) {
		LinkedList<PostHoles> r = new LinkedList<PostHoles>();
		if (dig == null) return r;
		ObjectSet<PostHoles> os = DB.query(new Predicate<PostHoles>() {
			@Override
			public boolean match(PostHoles lp) {
				return dig.equals(lp.getPeerId());
			}
		});
		r.addAll(os);
		return r;
	}

	@SuppressWarnings("serial")
	@Override
	public synchronized void saveBadPeer(final Peer p) {
		if (p == null) return;
		if (p.getPeerKeysAndIdentity() == null) return;
		if (p.getPeerKeysAndIdentity().getSignature().getDigest() == null) return;
		ObjectSet<BadPeer> osa = DB.query(new Predicate<BadPeer>() {
			@Override
			public boolean match(BadPeer ep) {
				return ep.Digest.equals(p.getPeerKeysAndIdentity().getSignature().getDigest());
			}
		});
		if (osa.size() == 0) {
			BadPeer bp = new BadPeer();
			bp.Digest = p.getPeerKeysAndIdentity().getSignature().getDigest();
			DB.store(bp);
			DB.commit();
			savePeer(p);
		}
	}

	@Override
	public synchronized List<Peer> listBadPeers() {
		Query q = DB.query();
		q.constrain(Peer.class);
		ObjectSet<Peer> os = q.execute();
		q = DB.query();
		q.constrain(BadPeer.class);
		ObjectSet<BadPeer> osa = q.execute();
		LinkedList<Peer> r =  new LinkedList<Peer>();
		Iterator<Peer> i = os.iterator();
		while (i.hasNext()) {
			Peer p = i.next();
			boolean found = false;
			Iterator<BadPeer> i2 = osa.iterator();
			while (i2.hasNext() && !found) {
				BadPeer bp = i2.next();
				if (p.getPeerKeysAndIdentity().getSignature().getDigest().equals(bp.Digest)) {
					found = true;
				}
			}
			if (found) {
				r.add(p);
			}
		}
		return r;
	}

	@SuppressWarnings("serial")
	@Override
	public synchronized boolean isBadPeer(final Object peerid) {
		ObjectSet<BadPeer> osa = DB.query(new Predicate<BadPeer>() {
			@Override
			public boolean match(BadPeer ep) {
				return ep.Digest.equals(peerid);
			}
		});
		if (osa.size() > 0) {
			return true;
		}
		return false;
	}

	@SuppressWarnings("serial")
	@Override
	public synchronized void removeBadPeer(final Object peerid) {
		ObjectSet<BadPeer> osa = DB.query(new Predicate<BadPeer>() {
			@Override
			public boolean match(BadPeer ep) {
				return ep.Digest.equals(peerid);
			}
		});
		Iterator<BadPeer> i = osa.iterator();
		while (i.hasNext()) {
			BadPeer bp = i.next();
			DB.delete(bp);
		}
	}

	@Override
	public synchronized void removePeer(Peer peer) {
		//NOTE: We cannot delete the peer itself because if it's marked bad
		//           we still expect to be able to return the peer data
		//           so we just mark the peer as bad.
		saveBadPeer(peer);
		//TODO: Remove the posts from this peer.
	}

}
