/*
 * Copyright 2015 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.manage.schema.extract;

import com.google.common.base.Equivalence.Wrapper;
import com.google.common.base.Function;
import com.google.common.base.Functions;
import com.google.common.base.Joiner;
import com.google.common.base.Predicate;
import com.google.common.collect.*;
import org.gradle.api.Action;
import org.gradle.model.internal.manage.schema.ModelProperty;
import org.gradle.model.internal.manage.schema.ModelSchema;
import org.gradle.model.internal.method.WeaklyTypeReferencingMethod;
import org.gradle.model.internal.type.ModelType;

import java.lang.reflect.Method;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;

public abstract class StructSchemaExtractionStrategySupport implements ModelSchemaExtractionStrategy {

    private final ModelSchemaAspectExtractor aspectExtractor;

    protected StructSchemaExtractionStrategySupport(ModelSchemaAspectExtractor aspectExtractor) {
        this.aspectExtractor = aspectExtractor;
    }

    @Override
    public <R> void extract(final ModelSchemaExtractionContext<R> extractionContext) {
        ModelType<R> type = extractionContext.getType();
        if (!isTarget(type)) {
            return;
        }

        CandidateMethods candidateMethods = ModelSchemaUtils.getCandidateMethods(type.getRawClass());
        Iterable<ModelPropertyExtractionContext> candidateProperties = selectProperties(extractionContext, candidateMethods);

        List<ModelPropertyExtractionResult<?>> extractedProperties = extractProperties(candidateProperties);
        List<ModelSchemaAspect> aspects = aspectExtractor.extract(extractionContext, extractedProperties);

        Set<WeaklyTypeReferencingMethod<?, ?>> nonPropertyMethods = getNonPropertyMethods(candidateMethods, extractedProperties);
        Iterable<ModelProperty<?>> properties = Iterables.transform(extractedProperties, new Function<ModelPropertyExtractionResult<?>, ModelProperty<?>>() {
            @Override
            public ModelProperty<?> apply(ModelPropertyExtractionResult<?> propertyResult) {
                return propertyResult.getProperty();
            }
        });

        ModelSchema<R> schema = createSchema(extractionContext, properties, nonPropertyMethods, aspects);
        for (ModelPropertyExtractionResult<?> propertyResult : extractedProperties) {
            toPropertyExtractionContext(extractionContext, propertyResult);
        }

        extractionContext.found(schema);
    }

    private Set<WeaklyTypeReferencingMethod<?, ?>> getNonPropertyMethods(CandidateMethods candidateMethods, List<ModelPropertyExtractionResult<?>> extractedProperties) {
        Set<Method> nonPropertyMethods = Sets.newLinkedHashSet(Iterables.transform(candidateMethods.allMethods().keySet(), new Function<Wrapper<Method>, Method>() {
            @Override
            public Method apply(Wrapper<Method> method) {
                return method.get();
            }
        }));
        for (ModelPropertyExtractionResult<?> extractedProperty : extractedProperties) {
            for (PropertyAccessorExtractionContext accessor : extractedProperty.getAccessors()) {
                nonPropertyMethods.removeAll(accessor.getDeclaringMethods());
            }
        }
        return Sets.newLinkedHashSet(Iterables.transform(nonPropertyMethods, new Function<Method, WeaklyTypeReferencingMethod<?, ?>>() {
            @Override
            public WeaklyTypeReferencingMethod<?, ?> apply(Method method) {
                return WeaklyTypeReferencingMethod.of(method);
            }
        }));
    }

    protected abstract boolean isTarget(ModelType<?> type);

    private Iterable<ModelPropertyExtractionContext> selectProperties(final ModelSchemaExtractionContext<?> context, CandidateMethods candidateMethods) {
        Map<String, ModelPropertyExtractionContext> propertiesMap = Maps.newTreeMap();
        for (Map.Entry<Wrapper<Method>, Collection<Method>> entry : candidateMethods.allMethods().entrySet()) {
            Method method = entry.getKey().get();
            PropertyAccessorType propertyAccessorType = PropertyAccessorType.of(method);
            Collection<Method> methodsWithEqualSignature = entry.getValue();
            if (propertyAccessorType != null) {
                String propertyName = propertyAccessorType.propertyNameFor(method);
                ModelPropertyExtractionContext propertyContext = propertiesMap.get(propertyName);
                if (propertyContext == null) {
                    propertyContext = new ModelPropertyExtractionContext(propertyName);
                    propertiesMap.put(propertyName, propertyContext);
                }
                propertyContext.addAccessor(new PropertyAccessorExtractionContext(propertyAccessorType, methodsWithEqualSignature));
            }
        }
        return Collections2.filter(propertiesMap.values(), new Predicate<ModelPropertyExtractionContext>() {
            @Override
            public boolean apply(ModelPropertyExtractionContext property) {
                return property.isReadable();
            }
        });
    }

    private static List<ModelPropertyExtractionResult<?>> extractProperties(Iterable<ModelPropertyExtractionContext> properties) {
        ImmutableList.Builder<ModelPropertyExtractionResult<?>> builder = ImmutableList.builder();
        for (ModelPropertyExtractionContext propertyContext : properties) {
            builder.add(extractProperty(propertyContext));
        }
        return builder.build();
    }

    private static ModelPropertyExtractionResult<?> extractProperty(ModelPropertyExtractionContext property) {
        ModelType<?> propertyType = determinePropertyType(property.getAccessor(PropertyAccessorType.GET_GETTER));
        if (propertyType == null) {
            propertyType = determinePropertyType(property.getAccessor(PropertyAccessorType.IS_GETTER));
        }
        if (propertyType == null) {
            propertyType = determinePropertyType(property.getAccessor(PropertyAccessorType.SETTER));
        }
        return createProperty(propertyType, property);
    }

    private static ModelType<?> determinePropertyType(PropertyAccessorExtractionContext accessor) {
        return accessor == null ? null : accessor.getAccessorType().propertyTypeFor(accessor.getMostSpecificDeclaration());
    }

    private static <P> ModelPropertyExtractionResult<P> createProperty(ModelType<P> propertyType, ModelPropertyExtractionContext propertyContext) {
        ImmutableMap.Builder<PropertyAccessorType, WeaklyTypeReferencingMethod<?, ?>> accessors = ImmutableMap.builder();
        for (PropertyAccessorExtractionContext accessor : propertyContext.getAccessors()) {
            WeaklyTypeReferencingMethod<?, ?> accessorMethod = WeaklyTypeReferencingMethod.of(accessor.getMostSpecificDeclaration());
            accessors.put(accessor.getAccessorType(), accessorMethod);
        }
        ModelProperty<P> property = new ModelProperty<P>(
            propertyType,
            propertyContext.getPropertyName(),
            propertyContext.getDeclaredBy(),
            accessors.build()
        );
        return new ModelPropertyExtractionResult<P>(property, propertyContext.getAccessors());
    }

    private static <R, P> void toPropertyExtractionContext(ModelSchemaExtractionContext<R> parentContext, ModelPropertyExtractionResult<P> propertyResult) {
        ModelProperty<P> property = propertyResult.getProperty();
        String propertyDescription = propertyDescription(parentContext, property);
        parentContext.child(property.getType(), propertyDescription, attachSchema(property));
    }

    private static <P> Action<? super ModelSchema<P>> attachSchema(final ModelProperty<P> property) {
        return new Action<ModelSchema<P>>() {
            @Override
            public void execute(ModelSchema<P> propertySchema) {
                property.setSchema(propertySchema);
            }
        };
    }

    private static String propertyDescription(ModelSchemaExtractionContext<?> parentContext, ModelProperty<?> property) {
        if (property.getDeclaredBy().size() == 1 && property.getDeclaredBy().contains(parentContext.getType())) {
            return String.format("property '%s'", property.getName());
        } else {
            ImmutableSortedSet<String> declaredBy = ImmutableSortedSet.copyOf(Iterables.transform(property.getDeclaredBy(), Functions.toStringFunction()));
            return String.format("property '%s' declared by %s", property.getName(), Joiner.on(", ").join(declaredBy));
        }
    }

    protected abstract <R> ModelSchema<R> createSchema(ModelSchemaExtractionContext<R> extractionContext, Iterable<ModelProperty<?>> properties, Set<WeaklyTypeReferencingMethod<?, ?>> nonPropertyMethods, Iterable<ModelSchemaAspect> aspects);
}
