// TerraViz.java by Martin Reddy, 1 Apr 1998.
// Copyright (C) 1998 SRI International

import java.awt.*;
import java.lang.*;
import java.applet.*;
import vrml.external.*;
import vrml.external.field.*;
import vrml.external.exception.*; 
import netscape.javascript.JSObject;

public class terraviz extends Applet {

  /* Some state variables to hold all dataset instances */

  public static final int MAX_DATASETS = 10;

  String[] dataset_name  = new String[MAX_DATASETS];
  String[] dataset_label = new String[MAX_DATASETS];
  int      numb_datasets = 0;

  String   models_name = null;

  /* The Browser instance - needed to do anything with the EAI */
  
  Browser browser = null;

  /* Strings for the AWT buttons */

  String  buttons[] = new String[MAX_DATASETS];
  String  buildings_on_button = "Load Features";
  String  buildings_off_button = "Unload Features";
  String  refresh_button = "Refresh State";
  String  reload_buildings_button = "Reload Features";
  String  reload_all_button = "Reload All";

  /* Status and busy state */

  boolean busy = false;
  String  status_line = new String("");

  /* variables to store the current state */

  String  curr_dataset   = null;
  boolean curr_buildings = false;

  // A simple Trace function that will output text to the Java console

  public void Trace( String message ) {
    System.out.println( message );
  }

  // Set the busy flag and output a suitable message 
  // in the description area of the VRML plugin

  public void SetBusy( boolean status ) {
    busy = status;
    if ( busy ) {
      browser.setDescription( "Updating scene graph..." ); 
      Status( "Updating scene graph..." );
    } else {
      browser.setDescription( "" );
      Status( "" );
    }
  }

  // The following function performs a Browser.getBrowser() call
  // in order to get a hold of the VRML scene in the same page
  // as this applet. We put this in a loop to get round problems
  // where the VRML plugin is not loaded in time.

  public Browser FindBrowser() {
    browser = null;

    for ( int count = 0; count < 666; count++ ) {

      /* Browser.getBrowser( this, "VRML", 0 ) doesn't seem to work */
      /* properly so we have to use a netscape-specific workaround  */

      JSObject win    = JSObject.getWindow(this);
      JSObject top    = (JSObject) win.getMember("top");
      JSObject frame  = (JSObject) top.getMember("VRML");  /* Frame name */
      JSObject doc    = (JSObject) frame.getMember("document");
      JSObject embeds = (JSObject) doc.getMember("embeds");
      browser         = (Browser) embeds.getSlot(0);

      if ( browser != null ) break;
      try { Thread.sleep( 100 ); }
      catch (InterruptedException ignored) {}
    }

    if ( browser == null ) {
      throw new Error( "Failed to get the browser!" );
    }

    return browser;
  }

  // Return the Node that is named "root_node" in the scene graph

  public Node FindRootNode() {

    /* Start off by making sure we have the Browser instance. */
    /* We do this every time in case a new scene has been     */
    /* loaded since that last time we came here. That way we  */
    /* don't need to manually refresh the instance, or do any */
    /* callback stuff to workout when a world has changed.    */

    if ( FindBrowser() == null ) return null;
    
    /* getting a hold of the root group node in the scene. */
    /* This must be named, e.g. DEF root_node Group {...   */

    Node root_node = browser.getNode( "root_node" );
    if ( root_node == null ) return null;

    return root_node;
  }

  // This routine is used to parse strings that are in the
  // format of "token: <string1> <string2>". This is used
  // for reading the Dataset: and Models: tags in the .wrl file.
  // The index value can be 0 or 1 (clamped to 1).

  public String ParseTokenValue( String in_string, String token, int index ) {
    StringBuffer buffer;
    int          in_string_len = in_string.length();
    int          pos = token.length();

    /* Skip initial whitespace after the tag */

    while ( pos < in_string_len && in_string.charAt(pos) == ' ' ) 
      pos++;

    /* Copy the first whitespace-delimited string */
	     
    buffer = new StringBuffer( "" );

    while ( pos < in_string_len && in_string.charAt(pos) != ' ' ) {
      buffer.append( in_string.charAt(pos) );
      pos++;
    }

    if ( index == 0 )
      return new String( buffer.toString() );

    /* skip the delimiting whitespace */

    while ( pos < in_string_len && in_string.charAt(pos) == ' ' ) 
      pos++;

    /* Read in the rest of the string as the dataset label */

    buffer = new StringBuffer( "" );

    while ( pos < in_string_len ) {
      buffer.append( in_string.charAt(pos) );
      pos++;
    }

    return new String( buffer.toString() );
  }

  // Try to find the NavigationInfo node in the scene & extract
  // all of the Dataset: strings and parse them. Currently we
  // assume that the NavigationInfo node is a direct child of
  // the root node in the scene (the one called "root_node").
  // N.B. we use NavigationInfo rather than WorldInfo because
  // the fields of WorldInfo are not exposedFields.

  public void FindDatasets() {
    numb_datasets = 0;

    Node root_node = FindRootNode();
    if ( root_node == null ) return;

    EventOutMFNode children = (EventOutMFNode)
      root_node.getEventOut( "children" );

    int num_nodes = children.getSize();
    Node nodes[]  = children.getValue();

    /* traverse through each node in the top level group */

    for ( int i = 0; i < num_nodes; ++i ) {
      String node_type = nodes[i].getType();

      if ( node_type.compareTo( "NavigationInfo" ) == 0 ) {

	/* Read the first URL string from the Inline's url field */

	EventOutMFString info_field = (EventOutMFString)
	  nodes[i].getEventOut( "type" );

	int    infos_len = info_field.getSize();
	String infos[]   = info_field.getValue();

	/* Go through each of the strings in the string array */
	/* and work out which ones begin with "Dataset:". The */
	/* format of these entries are:                       */
	/*  "Dataset:"[<whitespace>]<name><whitespace><label> */
	/* where <whitespace> is one or more spaces.          */

	for ( int l = 0; l < infos_len; ++l ) {
	  if ( infos[l].indexOf( "Dataset:" ) == 0 &&
	       numb_datasets < MAX_DATASETS ) {

	    dataset_name[numb_datasets] = ParseTokenValue( infos[l],
							   "Dataset:", 0 );
	    dataset_label[numb_datasets] = ParseTokenValue( infos[l],
							    "Dataset:", 1 );

	    numb_datasets++;

	  } else if ( infos[l].indexOf( "Models:" ) == 0 ) {
	    models_name = ParseTokenValue( infos[l], "Models:", 0 );
	  }
	}

      }
    }

  }

  // First is the init() function which is called once only
  // Here we add the AWT buttons to the HTML page

  public void init() {

    /* Do a getBrowser call and find all Dataset tags in the .wrl */

    FindDatasets();

    /* Add buttons for all of the different dataset types */

    for ( int i = 0; i < numb_datasets; ++i ) {
      buttons[i] = new String( dataset_label[i] );
      add( new Button( buttons[i] ) );
    }

    /* Add any other buttons, e.g. refresh, reload, etc. */

    add( new Button( buildings_on_button ) );
    add( new Button( buildings_off_button ) );
    add( new Button( refresh_button ) );
    /*add( new Button( reload_buildings_button ) );*/
    add( new Button( reload_all_button ) );

    /* Display some information on the Java Console */

    System.out.println( "TerraViz - Martin Reddy, 1 Apr 1998" );
    if ( browser == null )
      System.out.println( "getBrowser failed!" );
    else {
      System.out.println( "Browser = " + browser.getName() );
      System.out.println( "Version = " + browser.getVersion() );
    }
  }

  // Now we have the start() function which is called on refresh
  // Here we try and get hooks to the VRML plugin instance.

  public void start() {

    /* reconnect to the current VRML plug in and find dataset tags */
    /* output a trace message to say which dataset tags we found   */

    FindDatasets();

    System.out.println( "---> " + numb_datasets + " Dataset Tags Read:" );
    for ( int i = 0; i < numb_datasets; ++i ) {
      System.out.println( "Dataset: " + dataset_name[i]
			  + " (" + dataset_label[i] + ")");
    }
    System.out.println( "Models: " + models_name );
    System.out.println( "----" );
  }

  // set_url will find all Inline nodes and change the url
  // field of these, as long it contains a matching string.

  public void set_inline( String new_url, String match ) {

    Trace( "Entering set_inline()" );

    Node root_node = FindRootNode();
    if ( root_node == null ) return;

    SetBusy( true );

    /* Now get the "children" field of the group node and pass */
    /* this to set_inline_rec to recursively traverse & update */
    /* We put this inside a beginUpdate()..endUpdate() group   */
    /* so that we only refresh the scene once, at the end.     */

    /* browser.beginUpdate(); */ /* not implement in IRIX/CP1.02 */

    set_inline_recur( (EventOutMFNode) root_node.getEventOut( "children" ),
		      new_url, match );

    /* browser.endUpdate(); */

    /* take a copy of the current state */

    SetBusy( false );
  }

  // set_switch_recur is a recursive method called by set_switch.
  // it will traverse all of the children nodes in a grouping node
  // and if any Switch nodes are found, it will change the value
  // of the whichChoice field to "value". If another grouping node
  // is encountered, then this member is invoked recursively.

  public void set_inline_recur( EventOutMFNode children, String new_url,
				String match ) {
    if ( children == null ) return;

    /* Now get the "children" field of the group node and work */
    /* out how many nodes are contained in this group.         */

    int num_nodes = children.getSize();
    Node nodes[]  = children.getValue();

    /* traverse through each node in the group, checking for Switches */

    for ( int i = 0; i < num_nodes; ++i ) {
      String node_type = nodes[i].getType();

      if ( node_type.compareTo( "Inline" ) == 0 ) {

	/* Read the first URL string from the Inline's url field */

	EventOutMFString curr_urls = (EventOutMFString)
	  nodes[i].getEventOut( "url" );
	String curr_url = curr_urls.get1Value( 0 );

	/* Only continue if the url contains the matching string */
	/* A matching string of "none" matches an empty url.     */

	int match_index = curr_url.indexOf( match );
	if ( match_index >= 0 ||
	     ( match.equals("none") && curr_url.equals("") ) ) {

	  /* create the root part of the new url - we then */
	  /* just have to add the particular vtile's level */
	  /* and filename to this URL.                     */
	  
	  StringBuffer new_full_url = new StringBuffer( new_url );
	  
	  /* Now add the rest of the string from the original */
	  /* URL that immediately comes after the match string */
	  /* But not if new_url = "" (preserver the empty url) */
	  
	  if ( new_full_url.length() > 0 ) {
	    for ( int j = match_index + match.length(); 
		  j < curr_url.length(); ++j ) {
	      new_full_url.append( curr_url.charAt(j) );
	    }
	  }
	  
	  System.out.println( "Convert Inline " + curr_url + " to " +
			      new_full_url.toString() );

	  /* Now we have the new url for the Inline - set it! */
	  /* But let's be intelligent and check if it's the same */
	  
	  if ( new_full_url.toString().equals( curr_url ) == false ) {
	    EventInMFString new_urls = (EventInMFString)
	      nodes[i].getEventIn( "url" );
	    
	    new_urls.set1Value( 0, new_full_url.toString() );
	    
	  }
	}

      } else if ( node_type.compareTo( "Group" ) == 0 ||
		  node_type.compareTo( "Transform" ) == 0 ||
		  node_type.compareTo( "Anchor" ) == 0 ||
		  node_type.compareTo( "Collision" ) == 0 ) {
	
	/* If we have a grouping node, then recursively check */
	/* all of the children of this node also.             */

	set_inline_recur( (EventOutMFNode) nodes[i].getEventOut( "children" ),
			  new_url, match );

      } else if ( node_type.compareTo( "LOD" ) == 0 )  {
	
	/* If we have an LOD node, then recursively check */
	/* all of the nodes in its level array            */

	set_inline_recur( (EventOutMFNode) nodes[i].getEventOut( "level" ),
			  new_url, match );
      }
    }
  }

  // A wrapper method for the generic set_inline.
  // This is used to switch between vtile sets.

  public void set_dataset( String name ) {
    set_inline( name, "vtiles" );
    curr_dataset = name;
  }

  // A wrapper method for the generic set_inline.
  // This is used to switch building models on and off.

  public void set_buildings( boolean status ) {
    Status( "Updating cultural features..." );
    repaint();
    if ( status == true )
      set_inline( models_name, "none" );
    else
      set_inline( "", "Models" );
    curr_buildings = status;
    Status( "" );
    repaint();
  }

  // The following function will reapply the last state to
  // the current world, loading any new data if necessary.

  public void reapply_state() {
    FindBrowser();

    if ( curr_dataset != null )
      set_dataset( curr_dataset );

    set_buildings( curr_buildings );
  }

  // Reload the current scene from disk and preserve the current state

  public void reload_world() {
    FindBrowser();
    if ( browser == null ) return;

    /* Get the URL for the current scene */

    String urls[] = new String[1];
    if ( ( urls[0] = browser.getWorldURL() ) == null ) return;

    browser.setDescription( "Reloading " + urls[0] );

    /* Create a new Group node with nothing in it, then load */
    /* the current URL into the children node of this Group. */

    Node[] tmp = browser.createVrmlFromString( "Group { children [] } " );
    browser.createVrmlFromURL( urls, tmp[0], "children" );

    /* do a sleeping poll of the children node so that we wait */
    /* until the world has been fully loaded. We check this by */
    /* sleeping until we have >0 children in the new Group.    */

    boolean loaded = false;
    while ( loaded == false ) {
      EventOutMFNode curr = (EventOutMFNode) tmp[0].getEventOut("children");
      if ( curr.getSize() > 0 )
	loaded = true;
      else
	try { Thread.sleep( 100 ); } catch( InterruptedException e ) {}
    }

    /* Now replace the current world with the new world */
    /* and also refresh the current state               */

    EventOutMFNode new_wrl = (EventOutMFNode) tmp[0].getEventOut("children");
    browser.replaceWorld( new_wrl.getValue() );

    reapply_state();
  }

  // The button event handling routine

  public boolean action( Event event, Object what ) {

    /* Ignore this event if we are already doing something */

    if ( busy ) {
      Trace( "Ignoring button event - I'm busy" );
      return true;
    }

    if ( event.target instanceof Button ) {
      Button b = (Button) event.target;

      /* Check for a click on a dataset button */
      
      for ( int i = 0; i < numb_datasets; ++i ) {
	if ( b.getLabel() == buttons[i] ) {
	  set_dataset( dataset_name[i] );
	}
      }

      /* Check for a click on another type of button */

      if ( b.getLabel() == refresh_button ) {
	reapply_state();
      } else if ( b.getLabel() == reload_all_button ) {
	reload_world();
      } else if ( b.getLabel() == reload_buildings_button ) {
	set_buildings( false );
	set_buildings( true );
      } else if ( b.getLabel() == buildings_on_button ) {
	set_buildings( false );
	set_buildings( true );
      } else if ( b.getLabel() == buildings_off_button ) {
	set_buildings( false );
      }
    }

    return true;
  }

  // display some text in a Status field 

  public void Status( String text ) {
    Graphics g = this.getGraphics();
    Dimension d = this.size();

    int x1 = 0;
    int x2 = d.width-1;
    int y1 = d.height - 20;
    int y2 = 19;

    g.setColor( Color.lightGray );
    g.fillRect( x1, y1, x2, y2 );

    g.setColor( Color.black );
    g.drawRect( x1, y1, x2, y2 );

    if ( text == null ) text = status_line;
   
    g.drawString( text, x1 + 8, y1 + 14 );
    
    status_line = text;
  }

  // Set the background colour for the applet viewport

  public void paint( Graphics g ) {
    Dimension d = this.size();
    g.setColor( Color.white );
    g.fillRect( 0, 0, d.width, d.height );
    Status( null );
  }

  // Return information suitable for an About dialog box

  public String getAppletInfo() {
    return "TerraViz v0.1 by Martin Reddy, 1 Apr 1998.";
  }

}
