/* $Log: DiffPrint.java,v $
 * Revision 1.2  2000/03/02  16:59:54  stuart
 * add GPL
 *
 */
package bmsi.util;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.Vector;
//import com.objectspace.jgl.predicates.UnaryPredicate;

interface UnaryPredicate {
	boolean execute(Object obj);
}

/** A simple framework for printing change lists produced by <code>Diff</code>.
    @see bmsi.util.Diff
    @author Stuart D. Gathman
    Copyright (C) 2000 Business Management Systems, Inc.
<p>
    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 1, or (at your option)
    any later version.
<p>
    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.
<p>
    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., 675 Mass Ave, Cambridge, MA 02139, USA.
 */
public class DiffPrint {
	/** A Base class for printing edit scripts produced by Diff.
		 This class divides the change list into "hunks", and calls
		 <code>print_hunk</code> for each hunk.  Various utility methods
		 are provided as well.
   */
	public static abstract class Base {
		protected Base(Object[] a,Object[] b) {
			outfile = new PrintWriter(new OutputStreamWriter(System.out));
			file0 = a;
			file1 = b;
		}

		public void setWriter(PrintWriter writer)
		{
			outfile = writer;
		}

		/** Set to ignore certain kinds of lines when printing
			 an edit script.  For example, ignoring blank lines or comments.
		*/
		protected UnaryPredicate ignore = null;

		/** Set to the lines of the files being compared.
		 */
		protected Object[] file0, file1;

		/** Divide SCRIPT into pieces by calling HUNKFUN and
			 print each piece with PRINTFUN.
			 Both functions take one arg, an edit script.

			 PRINTFUN takes a subscript which belongs together (with a null
			 link at the end) and prints it.  */
		public void print_script(Diff.change script) {
			Diff.change next = script;

			while (next != null)
			{
				Diff.change t, end;

				/* Find a set of changes that belong together.  */
				t = next;
				end = hunkfun(next);

				/* Disconnect them from the rest of the changes,
					making them a hunk, and remember the rest for next iteration.  */
				next = end.link;
				end.link = null;
				//if (DEBUG)
				//  debug_script(t);

				/* Print this hunk.  */
				print_hunk(t);

				/* Reconnect the script so it will all be freed properly.  */
				end.link = next;
			}
			outfile.flush();
		}

		/** Called with the tail of the script
			 and returns the last link that belongs together with the start
			 of the tail. */

		protected Diff.change hunkfun(Diff.change hunk) {
			return hunk;
		}

		protected int first0, last0, first1, last1, deletes, inserts;
		protected PrintWriter outfile;

		/** Look at a hunk of edit script and report the range of lines in each file
			 that it applies to.  HUNK is the start of the hunk, which is a chain
			 of `struct change'.  The first and last line numbers of file 0 are stored
			 in *FIRST0 and *LAST0, and likewise for file 1 in *FIRST1 and *LAST1. 
			 Note that these are internal line numbers that count from 0.

			 If no lines from file 0 are deleted, then FIRST0 is LAST0+1.

			 Also set *DELETES nonzero if any lines of file 0 are deleted
			 and set *INSERTS nonzero if any lines of file 1 are inserted.
			 If only ignorable lines are inserted or deleted, both are
			 set to 0.  */

		protected void analyze_hunk(Diff.change hunk) {
			int f0, l0 = 0, f1, l1 = 0, show_from = 0, show_to = 0;
			int i;
			Diff.change next;
			boolean nontrivial = (ignore == null);

			show_from = show_to = 0;

			f0 = hunk.line0;
			f1 = hunk.line1;

			for (next = hunk; next != null; next = next.link)
			{
				l0 = next.line0 + next.deleted - 1;
				l1 = next.line1 + next.inserted - 1;
				show_from += next.deleted;
				show_to += next.inserted;
				for (i = next.line0; i <= l0 && ! nontrivial; i++)
					if (!ignore.execute(file0[i]))
						nontrivial = true;
				for (i = next.line1; i <= l1 && ! nontrivial; i++)
					if (!ignore.execute(file1[i]))
						nontrivial = true;
			}

			first0 = f0;
			last0 = l0;
			first1 = f1;
			last1 = l1;

			/* If all inserted or deleted lines are ignorable,
				tell the caller to ignore this hunk.  */

			if (!nontrivial)
				show_from = show_to = 0;

			deletes = show_from;
			inserts = show_to;
		}

		protected abstract void print_hunk(Diff.change hunk);
    
		protected void print_1_line(String pre,Object linbuf) {
			outfile.println(pre + linbuf.toString());
		}

		/** Print a pair of line numbers with SEPCHAR, translated for file FILE.
			 If the two numbers are identical, print just one number.

			 Args A and B are internal line numbers.
			 We print the translated (real) line numbers.  */

		protected void print_number_range (char sepchar, int a, int b) {
			/* Note: we can have B < A in the case of a range of no lines.
				In this case, we should print the line number before the range,
				which is B.  */
			if (++b > ++a)
				outfile.print("" + a + sepchar + b);
			else
				outfile.print(b);
		}

		public static char change_letter(int inserts, int deletes) {
			if (inserts == 0)
				return 'd';
			else if (deletes == 0)
				return 'a';
			else
				return 'c';
		}
	}

	/** Print a change list in the standard diff format.
	 */
	public static class NormalPrint extends Base {

		public NormalPrint(Object[] a,Object[] b) {
			super(a,b);
		}

		/** Print a hunk of a normal diff.
			 This is a contiguous portion of a complete edit script,
			 describing changes in consecutive lines.  */

		@Override
		protected void print_hunk (Diff.change hunk) {

			/* Determine range of line numbers involved in each file.  */
			analyze_hunk(hunk);
			if (deletes == 0 && inserts == 0)
				return;

			/* Print out the line number header for this hunk */
			print_number_range (',', first0, last0);
			outfile.print(change_letter(inserts, deletes));
			print_number_range (',', first1, last1);
			outfile.println();

			/* Print the lines that the first file has.  */
			if (deletes != 0)
				for (int i = first0; i <= last0; i++)
					print_1_line ("< ", file0[i]);

			if (inserts != 0 && deletes != 0)
				outfile.println("---");

			/* Print the lines that the second file has.  */
			if (inserts != 0)
				for (int i = first1; i <= last1; i++)
					print_1_line ("> ", file1[i]);
		}
	}

	/** Prints an edit script in a format suitable for input to <code>ed</code>.
		 The edit script must be generated with the reverse option to
		 be useful as actual <code>ed</code> input.
   */
	public static class EdPrint extends Base {

		public EdPrint(Object[] a,Object[] b) {
			super(a,b);
		}

		/** Print a hunk of an ed diff */
		@Override
		protected void print_hunk(Diff.change hunk) {

			/* Determine range of line numbers involved in each file.  */
			analyze_hunk (hunk);
			if (deletes == 0 && inserts == 0)
				return;

			/* Print out the line number header for this hunk */
			print_number_range (',', first0, last0);
			outfile.println(change_letter(inserts, deletes));

			/* Print new/changed lines from second file, if needed */
			if (inserts != 0)
			{
				boolean inserting = true;
				for (int i = first1; i <= last1; i++)
				{
					/* Resume the insert, if we stopped.  */
					if (! inserting)
						outfile.println(i - first1 + first0 + "a");
					inserting = true;

					/* If the file's line is just a dot, it would confuse `ed'.
						So output it with a double dot, and set the flag LEADING_DOT
						so that we will output another ed-command later
						to change the double dot into a single dot.  */

					if (".".equals(file1[i]))
					{
						outfile.println("..");
						outfile.println(".");
						/* Now change that double dot to the desired single dot.  */
						outfile.println(i - first1 + first0 + 1 + "s/^\\.\\././");
						inserting = false;
					}
					else
						/* Line is not `.', so output it unmodified.  */
						print_1_line ("", file1[i]);
				}

				/* End insert mode, if we are still in it.  */
				if (inserting)
					outfile.println(".");
			}
		}
	}

	/** Read a text file into an array of String.  This provides basic diff
		 functionality.  A more advanced diff utility will use specialized
		 objects to represent the text lines, with options to, for example,
		 convert sequences of whitespace to a single space for comparison
		 purposes.
   */
	static String[] slurp(String file) throws IOException {
		BufferedReader rdr = new BufferedReader(new FileReader(file));
		Vector<String> s = new Vector<String>();
		for (;;) {
			String line = rdr.readLine();
			if (line == null) break;
			s.addElement(line);
		}
		String[] a = new String[s.size()];
		s.copyInto(a);
		return a;
	}

	public static void main(String[] argv) throws IOException {
		String[] a = slurp(argv[argv.length - 2]);
		String[] b = slurp(argv[argv.length - 1]);
		Diff d = new Diff(a,b);
		boolean edstyle = "-e".equals(argv[0]);
		Diff.change script = d.diff_2(edstyle);
		if (script == null)
			System.err.println("No differences");
		else {
			Base p;
			if (edstyle)
				p = new EdPrint(a,b);
			else
				p = new NormalPrint(a,b);
			p.print_script(script);
		}
	}
}
