/* *******************************************************************
 * Copyright (c) 2002 Palo Alto Research Center, Incorporated (PARC).
 * All rights reserved. 
 * This program and the accompanying materials are made available 
 * under the terms of the Common Public License v1.0 
 * which accompanies this distribution and is available at 
 * http://www.eclipse.org/legal/cpl-v10.html 
 *  
 * Contributors: 
 *     Xerox/PARC     initial implementation 
 * ******************************************************************/

package org.aspectj.testing.util;

import org.aspectj.bridge.IMessageHandler;
import org.aspectj.bridge.MessageUtil;
import org.aspectj.util.FileUtil;
import org.aspectj.util.LangUtil;
import org.aspectj.util.Reflection;

import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import jdiff.text.FileLine;
import jdiff.util.Diff;
import jdiff.util.DiffNormalOutput;
import junit.framework.Assert;
import junit.framework.TestCase;
import junit.runner.TestCaseClassLoader;

/** 
 * Things that junit should perhaps have, but doesn't.
 * Note the file-comparison methods require JDiff to run,
 * but JDiff types are not required to resolve this class.
 * Also, the bytecode weaver is required to compare class
 * files, but not to compare other files.
 */

public final class TestUtil {

    private TestUtil() {
        super();
    }

    // ---- arrays

    public static void assertArrayEquals(String msg, Object[] expected, Object[] found) {
        TestCase.assertEquals(msg, Arrays.asList(expected), Arrays.asList(found));
    }

    // ---- unordered

    public static void assertSetEquals(Collection expected, Collection found) {
        assertSetEquals(null, expected, found);
    }

    public static void assertSetEquals(String msg, Object[] expected, Object[] found) {
        assertSetEquals(msg, Arrays.asList(expected), Arrays.asList(found));
    }

    public static void assertSetEquals(
        String msg,
        Collection expected,
        Collection found) {
        msg = (msg == null) ? "" : msg + ": ";

        Set results1 = new HashSet(found);
        results1.removeAll(expected);

        Set results2 = new HashSet(expected);
        results2.removeAll(found);

        if (results1.isEmpty()) {
            TestCase.assertTrue(
                msg + "Expected but didn't find: " + results2.toString(),
                results2.isEmpty());
        } else if (results2.isEmpty()) {
            TestCase.assertTrue(
                msg + "Didn't expect: " + results1.toString(),
                results1.isEmpty());
        } else {
            TestCase.assertTrue(
                msg
                    + "Expected but didn't find: "
                    + results2.toString()
                    + "\nDidn't expect: "
                    + results1.toString(),
                false);
        }
    }

    // ---- objects

    public static void assertCommutativeEquals(Object a, Object b, boolean should) {
        TestCase.assertEquals(a + " equals " + b, should, a.equals(b));
        TestCase.assertEquals(b + " equals " + a, should, b.equals(a));
        assertHashEquals(a, b, should);
    }

    private static void assertHashEquals(Object s, Object t, boolean should) {
        if (should) {
            TestCase.assertTrue(
                s + " does not hash to same as " + t,
                s.hashCode() == t.hashCode());
        } else {
            if (s.hashCode() == t.hashCode()) {
                System.err.println("warning: hash collision with hash = " + t.hashCode());
                System.err.println("  for " + s);
                System.err.println("  and " + t);
            }
        }
    }
    
    // -- reflective stuff

	public static void runMain(String classPath, String className) {
		runMethod(classPath, className, "main", new Object[] { new String[0] });
	}


	public static Object runMethod(String classPath, String className, String methodName, Object[] args) {
		classPath += File.pathSeparator + System.getProperty("java.class.path");
		
		ClassLoader loader = new TestCaseClassLoader(classPath);
		
		Class c=null;
		try {
			c = loader.loadClass(className);
		} catch (ClassNotFoundException e) {
			Assert.assertTrue("unexpected exception: " + e, false);
		}
		return Reflection.invokestaticN(c, methodName, args);
	}

		
	/**
	 * Checks that two multi-line strings have the same value.
	 * Each line is trimmed before comparision
	 * Produces an error on the particular line of conflict
	 */    
    public static void assertMultiLineStringEquals(String message, String s1, String s2) {
    	try {
	    	BufferedReader r1 = new BufferedReader(new StringReader(s1));
	    	BufferedReader r2 = new BufferedReader(new StringReader(s2));
	    	
	    	
	    	List lines = new ArrayList();
	    	String l1, l2;
	    	
	    	int index = 1;
	    	while(true) {
	    	    l1 = readNonBlankLine(r1);
	    	    l2 = readNonBlankLine(r2);
	    	    if (l1 == null || l2 == null) break;
	    	    if (l1.equals(l2)) {
	    	        lines.add(l1);
	    	    } else {
	    	    	showContext(lines);
	    	        Assert.assertEquals(message +"(line " + index +")", l1, l2);
	    	    }
	    	    index++;
	    	}
	    	if (l1 != null) showContext(lines);
	    	Assert.assertTrue(message + ": unexpected " + l1, l1 == null);
	    	if (l2 != null) showContext(lines);
	    	Assert.assertTrue(message + ": unexpected " + l2, l2 == null);
    	} catch (IOException ioe) {
    		Assert.assertTrue(message + ": caught " + ioe.getMessage(), false);
    	}
    }
    
	private static void showContext(List lines) {
		int n = lines.size();
		for (int i = Math.max(0, n - 8); i < n; i++) {
			System.err.println(lines.get(i));
		}
	}  	
	
	private static String readNonBlankLine(BufferedReader r) throws IOException {
		String l = r.readLine();
		if (l == null) return null;
        l = l.trim();
        // comment to include comments when reading
        int commentLoc = l.indexOf("//");
        if (-1 != commentLoc) {
            l = l.substring(0, commentLoc).trim();
        }
		if ("".equals(l)) return readNonBlankLine(r);
        return l;
	}
    
    /**
     * If there is an expected dir, expect each file in its subtree
     * to match a corresponding actual file in the base directory.
     * This does NOT check that all actual files have corresponding
     * expected files.
     * This ignores directory paths containing "CVS".
     * @param handler the IMessageHandler sink for error messages
     * @param expectedBaseDir the File path to the directory
     *         containing expected files, all of which are compared
     *         with any corresponding actual files
     * @param actualBaseDir the File path to the base directory
     *         from which to find any actual files corresponding
     *         to expected files.
     * @return true if all files in the expectedBaseDir directory tree
     *          have matching files in the actualBaseDir directory tree.
     */
    public static boolean sameDirectoryContents(
        final IMessageHandler handler, 
        final File expectedBaseDir,
        final File actualBaseDir,
        final boolean fastFail) {
        LangUtil.throwIaxIfNull(handler, "handler");
        if (!FileUtil.canReadDir(expectedBaseDir)) {
            MessageUtil.fail(handler, " expected dir not found: " + expectedBaseDir);
            return false;
        }
        if (!FileUtil.canReadDir(actualBaseDir)) {
            MessageUtil.fail(handler, " actual dir not found: " + actualBaseDir);
            return false;
        }
        String[] paths = FileUtil.listFiles(expectedBaseDir);
        boolean result = true;
        for (int i = 0; i < paths.length; i++) {
            if (-1 != paths[i].indexOf("CVS")) {
                continue;
            }
            if (!sameFiles(handler, expectedBaseDir, actualBaseDir, paths[i]) && result) {
                result = false;
                if (fastFail) {
                    break;
                }
            }
        }
        return result;
    }
    
    //------------ File-comparison utilities (XXX need their own class...)
    /**
     * Test interface to
     * compare two files, line by line, and report differences as one FAIL message
     * if a handler is supplied.  This preprocesses .class files by disassembling.
     * @param handler the IMessageHandler for any FAIL messages (null to ignore)
     * @param expectedFile the File path to the canonical file
     * @param actualFile the File path to the actual file, if any
     * @return true if the input files are the same, based on per-line comparisons
     */
    static boolean sameFiles (
        IMessageHandler handler,
        File expectedFile, 
        File actualFile) {
        return doSameFile(handler, null, null, expectedFile, actualFile);
    }

    /**
     * Test interface to
     * compare two files, line by line, and report differences as one FAIL message
     * if a handler is supplied.  This preprocesses .class files by disassembling.
     * This method assumes that the files are at the same offset from two
     * respective base directories.
     * @param handler the IMessageHandler for any FAIL messages (null to ignore)
     * @param expectedBaseDir the File path to the canonical file base directory
     * @param actualBaseDir the File path to the actual file base directory
     * @param path the String path offset from the base directories
     * @return true if the input files are the same, based on per-line comparisons
     */
    static boolean sameFiles (
        IMessageHandler handler,
        File expectedBaseDir, 
        File actualBaseDir, 
        String path) {
        File actualFile = new File(actualBaseDir, path);
        File expectedFile = new File(expectedBaseDir, path);
        return doSameFile(handler, expectedBaseDir, actualBaseDir, expectedFile, actualFile);
    }

    /**
     * This does the work, selecting a lineator subclass and converting public
     * API's to JDiff APIs for comparison.
     * Currently, all jdiff interfaces are method-local, so this class will load 
     * without it; if we do use it, we can avoid the duplication.
     */
    private static boolean doSameFile(
        IMessageHandler handler,
        File expectedBaseDir, 
        File actualBaseDir,
        File expectedFile,
        File actualFile) {
        String path = expectedFile.getPath();
        // XXX permit user to specify lineator
        ILineator lineator = Lineator.TEXT;
        if (path.endsWith(".class")) {
            if (ClassLineator.haveDisassembler() ) {
                lineator = Lineator.CLASS;
            } else {
                MessageUtil.abort(handler, "skipping - dissassembler not available");
                return false;
            }
        }
        CanonicalLine[] actualLines = null;
        CanonicalLine[] expectedLines = null;
        try {
            actualLines = lineator.getLines(handler, actualFile, actualBaseDir); 
            expectedLines = lineator.getLines(handler, expectedFile, expectedBaseDir);
        } catch (IOException e) {
            MessageUtil.fail(handler, "rendering lines ", e);
            return false;
        }
        if (!LangUtil.isEmpty(actualLines) && !LangUtil.isEmpty(expectedLines)) {
            // here's the transmutation back to jdiff - extract if publishing JDiff
            CanonicalLine[][] clines = new CanonicalLine[][] { expectedLines, actualLines };
            FileLine[][] flines = new FileLine[2][];
            for (int i = 0; i < clines.length; i++) {
                CanonicalLine[] cline = clines[i];
                FileLine[] fline = new FileLine[cline.length];
                for (int j = 0; j < fline.length; j++) {
					fline[j] = new FileLine(cline[j].canonical, cline[j].line);
				}
                flines[i] = fline;
            }

            Diff.change edits = new Diff(flines[0], flines[1]).diff_2(false);
            if ((null == edits) || (0 == (edits.inserted + edits.deleted))) {
                // XXX confirm with jdiff that null means no edits
                return true;
            } else {
                //String m = render(handler, edits, flines[0], flines[1]);
                StringWriter writer = new StringWriter();
                DiffNormalOutput out = new DiffNormalOutput(flines[0], flines[1]);
                out.setOut(writer);
                out.setLineSeparator(LangUtil.EOL);
                try {
                    out.writeScript(edits);
                } catch (IOException e) {
                    MessageUtil.fail(handler, "rendering edits", e);
                } finally {
                    if (null != writer) {
                        try { writer.close(); } 
                        catch (IOException e) {
                            MessageUtil.fail(handler, "closing after rendering edits", e);
                        }
                    }
                }
                String message = "diff between " 
                    + path 
                    + " in expected dir " 
                    + expectedBaseDir
                    + " and actual dir " 
                    + actualBaseDir 
                    + LangUtil.EOL
                    + writer.toString(); 
                MessageUtil.fail(handler, message);
            }
        }
        return false;
    }
    

    /** component that reduces file to CanonicalLine[] */
    public static interface ILineator {
        /** Lineator suitable for text files */
        public static final ILineator TEXT = new TextLineator();

        /** Lineator suitable for class files (disassembles first) */
        public static final ILineator CLASS = new ClassLineator();

        /**
         * Reduce file to CanonicalLine[].
         * @param handler the IMessageHandler for errors (may be null) 
         * @param file the File to render
         * @param basedir the File for the base directory (may be null) 
         * @return CanonicalLine[] of lines - not null, but perhaps empty
         */
        CanonicalLine[] getLines(
            IMessageHandler handler, 
            File file, 
            File basedir) throws IOException;
    }
    
    /** alias for jdiff FileLine to avoid client binding */
    public static class CanonicalLine {
        public static final CanonicalLine[] NO_LINES = new CanonicalLine[0];
        
        /** canonical variant of line for comparison */
        public final String canonical;
        
        /** actual line, for logging */
        public final String line;
        public CanonicalLine(String canonical, String line) {
            this.canonical = canonical;
            this.line = line;
        }
        public String toString() {
            return line;
        }
    }
    
    private abstract static class Lineator implements ILineator {
        /**
         * Reduce file to CanonicalLine[].
         * @param handler the IMessageHandler for errors (may be null) 
         * @param file the File to render
         * @param basedir the File for the base directory (may be null) 
         */
        public CanonicalLine[] getLines(
            IMessageHandler handler, 
            File file, 
            File basedir) 
            throws IOException {
        
            if (!file.canRead() || !file.isFile()) {
                MessageUtil.error(handler, "not readable file: " + basedir + " - " + file);
                return null;
            }
            // capture file as FileLine[]
            InputStream in = null;
            /*String path = */FileUtil.normalizedPath(file, basedir);
            LineStream capture = new LineStream();
            try { 
                lineate(capture, handler, basedir, file);
            } catch (IOException e) {
                MessageUtil.fail(handler, 
                    "NormalizedCompareFiles IOException reading " + file, e);
                return null;
            } finally {
                if (null != in) {
                    try { in.close(); }
                    catch (IOException e) {} // ignore
                }
                capture.flush();
                capture.close();
            }
            String missed = capture.getMissed();
            if (!LangUtil.isEmpty(missed)) {
                MessageUtil.warn(handler, 
                    "NormalizedCompareFiles missed input: " 
                    + missed);
                return null;
            } else {
                String[] lines = capture.getLines();
                CanonicalLine[] result = new CanonicalLine[lines.length];
                for (int i = 0; i < lines.length; i++) {
                    result[i] = new CanonicalLine(lines[i], lines[i]);
                }
                return result;
            }                    
        } 
        
        protected abstract void lineate(
            PrintStream sink,
            IMessageHandler handler, 
            File basedir, 
            File file) throws IOException;
    }
    
    private static class TextLineator extends Lineator {
        
        protected void lineate(
            PrintStream sink,
            IMessageHandler handler, 
            File basedir, 
            File file) throws IOException {
            InputStream in = null;
            try {
                in = new FileInputStream(file);
                FileUtil.copyStream(new DataInputStream(in), sink);
            } finally {
                try { in.close(); }
                catch (IOException e) {} // ignore
            }
        }
    }

    public static class ClassLineator extends Lineator {
        
        protected void lineate(
            PrintStream sink,
            IMessageHandler handler, 
            File basedir, 
            File file) throws IOException {
            String name = FileUtil.fileToClassName(basedir, file);
            // XXX re-enable preflight?
//            if ((null != basedir) && (path.length()-6 != name.length())) {
//                MessageUtil.error(handler, "unexpected class name \""
//                        + name + "\" for path " + path);
//                return null;
//            }
            disassemble(handler, basedir, name, sink);
        }
        
        public static boolean haveDisassembler() {
            try {
                return (null != Class.forName("org.aspectj.weaver.bcel.LazyClassGen"));
            } catch (ClassNotFoundException e) {
                // XXX fix
                //System.err.println(e.getMessage());
                //e.printStackTrace(System.err);
                return false;
            }
        }
        
        /** XXX  dependency on bcweaver/bcel */
        private static void disassemble(
            IMessageHandler handler, 
            File basedir, 
            String name, 
            PrintStream out) throws IOException {
            // LazyClassGen.disassemble(FileUtil.normalizedPath(basedir), name, capture);
            
            Throwable thrown = null;
            String basedirPath = FileUtil.normalizedPath(basedir);
            // XXX use reflection utilities to invoke dissassembler?
            try {
                // XXX need test to detect when this is refactored
                Class c = Class.forName("org.aspectj.weaver.bcel.LazyClassGen");
                Method m = c.getMethod("disassemble", 
                    new Class[] {String.class, String.class, PrintStream.class});
                m.invoke(null, new Object[] { basedirPath, name, out});
            } catch (ClassNotFoundException e) {
                thrown = e;
            } catch (NoSuchMethodException e) {
                thrown = e;
            } catch (IllegalAccessException e) {
                thrown = e;
            } catch (InvocationTargetException e) {
                Throwable t = e.getTargetException();
                if (t instanceof IOException) {
                    throw (IOException) t;
                }
                thrown = t;
            }
            if (null != thrown) {
                MessageUtil.fail(handler, "disassembling " + name  + " path: " + basedirPath,
                    thrown);
            }
        }
    }


    /**
     * Capture PrintStream output to String[]
     * (delimiting component String on println()),
     * also showing any missed text.
     */
    public static class LineStream extends PrintStream {
        StringBuffer sb = new StringBuffer();
        ByteArrayOutputStream missed;
        ArrayList sink;
        public LineStream() {
            super(new ByteArrayOutputStream());
            this.sink = new ArrayList();
            missed = (ByteArrayOutputStream) out;
        }
        
        /** @return any text not captured by our overrides */
        public String getMissed() {
            return missed.toString();
        }
        
        /** clear captured lines (but not missed text) */
        public void clear() {
            sink.clear();
        }
        
        /** 
         * Get String[] of lines printed,
         * delimited by println(..) calls.
         * @return lines printed, exclusive of any not yet terminated by newline 
         */
        public String[] getLines() {
            return (String[]) sink.toArray(new String[0]);
        }
        
        // ---------- PrintStream overrides
		public void println(Object x) {
			println(x.toString());
		}

		public void print(Object obj) {
			print(obj.toString());
		}

        public void println(char c) {
            sb.append(c);
            println();
        }
        public void println(char[] c) {
            sb.append(c);
            println();
        }
		public void print(char c) {
            sb.append(c);
		}

        public void print(char[] c) {
            sb.append(c);
        }
        
        public void println(String s) {
            print(s);
            println();
        }
        public void print(String s) {
            sb.append(s);
        }
        public void println() {
            String line = sb.toString();
            sink.add(line);
            sb.setLength(0);
        }
    }
    
 
}
