/** 

 File Synchronization Program:
 
  History:
    2002.1.10   ディレクトリ階層のコピー
    2001.12.30  シンクしたフォルダーの履歴を残すようにした	
**/

import javax.swing.*;
import javax.swing.event.*;
import javax.swing.tree.*;

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;

/** Syncのユーザ設定 **/
class SyncOptions {
	boolean valid = true;
	String name;
	File left;
	File right;
	String ignore;
	String[] ilist;
	boolean force;
	static boolean case_sensitive;
	long terror = 0;
	String synchist[][];
	boolean debug = false;
	boolean dryrun = false;
	boolean dolog = true;

	public SyncOptions(){
		left = new File("C:");
		right = new File("C:");
		synchist = new String[20][2]; // 20: history size
	}

	public SyncOptions(String fname) {
		this();
		load(fname);
	}

	public SyncOptions copy() {
		SyncOptions o = new SyncOptions();
		o.valid = valid;
		o.left = left;
		o.right = right;
		o.ignore = ignore;
		o.ilist = null;
		o.force = force;
		o.case_sensitive = case_sensitive;
		o.debug = debug;
		o.dryrun = dryrun;
		o.dolog = dolog;
		o.terror = terror;
		for (int i = 0; i < synchist.length; i++) {
			o.synchist[i][0] = synchist[i][0];
			o.synchist[i][1] = synchist[i][1];
		}
		return o;
	}

	/** 無視すべきファイル名リストに合致するかチェック
	 *  @return true:無視すべき名前のとき
	 */
	public boolean checkIgnore(String s) {
		if (ignore == null || s == null) return false;
		if (ilist == null) {
			try {
				StringTokenizer tk = new StringTokenizer(ignore, ";");
				ilist = new String[tk.countTokens()];
				for (int i = 0; i < ilist.length; i++) {
					ilist[i] = tk.nextToken();
					Sync.dbgout("token"+i + ": " + ilist[i]); //XXXX
				}
			} catch (Exception ex) {}
		}
		for (int i = 0; i < ilist.length; i++) {
			if (ilist[i] == null) continue;
			if (ilist[i].equals(s)) {
				return true;
			} else if (ilist[i].startsWith("*")) {
				if (s.endsWith(ilist[i].substring(1, ilist[i].length())))
					return true;
			} else if (ilist[i].endsWith("*")) {
				if (s.startsWith(ilist[i].substring(0, ilist[i].length()-1)))
					return true;
			}
		}
		return false;
	}

	public void save(String fname) {
		checkIgnore("aa");//XXXX
		try {
			PrintWriter out
				= new PrintWriter(new BufferedWriter(new FileWriter(fname)));
			out.println("%name " + name);
			out.println("%dir1 " + left.getPath());
			out.println("%dir2 " + right.getPath());
			if (ignore != null) out.println("%igno " + ignore);
			out.println("%forc " + force);
			out.println("%case_sensitive " + case_sensitive);
			out.println("%debug " + debug);
			out.println("%dryrun " + dryrun);
			out.println("%terr " + terror);
			out.println("%dolog " + dolog);
			// sync pair history
			for (int i = synchist.length-1; i >= 0; i--) {
				if (!is_empty(i)) {
					out.println("%hdir1 " + synchist[i][0]);
					out.println("%hdir2 " + synchist[i][1]);
				}
			}
			out.close();
		} catch (Exception ex) {System.out.println(""+ex);}
	}
	
	public boolean is_empty(int i) {
		return (synchist[i][0] == null || synchist[i][1] == null ||
			synchist[i][0].length() <= 0 || synchist[i][1].length() <= 0);
	}

	public void load(String fname) {
		try {
			BufferedReader in = new BufferedReader(new FileReader(fname));
			String l = null;

			while ((l = in.readLine()) != null) {
				if (l.startsWith("%name ")) {
					name = l.substring(6, l.length());
				} else if (l.startsWith("%dir1 ")) {
					left = new File(l.substring(6, l.length()));
				} else if (l.startsWith("%dir2 ")) {
					right = new File(l.substring(6, l.length()));
				} else if (l.startsWith("%forc ")) {
					if (l.endsWith("true")) force = true;
				} else if (l.startsWith("%case_sensitive ")) {
					case_sensitive = l.endsWith("true");
				} else if (l.startsWith("%debug ")) {
					debug = l.endsWith("true");
				} else if (l.startsWith("%dryrun ")) {
					dryrun = l.endsWith("true");
				} else if (l.startsWith("%dolog ")) {
					dolog = l.endsWith("true");
				} else if (l.startsWith("%igno ")) {
					ignore = l.substring(6, l.length());
				} else if (l.startsWith("%terr ")) {
					terror = Long.parseLong(l.substring(6, l.length()));
				} else if (l.startsWith("%hdir1 ")) {
					String s1 = l.substring(7, l.length());
					l = in.readLine();
					if (l.startsWith("%hdir2 ")) {
						String s2 = l.substring(7, l.length());
						addHistory(s1, s2);
					}
				}
			}
			in.close();
		} catch (Exception ex) {System.out.println(""+ex);}
	}

	/** 履歴に追加 **/	
	public void addHistory(String s1, String s2) {
		//System.out.println("addHistory " + s1 + "," + s2);//XXXX
		
		if (s1 == null || s1.length() <= 0 || s2 == null || s2.length() <= 0)
			return;
		for (int i = 0; i < synchist.length; i++) {
			if (synchist[i][0] != null && synchist[i][1] != null &&
			    synchist[i][0].equals(s1) && synchist[i][1].equals(s2))
			    return; // already listed
		}
		for (int i = synchist.length-1; i > 0; i--) {
			synchist[i][0] = synchist[i-1][0];
			synchist[i][1] = synchist[i-1][1];
		}
		synchist[0][0] = s1;
		synchist[0][1] = s2;
		
		//System.out.println("addHistory " + numHistory() + " " + s1 + "==" + s2);
	}
	
	public int numHistory() {
		int n = 0;
		for (int i = 0; i < synchist.length; i++) {
			if (!is_empty(i)) n++;
		}
		return n;
	}
}

/** SyncOptions を変更するためのダイアローグボックス **/
class OptionDialog extends JDialog implements ActionListener  {
	Container pane;
	JButton can_b, ok_b, left_b, right_b;
	JComboBox set_cb;
	JTextField ignore_tf, left_tf, right_tf, terror_tf;
	JCheckBox force_cb, case_cb, debug_cb, dryrun_cb, dolog_cb;
	SyncOptions opt;
	JComboBox history_cb;
	String history[][];
	String hist_menu[];

	public JButton addButton(Container pane, String title) {
		JButton btn = new JButton(title);
		btn.addActionListener(this);
		if (pane != null) pane.add(btn);
		return btn;
	}

	/** 履歴ComboBoxをopt.synchistにあわせて更新 **/
	public void updateHistMenu() {
		history_cb.removeAllItems();
		
		int n = opt.numHistory();
		//System.out.println("history length = " + n);//XXX
			
		if (n <= 0) return;
		
		history = new String[n][2];
		hist_menu = new String[n];

		int j = 0;		
		//System.out.println("updateHistMenu " + opt.synchist.length);//XXX
		for (int i = 0; i < opt.synchist.length; i++) {
			if (!opt.is_empty(i)) {
				try {
				history[j][0] = opt.synchist[i][0];
				history[j][1] = opt.synchist[i][1];
				hist_menu[j] = history[j][0] + " - " + history[j][1];
				j++;
				} catch (Exception e) {
					Sync.dbgout("updateHist " + e + "i=" + i + " j=" + j);
				}
			}
		}

		history_cb.setEditable(true);
		for (int i = hist_menu.length-1; i >= 0; i--) {
			history_cb.addItem(hist_menu[i]);
			Sync.dbgout("addMenu " + i + " " + hist_menu[i]);//XXX
		}
		history_cb.setEditable(false);
	}
	
	public void selectFromHistory(String s) {
		for (int i = 0; i < hist_menu.length; i++) {
			if (s.equals(hist_menu[i])) {
				left_tf.setText(history[i][0]);
				right_tf.setText(history[i][1]);
				opt.left = new File(left_tf.getText());
				opt.right = new File(right_tf.getText());
				return;
			}
		}
	}

	public void actionPerformed(ActionEvent e) {
		Object o = e.getSource();
		if (o == ok_b) {
			opt.valid = true;
			// widgetの設定をoptに反映させる
			//opt.name = set_cb.getText();
			opt.ignore = ignore_tf.getText();
			opt.force = force_cb.getSelectedObjects() != null;
			opt.left = new File(left_tf.getText());
			opt.right = new File(right_tf.getText());
			opt.ilist = null;
			opt.debug = debug_cb.getSelectedObjects() != null;
			opt.dryrun = dryrun_cb.getSelectedObjects() != null;
			opt.dolog = dolog_cb.getSelectedObjects() != null;
			opt.addHistory(left_tf.getText(), right_tf.getText());
			try {
				opt.terror = Long.parseLong(terror_tf.getText()) * 1000L;
			} catch (Exception ex) {}
			dispose();
		} else if (o == can_b) {
			opt.valid = false;
			dispose();
		} else if (o == left_b) {
			File f = Sync.chooseDirectory(this);
			if (f != null)
				left_tf.setText(f.getPath());
		} else if (o == right_b) {
			File f = Sync.chooseDirectory(this);
			if (f != null)
				right_tf.setText(f.getPath());
		} else if (o == history_cb) {
			Sync.dbgout("history_cb pressed");
			String s = (String)history_cb.getSelectedItem();
			selectFromHistory(s);
		}
	}

	public static JPanel addRowPanel(JPanel parent) {
		JPanel cp = new JPanel();
		cp.setLayout(new FlowLayout(FlowLayout.LEFT, 3, 2));
		if (parent != null) parent.add(cp);
		return cp;
	}

	public OptionDialog(JFrame owner, String title, SyncOptions opt) {
		super(owner, title, true); // modal dialog
		this.opt = opt;
		pane = getContentPane();
		pane.setLayout(new BorderLayout());

		JPanel center = new JPanel();
		center.setLayout(new GridLayout(0,1));

		/*
		JPanel cp = addRowPanel(center);
		cp.add(new JLabel("Settings Name:"));
		String scom[] = {"eDesk.new", "Sync", "My Documents"};
		set_cb = new JComboBox(scom);
		cp.add(set_cb);
		*/

		JPanel cp = addRowPanel(center);
		cp.add(new JLabel("Folder1:"));
		left_tf = new JTextField(opt.left.getPath(), 20);  cp.add(left_tf);
		left_b = addButton(cp, "参照...");

		cp = addRowPanel(center);
		cp.add(new JLabel("Folder2:"));
		right_tf = new JTextField(opt.right.getPath(), 20);  cp.add(right_tf);
		right_b = addButton(cp, "参照...");

		cp = addRowPanel(center);
		cp.add(new JLabel("History:"));
		String dummy[] = {"dummy"};
		history_cb = new JComboBox(dummy);
		updateHistMenu();
		history_cb.addActionListener(this);
		cp.add(history_cb);

		cp = addRowPanel(center);
		cp.add(new JLabel("IgnoreFiles:"));
		ignore_tf = new JTextField(opt.ignore, 25); cp.add(ignore_tf);

		cp = addRowPanel(center);
		force_cb = new JCheckBox("無変更のファイルもリストする", opt.force);
		cp.add(force_cb);
		
		cp = addRowPanel(center);
		case_cb = new JCheckBox("ファイル名の大文字･小文字を区別する", opt.case_sensitive);
		cp.add(case_cb);

		cp = addRowPanel(center);
		cp.add(new JLabel("時間誤差"));
		terror_tf = new JTextField(""+(opt.terror / 1000), 4);
		cp.add(terror_tf);
		cp.add(new JLabel("秒まで許容"));
		
		cp = addRowPanel(center);
		debug_cb = new JCheckBox("Debug out", opt.debug);
		dryrun_cb = new JCheckBox("Dryrun", opt.dryrun);
		dolog_cb = new JCheckBox("Log (log.txt)を残す", opt.dolog);
		cp.add(debug_cb);
		cp.add(dryrun_cb);
		cp.add(dolog_cb);

		JPanel bottom = new JPanel();
		bottom.setLayout(new FlowLayout(FlowLayout.RIGHT, 3, 4));

		can_b = addButton(bottom, "Cancel");
		ok_b = addButton(bottom, "OK");

		pane.add(center, BorderLayout.CENTER);
		pane.add(bottom, BorderLayout.SOUTH);

		this.pack();

		Sync.dbgout("option dialog");
	}
}

/** コピーすべきファイルの対。
 *   TreeNode　に対応している。
 **/
class FilePair {
	boolean leaf;  // f1, f2 are files?
	File f1, f2, dir1, dir2;
	int action;
	String label = "  ??  ";
	static final int NON = 0;
	static final int L2R = 1;
	static final int R2L = 2;
	static final int EQ = 3;
	static final String[] labels = 
	    {"  ???  ", "  -->  ", "  <--  ", "  ===  "};
	public FilePair(File dir1, File f1, File dir2, File f2, boolean leaf, int action) {
		this.f1 = f1;
		this.f2 = f2;
		this.dir1 = dir1;
		this.dir2 = dir2;
		
		if (f1 != null)
			this.f1 = new File(dir1, f1.getName());
		if (f2 != null)
			this.f2 = new File(dir2, f2.getName());
		this.action = action;
		this.leaf = leaf;
		updateLabel();
	}

	public DefaultMutableTreeNode makeNode() {
		return new DefaultMutableTreeNode(this);
	}

	public void updateLabelOld() {
		try {
			String s1 = (f1 != null) ?
					(f1.isDirectory() ? f1.getPath() : f1.getName()) : "        ";
			String s2 = (f2 != null) ?
					(f2.isDirectory() ? f2.getPath() : f2.getName()) : "        ";
			label = s1 + labels[action] + s2;
		} catch (Exception e) { label = "*"+e;}
	}
	
	public void updateLabel() {
		try {
			String s1 = (f1 != null) ? f1.getName() : "";
			String s2 = (f2 != null) ? f2.getName() : "";
			label = s1 + labels[action] + s2;
		} catch (Exception e) { label = "*"+e;}
	}

	public String toString() {
		return label;
	}

	public static String q(String s) {
		return "\"" + s + "\"";
	}

	/* obsolete */
	/*
	public String comLine() {
		if (f1 == null && f2 == null) {
			return null;
		} else if (f1 != null && f2 == null) {
			if (dir2 != null && dir2.isDirectory() && action == L2R) {
				if (f1.isDirectory())
					return "mkdir " + q(dir2.getPath() + "\\" + f1.getName());
				else
					return "copy " + q(f1.getPath()) + " " + q(dir2.getPath());
			} else {
				System.out.println("error1 " + f1 + " (" + dir2 + ")");
				return null;
			}
		} else if (f1 == null && f2 != null) {
			if (dir1 != null && dir1.isDirectory() && action == R2L) {
				if (f2.isDirectory())
					return "mkdir " + q(dir1.getPath() + "\\" + f2.getName());
				else
					return "copy " + q(f2.getPath()) + " " + q(dir1.getPath());
			} else {
				System.out.println("error2 (" + dir1 + ") " + f2);
				return null;
			}
		} else {
			if (f1.isDirectory() && f2.isDirectory()) {
				return null;
			} else if (f1.isDirectory() != f2.isDirectory()) {
				System.out.println("error3 " + f1 + " " + f2);
				return null;
			} else { // both are files
				switch (action) {
				case L2R:
					return "copy " + q(f1.getPath()) + " " + q(f2.getPath());
				case R2L:
					return "copy " + q(f2.getPath()) + " " + q(f1.getPath());
				default:
					return null;
				}
			}
		}
	}
	*/

	public boolean doAction(boolean dryrun, PrintWriter log) {
		/*
		if (dryrun) try {
			System.out.println("# " + dir1.getPath()+"/" + 
				(f1 != null ? f1.getName() : "null")  + labels[action] +
				dir2.getPath() + "/" + 
				(f2 != null ? f2.getName() : "null")
			);
			return true;
		} catch (Exception e) {
			Sync.dbgout("doAction:" + e);
			return true;
		}
		*/

		if (f1 == null && f2 == null) {
			return false;
		} else if (f1 != null && f2 == null) {
			if (dir2 != null && dir2.isDirectory() && action == L2R)
				return gcopy(f1, dir2, dryrun,log);
		} else if (f1 == null && f2 != null) {
			if (dir1 != null && dir1.isDirectory() && action == R2L)
				return gcopy(f2, dir1, dryrun,log);
		} else {
			if (f1.isDirectory() && f2.isDirectory()) {
				return true;
			} else if (f1.isDirectory() != f2.isDirectory()) {
				System.out.println("error3 " + f1 + " " + f2);
				return false;
			} else { // both are files
				switch (action) {
				case L2R: return gcopy(f1, f2, dryrun, log);
				case R2L: return gcopy(f2, f1, dryrun, log);
				}
			}
		}
		return true;
	}


	private static final int BUFSIZE = 65536;
	/** from からtoへファイルをコピーする。toはすでに存在するファイルでもよい。
	 *  toはディレクトリでもよい（そのディレクトリの下にfromと同名のファイルへ
	 *　コピーする。
	 *  (File#renameToがあるのにFile#copyが存在しないので自前で作った)
	 *
	 *  @return true - コピー成功, false - 失敗
	 **/
	public static boolean copy(File from, File to) {
		BufferedInputStream in = null;
		BufferedOutputStream out = null;
		boolean result = true;

		//Sync.dbgout("copy: " + from + " " + to);//XXXX
		//try {Thread.sleep(500);} catch (Exception ex){}

		try {
			if (!from.canRead() || from.isDirectory()) return false;
			if (to.isDirectory())
				to = new File(to, from.getName());
			long t = from.lastModified();

			in = new BufferedInputStream(new FileInputStream(from));
			out = new BufferedOutputStream(new FileOutputStream(to));
			byte[] buf = new byte[BUFSIZE];
			for (long len = from.length(); len > 0; ) {
				int n = in.read(buf, 0, (int)(len > BUFSIZE ? BUFSIZE : len));
				if (n < 0) break;
				out.write(buf, 0, n);
				len -= n;
			}
			out.close();
			to.setLastModified(t); // 更新時刻を元ファイルと同じにする
		} catch (Exception e) {
			System.out.println("fileCopy: " + from + "-->" + to);
			System.out.println(e.getMessage());
			Sync.error_message =
				"" + from+ "を" + to + "に転送中に以下のエラー発生:\n" +
				e.getMessage();
			result = false;
		}
		try {in.close();} catch (Exception ex){}
		try {out.close();} catch (Exception ex){}
		return result;
	}

	/** generic copy
	    from がディレクトリのときはto以下にmkdirする。
	**/	
	public static boolean gcopy(File from, File to, boolean dryrun, PrintWriter log) {
		if (from.isDirectory() && to.isDirectory()) { // mkdir
			return mkdir(new File(to, from.getName()), dryrun, log);
		}
		if (log != null) {
			log.println("copy " + from + " " + to);
		}
		if (dryrun) {
			Sync.dbgout("#copy " + from + "-->" + to);
			return true;
		}
		return copy(from, to);
	}

	public static boolean mkdir(File newdir, boolean dryrun, PrintWriter log) {
		if (log != null) {
			log.println("mkdir " + newdir);
		}
		if (dryrun) {
			Sync.dbgout("#mkdir " + newdir);
			return true;
		}
		return newdir.mkdir();
	}
}

/**
 *  Sync -- ファイル同期ツール
 *  @version $Id: Sync.java,v 1.1 1999/01/02 03:35:30 user Exp $
 */
public class Sync extends JPanel implements ActionListener, Runnable {
	SyncOptions opt;
	JPanel pane;
	JButton left_b, right_b, dir_b, prescan_b, start_b, stop_b;
	Color dir_back;
	JButton rem_b, option_b;
	JProgressBar pbar;
	JTree tree;
	JScrollPane spane;
	JLabel info_l;
	int direction = BOTH; // 0, 1, 2;
	Icon left_i, right_i, both_i;
	DefaultMutableTreeNode top;
	DefaultTreeModel top_m;
	JFrame frame;
	boolean stop_copy = false;
	boolean stop_copy_confirmed = false;

	int job_type = 0;
	public static final int PRESCAN = 1;
	public static final int DOSYNC = 2;

	public static final int LEFT = 0x2;
	public static final int BOTH = 0x3;
	public static final int RIGHT = 0x1;

	public void message(String s) {
		info_l.setText(s);
	}

	private void needRescan() {
		pbar.setValue(0);
		start_b.setEnabled(false);
		message("press 'Prescan' to refresh the list.");
	}



	public void actionPerformed(ActionEvent e) {
		Object o = e.getSource();
		if (o == dir_b) {
			switch (direction) {
			case LEFT:
				direction = BOTH;
				dir_b.setIcon(both_i); break;
			case BOTH:
				direction = RIGHT;
				dir_b.setIcon(right_i); break;
			case RIGHT:
				direction = LEFT;
				dir_b.setIcon(left_i); break;
			}
			needRescan();
		} else if (o == left_b) {
			File f = chooseDirectory(this);
			if (f != null) {
				opt.left = f;
				left_b.setText(f.getPath());
				needRescan();
			}
		} else if (o == right_b) {
			File f = chooseDirectory(this);
			if (f != null) {
				opt.right = f;
				right_b.setText(f.getPath());
				needRescan();
			}
		} else if (o == prescan_b) {
			if (opt.left == null || opt.right == null) {
				JOptionPane.showMessageDialog(null,
						"Please select folders before doing prescan.",
						"alert", JOptionPane.ERROR_MESSAGE);
			} else {
				startJob(PRESCAN);
			}
		} else if (o == start_b) {
			if (opt.left == null || opt.right == null) {
				JOptionPane.showMessageDialog(null,
					"Please select folders before doing sync.",
					"alert", JOptionPane.ERROR_MESSAGE);
			} else {
				startJob(DOSYNC);
			}
		} else if (o == stop_b) {
			stop_copy = true;
		} else if (o == rem_b) {
			doRemove();
		} else if (o == option_b) {
			doOptions();
		}
	}

	static String error_message;


	/** 時間のかかるジョブを別スレッドで実行 */
	public synchronized void startJob(int job) {
		this.job_type = job;
		Thread thread = new Thread(this);
		thread.start();
	}

	public synchronized void run() {
		error_message = "unknown reason";
		stop_copy_confirmed = stop_copy = false;
		stop_b.setEnabled(true); // 中断ボタン
		switch (job_type) {
		case PRESCAN:
			pbar.setValue(0);
			doPrescan();
			if (!stop_copy) start_b.setEnabled(true);
			break;
		case DOSYNC:
			Sync.dbgout("start sync...");//XXX
			copied_num = 0;
			pbar.setValue(0);
			
			PrintWriter log = null;
			if (opt.dolog) try {
				log = new PrintWriter(
						new BufferedWriter(new FileWriter("log.txt")));
			} catch (Exception e) {}
			doSync(top, opt.dryrun, log);
			if (log != null) try {
				log.close();
			} catch (Exception e) {}
			message("Sync done.  " + copied_num + " files/folders were copied");
			Sync.dbgout("..... done");//XXX
			break;
		}
		stop_b.setEnabled(false);
		if (stop_copy_confirmed) message("aborted.");
		if (blinked) dir_b.setBackground(dir_back);
	}

	public static File chooseDirectory(Component pane) {
		JFileChooser chooser = new JFileChooser();
		chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
		int return_val = chooser.showOpenDialog(pane);
		if (return_val == JFileChooser.APPROVE_OPTION)
			return chooser.getSelectedFile();
		return null;
	}

    public Sync() {
	    opt = new SyncOptions("sync.ini");

        left_b = new JButton(opt.left.getPath());
        left_b.addActionListener(this);
        left_b.setPreferredSize(new Dimension(240, 24));
        right_b = new JButton(opt.right.getPath());
        right_b.addActionListener(this);
		right_b.setPreferredSize(new Dimension(240, 24));

		left_i = new ImageIcon("Left.gif");
		both_i = new ImageIcon("Both.gif");
		right_i = new ImageIcon("Right.gif");
		dir_b = new JButton(both_i);  dir_back = dir_b.getBackground();
		dir_b.addActionListener(this);
		direction = BOTH;

        JPanel button_pane = new JPanel();
        button_pane.setLayout(new BorderLayout());
        button_pane.add(left_b, BorderLayout.WEST);
        button_pane.add(dir_b, BorderLayout.CENTER);
        button_pane.add(right_b, BorderLayout.EAST);

        JPanel button_pane2 = new JPanel();
        button_pane2.setLayout(new FlowLayout(FlowLayout.CENTER, 2, 2));
        prescan_b = new JButton("Prescan");
        start_b = new JButton("Start"); start_b.setEnabled(false);
        stop_b = new JButton("Stop");
        stop_b.setEnabled(false);

        prescan_b.setPreferredSize(new Dimension(80, 24));
        start_b.setPreferredSize(new Dimension(80, 24));
        stop_b.setPreferredSize(new Dimension(80, 24));

        button_pane2.add(prescan_b);
        prescan_b.addActionListener(this);
        button_pane2.add(start_b);
        start_b.addActionListener(this);
        button_pane2.add(stop_b);
        stop_b.addActionListener(this);

        pbar = new JProgressBar(JProgressBar.HORIZONTAL, 0, 100);
        pbar.setValue(0);

        JPanel tool_pane = new JPanel();
        tool_pane.setLayout(new BorderLayout());
        tool_pane.add(button_pane, BorderLayout.NORTH);
        tool_pane.add(button_pane2, BorderLayout.CENTER);
        tool_pane.add(pbar, BorderLayout.SOUTH);

		top = new DefaultMutableTreeNode("File List");
		top_m = new DefaultTreeModel(top);
		tree = new JTree(top_m);
		tree.setLargeModel(true); // 全アイテムが同じ高さ
        spane = new JScrollPane(tree);
        spane.setPreferredSize(new Dimension(320, 240));

        info_l = new JLabel("information");
        info_l.setPreferredSize(new Dimension(280, 20));
        rem_b = new JButton("Remove from list");
        rem_b.addActionListener(this);
        option_b = new JButton("Options...");
        option_b.addActionListener(this);

        JPanel bpane = new JPanel();
        bpane.setLayout(new BorderLayout());
        bpane.add(info_l, BorderLayout.WEST);
        bpane.add(option_b, BorderLayout.CENTER);
        bpane.add(rem_b, BorderLayout.EAST);

        // 最上位パネル
        this.setLayout(new BorderLayout());
        this.add(tool_pane, BorderLayout.NORTH);
        this.add(spane, BorderLayout.CENTER);
        this.add(bpane, BorderLayout.SOUTH);
    }

	/** prescanした内容（treenodeに保存されている）に従ってコピーを実行 **/
	public void doSync(MutableTreeNode dir, boolean dryrun, PrintWriter log) {
		if (stop_copy_confirmed && dir == null) return;
		for (int i = 0; i < dir.getChildCount(); i++) try {
			if (stop_copy_confirmed) return;
			DefaultMutableTreeNode node = (DefaultMutableTreeNode)dir.getChildAt(i);
			FilePair fp = (FilePair)node.getUserObject();
			if (fp == null) {
				dbgout("doSync: fp is null" + dir);
				continue;
			}

			if (checkAbort()) return;
			message(fp.toString());
			if (!fp.doAction(dryrun, log)) {
				int reply = JOptionPane.showConfirmDialog(null,
								error_message + "\n処理を中断しますか？",
								"abort?",
								JOptionPane.YES_NO_OPTION);
				if (reply == JOptionPane.YES_OPTION) {
					stop_copy = stop_copy_confirmed = true;
					return; // abort operation
				}
			}

			copied_num ++;
			try {
				pbar.setValue((copied_num * 100) / copy_num);
			} catch (Exception ex) {}
			//if (!node.isLeaf())
			doSync(node, dryrun, log);
		} catch (Exception ex) {
			dbgout("doSync:" + dir + " " + ex);
		}
	}

	private long last_time;
	private boolean blinked = false;

	private boolean checkAbort() {
		try {
			long t = System.currentTimeMillis();
			if (t - last_time > 300) {
				last_time = t;
				dir_b.setBackground(blinked ? dir_back : Color.orange);
				blinked = !blinked;
			}
		} catch (Exception e) {}


		if (!stop_copy) return false;
		int reply = JOptionPane.showConfirmDialog(null,
						"処理を中断しますか?", "abort?",
						JOptionPane.YES_NO_OPTION);
		if (reply == JOptionPane.YES_OPTION) {
			stop_copy = stop_copy_confirmed = true;
			return true;
		}
		stop_copy = stop_copy_confirmed = false;
		return false;
	}

	/** obsolete **/
	/*
	public void createCommand(PrintWriter out, MutableTreeNode dir) {
		if (dir == null) return;
		for (int i = 0; i < dir.getChildCount(); i++) {
			DefaultMutableTreeNode node = (DefaultMutableTreeNode)dir.getChildAt(i);
			FilePair fp = (FilePair)node.getUserObject();
			String com = fp.comLine();
			if (com != null) out.println(com);
			if (!node.isLeaf())
				createCommand(out, node);
		}
	}
	*/

	private long copy_size;
	private int copy_num;
	private int copied_num;
	private int scanned_num;
	private int tocopy_num;

	public void doPrescan() {
		message("prescanning...");

		top = new DefaultMutableTreeNode(
				opt.left.getPath() + "  =:=  " + opt.right.getPath());
		top_m = new DefaultTreeModel(top);
		tree.setModel(top_m);
		scanned_num = 0;
		tocopy_num = 0;
		copy_size = 0;
		
		if (opt.left == null) {
			message("left directory is not defined");
		} else if (opt.right == null) {
			message("right directory is not defined");
		} else if (!opt.left.isDirectory()) {
			message("" + opt.left + " is not a directory");
		} else if (!opt.right.isDirectory()) {
			message("" + opt.right + " is not a directory");
		} else {
			copy_num = prescan(top, opt.left, opt.right, true, true, 30);
			top_m.reload();
			copy_num = tocopy_num; // 2002.1.10
			message("" + copy_num + "files (" + (copy_size/1024) + "Kbytes) will be copied");
		}
	}

	private static File safeNext(Iterator it) {
		if (it != null && it.hasNext()) return (File)it.next();
		return null;
	}

	public void quit() {
		opt.save("sync.ini");
		System.exit(0);
	}

	/** 転送されるファイルの総量（バイト数）*/
	private static Comparator fcomparator;

	static {
		fcomparator = new Comparator() {
					public int compare(Object o1, Object o2) {
						return fcomp((File)o1, (File)o2);
					}
					public boolean equals(Object o2) {
						return this == o2;
					}
				};
	}

	/** file名称で比較するcomparator */
	private static int fcomp(File f1, File f2) {
		if (SyncOptions.case_sensitive) {
			return f1.getName().compareTo(f2.getName());
		} else {
			String s1 = f1.getName().toLowerCase();
			String s2 = f2.getName().toLowerCase();
			return s1.compareTo(s2);
		}
		// return f1.getName().compareTo(f2.getName());
	}

	/**
	 *  コピーすべきファイルを走査する
	 *  nest -- ディレクトリの深さ制限
	 *  @return コピーを要するファイルの個数
n	 */
	public int prescan(DefaultMutableTreeNode root, File dir1, File dir2, boolean dir1real, boolean dir2real, int nest) {
		if (stop_copy_confirmed || nest < 0) return 0;
		if (dir1real == false && dir2real == false) return 0;
		
		/*
		if ((dir1 != null && dir1real && !dir1.isDirectory()) ||
			(dir2 != null && dir2real && !dir2.isDirectory())) {
			System.out.println("Sync: not a directory:" + dir1 + " " + dir2);
			return 0;
		}
		*/
		int num = 0;

		SortedSet set1 = new TreeSet(fcomparator);
		SortedSet set2 = new TreeSet(fcomparator);
		SortedSet fset1 = new TreeSet(fcomparator);
		SortedSet fset2 = new TreeSet(fcomparator);

		// ファイルの一覧を取得
		if (dir1 != null && dir1real) {
			File[] list1 = dir1.listFiles();
			for (int i = 0; i < list1.length; i++) {
				if (opt.checkIgnore(list1[i].getName())) continue;
				if (list1[i].isDirectory())
					fset1.add(list1[i]);
				else
					set1.add(list1[i]);
			}
		}
		if (dir2 != null && dir2real) {
			File[] list2 = dir2.listFiles();
			for (int i = 0; i < list2.length; i++) {
				if (opt.checkIgnore(list2[i].getName())) continue;
				if (list2[i].isDirectory())
					fset2.add(list2[i]);
				else
					set2.add(list2[i]);
			}
		}

		// ファイルの付き合わせ
		Iterator it1 = set1.iterator();
		Iterator it2 = set2.iterator();

		
		File f1 = safeNext(it1);  // safeNext -- 最後はnullを返すnext
		File f2 = safeNext(it2);

		FilePair p = null;

		try {

		// file名リスト(it1, it2)を順にたどりながら、コピーすべきファイル
		// のペアとそのコピー方向(FilePairオブジェクト)を記録する。
		// 結果はrootに追加される。
		while (true) {
			if (checkAbort()) return num;

			dbgout("comp: " + f1 + "  " + f2);//XXXXX
			if (f1 == null && f2 == null) {  // 両方ともおわり
				break;
			} else if (f1 == null || (f2 != null && fcomp(f1, f2) > 0)) {
				if (opt.force || (direction & LEFT) != 0) {
					p = new FilePair(dir1, null,  dir2,f2, true, FilePair.R2L);
					root.add(p.makeNode());
					num++;
					tocopy_num++;
					copy_size += f2.length();
				}
				f2 = safeNext(it2);
			} else if (f2 == null || (f1 != null && fcomp(f1, f2) < 0)) {
				if (opt.force || (direction & RIGHT) != 0) {
					p = new FilePair(dir1, f1, dir2, null, true, FilePair.L2R);
					root.add(p.makeNode());
					num++;
					tocopy_num++;
					copy_size += f1.length();
				}
				f1 = safeNext(it1);
			} else if (f1 != null && f2 != null && fcomp(f1, f2) == 0) {
				// 同じファイル名
				int action = FilePair.EQ;
				if (Math.abs(f1.lastModified() - f2.lastModified()) <= opt.terror) {
					action = FilePair.EQ;   // same file
				} else if (f1.lastModified() < f2.lastModified()) {
					action = FilePair.R2L;  // f1 <== f2
				} else if (f1.lastModified() > f2.lastModified()) {
					action = FilePair.L2R;  // f1 ==> f2
				}
				if (opt.force ||
					(action == FilePair.R2L && (direction & LEFT) != 0) ||
					(action == FilePair.L2R && (direction & RIGHT) != 0)) {
					p = new FilePair(dir1, f1, dir2, f2, true, action);
					root.add(p.makeNode());
					num++;
					tocopy_num++;
					if (action == FilePair.R2L && (direction & LEFT) != 0)
						copy_size += f2.length();
					if (action == FilePair.L2R && (direction & RIGHT) != 0)
						copy_size += f1.length();
				}
				f1 = safeNext(it1);
				f2 = safeNext(it2);
			}
			scanned_num++;
			scanMessage();
		}

		} catch (Exception e) {System.out.println("xx" + e);}

		Sync.dbgout("Compare Folder");//XXXX
		

		// フォルダの処理
		it1 = fset1.iterator();
		it2 = fset2.iterator();
		f1 = safeNext(it1);
		f2 = safeNext(it2);
		try {

		while (true) {
			if (checkAbort()) return num;

			Sync.dbgout("Folder" + f1 + "--" + f2);//XXX
			if (f1 == null && f2 == null) {
				break;
			} else if (f1 == null || (f2 != null && fcomp(f1, f2) > 0)) {
				if (opt.force || (direction & LEFT) != 0) { // <--
					// dir1/f2 は存在しない
					File f1virt = new File(dir1, f2.getName());
					p = new FilePair(dir1, null, dir2, f2, false, FilePair.R2L);
					DefaultMutableTreeNode node = p.makeNode();
					root.add(node);
					int n = prescan(node, f1virt, f2, false, dir2real, nest-1);
					num += n + 1;
					tocopy_num++;
				}
				f2 = safeNext(it2);
			} else if (f2 == null || (f1 != null && fcomp(f1, f2) < 0)) {
				if (opt.force || (direction & RIGHT) != 0) { // -->
					p = new FilePair(dir1, f1, dir2, null, false, FilePair.L2R);
					DefaultMutableTreeNode node = p.makeNode();
					root.add(node);
					File f2virt = new File(dir2, f1.getName());
					int n = prescan(node, f1, f2virt, dir1real, false, nest-1);
					num += n + 1;
					tocopy_num ++;
				}
				f1 = safeNext(it1);
			} else if (f1 != null && f2 != null && fcomp(f1, f2) == 0) {
				// 同じフォルダ名の場合: 再帰的にprescanを適用
				DefaultMutableTreeNode node = new DefaultMutableTreeNode(
					new FilePair(dir1, f1, dir2, f2, false, FilePair.EQ));
				int n = prescan(node, f1, f2, dir1real, dir2real, nest-1);
				num += n;
				
				// フォルダ以下がすべてシンクされていたら、このノードは表示
				// する必要がないのでaddしない
				if (opt.force || n > 0)
					root.add(node);
				f1 = safeNext(it1);
				f2 = safeNext(it2);
			}
			
			scanned_num++;
			scanMessage();
		}

		} catch (Exception e) {System.out.println("folderscan:" + e);}

		top_m.reload();
		return num;
	}
	
	/** prescanの進捗表示 **/
	private void scanMessage() {
		message("scanning... " + tocopy_num + "/" + scanned_num);
		checkAbort();
	}
		

	/** 選択した項目をtreeから削除する */
	public void doRemove() {
		TreePath[] paths = tree.getSelectionPaths();
		for (int i = 0; i < paths.length; i++) {
			Sync.dbgout(paths[i].toString());
			DefaultMutableTreeNode node =
				(DefaultMutableTreeNode)paths[i].getLastPathComponent();
			Sync.dbgout("remove node: " + node);
			node.removeFromParent();
		}
		top_m.reload();
	}

	/** option設定ダイアローグボックスの生成と表示 **/
	public void doOptions() {
		SyncOptions opt_copy = opt.copy(); // 現状をコピー
		Dialog dialog = new OptionDialog(frame, "Sync Options", opt_copy);
		dialog.show();
		if (opt_copy.valid) {
			opt = opt_copy;
			left_b.setText(opt.left.getPath());
			right_b.setText(opt.right.getPath());
			dbg = opt.debug;
			opt.save("sync.ini");
			needRescan();
		}
	}

	static boolean dbg = true;
	public static void dbgout(String s) {
		if (dbg) System.out.println("|"+s);
	}

    public static void main(String[] args) {
	    String l_f = UIManager.getSystemLookAndFeelClassName();

	    if (args != null) {
		    for (int i = 0; i < args.length; i++) {
			    if (args[i].equals("-cross")) {
			    	l_f = UIManager.getCrossPlatformLookAndFeelClassName();
			    } else if (args[i].equals("-debug")) {
				    Sync.dbg = true;
				}
			}
		}

        try {UIManager.setLookAndFeel(l_f);} catch (Exception e) {}

        //Create the top-level container and add contents to it.
        JFrame frame = new JFrame("FileSync v020111");
        Sync app = new Sync();
        app.frame = frame;
        frame.getContentPane().add(app, BorderLayout.CENTER);

        //Finish setting up the frame, and show it.
        frame.addWindowListener(new WindowAdapter() {
            public void windowClosing(WindowEvent e) {
                System.exit(0);
            }
        });
        frame.pack();
        frame.setVisible(true);
    }
}

