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.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import org.ourfilesystem.filehander.FileHandler;
import org.ourfilesystem.postcodec.Codec;
import org.ourfilesystem.postcodec.PostDecoded;
import org.ourfilesystem.security.KeySet;
import org.ourfilesystem.security.SecurityTools;
import org.ourfilesystem.utilities.BBytes;
import org.ourfilesystem.utilities.Cloning;
import org.ourfilesystem.utilities.FileDigestIndex;
import org.ourfilesystem.utilities.FileLongIndex;
import org.ourfilesystem.utilities.FileUtils;

public class StorageImpl implements StorageInterface {

	public static int CUR_FILE_VERSION = 0x33;
	public static int CUR_POST_VERSION = 0x44;
	
	public static int MAXPOSTDIFF = 100;
	public static long MAXPOSTSIZE = 10L * 1024L;
	
	public static long MaxFileSize = 2L * 1024L * 1024L;
	
	private String BaseDir;
	private String PeerDir;
	private String FileDir;
	
	private Peer MyPeerData;
	private KeySet MyKeySet;
	private HashMap<BBytes, Peer> PeerCache;
	
	private HashMap<BBytes, PeerPostHoleList> PostHoles;
	
	private FileDigestIndex PeerIndex;
	private FileDigestIndex FileIndex;
	private FileLongIndex PostDateIndex;
	private FileLongIndex FileDateIndex;
	
	public static void writePost(LocalPost pst, OutputStream os) throws IOException {
		os.write(CUR_POST_VERSION);
		FileUtils.writeLong(pst.getLocalDate().getTime(), os);
		if (pst.getPost().getFileReferenceDigest() == null) {
			os.write(2);
		}
		else {
			os.write(1);
			FileUtils.writeBytes(os, ((BBytes)pst.getPost().getFileReferenceDigest()).getBytes());
		}
		FileUtils.writeString(((File)pst.getPost().getMessage()).getPath(), os);
		FileUtils.writeLong(pst.getPost().getPostNumber(), os);
		if (pst.getPost().isPosterHasFile()) { os.write(1); }
		else { os.write(0); }
		SecurityTools.writeSignedDigest(pst.getPost().getSignedDigest(), os);
	}
	
	public static LocalPost readPost(InputStream is) throws IOException {
		LocalPost lp = new LocalPost();
		Post p = new Post();
		lp.setPost(p);
		int v = is.read();
		if (v == CUR_POST_VERSION) {
			lp.setLocalDate(new Date(FileUtils.readLong(is)));
			int v2 = is.read();
			if (v2 == 2) { p.setFileReferenceDigest(null); } 
			else {
				p.setFileReferenceDigest(new BBytes(FileUtils.readBytes(is)));
			}
			p.setMessage(new File(FileUtils.readString(is)));
			p.setPostNumber(FileUtils.readLong(is));
			int hf = is.read();
			p.setPosterHasFile(hf == 1);
			p.setSignedDigest(SecurityTools.readSignedDigest(is));
		}
		else {
		}
		return lp;
	}
	
	public static void writeFileReference(LocalFileReference ref, OutputStream os) throws IOException {
		os.write(CUR_FILE_VERSION);
		FileUtils.writeLong(ref.getLocalDate().getTime(), os);
		FileUtils.writeBytes(os, ((BBytes)ref.getFileReference().getUnsignedDigest()).getBytes());
		FileUtils.writeString(ref.getFileReference().getFile().getPath(), os);
	}
	
	public static LocalFileReference readFileReference(InputStream is) throws IOException {
		LocalFileReference lf = new LocalFileReference();
		FileReference fr = new FileReference();
		lf.setFileReference(fr);
		int v = is.read();
		if (v == CUR_FILE_VERSION) {
			lf.setLocalDate(new Date(FileUtils.readLong(is)));
			fr.setUnsignedDigest(new BBytes(FileUtils.readBytes(is)));
			fr.setFile(new File(FileUtils.readString(is)));
		}
		else {
		}
		return lf;
	}
	
	public StorageImpl(String basedir) {
		PostHoles = new HashMap<BBytes,PeerPostHoleList>();
		File bd = new File(basedir);
		if (bd.exists()) {
			if (!bd.isDirectory()) {
				throw new RuntimeException("File found instead of directory: " + basedir);
			}
		}
		else {
			if (!bd.mkdirs()) {
				throw new RuntimeException("Could not create base directory: " + basedir);
			}
		}

		BaseDir = basedir;
		PeerDir = BaseDir + File.separator + "peers";
		FileDir = BaseDir + File.separator + "files";
		PeerIndex = new FileDigestIndex(PeerDir, PeerDir + File.separator + "peers.idx");
		FileIndex = new FileDigestIndex(FileDir, FileDir + File.separator + "file.idx");
		PostDateIndex = new FileLongIndex(PeerDir, PeerDir + File.separator + "date.idx");
		FileDateIndex = new FileLongIndex(FileDir, FileDir + File.separator + "date.idx");
		
		try {
			loadPeers();
		} catch (IOException e) {
			e.printStackTrace();
			throw new RuntimeException(e.getMessage());
		}
		//printFileIndexes();
	}
	
	private synchronized void loadPeers() throws IOException {
		PeerCache = new HashMap<BBytes, Peer>();
		Set<Object> lst = PeerIndex.list();
		Iterator<Object> i = lst.iterator();
		while (i.hasNext()) {
			BBytes b = (BBytes)i.next();
			File f = PeerIndex.get(b);
			if (f.exists()) {
				FileInputStream fis = new FileInputStream(f);
				Peer p = FileUtils.readPeer(fis);
				fis.close();
				PeerCache.put(b, p);
			}
		}
	}
		
	@Override
	public synchronized void saveMyPeerData(Peer peer) {
		MyPeerData = Cloning.clonePeer(peer);
		savePeer(MyPeerData);
		File f = new File(BaseDir + File.separator + "mypeer.dat");
		try {
			FileOutputStream fos = new FileOutputStream(f);
			FileUtils.writePeer(fos, peer);
			fos.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	@Override
	public synchronized Peer getMyPeerData() {
		if (MyPeerData == null) {
			File f = new File(BaseDir + File.separator + "mypeer.dat");
			try {
				FileInputStream fis = new FileInputStream(f);
				MyPeerData = FileUtils.readPeer(fis);
				fis.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		return MyPeerData;
	}

	@Override
	public synchronized void saveMyKeySet(KeySet keyset) {
		MyKeySet = keyset;
		File f = new File(BaseDir + File.separator + "mykey.dat");
		try {
			FileOutputStream fos = new FileOutputStream(f);
			SecurityTools.writeKeySet(keyset, fos);
			fos.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	@Override
	public synchronized KeySet getMyKeySet() {
		if (MyKeySet == null) {
			File f = new File(BaseDir + File.separator + "mykey.dat");
			try {
				FileInputStream fis = new FileInputStream(f);
				MyKeySet = SecurityTools.readKeySet(fis);
				fis.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		return MyKeySet;
	}

	@Override
	public synchronized void savePeer(Peer peer) {
		File f = PeerIndex.get(peer.getPeerKeysAndIdentity().getSignature().getDigest());
		if (f == null || !f.exists()) {
			long dirnum = PeerCache.size();
			File dirfile = new File(PeerDir + File.separator + dirnum);
			while (dirfile.exists()) {
				dirnum++;
				dirfile = new File(PeerDir + File.separator + dirnum);
			}
			dirfile.mkdirs();
			f = new File(dirfile.getPath() + File.separator + "peer.dat");
		}
		try {
			FileOutputStream fos = new FileOutputStream(f);
			FileUtils.writePeer(fos, peer);
			fos.close();
			PeerIndex.put(peer.getPeerKeysAndIdentity().getSignature().getDigest(), f);
			PeerCache.put((BBytes)peer.getPeerKeysAndIdentity().getSignature().getDigest(), peer);
		}
		catch (Exception e) {
			e.printStackTrace();
		}
	}

	@Override
	public synchronized Peer getPeer(Object peerid) {
		Peer p = PeerCache.get(peerid);
		return p;
	}

	@Override
	public synchronized List<Peer> getPeerList() {
		List<Peer> pl = new LinkedList<Peer>();
		pl.addAll(PeerCache.values());
		return pl;
	}

	//=====================================================================================
	//== Posts.
	//=====================================================================================
	private File getFilePostIndex(BBytes b) {
		File file = FileIndex.get(b);
		if (file == null) {
			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(this.FileDir + File.separator +
								d0n + File.separator +
								d1n + File.separator + 
								d2n + File.separator + val);
			while (d.exists()) {
				val ++;
				d = new File(this.FileDir + File.separator +
						d0n + File.separator +
						d1n + File.separator + 
						d2n + File.separator + val);
			}
			d.mkdirs();
			file = new File(d.getPath() + File.separator + "post.idx");
			try {
				file.createNewFile();
			} catch (IOException e) {
				e.printStackTrace();
			}
			FileIndex.put(b, file);
			return file;
		}
		return file;
	}
	
	private File getPeerDir(BBytes b) {
		File peerfile = PeerIndex.get(b);
		if (peerfile != null && peerfile.exists()) {
			File peerdir = new File(peerfile.getParent());
			if (peerdir.exists()) {
				if (peerdir.isDirectory()) {
					return peerdir;
				}
				else {
					System.out.println("Something went wrong!  This should be a directory: " + peerdir.getPath());
				}
			}
		}
		return null;
	}

	@Override
	public synchronized void savePost(LocalPost post) {
		File peerdir = getPeerDir((BBytes)post.getPost().getSignedDigest().getPeerIdentifier());
		if (peerdir != null) {
			FileLongIndex idx = new FileLongIndex(peerdir.getPath(), peerdir.getPath() + File.separator + "post.idx");
			PeerPostHoleList phl = new PeerPostHoleList(idx);
			phl.newPostNumber(post.getPost().getPostNumber());
			PostHoles.put((BBytes)post.getPost().getSignedDigest().getPeerIdentifier(), phl);			
			File pf = idx.get(post.getPost().getPostNumber());
			if (pf == null || !pf.exists()) {
				long subnum = post.getPost().getPostNumber() % 10240;
				File postdir = new File(peerdir + File.separator + subnum);
				if (!postdir.exists()) {
					postdir.mkdirs();
				}
				if (postdir.isDirectory()) {
					try {
						File messagefile = File.createTempFile("message", ".dat", postdir);
						File mfile = (File)post.getPost().getMessage();
						boolean ok = true;
						if (mfile != null && mfile.exists()) {
							if (mfile.length() > MAXPOSTSIZE) {
								ok = false;
								System.out.println("ERROR: Message file is too large.");
								mfile.delete();
								messagefile.delete();
							}
							else {
								messagefile.delete();
								ok = mfile.renameTo(messagefile);
								if (!ok) {
									System.out.println("ERROR: Failed to rename message file.");
								}
							}
						}				
						if (ok) {
							post.getPost().setMessage(messagefile);
							File postfile = File.createTempFile("postfile", ".dat", postdir);
							FileOutputStream fos = new FileOutputStream(postfile);
							writePost(post, fos);
							fos.close();
							idx.put(post.getPost().getPostNumber(), postfile);
							BBytes fbd = (BBytes)post.getPost().getFileReferenceDigest();
							if (fbd != null) {
								File fp = getFilePostIndex(fbd);
								FileDigestIndex fidx = new FileDigestIndex(PeerDir, fp.getPath());
								fidx.put(post.getPost().getSignedDigest().getDigest(), postfile);
								fidx.close();
							}
							long dateval = post.getLocalDate().getTime();
							File df = PostDateIndex.get(dateval);
							while (df != null) {
								dateval++;
								df = PostDateIndex.get(dateval);
							}
							PostDateIndex.put(dateval, postfile);								
						}
					}
					catch (Exception e) {
						e.printStackTrace();
					}
				}
			}
			idx.close();
		}
	}

	@Override
	public synchronized List<LocalPost> getPeerPosts(Peer p, int page, int pagesize) {
		List<LocalPost> r = new LinkedList<LocalPost>();
		File peerdir = getPeerDir((BBytes)p.getPeerKeysAndIdentity().getSignature().getDigest());
		if (peerdir != null) {
			FileLongIndex idx = new FileLongIndex(peerdir.getPath(), peerdir.getPath() + File.separator + "post.idx");
			PostHoles.put((BBytes)p.getPeerKeysAndIdentity().getSignature().getDigest(), new PeerPostHoleList(idx));
			Set<Object> pl = idx.list();
			if ((page*pagesize) < pl.size()) {
				Object ary[] = (Object[])pl.toArray();
				Arrays.sort(ary);
				for (int cnt = (page*pagesize); (cnt < ((page+1)*pagesize)) && (cnt < ary.length); cnt++) {
					File f = idx.get(ary[cnt]);
					if (f != null && f.exists()) {
						try {
							FileInputStream fis = new FileInputStream(f);
							LocalPost lp = readPost(fis);
							fis.close();
							r.add(lp);
						}
						catch (Exception e) {
							e.printStackTrace();
						}
					}
				}
			}
			idx.close();
		}
		return r;
	}

	@Override
	public synchronized List<LocalPost> getPeerPosts(Peer p, long start, long end) {
		end = Math.min(end, start + MAXPOSTDIFF);
		List<LocalPost> r = new LinkedList<LocalPost>();
		File peerdir = getPeerDir((BBytes)p.getPeerKeysAndIdentity().getSignature().getDigest());
		if (peerdir != null) {
			FileLongIndex idx = new FileLongIndex(peerdir.getPath(), peerdir.getPath() + File.separator + "post.idx");
			PostHoles.put((BBytes)p.getPeerKeysAndIdentity().getSignature().getDigest(), new PeerPostHoleList(idx));
			for (long cnt = start; cnt <= end; cnt++) {
				File f = idx.get(cnt);
				if (f != null && f.exists()) {
					try {
						FileInputStream fis = new FileInputStream(f);
						LocalPost lp = readPost(fis);
						fis.close();
						r.add(lp);
					}
					catch (Exception e) {
						e.printStackTrace();
					}
				}
			}
			idx.close();
		}
		return r;
	}
	
	public synchronized void printFileIndexes() {
		Peer myp = this.getMyPeerData();
		Set<Object> s = FileIndex.list();
		Iterator<Object> i = s.iterator();
		while (i.hasNext()) {
			BBytes dig = (BBytes)i.next();
			System.out.println("FILE: " + dig.toString().substring(0, 10));
			File f = FileIndex.get(dig);
			if (f != null && f.exists()) {
				FileDigestIndex fidx = new FileDigestIndex(PeerDir, f.getPath());
				Set<Object> rs = fidx.list();
				Iterator<Object> i2 = rs.iterator();
				while (i2.hasNext()) {
					System.out.println("      ------------------------------");
					File postfile = fidx.get(i2.next());
					if (postfile != null && postfile.exists()) {
						try {
							FileInputStream fis = new FileInputStream(postfile);
							LocalPost lp = readPost(fis);
							fis.close();
							File mf = new File(lp.getPost().getMessage().toString());
							if (mf.length() > 0) {
								System.out.println("      **************************************************************************************");
								try {
									PostDecoded pd = Codec.decode(lp);
									System.out.println("       BIG FILE: " + pd.getStringValues().get(FileHandler.FILENAME));
								}
								catch (Exception e) {
									e.printStackTrace();
								}
							}
							System.out.println("     My id: " + myp.getPeerKeysAndIdentity().getSignature().getDigest().toString().substring(0, 10));
							System.out.println("     D: " + lp.getLocalDate());
							System.out.println("     N: " + lp.getPost().getPostNumber());
							System.out.println("     F: " + lp.getPost().getSignedDigest().getPeerIdentifier().toString().substring(0,10));
							System.out.println("     M: " + mf);
							System.out.println("     H: " + lp.getPost().isPosterHasFile());
						}
						catch (Exception e) {
							e.printStackTrace();
						}
					}
				}
			}
		}
	}

	@Override
	public synchronized List<LocalPost> getFilePosts(Object dig, int page, int pagesize) {
		List<LocalPost> r = new LinkedList<LocalPost>();
		File file = FileIndex.get((BBytes)dig);
		if (file != null && file.exists()) {
			FileDigestIndex fidx = new FileDigestIndex(PeerDir, file.getPath());
			Set<Object> rs = fidx.list();
			if (rs.size() > (page*pagesize)) {
				Object ra[] = (Object[])rs.toArray();
				for (int cnt = (page*pagesize); (cnt < ra.length) && (cnt < ((page+1)*pagesize)); cnt++) {
					File f = fidx.get(ra[cnt]);
					if (f != null && f.exists()) {
						try {
							FileInputStream fis = new FileInputStream(f);
							LocalPost lp = readPost(fis);
							fis.close();
							r.add(lp);
						}
						catch (Exception e) {
							e.printStackTrace();
						}
					}					
				}
			}
			fidx.close();
		}
		return r;
	}

	@Override
	public synchronized List<LocalPost> getPosts(Date fromdate) {
		long time = fromdate.getTime();
		List<LocalPost> r = new LinkedList<LocalPost>();
		Set<Object> s = PostDateIndex.list();
		Iterator<Object> i = s.iterator();
		while (i.hasNext()) {
			long v = (Long)i.next();
			if (time <= v) {
				File f = PostDateIndex.get(v);
				if (f != null && f.exists()) {
					try {
						FileInputStream fis = new FileInputStream(f);
						LocalPost lp = readPost(fis);
						fis.close();
						r.add(lp);
					}
					catch (Exception e) {
						e.printStackTrace();
					}
				}									
			}
		}
		return r;
	}

	
	//===================================================================================
	//== FileReferences.
	//===================================================================================
	@Override
	public synchronized void saveFile(LocalFileReference ref) {
		File f = this.getFilePostIndex((BBytes)ref.getFileReference().getUnsignedDigest());
		if (f != null && f.exists() && f.length() <= MaxFileSize) {
			try {
				File p = f.getParentFile();
				File mfile = new File(p.getPath() + File.separator + "file.dat");
				File mf = ref.getFileReference().getFile();
				if (mf != null && mf.exists()) {
					boolean mv = mf.renameTo(mfile);
					if (mv) {
						ref.getFileReference().setFile(mfile);
						File ffile = new File(p.getPath() + File.separator + "ref.dat");
						FileOutputStream fos = new FileOutputStream(ffile);
						writeFileReference(ref, fos);
						fos.close();
						long time = ref.getLocalDate().getTime();
						File tf = this.FileDateIndex.get(time);
						while (tf != null) {
							time++;
							tf = this.FileDateIndex.get(time);
						}
						FileDateIndex.put(time, ffile);
					}
				}
			}
			catch (Exception e) {
				e.printStackTrace();
			}
		}
	}
	
	private synchronized File getFileReferenceFile(Object dig) {
		File f = FileIndex.get((BBytes)dig);
		if (f != null && f.exists()) {
			File p = f.getParentFile();
			File ffile = new File(p.getPath() + File.separator + "ref.dat");
			return ffile;
		}
		return null;
	}

	@Override
	public synchronized LocalFileReference getFileReference(Object dig) {
		File ffile = getFileReferenceFile(dig);
		if (ffile != null && ffile.exists()) {
			try {
				FileInputStream fis = new FileInputStream(ffile);
				LocalFileReference l = readFileReference(fis);
				fis.close();
				return l;
			}
			catch (Exception e) {
				e.printStackTrace();
			}
		}
		return null;
	}

	@Override
	public synchronized List<LocalFileReference> getFileReferences(int page, int pagesize) {
		List<LocalFileReference> r = new LinkedList<LocalFileReference>();
		Set<Object> s = FileDateIndex.list();
		Object ary[] = (Object[])s.toArray();
		Arrays.sort(ary);
		for (int cnt = (page*pagesize); (cnt < ary.length) && (cnt < ((page+1)*pagesize)); cnt++) {
			try {
				File f = FileDateIndex.get(ary[cnt]);
				FileInputStream fis = new FileInputStream(f);
				LocalFileReference l = readFileReference(fis);
				fis.close();
				r.add(l);
			}
			catch (Exception e) {
				e.printStackTrace();
			}
		}
		return r;
	}

	@Override
	public synchronized List<LocalFileReference> getFileReferences(Date fromdate) {
		List<LocalFileReference> r = new LinkedList<LocalFileReference>();
		long time = fromdate.getTime();
		Set<Object> s = FileDateIndex.list();
		Iterator<Object> i = s.iterator();
		while (i.hasNext()) {
			long val = (Long)i.next();
			if (time <= val) {
				try {
					File f = FileDateIndex.get(val);
					FileInputStream fis = new FileInputStream(f);
					LocalFileReference l = readFileReference(fis);
					fis.close();
					r.add(l);
				}
				catch (Exception e) {
					e.printStackTrace();
				}
			}
		}
		return r;
	}
	
	@Override
	public void close() {
		PeerIndex.close();
		FileIndex.close();
		PostDateIndex.close();
		FileDateIndex.close();
	}

	@Override
	public synchronized Long getLastPostNumber(Peer p) {
		File peerdir = getPeerDir((BBytes)p.getPeerKeysAndIdentity().getSignature().getDigest());
		if (peerdir != null) {
			FileLongIndex idx = new FileLongIndex(peerdir.getPath(), peerdir.getPath() + File.separator + "post.idx");
			PostHoles.put((BBytes)p.getPeerKeysAndIdentity().getSignature().getDigest(), new PeerPostHoleList(idx));
			long v = idx.getMaxValue();
			idx.close();
			return v;
		}
		return null;
	}

	@Override
	public List<PostHoles> getPostHoles(Peer p) {
		PeerPostHoleList phl = PostHoles.get(p.getPeerKeysAndIdentity().getSignature().getDigest());
		if (phl != null) {
			return phl.getHoles();
		}
		return null;
	}

	@Override
	public void saveBadPeer(Peer p) {
		// TODO Auto-generated method stub
		
	}

	@Override
	public List<Peer> listBadPeers() {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public boolean isBadPeer(Object id) {
		// TODO Auto-generated method stub
		return false;
	}

	@Override
	public void removeBadPeer(Object id) {
		// TODO Auto-generated method stub
		
	}

	@Override
	public void removePeer(Peer peer) {
		// TODO Auto-generated method stub
		
	}

}
