/*
 * Copyright 2009-2017 Aconex
 *
 * 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 io.pcp.parfait;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArrayList;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;

/**
 * A collection of Monitorables to be monitored by a given output source (or
 * sources). Each {@link Monitorable} is associated with a particular
 * MonitorableRegistry, and informs the registry of its own details via a
 * callback to {@link #register(Monitorable)} when the Monitorable is created.
 * the MonitorableRegistryListener interface can be used for clients interested in state changes
 * to be made aware of when new Monitorables are added, such as objects that wish
 * to serialize state to external stores. (ie... A PCPmmvWriter.. say..)
 */
public class MonitorableRegistry {
    private static final ConcurrentMap<String, MonitorableRegistry> NAMED_INSTANCES = new ConcurrentHashMap<String, MonitorableRegistry>();

    /**
     * A single central registry which can be used by non-Registry-aware
     * Monitorables. This is very limiting in terms of system flexibility and an
     * explicit {@link MonitorableRegistry} should be used instead.
     */
    public static MonitorableRegistry DEFAULT_REGISTRY = new MonitorableRegistry();

    /**
     * This is a TreeMap so that the Monitorables are maintained in alphabetical
     * order for convenience.
     */
    private final Map<String, Monitorable<?>> monitorables = new TreeMap<String, Monitorable<?>>();

    private final List<MonitorableRegistryListener> registryListeners = new CopyOnWriteArrayList<MonitorableRegistryListener>();

    /**
     * Informs this MonitorableRegistry of a new {@link Monitorable}; that
     * Monitorable will be added to the registry, assuming no Monitorable with
     * the same name has previously been registered.
     * 
     * @throws UnsupportedOperationException
     *             if the name of the provided monitorable has already been
     *             registered
     */
    public synchronized <T> void register(Monitorable<T> monitorable) {
        if (monitorables.containsKey(monitorable.getName())) {
            throw new UnsupportedOperationException(
                    "There is already an instance of the Monitorable [" + monitorable.getName()
                    + "] registered.");
        }
        monitorables.put(monitorable.getName(), monitorable);
        notifyListenersOfNewMonitorable(monitorable);
    }

    /**
     * Registers the monitorable if it does not already exist, but otherwise returns an already registered
     * Monitorable with the same name, Semantics and UNnit definition.  This method is useful when objects
     * appear and disappear, and then return, and the lifecycle of the application requires an attempt to recreate
     * the Monitorable without knowing if it has already been created.
     *
     * If there exists a Monitorable with the same name, but with different Semantics or Unit then an IllegalArgumentException
     * is thrown.
     *
     */
    @SuppressWarnings("unchecked")
    public synchronized <T> T registerOrReuse(Monitorable<T> monitorable) {
        String name = monitorable.getName();
        if (monitorables.containsKey(name)) {
            Monitorable<?> existingMonitorableWithSameName = monitorables.get(name);
            if (monitorable.getSemantics().equals(existingMonitorableWithSameName.getSemantics()) && monitorable.getUnit().equals(existingMonitorableWithSameName.getUnit())) {
                return (T) existingMonitorableWithSameName;
            } else {
                throw new IllegalArgumentException(String.format("Cannot reuse the same name %s for a monitorable with different Semantics or Unit: requested=%s, existing=%s", name, monitorable, existingMonitorableWithSameName));
            }
        } else {
            monitorables.put(name, monitorable);
            notifyListenersOfNewMonitorable(monitorable);
            return (T) monitorable;
        }
    }

    private void notifyListenersOfNewMonitorable(Monitorable<?> monitorable) {
        for (MonitorableRegistryListener listener : registryListeners) {
            listener.monitorableAdded(monitorable);
        }
    }


    /**
     * @return a list of all Monitorables which are registered with this
     *         MonitorableRegistry.
     */
    public synchronized Collection<Monitorable<?>> getMonitorables() {
        return ImmutableList.copyOf(monitorables.values());
    }

    /*
     * Testing only -- should be eliminated once the default registry is gone
     */
    public static void clearDefaultRegistry() {
        DEFAULT_REGISTRY = new MonitorableRegistry();
    }

    /**
     * Retrieves or creates a centrally-accessible named instance, identified
     * uniquely by the provided String. This is a convenience method to bridge
     * between the old-style 'single registry' model (see
     * {@link #DEFAULT_REGISTRY}) and having to pass a MonitorableRegistry down
     * to the very depths of your class hierarchy. This is especially useful
     * when instrumenting third-party code which cannot easily get access to a
     * given MonitorableRegistry from a non-static context.
     * 
     * @param name
     * @return
     */
    public static MonitorableRegistry getNamedInstance(String name) {
        MonitorableRegistry instance = NAMED_INSTANCES.get(name);
        if (instance == null) {
            instance = new MonitorableRegistry();
            MonitorableRegistry existing = NAMED_INSTANCES.putIfAbsent(name, instance);
            if (existing != null) {
                return existing;
            }
        }
        return instance;
    }

    public void addRegistryListener(MonitorableRegistryListener monitorableRegistryListener) {
        this.registryListeners.add(monitorableRegistryListener);
    }


    public void removeRegistryListener(MonitorableRegistryListener listener) {
        this.registryListeners.remove(listener);
    }

    @VisibleForTesting
    boolean containsMetric(String name) {
        return monitorables.containsKey(name);
    }

    @VisibleForTesting
    Monitorable<?> getMetric(String name) {
        return monitorables.get(name);
    }

}
