/*
 * 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 com.google.code.gossip.manager;

import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.management.Notification;
import javax.management.NotificationListener;

import org.apache.log4j.Logger;

import com.google.code.gossip.GossipMember;
import com.google.code.gossip.GossipService;
import com.google.code.gossip.GossipSettings;
import com.google.code.gossip.LocalGossipMember;
import com.google.code.gossip.event.GossipListener;
import com.google.code.gossip.event.GossipState;

public abstract class GossipManager extends Thread implements NotificationListener {

  public static final Logger LOGGER = Logger.getLogger(GossipManager.class);

  public static final int MAX_PACKET_SIZE = 102400;

  private final ConcurrentSkipListMap<LocalGossipMember, GossipState> members;

  private final LocalGossipMember me;

  private final GossipSettings settings;

  private final AtomicBoolean gossipServiceRunning;

  private final Class<? extends PassiveGossipThread> passiveGossipThreadClass;

  private final Class<? extends ActiveGossipThread> activeGossipThreadClass;

  private final GossipListener listener;

  private ActiveGossipThread activeGossipThread;

  private PassiveGossipThread passiveGossipThread;

  private ExecutorService gossipThreadExecutor;

  public GossipManager(Class<? extends PassiveGossipThread> passiveGossipThreadClass,
          Class<? extends ActiveGossipThread> activeGossipThreadClass, String cluster,
          String address, int port, String id, GossipSettings settings,
          List<GossipMember> gossipMembers, GossipListener listener) {
    this.passiveGossipThreadClass = passiveGossipThreadClass;
    this.activeGossipThreadClass = activeGossipThreadClass;
    this.settings = settings;
    me = new LocalGossipMember(cluster, address, port, id, System.currentTimeMillis(), this,
            settings.getCleanupInterval());
    members = new ConcurrentSkipListMap<>();
    for (GossipMember startupMember : gossipMembers) {
      if (!startupMember.equals(me)) {
        LocalGossipMember member = new LocalGossipMember(startupMember.getClusterName(),
                startupMember.getHost(), startupMember.getPort(), startupMember.getId(),
                System.currentTimeMillis(), this, settings.getCleanupInterval());
        members.put(member, GossipState.UP);
        GossipService.LOGGER.debug(member);
      }
    }
    gossipThreadExecutor = Executors.newCachedThreadPool();
    gossipServiceRunning = new AtomicBoolean(true);
    this.listener = listener;
    Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
      public void run() {
        GossipService.LOGGER.debug("Service has been shutdown...");
      }
    }));
  }

  /**
   * All timers associated with a member will trigger this method when it goes off. The timer will
   * go off if we have not heard from this member in <code> _settings.T_CLEANUP </code> time.
   */
  @Override
  public void handleNotification(Notification notification, Object handback) {
    LocalGossipMember deadMember = (LocalGossipMember) notification.getUserData();
    GossipService.LOGGER.debug("Dead member detected: " + deadMember);
    members.put(deadMember, GossipState.DOWN);
    if (listener != null) {
      listener.gossipEvent(deadMember, GossipState.DOWN);
    }
  }

  public void revivieMember(LocalGossipMember m) {
    for (Entry<LocalGossipMember, GossipState> it : this.members.entrySet()) {
      if (it.getKey().getId().equals(m.getId())) {
        it.getKey().disableTimer();
      }
    }
    members.remove(m);
    members.put(m, GossipState.UP);
    if (listener != null) {
      listener.gossipEvent(m, GossipState.UP);
    }
  }

  public void createOrRevivieMember(LocalGossipMember m) {
    members.put(m, GossipState.UP);
    if (listener != null) {
      listener.gossipEvent(m, GossipState.UP);
    }
  }

  public GossipSettings getSettings() {
    return settings;
  }

  /**
   * 
   * @return a read only list of members found in the UP state
   */
  public List<LocalGossipMember> getMemberList() {
    List<LocalGossipMember> up = new ArrayList<>();
    for (Entry<LocalGossipMember, GossipState> entry : members.entrySet()) {
      if (GossipState.UP.equals(entry.getValue())) {
        up.add(entry.getKey());
      }
    }
    return Collections.unmodifiableList(up);
  }

  public LocalGossipMember getMyself() {
    return me;
  }

  public List<LocalGossipMember> getDeadList() {
    List<LocalGossipMember> up = new ArrayList<>();
    for (Entry<LocalGossipMember, GossipState> entry : members.entrySet()) {
      if (GossipState.DOWN.equals(entry.getValue())) {
        up.add(entry.getKey());
      }
    }
    return Collections.unmodifiableList(up);
  }

  /**
   * Starts the client. Specifically, start the various cycles for this protocol. Start the gossip
   * thread and start the receiver thread.
   */
  public void run() {
    for (LocalGossipMember member : members.keySet()) {
      if (member != me) {
        member.startTimeoutTimer();
      }
    }
    try {
      passiveGossipThread = passiveGossipThreadClass.getConstructor(GossipManager.class)
              .newInstance(this);
      gossipThreadExecutor.execute(passiveGossipThread);
      activeGossipThread = activeGossipThreadClass.getConstructor(GossipManager.class)
              .newInstance(this);
      gossipThreadExecutor.execute(activeGossipThread);
    } catch (InstantiationException | IllegalAccessException | IllegalArgumentException
            | InvocationTargetException | NoSuchMethodException | SecurityException e1) {
      throw new RuntimeException(e1);
    }
    GossipService.LOGGER.debug("The GossipService is started.");
    while (gossipServiceRunning.get()) {
      try {
        // TODO
        TimeUnit.MILLISECONDS.sleep(1);
      } catch (InterruptedException e) {
        GossipService.LOGGER.warn("The GossipClient was interrupted.");
      }
    }
  }

  /**
   * Shutdown the gossip service.
   */
  public void shutdown() {
    gossipServiceRunning.set(false);
    gossipThreadExecutor.shutdown();
    if (passiveGossipThread != null) {
      passiveGossipThread.shutdown();
    }
    if (activeGossipThread != null) {
      activeGossipThread.shutdown();
    }
    try {
      boolean result = gossipThreadExecutor.awaitTermination(1000, TimeUnit.MILLISECONDS);
      if (!result) {
        LOGGER.error("executor shutdown timed out");
      }
    } catch (InterruptedException e) {
      LOGGER.error(e);
    }
  }
}