/// Evolver Copyright 2000, Morgan McGuire
/// matrix@alum.mit.edu   http://graphics3d.com/matrix
import javax.swing.Icon;
import java.awt.*;
import javax.swing.ImageIcon;
import java.awt.image.*;
import java.lang.ref.*;

/**
 * A rectangular grid of floating point triplets (e.g. an X x Y x 3
 * matrix).  To avoid memory allocation and garbage collection, pooled
 * memory is used for allocating.  Call alloc() to allocate a new
 * FloatImage and free() when you are done with it.
 * <P>
 * ELMatrix := ELVector*
 */
public class ELMatrix implements Icon, Cloneable
{
    private int width;
    private int height;
    int numElements;

    private boolean dirty = true;

    /**
     * Image used for paintIcon.  Weak pointers are used to 
     * alloc the GC to be efficient with these.
     */
    private SoftReference imageReference = new SoftReference(null);

    private synchronized BufferedImage getImage() 
    {
	BufferedImage image = (BufferedImage)imageReference.get();

	if (image == null) {
	    image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
	    imageReference = new SoftReference(image);
	    dirty = true;
	}

	if (dirty) {
	    int pixel[] = new int[numElements];
	    for(int i = numElements - 1; i >= 0; i--) {
		pixel[i] = color(red[i], grn[i], blu[i]);
	    }

	    image.setRGB(0, 0, width, height, pixel, 0, width);
	    //image.flush();
	    dirty = false;
	}

	return image;
    }

    float red[];
    float grn[];
    float blu[];

    /**
     * An array for accessing channels by index instead of direct
     * pointer. 
     */
    float channel[][];

    static private ELMatrix memoryPool[] = new ELMatrix[30];

    /**
     * When true, actual memory allocations (not ones that come from the
     * memory pool) will print.  It is normal for allocations to occur
     * until the memory pool is full.  If allocations are regularly
     * occuring during animation, there is either a memory leak
     * (unbalanced calls to alloc() and free()) or the memoryPool size
     * is too small.  
     */
    static public boolean debugNoisyAllocate = false;

    /**
     * Initializes an ELMatrix from an image.  You may also want to call
     * ELMatrix.alloc().  
     */
    public ELMatrix(String filename)
    {
	Image image = new ImageIcon(filename).getImage();
	int width, height;
	// Block until the image loads.
	do {
	    // The image might not be loaded yet.  getWidth and getHeight
	    // return -1 when this is the case.  Wait until it is done.
	    width = image.getWidth(null);
	    height = image.getHeight(null);
	    Thread.yield();
	} while ((width == -1) || (height == -1));

	PixelGrabber grabber = new PixelGrabber(image, 0, 0, width, height, true);
    
	int pixel[] = null;

	try {
	    if (grabber.grabPixels()) {
		pixel = (int[])grabber.getPixels();
		if (pixel == null) {
		    throw new Error("PixelGrabber.grabPixels() returned null.");
		}
	    } else {
		throw new Error("PixelGrabber.grabPixels() returned false.");
	    }
	} catch (InterruptedException e) {
	    throw new Error("" + e);
	}

	// Expand the pixels into matrix values
	init(width, height);

	for(int i = pixel.length - 1; i >= 0; i--) {
	    int value = pixel[i];
	    red[i] = ((value >> 16) & 0xFF) / 255.0f;
	    grn[i] = ((value >> 8) & 0xFF) / 255.0f;
	    blu[i] = (value & 0xFF) / 255.0f;
	}
    }

    /**
     * Creates a new image of all 0's.
     */
    private ELMatrix(int width, int height)
    {
	if (debugNoisyAllocate) {
	    System.out.println("ALLOCATE: ELMatrix(" + width + ", " + height + ").");
	}
    
	init(width, height);
    }

    private void init(int width, int height)
    {
	this.width = width;
	this.height = height;
	numElements = width * height;
	channel = new float[3][];
	channel[0] = red = new float[numElements];
	channel[1] = grn = new float[numElements];
	channel[2] = blu = new float[numElements];
	setDirty();
    }

    /**
     * Sets all values to (r, g, b)
     */
    public void setAll(float r, float g, float b)
    {
	for(int i = numElements - 1; i >= 0; i--) {
	    red[i] = r;
	    grn[i] = g;
	    blu[i] = b;
	}
    }

    /**
     * Returns the average value of one channel.
     */
    public double average(int c) 
    {
	float channel[] = this.channel[c];
	float sum = 0.0f;
	for(int i = channel.length - 1; i >= 0 ; i--) {
	    sum += channel[i];
	}
	return sum / channel.length;
    }

    /**
     * @param percent Scale percent. 1.0 = no zoom (beyond resizing).
     * Percent must be on the range (0, 2).
     */
    public void setScaled(ELMatrix in, final double percent) 
    {
	// For extra performance, pull the arrays into locals so we avoid
	// excess dereferences.
	float ired[] = in.red;
	float igrn[] = in.grn;
	float iblu[] = in.blu;

	float xspan = width;
	float yspan = height;

	// Source dimensions
	double p = 1.0 / percent;
	int w = (int)(in.getWidth() * p);
	int h = (int)(in.getHeight() * p);

	float xc1 = w / xspan;
	float yc1 = h / yspan;

	int xcenter = width / 2;
	int ycenter = height / 2;
	float inxcenter = in.getWidth() / 2f + 0.5f;
	float inycenter = in.getHeight() / 2f + 0.5f;
	
	final int inWidth = in.getWidth();
	final int inHeight = in.getHeight();
	
	int i = width * height - 1;
	for(int y = height - 1; y >= 0; y--) {
	    for(int x = width - 1; x >= 0; x--) {
		
		// Compute the source x, y coordinates
		// (w/xspan is constant, but we want to avoid int->float->int conversion)
		int nx = (int)((x - xcenter) * xc1 + inxcenter);
		int ny = (int)((y - ycenter) * yc1 + inycenter);
	  
		if ((nx >= 0) && (ny >= 0) && (nx < inWidth) && (ny < inHeight)) {
		    // Compute the source offset
		    int n = nx + width * ny;
		    
		    // Copy the pixel over
		    red[i] = ired[n];
		    grn[i] = igrn[n];
		    blu[i] = iblu[n];
		} else {
		    red[i] = 0f;
		    grn[i] = 0f;
		    blu[i] = 0f;
		}

		i--;
	    }      
	}	
    }

    /**
     * Sets this matrix to the rotation by angle of in.
     */
    public void setRotated(ELMatrix in, final double angle) 
    {
	checkSize(in);
	
	final int width = getWidth();
	final int height = getHeight();
	final int midx = width / 2;
	final int midy = height / 2;
	final double cos = Math.cos(angle);
	final double sin = Math.sin(angle);
	
	if ((width == 128) && (height == 128)) {
	    final float ir[] = in.channel[0];
	    final float ig[] = in.channel[1];
	    final float ib[] = in.channel[2];

	    final float or[] = channel[0];
	    final float og[] = channel[1];
	    final float ob[] = channel[2];
      
	    for(int oy = height - 1; oy >= 0; oy--) {
		double oyc1 = 320 + sin * (oy - 64);
		double oyc2 = 320 + cos * (oy - 64);
		int oyc3 = oy * width;
		for(int ox = width - 1; ox >= 0; ox--) {
		    int ooffset = ox + oyc3;

		    int ix = (int)(cos * (ox - 64) + oyc1) & 127;
		    int iy = (int)(oyc2 - sin * (ox - 64)) & 127;
		    int ioffset = ix + (iy << 7);
          
		    or[ooffset] = ir[ioffset];
		    og[ooffset] = ig[ioffset];
		    ob[ooffset] = ib[ioffset];
		}
	    }  
	} else {
	    for(int oy = height - 1; oy >= 0; oy--) {
		for(int ox = width - 1; ox >= 0; ox--) {
		    int ix = (int)(width * 2 + midx + cos * (ox - midx) + sin * (oy - midy)) % width;
		    int iy = (int)(height * 2 + midy + cos * (oy - midy) - sin * (ox - midx)) % height;
		    int ioffset = ix + iy * width;
		    int ooffset = ox + oy * width;
		    for(int c = 0; c < 3; c++) {
			channel[c][ooffset] = in.channel[c][ioffset];
		    }
		}
	    }  
	}

    }

    /**
     * Copies from other to this.
     */
    public void set(ELMatrix other) 
    {
	checkSize(other);
	System.arraycopy(other.red, 0, red, 0, numElements);
	System.arraycopy(other.grn, 0, grn, 0, numElements);
	System.arraycopy(other.blu, 0, blu, 0, numElements);
    }

    private void checkSize(ELMatrix other) 
    {
	if ((other.numElements != numElements) || other.width != width) {
	    throw new Error("Matrices must be the same size");
	}
    }

    public Object clone()
    {
	ELMatrix other = ELMatrix.alloc(width, height);
	other.set(this);
	return other;
    }

    public void set(int x, int y, float r, float g, float b)
    {
	int i = x + y * width;
	red[i] = r;
	grn[i] = g;
	blu[i] = b;
    }

    /**
     * Allocates an image from a memory pool.  Images are zeroed
     * when returned.
     */
    static synchronized public ELMatrix alloc(int width, int height)
    {
	// Find the first image with the right size.
	for(int i = memoryPool.length - 1; i >= 0; i--) {
	    ELMatrix f = memoryPool[i];

	    if ((f != null) && (width == f.width) && (height == f.height)) {
		// This is a match.  Remove it from the memory pool and return
		// the image.
		memoryPool[i] = null;
		f.setAll(0f, 0f, 0f);
		f.setDirty();
		return f;
	    }
	}

	// No appropriate image was found. Allocate a new one.  Note that
	// when freed, this will enter the memory pool.
	return new ELMatrix(width, height);
    }

    /**
     * Deallocates an image, placing it in the available memory pool.
     * This returns null to encourage the idiom: <CODE>im =
     * im.free()</CODE>, which allows garbage collection and tends to prevent
     * accidental dangling pointers.
     */
    public synchronized ELMatrix free()
    {
	// Find an empty slot in the memory pool.
	for(int i = memoryPool.length - 1; i >= 0; i--) {
	    if (memoryPool[i] == null) {
		memoryPool[i] = this;
		return null;
	    }
	}

	// No empty slot found.  Deallocate a random one to avoid thrashing.
	int i = (int)Math.floor(Math.random() * memoryPool.length);
	memoryPool[i] = this;
	return null;
    }

    /**
     * Number of bytes this object consumes.
     */
    public int memorySize()
    {
	return 16 + (9*4) + (numElements * 12);
    }  

    /**
     * Returns the width of the matrix.
     */
    public int getWidth()
    {
	return width;
    }
  
    /**
     * Returns the height of the matrix.
     */
    public int getHeight() 
    {
	return height;
    }

    // Inherited from Icon
    public int getIconWidth()
    {
	return getWidth();
    }

    // Inherited from Icon
    public int getIconHeight()
    {
	return getHeight();
    }

    /**
     * Marks this matrix's internal image as being dirty.  Set this when
     * you want paintIcon to update the image it is displaying.  You
     * also need to call repaint on any components that are using this
     * icon.
     */
    public void setDirty()
    {
	dirty = true;
    }

    /**
     * Causes the image to be cleaned, if dirty.  This is useful for
     * preventing cleaning on one thread (i.e. swing) while rendering on another. 
     */
    public void forceClean()
    {
	getImage();
    }

    // THREAD NOTE: This methods is synchronized to prevent one thread
    // (animation) from forcing a cleaning while another thread (swing)
    // is drawing.

    // Inherited from Icon
    synchronized public void paintIcon(Component c, Graphics g, int x, int y) 
    {
	BufferedImage image = getImage();
	g.drawImage(image, x, y, x + getIconWidth(), y + getIconHeight(), 
		    0, 0, this.width, this.height, null);    
    }

    synchronized public void drawImage(Graphics2D graphics, int x, int y, int width, int height) 
    {
	BufferedImage image = getImage();
	graphics.drawImage(image, x, y, x + width, y + height, 0, 0, this.width, this.height, null);
    }
  
    /**
     * Produces a packed integer color from rgb floats (0-1 range).
     */
    // DESIGN NOTE: This should get inlined by the Java compiler,
    // so there is no expense for using it as a function.
    static private int color(float rf, float gf, float bf) 
    {
	int r = (rf > 0) ? (int)(rf * 256) : 0;
	if (r >= 256) {
	    r = 255;
	}

	int g = (gf > 0) ? (int)(gf * 256) : 0;
	if (g >= 256) {
	    g = 255;
	}

	int b = (bf > 0) ? (int)(bf * 256) : 0;
	if (b >= 256) {
	    b = 255;
	}

	return 0xFF000000 + (r << 16) + (g << 8) + b;
    }
}

