/*
 * Copyright 2008 the original author or authors.
 * 
 * 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 org.codenarc.rule;

import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.ModuleNode;
import org.codenarc.source.SourceCode;
import org.codenarc.util.WildcardPattern;

import java.util.Collections;
import java.util.Comparator;
import java.util.List;

/**
 * Abstract superclass for Rules that use a Groovy AST Visitor.
 * <p/>
 * Each subclass must set the <code>astVisitorClass</code> property or else define a new
 * property with the same name, specifying the Class of the <code>AstVisitor</code>
 * to applied to the specified source code.
 *
 * @author Chris Mair
 * @author Hamlet D'Arcy
 */
public abstract class AbstractAstVisitorRule extends AbstractRule {

    protected static final String DEFAULT_CONST_NAME = "[A-Z][A-Z0-9_]*";
    protected static final String DEFAULT_FIELD_NAME = "[a-z][a-zA-Z0-9]*";
    protected static final String DEFAULT_VAR_NAME   = "[a-z][a-zA-Z0-9]*";
    protected static final String DEFAULT_TEST_FILES = ".*(Test|Tests|TestCase)\\.groovy";
    protected static final String DEFAULT_TEST_CLASS_NAMES = "*Test,*Tests,*TestCase";

    public static final String CLOSURE_TEXT = "{ -> ... }";

    /** Each concrete subclass must either set this property or define its own property with the same name */
    protected Class getAstVisitorClass() { return null; }

    /**
     * This rule is only applied to classes with names matching this value.
     *
     * The value may optionally be a comma-separated list of names, in which case one of the names must match.
     *
     * If a name includes a period ('.'), then it is assumed to specify a full package name, so the name
     * (pattern) is matched against each fully-qualified class name. Otherwise it is matched only against
     * the class name without a package.
     *
     * The name(s) may optionally include wildcard characters ('*' or '?').
     */
    private String applyToClassNames;

    /**
     * This rule is NOT applied to classes with names matching this value.
     *
     * The value may optionally be a comma-separated list of names, in which case any one of the names can match.
     *
     * If a name includes a period ('.'), then it is assumed to specify a full package name, so the name
     * (pattern) is matched against each fully-qualified class name. Otherwise it is matched only against
     * the class name without a package.
     *
     * The name(s) may optionally include wildcard characters ('*' or '?').
     */
    private String doNotApplyToClassNames;

    public AstVisitor getAstVisitor() {
        Class visitorClass = getAstVisitorClass();
        if (visitorClass == null) throw new IllegalArgumentException("The astVisitorClass property must not be null");
        if (!AstVisitor.class.isAssignableFrom(visitorClass)) throw new IllegalArgumentException("The astVisitorClass property must specify a class that implements AstVisitor");
        try {
            return (AstVisitor) visitorClass.newInstance();
        } catch (Exception e) {
            throw new RuntimeException(e);
        } 
    }

    @Override
    public void applyTo(SourceCode sourceCode, List<Violation> violations) {
        // If AST is null, skip this source code
        ModuleNode ast = sourceCode.getAst();
        if (ast != null && ast.getClasses() != null) {
            for (ClassNode classNode : ast.getClasses()) {
                if (shouldApplyThisRuleTo(classNode)) {
                    AstVisitor visitor = getAstVisitor();
                    visitor.setRule(this);
                    visitor.setSourceCode(sourceCode);
                    visitor.visitClass(classNode);
                    violations.addAll(visitor.getViolations());
                }
            }
        }
        Collections.sort(violations, new Comparator<Violation>() {
            public int compare(Violation o1, Violation o2) {
                if (o1 == null && o2 == null) return 0;
                if (o1 == null) return -1;
                if (o2 == null) return 1;
                if (o1.getLineNumber() == null && o2.getLineNumber() == null) return 0;
                if (o1.getLineNumber() == null) return -1;
                if (o2.getLineNumber() == null) return 1;
                return o1.getLineNumber().compareTo(o2.getLineNumber());
            }
        });
    }

    /**
     * Return true if this rule should be applied for the specified ClassNode, based on the
     * configuration of this rule.
     * @param classNode - the ClassNode
     * @return true if this rule should be applied for the specified ClassNode
     */
    protected boolean shouldApplyThisRuleTo(ClassNode classNode) {
        // TODO Consider caching applyTo, doNotApplyTo and associated WildcardPatterns
        boolean shouldApply = true;

        String applyTo = getApplyToClassNames();
        String doNotApplyTo = getDoNotApplyToClassNames();

        if (applyTo != null  && applyTo.length() > 0) {
            WildcardPattern pattern = new WildcardPattern(applyTo, true);
            shouldApply = pattern.matches(classNode.getNameWithoutPackage()) || pattern.matches(classNode.getName());
        }

        if (shouldApply && doNotApplyTo != null && doNotApplyTo.length() > 0) {
            WildcardPattern pattern = new WildcardPattern(doNotApplyTo, true);
            shouldApply = !pattern.matches(classNode.getNameWithoutPackage()) && !pattern.matches(classNode.getName());
        }

        return shouldApply;
    }

    public String getApplyToClassNames() {
        return applyToClassNames;
    }

    public void setApplyToClassNames(String applyToClassNames) {
        this.applyToClassNames = applyToClassNames;
    }

    public String getDoNotApplyToClassNames() {
        return doNotApplyToClassNames;
    }

    public void setDoNotApplyToClassNames(String doNotApplyToClassNames) {
        this.doNotApplyToClassNames = doNotApplyToClassNames;
    }
}