/*
 * @(#)MD5Verify.java
 *
 * This file is part of webCDwriter - Network CD Writing.
 *
 * Copyright (C) 2003-2004 Jörg P. M. Haeger
 *
 * webCDwriter is free software. See CDcreator.java for details.
 */

import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.*;
import java.security.MessageDigest;
import javax.swing.*;

/**
 * A MD5 sums checker.
 *
 * @version 20040422
 * @author Jörg P. M. Haeger
 */
class MD5Verify {
	private static boolean stdout = false;
	private static java.util.TreeMap treeMap;
	private static int
		MD5Errors = 0,
		notFoundErrors = 0,
		OKs = 0,
		readErrors = 0;

	private static long
		bytesError = 0,
		bytesOK = 0;

	private static JFrame frame;
	private static JTextArea errorArea, textArea;
	private static JScrollPane scrollPane;
	private static JLabel
		fileLabel,
		MD5ErrorLabel,
		notFoundErrorLabel,
		OKLabel,
		readErrorLabel,
		speedLabel;
	private static JProgressBar progressBar;
	private static MyBox verifyBox;
	private static Speedometer speedometer;
	private static int maxSpeed = 64;

	static String bytesToHex(byte[] bs) {
		String hexStr = "";
		for (int i = 0; i < bs.length; i++) {
			int c1 = (bs[i] & 0xF0) >> 4;
			if (c1 < 10)
				c1 += '0';
			else
				c1 += 'a' - 10;

			int c2 = bs[i] & 0x0F;
			if (c2 < 10)
				c2 += '0';
			else
				c2 += 'a' - 10;

			hexStr = hexStr + (char)c1 + (char)c2;
		}
		return hexStr;
	}

	static JMenuBar createMenuBar() {
		JMenuBar menuBar = new JMenuBar();
		JMenu menu;
		JMenuItem item;
		menu = new JMenu("File");

			item = new JMenuItem("Quit");
			item.addActionListener(new ActionListener() {
				public void actionPerformed(ActionEvent e) {
					System.exit(0);
				}
			});
			menu.add(item);

		menuBar.add(menu);
		menu = new JMenu("Help");

			item = new JMenuItem("About");
			item.addActionListener(new ActionListener() {
				public void actionPerformed(ActionEvent e) {
					JOptionPane.showMessageDialog(frame,
						new JLabel("<html>"
							+ Version.product
							+ " " + Version.version + "<br>"
							+ Version.copyright + "<br>"
							+ "<a href=\"" + Version.authorHome + "\">"
								+ Version.authorHome + "</a>"
							+ "</html>"));
				}
			});
			menu.add(item);

		menuBar.add(menu);
		return menuBar;
	}

	static JComponent createResultPanel() {
		MyBox vbox = new MyBox(BoxLayout.Y_AXIS);
		vbox.setAlignmentX(Box.LEFT_ALIGNMENT);
		vbox.setBorder(
			BorderFactory.createTitledBorder(
				BorderFactory.createEtchedBorder(), "Result"));
		OKLabel = new JLabel("No files");
		vbox.add(OKLabel);
		notFoundErrorLabel = new JLabel();
		vbox.add(notFoundErrorLabel);
		readErrorLabel = new JLabel();
		vbox.add(readErrorLabel);
		MD5ErrorLabel = new JLabel();
		vbox.add(MD5ErrorLabel);
		vbox.add(Box.createVerticalStrut(10));
		showErrorNum();

		JTabbedPane tabbedPane = new JTabbedPane();
		tabbedPane.setAlignmentX(Box.LEFT_ALIGNMENT);
		tabbedPane.addTab("Log", scrollPane);
		tabbedPane.addTab("Errors", new JScrollPane(errorArea));
		vbox.add(tabbedPane);

		return vbox;
	}

	static JComponent createVerifyPanel() {
		MyBox vbox = new MyBox(BoxLayout.Y_AXIS);
		vbox.setAlignmentX(Box.LEFT_ALIGNMENT);
		vbox.setBorder(
			BorderFactory.createTitledBorder(
				BorderFactory.createEtchedBorder(), "Verification"));
		fileLabel = new JLabel("No file");
		fileLabel.setAlignmentX(Box.LEFT_ALIGNMENT);
		vbox.add(fileLabel);
		vbox.add(Box.createVerticalStrut(5));
		progressBar = new JProgressBar();
		progressBar.setAlignmentX(Box.LEFT_ALIGNMENT);
		vbox.add(progressBar);
		vbox.add(Box.createVerticalStrut(5));
		speedLabel = new JLabel("Speed: 0x");
		vbox.add(speedLabel);
		vbox.add(Box.createVerticalStrut(10));
		verifyBox = vbox;
		return vbox;
	}

	static String getMD5Sum(File file) throws Exception {
		fileLabel.setText(file.getPath());
		progressBar.setMaximum((int) (file.length() / 1024));
		InputStream inStream = new FileInputStream(file);
		MessageDigest MD5 = MessageDigest.getInstance("MD5");
		int speed = 0;
		long count = 0;
		while (true) {
			byte[] bs = new byte[2 * 1024];
			int n = inStream.read(bs);
			if (n < 0)
				break;
			MD5.update(bs, 0, n);
			count += n;
			speedometer.inc(n);
			speedometer.slowDown(maxSpeed);
			progressBar.setValue((int) (count / 1024));
			int speed2 = (int) (10 * speedometer.getSpeed());
			speedLabel.setText("Speed: "
				+ speed2 / 10 + "." + speed2 % 10 + "x"
				+ " (" + (speed2 * 15) + " KB/s)");
		}
		return bytesToHex(MD5.digest());
	}

	static String format(int n, int width) {
		StringBuffer str = new StringBuffer();
		str.append(n);
		for (int i = 0; str.length() < width; i++)
			str.insert(0, ' ');
		return new String(str);
	}

	public static void main(String[] args) throws Exception {
		int arg0 = 0;
		while (arg0 < args.length) {
			if (args[arg0].equals("--help")) {
				stdout = true;
				println("Usage: java -jar MD5Verify.jar"
					+ " [options] [directory]");
				println("");
				println("Options:\t--stdout\tno GUI");
				println("\t\t--speed x\tspeed limit");
				println("");
				return;
			}
			else
			if (args[arg0].equals("--speed"))
				maxSpeed = Integer.parseInt(args[++arg0]);
			else
			if (args[arg0].equals("--stdout")) {
				stdout = true;
				println(Version.product + " " + Version.version
					+ " " + Version.copyright);
			}
			else
				break;
			arg0++;
		}

/*		UIManager.LookAndFeelInfo[] lafInfos
			= UIManager.getInstalledLookAndFeels();
		for (int i = 0; i < lafInfos.length; i++) {
			System.out.println(
				"L&F " + i + ": " + lafInfos[i].getName()
				+ " " + lafInfos[i].getClassName());
			if (lafInfos[i].getName().indexOf("Windows") >= 0)
				try {
					UIManager.setLookAndFeel(lafInfos[i].getClassName());
				}
				catch (Exception e) { }
		}
*/
		if (!stdout)
		try {
			String LnF = UIManager.getSystemLookAndFeelClassName();
			System.out.println("L&F: " + LnF);
			UIManager.setLookAndFeel(LnF);
		}
		catch (Exception e) { }

		frame = new JFrame("MD5Verify");
		Rectangle screenRect
			= frame.getGraphicsConfiguration().getBounds();
		int width = 640;
		int height = 480;
		frame.setBounds(screenRect.x + (screenRect.width - width) / 2,
			screenRect.y + (screenRect.height - height) / 2,
			width, height);
		frame.addWindowListener(new java.awt.event.WindowAdapter() {
			public void windowClosing(java.awt.event.WindowEvent e) {
				System.exit(0);
			}
		});
		frame.setJMenuBar(createMenuBar());

		// Log
		textArea = new JTextArea();
		scrollPane = new JScrollPane(textArea);

		// Errors
		errorArea = new JTextArea();

/*		JPanel logPanel = new JPanel();
		logPanel.setAlignmentX(Box.LEFT_ALIGNMENT);
		logPanel.setBorder(
			BorderFactory.createTitledBorder(
				BorderFactory.createEtchedBorder(), "Log"));
		logPanel.setLayout(new BoxLayout(logPanel, BoxLayout.Y_AXIS));
		logPanel.add(tabbedPane);
*/
		MyBox vbox = new MyBox(BoxLayout.Y_AXIS);
		vbox.setAlignmentX(Box.LEFT_ALIGNMENT);
		vbox.add(createVerifyPanel());
		vbox.add(createResultPanel());
//		vbox.add(logPanel);

		frame.getContentPane().add(vbox);
		if (!stdout)
			frame.setVisible(true);

		treeMap = new java.util.TreeMap();

		if (args.length > arg0)
			for (int i = arg0; i < args.length; i++)
				verify(new File(args[i]));
		else {
/*			File[] roots = File.listRoots();
			for (int i = 0; i < roots.length; i++) {
				print("root " + roots[i] + "\n");
				verify(roots[i]);
*/
			java.net.URL url = new Integer(0).getClass().getClassLoader()
				.getSystemResource("MD5Verify.class");
//			println("" + url);
//			println(url.getPath());

			String URLStr = url.toString();
			if (URLStr.startsWith("jar:"))
				URLStr = URLStr.substring(4);
			if (URLStr.startsWith("file:"))
				URLStr = URLStr.substring(5);
			int pos = URLStr.indexOf(".jar!");
			if (pos > 0)
				URLStr = URLStr.substring(0, pos + 4);

			File file = new File(URLStr);
			verify(file.getParentFile());
		}

		println("");
		println("" + OKs + " MD5 checksums / "
			+ bytesOK / 1024 / 1024 + " MB are identical");
		println("" + notFoundErrors + " files not found");
		println("" + readErrors + " read errors");
		println("" + MD5Errors + " MD5 checksums / "
			+ bytesError / 1024 / 1024 + " MB differ");
		println("Done");

		if (stdout)
			println("");
	}

	static void print(String str) {
		if (stdout) {
			System.out.print(str);
			System.out.flush();
			return;
		}

		textArea.append(str);

		// scroll down if the user has not moved the cursor
		int p = textArea.getCaretPosition();
		if (p == textArea.getText().length() - str.length())
			textArea.setCaretPosition(p + str.length());
	}

	static void println(String str) {
		print(str + "\n");
	}

	static void printlnError(String str) {
		if (!stdout)
			errorArea.append(str + "\n");
	}

	static void showErrorNum() {
		notFoundErrorLabel.setText(
			format(notFoundErrors, 5) + " files not found");
		MD5ErrorLabel.setText(
			format(MD5Errors, 5) + " MD5 sums / "
				+ bytesError / 1024 / 1024 + " MB differ");
		readErrorLabel.setText(
			format(readErrors, 5) + " read errors");
	}

	static void verify(File file) throws Exception {
		if (!file.isDirectory()) {
			verifyFile(file);
			return;
		}

		File dir = new File(file, "MD5Sums");
		if (dir.exists()) {
			println("directory to verify = " + dir);
			String[] names = dir.list(
				new FilenameFilter() {
				public boolean accept(File dir, String name) {
					return name.endsWith(".md5");
				}
			});

			if (names != null) {
				java.util.Arrays.sort(names);

				for (int i = names.length - 1; i >= 0; i--) {
					println("");
					println(names[i] + " -->");
					verify(new File(dir, names[i]));
				}
			}
		}

		dir = file;
		println("directory to verify = " + dir);
		String[] names = dir.list(
			new FilenameFilter() {
			public boolean accept(File dir, String name) {
				return name.endsWith(".md5");
			}
		});

		if (names == null)
			return;

		java.util.Arrays.sort(names);

		for (int i = names.length - 1; i >= 0; i--) {
			println("");
			println(names[i] + " -->");
			verify(new File(dir, names[i]));
		}

		verifyBox.setEnabled(false);
		fileLabel.setEnabled(false);
		progressBar.setEnabled(false);
		speedLabel.setEnabled(false);
	}

	static void verifyFile(File file) throws Exception {
		BufferedReader reader = new BufferedReader(
			new InputStreamReader(
				new FileInputStream(file), "UTF-8"));

		try {
			int cacheSize = 256 * 2048;
			reader.mark(cacheSize);
			for (int i = 0; i < cacheSize - 2; i++)
				if (reader.read() < 0)
					break;
			reader.reset();
		}
		catch (Exception e) {
			println("Exception: " + e.getMessage());
			return;
		}

		speedometer = new Speedometer();

		while (true) {
			String line = reader.readLine();
			if (line == null)
				break;

			String MD5 = line.substring(0, 32);
			String fileName = line.substring(34);
			print(fileName + "... ");
			File file2 = new File(file.getParent(), fileName);
			if (file.getParentFile().getName().equals("MD5Sums"))
				file2 = new File(file.getParentFile().getParent(), fileName);
			if (treeMap.containsKey(fileName)) {
				if (((String) treeMap.get(fileName)).equals(MD5))
					println("OK");
				else
					println("hidden version - cannot verify");
			}
			else
			if (!file2.exists()) {
				println("--> does not exist");
				printlnError(fileName + " does not exist");
				notFoundErrors++;
				showErrorNum();
			}
			else
			try {
				if (getMD5Sum(file2).equals(MD5)) {
					println("OK");
					OKs++;
					bytesOK += file2.length();
					OKLabel.setText(format(OKs, 5) + " files / "
						+ bytesOK / 1024 / 1024 + " MB seem to be OK");
				}
				else {
					println("--> MD5 differs");
					printlnError(fileName + " MD5 differs");
					MD5Errors++;
					bytesError += file2.length();
					showErrorNum();
				}
			}
			catch (Exception e) {
				println("--> " + e.getMessage());
				printlnError(fileName + " " + e.getMessage());
				if (e instanceof IOException) {
					readErrors++;
					showErrorNum();
				}
			}

			treeMap.put(fileName, MD5);
		}
	}

	/**
	 * Since pre 1.4 Java does not like a "new Box(BoxLayout.Y_AXIS)"
	 */
	private static class MyBox extends JPanel {
		MyBox(int axis) {
			setLayout(new BoxLayout(this, axis));
		}
	}
}
