/**
 * 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.cxf.jaxrs.impl;

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.logging.Logger;

import javax.ws.rs.RuntimeType;
import javax.ws.rs.core.Configuration;
import javax.ws.rs.core.Feature;

import org.apache.cxf.common.logging.LogUtils;
import org.apache.cxf.jaxrs.utils.AnnotationUtils;

public class ConfigurationImpl implements Configuration {
    private static final Logger LOG = LogUtils.getL7dLogger(ConfigurationImpl.class);
    private Map<String, Object> props = new HashMap<>();
    private RuntimeType runtimeType;
    private Map<Object, Map<Class<?>, Integer>> providers =
        new LinkedHashMap<>();
    private Map<Feature, Boolean> features = new LinkedHashMap<>();

    public ConfigurationImpl(RuntimeType rt) {
        this.runtimeType = rt;
    }

    public ConfigurationImpl(Configuration parent) {
        if (parent != null) {
            this.props.putAll(parent.getProperties());
            this.runtimeType = parent.getRuntimeType();

            Set<Class<?>> providerClasses = new HashSet<>(parent.getClasses());
            for (Object o : parent.getInstances()) {
                if (!(o instanceof Feature)) {
                    registerParentProvider(o, parent);
                } else {
                    Feature f = (Feature)o;
                    features.put(f, parent.isEnabled(f));
                }
                providerClasses.remove(o.getClass());
            }
            for (Class<?> cls : providerClasses) {
                registerParentProvider(createProvider(cls), parent);
            }

        }
    }

    private void registerParentProvider(Object o, Configuration parent) {
        Map<Class<?>, Integer> contracts = parent.getContracts(o.getClass());
        if (contracts != null) {
            providers.put(o, contracts);
        } else {
            register(o, AnnotationUtils.getBindingPriority(o.getClass()), 
                        ConfigurableImpl.getImplementedContracts(o, new Class<?>[]{}));
        }
    }

    @Override
    public Set<Class<?>> getClasses() {
        Set<Class<?>> classes = new HashSet<>();
        for (Object o : getInstances()) {
            classes.add(o.getClass());
        }
        return classes;
    }

    @Override
    public Map<Class<?>, Integer> getContracts(Class<?> cls) {
        for (Object o : getInstances()) {
            if (cls.isAssignableFrom(o.getClass())) {
                if (o instanceof Feature) {
                    return Collections.emptyMap();
                } else {
                    return providers.get(o);
                }
            }
        }
        return Collections.emptyMap();
    }

    @Override
    public Set<Object> getInstances() {
        Set<Object> allInstances = new HashSet<>();
        allInstances.addAll(providers.keySet());
        allInstances.addAll(features.keySet());
        return Collections.unmodifiableSet(allInstances);
    }

    @Override
    public Map<String, Object> getProperties() {
        return Collections.unmodifiableMap(props);
    }

    @Override
    public Object getProperty(String name) {
        return props.get(name);
    }

    @Override
    public Collection<String> getPropertyNames() {
        return Collections.unmodifiableSet(props.keySet());
    }

    @Override
    public RuntimeType getRuntimeType() {
        return runtimeType;
    }

    @Override
    public boolean isEnabled(Feature f) {
        return features.containsKey(f) && features.get(f);
    }

    @Override
    public boolean isEnabled(Class<? extends Feature> f) {
        for (Entry<Feature, Boolean> entry : features.entrySet()) {
            Feature feature = entry.getKey();
            Boolean enabled = entry.getValue();
            if (f.isAssignableFrom(feature.getClass()) && enabled.booleanValue()) {
                return true;
            }
        }
        return false;
    }

    @Override
    public boolean isRegistered(Object obj) {
        for (Object o : getInstances()) {
            if (o.equals(obj)) {
                return true;
            }
        }
        return false;
    }

    @Override
    public boolean isRegistered(Class<?> cls) {
        for (Object o : getInstances()) {
            if (cls == o.getClass()) {
                return true;
            }
        }
        return false;
    }

    public void setProperty(String name, Object value) {
        if (name == null) {
            throw new NullPointerException("Property name must not be null");
        }
        if (value == null) {
            props.remove(name);
        } else {
            props.put(name, value);
        }
    }

    public void setFeature(Feature f, boolean enabled) {
        features.put(f, enabled);
    }


    private void register(Object provider, int bindingPriority, Class<?>... contracts) {
        register(provider, initContractsMap(bindingPriority, contracts));
    }

    public boolean register(Object provider, Map<Class<?>, Integer> contracts) {
        if (provider.getClass() == Class.class) {
            if (isRegistered((Class<?>)provider)) {
                LOG.warning("Provider class " + ((Class<?>)provider).getName() + " has already been registered");
                return false;
            }
            provider = createProvider((Class<?>)provider);
        }
        if (isRegistered(provider)) {
            LOG.warning("Provider " + provider.getClass().getName() + " has already been registered");
            return false;
        }

        if (!contractsValid(provider, contracts)) {
            return false;
        }

        Map<Class<?>, Integer> metadata = providers.get(provider);
        if (metadata == null) {
            metadata = new HashMap<>();
            providers.put(provider, metadata);
        }
        for (Entry<Class<?>, Integer> entry : contracts.entrySet()) {
            if (entry.getKey().isAssignableFrom(provider.getClass())) {
                metadata.put(entry.getKey(), entry.getValue());
            }
        }
        return true;
    }

    private boolean contractsValid(Object provider, Map<Class<?>, Integer> contracts) {
        final Class<?> providerClass = provider.getClass();
        for (Class<?> contractInterface : contracts.keySet()) {
            if (!contractInterface.isAssignableFrom(providerClass)) {
                LOG.warning("Provider " + providerClass.getName() + " does not implement specified contract: "
                    + contractInterface.getName());
                return false;
            }
        }
        return true;
    }
    public static Map<Class<?>, Integer> initContractsMap(int bindingPriority, Class<?>... contracts) {
        Map<Class<?>, Integer> metadata = new HashMap<>();
        for (Class<?> contract : contracts) {
            metadata.put(contract, bindingPriority);
        }
        return metadata;
    }

    public static Object createProvider(Class<?> cls) {
        try {
            return cls.getDeclaredConstructor().newInstance();
        } catch (Throwable ex) {
            throw new RuntimeException(ex);
        }
    }
}
