package com.xith.java3d.overlay;

import javax.media.j3d.*;
import javax.vecmath.*;
import java.awt.*;
import java.awt.image.*;
import java.awt.image.Raster;
import java.awt.event.*;
import java.awt.color.*;
import java.util.*;

/**
 * Copyright:  Copyright (c) 2000,2001
 * Company:    Teseract Software, LLP
 * @author David Yazel
 * @author Will Holcomb
 */

public class OverlayBase implements Overlay, ScreenComponent {
    // I do not undersand what this is for
    private final static double consoleZ = 2.1f;

    protected int backgroundMode = BACKGROUND_COPY;
    protected int[] relativePosition = {PLACE_LEFT, PLACE_TOP};

    private BufferedImage backgroundImage;
    private boolean hasAlpha;
    private boolean visible;
    private boolean antialiased;
    private Rectangle bounds;                  // Canvas bounds occupied by this overlay
    private Dimension offset;
    private UpdateManager updateManager;
    private Canvas3D canvas3D;                // canvas being displayed on

    protected BufferedImage canvas;             // drawing bounds
    protected SubOverlay[] subOverlay         // list of SubOverlay nodes
	= new SubOverlay[0];                  //  blocking is performed using this so it can't be null
    protected int activeBuffer = SubOverlay.NEXT_BUFFER;

    protected BranchGroup consoleBranchGroup;        // branch group for overlay
    protected TransformGroup consoleTransformGroup;  // transform group -> screen coords

    // shared resources for the sub-overlays

    private RenderingAttributes renderAttributes;
    private PolygonAttributes polygonAttributes;
    private TextureAttributes textureAttributes;
    private TransparencyAttributes transparencyAttributes;
    
    // checks for altered elements

    public final static int VISIBLE = 0;
    public final static int POSITION = 1;
    public final static int ACTIVE_BUFFER = 2;
    private boolean[] dirtyCheck = new boolean[3];

    // Fires appropriate mouse events


    /**
     * Creates a new overlay covering the given canvas bounds. Updates are managed
     * automatically. This Overlay must still be attached to the view platform
     * transform.
     *
     * @param canvas3D Canvas being drawn onto
     * @param bounds    Bounds on the canvas covered by the overlay
     */
    public OverlayBase( Canvas3D canvas3D, Rectangle bounds ) {
	this( canvas3D, bounds, true, false, null );
    }

    public OverlayBase( Canvas3D canvas3D, Rectangle bounds, UpdateManager manager ) {
	this(canvas3D, bounds, true, false, manager);
    }

    public OverlayBase( Canvas3D canvas3D, Rectangle bounds, 
		    boolean clipAlpha, boolean blendAlpha ) {
	this(canvas3D, bounds, clipAlpha, blendAlpha, null);
    }

    public OverlayBase( Canvas3D canvas3D, Rectangle bounds,
		    boolean clipAlpha, boolean blendAlpha,
		    UpdateManager updateManager ) {
	this(canvas3D, bounds, clipAlpha, blendAlpha, updateManager, 2);
    }

    /**
     * Constructs an overlay window. This window will not be visible
     * unless it is added to the scene under the view platform transform
     *
     * @param canvas3D      The canvas the overlay is drawn on
     * @param bounds         The part of the canvas covered by the overlay
     * @param clipAlpha     Should the polygon clip where alpha is zero
     * @param blendAlpha    Should we blend to background where alpha is < 1
     * @param updateManager Responsible for allowing the Overlay to update
     *                       between renders. If this is null a default
     *                       manager is created.
     * @param numBuffers    The number of buffers to generate, the default
     *                       is two.
     */
    public OverlayBase( Canvas3D canvas3D, Rectangle bounds,
		    boolean clipAlpha, boolean blendAlpha,
		    UpdateManager updateManager, int numBuffers ) {
	this.canvas3D = canvas3D;
	this.visible = true;
	this.antialiased = true;
	this.bounds = bounds;
	this.offset = new Dimension(bounds.x, bounds.y);
	this.hasAlpha = clipAlpha || blendAlpha;
	this.canvas = OverlayUtilities.createBufferedImage(bounds.getSize(), hasAlpha);
	
	canvas3D.addComponentListener(new ComponentAdapter() {
		public void componentResized(ComponentEvent e) {
		    dirty(POSITION);
		}
	    });
	
	// define the branch group where we are putting all the sub-overlays
	
	consoleBranchGroup = new BranchGroup();
	consoleTransformGroup = new TransformGroup();
	consoleTransformGroup.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);
	consoleBranchGroup.addChild(consoleTransformGroup);

	if (updateManager == null) {
	    UpdateControlBehavior updateBehavior = new UpdateControlBehavior(this);
	    updateBehavior.setSchedulingBounds(new BoundingSphere());
	    consoleBranchGroup.addChild(updateBehavior);
	    updateManager = updateBehavior ;
	}

	this.updateManager = updateManager;

	// define the rendering attributes used by all sub-overlays
	
	renderAttributes = new RenderingAttributes();
	if (clipAlpha) {
	    renderAttributes.setAlphaTestFunction(RenderingAttributes.NOT_EQUAL);
	    renderAttributes.setAlphaTestValue(0);
	}
	renderAttributes.setDepthBufferEnable(true);
	renderAttributes.setDepthBufferWriteEnable(true);
	renderAttributes.setIgnoreVertexColors(true);
	renderAttributes.setCapability(RenderingAttributes.ALLOW_VISIBLE_READ);
	renderAttributes.setCapability(RenderingAttributes.ALLOW_VISIBLE_WRITE);
	
	// define the polygon attributes for all the sub-overlays
	
	polygonAttributes = new PolygonAttributes();
	polygonAttributes.setBackFaceNormalFlip(false);
	polygonAttributes.setCullFace(PolygonAttributes.CULL_NONE);
	polygonAttributes.setPolygonMode(PolygonAttributes.POLYGON_FILL);
	
	// define the texture attributes for all the sub-overlays
	
	textureAttributes = new TextureAttributes();
	textureAttributes.setTextureMode(TextureAttributes.REPLACE);
	textureAttributes.setPerspectiveCorrectionMode(TextureAttributes.FASTEST);
	
	// if this needs to support transparancy set up the blend
	
	if (hasAlpha) {
	    transparencyAttributes = new TransparencyAttributes(TransparencyAttributes.BLENDED, 1.0f);
	    textureAttributes.setTextureBlendColor(new Color4f(0, 0, 0, 1));
	}
	
	Vector overlays = OverlayUtilities.subdivide(bounds.getSize(), 16, 256);
	synchronized(subOverlay) {
	    subOverlay = new SubOverlay[overlays.size()];
	    int n = overlays.size();
	    for (int i = 0; i < n; i++) {
		Rectangle currentOverlay = (Rectangle)overlays.get(i);
		subOverlay[i] = new SubOverlay(this, currentOverlay, numBuffers);
		consoleTransformGroup.addChild(subOverlay[i].getShape());
	    }
	}

	initialize();
	
	// Dirty everything and an initial WakeupOnActivation will sync everything

	for(int i = dirtyCheck.length - 1; i >= 0; i--) {
	    dirtyCheck[i] = true;
	}

	repaint();
    }

    protected void initialize() {
    }
    
    public Rectangle getBounds() {
	return bounds;
    }

    public UpdateManager getUpdateManager() {
	return updateManager;
    }

    public void setUpdateManager(UpdateManager updateManager) {
	this.updateManager = updateManager;
	updateManager.updateRequested();
    }

    /**
     * Sets the relative offset of the overlay. How this translates into
     * screen coordinates depends on the value of relativePosition()
     */
    public void setOffset( Dimension offset ) {
	setOffset(offset.width, offset.height);
    }

    /**
     * Sets the relative offset of the overlay. How this translates into
     * screen coordinates depends on the value of relativePosition()
     */
    public void setOffset( int width, int height) {
	if(offset.width != width || offset.height != height) {
	    synchronized(offset) {
		offset.width = width;
		offset.height = height;
		dirty(POSITION);
	    }
	}
    }
    
    /**
     * Sets the relative position of the overlay on the screen using a 2 dimensional array.
     *
     * @param relativePosition[X_PLACEMENT] May be PLACE_LEFT, PLACE_RIGHT, or PLACE_CENTER
     * @param relativePosition[Y_PLACEMENT] May be PLACE_TOP, PLACE_BOTTOM, or PLACE_CENTER
     */
    public void setRelativePosition(int[] relativePositon) {
	setRelativePosition(relativePosition[X_PLACEMENT], relativePosition[Y_PLACEMENT]);
    }

    /**
     * Sets the relative position of the overlay on the screen.
     *
     * @param xType May be PLACE_LEFT, PLACE_RIGHT, or PLACE_CENTER
     * @param yType May be PLACE_TOP, PLACE_BOTTOM, or PLACE_CENTER
     */
    public void setRelativePosition(int xType, int yType) {
	if(relativePosition[X_PLACEMENT] != xType
	   || relativePosition[Y_PLACEMENT] != yType) {
	    relativePosition[X_PLACEMENT] = xType;
	    relativePosition[Y_PLACEMENT] = yType;
	    dirty(POSITION);
	}
    }

    /**
     * Return the root of the overlay and its sub-overlays so it can be
     * added to the scene graph
     */
    public BranchGroup getRoot() {
	return consoleBranchGroup;
    }
    
    /**
     * Returns if the Overlay may be semi-transparent
     */
    public boolean hasAlphaComponent() {
	return hasAlpha;
    }
    
    /**
     * Sets whether drawing onto this Overlay is anialiased.
     */
    public void setAntialiased(boolean antialiased) {
	if(this.antialiased != antialiased) {
	    this.antialiased = antialiased;
	    repaint();
	}
    }

    public boolean isAntialiased() {
        return antialiased;
    }

    /**
     * Returns the canvas being drawn on.
     */
    public Canvas3D getCanvas() {
	return canvas3D;
    }

    /**
     * Return the rendering attributes shared by all sub-overlays
     */
    public RenderingAttributes getRenderingAttributes() {
	return renderAttributes;
    }
    
    /**
     * Return the polygon attributes shared by all the sub-overlays
     */
    public PolygonAttributes getPolygonAttributes() {
	return polygonAttributes;
    }
    
    /**
     * Return the texture attributes shared by all the sub-overlays
     */
    public TextureAttributes getTextureAttributes() {
	return textureAttributes;
    }

    /**
     * Return the transparency attributes
     */
    public TransparencyAttributes getTransparencyAttributes() {
	return transparencyAttributes;
    }
    
    /**
     * Prepares the canvas to be painted.  This should only be called internally
     * or from an owner like the ScrollingOverlay class. paint(Graphics2D g)
     * should be used to paint the OverlayBase.
     */
    protected Graphics2D getGraphics() {
	if (backgroundMode == BACKGROUND_COPY && backgroundImage != null) {
	    canvas.setData(backgroundImage.getRaster());
	}
	Graphics2D g = (Graphics2D)canvas.getGraphics();
	if(antialiased) {
	    g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
			       RenderingHints.VALUE_ANTIALIAS_ON);
	    g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
			       RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
	}
	return g;
    }

    // Used to avoid calls to repaint backing up
    private boolean painting = false;
    
    /**
     * This is called to trigger a repaint of the overlay. This will return once
     * the back buffer has been built, but before the swap.
     */
    public void repaint() {
	if(!painting) {
	    painting = true;

	    Graphics2D g = getGraphics();
	    paint(g);
	    g.dispose();
	    
	    updateBuffer(canvas, SubOverlay.NEXT_BUFFER);
	    setActiveBuffer(SubOverlay.NEXT_BUFFER);

	    painting = false;
	} else {
	    System.err.println("Skipped paint in: " + this);
	}
    }

    protected void updateBuffer(BufferedImage image, int bufferIndex) {
	synchronized (subOverlay) {
	    for(int i = subOverlay.length - 1; i >= 0; i--) {
		subOverlay[i].updateBuffer(image, bufferIndex);
	    }
	}
    }

    /**
     * Changes the visibility of the overlay.
     */
    public void setVisible(boolean visible) {
	if(this.visible != visible) {
	    this.visible = visible;
	    dirty(VISIBLE);
	}
    }
    
    public boolean isVisible() {
	return visible;
    }

    /**
     * This is where the actualy drawing of the window takes place.  Override
     * this to alter the contents of what is show in the window.
     */
    public void paint(Graphics2D g) {
    }

    protected void setActiveBuffer(int activeBuffer) {
	this.activeBuffer = activeBuffer;
	dirty(ACTIVE_BUFFER);
    }

    /**
     * Sets the background to a solid color. If a background image already exists then
     * it will be overwritten with this solid color.  It is completely appropriate to
     * have an alpha component in the color if this is a alpha capable overlay.
     * In general you should only use background images if this is an overlay that is
     * called frequently, since you could always paint it inside the paint() method.
     * BackgroundMode must be in BACKGROUND_COPY for the background to be shown.
     */
    public void setBackgroundColor(Color color) {
	int pixels[] = new int[bounds.width * bounds.height];
	int rgb = color.getRGB();
	for (int i = pixels.length - 1; i >= 0; i--) {
	    pixels[i] = rgb;
	}
	getBackgroundImage().setRGB(0, 0, bounds.width, bounds.height, pixels, 0, bounds.width);
	repaint();
    }

    /**
     * Returns the background for the overlay. Updates to this image will not be shown in
     * the overlay until repaint() is called.
     * BackgroundMode must be in BACKGROUND_COPY for the background to be shown.
     */
    public BufferedImage getBackgroundImage() {
	if (backgroundImage == null) {
	    backgroundImage = OverlayUtilities.createBufferedImage(bounds.getSize(), hasAlpha);
	}
	return backgroundImage;
    }

    /**
     * Sets the background image to the one specified.  It does not have to be the same
     * size as the overlay but the it should be at least as big.
     * BackgroundMode must be in BACKGROUND_COPY for the background to be shown.
     */
    public void setBackgroundImage(BufferedImage backgroundImage) {
	if(this.backgroundImage != backgroundImage) {
	    this.backgroundImage = backgroundImage;
	    repaint();
	}
    }

    /**
     * Sets the background mode.  BACKGROUND_COPY will copy the raster data from the
     * background into the canvas before paint() is called. BACKGROUND_NONE will cause
     * the background to be disabled and not used.
     */
    public void setBackgroundMode(int mode) {
	if(backgroundMode != mode) {
	    backgroundMode = mode;
	    repaint();
	}
    }
    
    private void syncPosition() {
	synchronized(bounds) {
	    Dimension canvas3DSize = canvas3D.getSize();

	    OverlayUtilities.repositonBounds(bounds, relativePosition,
					    canvas3DSize, offset);

	    // get the field of view and then calculate the width in meters of the
	    // screen
	    
	    double fov = canvas3D.getView().getFieldOfView();
	    double consoleWidth = 2 * Math.tan(fov / 2.0) * consoleZ;
	    
	    // calculate the ratio between the canvas in pixels and the screen in 
	    // meters and use that to find the height of the screen in meters
	
	    double scale = consoleWidth / canvas3DSize.getWidth();
	    double consoleHeight = canvas3DSize.getHeight() * scale;
	
	    // The texture is upside down relative to the canvas so this has to
	    // be flipped to be in the right place. bounds needs to have the correct
	    // value to be used in Overlays that relay on it to know their position
	    // like mouseovers

	    Point flippedPoint = new Point(bounds.x,
					   canvas3DSize.height - bounds.height - bounds.y);

	    // build the plane offset
	
	    Transform3D planeOffset = new Transform3D();
	    planeOffset.setTranslation(new Vector3d(-consoleWidth / 2 + flippedPoint.getX() * scale,
						    -consoleHeight / 2 + flippedPoint.getY() * scale,
						    -consoleZ));    
	    planeOffset.setScale(scale);
	    consoleTransformGroup.setTransform(planeOffset);

	    dirtyCheck[POSITION] = false;
	}
    }

    private void syncVisible() {
	renderAttributes.setVisible(visible);
	dirtyCheck[VISIBLE] = false;
    }

    private void syncActiveBuffer() {
	synchronized (subOverlay) {
	    for(int i = subOverlay.length - 1; i >= 0; i--) {
		subOverlay[i].setActiveBufferIndex(activeBuffer);
	    }
	    dirtyCheck[ACTIVE_BUFFER] = false;
	}
    }

    public void dirty(int property) {
	dirtyCheck[property] = true;
	if (updateManager != null) {
	    updateManager.updateRequested();
	} else {
	    System.err.println("Null update manager in: " + this);
	}
    }

    public void update() {
	if(dirtyCheck[POSITION]) {
            syncPosition();
        }

        if(dirtyCheck[VISIBLE]) {
            syncVisible();
        }

        if (dirtyCheck[ACTIVE_BUFFER]) {
            syncActiveBuffer();
        }
    }
}
