/*
 * $Id$
 *
 * Copyright (c) 2000-2007 by Rodney Kinney
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License (LGPL) as published by the Free Software Foundation.
 *
 * This library 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, copies are available
 * at http://www.opensource.org.
 */
/*
 * Copyright (c) 2003 by Rodney Kinney.  All rights reserved.
 * Date: May 29, 2003
 */
package VASSAL.chat.node;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;
import java.util.logging.Logger;

import VASSAL.tools.PropertiesEncoder;
import VASSAL.tools.SequenceEncoder;

/**
 * Base class for the hierarchical server model.
 * Each node has a name, a list of children, and arbitrary extra information
 * encoded in the {@link #getInfo} string. Each node can be identified
 * globally by a path name. Messages sent to a node generally broadcast to
 * all descendents of the node.
 */
public class Node implements MsgSender {
  private static Logger logger = Logger.getLogger(MsgSender.class.getName());
  private String id;
  private String info;
  private Node parent;
  private List<Node> children = new ArrayList<Node>();

  public Node(Node parent, String id, String info) {
    this.parent = parent;
    this.id = id;
    this.info = info;
  }

  public String getId() {
    return id;
  }

  public String getInfo() {
    return info;
  }

  public String getInfoProperty(String propName) {
    try {
      return new PropertiesEncoder(info).getProperties().getProperty(propName);
    }
    catch (IOException e) {
      return null;
    }
  }

  public void setInfo(String info) {
    this.info = info;
  }

  public void setParent(Node parent) {
    this.parent = parent;
  }

  public Node getParent() {
    return parent;
  }

  public void remove(Node child) {
    logger.finer("Removing "+child+" from "+this); //$NON-NLS-1$ //$NON-NLS-2$
    children.remove(child);
  }

  public void add(Node child) {
    if (child.parent != null) {
      child.parent.remove(child);
    }
    logger.finer("Adding "+child+" to "+this); //$NON-NLS-1$ //$NON-NLS-2$
    children.add(child);
    child.setParent(this);
  }

  public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof Node)) return false;

    final Node node = (Node) o;

    if (id != null ? !id.equals(node.id) : node.id != null) return false;

    return true;
  }

  public int hashCode() {
    return (id != null ? id.hashCode() : 0);
  }

  public String toString() {
    return super.toString()+"[id="+id+"]"; //$NON-NLS-1$ //$NON-NLS-2$
  }

  /**
   * return the child node with the given id, or null if no match
   * @param id
   * @return
   */
  public Node getChild(String id) {
    for (Node n : getChildren()) {
      if (id.equals(n.getId())) {
        return n;
      }
    }
    return null;
  }

  /**
   * Return the descendant node with the given path relative to this node
   * @param path
   * @return
   */
  public Node getDescendant(String path) {
    Node n = this;
    SequenceEncoder.Decoder st = new SequenceEncoder.Decoder(path,'/');
    while (st.hasMoreTokens() && n != null) {
      String id = st.nextToken();
      n = n.getChild(id);
    }
    return n;
  }

  public void send(String msg) {
    for (Node n : getChildren()) {
      n.send(msg);
    }
  }

  public Node[] getLeafDescendants() {
    ArrayList<Node> l = new ArrayList<Node>();
    addLeaves(this, l);
    return l.toArray(new Node[l.size()]);
  }

  private void addLeaves(Node base, List<Node> l) {
    if (base.isLeaf()) {
      l.add(base);
    }
    else {
      for (Node n : base.getChildren()) {
        addLeaves(n,l);
      }
    }
  }

  public boolean isLeaf() {
    return false;
  }

  public Node[] getChildren() {
    synchronized (children) {
      return children.toArray(new Node[children.size()]);
    }
  }

  /**
   * Constructs from a path name.
   * Instantiates parent nodes with appropriate names as necessary.
   * @param base the top-level ancestor of the node to be built.
   *             Its name is not included in the path name
   * @param path
   * @return
   */
  public static Node build(Node base, String path) {
    Node node = null;
    SequenceEncoder.Decoder st = new SequenceEncoder.Decoder(path, '/');
    while (st.hasMoreTokens()) {
      String childId = st.nextToken();
      node = base.getChild(childId);
      if (node == null) {
        node = new Node(base, childId, null);
        base.add(node);
      }
      base = node;
    }
    return node;
  }

  /**
   * Builds a Node from a pathAndInfo string
   * @see #getPathAndInfo
   * @param base
   * @param pathAndInfo
   * @return
   */
  public Node buildWithInfo(Node base, String pathAndInfo) {
    Node node = null;
    SequenceEncoder.Decoder st = new SequenceEncoder.Decoder(pathAndInfo, '/');
    while (st.hasMoreTokens()) {
      SequenceEncoder.Decoder st2 = new SequenceEncoder.Decoder(st.nextToken(),'=');
      String childId = st2.nextToken();
      String childInfo = st2.nextToken();
      node = base.getChild(childId);
      if (node == null) {
        node = new Node(base, childId, null);
        base.add(node);
      }
      node.setInfo(childInfo);
      base = node;
    }
    return node;
  }

  private List<Node> getPathList() {
    ArrayList<Node> path = new ArrayList<Node>();
    for (Node n = this; n != null && n.getId() != null; n = n.getParent()) {
      path.add(n);
    }
    return path;
  }

  public String getPath() {
    synchronized (children) {
      SequenceEncoder se = new SequenceEncoder('/');
      List<Node> path = getPathList();
      for (ListIterator<Node> i = path.listIterator(path.size());
           i.hasPrevious(); ) {
        se.append(i.previous().getId());
      }
      return se.getValue();
    }
  }

  /**
   * Return a string in the format parentId=parentInfo/childId=childInfo/...
   * @return
   */
  public String getPathAndInfo() {
    synchronized (children) {
      SequenceEncoder se = new SequenceEncoder('/');
      List<Node> path = getPathList();
      for (ListIterator<Node> i = path.listIterator(path.size()-1);
           i.hasPrevious(); ) {
        Node n = i.previous();
        SequenceEncoder se2 =
          new SequenceEncoder(n.getId(),'=').append(n.getInfo());
        se.append(se2.getValue());
      }
      return se.getValue();
    }
  }
}

@@ -2356,24 +2356,174 @@
 de child) %7B%0A
+    // FIXME: added this to find out what is calling add(null)%0A    if (child == null) %7B%0A      throw new NullPointerException(%22child == null%22);%0A    %7D%0A%0A
     if (chil

Throw an NPE on Node.add(null), so we can see what's adding nulls.