package org.ourfilesystem.utilities;

/*
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.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.charset.Charset;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.ourfilesystem.db.Peer;
import org.ourfilesystem.db.Post;
import org.ourfilesystem.security.SecurityTools;

public class FileUtils {
	
	public static long MAXFILESIZE = 10L * 1024L * 1024L;
	
	public static void writeBytes(OutputStream os, byte b[]) throws IOException {
		if (b == null) {
			os.write(2);
		}
		else {
			os.write(1);
			byte ob[] = new byte[b.length + (Integer.SIZE/Byte.SIZE)];
			ByteBuffer buf = ByteBuffer.wrap(ob);
			buf.putInt(b.length);
			buf.put(b);
			os.write(ob);
		}
	}
	
	public static byte[] readBytes(InputStream i) throws IOException {
		int f = i.read();
		if (f == 2) {
			return null;
		}
		else {
			byte lb[] = new byte[Integer.SIZE/Byte.SIZE];
			fillBytes(lb, i);
			ByteBuffer buf = ByteBuffer.wrap(lb);
			int len = buf.getInt();
			if (len > MAXFILESIZE) {
				throw new IOException("Byte buffer too large.");
			}
			byte ob[] = new byte[len];
			fillBytes(ob, i);
			return ob;			
		}
	}
	
	public static void fillBytes(byte b[], InputStream i) throws IOException {
		int len = i.read(b);
		if (len == -1) { 
			//System.out.println("THIS SHOULD NOT BE!");
			//Thread.dumpStack();
		}
		int maxzeros = 100; // 60000
		while (len < b.length && len != -1) {
			int il = i.read(b, len, b.length-len);
			if (il == 0) {
				maxzeros--;
				if (maxzeros == 0) {
					throw new IOException("No data read.");
				}
				try {
					//NOTE: If a connection via proxy is lost it sometimes takes quite
					//a while before an exception on the socket is thrown.  In the mean
					//time we spin on the channel transfer which causes the thread to
					//run away.  Therefore, put the sleep here to let the thread idle for a bit
					//when there is no data being transfered.
					Thread.sleep(600);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			if (il > 0) {
				maxzeros = 100;
				len += il;
			}
		}
	}
	
	public static void writeLong(long v, OutputStream os) throws IOException {
		byte b[] = new byte[Long.SIZE/Byte.SIZE];
		ByteBuffer buf = ByteBuffer.wrap(b);
		buf.putLong(v);
		writeBytes(os, b);
	}
	
	public static long readLong(InputStream is) throws IOException {
		byte b[] = readBytes(is);
		ByteBuffer buf = ByteBuffer.wrap(b);
		return buf.getLong();
	}
	
	public static void writeInt(int v, OutputStream os) throws IOException {
		byte b[] = new byte[Integer.SIZE/Byte.SIZE];
		ByteBuffer buf = ByteBuffer.wrap(b);
		buf.putInt(v);
		writeBytes(os, b);
	}
	
	public static int readInt(InputStream is) throws IOException {
		byte b[] = readBytes(is);
		ByteBuffer buf = ByteBuffer.wrap(b);
		return buf.getInt();
	}
	
	public static void writeString(String s, OutputStream os) throws IOException {
		if (s == null) {
			os.write(2);
		}
		else {
			os.write(1);
			writeBytes(os, s.getBytes(Charset.forName("UTF-16BE")));
		}
	}
	
	public static String readString(InputStream is) throws IOException {
		int v = is.read();
		if (v == 2) {
			return null;
		}
		else {
			byte b[] = readBytes(is);
			return new String(b, Charset.forName("UTF-16BE"));
		}
	}
	
	public static List<File> findFiles(String dir, String filename) {
		List<File> rf = new LinkedList<File>();
		File df = new File(dir);
		if (df.isDirectory()) {
			Pattern p = Pattern.compile(filename);
			Matcher m = p.matcher("");
			findFiles(df, m, rf);
		}
		return rf;
	}
	
	private static void findFiles(File dir, Matcher m, List<File> rf) {
		if (dir.exists() && dir.isDirectory()) {
			File fl[] = dir.listFiles();
			for (int cnt = 0; cnt < fl.length; cnt++) {
				if (fl[cnt].exists()) {
					if (fl[cnt].isDirectory()) {
						findFiles(fl[cnt], m, rf);
					}
					else {
						m.reset(fl[cnt].getName());
						if (m.find()) {
							rf.add(fl[cnt]);
						}
					}
				}
			}
		}
	}
	
	public static void copyFile(File from, File to, boolean deleteoncopy) throws IOException {
		FileOutputStream fos = new FileOutputStream(to);
		FileChannel foc = fos.getChannel();
		FileInputStream fis = new FileInputStream(from);
		FileChannel fic = fis.getChannel();
		foc.transferFrom(fic, 0, from.length());
		foc.close();
		fic.close();
		if (deleteoncopy) {
			from.delete();
		}
	}
	
	public static void writePeer(OutputStream os, Peer p) throws IOException {
		FileUtils.writeLong(p.getUpdateCount(), os);
		FileUtils.writeString(p.getIntroduction(), os);
		SecurityTools.writePublicKeySetSigned(p.getPeerKeysAndIdentity(), os);
		FileUtils.writeString((String)p.getLocation(), os);
		FileUtils.writeString(p.getNickname(), os);
		SecurityTools.writeSignedDigest(p.getLocationSignature(), os);
	}
	
	public static Peer readPeer(InputStream is) throws IOException {
		Peer p = new Peer();
		p.setUpdateCount(FileUtils.readLong(is));
		p.setIntroduction(FileUtils.readString(is));
		p.setPeerKeysAndIdentity(SecurityTools.readPublicKeySetSigned(is));
		p.setLocation(FileUtils.readString(is));
		p.setNickname(FileUtils.readString(is));
		p.setLocationSignature(SecurityTools.readSignedDigest(is));
		return p;
	}
	
	public static Post readPost(InputStream is, File TempDir) throws IOException {
		Post p = new Post();
		int v = is.read();
		if (v == 1) {
			p.setFileReferenceDigest(new BBytes(FileUtils.readBytes(is)));
		}
		v = is.read();
		if (v == 1) {
			File f = File.createTempFile("postinput", ".dat", TempDir);
			FileUtils.readFile(is, f);
			p.setMessage(f);
		}
		p.setPostNumber(FileUtils.readLong(is));
		v = is.read();
		if (v == 1) { p.setPosterHasFile(true); }
		else { p.setPosterHasFile(false); }
		p.setSignedDigest(SecurityTools.readSignedDigest(is));
		return p;
	}
	
	public static void writePost(OutputStream os, Post p) throws IOException {
		if (p.getFileReferenceDigest() == null) {
			os.write(2);
		}
		else {
			os.write(1);
			FileUtils.writeBytes(os, ((BBytes)p.getFileReferenceDigest()).getBytes());
		}
		if (p.getMessage() == null) {
			os.write(2);
		}
		else {
			os.write(1);
			FileUtils.sendFile(os, (File)p.getMessage());
		}
		FileUtils.writeLong(p.getPostNumber(), os);
		if (p.isPosterHasFile()) { os.write(1); }
		else { os.write(0); }
		SecurityTools.writeSignedDigest(p.getSignedDigest(), os);			
	}
	
	public static void sendFile(OutputStream os, File in) throws IOException {
		long numlength = in.length();
		writeLong(numlength, os);
		FileInputStream fis = new FileInputStream(in);
		FileChannel fic = fis.getChannel();
		WritableByteChannel oc = Channels.newChannel(os);
		long pos = 0;
		while (numlength > 0) {
			long num = fic.transferTo(pos, numlength, oc);
			pos += num;
			numlength -= num;
		}
		fic.close();
	}
	
	public static void readFile(InputStream is, File f) throws IOException {
		FileOutputStream fos = new FileOutputStream(f);
		FileChannel foc = fos.getChannel();
		long len = readLong(is);
		if (len > MAXFILESIZE) {
			throw new RuntimeException("File too large.");
		}
		long pos = 0;
		ReadableByteChannel ic = Channels.newChannel(is);
		while (len > 0) {
			long nt = foc.transferFrom(ic, pos, len);
			if (nt == 0) {
				try {
					//NOTE: If a connection via proxy is lost it sometimes takes quite
					//a while before an exception on the socket is thrown.  In the mean
					//time we spin on the channel transfer which causes the thread to
					//run away.  Therefore, put the sleep here to let the thread idle for a bit
					//when there is no data being transfered.
					Thread.sleep(500);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			pos += nt;
			len -= nt;
		}
		foc.close();
	}
	
	public static void deleteDir(String f) {
		deleteDir(new File(f));
	}
	
	public static void deleteDir(File f) {
		if (f.exists()) {
			if (f.isDirectory()) {
				File fl[] = f.listFiles();
				for (int c = 0; c < fl.length; c++) {
					deleteDir(fl[c]);
				}
			}
			f.delete();
		}
	}
	
	public static boolean diff(File f0, File f1) throws IOException {
		if (f0.length() != f1.length()) return false;
		FileInputStream fi0 = new FileInputStream(f0);
		FileInputStream fi1 = new FileInputStream(f1);
		boolean eq = true;
		int b0, b1;
		do {
			b0 = fi0.read();
			b1 = fi1.read();
			if (b0 != b1) eq = false;
		} while ((b0 != -1 && b1 != -1) && eq); 
		fi0.close();
		fi1.close();
		return eq;
	}
	
	
}
