// Object tree - Site map version.
// Copyright (C) December 2003 Neil Fraser
// http://neil.fraser.name/software/tree

// This program is free software; you can redistribute it and/or
// modify it under the terms of version 2 of the GNU General
// Public License as published by the Free Software Foundation.

// 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.
// http://www.gnu.org/

import java.awt.*; // Needed for the scrollbars and other GUI stuff.
import java.net.*; // Needed for fetching the data file over the net.
import java.io.*;  // Needed for streaming in the datafile.


public class tree extends java.applet.Applet {
  // This class is the main Applet.  It is split into two panels,
  // the search panel (with a textfield, and two buttons), and
  // the scroll panel (with the graphics and two scrollbars).
  // Initialization and the search functionality are handled here.

  boolean bulletlink = false;
  boolean arrowlink = false;
  boolean textopen = false;
  boolean singleopen = false;

  Color textcolor = Color.black;
  Color bulletcolor = Color.black;
  Color arrowcolor = Color.blue;
  Font f;
  FontMetrics fmetrics;
  int fquantum;

  String errorMessage[] = {null, null};
  URL urlBase;
  String sTargetFrame = "";
  String sGet = "";

  tree_panel scrollPanel = new tree_panel(this);
  Panel searchPanel = new Panel();
  TextField findtext = null;
  Button findnext = null;
  Button findprev = null;


  public void init() {
    super.init();

    // Define the GUI elements.
    this.setLayout(new BorderLayout());
    this.add ("Center", scrollPanel);

    scrollPanel.vscroll = new Scrollbar(Scrollbar.VERTICAL);
    scrollPanel.hscroll = new Scrollbar(Scrollbar.HORIZONTAL);
    scrollPanel.vscroll.show(false);
    scrollPanel.hscroll.show(false);
    scrollPanel.add(scrollPanel.vscroll);
    scrollPanel.add(scrollPanel.hscroll);

    String find = getParameter("find");
    if (find != null) {
      searchPanel.setLayout(new FlowLayout());
      searchPanel.add(findtext = new TextField(10));
      // allow html parameter to set text on buttons (for multilingual support)
      searchPanel.add(findnext = new Button(getParameter("findnext") == null ? "Find next" : getParameter("findnext")));
      searchPanel.add(findprev = new Button(getParameter("findprev") == null ? "Find prev" : getParameter("findprev")));
      findprev.enable(false);
      if (find.equals("North") || find.equals("South") || find.equals("East") || find.equals("West")) {
        this.add (find, searchPanel);
      } else {
        errorMessage[0] = "find parameter must be one of:";
        errorMessage[1] = "North, South, East, West.";
        return;
      }
    }

    // Deal with other parameters.
    String datafile = getParameter("datafile");
    if (datafile == null)
      datafile = "data.txt";

    sTargetFrame = getParameter("target");
    if (sTargetFrame == null)
      sTargetFrame = "_self";    // use same frame as applet

    sGet = getParameter("get");
    if (sGet == null)
      sGet = "";

    String linkbase = getParameter("linkbase");
    if (linkbase == null) {
      urlBase = getDocumentBase();
    } else if (linkbase.equals("")) {
      urlBase = null;
    } else {
      try {
        urlBase = new URL(linkbase);
      } catch (MalformedURLException e) {
        errorMessage[0] = "Bad linkbase URL: " + linkbase;
        errorMessage[1] = e.toString();
        return;
      }
    }

    String colorparam = getParameter("bgcolor");
    if (colorparam != null)
      try {
        scrollPanel.setBackground(new Color(Integer.parseInt(colorparam, 16)));
        searchPanel.setBackground(new Color(Integer.parseInt(colorparam, 16)));
      } catch(Exception e) {
        errorMessage[0] = "Invalid bgcolor: " + colorparam;
        errorMessage[1] = e.toString();
        return;
      }
    colorparam = getParameter("textcolor");
    if (colorparam != null)
      try {
        textcolor = new Color(Integer.parseInt(colorparam, 16));
      } catch(Exception e) {
        errorMessage[0] = "Invalid textcolor: " + colorparam;
        errorMessage[1] = e.toString();
        return;
      }
    colorparam = getParameter("bulletcolor");
    if (colorparam != null)
      try {
        bulletcolor = new Color(Integer.parseInt(colorparam, 16));
      } catch(Exception e) {
        errorMessage[0] = "Invalid bulletcolor: " + colorparam;
        errorMessage[1] = e.toString();
        return;
      }
    colorparam = getParameter("arrowcolor");
    if (colorparam != null)
      try {
        arrowcolor = new Color(Integer.parseInt(colorparam, 16));
      } catch(Exception e) {
        errorMessage[0] = "Invalid arrowcolor: " + colorparam;
        errorMessage[1] = e.toString();
        return;
      }

    String fontname = getParameter("fontname");
    if (fontname == null)
      fontname = "TimesRoman";
    String fontsize = getParameter("fontsize");
    if (fontsize == null)
      fontsize = "12";
    try {
      f = new Font(fontname, Font.PLAIN, Integer.parseInt(fontsize));
    } catch(Exception e) {
      errorMessage[0] = "Invalid font parameter(s).";
      errorMessage[1] = e.toString();
      return;
    }
    fmetrics = getFontMetrics(f);
    fquantum = fmetrics.getHeight();

    // Define some subtle behaviour settings.
    bulletlink = (getParameter("bulletlink") != null && getParameter("bulletlink").equals("1"));
    arrowlink = (getParameter("arrowlink") != null && getParameter("arrowlink").equals("1"));
    textopen = (getParameter("textopen") != null && getParameter("textopen").equals("1"));
    singleopen = (getParameter("singleopen") != null && getParameter("singleopen").equals("1"));

    // Load the data file and construct the nodes.
    System.out.println("Loading the data file...");
    try {
      DataInput fstream = new DataInputStream(new URL(getCodeBase(), datafile).openStream());
      String currentLine = fstream.readLine();
      if (currentLine == null) {
        errorMessage[0] = "Data file is empty!";
        errorMessage[1] = datafile;
        return;
      }
      int level;
      String title;
      tree_node currentNode = new tree_node(null, 0, currentLine.trim());
      scrollPanel.root = currentNode;
      currentLine = fstream.readLine();
      while (currentLine != null) {
        if (currentLine.trim().length() == 0) {
          // Ignore blank lines.
          currentLine = fstream.readLine();
          continue;
        }
        title = currentLine;
        level = 0;
        while (title.length() != 0 && Character.isSpace(title.charAt(0))) {
          title = title.substring(1);
          level++;
        }
        if (level < 1) {
          errorMessage[0] = "This entry isn't a child of a previous entry:";
          errorMessage[1] = currentLine;
          return;
        } else if (level > currentNode.level() + 1) {
          errorMessage[0] = "This entry is more than one step deeper than its parent:";
          errorMessage[1] = currentLine;
          return;
        }
        // Back off until we meet the new node's parent.
        while (level <= currentNode.level())
          currentNode = currentNode.parent();
        currentNode = new tree_node(currentNode, level, title);
        currentLine = fstream.readLine();
      }
    } catch(Exception e) {
      errorMessage[0] = "Error while loading datafile.";
      errorMessage[1] = e.toString();
      return;
    }
    System.out.println("...finished parsing data file.");
    scrollPanel.root.open = true;
  }


  public boolean keyDown(Event event, int arg) {
    // Pressing 'Enter' in the search field is the same as pressing 'Find next'.
    if (event.target == findtext && arg == 10) {
      action(new Event(findnext, Event.ACTION_EVENT, null), null);
      return true;
    }
    return false;
  }


  public boolean action(Event event, Object arg) {
    // Deal with 'Find next' and 'Find prev' button hits.
    if (event.target != findnext && event.target != findprev)
      return false;

    if (findtext.getText().length() == 0)
      return true;

    tree_node pointer = scrollPanel.getSelected();
    String findme = findtext.getText().toLowerCase();
    do {
      pointer = ((pointer == null) ? scrollPanel.root : ((event.target == findnext) ? pointer.nextNode(false) : pointer.prevNode(false)));
      if (pointer != null && pointer.title().toLowerCase().indexOf(findme) != -1) {
        pointer.openParents(singleopen);
        scrollPanel.setSelected(pointer);
        break;
      }
    } while (pointer != null);

    findtext.requestFocus();
    return true;
  }


  public boolean handleEvent (Event event) {
    // If a scrollbar is touched, repaint the graphic panel.
    if (event.target == scrollPanel.hscroll || event.target == scrollPanel.vscroll)
      if (event.id==Event.SCROLL_ABSOLUTE ||
          event.id==Event.SCROLL_LINE_UP || event.id==Event.SCROLL_LINE_DOWN ||
          event.id==Event.SCROLL_PAGE_UP || event.id==Event.SCROLL_PAGE_DOWN)
        scrollPanel.repaint();

    return super.handleEvent(event);
  }


  public void start () {
    // Force all subcomponents to be repainted.
    scrollPanel.reshapeBars = true;
    scrollPanel.repaint();
  }

}

// ---------------------------------------------------------------------------

class tree_panel extends Panel {
  // This class is a panel on which the tree is stored and painted.
  // All rendering and scrollbar fiddling is done here.

  Scrollbar vscroll = new Scrollbar(Scrollbar.VERTICAL);
  Scrollbar hscroll = new Scrollbar(Scrollbar.HORIZONTAL);
  tree_node root;
  private tree host;
  private tree_node selected = null;
  private tree_node target = null;
  boolean reshapeBars = true;


  public tree_panel(tree host) {
    // Construct a graphics panel and set its host.
    this.host = host;
  }


  public void paint(Graphics g) {
    // Redraw the screen from scratch.
    if (host.errorMessage[0] != null) {
      System.out.println("> "+host.errorMessage[0]);
      System.out.println("> "+host.errorMessage[1]);
      g.drawString(host.errorMessage[0], 10, 20);
      g.drawString(host.errorMessage[1], 10, 50);
      return;
    }
    paintComponents(g);

    g.setFont(host.f);
    int fquantum = host.fquantum;
    int indent;
    int y = -vscroll.getValue() * fquantum;
    tree_node victim = root;
    int rank;
    int maxheight = 1; // Inflate by one to dodge IE scrollbar bug.
    int maxwidth = 0;
    int selectedy = 0;

    int block1 = 0;
    int block2 = 0;
    int block3 = 0;
    int block4 = 0;
    boolean block_enabled = false;

    do {
      y += fquantum;
      if (victim == selected)
        selectedy = y;

      // print the object
      if (size().height > y - fquantum && y >= 0) {
        indent = 2 + (victim.level() * fquantum);
        if (hscroll.isVisible())
          indent -= hscroll.getValue() * fquantum;

        if (victim == this.selected) {
          // Sometimes Netscape 4 fails to clear before redrawing.
          // Therefore make sure that the selected block is clear before XORing.
          g.setPaintMode();
          g.setColor(this.getBackground());
          g.fillRect(indent, y - fquantum + fquantum/8 + 1, host.fmetrics.stringWidth(victim.title()), fquantum);
        }
        g.setColor(host.textcolor);
        g.drawString(victim.title(), indent, y);
        if (victim == this.selected) {
          // Record where to draw the highlight block.
          block1 = indent;
          block2 = y - fquantum + fquantum/8 + 1;
          block3 = host.fmetrics.stringWidth(victim.title());
          block4 = fquantum;
          block_enabled = true;
        }
        if (victim == root) {
          // Don't draw a bullet or arrow on the root.
        } else if (victim.hasKids()) {
          g.setColor(host.arrowcolor);
          if (victim.open) {
            int[] triX = {indent - fquantum*7/8, indent - fquantum/8, indent - fquantum/2};
            int[] triY = {y - fquantum*3/8, y - fquantum*3/8, y};
            g.fillPolygon(triX, triY, 3);
          } else {
            int[] triX = {indent - fquantum*5/8, indent - fquantum*5/8, indent - fquantum/4};
            int[] triY = {y - fquantum*5/8 , y + fquantum/8, y - fquantum/4};
            g.fillPolygon(triX, triY, 3);
          }
        } else {
          g.setColor(host.bulletcolor);
          g.fillOval(indent - fquantum + fquantum/4, y - fquantum/2, fquantum/2-1, fquantum/2-1);
        }
      }
      maxwidth = Math.max(maxwidth, 1 + (victim.level() * fquantum) + host.fmetrics.stringWidth(victim.title()));
      maxheight++;

      // find the next object
      victim = victim.nextNode(true);

    } while (victim != null);

    int scrollwidth = 16;
    int hspace = size().width - (vscroll.isVisible() ? scrollwidth : 0);
    int vspace = size().height - (hscroll.isVisible() ? scrollwidth : 0);
    if (reshapeBars) {
      hscroll.reshape(0, vspace, hspace, scrollwidth);
      vscroll.reshape(hspace, 0, scrollwidth, vspace);
      reshapeBars = false;
    }

    // reconfigure the horizontal scrollbar
    if (maxwidth > hspace) {
      boolean overshoot = (hscroll.getValue() * fquantum + hspace > maxwidth + fquantum);
      // hscroll.setValues(hscroll.getValue(), hspace, 0, maxwidth + 1);
      // proportional scrollbars (above) don't work in NS3 & NS4 & IE3
      hscroll.setValues(hscroll.getValue(), 1, 0, (maxwidth - hspace) / fquantum + 1);
      if (!hscroll.isVisible()) {
        hspace -= scrollwidth;
        hscroll.setValue(0);
        hscroll.setLineIncrement(1);
        hscroll.show(true);
        reshapeBars = true;
      } else if (overshoot) {
        repaint();
        return;
      }
      hscroll.setPageIncrement(hspace/host.fquantum - 1);
    } else if (hscroll.isVisible()) {
      hscroll.setValue(0);
      hscroll.show(false);
      reshapeBars = true;
    }

    // reconfigure the vertical scrollbar
    if (maxheight > vspace/fquantum) {
      boolean overshoot = (vscroll.getValue() + vspace/fquantum > maxheight);
      // vscroll.setValues(vscroll.getValue(), vspace/fquantum, 0, maxheight + 1);
      // again, no proportional scrollbars.  Grrr...
      vscroll.setValues(vscroll.getValue(), 1, 0, maxheight - vspace/fquantum);
      if (!vscroll.isVisible()) {
        vspace -= scrollwidth;
        vscroll.setValue(0);
        vscroll.setLineIncrement(1);
        vscroll.show(true);
        reshapeBars = true;
      } else if (overshoot) {
        repaint();
        return;
      }
      vscroll.setPageIncrement(vspace/host.fquantum - 1);
    } else if (vscroll.isVisible()) {
      vscroll.setValue(0);
      vscroll.show(false);
      reshapeBars = true;
    }

    if (reshapeBars) {
      repaint();
      return;
    }

    // Clear the embarassing hole between the scrollbars.
    if (hscroll.isVisible() && vscroll.isVisible())
      g.clearRect(hspace, vspace, size().width, size().height);

    // Scroll to a location that can see the recently selected object.
    if (this.target != null) {
      int oldv = vscroll.getValue();
      int oldh = hscroll.getValue();

      if (vscroll.isVisible()) {
        if (selectedy <= 0 + fquantum)
          vscroll.setValue(vscroll.getValue() + selectedy/fquantum - 1);
        if (vspace <= selectedy)
          vscroll.setValue(vscroll.getValue() + (selectedy - vspace)/fquantum + 1);
      }

      if (hscroll.isVisible()) {
        hscroll.setValue(Math.max(hscroll.getValue(), target.level()-2 + (host.fmetrics.stringWidth(target.title()) - hspace + 2)/fquantum));
        hscroll.setValue(Math.min(hscroll.getValue(), target.level()-3));
      }
      this.target = null;

      if (oldv != vscroll.getValue() || oldh != hscroll.getValue())
        repaint();
    }

    // Determine if the Next or Prev buttons should be enabled.
    if (host.findnext != null) {
      host.findnext.enable(selected == null || selected.nextNode(false) != null);
      host.findprev.enable(selected != null && selected.prevNode(false) != null);
    }

    // Invert the highlighted item.
    // Do this last, because in Java 1.4.0 (and other recent versions) a call to setXORMode breaks further graphics calls.
    if (block_enabled) {
      g.setColor(Color.black);
      g.setXORMode(Color.white);
      g.fillRect(block1, block2, block3, block4);
      g.setPaintMode();
    }
  }


  public boolean mouseDown(Event evt, int x, int y) {
    // Determine what the user is clicking on.
    if (hscroll.isVisible() && y > size().height - hscroll.size().height)
      return false;
    if (vscroll.isVisible() && x > size().width - vscroll.size().width)
      return false;

    tree_node victim = root;
    int fquantum = host.fquantum;
    int height = y - fquantum - 2;
    if (vscroll.isVisible())
      height += vscroll.getValue() * fquantum;
    int pseudox = x;
    if (hscroll.isVisible())
      pseudox += hscroll.getValue() * fquantum;

    while ((height > 0) && (victim != null)) {
      height -= fquantum;
      victim = victim.nextNode(true);
    }
    if (victim == null)
      return true;

    if (pseudox <= 1 + (victim.level() * fquantum) && victim != root) {
      // clicking on arrow or bullet
      if (victim.hasKids()) {
        // open or close a branch
        victim.openParents(host.singleopen);
        victim.open = !victim.open;
        if (host.arrowlink) {
          // jump to hyperlink
          setSelected(victim);
          if (host.urlBase != null && victim.url(host.urlBase, host.sGet) != null) {
            System.out.println("Linking to: "+victim.url(host.urlBase, host.sGet));
            host.getAppletContext().showDocument(victim.url(host.urlBase, host.sGet), host.sTargetFrame);
          }
        }
        repaint();
      } else
        if (host.bulletlink) {
          // jump to hyperlink
          setSelected(victim);
          if (host.urlBase != null && victim.url(host.urlBase, host.sGet) != null) {
            System.out.println("Linking to: "+victim.url(host.urlBase, host.sGet));
            host.getAppletContext().showDocument(victim.url(host.urlBase, host.sGet), host.sTargetFrame);
          }
        }
    } else {
      // clicking on text
      // jump to hyperlink
      setSelected(victim);
      if (host.urlBase != null && victim.url(host.urlBase, host.sGet) != null) {
        System.out.println("Linking to: "+victim.url(host.urlBase, host.sGet));
        host.getAppletContext().showDocument(victim.url(host.urlBase, host.sGet), host.sTargetFrame);
      }
      if (host.textopen && victim.hasKids() && victim != root) {
        victim.openParents(host.singleopen);
        victim.open = !victim.open;
      }
    }
    return true;
  }


  public void setSelected(tree_node pointer) {
    // Sets the selected node.
    // Opens its parents, tweaks the scrollbars, and redraws.
    this.selected = pointer;
    pointer.openParents(host.singleopen);
    this.target = pointer;

    repaint();
  }


  public tree_node getSelected() {
    // Get method for selected node.
    return this.selected;
  }

}

// ---------------------------------------------------------------------------

class tree_node {
  // This class represents one object on the tree.  Each object has a
  // parent, and may have many child objects.  Note that the nodes are
  // all direct instances of the 'tree_node' class, they aren't really kids
  // of each other.  The relationship between them is defined by the
  // 'parent' and 'kids' properties.

  private String title;
  private tree_node parent;
  private tree_node[] kids;
  private int level;
  public boolean open = false;


  public tree_node (tree_node parentNode, int newlevel, String newtitle) {
    // Create a new node with the given characteristics.
    level = newlevel;
    title = newtitle;
    parent = parentNode;
    kids = new tree_node[0];

    // Tell my parent that I exist.
    if (parent != null)
      parent.addKid(this);
  }


  public void addKid (tree_node newNode) {
    // Add a new node into this node's list of kids.
    tree_node[] oldKids = kids;
    kids = new tree_node[oldKids.length + 1];
    for (int i = 0; i < oldKids.length; i++)
      kids[i] = oldKids[i];
    kids[kids.length-1] = newNode;
  }


  public tree_node nextNode(boolean onlyOpen) {
    // Return the node right below this one.
    // If onlyOpen is true, ignore kids of closed nodes.
    if ((open || !onlyOpen) && (kids.length > 0))
      return kids[0];

    int rank;
    boolean stepUp;
    tree_node victim = this;
    do {
      rank = 0;
      if (victim.parent == null)
        return null;
      while (victim.parent.kids[rank] != victim)
        rank++;
      victim = victim.parent;
      stepUp = (rank + 1 == victim.kids.length);
      stepUp = (rank + 1 == victim.kids.length);
      // Netscape 3.01 needs to have this line repeated.
    } while (stepUp);
    return victim.kids[rank + 1];
  }


  public tree_node prevNode(boolean onlyOpen) {
    // Return the node right above this one.
    // If onlyOpen is true, ignore kids of closed nodes.
    if (parent == null)
      return null;

    if ((!parent.open && onlyOpen) || (parent.kids[0] == this))
      return parent;

    int rank = 1;
    while (parent.kids[rank] != this)
      rank++;

    tree_node victim = this.parent.kids[rank - 1];

    while ((victim.open || !onlyOpen) && (victim.kids.length > 0))
      victim = victim.kids[victim.kids.length - 1];
    return victim;
  }


  public void openParents(boolean singleopen) {
    // Open all parents of this node, so that it will be visible.
    if (parent != null) {
      if (singleopen && this.hasKids())
        for (int i = 0; i < parent.kids.length; i++)
          if (parent.kids[i] != this)
            parent.kids[i].open = false;
      parent.open = true;
      parent.openParents(singleopen);
    }
  }


  public boolean hasKids() {
    // Does this node have any kids?
    return (kids.length != 0);
  }


  public int level() {
    // How deep is the node? (root=0)
    return this.level;
  }


  public tree_node parent() {
    // Who is my parent?
    return this.parent;
  }


  public URL url(URL base, String sGet) {
    // Return the URL that this node points to (strip out the title).
    String addy = title;
    int start = addy.lastIndexOf("|");
    if (start == -1)
      return null;
    addy = addy.substring(start + 1).trim() + sGet;
    try {
      return new URL(base, addy);
    } catch (MalformedURLException e) {
      return null;
    }
  }


  public String title() {
    // Return the title of the node (strip out the URL).
    int end = title.lastIndexOf("|");
    if (end == -1)
      return title.trim();
    else
      return title.substring(0, end).trim();
  }
}


