////////////////////////////////////////////////////////////////////////////////
// checkstyle: Checks Java source code for adherence to a set of rules.
// Copyright (C) 2001-2016 the original author or authors.
//
// 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 prevents the default implementation Serializable interface in
 * inner classes (Serializable interface are default if methods readObject() or
 * writeObject() are not override in class). Check has option, that allow
 * implementation only one method, if it true, but if it false - class must
 * implement both methods. For more information read
 * "Effective Java (2nd edition)" chapter 11, item 74, page 294.
 * </p>
 * @author <a href="mailto:IliaDubinin91@gmail.com">Ilia Dubinin</a>
 */
public class AvoidDefaultSerializableInInnerClassesCheck extends Check
{

    public static final String MSG_KEY = "avoid.default.serializable.in.inner.classes";

    /**
    *<b>
    *Option, that allow partial implementation of serializable interface.
    *</b>
    */
    private boolean allowPartialImplementation;

    /**
     * <p>
     * Set allow partly implementation serializable interface.
     * </p>
     * @param allow - Option, that allow partial implementation
     *        of serializable interface.
     */
    public void setAllowPartialImplementation(boolean allow)
    {
        this.allowPartialImplementation = allow;
    }

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

    @Override
    public void visitToken(DetailAST detailAST)
    {
        final boolean topLevelClass = detailAST.getParent() == null;
        if (!topLevelClass && isSerializable(detailAST)
                && !isStatic(detailAST)
                && !hasSerialazableMethods(detailAST))
        {
            final DetailAST implementsBlock = detailAST
                    .findFirstToken(TokenTypes.IMPLEMENTS_CLAUSE);
            log(implementsBlock.getLineNo(),
                    MSG_KEY);
        }
    }

    /**
     * <p>
     * Return true if it is nested class. Terminology is here :
     * http://download.oracle.com/javase/tutorial/java/javaOO/nested.html
     * </p>
     * @param classNode - class node
     * @return - boolean variable
     */
    private static boolean isStatic(DetailAST classNode)
    {
        boolean result = false;
        DetailAST modifiers = classNode.findFirstToken(TokenTypes.MODIFIERS);
        modifiers = modifiers.getFirstChild();
        while (!result && modifiers != null) {
            result = "static".equals(modifiers.getText());
            modifiers = modifiers.getNextSibling();
        }
        return result;
    }

    /**
     * <p>
     * Return true, if inner class contain override method readObject() and
     * writeObject();
     * </p>
     * @param classNode
     *        the start node of class definition.
     * @return The boolean value. True, if method was override.
     */
    private boolean hasSerialazableMethods(DetailAST classNode)
    {
        final DetailAST objectBody =
                classNode.findFirstToken(TokenTypes.OBJBLOCK);
        int numberOfSerializationMethods = 0;

        final SiblingIterator methodsIter = new SiblingIterator(objectBody);
        while (methodsIter.hasNextSibling())
        {
            final DetailAST methodNode = methodsIter.nextSibling();
            if (isPrivateMethod(methodNode)
                        && isVoidMethod(methodNode)
                        && (hasCorrectParameter(methodNode, "ObjectInputStream")
                        || hasCorrectParameter(methodNode, "ObjectOutputStream")
                        ))
            {
                numberOfSerializationMethods++;
            }
            if (numberOfSerializationMethods == 1
                && allowPartialImplementation
                || numberOfSerializationMethods == 2)
            {
                return true;
            }
        }
        return false;
    }

    /**
     *<b>
     * Nested class, that implements custom iterator for DetailAST method nodes.
     *</b>
     */
    private class SiblingIterator
    {
        /**
        *<b>
        *Next
        *</b>
        */
        private DetailAST next;

        /**
        *<b>
        *Children Iterator constructor.
        *</b>
        *@param parent - child parent.
        *@param childType - type of child.
        */
        public SiblingIterator(DetailAST parent)
        {
            next = parent.findFirstToken(TokenTypes.METHOD_DEF);
        }

        /**
        *<b>
        *Return boolean value, if has next element.
        *</b>
        *@return boolean value
        */
        public boolean hasNextSibling()
        {
            return next != null;
        }

        /**
        *<b>
        *Return next DetailAST element.
        *</b>
        *@return next DetailAST.
        */

        public DetailAST nextSibling()
        {
            final DetailAST result = next;
            while (next != null) {
                next = next.getNextSibling();
                if (next != null && next.getType() == TokenTypes.METHOD_DEF) {
                    break;
                }
            }
            return result;
        }
    }

    /**
     * <p>
     * Return true, if methods readObject() and writeObject() have correct
     * modifiers;
     * </p>
     * @param methodNode
     *        - current method node;
     * @return boolean value;
     */
    private static boolean isPrivateMethod(DetailAST methodNode)
    {
        DetailAST modifiers = methodNode.findFirstToken(TokenTypes.MODIFIERS);
        modifiers = modifiers.getFirstChild();
        boolean isPrivate = false;
        while (!isPrivate && modifiers != null) {
            isPrivate = "private".equals(modifiers.getText());
            modifiers = modifiers.getNextSibling();
        }
        return isPrivate;
    }

    /**
     * <p>
     * Return true, if method has void type.
     * </p>
     * @param methodNode - method node
     * @return boolean variable
     */
    private static boolean isVoidMethod(DetailAST methodNode)
    {
        DetailAST type = methodNode.findFirstToken(TokenTypes.TYPE);
        type = type.getFirstChild();
        return TokenTypes.LITERAL_VOID == type.getType();
    }

    /**
     * <p>
     * Return true, if method has correct parameters (ObjectInputStream for
     * readObject() and ObjectOutputStream for writeObject()).
     * </p>
     * @param methodNode - method node.
     * @param parameterText - correct parameter text.
     * @return boolean variable.
     */
    private static boolean hasCorrectParameter(DetailAST methodNode,
            String parameterText)
    {
        DetailAST parameters =
            methodNode.findFirstToken(TokenTypes.PARAMETERS);
        boolean result = false;
        if (parameters.getChildCount(TokenTypes.PARAMETER_DEF) == 1) {
            parameters = parameters.findFirstToken(TokenTypes.PARAMETER_DEF);
            parameters = parameters.findFirstToken(TokenTypes.TYPE);
            parameters = parameters.getFirstChild();
            result = parameterText.equals(parameters.getText());
        }
        return result;
    }

    /**
     * <p>
     * Return true, if class implement Serializable interface;
     * </p>
     * @param classDefNode
     *        - the start node for class definition.
     * @return boolean value. True, if class implements Serializable interface.
     */
    private static boolean isSerializable(DetailAST classDefNode)
    {
        DetailAST implementationsDef = classDefNode
                .findFirstToken(TokenTypes.IMPLEMENTS_CLAUSE);
        boolean result = false;
        if (implementationsDef != null) {
            implementationsDef = implementationsDef.getFirstChild();
            while (!result && implementationsDef != null) {
                if (implementationsDef.getType() == TokenTypes.DOT) {
                    implementationsDef = implementationsDef.getLastChild();
                }
                result = "Serializable".equals(implementationsDef.getText());
                implementationsDef = implementationsDef.getNextSibling();
            }
        }
        return result;
    }
}
