/*
 * Copyright 2013 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.core.rule.describe;

import com.google.common.base.Objects;
import net.jcip.annotations.ThreadSafe;
import org.gradle.api.UncheckedIOException;
import org.gradle.model.internal.method.WeaklyTypeReferencingMethod;
import org.gradle.model.internal.type.ModelType;

import java.io.IOException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.WeakHashMap;

// TODO some kind of context of why the method was attached (e.g. which plugin declared the rule)
// TODO some kind of instance state for the method (might be the same as context above)
@ThreadSafe
public class MethodModelRuleDescriptor extends AbstractModelRuleDescriptor {
    private final static Cache DESCRIPTOR_CACHE = new Cache();

    private final WeaklyTypeReferencingMethod<?, ?> method;
    private String description;
    private int hashCode;

    public MethodModelRuleDescriptor(ModelType<?> target, ModelType<?> returnType, Method method) {
        this(WeaklyTypeReferencingMethod.of(target, returnType, method));
    }

    public MethodModelRuleDescriptor(WeaklyTypeReferencingMethod<?, ?> method) {
        this.method = method;
    }

    @Override
    public void describeTo(Appendable appendable) {
        try {
            appendable.append(getDescription());
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    private String getDescription() {
        if (description == null) {
            description = STRING_INTERNER.intern(getClassName() + "#" + method.getName());
        }

        return description;
    }

    private String getClassName() {
        return method.getDeclaringType().getDisplayName();
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        MethodModelRuleDescriptor that = (MethodModelRuleDescriptor) o;
        return Objects.equal(method, that.method);
    }

    @Override
    public int hashCode() {
        int result = hashCode;
        if (result != 0) {
            return result;
        }
        result = Objects.hashCode(method);
        hashCode = result;
        return result;
    }

    public static <T, R> ModelRuleDescriptor of(WeaklyTypeReferencingMethod<T, R> method) {
        return DESCRIPTOR_CACHE.get(method.getDeclaringType().getConcreteClass(), method.getName());
    }

    private static class Cache {
        private final WeakHashMap<Class<?>, CacheEntry> cached = new WeakHashMap<Class<?>, CacheEntry>();

        public synchronized ModelRuleDescriptor get(Class<?> clazz, String method) {
            CacheEntry cacheEntry = cached.get(clazz);
            if (cacheEntry == null) {
                cacheEntry = new CacheEntry(clazz);
                cached.put(clazz, cacheEntry);
            }
            return cacheEntry.get(method);
        }

        private static class CacheEntry {
            private final ModelType<?> clazzType;
            private final Map<String, MethodModelRuleDescriptor> descriptors;

            public CacheEntry(Class<?> clazz) {
                this.clazzType = ModelType.of(clazz);
                this.descriptors = new HashMap<String, MethodModelRuleDescriptor>(clazz.getDeclaredMethods().length);
            }

            public MethodModelRuleDescriptor get(String methodName) {
                MethodModelRuleDescriptor desc = descriptors.get(methodName);
                if (desc != null) {
                    return desc;
                }
                Class<?> clazz = clazzType.getConcreteClass();
                Method[] declaredMethods = clazz.getDeclaredMethods();
                Method method = null;
                for (Method declaredMethod : declaredMethods) {
                    if (declaredMethod.getName().equals(methodName)) {
                        if (method != null) {
                            throw new IllegalStateException("Class " + clazz.getName() + " has more than one method named '" + methodName + "'");
                        }
                        method = declaredMethod;
                    }
                }
                if (method == null) {
                    throw new IllegalStateException("Class " + clazz.getName() + " has no method named '" + methodName + "'");
                }
                desc = new MethodModelRuleDescriptor(ModelType.of(clazz), ModelType.returnType(method), method);
                descriptors.put(methodName, desc);
                return desc;
            }
        }
    }
}

@@ -3051,67 +3051,11 @@
 thod
-.getDeclaringType().getConcreteClass(), method.getName()
 );%0A
+
     
@@ -3224,16 +3224,23 @@
 hronized
+ %3CT, R%3E
  ModelRu
@@ -3260,40 +3260,126 @@
 get(
-Class%3C?%3E clazz, String method) %7B
+WeaklyTypeReferencingMethod%3CT, R%3E method) %7B%0A            Class%3C?%3E clazz = method.getDeclaringType().getConcreteClass();
 %0A   
@@ -3597,37 +3597,280 @@
     
-return cacheEntry.get(method)
+MethodModelRuleDescriptor methodModelRuleDescriptor = cacheEntry.get(method);%0A            if (methodModelRuleDescriptor == null) %7B%0A                methodModelRuleDescriptor = new MethodModelRuleDescriptor(method);%0A            %7D%0A            return methodModelRuleDescriptor
 ;%0A  
@@ -3924,58 +3924,8 @@
 y %7B%0A
-            private final ModelType%3C?%3E clazzType;%0A
     
@@ -4051,62 +4051,8 @@
 ) %7B%0A
-                this.clazzType = ModelType.of(clazz);%0A
     
@@ -4190,32 +4190,39 @@
           public
+ %3CT, R%3E
  MethodModelRule
@@ -4240,28 +4240,113 @@
 get(
-String methodName) %7B
+WeaklyTypeReferencingMethod%3CT, R%3E weakMethod) %7B%0A                String methodName = weakMethod.getName();
 %0A   
@@ -4527,34 +4527,127 @@
     
-Class%3C?%3E clazz = clazz
+desc = new MethodModelRuleDescriptor(weakMethod);%0A                if (isOverloadedInClass(weakMethod.getDeclaring
 Type
+()
 .get
@@ -4653,33 +4653,340 @@
 tConcreteClass()
-;
+, methodName)) %7B%0A                    return desc;%0A                %7D%0A                // Only cache non-overloaded methods by name%0A                descriptors.put(methodName, desc);%0A                return desc;%0A            %7D%0A%0A            private boolean isOverloadedInClass(Class%3C?%3E clazz, String methodName) %7B
 %0A               
@@ -5292,124 +5292,19 @@
     
-throw new IllegalStateException(%22Class %22 + clazz.getName() + %22 has more than one method named '%22 + methodName + %22'%22)
+return true
 ;%0A  
@@ -5619,183 +5619,20 @@
     
-desc = new MethodModelRuleDescriptor(ModelType.of(clazz), ModelType.returnType(method), method);%0A                descriptors.put(methodName, desc);%0A                return desc
+return false
 ;%0A  

Fix failure to generate a method descriptor for an overloaded method