////////////////////////////////////////////////////////////////////////////////
// checkstyle: Checks Java source code for adherence to a set of rules.
// Copyright (C) 2001-2019 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-1307USA
////////////////////////////////////////////////////////////////////////////////

package com.google.checkstyle.test.base;

import java.io.BufferedReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.puppycrawl.tools.checkstyle.utils.CommonUtil;

public abstract class AbstractIndentationTestSupport extends AbstractModuleTestSupport {

    
private static final int TAB_WIDTH = 4;

    
private static final Pattern NONEMPTY_LINE_REGEX =
            
Pattern.compile(".*?\\S+.*?");

    
private static final Pattern LINE_WITH_COMMENT_REGEX =
            
Pattern.compile(".*?\\S+.*?(//indent:(\\d+) exp:((>=\\d+)|(\\d+(,\\d+)*?))( warn)?)");

    
private static final Pattern GET_INDENT_FROM_COMMENT_REGEX =
            
Pattern.compile("//indent:(\\d+).*?");

    
private static final Pattern MULTILEVEL_COMMENT_REGEX =
            
Pattern.compile("//indent:\\d+ exp:(\\d+(,\\d+)+?)( warn)?");

    
private static final Pattern SINGLE_LEVEL_COMMENT_REGEX =
            
Pattern.compile("//indent:\\d+ exp:(\\d+)( warn)?");

    
private static final Pattern NON_STRICT_LEVEL_COMMENT_REGEX =
            
Pattern.compile("//indent:\\d+ exp:>=(\\d+)( warn)?");

    
@Override
    
protected Integer[] getLinesWithWarn(String fileName) throws IOException {
        
return getLinesWithWarnAndCheckComments(fileName, TAB_WIDTH);
    
}

    
private static Integer[] getLinesWithWarnAndCheckComments(String aFileName,
            
final int tabWidth)
                    
throws IOException {
        
final List<Integer> result = new ArrayList<>();
        
try (BufferedReader br = Files.newBufferedReader(
                
Paths.get(aFileName), StandardCharsets.UTF_8)) {
            
int lineNumber = 1;
            
for (String line = br.readLine(); line != null; line = br.readLine()) {
                
final Matcher match = LINE_WITH_COMMENT_REGEX.matcher(line);
                
if (match.matches()) {
                    
final String comment = match.group(1);
                    
final int indentInComment = getIndentFromComment(comment);
                    
final int actualIndent = getLineStart(line, tabWidth);

                    
if (actualIndent != indentInComment) {
                        
throw new IllegalStateException(String.format(Locale.ROOT,
                                        
"File \"%1$s\" has incorrect indentation in comment."
                                                        
+ "Line %2$d: comment:%3$d, actual:%4$d.",
                                        
aFileName,
                                        
lineNumber,
                                        
indentInComment,
                                        
actualIndent));
                    
}

                    
if (isWarnComment(comment)) {
                        
result.add(lineNumber);
                    
}

                    
if (!isCommentConsistent(comment)) {
                        
throw new IllegalStateException(String.format(Locale.ROOT,
                                        
"File \"%1$s\" has inconsistent comment on line %2$d",
                                        
aFileName,
                                        
lineNumber));
                    
}
                
}
                
else if (NONEMPTY_LINE_REGEX.matcher(line).matches()) {
                    
throw new IllegalStateException(String.format(Locale.ROOT,
                                    
"File \"%1$s\" has no indentation comment or its format "
                                                    
+ "malformed. Error on line: %2$d(%3$s)",
                                    
aFileName,
                                    
lineNumber,
                                    
line));
                
}
                
lineNumber++;
            
}
        
}
        
return result.toArray(new Integer[0]);
    
}

    
private static int getIndentFromComment(String comment) {
        
final Matcher match = GET_INDENT_FROM_COMMENT_REGEX.matcher(comment);
        
match.matches();
        
return Integer.parseInt(match.group(1));
    
}

    
private static boolean isWarnComment(String comment) {
        
return comment.endsWith(" warn");
    
}

    
private static boolean isCommentConsistent(String comment) {
        
final int indentInComment = getIndentFromComment(comment);
        
final boolean isWarnComment = isWarnComment(comment);

        
final boolean result;
        
final CommentType type = getCommentType(comment);
        
switch (type) {
            
case MULTILEVEL:
                
result = isMultiLevelCommentConsistent(comment, indentInComment, isWarnComment);
                
break;

            
case SINGLE_LEVEL:
                
result = isSingleLevelCommentConsistent(comment, indentInComment, isWarnComment);
                
break;

            
case NON_STRICT_LEVEL:
                
result = isNonStrictCommentConsistent(comment, indentInComment, isWarnComment);
                
break;

            
case UNKNOWN:
                
throw new IllegalArgumentException("Cannot determine comment consistent");

            
default:
                
throw new IllegalStateException("Cannot determine comment is consistent");
        
}
        
return result;
    
}

    
private static boolean isNonStrictCommentConsistent(String comment,
            
int indentInComment, boolean isWarnComment) {
        
final Matcher nonStrictLevelMatch = NON_STRICT_LEVEL_COMMENT_REGEX.matcher(comment);
        
nonStrictLevelMatch.matches();
        
final int expectedMinimalIndent = Integer.parseInt(nonStrictLevelMatch.group(1));

        
return indentInComment >= expectedMinimalIndent && !isWarnComment
                
|| indentInComment < expectedMinimalIndent && isWarnComment;
    
}

    
private static boolean isSingleLevelCommentConsistent(String comment,
            
int indentInComment, boolean isWarnComment) {
        
final Matcher singleLevelMatch = SINGLE_LEVEL_COMMENT_REGEX.matcher(comment);
        
singleLevelMatch.matches();
        
final int expectedLevel = Integer.parseInt(singleLevelMatch.group(1));

        
return expectedLevel == indentInComment && !isWarnComment
                
|| expectedLevel != indentInComment && isWarnComment;
    
}

    
private static boolean isMultiLevelCommentConsistent(String comment,
            
int indentInComment, boolean isWarnComment) {
        
final Matcher multilevelMatch = MULTILEVEL_COMMENT_REGEX.matcher(comment);
        
multilevelMatch.matches();
        
final String[] levels = multilevelMatch.group(1).split(",");
        
final String indentInCommentStr = String.valueOf(indentInComment);
        
final boolean containsActualLevel =
                
Arrays.asList(levels).contains(indentInCommentStr);

        
return containsActualLevel && !isWarnComment
                
|| !containsActualLevel && isWarnComment;
    
}

    
private static CommentType getCommentType(String comment) {
        
CommentType result = CommentType.UNKNOWN;
        
final Matcher multilevelMatch = MULTILEVEL_COMMENT_REGEX.matcher(comment);
        
if (multilevelMatch.matches()) {
            
result = CommentType.MULTILEVEL;
        
}
        
else {
            
final Matcher singleLevelMatch = SINGLE_LEVEL_COMMENT_REGEX.matcher(comment);
            
if (singleLevelMatch.matches()) {
                
result = CommentType.SINGLE_LEVEL;
            
}
            
else {
                
final Matcher nonStrictLevelMatch = NON_STRICT_LEVEL_COMMENT_REGEX.matcher(comment);
                
if (nonStrictLevelMatch.matches()) {
                    
result = CommentType.NON_STRICT_LEVEL;
                
}
            
}
        
}
        
return result;
    
}

    
private static int getLineStart(String line, final int tabWidth) {
        
int lineStart = 0;
        
for (int index = 0; index < line.length(); ++index) {
            
if (!Character.isWhitespace(line.charAt(index))) {
                
lineStart = CommonUtil.lengthExpandedTabs(line, index, tabWidth);
                
break;
            
}
        
}
        
return lineStart;
    
}

    
private enum CommentType {

        
MULTILEVEL,
        
SINGLE_LEVEL,
        
NON_STRICT_LEVEL,
        
UNKNOWN,

    
}

}