package edu.berkeley.nlp.util;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

public class IOUtils {
	// Listing files in a directory
	public static List<File> getFilesUnder(String path, FileFilter fileFilter) {
		File root = new File(path);
		List<File> files = new ArrayList<File>();
		addFilesUnder(root, files, fileFilter);
		return files;
	}

	private static void addFilesUnder(File root, List<File> files,
			FileFilter fileFilter) {
		if (!fileFilter.accept(root))
			return;
		if (root.isFile()) {
			files.add(root);
			return;
		}
		if (root.isDirectory()) {
			File[] children = root.listFiles();
			for (int i = 0; i < children.length; i++) {
				File child = children[i];
				addFilesUnder(child, files, fileFilter);
			}
		}
	}

	// }

	public static File createTempFileHard(String prefix, String suffix) {
		try {
			return File.createTempFile(prefix, suffix);
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
	}

	public static boolean createNewFileEasy(String path) {
		if (path == null)
			return false;
		try {
			new File(path).createNewFile();
			return true;
		} catch (IOException e) {
			return false;
		}
	}

	public static boolean createNewFileIfNotExistsEasy(String path) {
		if (path == null)
			return false;
		if (new File(path).isFile())
			return true;
		return createNewFileEasy(path);
	}

	public static boolean createNewDirIfNotExistsEasy(String path) {
		if (path == null)
			return false;
		if (new File(path).isDirectory())
			return true;
		return new File(path).mkdir();
	}

	// Printing stuff straight to a file {
	@Deprecated
	public static <T> void filePrintList(String file, Collection<T> c)
			throws IOException {
		PrintWriter out = openOut(file);
		for (T x : c)
			out.println(x);
		out.close();
	}

	@Deprecated
	public static void filePrintf(String file, String format, Object... args)
			throws IOException {
		PrintWriter out = openOut(file);
		out.println(String.format(format, args));
		out.close();
	}

	@Deprecated
	public static void filePrintln(String file, Object o) throws IOException {
		PrintWriter out = openOut(file);
		out.println(o);
		out.close();
	}

	@Deprecated
	public static void filePrintlnEasy(String file, Object o) {
		PrintWriter out = openOutEasy(file);
		if (out == null)
			return;
		out.println(o);
		out.close();
	}

	// }

	// Opening files {
	// openBinIn
	public static ObjectInputStream openBinIn(String path) throws IOException {
		return openBinIn(new File(path));
	}

	public static ObjectInputStream openBinIn(File path) throws IOException {
		return new ObjectInputStream(new FileInputStream(path));
	}

	public static ObjectInputStream openBinInEasy(String path) {
		if (StrUtils.isEmpty(path))
			return null;
		return openBinInEasy(new File(path));
	}

	public static ObjectInputStream openBinInEasy(File path) {
		if (path == null)
			return null;
		try {
			return openBinIn(path);
		} catch (Exception e) {
			return null;
		}
	}

	public static ObjectInputStream openBinInHard(String path) {
		return openBinInHard(new File(path));
	}

	public static ObjectInputStream openBinInHard(File path) {
		try {
			return openBinIn(path);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	// openIn

	public static BufferedReader openInGZip(String path) throws IOException {
		GZIPInputStream is = new GZIPInputStream(new FileInputStream(path));
		return new BufferedReader(new InputStreamReader(is));
	}

	public static BufferedReader openIn(String path) throws IOException {
		return openIn(new File(path));
	}

	public static BufferedReader openIn(File path) throws IOException {
		InputStream is = new FileInputStream(path);
		if (path.getName().endsWith(".gz"))
			is = new GZIPInputStream(is);
		return new BufferedReader(CharEncUtils.getReader(is));
	}

	public static BufferedReader openInEasy(String path) {
		if (StrUtils.isEmpty(path))
			return null;
		return openInEasy(new File(path));
	}

	public static BufferedReader openInEasy(File path) {
		if (path == null)
			return null;
		try {
			return openIn(path);
		} catch (Exception e) {
			return null;
		}
	}

	public static BufferedReader openInHard(String path) {
		return openInHard(new File(path));
	}

	public static BufferedReader openInHard(File path) {
		try {
			return openIn(path);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	// openBinOut
	public static ObjectOutputStream openBinOut(String path) throws IOException {
		return openBinOut(new File(path));
	}

	public static ObjectOutputStream openBinOut(File path) throws IOException {
		return new ObjectOutputStream(new FileOutputStream(path));
	}

	public static ObjectOutputStream openBinOutEasy(String path) {
		if (StrUtils.isEmpty(path))
			return null;
		return openBinOutEasy(new File(path));
	}

	public static ObjectOutputStream openBinOutEasy(File path) {
		if (path == null)
			return null;
		try {
			return openBinOut(path);
		} catch (Exception e) {
			return null;
		}
	}

	public static ObjectOutputStream openBinOutHard(String path) {
		return openBinOutHard(new File(path));
	}

	public static ObjectOutputStream openBinOutHard(File path) {
		try {
			return openBinOut(path);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	public static PrintWriter openOutAppend(String path) throws IOException {
		return openOutAppend(new File(path));
	}

	public static PrintWriter openOutAppend(File path) throws IOException {
		return new PrintWriter(CharEncUtils.getWriter(new FileOutputStream(
				path, true)));
	}

	public static PrintWriter openOutAppendEasy(String path) {
		if (StrUtils.isEmpty(path))
			return null;
		return openOutAppendEasy(new File(path));
	}

	public static PrintWriter openOutAppendEasy(File path) {
		if (path == null)
			return null;
		try {
			return openOutAppend(path);
		} catch (Exception e) {
			return null;
		}
	}

	public static PrintWriter openOutAppendHard(String path) {
		return openOutAppendHard(new File(path));
	}

	public static PrintWriter openOutAppendHard(File path) {
		try {
			return openOutAppend(path);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	// openOut
	public static PrintWriter openOut(String path) throws IOException {
		return openOut(new File(path));
	}

	public static PrintWriter openOut(File path) throws IOException {
		OutputStream os = new FileOutputStream(path);
		if (path.getName().endsWith(".gz"))
			os = new GZIPOutputStream(os);
		return new PrintWriter(CharEncUtils.getWriter(os));
	}

	public static PrintWriter openOutEasy(String path) {
		if (StrUtils.isEmpty(path))
			return null;
		return openOutEasy(new File(path));
	}

	public static PrintWriter openOutEasy(File path) {
		if (path == null)
			return null;
		try {
			return openOut(path);
		} catch (Exception e) {
			return null;
		}
	}

	public static PrintWriter openOutHard(String path) {
		return openOutHard(new File(path));
	}

	public static PrintWriter openOutHard(File path) {
		try {
			return openOut(path);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	// }

	// Java binary serialization {
	// openObjIn
	public static ObjectInputStream openObjIn(String path) throws IOException {
		return openObjIn(new File(path));
	}

	public static ObjectInputStream openObjIn(File path) throws IOException {
		InputStream fis = new BufferedInputStream(new FileInputStream(path));
		return path.getName().endsWith(".gz") ? new ObjectInputStream(
				new GZIPInputStream(fis)) : new ObjectInputStream(fis);
	}

	// openObjOut
	public static ObjectOutputStream openObjOut(String path) throws IOException {
		return openObjOut(new File(path));
	}

	public static ObjectOutputStream openObjOut(File path) throws IOException {
		OutputStream fos = new BufferedOutputStream(new FileOutputStream(path));
		return path.getName().endsWith(".gz") ? new ObjectOutputStream(
				new GZIPOutputStream(fos)) : new ObjectOutputStream(fos);
	}

	// readObjFile
	public static Object readObjFile(String path) throws IOException,
			ClassNotFoundException {
		return readObjFile(new File(path));
	}

	public static Object readObjFile(File path) throws IOException,
			ClassNotFoundException {
		ObjectInputStream in = openObjIn(path);
		Object obj = in.readObject();
		in.close();
		return obj;
	}

	public static Object readObjFileEasy(String path) {
		if (StrUtils.isEmpty(path))
			return null;
		return readObjFileEasy(new File(path));
	}

	public static Object readObjFileEasy(File path) {
		if (path == null)
			return null;
		try {
			return readObjFile(path);
		} catch (Exception e) {
			return null;
		}
	}

	public static Object readObjFileHard(String path) {
		return readObjFileHard(new File(path));
	}

	public static Object readObjFileHard(File path) {
		try {
			return readObjFile(path);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	// writeObjFile
	public static void writeObjFile(String path, Object obj) throws IOException {
		writeObjFile(path, obj);
	}

	public static void writeObjFile(File path, Object obj) throws IOException {
		ObjectOutputStream out = openObjOut(path);
		out.writeObject(obj);
		out.close();
	}

	public static boolean writeObjFileEasy(String path, Object obj) {
		if (StrUtils.isEmpty(path))
			return false;
		return writeObjFileEasy(new File(path), obj);
	}

	public static boolean writeObjFileEasy(File path, Object obj) {
		if (path == null)
			return false;
		try {
			writeObjFile(path, obj);
			return true;
		} catch (Exception e) {
			return false;
		}
	}

	public static void writeObjFileHard(String path, Object obj) {
		writeObjFileHard(new File(path), obj);
	}

	public static void writeObjFileHard(File path, Object obj) {
		try {
			writeObjFile(path, obj);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	// }

	public static boolean closeEasy(BufferedReader in) {
		try {
			in.close();
			return true;
		} catch (IOException e) {
			return false;
		}
	}

	// Copying files {
	// Return number of bytes copied
	public static int copy(InputStream in, OutputStream out) throws IOException {
		byte[] buf = new byte[16384];
		int total = 0, n;
		while ((n = in.read(buf)) != -1) {
			total += n;
			out.write(buf, 0, n);
		}
		out.flush();
		return total;
	}

	// Return number of characters copied
	public static int copy(Reader in, Writer out) throws IOException {
		char[] buf = new char[16384];
		int total = 0, n;
		while ((n = in.read(buf)) != -1) {
			total += n;
			out.write(buf, 0, n);
		}
		out.flush();
		return total;
	}

	// }

	// File path operations {
	public static boolean createSymLink(String src, String dest) {
		try {
			String cmd = String.format("ln -s %s %s", src, dest);
			Runtime.getRuntime().exec(cmd);
			return true;
		} catch (IOException e) {
			return false;
		}
	}

	public static boolean purgePath(File oldPath) {
		// Ok, this isn't really purging because I'm paranoid.
		// Move <directory> to <directory>-purged[-<i>]
		for (int i = 0; i < 1000; i++) {
			File newPath = new File(oldPath.getParent(), oldPath.getName()
					+ ".purged" + (i == 0 ? "" : "-" + i));
			if (newPath.exists())
				continue;
			return oldPath.renameTo(newPath);
		}
		return false;
	}

	public static String stripFileExt(String file) {
		int i = file.lastIndexOf('.');
		if (i == -1)
			return file;
		return file.substring(0, i);
	}

	public static String getFileExt(String file) {
		int i = file.lastIndexOf('.');
		if (i == -1)
			return "";
		return file.substring(i + 1);
	}

	// }

	private static String removeComment(String line) {
		int i = -1;
		while ((i = line.indexOf("#", i + 1)) != -1) { // Look for comment
														// character
			if (i == 0 || line.charAt(i - 1) != '\\') // Make sure not escaped
				break;
		}
		return i == -1 ? line : line.substring(0, i);
	}

	private static String removeTrailingSpace(String line) {
		for (int i = line.length() - 1; i >= 0; i--)
			if (!Character.isWhitespace(line.charAt(i)))
				return line.substring(0, i + 1);
		return "";
	}

	// Program mode is where comments (#) and blank lines are skipped
	// and trailing \'s are combined into one line.
	// Anything after the first non-escaped # is a comment.
	public static interface LineMunger {
		public void beforeLine(boolean isContinuation);

		public void afterFullLine(String line); // Called when a full line is
												// read.
	}

	public static void doProgramLines(BufferedReader in, LineMunger lineMunger)
			throws IOException {
		String line;
		String carry = "";
		while (true) {
			if (lineMunger != null)
				lineMunger.beforeLine(carry.length() > 0);
			line = in.readLine();
			if (line == null)
				break;

			line = removeComment(line);
			line = removeTrailingSpace(line);
			if (line.endsWith("\\"))
				carry += line.substring(0, line.length() - 1);
			else {
				lineMunger.afterFullLine(carry + line);
				carry = "";
			}
			if (line.equals("") || line.startsWith("#"))
				continue;
		}
		if (carry.length() > 0)
			lineMunger.afterFullLine(carry);
	}

	public static void doProgramLines(String path, LineMunger lineMunger)
			throws IOException {
		BufferedReader in = openIn(path);
		doProgramLines(in, lineMunger);
		in.close();
	}

	public static class LineListMaker implements LineMunger {
		private List<String> lines = new ArrayList<String>();

		public void beforeLine(boolean isContinuation) {
		}

		public void afterFullLine(String line) {
			lines.add(line);
		}

		public List<String> getLines() {
			return lines;
		}
	}

	public static List<String> readProgramLines(String path) throws IOException {
		LineListMaker maker = new LineListMaker();
		doProgramLines(path, maker);
		return maker.getLines();
	}

	public static List<String> readProgramLinesHard(String path) {
		try {
			return readProgramLines(path);
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
	}

	// Ordinary read lines function
	public static List<String> readLines(String path) throws IOException {

		BufferedReader in = IOUtils.openIn(path);
		List<String> list = readLines(in);
		in.close();
		return list;
	}

	public static void writeLines(String path, List<String> lines)
			throws IOException {
		PrintWriter out = IOUtils.openOut(path);
		for (String line : lines) {
			out.println(line);
		}
		out.close();
	}

	public static void writeLinesHard(String path, List<String> lines) {
		try {
			PrintWriter out = IOUtils.openOut(path);
			for (String line : lines) {
				out.println(line);
			}
			out.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	public static Iterator<String> lineIterator(String path) throws IOException {
		final BufferedReader reader = IOUtils.openIn(path);
		return new Iterator<String>() {

			private String line;

			public boolean hasNext() {
				// TODO Auto-generated method stub
				try {
					return nextLine();
				} catch (Exception e) {
					e.printStackTrace();
				}
				return false;
			}

			private boolean nextLine() throws IOException {
				if (line != null) {
					return true;
				}
				line = reader.readLine();
				return line != null;
			}

			public String next() {
				// TODO Auto-generated method stub
				try {
					nextLine();
					String retLine = line;
					line = null;
					return retLine;
				} catch (IOException e) {
					throw new RuntimeException();
				}
			}

			public void remove() {
				// TODO Auto-generated method stub
				throw new RuntimeException("remove() not supported");
			}

		};
	}

	public static List<String> readLines(BufferedReader in) throws IOException {
		String line;
		List<String> lines = new ArrayList<String>();
		while ((line = in.readLine()) != null)
			lines.add(line);
		return lines;
	}

	public static List<String> readLinesEasy(String path) {
		try {
			return readLines(path);
		} catch (IOException e) {
			return Collections.EMPTY_LIST;
		}
	}

	public static List<String> readLinesHard(String path) {
		try {
			return readLines(path);
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
	}

	// Return the first line, null if it doesn't exist
	public static String readLine(String path) throws IOException {
		BufferedReader in = IOUtils.openIn(path);
		String line = in.readLine();
		in.close();
		return line;
	}

	public static String readLineEasy(String path) {
		try {
			return readLine(path);
		} catch (IOException e) {
			return null;
		}
	}

	public static void printLines(String path, List lines) throws IOException {
		PrintWriter out = IOUtils.openOut(path);
		printLines(out, lines);
		out.close();
	}

	public static void printLinesHard(String path, List lines) {
		try {
			printLines(path, lines);
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
	}

	public static boolean printLinesEasy(String path, List lines) {
		try {
			printLines(path, lines);
			return true;
		} catch (IOException e) {
			return false;
		}
	}

	public static void printLines(PrintWriter out, List lines) {
		for (Object line : lines)
			out.println(StrUtils.toString(line));
	}

	public static int readBigEndianInt(InputStream in) throws IOException {
		int a = in.read();
		if (a == -1)
			throw new IOException("EOF");
		int b = in.read();
		if (b == -1)
			throw new IOException("EOF");
		int c = in.read();
		if (c == -1)
			throw new IOException("EOF");
		int d = in.read();
		if (d == -1)
			throw new IOException("EOF");
		return (a << 24) + (b << 16) + (c << 8) + d;
	}
}
