/*
 * @(#)ClassfileCheckerCLI.java
 *
 * Copyright (C) 2004 Matt Albrecht
 * groboclown@users.sourceforge.net
 * http://groboutils.sourceforge.net
 *
 *  Permission is hereby granted, free of charge, to any person obtaining a
 *  copy of this software and associated documentation files (the "Software"),
 *  to deal in the Software without restriction, including without limitation
 *  the rights to use, copy, modify, merge, publish, distribute, sublicense,
 *  and/or sell copies of the Software, and to permit persons to whom the
 *  Software is furnished to do so, subject to the following conditions:
 *
 *  The above copyright notice and this permission notice shall be included in
 *  all copies or substantial portions of the Software.
 *
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
 *  THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 *  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 *  DEALINGS IN THE SOFTWARE.
 */

package net.sourceforge.groboutils.codecoverage.v2.util;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import net.sourceforge.groboutils.codecoverage.v2.IAnalysisModule;
import net.sourceforge.groboutils.codecoverage.v2.datastore.AnalysisModuleSet;
import net.sourceforge.groboutils.codecoverage.v2.datastore.ClassRecord;
import net.sourceforge.groboutils.codecoverage.v2.datastore.DirMetaDataReader;
import net.sourceforge.groboutils.codecoverage.v2.datastore.IClassMetaDataReader;
import net.sourceforge.groboutils.codecoverage.v2.datastore.IMetaDataReader;
import net.sourceforge.groboutils.codecoverage.v2.datastore.MarkRecord;

import org.apache.bcel.classfile.ClassParser;
import org.apache.bcel.classfile.CodeException;
import org.apache.bcel.classfile.ConstantPool;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.LineNumberTable;
import org.apache.bcel.classfile.Method;
import org.apache.bcel.generic.InstructionHandle;
import org.apache.bcel.generic.InstructionList;


/**
 * This file joins the class detail files, original class files, and covered
 * class files with the source, into a STDOUT report.
 *
 * @author    Matt Albrecht <a href="mailto:groboclown@users.sourceforge.net">groboclown@users.sourceforge.net</a>
 * @version   $Date: 2004/04/15 05:48:26 $
 * @since     January 15, 2004
 */
public class ClassfileCheckerCLI
{
    public static void main( String args[] )
            throws IOException
    {
        if (args.length <= 1)
        {
            System.out.println("ClassfileCheckerCLI:");
            System.out.println("   Compares original vs. covered class files");
            System.out.println("");
            System.out.println("   Usage:");
            System.out.println("      Report the lines in source code that are marked:");
            System.out.println("          <jdk stuff> " +
                ClassfileCheckerCLI.class.getName() +
                " -m classname datadir srcdir" );
            System.out.println("      Report the differences between the original and covered class files:");
            System.out.println("          <jdk stuff> " +
                ClassfileCheckerCLI.class.getName() +
                " -c classname origclassdir covclassdir" );
            System.out.println("      Both reports:");
            System.out.println("          <jdk stuff> " +
                ClassfileCheckerCLI.class.getName() +
                " -mc classname datadir srcdir origclassdir covclassdir" );
        }
        
        // note: this needs to be improved
        String type = args[0].toLowerCase();
        String cname = args[1];
        String datadir = null;
        String srcdir = null;
        String origclassdir = null;
        String covclassdir = null;
        boolean doMarkReport = false;
        boolean doClassReport = false;
        
        int pos = 2;
        if (type.indexOf('m') >= 0)
        {
            doMarkReport = true;
            datadir = args[pos++];
            srcdir = args[pos++];
        }
        if (type.indexOf('c') >= 0)
        {
            doClassReport = true;
            origclassdir = args[pos++];
            covclassdir = args[pos++];
        }
        
        ClassfileCheckerCLI ccc = new ClassfileCheckerCLI();
        
        
        if (doMarkReport)
        {
            ccc.reportMarkedSource( cname, new File( datadir ),
                new File( srcdir ), System.out );
        }
        
        if (doClassReport)
        {
            ccc.reportClassDiff( cname, new File( origclassdir ),
                new File( covclassdir ), System.out );
        }
    }
    
    public void reportMarkedSource( String classname, File coveredDataDir,
            File srcDir, PrintStream out )
            throws IOException
    {
        IMetaDataReader mdr = createMetaDataReader( coveredDataDir );
        File srcFile = getSourceFileForClass( classname, srcDir );
        reportMarkedSource( classname, mdr, srcFile, out );
    }
    
    
    /**
     * Comment the source code for the given class with the line number and
     * which analysis module marked each line.
     */
    public void reportMarkedSource( String classname, IMetaDataReader mdr,
            File srcFile, PrintStream out )
            throws IOException
    {
        out.println( "Marks on source for class "+classname );
        out.println( "" );
        
        MarkRecord mrL[] = getMarkRecords( classname, mdr );
        Map amIndex = createAnalysisModuleIndexMap( mdr );
        int maxAM = printLegend( amIndex, out );
        
        FileReader fr = new FileReader( srcFile );
        try
        {
            BufferedReader br = new BufferedReader( fr );
            int lineNo = 0;
            String line = br.readLine();
            while (line != null)
            {
                ++lineNo;
                
                out.print( ' ' + format8( lineNo ) );
                out.print( " | " );
                
                // find all the AM measure names that marked this line
                String amnL[] = markedLine( mrL, lineNo );
                for (int i = 1; i <= maxAM; ++i)
                {
                    boolean notfound = true;
                    Integer idx = new Integer(i);
                    for (int j = 0; j < amnL.length; ++j)
                    {
                        if (idx.equals( amIndex.get( amnL[j] ) ))
                        {
                            out.print( indexCode( i ) );
                            notfound = false;
                            break;
                        }
                    }
                    if (notfound)
                    {
                        out.print( " " );
                    }
                }
                out.println( " | " + line );
                
                line = br.readLine();
            }
        }
        finally
        {
            fr.close();
        }
    }
    
    
    /**
     * Perform a diff on the covered and original class files for a
     * specific class.  This only does a diff on the code, not the
     * fields.
     */
    public void reportClassDiff( String classname, File originalClassDir,
            File coveredClassDir, PrintStream out )
            throws IOException
    {
        File origClassF = getClassFileForClass( classname, originalClassDir );
        File covClassF = getClassFileForClass( classname, coveredClassDir );
        
        JavaClass origClass = createJavaClass( origClassF );
        JavaClass covClass = createJavaClass( covClassF );
        
        Method omL[] = origClass.getMethods();
        Method cmL[] = covClass.getMethods();
        
        out.println( "Comparing covered and original classes for class named "+
            classname );
        out.println( "(Original is on the left side, covered is on the right)" );
        
        // compare each method
        Set covNotFound = new HashSet();
        for (int i = 0; i < cmL.length; ++i)
        {
            covNotFound.add( cmL[i] );
        }
        for (int i = 0; i < omL.length; ++i)
        {
            String omName = omL[i].getName();
            String omSig = omL[i].getSignature();
            boolean notFound = true;
            for (int j = 0; notFound && j < cmL.length; ++j)
            {
                if (omName.equals( cmL[j].getName() ) &&
                    omSig.equals( cmL[j].getSignature() ))
                {
                    notFound = false;
                    covNotFound.remove( cmL[j] );
                    out.println("");
                    reportMethodDiff( omL[i], cmL[j],
                        origClass.getConstantPool(),
                        covClass.getConstantPool(), out );
                }
            }
            if (notFound)
            {
                out.println("");
                reportRemovedMethod( omL[i], out );
            }
        }
        Iterator iter = covNotFound.iterator();
        while (iter.hasNext())
        {
            out.println("");
            reportAddedMethod( (Method)iter.next(), out );
        }
    }
    
    public void reportMethodDiff( Method orig, Method cov,
            ConstantPool origCP, ConstantPool covCP, PrintStream out )
    {
        out.println( " = Method " + orig.getName() + " " +
            orig.getSignature() );
        LineNumberTable ot = orig.getLineNumberTable();
        LineNumberTable ct = cov.getLineNumberTable();
        CodeException oce[] = orig.getCode().getExceptionTable();
        CodeException cce[] = cov.getCode().getExceptionTable();
        
        // assume that bytecode instructions were only added, not removed
        InstructionHandle oList[] = (new InstructionList(
            orig.getCode().getCode() )).getInstructionHandles();
        InstructionHandle cList[] = (new InstructionList(
            cov.getCode().getCode() )).getInstructionHandles();
        int oNextI = 0;
        int cNextI = 0;
        int lastOLine = -2;
        int lastCLine = -2;
        while (oNextI < oList.length && cNextI < cList.length)
        {
            InstructionHandle oNext = null;
            InstructionHandle cNext = null;
            if (oNextI < oList.length)
            {
                oNext = oList[ oNextI ];
            }
            if (cNextI < cList.length)
            {
                cNext = cList[ cNextI ];
            }
            if (oNext == null ||
                !oNext.getInstruction().equals( cNext.getInstruction() ))
            {
                lastOLine = -2;
                int lineNo = ct.getSourceLine( cNext.getPosition() );
                if (lastCLine != lineNo)
                {
                    out.println( "  + Line: Orig: N/A    Covered: " +
                        format8( lineNo ) );
                }
                printCodeException( "Covered", cce, cNext.getPosition(), covCP, out );
                printSideBySide( null, cNext.getInstruction().toString( true ),
                    36, " | ", "    ", out );
                lastCLine = lineNo;
                ++cNextI;
            }
            else if (cNext == null)
            {
                lastCLine = -2;
                int lineNo = ot.getSourceLine( oNext.getPosition() );
                if (lastOLine != lineNo)
                {
                    out.println( "  - Line: Orig: " + format8( lineNo ) +
                        "    Covered: N/A" );
                }
                printCodeException( "Original", oce, oNext.getPosition(), origCP, out );
                printSideBySide( oNext.getInstruction().toString( true ), null,
                    36, " | ", "    ", out );
                lastOLine = lineNo;
                ++oNextI;
            }
            else
            {
                int olineNo = ot.getSourceLine( oNext.getPosition() );
                int clineNo = ct.getSourceLine( cNext.getPosition() );
                if (lastOLine != olineNo && lastCLine != clineNo)
                {
                    out.println( "  = Line: Orig: " + format8( olineNo ) +
                        "    Covered: " + format8( clineNo ) );
                }
                printCodeException( "Original", oce, oNext.getPosition(), origCP, out );
                printCodeException( "Covered", cce, cNext.getPosition(), covCP, out );
                printSideBySide( oNext.getInstruction().toString( true ),
                    cNext.getInstruction().toString( true ),
                    36, " | ", "    ", out );
                lastOLine = olineNo;
                lastCLine = clineNo;
                ++oNextI;
                ++cNextI;
            }
        }
    }
    
    
    public void reportRemovedMethod( Method orig, PrintStream out )
    {
        out.println( " < Method Removed in covered version: " + orig.getName() +
            " " + orig.getSignature() );
        LineNumberTable ot = orig.getLineNumberTable();
        
        InstructionHandle oNext =
            (new InstructionList( orig.getCode().getCode() )).getStart();
        while (oNext != null)
        {
            out.println( "  - Line: Orig: " +
                format8( ot.getSourceLine( oNext.getPosition() ) ) +
                "    Covered: N/A" );
            printSideBySide( oNext.getInstruction().toString( true ), null,
                36, " | ", "    ", out );
            oNext = oNext.getNext();
        }
    }
    
    
    public void reportAddedMethod( Method cov, PrintStream out )
    {
        out.println( " > Method Added in covered version: " + cov.getName() +
            " " + cov.getSignature() );
        LineNumberTable ct = cov.getLineNumberTable();
        
        InstructionHandle cNext =
            (new InstructionList( cov.getCode().getCode() )).getStart();
        while (cNext != null)
        {
            out.println( "  + Line: Orig: N/A    Covered: " +
                format8( ct.getSourceLine( cNext.getPosition() ) ) );
            printSideBySide( null, cNext.getInstruction().toString( true ),
                36, " | ", "    ", out );
            cNext = cNext.getNext();
        }
    }
    
    
    /**
     * Returns the measure names of the analysis modules that marked this
     * particular line.
     */
    public String[] markedLine( MarkRecord mrL[], int lineNo )
    {
        Set ret = new HashSet();
        for (int i = 0; i < mrL.length; ++i)
        {
            if (mrL[i].getLineNumber() == lineNo)
            {
                ret.add( mrL[i].getAnalysisModule() );
            }
        }
        
        return (String[])ret.toArray( new String[ ret.size() ] );
    }

    
    
    /**
     * Returns all class files related to the given classname.  That is,
     * the class and its inner classes.
     */
    public File[] getClassFilesForClass( String classname, File classdir )
    {
        String pkgname = getPackageName( classname ).replace( '.',
            File.separatorChar );
        String cname = getJustClassName( classname );
        List ret = new ArrayList();
        if (classdir != null)
        {
            String match1 = cname + ".class";
            String match2 = cname + '$';
            File pkgdir = new File( classdir, pkgname );
            if (pkgdir.exists() && pkgdir.isDirectory())
            {
                String files[] = pkgdir.list();
                for (int i = 0; i < files.length; ++i)
                {
                    if (files[i].equals( match1 ) ||
                        (files[i].startsWith( match2 ) &&
                        files[i].endsWith( ".class" )))
                    {
                        ret.add( new File( pkgdir, files[i] ) );
                    }
                }
            }
        }
        return (File[])ret.toArray( new File[ ret.size() ] );
    }
    
    
    /**
     * Returns only the class file for the given class name.
     */
    public File getClassFileForClass( String classname, File classdir )
            throws IOException
    {
        String fname = classname.replace( '.', File.separatorChar ) + ".class";
        File cf = new File( classdir, fname );
        if (!cf.exists() || !cf.isFile())
        {
            throw new java.io.FileNotFoundException(
                "No such class file '"+fname+"' in directory '"+
                classdir+"'" );
        }
        return cf;
    }
    
    
    /**
     * Returns only the class file for the given class name.
     */
    public File getSourceFileForClass( String classname, File srcdir )
            throws IOException
    {
        int pos = classname.indexOf( '$' );
        if (pos > 0)
        {
            classname = classname.substring( 0, pos );
        }
        String fname = classname.replace( '.', File.separatorChar ) + ".java";
        File cf = new File( srcdir, fname );
//System.err.println("Checking file ["+cf+"]: exists="+
//    cf.exists()+", isfile="+cf.isFile());
        if (!cf.exists() || !cf.isFile())
        {
            throw new java.io.FileNotFoundException(
                "No such source file '"+fname+"' in directory '"+
                srcdir );
        }
        return cf;
    }
    
    
    public MarkRecord[] getMarkRecords( String classname, IMetaDataReader mdr )
            throws IOException
    {
        AnalysisModuleSet ams = mdr.getAnalysisModuleSet();
        IAnalysisModule amL[] = ams.getAnalysisModules();
        List ret = new ArrayList();
        for (int i = 0; i < amL.length; ++i)
        {
            IClassMetaDataReader cmdr = mdr.getClassReader( amL[i] );
            String[] sigs = getMachingClassSignatures( classname,
                cmdr.getClassSignatures() );
            for (int j = 0; j < sigs.length; ++j)
            {
                ClassRecord cr = cmdr.readClass( sigs[j] );
                if (cr != null)
                {
                    MarkRecord[] mrL = cr.getMarksForAnalysisModule( amL[i] );
                    for (int k = 0; k < mrL.length; ++k)
                    {
                        ret.add( mrL[k] );
                    }
                }
            }
        }
        
        return (MarkRecord[])ret.toArray( new MarkRecord[ ret.size() ] );
    }
    
    
    protected JavaClass createJavaClass( File filename )
            throws IOException
    {
        ClassParser cp = new ClassParser( filename.getAbsolutePath() );
        return cp.parse();
    }
    
    
    protected String[] getMachingClassSignatures( String classname,
            String sigs[] )
    {
        List ret = new ArrayList();
        String match1 = classname + '-';
        String match2 = classname + '$';
        if (sigs != null)
        {
            for (int i = 0; i < sigs.length; ++i)
            {
                if (sigs[i].startsWith( match1 ) ||
                    sigs[i].startsWith( match2 ))
                {
                    ret.add( sigs[i] );
                }
            }
        }
        return (String[])ret.toArray( new String[ ret.size() ] );
    }
    
    
    protected String getPackageName( String classname )
    {
        String pkgname = "";
        int i = classname.lastIndexOf( '.' );
        if (i >= 0)
        {
            pkgname = classname.substring( 0, i-1 );
        }
        return pkgname;
    }
    
    
    protected String getJustClassName( String classname )
    {
        String cname = classname;
        int i = classname.lastIndexOf( '.' );
        if (i >= 0)
        {
            cname = classname.substring( i+1 );
        }
        return cname;
    }
    
    
    protected IMetaDataReader createMetaDataReader( File datadir )
            throws IOException
    {
        return new DirMetaDataReader( datadir );
    }
    
    
    protected Map createAnalysisModuleIndexMap( IMetaDataReader mdr )
            throws IOException
    {
        HashMap map = new HashMap();
        int count = 0;
        AnalysisModuleSet ams = mdr.getAnalysisModuleSet();
        IAnalysisModule amL[] = ams.getAnalysisModules();
        for (int j = 0; j < amL.length; ++j)
        {
            if (!map.containsKey( amL[j].getMeasureName() ))
            {
                map.put( amL[j].getMeasureName(), new Integer( ++count ) );
            }
        }
        return map;
    }
    
    
    protected int printLegend( Map amToIndex, PrintStream out )
    {
        out.println( "Legend of Analysis Modules:" );
        int index = 0;
        Set entries = amToIndex.entrySet();
        boolean found = true;
        while (found)
        {
            Integer x = new Integer( ++index );
            found = false;
            Iterator iter = entries.iterator();
            while (iter.hasNext())
            {
                Map.Entry e = (Map.Entry)iter.next();
                if (e.getValue().equals( x ))
                {
                    found = true;
                    out.println( "    "+indexCode( index )+" = "+
                        (String)e.getKey() );
                }
            }
        }
        
        out.println( "" );
        out.print( "  lineno | " );
        for (int i = 1; i < index; ++i)
        {
            out.print( indexCode( i ) );
        }
        out.println( " | source code" );
        for (int i = 0; i < 78; ++i)
        {
            out.print( "-" );
        }
        out.println( "-" );
        
        return index - 1;
    }
    
    
    protected String indexCode( int i )
    {
        return ""+(char)('A' + i - 1);
    }
    
    
    protected String format8( int i )
    {
        StringBuffer sb = new StringBuffer();
        for (int top = 1000000; top > 1 && i < top; top /= 10)
        {
            sb.append( ' ' );
        }
        sb.append( i );
        return sb.toString();
    }
    
    
    protected String[] formatWidth( String t, int width )
    {
        List ret = new ArrayList();
        while (t.length() > width)
        {
            ret.add( t.substring( 0, width ) );
            t = t.substring( width );
        }
        StringBuffer sb = new StringBuffer( t );
        while (sb.length() < width)
        {
            sb.append( " " );
        }
        ret.add( sb.toString() );
        return (String[])ret.toArray( new String[ ret.size() ] );
    }
    
    
    protected void printCodeException( String type, CodeException[] ce, int pos,
            ConstantPool cp, PrintStream out )
    {
        for (int i = 0; i < ce.length; ++i)
        {
            if (pos == ce[i].getStartPC())
            {
                out.println( "  { " + type +
                    ": start try block for exception " +
                    getClassName( cp, ce[i].getCatchType() ) );
            }
            if (pos == ce[i].getEndPC())
            {
                out.println( "  } " + type +
                    ": end try block for exception " +
                    getClassName( cp, ce[i].getCatchType() ) );
            }
            if (pos == ce[i].getHandlerPC())
            {
                out.println( "  > " + type +
                    ": exception handler block for " +
                    getClassName( cp, ce[i].getCatchType() ) );
            }
        }
    }
    
    
    protected void printSideBySide( String left, String right, int width,
            String sep, String indent, PrintStream out )
    {
        if (left == null)
        {
            left = "";
        }
        if (right == null)
        {
            right = "";
        }
        String blank = formatWidth( "", width )[0];
        String fl[] = formatWidth( left, width );
        String rl[] = formatWidth( right, width );
        int len = (fl.length > rl.length) ? fl.length : rl.length;
        for (int i = 0; i < len; ++i)
        {
            out.print( indent );
            if (i >= fl.length)
            {
                out.print( blank );
            }
            else
            {
                out.print( fl[i] );
            }
            out.print( sep );
            if (i >= rl.length)
            {
                out.println( blank );
            }
            else
            {
                out.println( rl[i] );
            }
        }
    }
    
    
    protected String getClassName( ConstantPool cp, int typeIndex )
    {
        String name;
        try
        {
            name = org.apache.bcel.classfile.Utility.
                compactClassName( cp.getConstantString( typeIndex,
                org.apache.bcel.Constants.CONSTANT_Class), false );
        }
        catch (org.apache.bcel.classfile.ClassFormatException ex)
        {
            // ignore
            name = "<unknown: "+typeIndex+">";
        }
        return name;
    }
}