/*
 * Copyright (C) The Apache Software Foundation. All rights reserved.
 *
 * This software is published under the terms of the Apache Software License
 * version 1.1, a copy of which has been included with this distribution in
 * the LICENSE.txt file.
 */
package org.apache.tools.todo.taskdefs.metamata;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.EmptyStackException;
import java.util.Iterator;
import java.util.Stack;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.sax.SAXTransformerFactory;
import javax.xml.transform.sax.TransformerHandler;
import javax.xml.transform.stream.StreamResult;
import org.apache.tools.todo.taskdefs.exec.ExecuteStreamHandler;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;

/**
 * A handy metrics handler. Most of this code was done only with the screenshots
 * on the documentation since the evaluation version as of this writing does not
 * allow to save metrics or to run it via command line. <p>
 *
 * This class can be used to transform a text file or to process the output
 * stream directly.
 *
 * @author <a href="mailto:sbailliez@imediation.com">Stephane Bailliez</a>
 */
public class MMetricsStreamHandler
    implements ExecuteStreamHandler
{
    /**
     * CLASS construct, it should be named something like 'MyClass'
     */
    protected final static String CLASS = "class";

    /**
     * package construct, it should be look like 'com.mycompany.something'
     */
    protected final static String PACKAGE = "package";

    /**
     * FILE construct, it should look like something 'MyClass.java' or
     * 'MyClass.class'
     */
    protected final static String FILE = "file";

    /**
     * METHOD construct, it should looke like something 'doSomething(...)' or
     * 'doSomething()'
     */
    protected final static String METHOD = "method";

    protected final static String[] ATTRIBUTES = {"name", "vg", "loc",
                                                  "dit", "noa", "nrm", "nlm", "wmc", "rfc", "dac", "fanout", "cbo", "lcom", "nocl"
    };

    /**
     * the stack where are stored the metrics element so that they we can know
     * if we have to close an element or not.
     */
    protected Stack stack = new Stack();

    /**
     * metrics handler
     */
    protected TransformerHandler metricsHandler;

    /**
     * reader for stdout
     */
    protected InputStream metricsOutput;

    /**
     * this is where the XML output will go, should mostly be a file the caller
     * is responsible for flushing and closing this stream
     */
    protected OutputStream xmlOutputStream;

    MMetricsStreamHandler( OutputStream xmlOut )
    {
        this.xmlOutputStream = xmlOut;
    }

    /**
     * Ignore.
     *
     * @param p1 The new ProcessErrorStream value
     * @exception IOException Description of Exception
     */
    public void setProcessErrorStream( InputStream p1 )
        throws IOException
    {
    }

    /**
     * Ignore.
     *
     * @param p1 The new ProcessInputStream value
     * @exception IOException Description of Exception
     */
    public void setProcessInputStream( OutputStream p1 )
        throws IOException
    {
    }

    /**
     * Set the inputstream
     *
     * @param is The new ProcessOutputStream value
     * @exception IOException Description of Exception
     */
    public void setProcessOutputStream( InputStream is )
        throws IOException
    {
        metricsOutput = is;
    }

    public void start()
        throws IOException
    {
        // create the transformer handler that will be used to serialize
        // the output.
        TransformerFactory factory = TransformerFactory.newInstance();
        if( !factory.getFeature( SAXTransformerFactory.FEATURE ) )
        {
            throw new IllegalStateException( "Invalid Transformer factory feature" );
        }
        try
        {
            metricsHandler = ( (SAXTransformerFactory)factory ).newTransformerHandler();
            metricsHandler.setResult( new StreamResult( new OutputStreamWriter( xmlOutputStream, "UTF-8" ) ) );
            Transformer transformer = metricsHandler.getTransformer();
            transformer.setOutputProperty( OutputKeys.INDENT, "yes" );

            // start the document with a 'metrics' root
            metricsHandler.startDocument();
            AttributesImpl attr = new AttributesImpl();
            attr.addAttribute( "", "company", "company", "CDATA", "metamata" );
            metricsHandler.startElement( "", "metrics", "metrics", attr );

            // now parse the whole thing
            parseOutput();

        }
        catch( Exception e )
        {
            throw new IOException( e.getMessage() );
        }
    }

    /**
     * Pretty dangerous business here.
     */
    public void stop()
    {
        try
        {
            // we need to pop everything and close elements that have not been
            // closed yet.
            while( stack.size() > 0 )
            {
                ElementEntry elem = (ElementEntry)stack.pop();
                metricsHandler.endElement( "", elem.getType(), elem.getType() );
            }
            // close the root
            metricsHandler.endElement( "", "metrics", "metrics" );
            // document is finished for good
            metricsHandler.endDocument();
        }
        catch( SAXException e )
        {
            e.printStackTrace();
            throw new IllegalStateException( e.getMessage() );
        }
    }

    /**
     * return the construct type of the element. We can hardly recognize the
     * type of a metrics element, so we are kind of forced to do some black
     * magic based on the name and indentation to recognize the type.
     *
     * @param elem the metrics element to guess for its type.
     * @return the type of the metrics element, either PACKAGE, FILE, CLASS or
     *      METHOD.
     */
    protected String getConstructType( MetricsElement elem )
    {
        // ok no doubt, it's a file
        if( elem.isCompilationUnit() )
        {
            return FILE;
        }

        // same, we're sure it's a method
        if( elem.isMethod() )
        {
            return METHOD;
        }

        // if it's empty, and none of the above it should be a package
        if( stack.size() == 0 )
        {
            return PACKAGE;
        }

        // ok, this is now black magic time, we will guess the type based on
        // the previous type and its indent...
        final ElementEntry previous = (ElementEntry)stack.peek();
        final String prevType = previous.getType();
        final int prevIndent = previous.getIndent();
        final int indent = elem.getIndent();
        // we're just under a file with a bigger indent so it's a class
        if( prevType.equals( FILE ) && indent > prevIndent )
        {
            return CLASS;
        }

        // we're just under a class with a greater or equals indent, it's a class
        // (there might be several classes in a compilation unit and inner classes as well)
        if( prevType.equals( CLASS ) && indent >= prevIndent )
        {
            return CLASS;
        }

        // we assume the other are package
        return PACKAGE;
    }

    /**
     * Create all attributes of a MetricsElement skipping those who have an
     * empty string
     *
     * @param elem
     * @return Description of the Returned Value
     */
    protected Attributes createAttributes( MetricsElement elem )
    {
        AttributesImpl impl = new AttributesImpl();
        int i = 0;
        String name = ATTRIBUTES[ i++ ];
        impl.addAttribute( "", name, name, "CDATA", elem.getName() );
        Iterator metrics = elem.getMetrics();
        for( ; metrics.hasNext(); i++ )
        {
            String value = (String)metrics.next();
            if( value.length() > 0 )
            {
                name = ATTRIBUTES[ i ];
                impl.addAttribute( "", name, name, "CDATA", value );
            }
        }
        return impl;
    }

    /**
     * read each line and process it
     *
     * @exception IOException Description of Exception
     * @exception SAXException Description of Exception
     */
    protected void parseOutput()
        throws IOException, SAXException, ParseException
    {
        BufferedReader br = new BufferedReader( new InputStreamReader( metricsOutput ) );
        String line = null;
        while( ( line = br.readLine() ) != null )
        {
            processLine( line );
        }
    }

    /**
     * Process a metrics line. If the metrics is invalid and that this is not
     * the header line, it is display as info.
     *
     * @param line the line to process, it is normally a line full of metrics.
     * @exception SAXException Description of Exception
     */
    protected void processLine( String line )
        throws SAXException, ParseException
    {
        if( line.startsWith( "Construct\tV(G)\tLOC\tDIT\tNOA\tNRM\tNLM\tWMC\tRFC\tDAC\tFANOUT\tCBO\tLCOM\tNOCL" ) )
        {
            return;
        }
        MetricsElement elem = MetricsElement.parse( line );
        startElement( elem );
    }

    /**
     * Start a new construct. Elements are popped until we are on the same
     * parent node, then the element type is guessed and pushed on the stack.
     *
     * @param elem the element to process.
     * @throws SAXException thrown if there is a problem when sending SAX
     *      events.
     */
    protected void startElement( MetricsElement elem )
        throws SAXException
    {
        // if there are elements in the stack we possibly need to close one or
        // more elements previous to this one until we got its parent
        int indent = elem.getIndent();
        if( stack.size() > 0 )
        {
            ElementEntry previous = (ElementEntry)stack.peek();
            // close nodes until you got the parent.
            try
            {
                while( indent <= previous.getIndent() && stack.size() > 0 )
                {
                    stack.pop();
                    metricsHandler.endElement( "", previous.getType(), previous.getType() );
                    previous = (ElementEntry)stack.peek();
                }
            }
            catch( EmptyStackException ignored )
            {
            }
        }

        // ok, now start the new construct
        String type = getConstructType( elem );
        Attributes attrs = createAttributes( elem );
        metricsHandler.startElement( "", type, type, attrs );

        // make sure we keep track of what we did, that's history
        stack.push( new ElementEntry( type, indent ) );
    }

    /**
     * helper class to keep track of elements via its type and indent that's all
     * we need to guess a type.
     *
     * @author RT
     */
    private final static class ElementEntry
    {
        private int indent;
        private String type;

        ElementEntry( String type, int indent )
        {
            this.type = type;
            this.indent = indent;
        }

        public int getIndent()
        {
            return indent;
        }

        public String getType()
        {
            return type;
        }
    }
}

class MetricsElement
{

    private final static NumberFormat METAMATA_NF;

    private final static NumberFormat NEUTRAL_NF;

    private String construct;

    private int indent;

    private ArrayList metrics;

    static
    {
        METAMATA_NF = NumberFormat.getInstance();
        METAMATA_NF.setMaximumFractionDigits( 1 );
        NEUTRAL_NF = NumberFormat.getInstance();
        if( NEUTRAL_NF instanceof DecimalFormat )
        {
            ( (DecimalFormat)NEUTRAL_NF ).applyPattern( "###0.###;-###0.###" );
        }
        NEUTRAL_NF.setMaximumFractionDigits( 1 );
    }

    MetricsElement( int indent, String construct, ArrayList metrics )
    {
        this.indent = indent;
        this.construct = construct;
        this.metrics = metrics;
    }

    public static MetricsElement parse( String line )
        throws ParseException
    {
        final ArrayList metrics = new ArrayList();
        int pos;

        // i'm using indexOf since I need to know if there are empty strings
        // between tabs and I find it easier than with StringTokenizer
        while( ( pos = line.indexOf( '\t' ) ) != -1 )
        {
            String token = line.substring( 0, pos );
            // only parse what coudl be a valid number. ie not constructs nor no value
            /*
             * if (metrics.size() != 0 || token.length() != 0){
             * Number num = METAMATA_NF.parse(token); // parse with Metamata NF
             * token = NEUTRAL_NF.format(num.doubleValue()); // and format with a neutral NF
             * }
             */
            metrics.add( token );
            line = line.substring( pos + 1 );
        }
        metrics.add( line );

        // there should be exactly 14 tokens (1 name + 13 metrics), if not, there is a problem !
        if( metrics.size() != 14 )
        {
            throw new ParseException( "Could not parse the following line as a metrics: -->" + line + "<--", -1 );
        }

        // remove the first token it's made of the indentation string and the
        // construct name, we'll need all this to figure out what type of
        // construct it is since we lost all semantics :(
        // (#indent[/]*)(#construct.*)
        String name = (String)metrics.get( 0 );
        metrics.remove( 0 );
        int indent = 0;
        pos = name.lastIndexOf( '/' );
        if( pos != -1 )
        {
            name = name.substring( pos + 1 );
            indent = pos + 1;// indentation is last position of token + 1
        }
        return new MetricsElement( indent, name, metrics );
    }

    public int getIndent()
    {
        return indent;
    }

    public Iterator getMetrics()
    {
        return metrics.iterator();
    }

    public String getName()
    {
        return construct;
    }

    public boolean isCompilationUnit()
    {
        return ( construct.endsWith( ".java" ) || construct.endsWith( ".class" ) );
    }

    public boolean isMethod()
    {
        return ( construct.endsWith( "(...)" ) || construct.endsWith( "()" ) );
    }
}

