////////////////////////////////////////////////////////////////////////////////
// checkstyle: Checks Java source code for adherence to a set of rules.
// Copyright (C) 2001-2012 Oliver Burn
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
////////////////////////////////////////////////////////////////////////////////

package com.github.sevntu.checkstyle.checks.coding;

import com.puppycrawl.tools.checkstyle.api.Check;
import com.puppycrawl.tools.checkstyle.api.DetailAST;
import com.puppycrawl.tools.checkstyle.api.TokenTypes;


/**
 * <p>
 * This Check detects 3 most common cases of incorrect finalize() method implementation:
 * </p>
 * <p>
 * 1. Negates effect of superclass finalize:
 * </p>
 * <pre>
 * protected void finalize() { }
 * protected void finalize() { doSomething(); }
 * </pre>
 * <p>
 * 2. Useless (or worse) finalize:
 * </p>
 * <pre>
 * protected void finalize() { super.finalize(); }
 * </pre>
 * <p>
 * 3. Public finalize:
 * </p>
 * <pre>
 * public void finalize(){ 
 *     try {doSomething();} 
 *     finally {super.finalize()}
 * }</pre>
 * 
 * @author <a href="mailto:maxvetrenko2241@gmail.com">Max Vetrenko</a>
 *
 */
public class FinalizeImplementationCheck extends Check
{

    /**
     * The key is pointing to the warning message text in "messages.properties"
     * file.
     */
    public static final String MSG_KEY_MISSED_TRY_FINALLY =
            "finalize.implementation.missed.try.finally";

    /**
     * The key is pointing to the warning message text in "messages.properties"
     * file.
     */
    public static final String MSG_KEY_PUBLIC_FINALIZE = "finalize.implementation.public";

    /**
     * The key is pointing to the warning message text in "messages.properties"
     * file.
     */
    public static final String MSG_KEY_USELESS_FINALIZE = "finalize.implementation.useless";

    /**
     * The key is pointing to the warning message text in "messages.properties"
     * file.
     */
    public static final String MSG_KEY_MISSED_SUPER_FINALIZE_CALL =
            "finalize.implementation.missed.super.finalize";

    /**
     * The name of finalize() method.
     */
    private static final String FINALIZE_METHOD_NAME = "finalize";

    @Override
    public int[] getDefaultTokens()
    {
        return new int[] { TokenTypes.METHOD_DEF };
    }

    @Override
    public void visitToken(DetailAST methodDefToken)
    {
        if (isFinalizeMethodSignature(methodDefToken)) {

            String warningMessage = validateFinalizeMethod(methodDefToken);
            
            if (warningMessage != null) {
                log(methodDefToken.getLineNo(), warningMessage);
            }
        }
    }

    /**
     * Checks, if finalize implementation is correct. If implementation is bad, 
     * this method will call log() with suitable warning message. 
     * @param finalizeMethodToken 
     *        current finalize() token
     * @return warning message or null, if all is well. 
     */
    private static String validateFinalizeMethod(DetailAST finalizeMethodToken)
    {
        String warningMessage = null;
        if (hasModifier(TokenTypes.LITERAL_PROTECTED, finalizeMethodToken)) {
                DetailAST methodOpeningBrace = finalizeMethodToken.getLastChild();
                DetailAST literalTry = methodOpeningBrace.findFirstToken(TokenTypes.LITERAL_TRY);
                
                if (literalTry == null) {
                    if (containsSuperFinalizeCall(methodOpeningBrace)) {
                        warningMessage = MSG_KEY_USELESS_FINALIZE;
                    } else {
                        warningMessage = MSG_KEY_MISSED_TRY_FINALLY;
                    }
                    
                } else {
                    DetailAST literalFinally = literalTry.findFirstToken(TokenTypes.LITERAL_FINALLY);
                    
                    if (literalFinally != null &&
                            !containsSuperFinalizeCall(literalFinally.getLastChild())) {
                        warningMessage = MSG_KEY_MISSED_SUPER_FINALIZE_CALL;
                    }
                }
            
        } else {
            warningMessage = MSG_KEY_PUBLIC_FINALIZE;
        }
        return warningMessage;
    }

    /**
     * Checks, if current method is finalize().
     * @param methodDefToken
     *        current method definition.
     * @return true, if method is finalize() method.
     */
    public static boolean isFinalizeMethodSignature(DetailAST methodDefToken)
    {
        return !hasModifier(TokenTypes.LITERAL_STATIC ,methodDefToken)
                && isFinalizeMethodName(methodDefToken) && isVoid(methodDefToken)
                && getParamsCount(methodDefToken) == 0;
    }
    
    /**
     * Checks, if finalize() has "static" access modifier.
     * @param modifierType 
     *        modifier type.
     * @param methodToken
     *        MODIFIRES Token.
     * @return true, if finalize() has "protected" access modifier.
     */
    public static boolean hasModifier(int modifierType, DetailAST methodToken)
    {
        DetailAST modifiersToken = methodToken.getFirstChild();
        return modifiersToken.findFirstToken(modifierType) != null;
    }

    /**
     * Checks, if current method name is "finalize".
     * @param methodDefToken
     *        method definition Token.
     * @return true, if current method name is "finalize".
     */
    private static boolean isFinalizeMethodName(DetailAST methodDefToken)
    {
        DetailAST identToken = methodDefToken.findFirstToken(TokenTypes.IDENT);
        return FINALIZE_METHOD_NAME.equals(identToken.getText());
    }

    
    /**
     * Checks, if method is void.
     * @param methodDefToken
     *        method definition Token.
     * @return true, if method is void.
     */
    private static boolean isVoid(DetailAST methodDefToken)
    {
        DetailAST typeToken = methodDefToken.findFirstToken(TokenTypes.TYPE);
        return typeToken.findFirstToken(TokenTypes.LITERAL_VOID) != null;
    }
    
    /**
     * Counts number of parameters.
     * @param methodDefToken
     *        method definition Token.
     * @return number of parameters.
     */
    private static int getParamsCount(DetailAST methodDefToken)
    {
        return methodDefToken.findFirstToken(TokenTypes.PARAMETERS).getChildCount();
    }

    /**
     * Checks, if finalize() is empty.
     * @param methodOpeningBraceToken
     *        method opening brace.
     * @return true, if finalize() is empty.
     */
    public static boolean isMethodEmpty(DetailAST methodOpeningBraceToken)
    {
        return methodOpeningBraceToken.getFirstChild().getType() == TokenTypes.RCURLY;
    }

    /**
     * Checks, if current method has super.finalize() call.
     * @param openingBrace
     *        current method definition.
     * @return true, if method has super.finalize() call.
     */
    public static boolean containsSuperFinalizeCall(DetailAST openingBrace)
    {
        DetailAST methodCallToken = openingBrace.getFirstChild().getFirstChild();
        if (methodCallToken != null) {
            DetailAST dotToken = methodCallToken.getFirstChild();
            if (dotToken.findFirstToken(TokenTypes.LITERAL_SUPER) != null) {
                return true;
            }
        }
        return false;
    }

    /**
     * Checks, if current method has try-finally block.
     * @param methodOpeningBrace
     *        Method opening brace.
     * @return true if current method has try-finally block
     */
    public static boolean hasTryFinallyBlock(DetailAST methodOpeningBrace)
    {
        DetailAST tryToken = methodOpeningBrace.findFirstToken(TokenTypes.LITERAL_TRY);  
        return tryToken != null && tryToken.getLastChild().getType() == TokenTypes.LITERAL_FINALLY;
    }
}
