/*
 * Copyright 1999-2017 Alibaba Group.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.alibaba.p3c.pmd.lang.java.rule.concurrent;

import java.util.List;
import java.util.concurrent.ThreadFactory;

import com.alibaba.p3c.pmd.lang.java.rule.AbstractAliRule;
import com.alibaba.p3c.pmd.lang.java.rule.util.NodeUtils;

import net.sourceforge.pmd.lang.java.ast.ASTAllocationExpression;
import net.sourceforge.pmd.lang.java.ast.ASTBlockStatement;
import net.sourceforge.pmd.lang.java.ast.ASTClassOrInterfaceDeclaration;
import net.sourceforge.pmd.lang.java.ast.ASTClassOrInterfaceType;
import net.sourceforge.pmd.lang.java.ast.ASTFieldDeclaration;
import net.sourceforge.pmd.lang.java.ast.ASTFormalParameter;
import net.sourceforge.pmd.lang.java.ast.ASTFormalParameters;
import net.sourceforge.pmd.lang.java.ast.ASTImplementsList;
import net.sourceforge.pmd.lang.java.ast.ASTInitializer;
import net.sourceforge.pmd.lang.java.ast.ASTLambdaExpression;
import net.sourceforge.pmd.lang.java.ast.ASTMethodDeclaration;
import net.sourceforge.pmd.lang.java.ast.ASTResultType;
import net.sourceforge.pmd.lang.java.ast.ASTType;
import net.sourceforge.pmd.lang.java.ast.ASTVariableDeclarator;
import net.sourceforge.pmd.lang.java.ast.Token;

/**
 * [Mandatory] Threads should be provided by thread pools. Explicitly creating threads is not allowed.
 * Note: Using thread pool can reduce the time of creating and destroying thread and save system resource.
 * If we do not use thread pools, lots of similar threads will be created which lead to
 * "running out of memory" or over-switching problems.
 *
 * Detection rule
 * New Thread can only be created in ThreadFactory.newThread method,as Runtime.getRuntime().addShutdownHook() parameter,
 * or in static block
 *
 * @author caikang
 * @date 2016/11/15
 * @see ThreadShouldSetNameRule
 */
public class AvoidManuallyCreateThreadRule extends AbstractAliRule {

    private static final String METHOD_NEW_THREAD = "newThread";

    @Override
    public Object visit(ASTAllocationExpression node, Object data) {
        if (node.getType() != Thread.class) {
            return super.visit(node, data);
        }
        if (isAddShutdownHook(node) || isInStaticInitializer(node)) {
            return super.visit(node, data);
        }
        //Allocation in lambda block is ignored
        if (node.getFirstParentOfType(ASTLambdaExpression.class) != null) {
            return super.visit(node, data);
        }
        ASTFieldDeclaration fieldDeclaration = node.getFirstParentOfType(ASTFieldDeclaration.class);
        //field declaration with thread allocated
        if (fieldDeclaration != null && NodeUtils.getNodeType(fieldDeclaration) == Thread.class) {
            return addViolationAndReturn(node, data);
        }
        //Declare thread factory field use lambda
        if (node.getDataFlowNode() == null && node.getFirstParentOfType(ASTLambdaExpression.class) != null) {
            if (fieldDeclaration == null || NodeUtils.getNodeType(fieldDeclaration) != ThreadFactory.class) {
                return addViolationAndReturn(node, data);
            }
            return super.visit(node, data);
        }

        //in newThread(Runnable) method is ok
        if (isInNewThreadMethod(node)) {
            return super.visit(node, data);
        }
        //implements of ThreadFactory
        boolean isThreadFactory = (checkForNamingClass(node) || threadFactoryVariable(node))
            && isInPrimaryOrProtectedMethod(node);
        if (isThreadFactory) {
            return super.visit(node, data);
        }
        return addViolationAndReturn(node, data);
    }

    private boolean isAddShutdownHook(ASTAllocationExpression node) {
        ASTBlockStatement blockStatement = node.getFirstParentOfType(ASTBlockStatement.class);
        if (blockStatement == null) {
            return false;
        }
        Token token = (Token)blockStatement.jjtGetFirstToken();
        return Runtime.class.getSimpleName().equals(token.image);
    }

    private boolean isInStaticInitializer(ASTAllocationExpression node) {
        ASTInitializer initializer = node.getFirstParentOfType(ASTInitializer.class);
        return initializer != null && initializer.isStatic();
    }

    private boolean threadFactoryVariable(ASTAllocationExpression node) {
        ASTMethodDeclaration methodDeclaration = node.getFirstParentOfType(ASTMethodDeclaration.class);
        if (methodDeclaration == null) {
            return false;
        }
        ASTVariableDeclarator variableDeclarator = methodDeclaration.getFirstParentOfType(ASTVariableDeclarator.class);
        return variableDeclarator != null && variableDeclarator.getType() == ThreadFactory.class;
    }

    private boolean isInNewThreadMethod(ASTAllocationExpression node) {
        ASTMethodDeclaration methodDeclaration = node.getFirstParentOfType(ASTMethodDeclaration.class);
        if (methodDeclaration == null) {
            return false;
        }
        if (!returnThread(methodDeclaration)) {
            return false;
        }
        if (!METHOD_NEW_THREAD.equals(methodDeclaration.getMethodName())) {
            return false;
        }
        List<ASTFormalParameter> parameters = methodDeclaration.getFirstDescendantOfType(ASTFormalParameters.class)
            .findChildrenOfType(ASTFormalParameter.class);
        return parameters.size() == 1
            && parameters.get(0).getFirstChildOfType(ASTType.class).getType() == Runnable.class;
    }

    private boolean isInPrimaryOrProtectedMethod(ASTAllocationExpression node) {
        ASTMethodDeclaration methodDeclaration = node.getFirstParentOfType(ASTMethodDeclaration.class);
        return methodDeclaration != null && returnThread(methodDeclaration) && (methodDeclaration.isPrivate()
            || methodDeclaration.isProtected());
    }

    private boolean returnThread(ASTMethodDeclaration methodDeclaration) {
        ASTResultType resultType = methodDeclaration.getFirstChildOfType(ASTResultType.class);
        ASTType type = resultType.getFirstChildOfType(ASTType.class);
        return type != null && type.getType() == Thread.class;
    }

    private Object addViolationAndReturn(ASTAllocationExpression node, Object data) {
        addViolationWithMessage(data, node, "java.concurrent.AvoidManuallyCreateThreadRule.violation.msg");
        return super.visit(node, data);
    }

    private boolean checkForNamingClass(ASTAllocationExpression node) {
        ASTClassOrInterfaceDeclaration classOrInterfaceDeclaration =
            node.getFirstParentOfType(ASTClassOrInterfaceDeclaration.class);
        if (classOrInterfaceDeclaration == null) {
            return false;
        }
        ASTImplementsList implementsList = classOrInterfaceDeclaration.getFirstChildOfType(ASTImplementsList.class);
        if (implementsList == null) {
            return false;
        }
        List<ASTClassOrInterfaceType> interfaceTypes = implementsList.findChildrenOfType(ASTClassOrInterfaceType.class);
        for (ASTClassOrInterfaceType type : interfaceTypes) {
            if (type.getType() == ThreadFactory.class) {
                return true;
            }
        }
        return false;
    }
}
