package org.incava.doctorj;

import java.util.*;
import org.incava.analysis.Report;
import org.incava.java.*;
import org.incava.log.Log;
import org.incava.text.SpellChecker;


/**
 * Checks for violations of rules applying to exceptions.
 */
public class ExceptionDocAnalyzer extends DocAnalyzer
{
    public final static String MSG_EXCEPTION_WITHOUT_CLASS_NAME = "Exception without class name";

    public final static String MSG_EXCEPTION_WITHOUT_DESCRIPTION = "Exception without description";

    public final static String MSG_EXCEPTIONS_NOT_ALPHABETICAL = "Exceptions not alphabetical";

    public final static String MSG_EXCEPTION_NOT_IN_THROWS_LIST = "Exception not in throws list";

    public final static String MSG_EXCEPTION_MISSPELLED = "Exception misspelled";

    public final static String MSG_EXCEPTION_NOT_DOCUMENTED = "Exception not documented";

    protected final static int CHKLVL_EXCEPTIONS_ALPHABETICAL = 2;

    protected final static int CHKLVL_EXCEPTION_DOC_EXISTS = 1;
    
    private static List runtimeExceptions = new ArrayList();

    static {
        runtimeExceptions.add("ArithmeticException");
        runtimeExceptions.add("ArrayIndexOutOfBoundsException");
        runtimeExceptions.add("ArrayStoreException");
        runtimeExceptions.add("BufferOverflowException");
        runtimeExceptions.add("BufferUnderflowException");
        runtimeExceptions.add("CMMException");
        runtimeExceptions.add("CannotRedoException");
        runtimeExceptions.add("CannotUndoException");
        runtimeExceptions.add("ClassCastException");
        runtimeExceptions.add("ConcurrentModificationException");
        runtimeExceptions.add("DOMException");
        runtimeExceptions.add("EmptyStackException");
        runtimeExceptions.add("IllegalArgumentException");
        runtimeExceptions.add("IllegalMonitorStateException");
        runtimeExceptions.add("IllegalPathStateException");
        runtimeExceptions.add("IllegalStateException");
        runtimeExceptions.add("ImagingOpException");
        runtimeExceptions.add("IndexOutOfBoundsException");
        runtimeExceptions.add("MissingResourceException");
        runtimeExceptions.add("NegativeArraySizeException");
        runtimeExceptions.add("NoSuchElementException");
        runtimeExceptions.add("NullPointerException");
        runtimeExceptions.add("ProfileDataException");
        runtimeExceptions.add("ProviderException");
        runtimeExceptions.add("RasterFormatException");
        runtimeExceptions.add("SecurityException");
        runtimeExceptions.add("StringIndexOutOfBoundsException");
        runtimeExceptions.add("SystemException");
        runtimeExceptions.add("UndeclaredThrowableException");
        runtimeExceptions.add("UnmodifiableSetException");
        runtimeExceptions.add("UnsupportedOperationException");
    }

    private JavadocNode javadoc;

    private SimpleNode function;

    private ASTNameList throwsList;

    private List documentedExceptions = new ArrayList();

    /**
     * Creates and runs the exception documentation analyzer.
     *
     * @param report   The report to which to send violations.
     * @param javadoc  The javadoc for the function. Should not be null.
     * @param function The constructor or method.
     */
    public ExceptionDocAnalyzer(Report report, JavadocNode javadoc, SimpleNode function)
    {
        super(report);

        this.javadoc = javadoc;
        this.function = function;
        this.throwsList = getThrowsList(function);
    }
    
    public void run()
    {
        Log.log("function: " + function);

        // foreach @throws / @exception tag:
        //  - check for target
        //  - check for description
        //  - check that target is declared, or is subclass of RuntimeException
        //  - in alphabetical order

        boolean alphabeticalReported = false;
        String  previousException    = null;
            
        JavadocTaggedNode[] taggedComments = javadoc.getTaggedComments();
        for (int ti = 0; ti < taggedComments.length; ++ti) {
            JavadocTag tag = taggedComments[ti].getTag();
            Log.log("considering tag: " + tag);
            if (tag.text.equals(JavadocTags.EXCEPTION) || tag.text.equals(JavadocTags.THROWS)) {
                Log.log("got exception tag");
                
                JavadocElement tgt = taggedComments[ti].getTarget();
                Log.log("target: " + tgt);

                if (tgt == null) {
                    if (isCheckable(function, CHKLVL_TAG_CONTENT)) {
                        addViolation(MSG_EXCEPTION_WITHOUT_CLASS_NAME, tag.start, tag.end);
                    }
                }
                else {
                    if (taggedComments[ti].getDescriptionNonTarget() == null && isCheckable(function, CHKLVL_TAG_CONTENT)) {
                        addViolation(MSG_EXCEPTION_WITHOUT_DESCRIPTION, tgt.start, tgt.end);
                    }

                    int    lastDot = tgt.text.lastIndexOf('.');
                    String tgtStr  = lastDot == -1 ? tgt.text : tgt.text.substring(lastDot + 1);
                        
                    checkAgainstCode(tag, tgt, tgtStr);

                    if (!alphabeticalReported && isCheckable(function, CHKLVL_EXCEPTIONS_ALPHABETICAL) && previousException != null && previousException.compareTo(tgtStr) > 0) {
                        addViolation(MSG_EXCEPTIONS_NOT_ALPHABETICAL, tgt.start, tgt.end);
                        alphabeticalReported = true;
                    }
                        
                    previousException = tgtStr;
                }
            }
        }

        Log.log("documentedExceptions: " + documentedExceptions);

        if (throwsList != null && isCheckable(function, CHKLVL_EXCEPTION_DOC_EXISTS)) {
            reportUndocumentedExceptions();
        }            
    }

    protected void checkAgainstCode(JavadocTag tag, JavadocElement tgt, String exceptionName)
    {
        ASTName name = getMatchingException(exceptionName);
        if (name == null) {
            name = getClosestMatchingException(exceptionName);
            if (name == null) {
                if (!runtimeExceptions.contains(exceptionName)) {
                    // this violation is an error, not a warning:
                    addViolation(MSG_EXCEPTION_NOT_IN_THROWS_LIST, tgt.start, tgt.end);
                }
            }
            else {
                // this violation is an error:
                addViolation(MSG_EXCEPTION_MISSPELLED, tgt.start, tgt.end);
                documentedExceptions.add(name.getLastToken().image);
            }
        }
        else {
            documentedExceptions.add(exceptionName);
        }
    }
    
    protected void reportUndocumentedExceptions()
    {
        int nNames = throwsList.jjtGetNumChildren();
        for (int ni = 0; ni < nNames; ++ni) {
            ASTName name      = (ASTName)throwsList.jjtGetChild(ni);
            // by using the last token, we disregard the package:
            Token   nameToken = name.getLastToken();
            Log.log("considering name: " + name + " (" + nameToken + ")");
            if (!documentedExceptions.contains(nameToken.image)) {
                addViolation(MSG_EXCEPTION_NOT_DOCUMENTED, nameToken.beginLine, nameToken.beginColumn, nameToken.beginLine, nameToken.beginColumn + nameToken.image.length() - 1);
            }
        }
    }

    /**
     * Returns the first name in the list that matches the given string.
     */
    protected ASTName getMatchingException(String str)
    {
        if (throwsList == null) {
            return null;
        }
        else {
            int nNames = throwsList.jjtGetNumChildren();
            for (int ni = 0; ni < nNames; ++ni) {
                ASTName name      = (ASTName)throwsList.jjtGetChild(ni);
                // by using the last token, we disregard the package:
                Token   nameToken = name.getLastToken();
                Log.log("considering name: " + name + " (" + nameToken + ")");
                if (nameToken.image.equals(str)) {
                    return name;
                }
            }
            Log.log("no exact match for '" + str + "'");
            return null;
        }
    }

    /**
     * Returns the name in the list that most closely matches the given string.
     */
    protected ASTName getClosestMatchingException(String str)
    {
        if (throwsList == null) {
            return null;
        }
        else {
            SpellChecker spellChecker = new SpellChecker();

            int bestDistance = -1;
            int bestIndex    = -1;
        
            int nNames = throwsList.jjtGetNumChildren();
            for (int ni = 0; ni < nNames; ++ni) {
                ASTName name      = (ASTName)throwsList.jjtGetChild(ni);
                Token   nameToken = name.getLastToken();
                int     dist      = spellChecker.editDistance(nameToken.image, str);
            
                if (dist >= 0 && dist <= SpellChecker.DEFAULT_MAX_DISTANCE && (bestDistance == -1 || dist < bestDistance)) {
                    bestDistance = dist;
                    bestIndex = ni;
                }
            }

            return bestIndex == -1 ? null : (ASTName)throwsList.jjtGetChild(bestIndex);
        }
    }

    /**
     * Returns the throws list for the function.
     */
    protected static ASTNameList getThrowsList(SimpleNode function)
    {
        List     children = function.getChildren();
        Iterator cit      = children.iterator();

        while (cit.hasNext()) {
            Object childObj = cit.next();
            if (childObj instanceof Token) {
                if (((Token)childObj).kind == Java14ParserConstants.THROWS) {
                    return (ASTNameList)cit.next();
                }
            }
        }
        return null;
    }

}
