
package brn.sim;

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.BufferedWriter;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.ObjectOutputStream;
import java.io.Writer;
import java.net.Inet4Address;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import jist.runtime.JistAPI;
import jist.runtime.Main;
import jist.swans.mac.MacAddress;
import jist.swans.misc.Util;
import jist.swans.net.NetAddress;

import org.apache.log4j.Logger;

import brn.distsim.ormapper.util.DBSaver;
import brn.distsim.ormapper.util.DbBinarySaver;
import brn.sim.data.AbstractDiagramData;
import brn.sim.data.AveragedTimeLine;
import brn.sim.data.DiagramData;
import brn.sim.data.DiagramDataHist;
import brn.sim.data.FlowStats;
import brn.sim.data.ForwardGraphData;
import brn.sim.data.Line;
import brn.sim.data.LinkTableData;
import brn.sim.data.PropertiesData;
import brn.sim.data.TimeBar;
import brn.sim.data.TimeLine;
import brn.sim.data.XplotSerializer;

/**
 * All simulationn data to persist or to display must be registered here.
 *
 * TODO add RadioDump, JavisTrace and WiresharkEventHandler
 *
 * TODO write a database manager using xstream xml mapping??
 *
 * @author kurth
 */
public abstract class DataManager {

  public static final Logger log = Logger.getLogger(DataManager.class.getName());

  /** all, including very verbose and BIG! information */
  public static final int LEVEL_ALL              = 4;
  /** additional information */
  public static final int LEVEL_ADDITIONAL       = 3;
  /** data at a basic information (and aggregation) level */
  public static final int LEVEL_BASIC            = 2;
  /** important data */
  // TODO GLOBAL only !!
  public static final int LEVEL_IMPORTANT        = 1;
  /** nothing */
  public static final int LEVEL_OFF              = 0;


  public abstract interface DataContribution {
    int getType();
    String[] getPath();
    void setActive(boolean active);
  }

  public static abstract class DataContributor {
    private DataManager dataManager;
    private boolean registered = false;
    /** the current data contribution level */
    private int level = DataManager.LEVEL_OFF;

    protected int addData(DataContribution data, int level) {
      boolean active = this.level >= level;
      data.setActive(active);
      // do not add if level is to low
      if (!active)
        return -1;
      // TODO callback to data and inform about the current level!
      // data does not have to do anything if the current level is to low!
      return dataManager.addData(this, data);
    }

    protected int addData(String[] path, int type, int level) {
      boolean active = this.level >= level;
      // do not add if level is to low
      if (!active)
        return -1;
      return dataManager.addData(this, path, type);
    }

    public void removeDataManager() {
      this.dataManager = null;
    }

    public void setDataManager(DataManager manager) {
      this.dataManager = manager;
    }

    public DataContribution getContribution(int id, String[] path) {
      return null; // should be handled in data manager ...
    }

    public String getContent(int id, DataContribution contribution) throws Exception {
      if (contribution.getType() == DriverRemote.ITEM_TYPE_DATA) {
        AbstractDiagramData data = (AbstractDiagramData) contribution;
        XplotSerializer seri = new XplotSerializer(data, "green");
        return seri.getContent();
      }
      else if (contribution.getType() == DriverRemote.ITEM_TYPE_ARPTABLE) {
        // TODO
      }
      else if (contribution.getType() == DriverRemote.ITEM_TYPE_LINKTABLE) {
      }
      else if (contribution.getType() == DriverRemote.ITEM_TYPE_FWDGRAPH) {
        // handled in sub-class
      }
      else if (contribution.getType() == DriverRemote.ITEM_TYPE_CONFIG) {
        // handled in sub-class
      }

      return Util.encodeObject(contribution);
    }

    public String getFileName(int id, String[] path, DataContribution contribution) {
      if (contribution.getType() == DriverRemote.ITEM_TYPE_DATA) {
        return getPath(path) + ".xpl";
      }
      else if (contribution.getType() == DriverRemote.ITEM_TYPE_ARPTABLE) {
        return getPath(path) + ".txt";
      }
      else if (contribution.getType() == DriverRemote.ITEM_TYPE_LINKTABLE) {
        return getPath(path) + ".dot";
      }
      else if (contribution.getType() == DriverRemote.ITEM_TYPE_FWDGRAPH) {
        // handled in sub-class
      }
      else if (contribution.getType() == DriverRemote.ITEM_TYPE_CONFIG) {
        // handled in sub-class
      }

      return DataManager.getPath(path);
    }

    public abstract String getId();

    public void registerHandlers() {
      if (registered)
        log.fatal("Data Contributor " + this + " registered more than once");
      registered = true;
    }

    public int getLevel() {
      return level;
    }

    public void setLevel(int level) {
      this.level = level;
    }
  }

  private static class ContributionEntry {
    public int id;
    public DataContribution contribution;
    public DataContributor contributor;
    public String[] path;
    public int type;

    public ContributionEntry(int id, DataContribution contribution, DataContributor contributor) {
      super();
      this.id = id;
      this.contribution = contribution;
      this.contributor = contributor;
      this.path = contribution.getPath();
      this.type = contribution.getType();
    }

    public ContributionEntry(int id, DataContributor contributor, String[] path, int type) {
      super();
      this.id = id;
      this.contribution = null;
      this.contributor = contributor;
      this.path = path;
      this.type = type;
    }

  }

  //////////////////////////////////////////////////
  // locals
  //

  private List lstContributors;

  protected Map mapData;

  private int nextId = -1;

  private PropertyChangeSupport propertyChanged;

  //////////////////////////////////////////////////
  // initialization
  //

  public DataManager() {
    this.lstContributors = new LinkedList();
    this.mapData = new HashMap();
    this.propertyChanged = new PropertyChangeSupport(this);
  }

  public void addPropertyChangeListener(PropertyChangeListener listener) {
    propertyChanged.addPropertyChangeListener(listener);
  }

  public void removePropertyChangeListener(PropertyChangeListener listener) {
    propertyChanged.removePropertyChangeListener(listener);
  }

//  public void addData(AbstractDiagramData collector) {
//    int id = ++nextId;
//    mapData.put(new Integer(id), collector);
//
//    DriverRemote.Item item = new DriverRemote.Item(
//        DriverRemote.ITEM_TYPE_DATA, id, collector.getPath());
//    propertyChanged.firePropertyChange("dataAdded", null, item);
//  }
//
//  public AbstractDiagramData getData(int id) {
//    return (AbstractDiagramData) mapData.get(new Integer(id));
//  }
//
//  public Collection getIds() {
//    return mapData.keySet();
//  }
//
//  public Collection getData() {
//    return mapData.values();
//  }

  //////////////////////////////////////////////////
  // common
  //

  /**
   * Open data store and install all listeners.
   *
   * @throws Exception
   */
  public abstract void open() throws Exception;

  public abstract void close() throws Exception;

  public void add(DataContributor contributor) {
    lstContributors.add(contributor);

    contributor.setDataManager(this);
  }

  public int addData(DataContributor contributor, DataContribution contribution) {
    if (Main.ASSERT)
      Util.assertion(contributor != null);

//    // TODO find a better place
//    if (contribution instanceof AbstractDiagramData) {
//      AbstractDiagramData data = (AbstractDiagramData) contribution;
//      if (!data.isVisible())
//        return -1;
//    }

    // TODO check if path is unique
    int dataId = nextId++;
    mapData.put(new Integer(dataId),
        new ContributionEntry(dataId, contribution, contributor));

    propertyChanged.firePropertyChange("contributionAdded", null,
        new DriverRemote.Item(contribution.getType(), dataId, contribution.getPath()));
    return dataId;
  }

  public int addData(DataContributor contributor, String[] path, int type) {
    if (Main.ASSERT)
      Util.assertion(contributor != null);

    // TODO check if path is nuique
    int dataId = nextId++;
    mapData.put(new Integer(dataId),
        new ContributionEntry(dataId, contributor, path, type));

    propertyChanged.firePropertyChange("contributionAdded", null,
        new DriverRemote.Item(type, dataId, path));
    return dataId;
  }

  public DataContributor getContributor(String id) {
    for (int i = 0; i < lstContributors.size(); i++) {
      DataContributor contributor = (DataContributor) lstContributors.get(i);
      if (id.equals(contributor.getId()))
        return contributor;
    }

    return null;
  }

  public List getContributors() {
    return lstContributors;
  }

  public DataContribution getContribution(int id) {
    ContributionEntry entry = (ContributionEntry) mapData.get(new Integer(id));
    if (entry.contribution != null)
      return entry.contribution;

    return entry.contributor.getContribution(id, entry.path);
  }

  //  /**
//   * add the specified information item.
//   * @param item
//   */
//  protected void addInformationItem(Item item) {
//    if (DriverRemote.ITEM_TYPE_DATA == item.type) {
//      AbstractDiagramData data =
//        AbstractDiagramData.getManager().getData(item.id);
//
//      if (!data.isVisible())
//        return;
//    }
//
//    lstDataItems.add(item);
//  }


  public static String getPath(String[] path) {
    String ret = "";
    for (int i = 0; i < path.length-1; i++)
      ret += path[i] + ", ";
    ret += path[path.length-1];
    return ret;
  }

  //////////////////////////////////////////////////
  // file adapter
  //

  /**
   * Stores all data in an separate directory as files.
   *
   * @author kurth
   */
  public static class File extends DataManager implements JistAPI.DoNotRewrite {

    protected String directory;

    /**
     * Constructs a data manager with file storage support.
     *
     * @param directory the directory for storage (created, if needed)
     */
    public File(String directory) {
      this.directory = directory;
    }

    /*
     * (non-Javadoc)
     * @see brn.sim.DataManager#open()
     */
    public void open() throws Exception {
      (new java.io.File(directory)).mkdirs();
    }

    /*
     * (non-Javadoc)
     * @see brn.sim.DataManager#close()
     */
    public void close() throws Exception {
      Iterator iter = mapData.values().iterator();
      while (null != iter && iter.hasNext()) {
        ContributionEntry entry = (ContributionEntry) iter.next();

        String content = null;
        try {
          content = entry.contributor.getContent(entry.id, entry.contribution);
        } catch (Exception e) {
          e.printStackTrace();
        }
        if (null == content)
          continue;

        String fileName = entry.contributor.getFileName(entry.id, entry.path, entry.contribution);
        fileName = fileName.replace(' ', '_');
        Writer writer = new BufferedWriter(new FileWriter(
            new java.io.File(directory, fileName)));

        writer.write(content);
        writer.flush();
        writer.close();
      }
    }
  }


  //////////////////////////////////////////////////
  // file binary
  //

  public static class FileBinary extends File {

    public FileBinary(String directory) {
      super(directory);
    }

    /*
     * (non-Javadoc)
     * @see brn.sim.DataManager#close()
     */
    public void close() throws Exception {
      Iterator iter = mapData.values().iterator();
      while (null != iter && iter.hasNext()) {
        ContributionEntry entry = (ContributionEntry) iter.next();
        DataContribution contrib = entry.contribution;

        if (null == contrib)
          contrib = entry.contributor.getContribution(entry.id, entry.path);

        String fileName = entry.contributor.getFileName(entry.id, entry.path, entry.contribution);
        fileName = fileName.replace(' ', '_');

        FileOutputStream fos = new FileOutputStream(new java.io.File(directory, fileName));
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(contrib);

        oos.close();
        fos.close();
      }
    }
  }

  //////////////////////////////////////////////////
  // database adapter
  //

  /**
   * Stores all data in database.
   *
   * @author kurth
   */
  public static class Database extends DataManager implements JistAPI.DoNotRewrite {

    /** saves all data in the database */
    protected DbBinarySaver saver;
    private String defUrl;
    private String dbUrl;
    private String dbUser;
    private String dbPasswd;
    private int jobId;

    /**
     * Constructs a data manager with database support. The job id must be
     * known in advance (e.g. from distsim wrapper).
     *
     * @param dbUrl the jdbc url to the database.
     * @param dbUser the database user
     * @param dbPasswd the password to use.
     * @param jobId job id for identification purposes.
     */
    public Database(String dbUrl, String dbUser, String dbPasswd, int jobId) {
      this.dbUrl = dbUrl;
      this.dbUser = dbUser;
      this.dbPasswd = dbPasswd;
      this.jobId = jobId;
      if (Main.ASSERT)
        Util.assertion(jobId != 0
            && null != dbUrl && null != dbUrl && null != dbPasswd);
    }

    /**
     * Constructs a data manager with database support. The job id is generated
     * using the given definitions database.
     *
     * @param defUrl url to the definitions database
     * @param resUrl url to the results database
     * @param dbUser the database user
     * @param dbPasswd the password to use.
     */
    public Database(String defUrl, String resUrl, String dbUser, String dbPasswd) {
      this.defUrl = defUrl;
      this.dbUrl = resUrl;
      this.dbUser = dbUser;
      this.dbPasswd = dbPasswd;
      this.jobId = 0;
      if (Main.ASSERT)
        Util.assertion(null != defUrl
            && null != dbUrl && null != dbUrl && null != dbPasswd);
    }

    /*
     * (non-Javadoc)
     * @see brn.sim.DataManager#open()
     */
    public void open() throws Exception {
      if (jobId != 0)
        saver = new DbBinarySaver(dbUrl, dbUser, dbPasswd, jobId);
      else
        saver = new DbBinarySaver(dbUrl, dbUser, dbPasswd, defUrl, dbUser, dbPasswd);
      jobId = saver.getSimulationId();
    }

    /*
     * (non-Javadoc)
     * @see brn.sim.DataManager#close()
     */
    public void close() throws Exception {
      log.info("saving data...");

      Iterator iter = mapData.values().iterator();
      while (null != iter && iter.hasNext()) {
        ContributionEntry entry = (ContributionEntry) iter.next();
        DataContribution contrib = entry.contribution;
        if (null == contrib)
          contrib = entry.contributor.getContribution(entry.id, entry.path);

        save(entry, contrib, 0);
      }

      try {
        saver.finalize();
      } catch (Throwable e) {
        throw new RuntimeException("couldn't save results", e);
      }
      log.info("finished");
    }

    private void save(ContributionEntry entry, DataContribution contrib, int retry) {
      if (retry > 6)
        throw new RuntimeException("giving up saving - too many retries (contrib "
            +getPath(contrib.getPath()) + ")");

      try {
        saver.save(entry.id, entry.type, getPath(entry.path), contrib);
      } catch (Throwable e) {
        log.error("error during save (contrib "+getPath(contrib.getPath())
            +", retry "+retry+")", e);
        try {
          saver.finalize();
          saver = new DbBinarySaver(dbUrl, dbUser, dbPasswd, jobId);
        } catch (Throwable e1) {
          throw new RuntimeException("couldn't save results", e1);
        }

        save(entry, contrib, retry +1);
      }

      if (retry > 0)
        log.warn("entry saved (contrib "+getPath(contrib.getPath())
          +", retry "+retry+")");
    }
  }

  /**
   * Stores all data in database with hibernate mappings.
   *
   * TODO
   *
   * @author kurth
   */
  public static class HibernateDatabase extends DataManager implements JistAPI.DoNotRewrite {

    /** saves all data in the database */
//    protected DBSaver saver;
    protected DbBinarySaver saver;
    private String defUrl;
    private String dbUrl;
    private String dbUser;
    private String dbPasswd;
    private int jobId;

    private Set classesToMap;

    private static final Class[] mappedClasses = {
      Integer.class,
      Double.class,
      Boolean.class,
      Character.class,
      Number.class,
      Byte.class,
      Long.class,
      Float.class,
//      String.class,
      Inet4Address.class,
      NetAddress.class,
      MacAddress.class,

      HibernateDatabase.Data.class,
      AbstractParams.class,

      Line.class,
      TimeLine.class,
      AveragedTimeLine.class,
      AbstractDiagramData.class,
      DiagramData.class,
//      DiagramDataCuml.class,
      DiagramDataHist.class,
//      DiagramDataRated.class,

      ForwardGraphData.class,
      ForwardGraphData.FwdLink.class,
      ForwardGraphData.Node.class,

      LinkTableData.class,
      LinkTableData.Node.class,
      LinkTableData.Link.class,

      TimeBar.class,
      TimeBar.Entry.class,

      PropertiesData.class,
      FlowStats.class,
    };

    private static final String[] notTomapClasses = {
      "java.lang.String",
      "java.util.AbstractCollection",
      "java.util.AbstractList",
      "java.util.AbstractSet",
      "java.util.AbstractMap",
      "java.util.ArrayList",
      "java.util.HashMap",
      "java.util.HashMap$Entry",
      "java.util.HashMap$EntrySet",
      "java.util.HashSet",
      "java.util.Hashtable",
      "java.util.Hashtable$Entry",
      "java.net.InetAddress",
      "java.net.Inet4Address",
      "java.beans.PropertyChangeListener",
      "java.beans.PropertyChangeSupport",
      "sun.awt.EventListenerAggregate",
    };

    /**
     * Constructs a data manager with database support. The job id must be
     * known in advance (e.g. from distsim wrapper).
     *
     * @param dbUrl the jdbc url to the database.
     * @param dbUser the database user
     * @param dbPasswd the password to use.
     * @param jobId job id for identification purposes.
     */
    public HibernateDatabase(String dbUrl, String dbUser, String dbPasswd, int jobId) {
      this.dbUrl = dbUrl;
      this.dbUser = dbUser;
      this.dbPasswd = dbPasswd;
      this.jobId = jobId;
      assert (jobId != 0);
      classesToMap = new HashSet();
    }

    public static class Data {
      public Map data;

      /**
       * @return the data
       */
      public Map getData() {
        return data;
      }

      /**
       * @param data the data to set
       */
      public void setData(Map data) {
        this.data = data;
      }
    }

    /**
     * Constructs a data manager with database support. The job id is generated
     * using the given definitions database.
     *
     * @param defUrl url to the definitions database
     * @param resUrl url to the results database
     * @param dbUser the database user
     * @param dbPasswd the password to use.
     */
    public HibernateDatabase(String defUrl, String resUrl, String dbUser, String dbPasswd) {
      this.defUrl = defUrl;
      this.dbUrl = resUrl;
      this.dbUser = dbUser;
      this.dbPasswd = dbPasswd;
      this.jobId = 0;
      classesToMap = new HashSet();
    }

    /* (non-Javadoc)
     * @see brn.sim.DataManager#addData(brn.sim.DataManager.DataContributor, brn.sim.DataManager.DataContribution)
     */
    public int addData(DataContributor contributor, DataContribution contribution) {
      classesToMap.add(contribution.getClass());
      return super.addData(contributor, contribution);
    }

    /*
     * (non-Javadoc)
     * @see brn.sim.DataManager#open()
     */
    public void open() throws Exception {
      if (jobId != 0)
        saver = new DbBinarySaver(dbUrl, dbUser, dbPasswd, jobId);
      else
        saver = new DbBinarySaver(dbUrl, dbUser, dbPasswd, defUrl, dbUser, dbPasswd);

//      if (log.isDebugEnabled())
//      log.debug("adding classes to mapping");
//      saver.setClassesNotToMap(notTomapClasses);
//      saver.addToMapping(classesToMap);
//
//      if (log.isDebugEnabled())
//      log.debug("rebuilding session");
//      saver.rebuildSession();
//      if (log.isDebugEnabled())
//      log.debug("init finished");
    }

    /*
     * (non-Javadoc)
     * @see brn.sim.DataManager#close()
     */
    public void close() throws Exception {
//      Data data = new Data();
//      data.data = new HashMap(mapData.size());
//
      log.info("saving data...");
//
//      if (log.isDebugEnabled())
//      log.debug("preparing data");
//      Iterator iter = mapData.values().iterator();
//      while (null != iter && iter.hasNext()) {
//        ContributionEntry entry = (ContributionEntry) iter.next();
//        DataContribution contribution = entry.contribution;
//        if (null == contribution)
//          contribution = entry.contributor.getContribution(entry.id);
//
//        data.data.put(new Integer(entry.id), contribution);
//      }

      try {
//        // TODO really use this whole reflection stuff??
//        if (log.isDebugEnabled())
//        log.debug("inspecting object for mapping update");
//        saver.addToMapping(data);
//
//        if (log.isDebugEnabled())
//        log.debug("rebuilding session");
//        saver.rebuildSession();

        if (log.isDebugEnabled())
          log.debug("saving data");
//        saver.save(data, String.valueOf(jobId), false);
        // the following alternative is much slower (factor 10x)
        Iterator iter = mapData.values().iterator();
        while (null != iter && iter.hasNext()) {
          ContributionEntry entry = (ContributionEntry) iter.next();
          DataContribution contrib = entry.contribution;
          if (null == contrib)
            contrib = entry.contributor.getContribution(entry.id, entry.path);
          saver.save(entry.id, entry.type, getPath(entry.path), contrib);
        }

        if (log.isDebugEnabled())
          log.debug("data saved");
      } catch (Exception e) {
        e.printStackTrace();
      }
//      if (null != saver.getSetBadClasses()
//          && 0 != saver.getSetBadClasses().size())
//        log.error("The following classes are not mapped : " + saver.getSetBadClasses());

      try {
        saver.finalize();
      } catch (Throwable e) {
        throw new RuntimeException("couldn't save results", e);
      }
      log.info("finished");
    }

    /**
     * Statically initializes the database mapping with a prefilled class vector.
     *
     * @param dbUrl  URL to results DB
     * @param dbUser user in results DB
     * @param dbPasswd password in results DB
     * @throws Throwable
     */
    protected static void createMapping(String dbUrl, String dbUser, String dbPasswd) throws Throwable {
      DBSaver saver = new DBSaver(dbUrl, dbUser, dbPasswd, 0);
      saver.setClassesNotToMap(notTomapClasses);
      for (int i = 0; i < mappedClasses.length; i++) {
        saver.addClassToMapping(mappedClasses[i]);
      }
      saver.rebuildSession();
      saver.finalize();
    }
  }

  /**
   * Does not save any data, simply discards them. Useful for testing purposes.
   *
   * @author kurth
   */
  public static class Discard extends DataManager {

    /*
     * (non-Javadoc)
     * @see brn.sim.DataManager#close()
     */
    public void close() throws Exception {
      // pass...
    }

    /*
     * (non-Javadoc)
     * @see brn.sim.DataManager#open()
     */
    public void open() throws Exception {
      // pass...
    }

  }

  /**
   * Init database mappings
   *
   * @param args
   */
  public static void main(String[] args) throws Throwable {
    String dbUrl = args[0];
    String dbUser = args[1];
    String dbPasswd = args[2];

    HibernateDatabase.createMapping(dbUrl, dbUser, dbPasswd);
  }

}
