package brn.swans.route.metric;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;

import jist.runtime.JistAPI;
import jist.runtime.Main;
import jist.runtime.Util;
import jist.swans.Constants;

import org.apache.log4j.Logger;

import brn.swans.route.metric.AbstractRouteMetric.LinkMetricChangedEvent;
import brn.swans.route.metric.RouteMetricInterface.EventListener;

/**
 * Keeps a Link state database and calculates Weighted Shortest Path for other
 * elements.
 * <p/>
 * Runs dijkstra's algorithm occasionally.
 *
 * @author Zubow
 * @version 1.0
 */
public abstract class LinkTable {

  /**
   * Represents a tupel pair in the link table.
   */
  protected static class ObjectPair {

    protected Object from;

    protected Object to;

    public ObjectPair() {
    }

    public ObjectPair(Object from, Object to) {
      this.from = from;
      this.to = to;
    }

    public boolean contains(Object foo) {
      if (foo.equals(to))
        return true;
      return foo.equals(from);
    }

    public boolean equals(Object oth) {
      if (this == oth) {
        return true;
      }

      if (oth == null) {
        return false;
      }

      if (oth.getClass() != getClass()) {
        return false;
      }

      ObjectPair other = (ObjectPair) oth;
      if (this.from == null) {
        if (other.from != null) {
          return false;
        }
      } else {
        if (!this.from.equals(other.from)) {
          return false;
        }
      }
      if (this.to == null) {
        if (other.to != null) {
          return false;
        }
      } else {
        if (!this.to.equals(other.to)) {
          return false;
        }
      }

      return true;
    }

    public int hashCode() {
      final int PRIME = 1000003;
      int result = 0;
      if (from != null) {
        result = PRIME * result + from.hashCode();
      }
      if (to != null) {
        result = PRIME * result + to.hashCode();
      }

      return result;
    }

    public String toString() {
      return from.toString() + " -> " + to.toString();
    }
  }

  /**
   * The metric must be positive value in descending order, i.e. smaller values
   * are better.
   *
   * TODO we only know the etx of local links. links which are placed in here
   * from route discovery do not have such information. will this be a problem
   * for the querier??
   *
   * @author kurth
   */
  public static class LinkData extends ObjectPair implements Cloneable  {
    protected int metric = Integer.MAX_VALUE;

    /** etx = 1/(1-PER) */
    protected int etx = -1;

    protected long seq = -1;

    protected int age;

    protected boolean permanent;

    protected long lastUpdated;

    private LinkTable linkTable;

    protected LinkData() {
    }

    public Object from() {
      return from;
    }

    public Object to() {
      return to;
    }

    public long seq() {
      return seq;
    }

    public int metric() {
      return metric;
    }

    public int age() {
      long now = JistAPI.getTime();
      return (int) (age + ((now - lastUpdated) / Constants.MILLI_SECOND / 1000));
    }

    public String toString() {
      return from + " -> " + to + " : " + metric;
    }

    public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;

      final LinkData linkInfo = (LinkData) o;

      if (from != null ? !from.equals(linkInfo.from) : linkInfo.from != null) return false;
      if (to != null ? !to.equals(linkInfo.to) : linkInfo.to != null) return false;

      return true;
    }

    public int hashCode() {
      int result;
      result = (from != null ? from.hashCode() : 0);
      result = 29 * result + (to != null ? to.hashCode() : 0);
      return result;
    }

    public int etx() {
      return etx;
    }

    public Object clone() {
      try {
        return super.clone();
      } catch (CloneNotSupportedException e) {
        throw new RuntimeException(e);
      }
    }

    public void update(long seq, int age, int metric, int etx) {
      if (Main.ASSERT) {
        Util.assertion(metric > 0);
        Util.assertion(etx >= 100);
      }
      // AZu: new sequence number must be greater than the old one;
      // otherwise no update will be taken
//      if (Main.ASSERT)
//        Util.assertion (this.seq <= seq);
//      link.seq ++;
      if (seq <= this.seq)
        return;

      // handle event
      if (linkTable.eventListeners.size() != 0)
        linkTable.handleOnUpdateEvent(this.from, this.to, this.metric, metric);

      int oldMetric = this.metric;

      this.seq = seq;
      this.age = age;
      this.metric = metric;
      this.etx = etx;
      lastUpdated = JistAPI.getTime();

      if (oldMetric != metric && linkTable.linkMetricChangedEvent.isActive())
        linkTable.linkMetricChangedEvent.handle(this, LinkMetricChangedEvent.UPDATED);
    }

    private void setLinkTable(LinkTable linkTable) {
      this.linkTable = linkTable;
    }
  }

  protected class HostData implements Cloneable {
    public Object ip;

    // indicates whether the current node is active or not.
    protected boolean active;

    protected HostData() {
      this.active = true;
    }

    public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;

      final HostData hostInfo = (HostData) o;

      if (active != hostInfo.active) return false;
      if (ip != null ? !ip.equals(hostInfo.ip) : hostInfo.ip != null) return false;

      return true;
    }

    public int hashCode() {
      int result;
      result = (ip != null ? ip.hashCode() : 0);
      result = 29 * result + (active ? 1 : 0);
      return result;
    }

    public Object clone() {
      try {
        return super.clone();
      } catch (CloneNotSupportedException e) {
        throw new RuntimeException(e);
      }
    }
  }


  /**
   * logger.
   */
  private static Logger log = Logger.getLogger(LinkTable.class.getName());


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

  private long staleTimeout = RouteEtxEttMetric.STALE_TIMEOUT;

  protected LinkMetricChangedEvent linkMetricChangedEvent;

  private List eventListeners;

  /** prototype for link data creation */
  private LinkData prototypeLink = new LinkData();

  /** prototype for host data creation */
  private HostData prototypeHost = new HostData();


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

  public LinkTable() {
    this.eventListeners = new ArrayList();
  }

  public LinkTable(long tau) {
    this();
    this.staleTimeout = tau;
  }

  public int hashCode() {
    return 1;
  }

  public void addEventListener(EventListener listener) {
    this.eventListeners.add(listener);
  }

  public void handleOnUpdateEvent(Object from, Object to, int old_metric, int metric) {
    for (int i = 0; i < eventListeners.size(); i++) {
      EventListener eventListener = (EventListener) eventListeners.get(i);
      eventListener.onUpdate(from, to, old_metric, metric);
    }
  }

  // ////////////////////////////////////////////////
  // accessors
  //

  public LinkMetricChangedEvent getLinkMetricChangedEvent() {
    return linkMetricChangedEvent;
  }

  public void setLinkMetricChangedEvent(
      LinkMetricChangedEvent linkMetricChangedEvent) {
    this.linkMetricChangedEvent = linkMetricChangedEvent;
  }

  public void setPrototypeLink(LinkData prototype) {
    this.prototypeLink = prototype;
  }

  public void setPrototypeHost(HostData prototype) {
    this.prototypeHost = prototype;
  }

  // ////////////////////////////////////////////////
  // overwrites
  //

  public abstract HostData getHostInfo(Object from);

  protected abstract void setHostInfo(Object from, HostData nfrom);

  public abstract Collection /* HostInfo */ getHosts();

  public abstract LinkData getLinkInfo(Object from, Object to);

  protected abstract void setLinkInfo(Object from, Object to, LinkData lnfo);

  public abstract Collection /* LinkInfo */ getLinks();

  // ////////////////////////////////////////////////
  // operations
  //

  /**
   * Returns the host information to the given ip address. Creates the entry,
   * if necessary.
   *
   * @param from the ip address of the host to retrieve
   * @return the host info
   */
  public HostData getHost(Object from) {
    /* make sure both the hosts exist */
    HostData nfrom = getHostInfo(from);
    if (nfrom== null) {
      nfrom = (HostData) prototypeHost.clone();
      nfrom.ip = from;
      setHostInfo(from, nfrom);
    }
    return nfrom;
  }

  /**
   * Returns the link information to the given ip address. Creates the entry,
   * if necessary.
   *
   * @param from the ip address of the link to retrieve
   * @param to the ip address of the link to retrieve
   * @return the host info
   */
  public LinkData getLink(Object from, Object to) {
    // create host entries
    getHost(from);
    getHost(to);

    LinkData lnfo = getLinkInfo(from, to);
    if (lnfo == null) {
      lnfo = (LinkData) prototypeLink.clone();
      lnfo.setLinkTable(this);
      lnfo.from = from;
      lnfo.to = to;
      setLinkInfo(from, to, lnfo);
      if (linkMetricChangedEvent.isActive())
        linkMetricChangedEvent.handle(lnfo, LinkMetricChangedEvent.CREATED);
    }

    return lnfo;
  }

  public String printLinks() {
    StringBuffer sa = new StringBuffer();

    Iterator iter = getLinks().iterator();
    while (iter != null && iter.hasNext()) {
      LinkData n = (LinkData) iter.next();
      sa.append(n.from()).append(" ").append(n.to());
      sa.append(" ").append(n.metric());
      sa.append(" ").append(n.seq).append(" ").append(n.age()).append("\n");
    }
    return sa.toString();
  }

  public void clearStale() {
    Iterator iter = getLinks().iterator();

    while (iter != null && iter.hasNext()) {
      LinkData nfo = (LinkData) iter.next();

      if (staleTimeout < nfo.age() && !nfo.permanent) {
        if (linkMetricChangedEvent.isActive())
          linkMetricChangedEvent.handle(nfo, LinkMetricChangedEvent.DELETED);

        setLinkInfo(nfo.from, nfo.to, null);

        if (log.isDebugEnabled()) {
          log.debug(" * LinkTable: link " + nfo.from + " -> " + nfo.to
              + " timed out.\n");
        }
      }
    }
  }

  public void removeLink(Object a, Object b) {
    setLinkInfo(a, b, null);
  }

}