/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.apache.openejb.config.provider;

import org.apache.openejb.config.sys.ServiceProvider;
import org.apache.openejb.util.SuperProperties;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * A few principles guide this new implementation of ServiceProvider tracking
 * <p/>
 * 1. Never expose the raw datastructure underneath so providers
 * must be registered via the "front door" and can't be slipped
 * in without proper parent provider resolution.
 * <p/>
 * 2. How to locate providers from disk or parse xml
 * is completely abstracted from this implementation.
 *
 * @version $Rev$ $Date$
 */
public class ProviderManager {

    private final List<String> namespaces = new LinkedList<String>();
    private final Map<ID, ServiceProvider> providers = new LinkedHashMap<ID, ServiceProvider>();
    private final ProviderLoader loader;

    public ProviderManager(final ProviderLoader loader) {
        this.loader = loader;
    }

    public ServiceProvider get(final String namespace, final String name) {
        final ID id = new ID(namespace, name);
        return getProvider(id, new LinkedHashSet<ID>());
    }

    public List<ServiceProvider> getAll() {
        return new ArrayList<ServiceProvider>(providers.values());
    }

    public void register(final String namespace, final ServiceProvider provider) {
        if (provider == null) {
            throw new IllegalArgumentException("provider cannot be null");
        }

        final ID id = new ID(namespace, provider.getId());

        register(id, provider, new LinkedHashSet<ID>());
    }

    public List<ServiceProvider> load(String namespace) {
        if (namespace == null) {
            throw new IllegalArgumentException("namespace cannot be null");
        }

        namespace = namespace.toLowerCase();

        if (!namespaces.contains(namespace)) {
            namespaces.add(namespace);

            { // load
                final ArrayList<ServiceProvider> list = new ArrayList<ServiceProvider>(loader.load(namespace));
                for (final ServiceProvider provider : list) {
                    register(namespace, provider);
                }
            }
        }

        final List<ServiceProvider> providers = new ArrayList<ServiceProvider>();
        for (final Map.Entry<ID, ServiceProvider> entry : this.providers.entrySet()) {
            if (entry.getKey().getNamespace().equals(namespace)) {
                providers.add(entry.getValue());
            }
        }

        return providers;
    }

    private void register(final ID id, final ServiceProvider provider, final Set<ID> seen) {
        if (providers.containsKey(id)) {
            return;
        }

        if (provider.getParent() != null) {

            final ID parentId = ID.parse(provider.getParent(), id);

            // Pass in a stack on this call to prevent circular reference
            final ServiceProvider parent = getProvider(parentId, seen);

            if (parent == null) {
                throw new NoSuchParentProviderException(provider, parentId);
            }

            inherit(provider, parent);
        }

        validate(id, provider);

        providers.put(id, provider);
    }

    private void inherit(final ServiceProvider child, final ServiceProvider parent) {

        if (n(child.getClassName())) {
            child.setClassName(parent.getClassName());
        }
        if (n(child.getConstructor())) {
            child.setConstructor(parent.getConstructor());
        }
        if (n(child.getFactoryName())) {
            child.setFactoryName(parent.getFactoryName());
        }
        if (n(child.getDescription())) {
            child.setDescription(parent.getDescription());
        }
        if (n(child.getDisplayName())) {
            child.setDisplayName(parent.getDisplayName());
        }
        if (n(child.getService())) {
            child.setService(parent.getService());
        }

        { // types
            final Set<String> types = new HashSet<String>();
            types.addAll(parent.getTypes());
            types.addAll(child.getTypes());

            child.getTypes().clear();
            child.getTypes().addAll(types);
        }

        { // properties
            final SuperProperties properties = new SuperProperties();
            properties.putAll(parent.getProperties());
            properties.putAll(child.getProperties());

            child.getProperties().clear();
            child.getProperties().putAll(properties);
        }
    }

    private boolean n(final String s) {
        return s == null || s.length() == 0;
    }

    private ServiceProvider getProvider(final ID id, final Set<ID> seen) {
        if (seen.contains(id)) {
            throw new ProviderCircularReferenceException(seen);
        }
        seen.add(id);

        { // Already loaded?
            final ServiceProvider provider = providers.get(id);
            if (provider != null) {
                return provider;
            }
        }


        { // Can we load it?
            final ServiceProvider provider = loader.load(id);
            if (provider != null) {
                register(id, provider, seen);
                return provider;
            }
        }

        return null;
    }

    private void validate(final ID id, final ServiceProvider provider) {
        id.validate();

        if (provider.getService() == null) {
            throw new InvalidProviderDeclarationException("'service' attribute cannot be null", id, provider);
        }
        // TODO - validate provider
    }

    public ProviderLoader getLoader() {
        return loader;
    }


}
