/*
 * Copyright DataStax, Inc.
 *
 * 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 com.datastax.oss.driver.internal.core.metadata;

import com.datastax.oss.driver.api.core.metadata.Node;
import com.datastax.oss.driver.api.core.metadata.NodeStateListener;
import com.datastax.oss.driver.api.core.session.Session;
import com.datastax.oss.driver.internal.core.util.Loggers;
import edu.umd.cs.findbugs.annotations.NonNull;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Consumer;
import net.jcip.annotations.ThreadSafe;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Combines multiple node state listeners into a single one.
 *
 * <p>Any exception thrown by a child listener is caught and logged.
 */
@ThreadSafe
public class MultiplexingNodeStateListener implements NodeStateListener {

  private static final Logger LOG = LoggerFactory.getLogger(MultiplexingNodeStateListener.class);

  private final List<NodeStateListener> listeners = new CopyOnWriteArrayList<>();

  public MultiplexingNodeStateListener() {}

  public MultiplexingNodeStateListener(NodeStateListener... listeners) {
    this(Arrays.asList(listeners));
  }

  public MultiplexingNodeStateListener(Collection<NodeStateListener> listeners) {
    addListeners(listeners);
  }

  private void addListeners(Collection<NodeStateListener> source) {
    for (NodeStateListener listener : source) {
      addListener(listener);
    }
  }

  private void addListener(NodeStateListener toAdd) {
    Objects.requireNonNull(toAdd, "listener cannot be null");
    if (toAdd instanceof MultiplexingNodeStateListener) {
      addListeners(((MultiplexingNodeStateListener) toAdd).listeners);
    } else {
      listeners.add(toAdd);
    }
  }

  public void register(@NonNull NodeStateListener listener) {
    addListener(listener);
  }

  @Override
  public void onAdd(@NonNull Node node) {
    invokeListeners(listener -> listener.onAdd(node), "onAdd");
  }

  @Override
  public void onUp(@NonNull Node node) {
    invokeListeners(listener -> listener.onUp(node), "onUp");
  }

  @Override
  public void onDown(@NonNull Node node) {
    invokeListeners(listener -> listener.onDown(node), "onDown");
  }

  @Override
  public void onRemove(@NonNull Node node) {
    invokeListeners(listener -> listener.onRemove(node), "onRemove");
  }

  @Override
  public void onSessionReady(@NonNull Session session) {
    invokeListeners(listener -> listener.onSessionReady(session), "onSessionReady");
  }

  @Override
  public void close() throws Exception {
    for (NodeStateListener listener : listeners) {
      try {
        listener.close();
      } catch (Exception e) {
        Loggers.warnWithException(
            LOG, "Unexpected error while closing node state listener {}.", listener, e);
      }
    }
  }

  private void invokeListeners(@NonNull Consumer<NodeStateListener> action, String event) {
    for (NodeStateListener listener : listeners) {
      try {
        action.accept(listener);
      } catch (Exception e) {
        Loggers.warnWithException(
            LOG,
            "Unexpected error while notifying node state listener {} of an {} event.",
            listener,
            event,
            e);
      }
    }
  }
}
