/*
 * 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.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import javax.management.JMX;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;

import org.apache.log4j.Logger;

import brn.distsim.jmx.DescriptionMBean;
import brn.distsim.wrapper.MainMXBean;

public class WrapperManager extends DescriptionMBean implements WrapperManagerMXBean {
  public static final Logger log = Logger.getLogger(WrapperManager.class.getName());

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

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

  private Map wrapperThreads;

  private InetSocketAddress socketAddr;

  public WrapperManager() {
    super(WrapperManagerMXBean.class, true);
    this.wrapperThreads = Collections.synchronizedMap(new HashMap());

    try {
      this.socketAddr = new InetSocketAddress(InetAddress.getLocalHost(),
          Main.getRmiRegistryPort());
    } catch (UnknownHostException e) {
      log.error("Unable to determine name of localhost",  e);
      // TODO
      throw new RuntimeException(e);
    }

    log.info("running...");
  }

  /**
   * @param id
   * @return a proxy for the {@link MainMXBean}
   */
  private MainMXBean getWrapperMXBean(int id) {
    MBeanServer server = ManagementFactory.getPlatformMBeanServer();
    ObjectName name;
    try {
      name = new ObjectName(brn.distsim.wrapper.Main.getWrapperObjectName(id));
    } catch (MalformedObjectNameException e) {
      e.printStackTrace();
      throw new RuntimeException(e);
    }
    MainMXBean other = (MainMXBean) JMX.newMBeanProxy(server, name, MainMXBean.class);
    return other;
  }

  /**
   * @return the host name
   */
  public InetSocketAddress getSocketAddr() {
    return socketAddr;
  }

  public synchronized void shutDown() {
    log.info("shutting down...");

    this.stopWrapperAll();

    MBeanServer server = ManagementFactory.getPlatformMBeanServer();
    try {
      server.unregisterMBean(new ObjectName(JMX_WRAPPER));
    } catch (Exception e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
      throw new RuntimeException(e);
    }
  }

  /*
   * (non-Javadoc)
   * @see brn.distsim.DistsimMXBean#startWrapper(int, int, 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 startWrapper(int idStart, int number, String dbHost,
      String dbName, String dbUser, String dbPasswd, String dbResHost,
      String dbResName, String dbResUser, String dbResPasswd) {
    if (number <= 0) {
      throw new IllegalArgumentException("number must be non-negative");
    }

    for (int i = 0; i < number; i++) {
      this.startWrapper(idStart+i, dbHost, dbName, dbUser, dbPasswd,
          dbResHost, dbResName, dbResUser, dbResPasswd);
    }
  }

  private void startWrapper(int id, String dbHost, String dbName, String dbUser, String dbPasswd,
      String dbResHost, String dbResName, String dbResUser, String dbResPasswd) {
    if (null != wrapperThreads.get(Integer.valueOf(id))) {
      log.error("Wrapper with id " + id + " already running");
      throw new IllegalArgumentException("Wrapper with id " + id + " already running");
    }
    checkStringParams(dbHost, "dbName");
    checkStringParams(dbName, "dbName");
    checkStringParams(dbUser, "dbUser");
    checkStringParams(dbPasswd, "dbPasswd");
    checkStringParams(dbResHost, "dbResHost");
    checkStringParams(dbResName, "dbResName");
    checkStringParams(dbResUser, "dbResUser");
    checkStringParams(dbResPasswd, "dbResPasswd");


    log.info("Starting wrapper " + id);

    // Prepare confic
    final Properties wrapperProp = new Properties();
    wrapperProp.setProperty("definitions.host", dbHost);
    wrapperProp.setProperty("definitions.database", dbName);
    wrapperProp.setProperty("definitions.username", dbUser);
    wrapperProp.setProperty("definitions.password", dbPasswd);

    wrapperProp.setProperty("results.host", dbResHost);
    wrapperProp.setProperty("results.database", dbResName);
    wrapperProp.setProperty("results.username", dbResUser);
    wrapperProp.setProperty("results.password", dbResPasswd);

    String arch = System.getProperties().getProperty("os.arch") + "-" +
      System.getProperties().getProperty("os.name");
    wrapperProp.setProperty("host.id", String.valueOf(id));
    wrapperProp.setProperty("host.description", "gurkenhannes");
    wrapperProp.setProperty("host.architecture", arch);
    wrapperProp.setProperty("host.name", socketAddr.getHostName());

    // Create ant thread
    Thread thread = new Thread(new Runnable() {
      public void run() {
        try {
          brn.distsim.wrapper.Main.run(wrapperProp);
        } catch (IOException e) {
          // TODO Auto-generated catch block
          e.printStackTrace();
        }
      }
    }, "WrapperThread-"+id);
    thread.setDaemon(true);
    wrapperThreads.put(Integer.valueOf(id), thread);

    // run ant thread
    thread.start();
    Thread.yield();

    // Wait for termination
    MBeanServer server = ManagementFactory.getPlatformMBeanServer();
    try {
      ObjectName name = new ObjectName(brn.distsim.wrapper.Main.getWrapperObjectName(id));
      for (int i = 0; i < 100 && thread.isAlive(); i++) {
        Thread.sleep(100);

          if (!server.isRegistered(name))
            continue;
          break;
      }
    } catch (Exception e) {}

    try {
      ObjectName name = new ObjectName(brn.distsim.wrapper.Main.getWrapperObjectName(id));
      if (!server.isRegistered(name)) {
        stopWrapper(id);
        throw new RuntimeException("Unable to start wrapper " + id);
      }
    } catch (Exception e) {}

    log.info("Wrapper " + id + " started");
  }

  private void checkStringParams(String params, String name) {
    if (params == null || 0 == params.length()) {
      throw new IllegalArgumentException("Parameter <" + name + "> not set");
    }
  }

  /*
   * (non-Javadoc)
   * @see brn.distsim.DistsimMBean#stopWrapper(int)
   */
  public void stopWrapper(int id) {
    Thread thread = (Thread) wrapperThreads.remove(Integer.valueOf(id));
    if (null == thread) {
      log.error("Wrapper with id " + id + " not found");
      throw new IllegalArgumentException("Wrapper with id " + id + " not found");
    }

    // Connect wrapper via jmx and stop
    try {
      MBeanServer server = ManagementFactory.getPlatformMBeanServer();
      ObjectName name = new ObjectName(brn.distsim.wrapper.Main.getWrapperObjectName(id));
      server.invoke(name, "stop", null, null);
    } catch (Exception e) {
      log.error("Unable to stop wrapper via JMX", e);
    }

    // Wait for termination
    try {
      for (int i = 0; i < 100; i++) {
        Thread.sleep(100);
        if (!thread.isAlive())
          break;
      }
    } catch (InterruptedException e) {}

    // Hard termination, if necessary...
    if (thread.isAlive())
      thread.interrupt();
  }

  private void checkDeadWrapperThreads() {
    Iterator iter = wrapperThreads.keySet().iterator();
    while (null != iter && iter.hasNext()) {
      Integer id = (Integer) iter.next();
      Thread thread = (Thread) wrapperThreads.get(id);
      if (null == thread || !thread.isAlive()) {
        try {
          this.stopWrapper(id);
        } catch (Exception e) {}
      }
    }
  }

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

    Set keys = wrapperThreads.keySet();
    Iterator iter = keys.iterator();
    for (int i = 0; i < keys.size(); i++) {
      int id = ((Integer) iter.next()).intValue();
      String simulation = null;

      try {
        MainMXBean other = getWrapperMXBean(id);
        simulation = other.getCurrentSimulation();
      } catch (Exception e) {
        log.error("Unable to get simuliation", e);
        throw new RuntimeException("Unable to query wrapper " + id, e);
      }

      list.add(new WrapperData(id, simulation, this.socketAddr.toString()));
    }

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

  /*
   * (non-Javadoc)
   * @see brn.distsim.DistsimMXBean#stopWrapperAll()
   */
  public void stopWrapperAll() {
    // first local
    while (!wrapperThreads.isEmpty()) {
      Integer id = (Integer) wrapperThreads.keySet().iterator().next();
      this.stopWrapper(id.intValue());
    }
  }

  /*
   * (non-Javadoc)
   * @see brn.distsim.DistsimMXBean#continueWrapperAll()
   */
  public void continueWrapperAll() {
    // first local
    Iterator iter = wrapperThreads.keySet().iterator();
    while (null != iter && iter.hasNext()) {
      int id = ((Integer) iter.next()).intValue();
      try {
        MainMXBean other = getWrapperMXBean(id);
        other.continueSim();
      } catch (Exception e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
        throw new RuntimeException("Unable to call wrapper " + id, e);
      }
    }
  }

  /*
   * (non-Javadoc)
   * @see brn.distsim.DistsimMXBean#pauseWrapperAll()
   */
  public void pauseWrapperAll() {
    // first local
    Iterator iter = wrapperThreads.keySet().iterator();
    while (null != iter && iter.hasNext()) {
      int id = ((Integer) iter.next()).intValue();
      try {
        MainMXBean other = getWrapperMXBean(id);
        other.pauseSim();
      } catch (Exception e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
        throw new RuntimeException("Unable to call wrapper " + id, e);
      }
    }
  }

  /*
   * (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
    Iterator iter = wrapperThreads.keySet().iterator();
    while (null != iter && iter.hasNext()) {
      int id = ((Integer) iter.next()).intValue();
      try {
        MainMXBean other = getWrapperMXBean(id);
        other.changeDatabase(dbHost, dbName, dbUser, dbPasswd,
            dbResHost, dbResName, dbResUser, dbResPasswd);
      } catch (Exception e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
        throw new RuntimeException("Unable to call wrapper " + id, e);
      }
    }
  }

  public void startWrapper(int idStart, int number, String dbHost,
      String dbName, String dbUser, String dbPasswd) {
    this.startWrapper(idStart, number, dbHost, dbName, dbUser, dbPasswd,
        dbHost, dbName, dbUser, dbPasswd);
  }
}
