/*
 * $RCSfile: FogRetained.java,v $
 *
 * Copyright (c) 2007 Sun Microsystems, Inc. All rights reserved.
 *
 * Use is subject to license terms.
 *
 * $Revision: 1.6 $
 * $Date: 2007/02/09 17:17:58 $
 * $State: Exp $
 */

package javax.media.j3d;

import javax.vecmath.*;
import java.util.Enumeration;
import java.util.Vector;
import java.util.ArrayList;

/**
 * The Fog leaf node defines Fog parameters.
 * It also specifies an region of influence in which this fog node
 * is active.
 */
abstract class FogRetained extends LeafRetained{

    // Statics used when something in the fog changes
    static final int COLOR_CHANGED           = 0x0001;
    static final int SCOPE_CHANGED           = 0x0002;
    static final int BOUNDS_CHANGED          = 0x0004;
    static final int BOUNDINGLEAF_CHANGED    = 0x0008;
    static final int INIT_MIRROR             = 0x0010;
    static final int CLEAR_MIRROR            = 0x0020;
    static final int LAST_DEFINED_BIT        = 0x0020;

    // Fog color.
    Color3f color = new Color3f(0.0f, 0.0f, 0.0f);

    /**
     * The Boundary object defining the lights's region of influence.
     */  
    Bounds regionOfInfluence = null;

    /** 
     * The bounding leaf reference
     */
    BoundingLeafRetained boundingLeaf = null;

    /**
     * Vector of GroupRetained  nodes that scopes this fog.
     */
    Vector scopes = new Vector();

    // An int that is set when this fog is changed 
    int isDirty = 0xffff;

    // This is true when this fog is referenced in an immediate mode context
    boolean inImmCtx = false;

    /**
     * The transformed value of the applicationRegion.
     */
    Bounds region = null;

    // A reference to the scene graph fog 
    FogRetained sgFog = null;

    // The mirror copy of this fog
    FogRetained mirrorFog = null;

    // Target threads to be notified when light changes
    static final int targetThreads = J3dThread.UPDATE_RENDERING_ENVIRONMENT |
                                     J3dThread.UPDATE_RENDER;

    // Boolean to indicate if this object is scoped (only used for mirror objects
    boolean isScoped = false;

    // The object that contains the dynamic HashKey - a string type object
    // Used in scoping 
    HashKey tempKey = new HashKey(250);

    /**
     * The EnvironmentSets which reference this fog.
     * Note that multiple RenderBin update thread may access
     * this shared environmentSets simultaneously.
     * So we use UnorderList when sync. all the operations.
     */
    UnorderList environmentSets = new UnorderList(1, EnvironmentSet.class);

    // Is true, if the mirror fog is viewScoped
    boolean isViewScoped = false;

    // Scale value extracted from localToVworld transform
    private double localToVworldScale = 1.0;

    FogRetained() {
	localBounds = new BoundingBox();
	((BoundingBox)localBounds).setLower( 1.0, 1.0, 1.0);
	((BoundingBox)localBounds).setUpper(-1.0,-1.0,-1.0);
    }

    /**
     * Initialize the fog color to the specified color.
     */
    void initColor(Color3f color) {
	this.color.set(color);
    }
    /**
     * Sets the fog color to the specified color and send message
     */
    void setColor(Color3f color) {
	this.color.set(color);
	sendMessage(COLOR_CHANGED, new Color3f(color));
    }

    /**
     * Sets the fog color to the specified color.
     */
    void initColor(float r, float g, float b) {
	this.color.x = r;
	this.color.y = g;
	this.color.z = b;
    }
    /**
     * Sets the fog color to the specified color and send message
     */
    void setColor(float r, float g, float b) {
	initColor(r, g, b);
	sendMessage(COLOR_CHANGED, new Color3f(r, g, b));
    }

    /**
     * Retrieves the fog color.
     */
    void getColor(Color3f color) {
	color.set(this.color);
    }

    /**
     * Set the Fog's region of influence.
     */  
    void initInfluencingBounds(Bounds region) {
	if (region != null) {
            this.regionOfInfluence = (Bounds) region.clone();
	} else {
	    this.regionOfInfluence = null;
	}
	if (staticTransform != null) {
	    this.regionOfInfluence.transform(staticTransform.transform);
	}
    }

    /**
     * Set the Fog's region of influence and send message
     */  
    void setInfluencingBounds(Bounds region) {
	initInfluencingBounds(region);
	sendMessage(BOUNDS_CHANGED, 
		    (region != null ? region.clone() : null));
    }

    /**  
     * Get the Fog's region of Influence.
     */  
    Bounds getInfluencingBounds() {
	Bounds b = null;
	if (regionOfInfluence != null) {
	    b = (Bounds)regionOfInfluence.clone();
            if (staticTransform != null) {
                Transform3D invTransform = staticTransform.getInvTransform();
                b.transform(invTransform);
            }
	}
	return b;
    }

    /**
     * Set the Fog's region of influence to the specified Leaf node.
     */  
    void initInfluencingBoundingLeaf(BoundingLeaf region) {
	if (region != null) {
	    boundingLeaf = (BoundingLeafRetained)region.retained;
	} else {
	    boundingLeaf = null;
	}
    }

    /**
     * Set the Fog's region of influence to the specified Leaf node.
     */  
    void setInfluencingBoundingLeaf(BoundingLeaf region) {
	if (boundingLeaf != null)
	    boundingLeaf.mirrorBoundingLeaf.removeUser(mirrorFog);
	if (region != null) {
	    boundingLeaf = (BoundingLeafRetained)region.retained;
	    boundingLeaf.mirrorBoundingLeaf.addUser(mirrorFog);
	} else {
	    boundingLeaf = null;
	}
	sendMessage(BOUNDINGLEAF_CHANGED, 
		    (boundingLeaf != null ? 
		     boundingLeaf.mirrorBoundingLeaf : null));
    }


    /**  
     * Get the Fog's region of influence.
     */  
    BoundingLeaf getInfluencingBoundingLeaf() {
	return (boundingLeaf != null ? 
		(BoundingLeaf)boundingLeaf.source : null);
    }

    /**
     * Replaces the specified scope with the scope provided.
     * @param scope the new scope
     * @param index which scope to replace
     */
    void initScope(Group scope, int index) {
	scopes.setElementAt((GroupRetained)(scope.retained), index);

    }

    /**
     * Replaces the specified scope with the scope provided.
     * @param scope the new scope
     * @param index which scope to replace
     */
    void setScope(Group scope, int index) {

	ArrayList addScopeList = new ArrayList();
	ArrayList removeScopeList = new ArrayList();
	GroupRetained group;
	Object[] scopeInfo = new Object[3];

	
	group = (GroupRetained) scopes.get(index);
	tempKey.reset();
	group.removeAllNodesForScopedFog(mirrorFog, removeScopeList, tempKey);

	group = (GroupRetained)scope.retained;
	initScope(scope, index);
	tempKey.reset();
	// If its a group, then add the scope to the group, if
	// its a shape, then keep a list to be added during
	// updateMirrorObject
	group.addAllNodesForScopedFog(mirrorFog,addScopeList, tempKey);

	scopeInfo[0] = addScopeList;
	scopeInfo[1] = removeScopeList;
	scopeInfo[2] = (scopes.size() > 0 ? Boolean.TRUE:Boolean.FALSE);
	sendMessage(SCOPE_CHANGED, scopeInfo);
    }
    
    /**
     * Inserts the specified scope at specified index.before the
     * fog is live
     * @param scope the new scope
     * @param index position to insert new scope at
     */
    void initInsertScope(Node scope, int index) {
        GroupRetained group = (GroupRetained)scope.retained;
	group.setFogScope();
	scopes.insertElementAt((GroupRetained)(scope.retained), index);
    }

    /**
     * Inserts the specified scope at specified index and sends
     * a message
     * @param scope the new scope
     * @param index position to insert new scope at
     */
    void insertScope(Node scope, int index) {
	Object[] scopeInfo = new Object[3];
	ArrayList addScopeList = new ArrayList();

	initInsertScope(scope, index);
        GroupRetained group = (GroupRetained)scope.retained;
	tempKey.reset();
	group.addAllNodesForScopedFog(mirrorFog,addScopeList, tempKey);
	scopeInfo[0] = addScopeList;
	scopeInfo[1] = null;
	scopeInfo[2] = (scopes.size() > 0 ? Boolean.TRUE: Boolean.FALSE);
	sendMessage(SCOPE_CHANGED, scopeInfo);
    }
    

    void initRemoveScope(int index) {
	GroupRetained group  = (GroupRetained)scopes.elementAt(index);
	scopes.removeElementAt(index);
	group.removeFogScope();

    }

    void removeScope(int index) {

	Object[] scopeInfo = new Object[3];
	ArrayList removeScopeList = new ArrayList();
	GroupRetained group  = (GroupRetained)scopes.elementAt(index);

	tempKey.reset();
	group.removeAllNodesForScopedFog(mirrorFog, removeScopeList, tempKey);

	initRemoveScope(index);
	scopeInfo[0] = null;
	scopeInfo[1] = removeScopeList;
	scopeInfo[2] = (scopes.size() > 0 ? Boolean.TRUE: Boolean.FALSE);
	sendMessage(SCOPE_CHANGED, scopeInfo);
    }

    
    /**
     * Returns the scope specified by the index.
     * @param index which scope to return
     * @return the scoperen at location index
     */
    Group getScope(int index) {
	return (Group)(((GroupRetained)(scopes.elementAt(index))).source);
    }
  
    /**
     * Returns an enumeration object of the scoperen.
     * @return an enumeration object of the scoperen
     */  
    Enumeration getAllScopes() {
	Enumeration elm = scopes.elements();
	Vector v = new Vector(scopes.size());
	while (elm.hasMoreElements()) {
	    v.add( ((GroupRetained) elm.nextElement()).source);
	}
	return v.elements();
    }

    /**
     * Appends the specified scope to this node's list of scopes before
     * the fog is alive
     * @param scope the scope to add to this node's list of scopes
     */
    void initAddScope(Group scope) {
        GroupRetained group = (GroupRetained)scope.retained;
	scopes.addElement((GroupRetained)(scope.retained));
	group.setFogScope();
    }

    /**
     * Appends the specified scope to this node's list of scopes.
     * @param scope the scope to add to this node's list of scopes
     */
    void addScope(Group scope) {

	Object[] scopeInfo = new Object[3];
	ArrayList addScopeList = new ArrayList();
        GroupRetained group = (GroupRetained)scope.retained;

	initAddScope(scope);
	tempKey.reset();
	group.addAllNodesForScopedFog(mirrorFog,addScopeList, tempKey);
	scopeInfo[0] = addScopeList;
	scopeInfo[1] = null;
	scopeInfo[2] = (scopes.size() > 0 ? Boolean.TRUE: Boolean.FALSE);
	sendMessage(SCOPE_CHANGED, scopeInfo);
    }
   
    /**
     * Returns a count of this nodes' scopes.
     * @return the number of scopes descendant from this node
     */
    int numScopes() {
	return scopes.size();
    }

  /** 
   * Returns the index of the specified scope within this nodes' list of scopes
   * @param scope whose index is desired
   * @return index of specified scope
   */
  int indexOfScope(Group scope) {
    if(scope != null)
      return scopes.indexOf((GroupRetained)scope.retained);
    else
      return scopes.indexOf(null);
  }

  /** 
   * Removes the specified scope from this nodes' list of scopes
   * @param scope to be removed. If the scope is not found,
   * the method returns silently
   */
  void removeScope(Group scope) {
    int i = indexOfScope(scope);
    if(i >= 0)
      removeScope(i);
  }

  void initRemoveScope(Group scope) {
   int i = indexOfScope(scope);
    if(i >= 0)
      initRemoveScope(i);
  } 

  /** 
   * Removes all the scopes from this node's list of scopes.
   * The node should revert to universal 
   * scope after this method returns
   */
  void removeAllScopes() {
    Object[] scopeInfo = new Object[3];
    ArrayList removeScopeList = new ArrayList();
    GroupRetained group;
    int n = scopes.size();

    tempKey.reset();
    for(int index = n-1; index >= 0; index--) {
       group  = (GroupRetained)scopes.elementAt(index);
       group.removeAllNodesForScopedFog(mirrorFog, removeScopeList, tempKey);
       initRemoveScope(index);
    }
    scopeInfo[0] = null;
    scopeInfo[1] = removeScopeList;
    scopeInfo[2] = Boolean.FALSE;
    sendMessage(SCOPE_CHANGED, scopeInfo);
  }
  
    /**
     * Removes all scopes from this node
     */
    void initRemoveAllScopes() {
	int n = scopes.size();
	for(int index = n-1; index >= 0; index--) 
	    initRemoveScope(index);
    }

    /**
     * This sets the immedate mode context flag
     */
    void setInImmCtx(boolean inCtx) {
        inImmCtx = inCtx;
    }
 
    /**
     * This gets the immedate mode context flag
     */
    boolean getInImmCtx() {
        return (inImmCtx);
    }

    boolean isScoped() {
	return (scopes != null);
    }

    /**
     * This abstract method is used to update the current native
     * context fog values.
     */
    abstract void update(Context ctx, double scale);


    void updateImmediateMirrorObject(Object[] objs) {
	GroupRetained group;
	Vector currentScopes;
	int i, nscopes;
	Transform3D trans;

	int component = ((Integer)objs[1]).intValue();
	if ((component & BOUNDS_CHANGED) != 0) {
	    mirrorFog.regionOfInfluence = (Bounds) objs[2];
	    if (mirrorFog.boundingLeaf == null) {
		if (objs[2] != null) {
		    mirrorFog.region = ((Bounds)mirrorFog.regionOfInfluence).copy(mirrorFog.region);
		    mirrorFog.region.transform(
				mirrorFog.regionOfInfluence,
				getCurrentLocalToVworld());
		}
		else {
		    mirrorFog.region = null;
		}
	    }
	}
	else if  ((component & BOUNDINGLEAF_CHANGED) != 0) {
	    mirrorFog.boundingLeaf = (BoundingLeafRetained)objs[2];
	    if (objs[2] != null) {
		mirrorFog.region = (Bounds)mirrorFog.boundingLeaf.transformedRegion;
	    }
	    else { 
		if (mirrorFog.regionOfInfluence != null) {
		    mirrorFog.region = ((Bounds)mirrorFog.regionOfInfluence).copy(mirrorFog.region);
		    mirrorFog.region.transform(
				mirrorFog.regionOfInfluence,
				getCurrentLocalToVworld());
		}
		else {
		    mirrorFog.region = null;
		}
		
	    }
	}
	else if ((component & SCOPE_CHANGED) != 0) {
	    Object[] scopeList = (Object[])objs[2];
	    ArrayList addList = (ArrayList)scopeList[0];
	    ArrayList removeList = (ArrayList)scopeList[1];
	    boolean isScoped = ((Boolean)scopeList[2]).booleanValue();
	    
	    if (addList != null) {
		mirrorFog.isScoped = isScoped;
		for (i = 0; i < addList.size(); i++) {
		    Shape3DRetained obj = ((GeometryAtom)addList.get(i)).source;
		    obj.addFog(mirrorFog);
		}
	    }
	    
	    if (removeList != null) {
		mirrorFog.isScoped = isScoped;
		for (i = 0; i < removeList.size(); i++) {
		    Shape3DRetained obj = ((GeometryAtom)removeList.get(i)).source;
		    obj.removeFog(mirrorFog);
		}
	    }
	}


    }

    /**
     * The update Object function.
     */
    void updateMirrorObject(Object[] objs) {

	int component = ((Integer)objs[1]).intValue();
	if ((component & COLOR_CHANGED) != 0) {
	    mirrorFog.color.set((Color3f)objs[2]);
	}
	if ((component & INIT_MIRROR) != 0) {
	    mirrorFog.color.set((Color3f)objs[3]);
	}
    }


    /**
     * Note: This routine will only be called on
     * the mirror object - will update the object's
     * cached region and transformed region 
     */
    void updateBoundingLeaf() {
        if (boundingLeaf != null && boundingLeaf.switchState.currentSwitchOn) {
            region = boundingLeaf.transformedRegion;
        } else {
            if (regionOfInfluence != null) {
		region = regionOfInfluence.copy(region);
                region.transform(regionOfInfluence, 
				 getCurrentLocalToVworld());
            } else {
                region = null;
            }
        }
    }

    /**
     * This setLive routine just calls the superclass's method (after
     * checking for use by an immediate context).  It is up to the
     * subclasses of fog to add themselves to the list of fogs
     */
    void setLive(SetLiveState s) {
        GroupRetained group;
	Vector currentScopes;
	int i, nscopes;
	TransformGroupRetained[] tlist;

        if (inImmCtx) {
	    throw new IllegalSharingException(J3dI18N.getString("FogRetained0"));
        }
        super.doSetLive(s);

	if (inSharedGroup) {
	    throw new
		IllegalSharingException(J3dI18N.getString("FogRetained1"));
	}

	
	// Create the mirror object
	// Initialization of the mirror object during the INSERT_NODE
	// message (in updateMirrorObject)
	if (mirrorFog == null) {
	    //	    mirrorFog = (FogRetained)this.clone(true);
	    mirrorFog = (FogRetained)this.clone();
	    // Assign the bounding leaf of this mirror object as null
	    // it will later be assigned to be the mirror of the lights
	    // bounding leaf object
	    mirrorFog.boundingLeaf = null;
	    mirrorFog.sgFog = this;
	}
	//	initMirrorObject();
	// If bounding leaf is not null, add the mirror object as a user
	// so that any changes to the bounding leaf will be received
	if (boundingLeaf != null) {
	    boundingLeaf.mirrorBoundingLeaf.addUser(mirrorFog);
	}
	
	if ((s.viewScopedNodeList != null) && (s.viewLists != null)) {
	    s.viewScopedNodeList.add(mirrorFog);
	    s.scopedNodesViewList.add(s.viewLists.get(0));
	} else {
	    s.nodeList.add(mirrorFog);
	}

        if (s.transformTargets != null && s.transformTargets[0] != null) {
            s.transformTargets[0].addNode(mirrorFog, Targets.ENV_TARGETS);
	    s.notifyThreads |= J3dThread.UPDATE_TRANSFORM;
        }



	// process switch leaf
        if (s.switchTargets != null &&
                        s.switchTargets[0] != null) {
            s.switchTargets[0].addNode(mirrorFog, Targets.ENV_TARGETS);
        }
        mirrorFog.switchState = (SwitchState)s.switchStates.get(0);

	s.notifyThreads |= J3dThread.UPDATE_RENDERING_ENVIRONMENT|
	    J3dThread.UPDATE_RENDER;



	super.markAsLive();


    }
    // This is called on the parent object
    void initMirrorObject(Object[] args) {
	Shape3DRetained shape;
	Object[] scopeInfo = (Object[]) args[2];
	Boolean scoped = (Boolean)scopeInfo[0];
	ArrayList shapeList = (ArrayList)scopeInfo[1];
	BoundingLeafRetained bl=(BoundingLeafRetained)((Object[])args[4])[0];
	Bounds bnds = (Bounds)((Object[])args[4])[1];

	mirrorFog.inBackgroundGroup = ((Boolean)((Object[])args[4])[2]).booleanValue();
	mirrorFog.geometryBackground = (BackgroundRetained)((Object[])args[4])[3];
	for (int i = 0; i < shapeList.size(); i++) {
	    shape = ((GeometryAtom)shapeList.get(i)).source;
	    shape.addFog(mirrorFog);
	}
	mirrorFog.isScoped = scoped.booleanValue();

	if (bl != null) {
	    mirrorFog.boundingLeaf = bl.mirrorBoundingLeaf;
	    mirrorFog.region = boundingLeaf.transformedRegion;
	} else {
	    mirrorFog.boundingLeaf = null;
	    mirrorFog.region = null;
	}
	
	if (bnds != null) {
	    mirrorFog.regionOfInfluence = bnds;
	    if (mirrorFog.region == null) {
		mirrorFog.region = (Bounds)regionOfInfluence.clone();
		mirrorFog.region.transform(regionOfInfluence, getLastLocalToVworld());
	    }
	}
	else {
	    mirrorFog.regionOfInfluence = null;
	}

    }

   // This is called on the parent object
    void clearMirrorObject(Object[] args) {
	Shape3DRetained shape;
	ArrayList shapeList = (ArrayList)args[2];
	ArrayList removeScopeList = new ArrayList();
	
	for (int i = 0; i < shapeList.size(); i++) {
	    shape = ((GeometryAtom)shapeList.get(i)).source;
	    shape.removeFog(mirrorFog);
	}		
	mirrorFog.isScoped = false;	



    }

    

    /**
     * This clearLive routine first calls the superclass's method, then
     * it removes itself to the list of fogs
     */
    void clearLive(SetLiveState s) {
	int i, j;
        GroupRetained group;

        super.clearLive(s);

        if (s.switchTargets != null &&
                        s.switchTargets[0] != null) {
            s.switchTargets[0].addNode(mirrorFog, Targets.ENV_TARGETS);
        }
	s.notifyThreads |= J3dThread.UPDATE_RENDERING_ENVIRONMENT|
	    J3dThread.UPDATE_RENDER;

	// Remove this mirror light as users of the bounding leaf 
	if (mirrorFog.boundingLeaf != null)
	    mirrorFog.boundingLeaf.removeUser(mirrorFog);
	
	if ((s.viewScopedNodeList != null) && (s.viewLists != null)) {
	    s.viewScopedNodeList.add(mirrorFog);
	    s.scopedNodesViewList.add(s.viewLists.get(0));
	} else {
	    s.nodeList.add(mirrorFog);
	}
	if (s.transformTargets != null && s.transformTargets[0] != null) {
            s.transformTargets[0].addNode(mirrorFog, Targets.ENV_TARGETS);
	    s.notifyThreads |= J3dThread.UPDATE_TRANSFORM;
	}


	if (scopes.size() > 0) {
	    J3dMessage createMessage = new J3dMessage();
	    createMessage.threads = J3dThread.UPDATE_RENDERING_ENVIRONMENT;
	    createMessage.universe = universe;
	    createMessage.type = J3dMessage.FOG_CHANGED;
	    createMessage.args[0] = this;
	    createMessage.args[1]= new Integer(CLEAR_MIRROR);
	    ArrayList removeScopeList = new ArrayList();
	    for (i = 0; i < scopes.size(); i++) {
		group = (GroupRetained)scopes.get(i);
		tempKey.reset();
		group.removeAllNodesForScopedFog(mirrorFog, removeScopeList, tempKey);
	    }
	    createMessage.args[2] = removeScopeList;
	    VirtualUniverse.mc.processMessage(createMessage);
	}
    }

    // Clone the retained side only, internal use only	
     protected Object clone() {
         FogRetained fr = (FogRetained)super.clone();

         fr.color = new Color3f(color);
         Bounds b = getInfluencingBounds();
         if (b != null) {
             fr.initInfluencingBounds(b);
         }
 
         fr.scopes = new Vector();
         fr.isDirty = 0xffff;
         fr.inImmCtx = false;
         fr.region = null;
         fr.sgFog = null;
         fr.mirrorFog = null;
	 fr.environmentSets = new UnorderList(1, EnvironmentSet.class);
         return fr;
     }

    void updateTransformChange() {
	super.updateTransformChange();
        setLocalToVworldScale(sgFog.getLastLocalToVworld().getDistanceScale());
    }

    // Called on mirror object
    void updateImmediateTransformChange() {
	// If bounding leaf is null, tranform the bounds object
	if (boundingLeaf == null) {
	    if (regionOfInfluence != null) {
		region = regionOfInfluence.copy(region);
		region.transform(regionOfInfluence,
				 sgFog.getCurrentLocalToVworld());
	    }
	    
	}
    }
    
    final void sendMessage(int attrMask, Object attr) {
	J3dMessage createMessage = new J3dMessage();
	createMessage.threads = targetThreads;
	createMessage.universe = universe;
	createMessage.type = J3dMessage.FOG_CHANGED;
	createMessage.args[0] = this;
	createMessage.args[1]= new Integer(attrMask);
	createMessage.args[2] = attr;
	VirtualUniverse.mc.processMessage(createMessage);
    }    

    void mergeTransform(TransformGroupRetained xform) {
	super.mergeTransform(xform);
        if (regionOfInfluence != null) {
            regionOfInfluence.transform(xform.transform);
        }
    }
    void getMirrorObjects(ArrayList leafList, HashKey key) {
	leafList.add(mirrorFog);
    }

    /**
     * Scale distances from local to eye coordinate
     */
    protected void validateDistancesInEc(double vworldToCoexistenceScale) {
        assert false : "subclasses should override this method";
    }

    double getLocalToVworldScale() {
        return localToVworldScale;
    }

    void setLocalToVworldScale(double localToVworldScale) {
        this.localToVworldScale = localToVworldScale;
    }
}
