/*
 * Copyright (C) 2016-2019 Code Defenders contributors
 *
 * This file is part of Code Defenders.
 *
 * Code Defenders is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or (at
 * your option) any later version.
 *
 * Code Defenders 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
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Code Defenders. If not, see <http://www.gnu.org/licenses/>.
 */
package org.codedefenders.util.analysis;

import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.apache.commons.lang3.Range;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.github.javaparser.JavaParser;
import com.github.javaparser.ParseProblemException;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.ImportDeclaration;
import com.github.javaparser.ast.NodeList;
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
import com.github.javaparser.ast.body.ConstructorDeclaration;
import com.github.javaparser.ast.body.FieldDeclaration;
import com.github.javaparser.ast.body.MethodDeclaration;
import com.github.javaparser.ast.body.VariableDeclarator;
import com.github.javaparser.ast.comments.Comment;
import com.github.javaparser.ast.stmt.BlockStmt;
import com.github.javaparser.ast.stmt.IfStmt;
import com.github.javaparser.ast.stmt.Statement;
import com.github.javaparser.ast.type.PrimitiveType;
import com.github.javaparser.ast.visitor.VoidVisitorAdapter;
import com.github.javaparser.printer.PrettyPrinterConfiguration;

/**
 * Code analysing class which uses {@link ResultVisitor} to iterate through java class code
 * using the {@link #visitCode(String, String) visitCode} method.
 * Stores results in a {@link CodeAnalysisResult} instance and returns it.
 *
 * @author <a href="https://github.com/werli">Phil Werli<a/>
 * @see ResultVisitor
 * @see CodeAnalysisResult
 */
public class ClassCodeAnalyser {
    private static final Logger logger = LoggerFactory.getLogger(ClassCodeAnalyser.class);
    private static final VoidVisitorAdapter<CodeAnalysisResult> resultVisitor = new ResultVisitor();

    /**
     * Iterates through a java class code to extract following information in a {@link CodeAnalysisResult}:
     * <ul>
     * <li>Strings of imports</li>
     * <li>Lines of compile time constants</li>
     * <li>Lines of non coverable code</li>
     * <li>Lines of not initialized fields</li>
     * <li>{@link Range Ranges} of methods signatures</li>
     * <li>{@link Range Ranges} of methods</li>
     * <li>{@link Range Ranges} if-condition statements and their bracket pair</li>
     * </ul>
     *
     * @param className The name of the visited class.
     * @param sourceCode the source code of the visited class.
     * @return a result, may be empty, but never {@code null}.
     */
    public static CodeAnalysisResult visitCode(String className, String sourceCode) {
        final CodeAnalysisResult result = new CodeAnalysisResult();
        try {
            final CompilationUnit cu = JavaParser.parse(sourceCode);
            resultVisitor.visit(cu, result);
        } catch (ParseProblemException e) {
            logger.warn("Failed to parse {}. Aborting code visit.", className);
        }
        return result;
    }

    /**
     * Custom implementation of {@link VoidVisitorAdapter} which collects lines of non coverable code, not
     * initialized fields and ranges of methods and its signatures as well as matching brackets.
     */
    private static class ResultVisitor extends VoidVisitorAdapter<CodeAnalysisResult> {
        private final PrettyPrinterConfiguration printer = new PrettyPrinterConfiguration().setPrintComments(false);

        @Override
        public void visit(IfStmt ifStmt, CodeAnalysisResult result) {
            super.visit(ifStmt, result);
            extractResultsFromIfStmt(ifStmt, result);
        }

        @Override
        public void visit(BlockStmt n, CodeAnalysisResult result) {
            super.visit(n, result);

            int blockStart = n.getBegin().get().line;
            int blockEnd = n.getEnd().get().line;

            Set<Integer> nonEmptyLines = new HashSet<>();

            // Those correspond to '{' and '}' and are not empty by default

            nonEmptyLines.add(blockStart);
            nonEmptyLines.add(blockEnd);

            // Lines which contain comments are not empty
            for (Comment c : n.getAllContainedComments()) {
                for (int line = c.getBegin().get().line; line <= c.getEnd().get().line; line++) {
                    nonEmptyLines.add( line );
                }
            }

            // Processing the block
            NodeList<Statement> statements = n.getStatements();
            if (!statements.isEmpty()) {
                // Lines which contain statements (and are not block themselves) are not empty
                for (Statement s : statements) {
                    if (s instanceof BlockStmt) {
                        continue;
                    }
                    for (int line = s.getBegin().get().line; line <= s.getEnd().get().line; line++) {
                        nonEmptyLines.add(line);
                    }
                }

                Statement lastInnerStatement = statements.get(statements.size() - 1);
                int end = n.getEnd().get().line;
                if (lastInnerStatement.getEnd().get().line < end) {
                    result.nonCoverableCode(end);
                }


                Set<Integer> emptyLines = new HashSet<>();
                // At this point we get empty lines by difference by removing from the block all the non empty lines
                for (int line = blockStart; line <= blockEnd; line++) {
                    if (nonEmptyLines.contains(line)) {
                        continue;
                    }
                    result.emptyLine(line);
                    emptyLines.add(line);
                }

                // We finally found the lines which can cover those empty lines by looking at the first non-empty, non-comment statement
                // If that is covered, then the empty is covered. TODO Not sure how we handle the '{' opening the blocks tho.
                for (int emptyLine : emptyLines) {
                    // By default the end of the block which directly contains the empty line is the statement which can cover it
                    int coveringLine = blockEnd;
                    for (Statement s : statements) {
                        int sStart = s.getBegin().get().line;
                        if (sStart < emptyLine) {
                            // Skip statements that are before the empty line
                            continue;
                        } else {
                            // This works because statements are not empty and are not comments
                            if (sStart <= coveringLine) {
                                coveringLine = sStart;
                            }
                        }
                    }
                    result.lineCoversEmptyLine(coveringLine, emptyLine);
                }
            }


        }

        @Override
        public void visit(FieldDeclaration n, CodeAnalysisResult arg) {
            super.visit(n, arg);
            extractResultsFromFieldDeclaration(n, arg);
        }

        @Override
        public void visit(MethodDeclaration n, CodeAnalysisResult arg) {
            super.visit(n, arg);
            extractResultsFromMethodDeclaration(n, arg);
        }

        @Override
        public void visit(ConstructorDeclaration n, CodeAnalysisResult arg) {
            super.visit(n, arg);
            extractResultsFromConstructorDeclaration(n, arg);
        }

        @Override
        public void visit(ImportDeclaration n, CodeAnalysisResult arg) {
            super.visit(n, arg);
            final String imported = n.toString(printer);
            arg.imported(imported);
        }

        @Override
        public void visit(ClassOrInterfaceDeclaration n, CodeAnalysisResult result) {
            super.visit(n, result);
            result.nonCoverableCode(n.getEnd().get().line);
        }

        private void extractResultsFromIfStmt(IfStmt ifStmt, CodeAnalysisResult result) {
            Statement then = ifStmt.getThenStmt();
            Statement otherwise = ifStmt.getElseStmt().orElse(null);
            if (then instanceof BlockStmt) {

                List<Statement> thenBlockStmts = ((BlockStmt) then).getStatements();
                if (otherwise == null) {
                    /*
                     * This takes only the non-coverable one, meaning
                     * that if } is on the same line of the last stmt it
                     * is not considered here because it is should be already
                     * considered
                     */
                    if (!thenBlockStmts.isEmpty()) {
                        Statement lastInnerStatement = thenBlockStmts.get(thenBlockStmts.size() - 1);
                        if (lastInnerStatement.getEnd().get().line < ifStmt.getEnd().get().line) {
                            result.closingBracket(Range.between(then.getBegin().get().line, ifStmt.getEnd().get().line));
                            result.nonCoverableCode(ifStmt.getEnd().get().line);
                        }
                    }
                } else {
                    result.closingBracket(Range.between(then.getBegin().get().line, then.getEnd().get().line));
                    result.nonCoverableCode(otherwise.getBegin().get().line);
                }
            }
        }

        private static void extractResultsFromFieldDeclaration(FieldDeclaration f, CodeAnalysisResult result) {
            final boolean compileTimeConstant = f.isFinal() && ((f.getCommonType() instanceof PrimitiveType) || (String.class.getSimpleName().equals(f.getElementType().asString())));
            for (VariableDeclarator v : f.getVariables()) {
                for (int line = v.getBegin().get().line; line <= v.getEnd().get().line; line++) {
                    if (compileTimeConstant) {
                        logger.debug("Found compile-time constant " + v);
                        // compile time targets are non coverable, too
                        result.compileTimeConstant(line);
                        result.nonCoverableCode(line);
                    }
                    if (!v.getInitializer().isPresent()) {
                        // non initialized fields are non coverable
                        result.nonInitializedField(line);
                        result.nonCoverableCode(line);
                    }
                }
            }
        }

        private static void extractResultsFromMethodDeclaration(MethodDeclaration md, CodeAnalysisResult result) {
            // Note that md.getEnd().get().line returns the last line of the method, not of the signature
            if (!md.getBody().isPresent()) {
                return;
            }
            BlockStmt body = md.getBody().get();

            // Since a signature might span over different lines we need to get to its body and take its beginning
            // Also note that interfaces have no body ! So this might fail !
            int methodBegin = md.getBegin().get().line;
            int methodBodyBegin = body.getBegin().get().line;
            int methodEnd = md.getEnd().get().line;
            for (int line = methodBegin; line <= methodBodyBegin; line++) {
                // method signatures are non coverable
                result.nonCoverableCode(line);
            }

            String signature = md.getDeclarationAsString(false, false, false);
            signature = signature.substring(signature.indexOf(' ') + 1); // Remove return type

            result.methodSignatures(Range.between(methodBegin, methodBodyBegin));
            result.methods(Range.between(methodBegin, methodEnd));
            result.testAccordionMethodDescription(signature, methodBegin, methodEnd);
        }

        private static void extractResultsFromConstructorDeclaration(ConstructorDeclaration cd, CodeAnalysisResult result) {
            // Constructors always have a body.
            int constructorBegin = cd.getBegin().get().line;
            int constructorBodyBegin = cd.getBody().getBegin().get().line;
            int constructorEnd = cd.getEnd().get().line;

            for (int line = constructorBegin; line <= constructorBodyBegin; line++) {
                // constructor signatures are non coverable
                result.nonCoverableCode(line);
            }

            String signature = cd.getDeclarationAsString(false, false, false);
            result.testAccordionMethodDescription(signature, constructorBegin, constructorEnd);

            result.methodSignatures(Range.between(constructorBegin, constructorBodyBegin));
            result.methods(Range.between(constructorBegin, constructorEnd));
        }
    }
}