/*
 * This file is part of the DistSim distributed simulation framework (wrapper)
 * Copyright (C) 2008 Mathias Kurth
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

package brn.distsim;

import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.net.InetSocketAddress;
import java.sql.Connection;
import java.sql.DriverManager;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.Map.Entry;

import javax.management.JMX;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import javax.management.remote.JMXServiceURL;

import org.apache.log4j.Logger;

import brn.distsim.data.Host;
import brn.distsim.jmx.DescriptionMBean;

import com.sun.jdmk.remote.cascading.CascadingService;

/**
 * TODO Make MBean work in JBoss, use MySql MBean packaged database,
 * add some result reports via Eclipse BIRT
 *
 * TODO to many processes and threads are created!
 *
 * @author kurth
 */
public class DistsimManager extends DescriptionMBean implements DistsimManagerMXBean {
  public static final Logger log = Logger.getLogger(DistsimManager.class.getName());

  public static final String JMX_NAME = "brn.distsim.jmx:type=Manager,name=Distsim";

  public static final String JMX_DISTSIM = "com.sun.jdmk:type=CascadingService,name=DistsimCascade";

  private CascadingService cascadeDistsim;

  private Map<InetSocketAddress, ObjectName> mapHostUrl;

  private WrapperManager wrapperManager;

  private boolean restart = false;

  private Date startTime;

  public DistsimManager(WrapperManager wrapperManager) {
    super(DistsimManagerMXBean.class, true);
    this.mapHostUrl = Collections.synchronizedMap(
        new HashMap<InetSocketAddress, ObjectName>());
    this.wrapperManager = wrapperManager;
    this.startTime = new Date();
    log.info("running...");
  }

  /**
   * TODO use port number in prefix!!
   * @param host
   * @return the mount prefix for the given distsim host
   */
  private String getMountPrefix(String host) {
    String mountPrefix = "brn.distsim.mount." + host;
    return mountPrefix;
  }

  /**
   * @param name
   * @return the MXBean specified by the name
   */
  private DistsimManagerMXBean getMXBean(ObjectName name) {
    MBeanServer server = ManagementFactory.getPlatformMBeanServer();
    DistsimManagerMXBean other = JMX.newMXBeanProxy(server, name, DistsimManagerMXBean.class);
    return other;
  }

  /**
   * Runs the server process.
   *
   * @return whether to restart the server
   */
  public synchronized boolean run() {
    MBeanServer server = ManagementFactory.getPlatformMBeanServer();
    try {
      cascadeDistsim = new CascadingService();
      server.registerMBean(cascadeDistsim, new ObjectName(JMX_DISTSIM));
    } catch (Exception e1) {
      // TODO Auto-generated catch block
      e1.printStackTrace();
      throw new RuntimeException(e1);
    }

    try {
      this.wait();
    } catch (InterruptedException e) {
      e.printStackTrace();
    }

    this.wrapperManager.shutDown();
    this.stopWrapperAll();

    try {
      server.unregisterMBean(new ObjectName(JMX_DISTSIM));
    } catch (Exception e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
      throw new RuntimeException(e);
    }

    assert (cascadeDistsim.getMountPointIDs().length == 0);
    return restart;
  }

  /*
   * (non-Javadoc)
   * @see brn.distsim.DistsimMBean#shutDown()
   */
  public synchronized void shutDown() {
    log.info("shutting down...");
    this.notify();
  }

  /*
   * (non-Javadoc)
   * @see brn.distsim.DistsimManagerMXBean#restart()
   */
  public synchronized void restart() {
    log.info("restarting down...");
    this.restart  = true;
    this.notify();
  }

  public void restartAll() {
    Iterator<ObjectName> iter = mapHostUrl.values().iterator();
    while (null != iter && iter.hasNext()) {
      DistsimManagerMXBean other = getMXBean(iter.next());

      other.restartAll();
    }

    this.restart();
  }

  /*
   * (non-Javadoc)
   * @see brn.distsim.DistsimManagerMXBean#getActiveWrappers()
   */
  public WrapperData[] getActiveWrappers() {
    List<WrapperData> list = new ArrayList<WrapperData>();

    list.addAll(Arrays.asList(wrapperManager.getActiveWrappers()));

    // then remote
    Iterator<ObjectName> iter = mapHostUrl.values().iterator();
    while (null != iter && iter.hasNext()) {
      ObjectName next = iter.next();
      DistsimManagerMXBean other = getMXBean(next);
      List<WrapperData> res = null;
      try {
        res = Arrays.asList(other.getActiveWrappers());
      } catch (Exception e) {
        log.error("Unable to query " + next + " ("+e.getMessage()+"), removing");
        log.debug(e);
        this.removeDistsim(next);
        continue;
      }
      list.addAll(res);
    }

    return list.toArray(new WrapperData[0]);
  }

  private void removeDistsim(ObjectName name) {
    // then remote
    Iterator<Entry<InetSocketAddress, ObjectName>> iter = mapHostUrl.entrySet().iterator();
    while (null != iter && iter.hasNext()) {
      Entry<InetSocketAddress, ObjectName> next = iter.next();
      if (name.equals(next.getValue())) {
        this.removeDistsim(next.getKey(), false);
        return;
      }
    }
  }

  /*
   * (non-Javadoc)
   * @see brn.distsim.DistsimMXBean#addDistsim(java.lang.String)
   */
  public void addDistsim(String addressList) {
    List<InetSocketAddress> lstAddr = addrListToArray(addressList);

    for (int i = 0; i < lstAddr.size(); i++) {
      this.addDistsim(lstAddr.get(i));
    }
  }

  /*
   * (non-Javadoc)
   * @see brn.distsim.DistsimManagerMXBean#addDistsimFromDb(java.lang.String, java.lang.String, java.lang.String, java.lang.String)
   */
  public void addDistsimFromDb(String dbHost, String dbName, String dbUser,
      String dbPasswd) {
    try {
      Class.forName("com.mysql.jdbc.Driver");
      String dbUrl = "jdbc:mysql://" + dbHost.trim() + "/" + dbName.trim();
      Connection db = DriverManager.getConnection(dbUrl, dbUser, dbPasswd);
      Map<Integer, String> mapHosts = Host.getHosts(db);
      db.close();
      Iterator<String> iter = mapHosts.values().iterator();
      while (iter != null && iter.hasNext()) {
        String host = iter.next();
        try {

          this.addDistsim(host);
        } catch (Exception e) {
          log.debug("Unable to add " + host + " from db", e);
        }
      }
    } catch (Exception e) {
      // TODO Auto-generated catch block
      log.error("Unable to fetch hosts from db", e);
      throw new RuntimeException(e);
    }
  }

  private void addDistsim(InetSocketAddress addr) {
    if (wrapperManager.getSocketAddr().equals(addr)
        ||mapHostUrl.containsKey(addr)
        ||addr.getHostName().equals("test")) {
      log.debug("Skip distsim at " + addr);
      return;
    }

    log.info("Adding distsim at " + addr);

    String host = addr.getHostName();
    int port = addr.getPort();
    try {
      String mountPrefix = getMountPrefix(host);
      String mountPoint = Main.getJmxUrlPrefix(host,port) + "/" + Main.RMI_NAME;

      unmount(mountPoint);
      cascadeDistsim.mount(new JMXServiceURL(mountPoint), null,
          new ObjectName("brn.distsim.*:*"), mountPrefix);

      // remember local object name
      InetSocketAddress address = new InetSocketAddress(host, port);
      ObjectName name = new ObjectName(mountPrefix + "/" + JMX_NAME);
      this.mapHostUrl.put(address, name);
    } catch (Exception e) {
      // TODO
      log.debug("Unable to add distsim at " + addr, e);
      throw new RuntimeException("Unable to add distsim at " + addr, e);
    }
  }

  /**
   * @param url
   * @throws IOException
   */
  private void unmount(String url) throws IOException {
    String[] ids = cascadeDistsim.getMountPointIDs();
    for (int i=0; i < ids.length; i++) {
      if (ids[i].contains(url)) {
        cascadeDistsim.unmount(ids[i]);
      }
    }
  }

  private void removeDistsim(InetSocketAddress address, boolean stop) {
    ObjectName name = this.mapHostUrl.remove(address);

    // TODO unmount first and shutdown via remote reference
    if (stop && null != name) {
      DistsimManagerMXBean other = getMXBean(name);
      other.shutDown();
    }

    try {
      unmount(getMountPrefix(address.getHostName()));
    } catch (IOException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
      throw new RuntimeException("Unable to remove distsim at " + address, e);
    }

    // be sure to force unmount
    if (null == name) {
      log.error("Distsim on " + address + " not attached.");
      throw new IllegalArgumentException("Distsim on " + address + " not attached.");
    }
  }

  private void removeDistsim(String addressList, boolean stop) {
    List<InetSocketAddress> lstAddr = addrListToArray(addressList);

    for (int i = 0; i < lstAddr.size(); i++) {
      this.removeDistsim(lstAddr.get(i), stop);
    }
  }

  private List<InetSocketAddress> addrListToArray(String addressList) {
    StringTokenizer toki = new StringTokenizer(addressList);
    List<InetSocketAddress> ret = new ArrayList<InetSocketAddress>(toki.countTokens());

    while (toki.hasMoreTokens()) {
      String token = toki.nextToken();
      String[] s = token.split(":", 2);
      String hostName = s[0];
      int port = 0;
      if (s.length > 1)
        port = Integer.parseInt(s[1]);
      else
        port = Main.getRmiRegistryPort();

      ret.add(new InetSocketAddress(hostName, port));
    }

    return ret;
  }


  /*
   * (non-Javadoc)
   * @see brn.distsim.DistsimMXBean#removeDistsim(java.lang.String)
   */
  public void removeDistsim(String host) {
    this.removeDistsim(host, false);
  }

  /*
   * (non-Javadoc)
   * @see brn.distsim.DistsimMXBean#removeDistsimAll()
   */
  public void removeDistsimAll() {
    while (!this.mapHostUrl.isEmpty()) {
      InetSocketAddress host = this.mapHostUrl.keySet().iterator().next();
      removeDistsim(host, false);
    }
  }

  /*
   * (non-Javadoc)
   * @see brn.distsim.DistsimMXBean#stopDistsim(java.lang.String)
   */
  public void stopDistsim(String host) {
    this.removeDistsim(host, true);
  }

  /*
   * (non-Javadoc)
   * @see brn.distsim.DistsimMXBean#stopDistsimAll()
   */
  public void stopDistsimAll() {
    while (!this.mapHostUrl.isEmpty()) {
      InetSocketAddress addr = this.mapHostUrl.keySet().iterator().next();
      removeDistsim(addr, true);
    }
  }

  /*
   * (non-Javadoc)
   * @see brn.distsim.DistsimMXBean#getActiveDistsims()
   */
  public String[] getActiveDistsims() {
    Set<InetSocketAddress> hosts = this.mapHostUrl.keySet();
    String[] ret = new String[hosts.size()];
    Iterator<InetSocketAddress> iter =  hosts.iterator();
    int i = 0;
    while (iter.hasNext()) {
      ret[i] = iter.next().toString();
      i++;
    }
    return ret;
  }

  /*
   * (non-Javadoc)
   * @see brn.distsim.DistsimMXBean#startWrapper(java.lang.String, int, int, java.lang.String)
   */
  public void startWrapper(String host, int idStart, int number, String dbHost,
      String dbName, String dbUser, String dbPasswd) {
    InetSocketAddress addr = this.addrListToArray(host).get(0);
    this.startWrapper(addr, idStart, number, dbHost, dbName, dbUser, dbPasswd);
  }

  /*
   * (non-Javadoc)
   * @see brn.distsim.DistsimMXBean#startWrapper(java.lang.String, int, int, java.lang.String)
   */
  public void startWrapper(InetSocketAddress addr, int idStart, int number, String dbHost,
      String dbName, String dbUser, String dbPasswd) {
    if (wrapperManager.getSocketAddr().equals(addr)) {
      if (null == dbName || dbName.length() == 0)
        dbName = System.getProperty("brn.distsim.db.name");
      if (null == dbUser || dbUser.length() == 0)
        dbUser = System.getProperty("brn.distsim.db.user");
      if (null == dbPasswd || dbPasswd.length() == 0)
        dbPasswd = System.getProperty("brn.distsim.db.password");

      wrapperManager.startWrapper(idStart, number, dbHost, dbName, dbUser, dbPasswd,
          dbHost, dbName, dbUser, dbPasswd);
      return;
    }

    ObjectName name = this.mapHostUrl.get(addr);
    if (null == name) {
      log.error("Distsim on host " + addr + " not attached.");
      throw new IllegalArgumentException("Distsim on host " + addr + " not attached.");
    }

    DistsimManagerMXBean other = getMXBean(name);
    other.startWrapper(addr.getHostName(), idStart, number,
        dbHost, dbName, dbUser, dbPasswd);
  }

  /*
   * (non-Javadoc)
   * @see brn.distsim.DistsimManagerMXBean#startWrapperAll(java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String)
   */
  public void startWrapperAll(String startString, String dbHost, String dbName,
      String dbUser, String dbPasswd) {
    StringTokenizer toki = new StringTokenizer(startString);
    while (toki.hasMoreTokens()) {
      InetSocketAddress socketAddr = this.addrListToArray(toki.nextToken()).get(0);
      String[] idNumber = toki.nextToken().split(":");
      int idStart = Integer.parseInt(idNumber[0]);
      int number = 1;
      if (idNumber.length > 1)
        number = Integer.parseInt(idNumber[1]);

      this.startWrapper(socketAddr, idStart, number, dbHost, dbName, dbUser, dbPasswd);
    }
  }

  /*
   * (non-Javadoc)
   * @see brn.distsim.DistsimManagerMXBean#stopWrapper(java.lang.String)
   */
  public void stopWrapper(String host) {
    InetSocketAddress addr = this.addrListToArray(host).get(0);
    if (wrapperManager.getSocketAddr().equals(addr)) {
      wrapperManager.stopWrapperAll();
      return;
    }

    ObjectName name = this.mapHostUrl.remove(addr);
    if (null == name) {
      log.error("Distsim on " + addr + " not attached.");
      throw new IllegalArgumentException("Distsim on " + addr + " not attached.");
    }

    DistsimManagerMXBean other = getMXBean(name);
    other.stopWrapper(host);
  }

  /*
   * (non-Javadoc)
   * @see brn.distsim.DistsimMXBean#stopWrapperAll()
   */
  public void stopWrapperAll() {
    // first local
    wrapperManager.stopWrapperAll();

    // then remote
    Iterator<ObjectName> iter = mapHostUrl.values().iterator();
    while (null != iter && iter.hasNext()) {
      DistsimManagerMXBean other = getMXBean(iter.next());

      other.stopWrapperAll();
    }
  }

  /*
   * (non-Javadoc)
   * @see brn.distsim.DistsimMXBean#continueWrapperAll()
   */
  public void continueWrapperAll() {
    // first local
    wrapperManager.continueWrapperAll();

    // then remote
    Iterator<ObjectName> iter = mapHostUrl.values().iterator();
    while (null != iter && iter.hasNext()) {
      DistsimManagerMXBean other = getMXBean(iter.next());

      other.continueWrapperAll();
    }
  }

  /*
   * (non-Javadoc)
   * @see brn.distsim.DistsimMXBean#pauseWrapperAll()
   */
  public void pauseWrapperAll() {
    // first local
    wrapperManager.continueWrapperAll();

    // then remote
    Iterator<ObjectName> iter = mapHostUrl.values().iterator();
    while (null != iter && iter.hasNext()) {
      DistsimManagerMXBean other = getMXBean(iter.next());

      other.pauseWrapperAll();
    }
  }

  /*
   * (non-Javadoc)
   * @see brn.distsim.DistsimMXBean#changeDatabaseAll(java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String)
   */
  public void changeDatabaseAll(String dbHost, String dbName, String dbUser,
      String dbPasswd, String dbResHost, String dbResName, String dbResUser,
      String dbResPasswd) {
    // first local
    wrapperManager.changeDatabaseAll(dbHost, dbName, dbUser, dbPasswd,
        dbResHost, dbResName, dbResUser, dbResPasswd);

    // then remote
    Iterator<ObjectName> iter = mapHostUrl.values().iterator();
    while (null != iter && iter.hasNext()) {
      DistsimManagerMXBean other = getMXBean(iter.next());

      other.changeDatabaseAll(dbHost, dbName, dbUser, dbPasswd,
          dbResHost, dbResName, dbResUser, dbResPasswd);
    }
  }

  /*
   * (non-Javadoc)
   * @see brn.distsim.DistsimManagerMXBean#getStartTime()
   */
  public String getStartTime() {
    return startTime.toString();
  }

}
