/*
 * This file is part of the DistSim distributed simulation framework (wrapper)
 * Copyright (C) 2007 Ulf Hermann
 *
 * 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.wrapper;

import java.io.File;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.sql.SQLException;
import java.util.Properties;
import java.util.Random;

import javax.management.MBeanServer;
import javax.management.ObjectName;

public class Main extends UnicastRemoteObject implements RemoteSimulation, MainMXBean {

  private static final long serialVersionUID = -6135718335012679320L;

  private static final String JMX_NAME = "brn.distsim.jmx:type=Wrapper";

  public static class ConfigData {
    private String definitionsHost;
    private String definitionsDatabase;
    private String definitionsUsername;
    private String definitionsPassword;

    private String resultsHost;
    private String resultsDatabase;
    private String resultsUsername;
    private String resultsPassword;

    private int hostId;
    private String hostDescription;
    private String hostArchitecture;
    private String hostName;

    public ConfigData() {
    }
    public ConfigData(String definitionsHost, String definitionsDatabase,
        String definitionsUsername, String definitionsPassword,
        String resultsHost, String resultsDatabase,
        String resultsUsername, String resultsPassword, int hostId,
        String hostDescription, String hostArchitecture, String hostName) {
      super();
      this.definitionsHost = definitionsHost;
      this.definitionsDatabase = definitionsDatabase;
      this.definitionsUsername = definitionsUsername;
      this.definitionsPassword = definitionsPassword;
      this.resultsHost = resultsHost;
      this.resultsDatabase = resultsDatabase;
      this.resultsUsername = resultsUsername;
      this.resultsPassword = resultsPassword;
      this.hostId = hostId;
      this.hostDescription = hostDescription;
      this.hostArchitecture = hostArchitecture;
      this.hostName = hostName;
    }

    public String getDefinitionsHost() {
      return definitionsHost;
    }
    public String getDefinitionsDatabase() {
      return definitionsDatabase;
    }
    public String getDefinitionsUsername() {
      return definitionsUsername;
    }
    public String getDefinitionsPassword() {
      return definitionsPassword;
    }
    public String getResultsHost() {
      return resultsHost;
    }
    public String getResultsDatabase() {
      return resultsDatabase;
    }
    public String getResultsUsername() {
      return resultsUsername;
    }
    public String getResultsPassword() {
      return resultsPassword;
    }
    public int getHostId() {
      return hostId;
    }
    public String getHostDescription() {
      return hostDescription;
    }
    public String getHostArchitecture() {
      return hostArchitecture;
    }
    public String getHostName() {
      return hostName;
    }

    public void setDefinitionsHost(String definitionsHost) {
      this.definitionsHost = definitionsHost;
    }

    public void setDefinitionsDatabase(String definitionsDatabase) {
      this.definitionsDatabase = definitionsDatabase;
    }

    public void setDefinitionsUsername(String definitionsUsername) {
      this.definitionsUsername = definitionsUsername;
    }

    public void setDefinitionsPassword(String definitionsPassword) {
      this.definitionsPassword = definitionsPassword;
    }

    public void setResultsHost(String resultsHost) {
      this.resultsHost = resultsHost;
    }

    public void setResultsDatabase(String resultsDatabase) {
      this.resultsDatabase = resultsDatabase;
    }

    public void setResultsUsername(String resultsUsername) {
      this.resultsUsername = resultsUsername;
    }

    public void setResultsPassword(String resultsPassword) {
      this.resultsPassword = resultsPassword;
    }

    public void setHostId(int hostId) {
      this.hostId = hostId;
    }

    public void setHostDescription(String hostDescription) {
      this.hostDescription = hostDescription;
    }

    public void setHostArchitecture(String hostArchitecture) {
      this.hostArchitecture = hostArchitecture;
    }

    public void setHostName(String hostName) {
      this.hostName = hostName;
    }


  }

  protected Main() throws IOException {
    super();
    configuration = new Properties();
    configuration.load(System.in);
    random = new Random();
    stop = false;
    pause = false;
    workingDir = new File(System.getProperty("user.dir"));
  }

  public Main(Properties properties) throws RemoteException {
    super();
    configuration = (Properties) properties.clone();
    random = new Random();
    stop = false;
    pause = false;
    workingDir = null;
  }

  private DBConnector connector;

  private Properties configuration;

  private Random random;

  private final int delay = 10000;

  private Simulation simulation;

  private boolean stop;

  private boolean pause;

  private Thread runThread;

  private File workingDir;

  private String lastStudyName;

  private String lastStudyVersion;

  private void prepareConnector() {
    while (true) {
      if (null != connector)
        connector.close();
      connector = null;
      if (stop)
        break;
      try {
        connector = new DBConnector(getConfig(),
            workingDir.getAbsolutePath() + File.separator);
        break;
      } catch (SQLException e) {
        e.printStackTrace();
        // ignore
      }

      try {
        Thread.sleep(random.nextInt(delay));
      } catch (InterruptedException e) {
        if (stop)
          break;
      }
    }
  }

  /**
   * @param args
   * @throws IOException
   *             if the properties can't be read
   */
  public static void main(String[] args) throws IOException {
    Main main = new Main();

    try {
      main.run();
    } catch (Throwable e) {
      e.printStackTrace();
      System.err.println("Exiting ...");
      System.exit(1);
    }

    System.exit(0); // force the registry to terminate
  }

  private static boolean registerJmx(Main main, String wrapperObjectName) {
    MBeanServer server = ManagementFactory.getPlatformMBeanServer();
    try {

      ObjectName name = new ObjectName( wrapperObjectName );
      server.registerMBean( main, name );
    } catch (Exception e1) {
      // TODO Auto-generated catch block
      e1.printStackTrace();
      return false;
    }
    return true;
  }

  private static void unregisterJmx(String wrapperObjectName) {
    MBeanServer server = ManagementFactory.getPlatformMBeanServer();
    try {
      ObjectName name = new ObjectName( wrapperObjectName );
      if (server.isRegistered(name))
        server.unregisterMBean(name);
    } catch (Exception e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    }
  }

  private static boolean nuke(File path) throws IOException {
    if (!path.exists())
      return true;
    
    File[] files = path.listFiles();
    for(int i=0; i<files.length; ++i)
    {
       if(files[i].isDirectory())
          nuke(files[i]);
       else
         files[i].delete();
    }
    return path.delete();
  }

  /**
   * Start in-process server. Create temp working dir, create wrapper and register
   * mxbean, run the wrapper, unregister and delete working dir....
   * @param properties
   * @throws IOException
   */
  public static void run(Properties properties) throws IOException {

    Main main = new Main(properties);
    ConfigData config = main.getConfig();

    // create working dir
    File parentDir = new File(System.getProperty("user.dir"));
    File workingDir = File.createTempFile("wrapper"+config.getHostId()+"-", null, parentDir);
    workingDir.delete();
    if (!workingDir.mkdir())
      throw new RuntimeException("Unable to create working dir " + workingDir.getAbsolutePath());
    main.setWorkingDir(workingDir);

    String wrapperObjectName = getWrapperObjectName(config.getHostId());

    try {
      // register bean
      if (!registerJmx(main, wrapperObjectName))
        throw new RuntimeException("Unable to register bean with name  " + wrapperObjectName);

      // run
      main.run();
    } catch (Throwable e) {
      e.printStackTrace();
    } finally {
      // unregister
      unregisterJmx(wrapperObjectName);

      // delete working dir
      if (!nuke(workingDir))
        throw new RuntimeException("Unable to delete directory " + workingDir.getAbsolutePath());
    }
 }

  private void setWorkingDir(File workingDir) {
    this.workingDir = workingDir;
  }

  public void run() throws IOException {
    this.runThread = Thread.currentThread();
    Properties jobConfig = new Properties();
    try {
      while (!stop) {
        try {
          try {
            Thread.sleep(random.nextInt(delay));
          } catch (InterruptedException e) {
          }
          if (stop)
            return;

          if (null == connector)
            prepareConnector();

          if (pause)
            continue;
          if (stop)
            return;

          simulation = connector.query(jobConfig);
          if (simulation != null) {
            if (!checkWorkingDir(workingDir, simulation)) {
              System.out.println("simulation canceled");
              connector.jobCanceled();
              simulation = null;
              continue;
            }
            jobConfig.putAll(configuration);
            if (connector.queryPackagesAndFiles(simulation)
                && simulation.execute(jobConfig, workingDir)) {
              System.out.println("simulation finished");
              connector.jobFinished();
            } else {
              System.out.println("simulation canceled");
              connector.jobCanceled();
            }
            jobConfig.clear();
            simulation = null;
            continue;
          }
        } catch (SQLException e) {
          prepareConnector();
        }
      }
    } finally {
      if (null != connector)
        connector.close();
    }
  }

  /**
   * Delete all files in working dir, if the simulation version changes. 
   * @param workingDir
   * @param simulation
   * @throws IOException 
   */
  private boolean checkWorkingDir(File workingDir, Simulation simulation) {
    try {
      if (!simulation.getStudyName().equals(lastStudyName)
        || !simulation.getStudyVersion().equals(lastStudyVersion)) {
        System.out.println("Clearing working directory for new study " + 
            simulation.getStudyName());
        // delete working dir
        if (!nuke(workingDir))
          throw new RuntimeException("Unable to delete directory " 
              + workingDir.getAbsolutePath());
  
        if (!workingDir.mkdir())
          throw new RuntimeException("Unable to create working dir " 
              + workingDir.getAbsolutePath());
      }
    } catch (IOException e) {
      e.printStackTrace();
      return false;
    } finally {    
      this.lastStudyName = simulation.getStudyName();
      this.lastStudyVersion = simulation.getStudyVersion();
    }
    return true;
  }

  public int getId() {
    return Integer.parseInt(configuration.getProperty("host.id").trim());
  }

  public String getDescription() {
    return configuration.getProperty("host.description").trim();
  }

  public String getArchitecture() {
    return configuration.getProperty("host.architecture").trim();
  }

  public boolean cancel() {
    if (simulation != null)
      simulation.destroy();
    return true;
  }

  public boolean stop()  {
    stop = true;
    if (simulation != null)
      simulation.destroy();
    System.out.println("stopping");
    this.runThread.interrupt();
    return true;
  }

  public boolean stopLater()  {
    stop = true;
    return true;
  }

  public String getName() {
    return configuration.getProperty("host.name").trim();
  }

  /*
   * (non-Javadoc)
   * @see brn.distsim.wrapper.MainMBean#cancelSim()
   */
  public boolean cancelSim() {
    return cancel();
  }

  /*
   * (non-Javadoc)
   * @see brn.distsim.wrapper.MainMBean#continueSim()
   */
  public void continueSim() {
    this.pause = false;
  }

  /*
   * (non-Javadoc)
   * @see brn.distsim.wrapper.MainMBean#pauseSim()
   */
  public void pauseSim() {
    this.pause = true;
  }

  /*
   * (non-Javadoc)
   * @see brn.distsim.wrapper.MainMXBean#getCurrentSimulation()
   */
  public String getCurrentSimulation() {
    return (simulation == null ? "None" : simulation.toString());
  }


  /*
   * (non-Javadoc)
   * @see brn.distsim.wrapper.MainMXBean#changeDatabase(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 changeDatabase(String dbHost, String dbName, String dbUser,
      String dbPasswd, String dbResHost, String dbResName,
      String dbResUser, String dbResPasswd) {
    configuration.setProperty("definitions.host", dbHost);
    configuration.setProperty("definitions.database", dbName);
    configuration.setProperty("definitions.username", dbUser);
    configuration.setProperty("definitions.password", dbPasswd);
    configuration.setProperty("results.host", dbResHost);
    configuration.setProperty("results.database", dbResName);
    configuration.setProperty("results.username", dbResUser);
    configuration.setProperty("results.password", dbResPasswd);
    connector = null;
  }

  /*
   * (non-Javadoc)
   * @see brn.distsim.wrapper.MainMXBean#getConfig()
   */
  public ConfigData getConfig() {
    return new ConfigData(configuration.getProperty("definitions.host")
        .trim(), configuration.getProperty("definitions.database")
        .trim(), configuration.getProperty("definitions.username")
        .trim(), configuration.getProperty("definitions.password")
        .trim(),

    configuration.getProperty("results.host").trim(), configuration
        .getProperty("results.database").trim(), configuration
        .getProperty("results.username").trim(), configuration
        .getProperty("results.password").trim(),

    Integer.parseInt(configuration.getProperty("host.id")), configuration
        .getProperty("host.description").trim(), configuration
        .getProperty("host.architecture").trim(), configuration
        .getProperty("host.name").trim());
  }

  /*
   * (non-Javadoc)
   * @see brn.distsim.wrapper.MainMXBean#getBlackList()
   */
  public Integer[] getBlackList() {
    return (Integer[]) connector.getBlackList().toArray(new Integer[0]);
  }

  public File getWorkingDir() {
    return workingDir;
  }

  public static String getWrapperObjectName(int id) {
    return brn.distsim.wrapper.Main.JMX_NAME + ",name="+id;
  }

}
