/*
 * 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.core;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.util.concurrent.UncheckedExecutionException;
import org.gradle.internal.UncheckedException;
import org.gradle.model.internal.manage.binding.StructBindingsStore;
import org.gradle.model.internal.manage.schema.ModelSchema;
import org.gradle.model.internal.manage.schema.ModelSchemaStore;
import org.gradle.model.internal.manage.schema.extract.ManagedImplStructNodeInitializerExtractionStrategy;
import org.gradle.model.internal.manage.schema.extract.ModelMapNodeInitializerExtractionStrategy;
import org.gradle.model.internal.manage.schema.extract.ModelSetNodeInitializerExtractionStrategy;
import org.gradle.model.internal.manage.schema.extract.NodeInitializerExtractionStrategy;
import org.gradle.model.internal.manage.schema.extract.ScalarCollectionNodeInitializerExtractionStrategy;
import org.gradle.model.internal.manage.schema.extract.ScalarTypes;
import org.gradle.model.internal.manage.schema.extract.SpecializedMapNodeInitializerExtractionStrategy;
import org.gradle.model.internal.type.ModelType;
import org.gradle.model.internal.type.ModelTypes;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutionException;

public class DefaultNodeInitializerRegistry implements NodeInitializerRegistry {
    public static final ModelReference<NodeInitializerRegistry> DEFAULT_REFERENCE = ModelReference.of("nodeInitializerRegistry", NodeInitializerRegistry.class);

    private final LoadingCache<NodeInitializerContext<?>, NodeInitializer> cache = CacheBuilder.newBuilder()
        .weakValues()
        .build(new CacheLoader<NodeInitializerContext<?>, NodeInitializer>() {
            @Override
            public NodeInitializer load(NodeInitializerContext<?> context) throws Exception {
                return extractNodeInitializer(context);
            }
        });

    private final List<NodeInitializerExtractionStrategy> allStrategies;
    private final List<NodeInitializerExtractionStrategy> additionalStrategies;
    private final ModelSchemaStore schemaStore;

    public DefaultNodeInitializerRegistry(ModelSchemaStore schemaStore, StructBindingsStore structBindingsStore) {
        this.schemaStore = schemaStore;
        this.allStrategies = new ArrayList<>(Arrays.asList(
            new ModelSetNodeInitializerExtractionStrategy(),
            new SpecializedMapNodeInitializerExtractionStrategy(),
            new ModelMapNodeInitializerExtractionStrategy(),
            new ScalarCollectionNodeInitializerExtractionStrategy(),
            new ManagedImplStructNodeInitializerExtractionStrategy(structBindingsStore)
        ));
        additionalStrategies = new ArrayList<>();
    }

    private ModelTypeInitializationException canNotConstructTypeException(NodeInitializerContext<?> context) {
        ImmutableSortedSet.Builder<ModelType<?>> constructibleTypes = ImmutableSortedSet.orderedBy(ModelTypes.displayOrder());
        for (NodeInitializerExtractionStrategy extractor : additionalStrategies) {
            for (ModelType<?> constructibleType : extractor.supportedTypes()) {
                if (context.getConstraints().isSatisfiedBy(constructibleType)) {
                    constructibleTypes.add(constructibleType);
                }
            }
        }
        return new ModelTypeInitializationException(context, schemaStore, ScalarTypes.TYPES, constructibleTypes.build());
    }

    @Override
    public NodeInitializer getNodeInitializer(NodeInitializerContext<?> nodeInitializerContext) {
        try {
            return cache.get(nodeInitializerContext);
        } catch (ExecutionException e) {
            throw UncheckedException.throwAsUncheckedException(e);
        } catch (UncheckedExecutionException e) {
            throw UncheckedException.throwAsUncheckedException(e.getCause());
        }
    }

    private <T> NodeInitializer extractNodeInitializer(NodeInitializerContext<T> context) {
        ModelSchema<T> schema = schemaStore.getSchema(context.getModelType());
        for (NodeInitializerExtractionStrategy extractor : allStrategies) {
            NodeInitializer nodeInitializer = extractor.extractNodeInitializer(schema, context);
            if (nodeInitializer != null) {
                return nodeInitializer;
            }
        }
        throw canNotConstructTypeException(context);
    }

    @Override
    public void ensureHasInitializer(NodeInitializerContext<?> nodeInitializer) {
        getNodeInitializer(nodeInitializer);
    }

    @Override
    public void registerStrategy(NodeInitializerExtractionStrategy strategy) {
        allStrategies.add(0, strategy);
        additionalStrategies.add(0, strategy);
    }
}
