/*
 *      @(#)Browser.java 1.168 99/03/24 15:31:16
 *
 * Copyright (c) 1996-1998 Sun Microsystems, Inc. All Rights Reserved.
 *
 * Sun grants you ("Licensee") a non-exclusive, royalty free, license to use,
 * modify and redistribute this software in source and binary code form,
 * provided that i) this copyright notice and license appear on all copies of
 * the software; and ii) Licensee does not utilize the software in a manner
 * which is disparaging to Sun.
 *
 * This software is provided "AS IS," without a warranty of any kind. ALL
 * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING ANY
 * IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE OR
 * NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN AND ITS LICENSORS SHALL NOT BE
 * LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING
 * OR DISTRIBUTING THE SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN OR ITS
 * LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR DIRECT,
 * INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER
 * CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF THE USE OF
 * OR INABILITY TO USE SOFTWARE, EVEN IF SUN HAS BEEN ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGES.
 *
 * This software is not designed or intended for use in on-line control of
 * aircraft, air traffic, aircraft navigation or aircraft communications; or in
 * the design, construction, operation or maintenance of any nuclear
 * facility. Licensee represents and warrants that it will not use or
 * redistribute the Software for such purposes.
 */

/*
 * @Author: Rick Goldberg
 * @Author: Doug Gehringer
 *
 */


package com.sun.j3d.loaders.vrml97.impl;

import java.awt.AWTEvent;
import java.awt.Frame;
import java.applet.*;
import java.net.URL;
import java.net.URLConnection;
import java.util.Hashtable;
import java.util.Stack;
import java.util.Vector;
import java.util.Enumeration;
import java.io.InputStream;
import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.ByteArrayInputStream;
import java.lang.System;
import com.sun.j3d.audioengines.javasound.*;

import javax.media.j3d.*;
import javax.vecmath.*;

/**
 * Browser J3D structure:
 * <p>
 * The browser has two kinds of J3D structures: permanent structures which
 * are changed the current scene and reset for each URL, and a scene
 * branchgraph which holds the J3D tree parsed from the current VRML url.
 *
 * <p>
 * The permanent BranchGroup is called browserRoot.  It holds:
 * <UL>
 * <LI> the browser's behaviors (which control the VRML runtime) </LI>
 * <LI> a FogSlot and a BackgroundSlot which groups which hold the currently
 *      active VRML Fog or Background </LI>
 * <LI> The default ViewPlatform, associated with the default Viewpoint </LI>
 * </UL>
 * <p>
 * Views:
 * <p>
 * There is one View, PhysicalBody and PhysicalEnvironment which are used 
 * by the currently active Viewpoint and NavigationInfo.  
 * <p>
 * Each file Viewpoint defines a new ViewPlaform, which get associated with
 * the view when the viewpoint is bound.
 * <p>
 * There are default Viewpoint, NavigationInfo, Fog and Background VRML nodes
 * which are used if the file does not define them
 * <p>
 * The initial VP, NI, Fog and BG are the value first seen from the file.  This
 * value will be bound in loadFinalize().
 * <p>
 * The VP, NI, Fog and BG stacks are used after the file is loaded.  When each
 * node is bound, if it is already on the stack, it removed itself from the
 * stack and then it pushes itself onto the stack.  The top entry on the stack
 * is the currently active value.
 * <p>
 * The browser also has an ambient light and a directional light (the
 * headlight).  These are added to the TransformGroup which holds the
 * ViewPlatform associated with the current Viewpoint.
 *
 */


public class Browser {

    // VRML data
    String 			name="Java3D VRML'97 Browser";
    String 			version="unknown";
    String 			description;
    float 			speed;
    float 			frameRate;

    // J3D stuff owned by the browser
    Canvas3D			canvas;
    VirtualUniverse		universe;
    Locale			locale;
    PhysicalBody                body;
    PhysicalEnvironment         environment;
    View                        view;
    AudioDevice3D		audioDevice;

    // the browser's permanent state
    BranchGroup			browserRoot;
    javax.media.j3d.DirectionalLight browserDirLight;
    javax.media.j3d.AmbientLight browserAmbLight;
    Evagation			evagation; // browser behaviors
    SimTicker 			simTicker;
    FrameCounter		frameCount;

    BranchGroup			browserSoundAtts;
    BranchGroup			browserLightGroup;
    RGroup			browserBackgroundSlot;
    RGroup			browserFogSlot;

    // Bindable Node stacks
    Stack 			viewpointStack = null;
    Stack 			navigationInfoStack = null;
    Stack 			fogStack = null;
    Stack 			backgroundStack = null;

    // default VRML objects, used if file does not define these
    Viewpoint			defViewpoint;
    NavigationInfo		defNavInfo;
    SphereSensor		sceneExaminer;
    Background			defBackground;
    Fog				defFog;
    WorldInfo			defWorldInfo;

    // The loader which parses input files
    protected Loader			loader;

    // Stuff loaded from the current URL
    BranchGroup 		curScene; // top level J3D nodes from the file
    SceneTransform		curSceneT;
    BoundingSphere		sceneBounds;
    Vector 			viewpointList;
    // list of SharedGroups to compile
    Vector 			sharedGroups;
    // lists of active sensors that the browser must pass events to
    Vector 			timeSensors;
    Vector 			visibilitySensors;
    Vector 			touchSensors;
    Vector 			audioClips;
    // giles 12/18/1999  Add access to DEFed objects
    Hashtable			namedObjs;
    // the initial values specified by the current file.  Null if not specified
    // by file
    Viewpoint			initViewpoint;
    NavigationInfo		initNavInfo;
    Background			initBackground;
    Fog				initFog;
    WorldInfo			initWorldInfo; 

    // the currently active values VRML
    NavigationInfo		curNavInfo;
    Background			curBackground;
    Fog				curFog;
    Viewpoint			curViewpoint;
    WorldInfo			curWorldInfo;

    // these are all pieces of the current viewpoint
    TransformGroup              curViewGroup;
    TransformGroup		curImplGroup; // see Viewpoint
    BranchGroup			curImplGroupBranch;
    ViewPlatform                curViewPlatform;

    // used for batching tranform changes
    TransformBuf		pendingTransforms = new TransformBuf();

    // state info for startup
    boolean			simTickEnable = false;
    boolean			resetOnNextFrame = false;

    // render state
    boolean 			stopped = false;
    boolean 			soundinited = false;

    // the time of the current event
    double			eventTime;

    // utility stuff
    PickRay 			pickRay = new PickRay();
    SceneGraphPath[]		stuffPicked;
    Transform3D			identity = new Transform3D();
    RoutePrinter	        routePrinter = new RoutePrinter();	
    TreePrinter			treePrinter = new TreePrinter();
    static boolean		printRoutes = false;
    int				routeDepth = 0;

    // peformance stats
    int				numTris = 0;
    NumFormat			numFormat = new NumFormat();
    int				numFrames = 0;
    int				numSimTicks = 0;
    double			renderTime = 0;
    double			routeTime = 0;
    double			frameStartTime = 0;				
    double			netStartTime = 0;				
    double			start;
    static long			memUsage;
    static long 		memLowLimit = 800000;
    // debug mode
    static boolean 		debug;
    static boolean		debug2; //fine grained
    boolean			timing;
    boolean			pickEcho;
    double			attachTime;
    boolean			checkDelay;
    double			relTimeBase = 0.0;
    Vector			debugVec = new Vector();

    // Browser awt info
    java.awt.Container 		container;

    // unclassified stuff
    BoundingLeaf		defBoundingLeaf;
    static Browser		instance;
    int vi = 0;


    // giles  12/18/1999  Allow access to DEFed objects 
    public Hashtable getNamedObjs() {
       return namedObjs;
    }

    public Browser(Canvas3D vc3d) {
	canvas = vc3d;
	browserInit(new Loader(this));
    }

    public Browser(Canvas3D vc3d, Loader l) {
	canvas = vc3d;
	browserInit(l);
    }

    public Browser(Canvas3D vc3d, boolean wait) {
	canvas = vc3d;
    }
	

    public Browser() {
	canvas = new Canvas3D(null);
	browserInit(new Loader(this));
    }

    protected void setLoader(Loader l) {
	loader = l;
	browserInit(l);
    }

    void browserInit(Loader l) {
	loader = l;
	timing = loader.timing;
	printRoutes = loader.printRoutes;
	debug = loader.debug;
	initBrowserObjs();
	loadDefaults();
    }

    void initBrowserObjs() {
	instance = this;

	fogStack = new Stack();
	viewpointStack = new Stack();
	backgroundStack = new Stack();
	navigationInfoStack = new Stack();

	defViewpoint = new Viewpoint(loader);
	defNavInfo = new NavigationInfo(loader);
	defBackground = new Background(loader);
	defWorldInfo = new WorldInfo(loader);
	// no need for default fog - remove after testing
	defFog = new Fog(loader);

	// init the def objects
	defViewpoint.initImpl();
	defNavInfo.initImpl();
	defBackground.initImpl();
	defFog.initImpl();
	defWorldInfo.initImpl();

	// initialize the J3D objects
        universe = new VirtualUniverse();
        locale = new Locale(universe);

	body = new PhysicalBody();
        environment = new PhysicalEnvironment();
        view = new View();
        view.addCanvas3D(canvas);
        view.setPhysicalBody(body);
        view.setPhysicalEnvironment(environment);

	// This branchgroup holds the browser's J3D state.  The browser 
	// behaviors, fog and background are attached here
	browserRoot = new RGroup();
	curSceneT = new SceneTransform(loader);
	curSceneT.initImpl();

	evagation = new Evagation(this);
	evagation.setSchedulingBoundingLeaf(loader.infiniteBoundingLeaf);
	browserRoot.addChild(evagation);

	simTicker = new SimTicker(this);
	simTicker.setSchedulingBoundingLeaf(loader.infiniteBoundingLeaf);
	browserRoot.addChild(simTicker);

	browserBackgroundSlot = new RGroup(); 
	browserBackgroundSlot.addChild(defBackground.getBackgroundImpl());
	browserRoot.addChild(browserBackgroundSlot);
	browserFogSlot = new RGroup();
	//browserFogSlot.addChild(defFog.getFogImpl());
	browserRoot.addChild(browserFogSlot);

	// the default Viewpoint is located here
	browserRoot.addChild(defViewpoint.getImplNode());
	locale.addBranchGraph(browserRoot);

	// The browserLightGroup is added to the Viewpoint's group after the
	// view platform (so that it moves with the VP)
	browserAmbLight = new javax.media.j3d.AmbientLight(true,
	    new Color3f(0.2f, 0.2f, 0.2f));
	browserAmbLight.setCapability(javax.media.j3d.Light.ALLOW_STATE_WRITE);
	browserDirLight = new javax.media.j3d.DirectionalLight();
	browserDirLight.setColor(new Color3f(0.8f, 0.8f, 0.8f));
	browserDirLight.setCapability(javax.media.j3d.Light.ALLOW_STATE_WRITE);
	browserDirLight.setInfluencingBounds(loader.infiniteBounds);

	browserLightGroup = new RGroup();
	browserLightGroup.addChild(browserDirLight);
	browserLightGroup.addChild(browserAmbLight);

	AuralAttributes aa = new AuralAttributes();
	aa.setFrequencyScaleFactor(.1f);
	Soundscape sc = new Soundscape(loader.infiniteBoundingLeaf.getRegion(),aa);
	browserSoundAtts = new RGroup();
	browserSoundAtts.addChild(sc);
	browserRoot.addChild(browserSoundAtts);
    	audioDevice = new JavaSoundMixer(environment);
	audioDevice.initialize();
	environment.setAudioDevice(audioDevice);
	RGroup browserBoundLeafGroup = new RGroup();
	browserBoundLeafGroup.addChild(loader.infiniteBoundingLeaf);
	browserRoot.addChild(browserBoundLeafGroup);

    }

    void loadDefaults() {
	curViewpoint = defViewpoint;
	curNavInfo = defNavInfo;
	curBackground = defBackground;
	//curFog =  defFog;

	initViewpoint = null;
	initNavInfo = null;
	initBackground = null;
	initFog = null;
	try {
	    browserFogSlot.detach();
	    while ( browserFogSlot.numChildren() > 0 ) {
	        browserFogSlot.removeChild(0);
	    }
	} catch ( Exception e ) { e.printStackTrace();}
	browserFogSlot = new RGroup();
	browserRoot.addChild(browserFogSlot);
	//browserFogSlot.addChild(defFog.getFogImpl());
	browserBackgroundSlot.removeChild(0);
	browserBackgroundSlot.addChild(defBackground.getBackgroundImpl());
	updateView();

    }

    public synchronized void loadURL(String[] urlString, String[] parameter)
	throws vrml.InvalidVRMLSyntaxException, 
		java.io.IOException, java.net.MalformedURLException {

	URL worldURL=null;
	URL cb=null;

	simTickEnable = false;
	simTicker.setEnable(false);

	System.gc();

	urlString[0]=urlString[0].replace((char)0x5c,(char)0x2f);

	try {
	    worldURL = new URL(urlString[0]);
	} catch (java.net.MalformedURLException murle ) {
	    if( murle.getMessage().indexOf("no protocol") >= 0 ) {
		try {
		    if(this.container instanceof Applet) {
		        cb = ((Applet)(this.container)).getCodeBase();
	     	    }
		    worldURL = new URL(cb,urlString[0]);
		} catch (java.net.MalformedURLException murle2) { murle2.printStackTrace(); }
	    }
	}

	loader.setWorldURL(null, worldURL);
        ContentNegotiator cn = new ContentNegotiator(worldURL);
	try {
            ByteArrayInputStream bais = 
		new ByteArrayInputStream((byte[])cn.getContent());

	    doParse(bais);
	// cn already threw fnf
	} catch ( NullPointerException npe ) { throw new java.io.IOException();} 
	loadFinalize();
	
    }

    public void loadStringAsVrml(String sourceVrml) {
	simTickEnable = false;
	simTicker.setEnable(false);

	//loader.setWorldURL(null, null);

	ByteArrayInputStream bais = new  	
		ByteArrayInputStream(sourceVrml.getBytes());

	doParse(bais);
	loadFinalize();

    }
    
    public void loadScene(Scene sceneIn) {
	simTickEnable = false;
	simTicker.setEnable(false);
	
	doParse((InputStream)null,sceneIn);
	loadFinalize();
    }

    void doParse(InputStream is) {
	doParse(is,(Scene)null);
    }
	

    void doParse(InputStream is,Scene sceneIn) {

	clear();

	if (loader.debug) {
// DEBUG
	  try {
	    System.out.println("locale before scene is added:");
	    browserRoot.detach();
	    System.out.println("did detach");
            
	    treePrinter.print(browserRoot);
	    System.out.println("did treePrint");
	    locale.addBranchGraph(browserRoot);
	    System.out.println("did addBack");
	    System.out.println();
	  } catch (Exception e) { e.printStackTrace(); }
	}

	curScene = new RGroup();
	curSceneT = new SceneTransform(loader);
	curSceneT.initImpl();

	sceneExaminer = new SphereSensor(loader);
	sceneExaminer.autoSpinFrameWait.setValue(1);
	// do the safe thing and use the vrml event
        MFNode curSceneNodes = (MFNode)curSceneT.getField("addChildren");
        curSceneNodes.addValue(sceneExaminer);
	
	curScene.addChild(curSceneT.impl);

	Scene scene = null;

	if(is!=null) {
	  try {
	     scene = loader.load(is);
	  } catch (Exception e) {
	     containerMessage(this.container, e.toString());
	     clear();
	  }
	} else { scene=sceneIn; }

	// extract the stuff we need from the scene

	// first the top level nodes
	for (Enumeration e = scene.objects.elements(); e.hasMoreElements();) {
	    BaseNode node = (BaseNode) e.nextElement();
	    if (node != null)  {
		if(debug)System.out.println("Adding to browser "+
			node.toStringId());

		node.updateParent(curSceneT.impl);
		javax.media.j3d.Node implNode = node.getImplNode();
		if (node instanceof GroupBase) {
			debugVec.addElement(node);
		}

		// bug, assume for now that Viewpoints are not nested within
		// any transforms, later we'll need to transform the viewpoint
		// by the inverse of what the parent would have been, since,
		// that factors out of the tree when we pull the Viewpoint up
		// to the top curScene.

		if ( node instanceof Viewpoint && implNode != null) {
		    curScene.addChild(implNode);
		} else if (implNode != null) {
		    implNode.setCapability(javax.media.j3d.Node.ALLOW_BOUNDS_READ);
		    implNode.setCapability(javax.media.j3d.Node.ALLOW_LOCAL_TO_VWORLD_READ);
		    if(debug)System.out.println("curScene: "+curScene);
		    if(debug)System.out.println("Adding to scene "+implNode);
		    curSceneT.impl.addChild(implNode);
		    if (node instanceof 
			    com.sun.j3d.loaders.vrml97.impl.DirectionalLight) {
			javax.media.j3d.DirectionalLight dirLight = 
			    ((com.sun.j3d.loaders.vrml97.impl.DirectionalLight)
				    node).dirLight;
			dirLight.addScope(curSceneT.impl);
		    }
		}
	    }
	}

	// over zealous cleaning
	curSceneT.impl.setCapability(javax.media.j3d.Node.ALLOW_PICKABLE_WRITE);
	curSceneT.impl.setCapability(javax.media.j3d.Node.ENABLE_PICK_REPORTING);
	curSceneT.impl.setPickable(true);
	// could call 
	// TreeCleaner.cleanSubgraph(curSceneT);
	// but that would be a no-op since ENABLE_PICK_REPORTING is set.
	// (it *would* clear the collidable flags, but compile() would still
	// not have any effect)

	// then the bindable nodes
	viewpointList = scene.viewpoints;
	if (scene.viewpoints.size() > 0) {
	    initViewpoint = (Viewpoint)scene.viewpoints.elementAt(0);
	}
	if (scene.navInfos.size() > 0) {
	    initNavInfo = (NavigationInfo)scene.navInfos.elementAt(0);
	}
	if (scene.backgrounds.size() > 0) {
	    initBackground = (Background)scene.backgrounds.elementAt(0);
	}
	if (scene.fogs.size() > 0) {
	    initFog = (Fog)scene.fogs.elementAt(0);
	}
	// and the other stuff we need from the scene
	timeSensors = scene.timeSensors;
	visibilitySensors = scene.visibilitySensors;
	touchSensors = scene.touchSensors;
	sharedGroups = scene.sharedGroups;
	audioClips = scene.audioClips;
        namedObjs = scene.defTable;
	numTris = scene.numTris;

	// this used to be handled in the parser, and could 
	// be cleaned up some.
	if (scene.worldInfo != null) {
	    curWorldInfo = scene.worldInfo;
	} else {
	    curWorldInfo = defWorldInfo;
	}
	name = curWorldInfo.title.getValue();
	// how much info fits on the titlebar? TBD, implement the 
	// info popup in the player, but for now this gives us a titlebar.
	description = curWorldInfo.info.get1Value(0);

	if (debug) {
	   System.out.println("Parsed scene makes J3D scene graph:");
	   treePrinter.print(curScene);
	}

	/* bother to compile() since the pickable flag is still on
	 * for all the Shape3D's ?*/
	 if(!debug) { 
	      curScene.compile();
	      Enumeration e = sharedGroups.elements();
	      while (e.hasMoreElements()) {
	 	   SharedGroup sg = (SharedGroup) e.nextElement();
         		sg.compile();
 	      }
 	 }
	 
	loader.loaded=true;
	updateView();


	//attach the scene to the objRoot
	if (debug) {
	    System.out.println("Locale already has "+locale.numBranchGraphs());
	}
	locale.addBranchGraph(curScene);

	cleanUp();
	if (debug || timing) {
	    System.out.println("Scene contains " + numTris + " triangles");
	}
	if(debug)System.out.println("Load completed");
    }
	
    protected void clear() {
	int n;

	loader.setLoaded(false);
	if(debug)System.out.println("Browser:clear()");

	if (curScene != null) {
	    curScene.detach();
	}
	
	curScene = null;

	// load up the defaults.  This will move the browserLightGroup to 
	// the defViewpoint
	loadDefaults();

	numTris = 0;
	loader.clear();
        disableSounds();

	viewpointList = null;
        timeSensors = null;
        visibilitySensors = null;
        touchSensors = null;
        audioClips = null;

        viewpointStack.removeAllElements(); 
	navigationInfoStack.removeAllElements();
        fogStack.removeAllElements();
	backgroundStack.removeAllElements(); 

	evagation.resetViewpoint();
	debugVec = new Vector();
	cleanUp();

    	audioDevice = new JavaSoundMixer(environment);
	audioDevice.initialize();
	environment.setAudioDevice(audioDevice);
	

    }

    protected void loadFinalize() {
	SFBool set_bind;

	// finalize the TimeSensors
	for (int i = 0; i < timeSensors.size(); i++) {
	    ((TimeSensor)(timeSensors.elementAt(i))).doneParse();
	}

	if (printRoutes) {
	    for (int i = 0; i < touchSensors.size(); i++) {
		// print the routes attached to touch sensors
		routePrinter.printRoutes(
			(TouchSensor)touchSensors.elementAt(i));
	    }
	}

	// bind the initial bindables, these will make cur = init
	if (initFog != null) {
	    set_bind = (SFBool)initFog.getEventIn("bind");
	    set_bind.setValue(true);
	}
	if (initBackground != null) {
	    set_bind = (SFBool)initBackground.getEventIn("bind");
	    set_bind.setValue(true);
	}
	if (initNavInfo != null) {
	    set_bind = (SFBool)initNavInfo.getEventIn("bind");
	    set_bind.setValue(true);
	}
	// do this last, since it depends on NavInfo
	if (initViewpoint != null) {
	    set_bind = (SFBool)initViewpoint.getEventIn("bind");
	    set_bind.setValue(true);
	}
	// binding the VP will reset the View

	// all set
	simTickEnable = true;
	simTicker.setEnable(true);

	sceneBounds = (BoundingSphere)(((TransformGroup)curSceneT.impl).getBounds());
	if (sceneBounds.getRadius()==0.0) {
	    sceneBounds.setRadius(1.0);
	    curScene.setBounds(sceneBounds);
	}

	curSceneT.setSceneBounds(sceneBounds);

	// arbitrary formula here:
	// set the spinKick (ammount to amplify
	// mouse angle delta) depending on relative
	// size of object. 

	sceneExaminer.spinKick.setValue(((float)(sceneBounds.getRadius()))/2.0f);

	if (viewpointList.size() == 0)  {
	    curViewpoint.frameObject(sceneBounds);
	}

	// do the sounds after all else
	querySounds();

	// bug: see javasound duration bug
	frameCountAdd();
	
	initTiming();
	resetViewpoint();
		
    }

    synchronized void querySounds() {
	// sounds should be live, scene should be
	// ready; can now initialize the clips with the known
	// duration needed to calculate event responses.
	for ( int i = 0; i < audioClips.size(); i++ ) {
	    AudioClip cl = (AudioClip)(audioClips.elementAt(i));
	    javax.media.j3d.Sound s = cl.sound.soundImpl;
	    javax.media.j3d.MediaContainer mc = cl.impl;
	    // need to prepare the sound now if we want to know
	    // how long it is in duration time.
	    s.setSoundData(mc);
	    cl.setDuration(s.getDuration()/1000.0);
	}

	int numChannels = audioDevice.getTotalChannels();
	if (debug) { 
	    System.out.println("audioDevice has "+numChannels+"channels"); 
	}
    }

    void frameCountAdd() {

	// JavaSoundMixer will not report the size of the sound
	// until after the scene is live and into a couple of frames.
	// this is a temporary workaround which should dissappear
	// remove the old frame count
	if ( browserRoot != null ) {
	    Enumeration e = browserRoot.getAllChildren();
	    int ind = 0;
	    try {
	        while ( e.hasMoreElements() ) {
	           if( e.nextElement() != frameCount.rHandle ) 
		        ind++;
	           else
		        break;
	        }
	        // double check it was found
	        if ( browserRoot.getChild(ind) == frameCount.rHandle )
	           browserRoot.removeChild(ind);
	    } catch ( NullPointerException npe ) {
	        ;// first time through, frameCount hasn't been init'd
	    }

	    frameCount = new FrameCounter(this,4,"soundSync");
	    //frameCount = new FrameCounter(this,1,"soundSync");
	    frameCount.setSchedulingBoundingLeaf(loader.infiniteBoundingLeaf);
	    browserRoot.addChild(frameCount.rHandle);
	}

    }

    void frameCountCallback(FrameCounter b) {
	if (b.name.equals("soundSync")) {
		querySounds();
	}
    }



    // This does two things: keeps track of the initial viewpoint, which 
    // is bound in loadFinalize() and makes a list of all the viewpoints.
    protected void addViewpoint(Viewpoint viewpoint) {
	if (initViewpoint == null) { 
	    // this is the first one we have seen
	    initViewpoint = viewpoint;
	}
	viewpointList.addElement(viewpoint);
    }


    protected void updateView() {
     if (loader.loaded) {
        if(debug)System.out.println("updateView");

	// clear the behavior so as to remove "jump"
	evagation.resetViewpoint();

	curImplGroupBranch = (BranchGroup)curViewpoint.impl;
	curImplGroup = (TransformGroup)curViewpoint.implOrient;
	curViewGroup = (TransformGroup)curViewpoint.implBrowser;
	curViewPlatform = (ViewPlatform)curViewpoint.implViewPlatform;

	// re-init the browserGroup transform
	// this is the one that "moves"
	curViewGroup.setTransform(identity);

	curViewPlatform.setActivationRadius(2112.0f);
	
	view.setFieldOfView(curViewpoint.fieldOfView.value);
        System.out.println("fieldOfView: " + curViewpoint.fieldOfView.value);
	double frontClip;
	if (curNavInfo.avatarSize.mfloat.length > 0) {
	    frontClip = curNavInfo.avatarSize.mfloat[0] / 2.0d;
	} else {
	    // default value
	    frontClip = 0.25 / 2.0d;
	}

	
	view.setFrontClipDistance(frontClip);
	double backClip;
	if (curNavInfo.visibilityLimit.value > 0.0) { 
	    backClip = curNavInfo.visibilityLimit.value;
	} else {
	    backClip = frontClip * 2999.0; // no greater than 3000 or loss of
					 // zbuff resolution
	}
	view.setBackClipDistance(backClip);

	browserDirLight.setEnable(curNavInfo.headlight.value);
	browserAmbLight.setEnable(curNavInfo.headlight.value);

	browserLightGroup.detach();
	curViewGroup.addChild(browserLightGroup);

	view.attachViewPlatform(curViewPlatform);
        view.setPhysicalBody(body);
        view.setPhysicalEnvironment(environment);

	if (timing) {
	    start = Time.getNow();
	}

	evagation.setViewGroup(curViewGroup);

	// cant scale above the viewPlatform, so scale inversly above the scene
	// 1.6 meters is avatar's default height

	try {
	    float s = 1.6f/curNavInfo.avatarSize.mfloat[1];
	    curSceneT.scale.setValue(s,s,s);
	} catch (NullPointerException npe) {;//expected 1st time

	} catch (ArrayIndexOutOfBoundsException aioobe) {;}

	cleanUp();
      }
    }

    void updateBackground() {
	browserBackgroundSlot.removeChild(0);
	browserBackgroundSlot.addChild(curBackground.getBackgroundImpl());
    }


    // the reason why this is special cased so, is that there is not
    // "default fog" which kind of inverts the logic
    void updateFog() {
	try {
	    browserFogSlot.detach();
	    while ( browserFogSlot.numChildren() > 0 ) {
	        browserFogSlot.removeChild(0);
	    }
	} catch (Exception e) { e.printStackTrace();  }
 	browserFogSlot = new RGroup();	
	browserRoot.addChild(browserFogSlot);
	browserFogSlot.addChild(curFog.getFogImpl());
    }

    // rename fovChanged ?
    void viewChanged(Viewpoint changedView) {
	if (changedView == curViewpoint) {
	    view.setFieldOfView(curViewpoint.fieldOfView.value);
	    // TODO: handle description
	}
    }

    public void setViewpoint(int vi) {
	if (viewpointList.size() > 0) {
	    // this causes bindableChanged with this view stack
	    Viewpoint viewpoint = (Viewpoint)viewpointList.elementAt(vi);
	    SFBool set_bind = (SFBool)viewpoint.getEventIn("bind");
	    set_bind.setValue(true);
	}
    }   

    public void resetViewpoint() {
	updateView();  // this will clean out any changes
    }

    public String[] getViewpointDescriptions() {
	String[] vd = new String[viewpointList.size()];
	for (int i = 0; i < viewpointList.size(); i++) {
	    Viewpoint cur = (Viewpoint)viewpointList.elementAt(i);
	    if ((cur.description.string == null) ||
	         cur.description.string.equals("")) {
	       vd[i] = "Viewpoint " + i;
	    } else {
	       vd[i] = cur.description.string;
	    }
	}
	return vd;
    }

    void bindableChanged(Stack changedStack) {
	if(debug)System.out.println(this+"bindableChanged()"+changedStack);
	if (changedStack == viewpointStack) {
	    Viewpoint newViewpoint = (Viewpoint) viewpointStack.peek();
	    if (newViewpoint != null && newViewpoint != curViewpoint ) {
		if(debug)System.out.println(newViewpoint+" bound");
		curViewpoint = newViewpoint;
		updateView();
	    } 
	} else if (changedStack == navigationInfoStack) {
	    NavigationInfo newNavInfo = 
		(NavigationInfo)navigationInfoStack.peek();
	    if (newNavInfo != curNavInfo && newNavInfo != null) {
		curNavInfo = newNavInfo;
		updateView(); // view uses NavInfo for some info
		// TODO: add updateNavInfo to change browser mode (walk,
		// examine) when implemented 
		
	    }
	} else if (changedStack == backgroundStack) {
	    Background newBackground = (Background)backgroundStack.peek();
	    if (newBackground != curBackground && newBackground != null) {
		curBackground = newBackground;
		updateBackground();
	    }
	} else if (changedStack == fogStack) {
	    Fog newFog = (Fog)fogStack.peek();
	    if (newFog != curFog && newFog!=null) {
		curFog = newFog;
		updateFog();
	    } 
	}
    }

    void preRender() {
	if (timing) {
	    frameStartTime = Time.getNow();
	    if (checkDelay) {
		double delay = frameStartTime - attachTime;
		System.out.println("Attach to render delay = "  + 
		    numFormat.format(delay,2) + " seconds");
	    }
	}
	try {
	   simTick();
	} catch (Exception e0 ) { e0.printStackTrace(); }
    }

    void postRender() {
	if (timing) {
	    double now = Time.getNow();
	    double elapsed = now - frameStartTime;
	    renderTime += elapsed;
	    if (elapsed < 0.0) {
		System.out.println("Negative elaspsed time for frame: "+
			numFormat.format(elapsed,2) + " seconds");
		renderTime = -1;
	    }
	    if (checkDelay) {
		System.out.println("Time to render first frame = "  + 
		    numFormat.format(elapsed,2) + " seconds");
		checkDelay = false;
	    }
	}
	numFrames++;
	if (timing && ((numFrames % 10) == 0)) {
	    outputTiming();
	}
	//try {
	   //simTick();
	//} catch (Exception e0 ) { e0.printStackTrace(); }
	// Reset here to that if previous output was between pre and post 
	// render we don't include output time in timing
	if (resetOnNextFrame) {
	    resetTiming();
	    resetOnNextFrame = false;
	}
    }

    void initTiming()  {
	Time.setSystemInitTime();
	resetTiming();
    }

    protected void resetTiming()  {
	double now = Time.getNow();
	netStartTime = now;
	renderTime = 0.0;
	routeTime = 0.0;
	numFrames = 0;
	numSimTicks = 0;
	relTimeBase = 0.0;
    }

    double relativeTime(double timeVal) {
	if (relTimeBase == 0.0) {
	    relTimeBase = timeVal;
	}
	return timeVal - relTimeBase;
    }

    double beginRoute() {
	if (routeDepth++ == 0) {
	    eventTime = relativeTime(Time.getNow());
	    if(debug)System.out.println("beginRoute() base eventTime = " + 
	    	eventTime);
	}
	return eventTime;

    }

    void beginRoute(double now) {
	if (routeDepth++ == 0) {
	    eventTime = now;
	    //if(debug2)System.out.println("beginRoute(now) base eventTime = " + 
	    	    //relativeTime(eventTime));
	}
    }

    double eventTime() {
	//if(debug2)System.out.println("EventTime = " + relativeTime(eventTime));
	return eventTime;
    }

    void endRoute() {
	routeDepth--;
    }

    void simTick() {

	if (true) {
	    double now = relativeTime(Time.getNow());

	    numSimTicks++;


	    // any other things that would be better to batch like this?
	    // mpeg frames to Textures.
	    pendingTransforms.startBatchLoading();

	    beginRoute(now); // set the event time for all events 
			     
	    if (timeSensors != null) {
		for (Enumeration e = timeSensors.elements(); 
			e.hasMoreElements();){
		    TimeSensor ts = (TimeSensor)e.nextElement();
		    if (ts.enabled.value == true) {
		//	ts.simTick(Time.getNow());
		//	ts.simTick(relativeTime(Time.getNow()));
		//      ts.simTick(relativeTime(now));
		        ts.simTick(now);
		    }
		}
	    }

	    if (audioClips != null) {
		for (Enumeration e = audioClips.elements(); 
			e.hasMoreElements();){
		    AudioClip clip = (AudioClip)e.nextElement();
		    if (clip.sound != null) {
		        clip.simTick(now);
		    }
		}
	    }

	    evagation.simTick(now);

	    // update the pending transforms
	    // if any of the previous behaviors had resulted in setValue()
	    // to any Transforms during this simTick()

	    pendingTransforms.stopBatchLoading();

	    for (int i = 0; i < pendingTransforms.size; i++) {
		pendingTransforms.array[i].updateTransform();
		//// just over write and keeping size as a TransforBufMark
		pendingTransforms.array[i] = null; // to allow GC 
	    }

	    pendingTransforms.size = 0;

	    endRoute();

	    routeTime += (relativeTime(Time.getNow()) - now);

	    // hard to track during runtime stuff can be added to the generic
	    // debug 
	    if(false) {
		for (Enumeration e = debugVec.elements(); e.hasMoreElements();){
		        Object o = e.nextElement();
			Transform3D t = new Transform3D();
			try {
			    ((Node)o).implNode.getLocalToVworld(t);
			} catch (Exception ex)  { ex.printStackTrace(); }
			System.out.println(t);
		        System.out.println("+++++++++++++++++++++++++");
	       } 
	    }
	}
    }

    public void outputTiming() {
	if ((numFrames > 0) && timing) {
	    double now = Time.getNow();
	    double elapsed = now - netStartTime;
	    double netTrisPerSec = 
		numTris * numFrames / (1000 * elapsed);
	    double trisPerSec = 
		numTris * numFrames / (1000 * renderTime);

	    System.out.print(numFormat.format(elapsed, 1) +
		" seconds "+ numFrames + " frames " + 
		"overall: " + 
		numFormat.format(netTrisPerSec,1) + "K tris/sec");
	    if (renderTime > 0.0) {
		System.out.println(" render: " + 
		    numFormat.format(trisPerSec,1) + "K tris/sec");
	    } else {
		System.out.println(" render: ???");
	    }
	    if (routeTime > 0.0) {
		System.out.println(numFormat.format(routeTime,2) + 
			" seconds updating nodes (" +
			numFormat.format(routeTime * 100.0 / elapsed, 1) + 
			"%) " + numSimTicks + " ticks " + 
			numFormat.format(routeTime * 1000.0 /numSimTicks, 1) + 
			"ms/tick");
	    }
	}
	resetOnNextFrame = true;
    }

    void disableSounds() {
	if (audioClips != null) {
	    for (Enumeration e = audioClips.elements(); 
						    e.hasMoreElements();){
		AudioClip clip = (AudioClip)e.nextElement();
		if (clip.sound != null) {
		    clip.sound.setEnable(false);
		}
	    }
	}
    }
    void enableSounds() {
	if (audioClips != null) {
	    for (Enumeration e = audioClips.elements(); 
						    e.hasMoreElements();){
		AudioClip clip = (AudioClip)e.nextElement();
		if (clip.sound != null) {
		    clip.sound.setEnable(true);
		}
	    }
        }
    }

    public void shutDown() {
	outputTiming();
	disableSounds();
	System.exit(0);
    }

    public Canvas3D getCanvas3D() {
	return canvas;
    }
	    
    public void createVrmlFromURL(String[] url, BaseNode node, String event)
	    throws vrml.InvalidVRMLSyntaxException{;}

    public String getName(){
	return name;
    }

    public String getVersion(){
	return version;
    }

    public float getCurrentSpeed(){
	return speed;
    }

    public float getCurrentFrameRate(){
	return frameRate;
    }

    public String getWorldURL(){
	if(loader.worldURL != null)
		return loader.worldURL.toString();
	else return new String("NULL worldURL");
    }

    public void replaceWorld(BaseNode[] nodes){
	// TODO: implement
    }
	    
    public BaseNode[] createVrmlFromString(String vrmlSyntax) 
	throws vrml.InvalidVRMLSyntaxException {
	// todo: VRML2parser(String);
	//Node node[] = (new VRML2Parser(vrmlSyntax)).returnScene();
	return null;
    }

    protected void setDefTable(Hashtable t) {
	// currently not used, but could be saved for use by EAI
	// TODO: memory optimization: set children=null on DEF'd group nodes
	// without routes that can change the children and on DEF'd Index
	// Face Sets which have child nodes that can't change, etc.
    }

    public void setDescription(String description) {
	this.description=description;
    }
    
    public String getDescription() {
	return description;
    }

    public void addRoute(BaseNode fromNode, String fromEventOut,
	    BaseNode toNode, String toEventIn) {
	fromEventOut = Field.baseName(fromEventOut);
	toEventIn = Field.baseName(toEventIn);
	loader.connect(fromNode, fromEventOut, toNode, toEventIn, true);
    }

    protected void addRouteInternal(BaseNode fromNode, String fromEventOut,
	    BaseNode toNode, String toEventIn) {
	fromEventOut = Field.baseName(fromEventOut);
	toEventIn = Field.baseName(toEventIn);
	loader.connect(fromNode, fromEventOut, toNode, toEventIn, false);
    }

    public void deleteRoute(BaseNode fromNode, String fromEventOut,
	BaseNode toNode, String toEventIn){

	fromEventOut = Field.baseName(fromEventOut);
	toEventIn = Field.baseName(toEventIn);

	Field fromField;
	Field toField;
	if ( fromNode instanceof Script ) {
	  fromField = ((Script)fromNode).getField(fromEventOut);
	} else {
	  fromField = ((Node)fromNode).getField(fromEventOut);
	}
	if (!fromField.isEventOut()) {
	    throw new vrml.InvalidEventOutException();
	}

	if ( toNode instanceof Script ) {
	  toField = ((Script)toNode).getField(toEventIn);
	} else {
	  toField = ((Node)toNode).getField(toEventIn);
	} 
	if (!toField.isEventIn()) {
	    throw new vrml.InvalidEventInException();
	}

	fromField.deleteConnection(toField);
	// TODO: someday have a "unroute_" method?
    
    }

    // we may have defined several evagations that the 
    // canvas didn't know about. ie, walk, fly
    public void processEvent(AWTEvent evt) {
        evagation.processEvent(evt);
    }

    public void startRender() {
        // only call this if a stop render has been called
        if(stopped) {
	   canvas.startRenderer();
	   enableSounds();
	}
        stopped=false;

    }

    public void stopRender() {
        if(!stopped) {
	   canvas.stopRenderer();
	   disableSounds();
	}
        stopped=true;
    }

    public void cleanUp() {
	long mem = Runtime.getRuntime().freeMemory();
	long tot = Runtime.getRuntime().totalMemory();
	if (mem < memLowLimit) { 
	    if(debug)System.out.println("Memory usage: "+mem+" of "+tot+" left");
	    if(debug)System.out.println("Taking out trash..."); 
	    System.gc();
	    memUsage = Runtime.getRuntime().freeMemory() - mem;
	    if(debug)System.out.println("Reclaimed "+ memUsage +" bytes.");
	    memLowLimit -= 1000000;
	    if (memLowLimit < 500000) memLowLimit = 500000;
	}
    }

    public URL getURL() {
	return loader.worldURL;
    }

    byte[] getBytes(String URLstring) {
	
	ContentNegotiator cn;
	URL fu;
	byte[] buf = new byte[1];
	try {
	   fu = new URL(URLstring);
	   cn = new ContentNegotiator(fu);
	   buf = (byte[])cn.getContent();
	} catch (Exception e) {System.out.println(e);}
	//if(debug)System.out.println(new String(buf));
	return buf;
    }

	   

    byte[] getRelBytes(String relURL) {
	String fullURL = loader.worldURLBaseName+relURL;
	byte[] buf = getBytes(fullURL);
	return buf;
    }

    int count(Enumeration e) {
	int c = 0;
	while ( e.hasMoreElements() ) {
	    c++;
	    e.nextElement();
	} return c;
    }


    public void removeCanvas3D(Canvas3D can) {
	view.removeCanvas3D(can);
    }

    public void addCanvas3D(Canvas3D can) {
	view.addCanvas3D(can);
    }

    public void setAWTContainer(java.awt.Container container) {
	this.container = container;
    }

    void containerMessage(java.awt.Container c, String mesg) {
	if ( c instanceof Applet ) {
	    ((Applet)(c)).showStatus(mesg);
	} else if ( c instanceof Frame ) {
	    ((Frame)(c)).setTitle(mesg);
	}
    }

    static Browser getBrowser() {
	return instance;
    }

    public void setAutoSmooth(boolean s) {
	loader.autoSmooth = s;
    }

}
