/*
 *  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.harmony.security.fortress;

import java.security.Provider;
import java.security.Security;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;


/**
 * This class contains information about all registered providers and preferred
 * implementations for all "serviceName.algName".
 */
public class Services {

    /**
     * The HashMap that contains information about preferred implementations for
     * all serviceName.algName in the registered providers.
     * Set the initial size to 600 so we don't grow to 1024 by default because
     * initialization adds a few entries more than the growth threshold.
     */
    private static final Map<String, Provider.Service> services
            = new HashMap<String, Provider.Service>(600);

    /**
     * Save default SecureRandom service as well.
     * Avoids similar provider/services iteration in SecureRandom constructor.
     */
    private static Provider.Service cachedSecureRandomService;

    /**
     * Need refresh flag.
     */
    private static boolean needRefresh;

    /**
     * The cacheVersion is changed on every update of service
     * information. It is used by external callers to validate their
     * own caches of Service information.
     */
    private static int cacheVersion = 1;

    /**
     * Registered providers.
     */
    private static final List<Provider> providers = new ArrayList<Provider>(20);

    /**
     * Hash for quick provider access by name.
     */
    private static final Map<String, Provider> providersNames = new HashMap<String, Provider>(20);
    static {
        String providerClassName = null;
        int i = 1;
        ClassLoader cl = ClassLoader.getSystemClassLoader();

        while ((providerClassName = Security.getProperty("security.provider." + i++)) != null) {
            try {
                Class providerClass = Class.forName(providerClassName.trim(), true, cl);
                Provider p = (Provider) providerClass.newInstance();
                providers.add(p);
                providersNames.put(p.getName(), p);
                initServiceInfo(p);
            } catch (ClassNotFoundException ignored) {
            } catch (IllegalAccessException ignored) {
            } catch (InstantiationException ignored) {
            }
        }
        Engine.door.renumProviders();
    }

    /**
     * Returns a copy of the registered providers as an array.
     */
    public static synchronized Provider[] getProviders() {
        return providers.toArray(new Provider[providers.size()]);
    }

    /**
     * Returns a copy of the registered providers as a list.
     */
    public static synchronized List<Provider> getProvidersList() {
        return new ArrayList<Provider>(providers);
    }

    /**
     * Returns the provider with the specified name.
     */
    public static synchronized Provider getProvider(String name) {
        if (name == null) {
            return null;
        }
        return providersNames.get(name);
    }

    /**
     * Inserts a provider at a specified 1-based position.
     */
    public static synchronized int insertProviderAt(Provider provider, int position) {
        int size = providers.size();
        if ((position < 1) || (position > size)) {
            position = size + 1;
        }
        providers.add(position - 1, provider);
        providersNames.put(provider.getName(), provider);
        setNeedRefresh();
        return position;
    }

    /**
     * Removes the provider at the specified 1-based position.
     */
    public static synchronized void removeProvider(int providerNumber) {
        Provider p = providers.remove(providerNumber - 1);
        providersNames.remove(p.getName());
        setNeedRefresh();
    }

    /**
     * Adds information about provider services into HashMap.
     */
    public static synchronized void initServiceInfo(Provider p) {
        for (Provider.Service service : p.getServices()) {
            String type = service.getType();
            if (cachedSecureRandomService == null && type.equals("SecureRandom")) {
                cachedSecureRandomService = service;
            }
            String key = type + "." + service.getAlgorithm().toUpperCase(Locale.US);
            if (!services.containsKey(key)) {
                services.put(key, service);
            }
            for (String alias : Engine.door.getAliases(service)) {
                key = type + "." + alias.toUpperCase(Locale.US);
                if (!services.containsKey(key)) {
                    services.put(key, service);
                }
            }
        }
    }

    /**
     * Returns true if services contain any provider information.
     */
    public static synchronized boolean isEmpty() {
        return services.isEmpty();
    }

    /**
     * Looks up the requested service by type and algorithm. The
     * service key should be provided in the same format used when
     * registering a service with a provider, for example,
     * "KeyFactory.RSA".
     *
     * Callers can cache the returned service information but such
     * caches should be validated against the result of
     * Service.getCacheVersion() before use.
     */
    public static synchronized Provider.Service getService(String key) {
        return services.get(key);
    }

    /**
     * Returns the default SecureRandom service description.
     */
    public static synchronized Provider.Service getSecureRandomService() {
        getCacheVersion();  // used for side effect of updating cache if needed
        return cachedSecureRandomService;
    }

    /**
     * In addition to being used here when the list of providers
     * changes, this method is also used by the Provider
     * implementation to indicate that a provides list of services has
     * changed.
     */
    public static synchronized void setNeedRefresh() {
        needRefresh = true;
    }

    /**
     * Returns the current cache version. This has the possible side
     * effect of updating the cache if needed.
     */
    public static synchronized int getCacheVersion() {
        if (needRefresh) {
            cacheVersion++;
            synchronized (services) {
                services.clear();
            }
            cachedSecureRandomService = null;
            for (Provider p : providers) {
                initServiceInfo(p);
            }
            needRefresh = false;
        }
        return cacheVersion;
    }
}
