/*
 * Copyright 2014 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.gradle.model.internal.inspect;

import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import net.jcip.annotations.ThreadSafe;
import org.gradle.api.Transformer;
import org.gradle.model.InvalidModelRuleDeclarationException;
import org.gradle.model.internal.core.ModelRuleRegistration;
import org.gradle.model.internal.core.rule.describe.MethodModelRuleDescriptor;
import org.gradle.model.internal.core.rule.describe.ModelRuleDescriptor;
import org.gradle.model.internal.type.ModelType;
import org.gradle.util.CollectionUtils;

import java.lang.reflect.*;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

@ThreadSafe
public class ModelRuleInspector {

    private final Iterable<MethodModelRuleExtractor> handlers;

    public ModelRuleInspector(Iterable<MethodModelRuleExtractor> handlers) {
        this.handlers = handlers;
    }

    private String describeHandlers() {
        String desc = Joiner.on(", ").join(CollectionUtils.collect(handlers, new Transformer<String, MethodModelRuleExtractor>() {
            public String transform(MethodModelRuleExtractor original) {
                return original.getDescription();
            }
        }));

        return "[" + desc + "]";
    }

    private static RuntimeException invalid(Class<?> source, String reason) {
        return new InvalidModelRuleDeclarationException("Type " + source.getName() + " is not a valid model rule source: " + reason);
    }

    private static RuntimeException invalid(Method method, String reason) {
        return invalid("model rule method", new MethodModelRuleDescriptor(method), reason);
    }

    private static RuntimeException invalid(String description, ModelRuleDescriptor rule, String reason) {
        StringBuilder sb = new StringBuilder();
        rule.describeTo(sb);
        sb.append(" is not a valid ").append(description).append(": ").append(reason);
        return new InvalidModelRuleDeclarationException(sb.toString());
    }

    public <T> List<ModelRuleRegistration> inspect(Class<T> source, RuleSourceDependencies dependencies) {
        validate(source);
        final Method[] methods = source.getDeclaredMethods();

        // sort for determinism
        Arrays.sort(methods, new Comparator<Method>() {
            public int compare(Method o1, Method o2) {
                return o1.toString().compareTo(o2.toString());
            }
        });

        ImmutableList.Builder<ModelRuleRegistration> registrations = ImmutableList.builder();

        for (Method method : methods) {
            if (method.getTypeParameters().length > 0) {
                throw invalid(method, "cannot have type variables (i.e. cannot be a generic method)");
            }

            MethodRuleDefinition<?, ?> ruleDefinition = DefaultMethodRuleDefinition.create(source, method);
            MethodModelRuleExtractor handler = getMethodHandler(ruleDefinition);
            if (handler != null) {
                validate(method);
                ModelRuleRegistration registration = handler.registration(ruleDefinition, dependencies);
                if (registration != null) {
                    registrations.add(registration);
                }
            }
        }
        return registrations.build();
    }

    private MethodModelRuleExtractor getMethodHandler(MethodRuleDefinition<?, ?> ruleDefinition) {
        MethodModelRuleExtractor handler = null;
        for (MethodModelRuleExtractor candidateHandler : handlers) {
            if (candidateHandler.getSpec().isSatisfiedBy(ruleDefinition)) {
                if (handler == null) {
                    handler = candidateHandler;
                } else {
                    throw invalid("model rule method", ruleDefinition.getDescriptor(), "can only be one of " + describeHandlers());
                }
            }
        }
        return handler;
    }

    /**
     * Validates that the given class is effectively static and has no instance state.
     *
     * @param source the class the validate
     */
    public void validate(Class<?> source) throws InvalidModelRuleDeclarationException {

        // TODO - exceptions thrown here should point to some extensive documentation on the concept of class rule sources

        int modifiers = source.getModifiers();

        if (Modifier.isInterface(modifiers)) {
            throw invalid(source, "must be a class, not an interface");
        }
        if (Modifier.isAbstract(modifiers)) {
            throw invalid(source, "class cannot be abstract");
        }

        if (source.getEnclosingClass() != null) {
            if (Modifier.isStatic(modifiers)) {
                if (Modifier.isPrivate(modifiers)) {
                    throw invalid(source, "class cannot be private");
                }
            } else {
                throw invalid(source, "enclosed classes must be static and non private");
            }
        }

        Class<?> superclass = source.getSuperclass();
        if (!superclass.equals(Object.class)) {
            throw invalid(source, "cannot have superclass");
        }

        Constructor<?>[] constructors = source.getDeclaredConstructors();
        for (Constructor<?> constructor : constructors) {
            if (constructor.getParameterTypes().length > 0) {
                throw invalid(source, "cannot declare a constructor that takes arguments");
            }
        }

        Field[] fields = source.getDeclaredFields();
        for (Field field : fields) {
            int fieldModifiers = field.getModifiers();
            if (!field.isSynthetic() && !(Modifier.isStatic(fieldModifiers) && Modifier.isFinal(fieldModifiers))) {
                throw invalid(source, "field " + field.getName() + " is not static final");
            }
        }
    }

    private void validate(Method ruleMethod) {
        // TODO validations on method: synthetic, bridge methods, varargs, abstract, native
        ModelType<?> returnType = ModelType.returnType(ruleMethod);
        if (returnType.isRawClassOfParameterizedType()) {
            throw invalid(ruleMethod, "raw type " + returnType + " used for return type (all type parameters must be specified of parameterized type)");
        }

        int i = 0;
        for (Type type : ruleMethod.getGenericParameterTypes()) {
            ++i;
            ModelType<?> modelType = ModelType.of(type);
            if (modelType.isRawClassOfParameterizedType()) {
                throw invalid(ruleMethod, "raw type " + modelType + " used for parameter " + i + " (all type parameters must be specified of parameterized type)");
            }
        }
    }

}

@@ -2635,12 +2635,8 @@
 blic
- %3CT%3E
  Lis
@@ -2674,17 +2674,17 @@
 t(Class%3C
-T
+?
 %3E source

Remove unnecessary type param.