/*
 * Copyright (c) 2013, Slick2D
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * - Redistributions of source code must retain the above copyright notice,
 *   this list of conditions and the following disclaimer.
 * - Redistributions in binary form must reproduce the above copyright notice,
 *   this list of conditions and the following disclaimer in the documentation
 *   and/or other materials provided with the distribution.
 * - Neither the name of the Slick2D nor the names of its contributors may be
 *   used to endorse or promote products derived from this software without
 *   specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

package org.newdawn.slick;

import java.io.IOException;
import java.io.InputStream;

import org.newdawn.slick.opengl.ImageData;
import org.newdawn.slick.opengl.InternalTextureLoader;
import org.newdawn.slick.opengl.Texture;
import org.newdawn.slick.opengl.TextureImpl;
import org.newdawn.slick.opengl.pbuffer.GraphicsFactory;
import org.newdawn.slick.opengl.renderer.Renderer;
import org.newdawn.slick.opengl.renderer.SGL;
import org.newdawn.slick.util.Log;

/**
 * An image loaded from a file and renderable to the canvas
 *
 * @author kevin
 */
@SuppressWarnings("unused")
public class Image implements Renderable {
	/** The top left corner identifier */
	public static final int TOP_LEFT = 0;
	/** The top right corner identifier */
	public static final int TOP_RIGHT = 1;
	/** The bottom right corner identifier */
	public static final int BOTTOM_RIGHT = 2;
	/** The bottom left corner identifier */
	public static final int BOTTOM_LEFT = 3;
	
	/** The renderer to use for all GL operations */
	protected static SGL GL = Renderer.get();
	
	/** The sprite sheet currently in use */
	protected static Image inUse;
	/** Use Linear Filtering */
	public static final int FILTER_LINEAR = 1;
	/** Use Nearest Filtering */
	public static final int FILTER_NEAREST = 2;
	
	/** The OpenGL texture for this image */
	protected Texture texture;
	/** The width of the image */
	protected int width;
	/** The height of the image */
	protected int height;
	/** The texture coordinate width to use to find our image */
	protected float textureWidth;
	/** The texture coordinate height to use to find our image */
	protected float textureHeight;
	/** The x texture offset to use to find our image */
	protected float textureOffsetX;
	/** The y texture offset to use to find our image */
	protected float textureOffsetY;
    /** Angle to rotate the image to. */
	protected float angle;
	/** The alpha to draw the image at */
	protected float alpha = 1.0f;
	/** The name given for the image */
	protected String ref;
	/** True if this image's state has been initialised */
	protected boolean inited = false;
	/** A pixelData holding the pixel data if it's been read for this texture */
	protected byte[] pixelData;
	/** True if the image has been destroyed */
	protected boolean destroyed;

	/** The x coordinate of the centre of rotation */
    protected float centerX; 
    /** The y coordinate of the centre of rotation */
    protected float centerY; 
    
    /** A meaningful name provided by the user of the image to tag it */
    protected String name;
    
    /** The colours for each of the corners */
    protected Color[] corners;
    /** The OpenGL max filter */
    private int filter = SGL.GL_LINEAR;
    
    /** True if the image should be flipped vertically */
    private boolean flipped;
    /** The transparent colour set if any */
    private Color transparent;
    
	/**
	 * Create a texture as a copy of another
	 * 
	 * @param other The other texture to copy
	 */
	protected Image(Image other) {
		this.width = other.getWidth();
		this.height = other.getHeight();
		this.texture = other.texture;
		this.textureWidth = other.textureWidth;
		this.textureHeight = other.textureHeight;
		this.ref = other.ref;
		this.textureOffsetX = other.textureOffsetX;
		this.textureOffsetY = other.textureOffsetY;
	
		centerX = width / 2f;
		centerY = height / 2f;
		inited = true;
	}
	
	/**
	 * Cloning constructor - only used internally.
	 */
	protected Image() {
	}
	
    /**
	 * Creates an image using the specified texture
	 * 
	 * @param texture
	 *            The texture to use
	 */
	public Image(Texture texture) {
		this.texture = texture;
		ref = texture.toString();
		clampTexture();
	}
	    
	/**
	 * Create an image based on a file at the specified location
	 * 
	 * @param ref
	 *            The location of the image file to load
	 * @throws SlickException
	 *             Indicates a failure to load the image
	 */
	public Image(String ref) throws SlickException  {
		this(ref, false);
	}

	/**
	 * Create an image based on a file at the specified location
	 * 
	 * @param ref The location of the image file to load
	 * @param trans The color to be treated as transparent
	 * @throws SlickException Indicates a failure to load the image
	 */
	public Image(String ref, Color trans) throws SlickException  {
		this(ref, false, FILTER_LINEAR, trans);
	}
	
	/**
	 * Create an image based on a file at the specified location
	 * 
	 * @param ref The location of the image file to load
	 * @param flipped True if the image should be flipped on the y-axis on load
	 * @throws SlickException Indicates a failure to load the image
	 */
	public Image(String ref, boolean flipped) throws SlickException {
		this(ref, flipped, FILTER_LINEAR);
	}

	/**
	 * Create an image based on a file at the specified location
	 * 
	 * @param ref The location of the image file to load
	 * @param flipped True if the image should be flipped on the y-axis on load
	 * @param filter The filtering method to use when scaling this image
	 * @throws SlickException Indicates a failure to load the image
	 */
	public Image(String ref, boolean flipped, int filter) throws SlickException {
		this(ref, flipped, filter, null);
	}
	
	/**
	 * Create an image based on a file at the specified location
	 * 
	 * @param ref The location of the image file to load
	 * @param flipped True if the image should be flipped on the y-axis on load
	 * @param f The filtering method to use when scaling this image
	 * @param transparent The color to treat as transparent
	 * @throws SlickException Indicates a failure to load the image
	 */
	public Image(String ref, boolean flipped, int f, Color transparent) throws SlickException {
		this.filter = f == FILTER_LINEAR ? SGL.GL_LINEAR : SGL.GL_NEAREST;
		this.transparent = transparent;
		this.flipped = flipped;
		
		try {
			this.ref = ref;
			int[] trans = null;
			if (transparent != null) {
				trans = new int[3];
				trans[0] = (int) (transparent.r * 255);
				trans[1] = (int) (transparent.g * 255);
				trans[2] = (int) (transparent.b * 255);
			}
			texture = InternalTextureLoader.get().getTexture(ref, flipped, filter, trans);
		} catch (IOException e) {
			Log.error(e);
			throw new SlickException("Failed to load image from: "+ref, e);
		}
	}
	
	/**
	 * Set the image filtering to be used. Note that this will also affect any
	 * image that was derived from this one (i.e. sub-images etc)
	 * 
	 * @param f The filtering mode to use
	 */
	public void setFilter(int f) {
		this.filter = f == FILTER_LINEAR ? SGL.GL_LINEAR : SGL.GL_NEAREST;

		texture.bind();
		GL.glTexParameteri(SGL.GL_TEXTURE_2D, SGL.GL_TEXTURE_MIN_FILTER, filter); 
        GL.glTexParameteri(SGL.GL_TEXTURE_2D, SGL.GL_TEXTURE_MAG_FILTER, filter); 
	}
	
	/**
	 * Create an empty image
	 * 
	 * @param width The width of the image
	 * @param height The height of the image
	 * @throws SlickException Indicates a failure to create the underlying resource
	 */
	public Image(int width, int height) throws SlickException {
		this(width, height, FILTER_NEAREST);
	}
	
	/**
	 * Create an empty image
	 * 
	 * @param width The width of the image
	 * @param height The height of the image
	 * @param f The filter to apply to scaling the new image
	 * @throws SlickException Indicates a failure to create the underlying resource
	 */
	public Image(int width, int height, int f) throws SlickException {
		ref = super.toString();
		this.filter = f == FILTER_LINEAR ? SGL.GL_LINEAR : SGL.GL_NEAREST;
		
		try {
			texture = InternalTextureLoader.get().createTexture(width, height, this.filter);
		} catch (IOException e) {
			Log.error(e);
			throw new SlickException("Failed to create empty image "+width+"x"+height);
		}
		
		init();
	}
	
	/**
	 * Create an image based on a file at the specified location
	 * 
	 * @param in The input stream to read the image from
	 * @param ref The name that should be assigned to the image
	 * @param flipped True if the image should be flipped on the y-axis  on load
	 * @throws SlickException Indicates a failure to load the image
	 */
	public Image(InputStream in, String ref, boolean flipped) throws SlickException {
		this(in, ref, flipped, FILTER_LINEAR);
	}

	/**
	 * Create an image based on a file at the specified location
	 * 
	 * @param in The input stream to read the image from
	 * @param ref The name that should be assigned to the image
	 * @param flipped True if the image should be flipped on the y-axis on load
	 * @param filter The filter to use when scaling this image
	 * @throws SlickException Indicates a failure to load the image
	 */
	public Image(InputStream in, String ref, boolean flipped,int filter) throws SlickException {
		load(in, ref, flipped, filter, null);
	}
	
	/**
	 * Create an image from a pixelData of pixels
	 * 
	 * @param buffer The pixelData to use to create the image
	 */
	Image(ImageBuffer buffer) {
		this(buffer, FILTER_LINEAR);
        TextureImpl.bindNone();
	}
	
	/**
	 * Create an image from a pixelData of pixels
	 * 
	 * @param buffer The pixelData to use to create the image
	 * @param filter The filter to use when scaling this image
	 */
	Image(ImageBuffer buffer, int filter) {
		this((ImageData) buffer, filter);
        TextureImpl.bindNone();
	}

	/**
	 * Create an image from a image data source
	 * 
	 * @param data The pixelData to use to create the image
	 */
	public Image(ImageData data) {
		this(data, FILTER_LINEAR);
	}
	
	/**
	 * Create an image from a image data source. Note that this method uses 
	 * 
	 * @param data The pixelData to use to create the image
	 * @param f The filter to use when scaling this image
	 */
	public Image(ImageData data, int f) {
		try {
			this.filter = f == FILTER_LINEAR ? SGL.GL_LINEAR : SGL.GL_NEAREST;
			texture = InternalTextureLoader.get().getTexture(data, this.filter);
			ref = texture.toString();
		} catch (IOException e) {
			Log.error(e);
		}
	}

	/** 
	 * Get the OpenGL image filter in use
	 * 
	 * @return The filter for magnification
	 */
	public int getFilter() {
		return filter;
	}
	
	/** 
	 * Get the reference to the resource this image was loaded from, if any. Note that
	 * this can be null in the cases where an image was programatically generated.
	 * 
	 * @return The reference to the resource the reference was loaded from
	 */
	public String getResourceReference() {
		return ref;
	}
	
	/**
	 * Set the filter to apply when drawing this image
	 * 
	 * @param r The red component of the filter colour
	 * @param g The green component of the filter colour
	 * @param b The blue component of the filter colour
	 * @param a The alpha component of the filter colour
	 */
	public void setImageColor(float r, float g, float b, float a) {
		setColor(TOP_LEFT, r, g, b, a);
		setColor(TOP_RIGHT, r, g, b, a);
		setColor(BOTTOM_LEFT, r, g, b, a);
		setColor(BOTTOM_RIGHT, r, g, b, a);
	}
	
	/**
	 * Set the filter to apply when drawing this image
	 * 
	 * @param r The red component of the filter colour
	 * @param g The green component of the filter colour
	 * @param b The blue component of the filter colour
	 */
	public void setImageColor(float r, float g, float b) {
		setColor(TOP_LEFT, r, g, b);
		setColor(TOP_RIGHT, r, g, b);
		setColor(BOTTOM_LEFT, r, g, b);
		setColor(BOTTOM_RIGHT, r, g, b);
	}
	
	/** 
	 * Set the color of the given corner when this image is rendered. This is 
	 * useful lots of visual effect but especially light maps
	 * 
	 * @param corner The corner identifier for the corner to be set
	 * @param r The red component value to set (between 0 and 1)
	 * @param g The green component value to set (between 0 and 1)
	 * @param b The blue component value to set (between 0 and 1)
	 * @param a The alpha component value to set (between 0 and 1)
	 */
	public void setColor(int corner, float r, float g, float b, float a) {
		if (corners == null) {
			corners = new Color[] {new Color(1,1,1,1f),new Color(1,1,1,1f), new Color(1,1,1,1f), new Color(1,1,1,1f)};
		}
		
		corners[corner].r = r;
		corners[corner].g = g;
		corners[corner].b = b;
		corners[corner].a = a;
	}

	/** 
	 * Set the color of the given corner when this image is rendered. This is 
	 * useful lots of visual effect but especially light maps
	 * 
	 * @param corner The corner identifier for the corner to be set
	 * @param r The red component value to set (between 0 and 1)
	 * @param g The green component value to set (between 0 and 1)
	 * @param b The blue component value to set (between 0 and 1)
	 */
	public void setColor(int corner, float r, float g, float b) {
		if (corners == null) {
			corners = new Color[] {new Color(1,1,1,1f),new Color(1,1,1,1f), new Color(1,1,1,1f), new Color(1,1,1,1f)};
		}
		
		corners[corner].r = r;
		corners[corner].g = g;
		corners[corner].b = b;
	}
	
	/**
	 * Clamp the loaded texture to it's edges
	 */
	public void clampTexture() {
        if (GL.canTextureMirrorClamp()) {
        	GL.glTexParameteri(SGL.GL_TEXTURE_2D, SGL.GL_TEXTURE_WRAP_S, SGL.GL_MIRROR_CLAMP_TO_EDGE_EXT);
        	GL.glTexParameteri(SGL.GL_TEXTURE_2D, SGL.GL_TEXTURE_WRAP_T, SGL.GL_MIRROR_CLAMP_TO_EDGE_EXT);
        } else {
        	GL.glTexParameteri(SGL.GL_TEXTURE_2D, SGL.GL_TEXTURE_WRAP_S, SGL.GL_CLAMP);
        	GL.glTexParameteri(SGL.GL_TEXTURE_2D, SGL.GL_TEXTURE_WRAP_T, SGL.GL_CLAMP);
        }
	}
	
	/**
	 * Give this image a meaningful tagging name. Can be used as user data/identifier
	 * for the image.
	 * 
	 * @param name The name to assign the image
	 */
	public void setName(String name) {
		this.name = name;
	}
	
	/**
	 * Return a meaningful tagging name that has been assigned to this image. 
	 * 
	 * @return A name or null if the name hasn't been set
	 */
	public String getName() {
		return name;
	}
	
	/**
	 * Get a graphics context that can be used to draw to this image
	 * 
	 * @return The graphics context used to render to this image
	 * @throws SlickException Indicates a failure to create a graphics context
	 */
	public Graphics getGraphics() throws SlickException {
		return GraphicsFactory.getGraphicsForImage(this);
	}
	
	/**
	 * Load the image
	 * 
	 * @param in The input stream to read the image from
	 * @param ref The name that should be assigned to the image
	 * @param flipped True if the image should be flipped on the y-axis  on load
	 * @param f The filter to use when scaling this image
	 * @param transparent The color to treat as transparent
	 * @throws SlickException Indicates a failure to load the image
	 */
	private void load(InputStream in, String ref, boolean flipped, int f, Color transparent) throws SlickException {
		this.filter = f == FILTER_LINEAR ? SGL.GL_LINEAR : SGL.GL_NEAREST;
		
		try {
			this.ref = ref;
			int[] trans = null;
			if (transparent != null) {
				trans = new int[3];
				trans[0] = (int) (transparent.r * 255);
				trans[1] = (int) (transparent.g * 255);
				trans[2] = (int) (transparent.b * 255);
			}
			texture = InternalTextureLoader.get().getTexture(in, ref, flipped, filter, trans);
		} catch (IOException e) {
			Log.error(e);
			throw new SlickException("Failed to load image from: "+ref, e);
		}
	}

	/**
	 * Bind to the texture of this image
	 */
	public void bind() {
		texture.bind();
	}

	/**
	 * Reinitialise internal data
	 */
	protected void reinit() {
		inited = false;
		init();
	}
	
	/**
	 * Initialise internal data
	 */
	protected final void init() {
		if (inited) {
			return;
		}
		
		inited = true;
		if (texture != null) {
			width = texture.getImageWidth();
			height = texture.getImageHeight();
			textureOffsetX = 0;
			textureOffsetY = 0;
			textureWidth = texture.getWidth();
			textureHeight = texture.getHeight();
		}
		
		initImpl();
	
		centerX = width / 2f;
		centerY = height / 2f;
	}

	/**
	 * Hook for subclasses to perform initialisation
	 */
	protected void initImpl() {
		
	}
	
	/**
	 * Draw this image at the current location
	 */
	public void draw() {
		draw(0,0);
	}
	
	/**
	 * Draw the image based on its center 
	 * 
	 * @param x The x coordinate to place the image's center at
	 * @param y The y coordinate to place the image's center at
	 */
	public void drawCentered(float x, float y) {
		draw(x-(getWidth()/2),y-(getHeight()/2));
	}

	/**
	 * Draw the image based on its center with a color filter
	 *
	 * @param x The x coordinate to place the image's center at
	 * @param y The y coordinate to place the image's center at
	 * @param color The color filter to apply
	 */
	public void drawCentered(float x, float y, Color color) {
		draw(x - (getWidth() / 2f), y - (getHeight() / 2f), color);
	}
	
	/**
	 * Draw this image at the specified location
	 * 
	 * @param x The x location to draw the image at
	 * @param y The y location to draw the image at
	 */
	@Override
	public void draw(float x, float y) {
		init();
		draw(x,y,width,height);
	}
	
	/**
	 * Draw this image at the specified location
	 * 
	 * @param x The x location to draw the image at
	 * @param y The y location to draw the image at
	 * @param filter The color to filter with when drawing
	 */
	public void draw(float x, float y, Color filter) {
		init();
		draw(x,y,width,height, filter);
	}

	/**
	 * Draw this image as part of a collection of images
	 * 
	 * @param x The x location to draw the image at
	 * @param y The y location to draw the image at
	 * @param width The width to render the image at
	 * @param height The height to render the image at
	 */
	public void drawEmbedded(float x,float y,float width,float height) {
		init();
		
		if (corners == null) {
		    GL.glTexCoord2f(textureOffsetX, textureOffsetY);
			GL.glVertex3f(x, y, 0);
			GL.glTexCoord2f(textureOffsetX, textureOffsetY + textureHeight);
			GL.glVertex3f(x, y + height, 0);
			GL.glTexCoord2f(textureOffsetX + textureWidth, textureOffsetY
					+ textureHeight);
			GL.glVertex3f(x + width, y + height, 0);
			GL.glTexCoord2f(textureOffsetX + textureWidth, textureOffsetY);
			GL.glVertex3f(x + width, y, 0);
		} else {
			corners[TOP_LEFT].bind();
		    GL.glTexCoord2f(textureOffsetX, textureOffsetY);
			GL.glVertex3f(x, y, 0);
			corners[BOTTOM_LEFT].bind();
			GL.glTexCoord2f(textureOffsetX, textureOffsetY + textureHeight);
			GL.glVertex3f(x, y + height, 0);
			corners[BOTTOM_RIGHT].bind();
			GL.glTexCoord2f(textureOffsetX + textureWidth, textureOffsetY
					+ textureHeight);
			GL.glVertex3f(x + width, y + height, 0);
			corners[TOP_RIGHT].bind();
			GL.glTexCoord2f(textureOffsetX + textureWidth, textureOffsetY);
			GL.glVertex3f(x + width, y, 0);
		}
	}

	/**
	 * Get the x offset in texels into the source texture
	 * 
	 * @return The x offset 
	 */
	public float getTextureOffsetX() {
		init();
		
		return textureOffsetX;
	}

	/**
	 * Get the y offset in texels into the source texture
	 * 
	 * @return The y offset 
	 */
	public float getTextureOffsetY() {
		init();
		
		return textureOffsetY;
	}

	/**
	 * Get the width in texels into the source texture
	 * 
	 * @return The width
	 */
	public float getTextureWidth() {
		init();
		
		return textureWidth;
	}

	/**
	 * Get the height in texels into the source texture
	 * 
	 * @return The height
	 */
	public float getTextureHeight() {
		init();
		
		return textureHeight;
	}
	
	/**
	 * Draw the image with a given scale
	 * 
	 * @param x The x position to draw the image at
	 * @param y The y position to draw the image at
	 * @param scale The scaling to apply
	 */
	public void draw(float x,float y,float scale) {
		init();
		draw(x,y,width*scale,height*scale,Color.white);
	}
	
	/**
	 * Draw the image with a given scale
	 * 
	 * @param x The x position to draw the image at
	 * @param y The y position to draw the image at
	 * @param scale The scaling to apply
	 * @param filter The colour filter to adapt the image with
	 */
	public void draw(float x,float y,float scale,Color filter) {
		init();
		draw(x,y,width*scale,height*scale,filter);
	}
	
	/**
	 * Draw this image at a specified location and size
	 * 
	 * @param x
	 *            The x location to draw the image at
	 * @param y
	 *            The y location to draw the image at
	 * @param width
	 *            The width to render the image at
	 * @param height
	 *            The height to render the image at
	 */
	public void draw(float x,float y,float width,float height) {
		init();
		draw(x,y,width,height,Color.white);
	}

	/**
	 * Draw this image at a specified location and size
	 * 
	 * @param x The x location to draw the image at
	 * @param y The y location to draw the image at
	 * @param hshear The amount to shear the bottom points by horizontally
	 * @param vshear The amount to shear the right points by vertically
	 */
    public void drawSheared(float x,float y, float hshear, float vshear) { 
    	this.drawSheared(x, y, hshear, vshear, Color.white);
    }
	/**
	 * Draw this image at a specified location and size
	 * 
	 * @param x The x location to draw the image at
	 * @param y The y location to draw the image at
	 * @param hshear The amount to shear the bottom points by horizontally
	 * @param vshear The amount to shear the right points by vertically
	 * @param filter The colour filter to apply
	 */
    public void drawSheared(float x,float y, float hshear, float vshear, Color filter) { 
    	if (alpha != 1) {
    		if (filter == null) {
    			filter = Color.white;
    		}
    		
    		filter = new Color(filter);
    		filter.a *= alpha;
    	}
        if (filter != null) { 
            filter.bind(); 
        } 
        
        texture.bind(); 
        
        GL.glTranslatef(x, y, 0);
        if (angle != 0) {
	        GL.glTranslatef(centerX, centerY, 0.0f); 
	        GL.glRotatef(angle, 0.0f, 0.0f, 1.0f); 
	        GL.glTranslatef(-centerX, -centerY, 0.0f); 
        }
        
        GL.glBegin(SGL.GL_QUADS); 
        	init();
		
		    GL.glTexCoord2f(textureOffsetX, textureOffsetY);
			GL.glVertex3f(0, 0, 0);
			GL.glTexCoord2f(textureOffsetX, textureOffsetY + textureHeight);
			GL.glVertex3f(hshear, height, 0);
			GL.glTexCoord2f(textureOffsetX + textureWidth, textureOffsetY
					+ textureHeight);
			GL.glVertex3f(width + hshear, height + vshear, 0);
			GL.glTexCoord2f(textureOffsetX + textureWidth, textureOffsetY);
			GL.glVertex3f(width, vshear, 0);
        GL.glEnd(); 
        
        if (angle != 0) {
	        GL.glTranslatef(centerX, centerY, 0.0f); 
	        GL.glRotatef(-angle, 0.0f, 0.0f, 1.0f); 
	        GL.glTranslatef(-centerX, -centerY, 0.0f); 
        }
        GL.glTranslatef(-x, -y, 0);
    } 
    
	/**
	 * Draw this image at a specified location and size
	 * 
	 * @param x The x location to draw the image at
	 * @param y The y location to draw the image at
	 * @param width The width to render the image at
	 * @param height The height to render the image at
	 * @param filter The color to filter with while drawing
	 */
    public void draw(float x,float y,float width,float height,Color filter) { 
    	if (alpha != 1) {
    		if (filter == null) {
    			filter = Color.white;
    		}
    		
    		filter = new Color(filter);
    		filter.a *= alpha;
    	}
        if (filter != null) { 
            filter.bind(); 
        } 
       
        texture.bind(); 
        
        GL.glTranslatef(x, y, 0);
        if (angle != 0) {
	        GL.glTranslatef(centerX, centerY, 0.0f); 
	        GL.glRotatef(angle, 0.0f, 0.0f, 1.0f); 
	        GL.glTranslatef(-centerX, -centerY, 0.0f); 
        }
        
        GL.glBegin(SGL.GL_QUADS); 
            drawEmbedded(0,0,width,height); 
        GL.glEnd(); 
        
        if (angle != 0) {
	        GL.glTranslatef(centerX, centerY, 0.0f); 
	        GL.glRotatef(-angle, 0.0f, 0.0f, 1.0f); 
	        GL.glTranslatef(-centerX, -centerY, 0.0f); 
        }
        GL.glTranslatef(-x, -y, 0);
    } 

	/**
	 * Draw this image at a specified location and size as a silohette
	 * 
	 * @param x The x location to draw the image at
	 * @param y The y location to draw the image at
	 * @param width The width to render the image at
	 * @param height The height to render the image at
	 */
	public void drawFlash(float x,float y,float width,float height) {
		drawFlash(x,y,width,height,Color.white);
	}
	
	/**
	 * Set the centre of the rotation when applied to this image
	 * 
	 * @param x The x coordinate of center of rotation relative to the top left corner of the image
	 * @param y The y coordinate of center of rotation relative to the top left corner of the image
	 */
	public void setCenterOfRotation(float x, float y) {
		centerX = x;
		centerY = y;
	}

	/**
	 * Get the x component of the center of rotation of this image
	 * 
	 * @return The x component of the center of rotation 
	 */
	public float getCenterOfRotationX() {
		init();
		
		return centerX;
	}
	
	/**
	 * Get the y component of the center of rotation of this image
	 * 
	 * @return The y component of the center of rotation 
	 */
	public float getCenterOfRotationY() {
		init();
		
		return centerY;
	}
	
	/**
	 * Draw this image at a specified location and size as a silohette
	 * 
	 * @param x The x location to draw the image at
	 * @param y The y location to draw the image at
	 * @param width The width to render the image at
	 * @param height The height to render the image at
	 * @param col The color for the sillohette
	 */
	public void drawFlash(float x,float y,float width,float height, Color col) {
		init();
		
		col.bind();
		texture.bind();

		if (GL.canSecondaryColor()) {
			GL.glEnable(SGL.GL_COLOR_SUM_EXT);
			GL.glSecondaryColor3ubEXT((byte)(col.r * 255), 
													 (byte)(col.g * 255), 
													 (byte)(col.b * 255));
		}
		
		GL.glTexEnvi(SGL.GL_TEXTURE_ENV, SGL.GL_TEXTURE_ENV_MODE, SGL.GL_MODULATE);

        GL.glTranslatef(x, y, 0);
        if (angle != 0) {
	        GL.glTranslatef(centerX, centerY, 0.0f); 
	        GL.glRotatef(angle, 0.0f, 0.0f, 1.0f); 
	        GL.glTranslatef(-centerX, -centerY, 0.0f); 
        }
        
		GL.glBegin(SGL.GL_QUADS);
			drawEmbedded(0,0,width,height);
		GL.glEnd();

        if (angle != 0) {
	        GL.glTranslatef(centerX, centerY, 0.0f); 
	        GL.glRotatef(-angle, 0.0f, 0.0f, 1.0f); 
	        GL.glTranslatef(-centerX, -centerY, 0.0f); 
        }
        GL.glTranslatef(-x, -y, 0);
        
		if (GL.canSecondaryColor()) {
			GL.glDisable(SGL.GL_COLOR_SUM_EXT);
		}
	}

	/**
	 * Draw this image at a specified location and size in a white silohette
	 * 
	 * @param x The x location to draw the image at
	 * @param y The y location to draw the image at
	 */
	public void drawFlash(float x,float y) {
		drawFlash(x,y,getWidth(),getHeight());
	}
	
    /**
     * Set the angle to rotate this image to.  The angle will be normalized to 
     * be 0 <= angle < 360.  The image will be rotated around its center.
     * 
     * @param angle The angle to be set
     */
    public void setRotation(float angle) { 
        this.angle = angle % 360.0f; 
    } 
    
    /**
     * Get the current angle of rotation for this image.
     * The image will be rotated around its center.
     * 
     * @return The current angle.
     */
    public float getRotation() { 
        return angle; 
    } 
    
    /**
     * Get the alpha value to use when rendering this image
     * 
     * @return The alpha value to use when rendering this image
     */
    public float getAlpha() {
    	return alpha;
    }
    
    /**
     * Set the alpha value to use when rendering this image
     * 
     * @param alpha The alpha value to use when rendering this image
     */
    public void setAlpha(float alpha) {
    	this.alpha = alpha;
    }
    
    /**
     * Add the angle provided to the current rotation.  The angle will be normalized to 
     * be 0 <= angle < 360.  The image will be rotated around its center.
     *  
     * @param angle The angle to add.
     */
    public void rotate(float angle) { 
        this.angle += angle;
        this.angle = this.angle % 360;
    } 

	/**
	 * Get a sub-part of this image. Note that the create image retains a reference to the
	 * image data so should anything change it will affect sub-images too.
	 * 
	 * @param x The x coordinate of the sub-image
	 * @param y The y coordinate of the sub-image
	 * @param width The width of the sub-image
	 * @param height The height of the sub-image
	 * @return The image represent the sub-part of this image
	 */
	public Image getSubImage(int x,int y,int width,int height) {
		init();
		
		float newTextureOffsetX = ((x / (float) this.width) * textureWidth) + textureOffsetX;
		float newTextureOffsetY = ((y / (float) this.height) * textureHeight) + textureOffsetY;
		float newTextureWidth = ((width / (float) this.width) * textureWidth);
		float newTextureHeight = ((height / (float) this.height) * textureHeight);
		
		Image sub = new Image();
		sub.inited = true;
		sub.texture = this.texture;
		sub.textureOffsetX = newTextureOffsetX;
		sub.textureOffsetY = newTextureOffsetY;
		sub.textureWidth = newTextureWidth;
		sub.textureHeight = newTextureHeight;
		
		sub.width = width;
		sub.height = height;
		sub.ref = ref;
		sub.centerX = width / 2f;
		sub.centerY = height / 2f;
		
		return sub;
	}

	/**
	 * Draw a section of this image at a particular location and scale on the screen
	 * 
	 * @param x The x position to draw the image
	 * @param y The y position to draw the image
	 * @param srcx The x position of the rectangle to draw from this image (i.e. relative to this image)
	 * @param srcy The y position of the rectangle to draw from this image (i.e. relative to this image)
	 * @param srcx2 The x position of the bottom right cornder of rectangle to draw from this image (i.e. relative to this image)
	 * @param srcy2 The t position of the bottom right cornder of rectangle to draw from this image (i.e. relative to this image)
	 */
	public void draw(float x, float y, float srcx, float srcy, float srcx2, float srcy2) {
		draw(x,y,x+width,y+height,srcx,srcy,srcx2,srcy2);
	}
	
	/**
	 * Draw a section of this image at a particular location and scale on the screen
	 * 
	 * @param x The x position to draw the image
	 * @param y The y position to draw the image
	 * @param x2 The x position of the bottom right corner of the drawn image
	 * @param y2 The y position of the bottom right corner of the drawn image
	 * @param srcx The x position of the rectangle to draw from this image (i.e. relative to this image)
	 * @param srcy The y position of the rectangle to draw from this image (i.e. relative to this image)
	 * @param srcx2 The x position of the bottom right cornder of rectangle to draw from this image (i.e. relative to this image)
	 * @param srcy2 The t position of the bottom right cornder of rectangle to draw from this image (i.e. relative to this image)
	 */
	public void draw(float x, float y, float x2, float y2, float srcx, float srcy, float srcx2, float srcy2) {
		draw(x,y,x2,y2,srcx,srcy,srcx2,srcy2,Color.white);
	}
	
	/**
	 * Draw a section of this image at a particular location and scale on the screen
	 * 
	 * @param x The x position to draw the image
	 * @param y The y position to draw the image
	 * @param x2 The x position of the bottom right corner of the drawn image
	 * @param y2 The y position of the bottom right corner of the drawn image
	 * @param srcx The x position of the rectangle to draw from this image (i.e. relative to this image)
	 * @param srcy The y position of the rectangle to draw from this image (i.e. relative to this image)
	 * @param srcx2 The x position of the bottom right cornder of rectangle to draw from this image (i.e. relative to this image)
	 * @param srcy2 The t position of the bottom right cornder of rectangle to draw from this image (i.e. relative to this image)
	 * @param filter The colour filter to apply when drawing
	 */
	public void draw(float x, float y, float x2, float y2, float srcx, float srcy, float srcx2, float srcy2, Color filter) {
		init();

    	if (alpha != 1) {
    		if (filter == null) {
    			filter = Color.white;
    		}
    		
    		filter = new Color(filter);
    		filter.a *= alpha;
    	}
		filter.bind();
		texture.bind();
		
        GL.glTranslatef(x, y, 0);
        if (angle != 0) {
	        GL.glTranslatef(centerX, centerY, 0.0f); 
	        GL.glRotatef(angle, 0.0f, 0.0f, 1.0f); 
	        GL.glTranslatef(-centerX, -centerY, 0.0f); 
        }
        
        GL.glBegin(SGL.GL_QUADS); 
			drawEmbedded(0,0,x2-x,y2-y,srcx,srcy,srcx2,srcy2);
        GL.glEnd(); 
        
        if (angle != 0) {
	        GL.glTranslatef(centerX, centerY, 0.0f); 
	        GL.glRotatef(-angle, 0.0f, 0.0f, 1.0f); 
	        GL.glTranslatef(-centerX, -centerY, 0.0f); 
        }
        GL.glTranslatef(-x, -y, 0);
        
//		GL.glBegin(SGL.GL_QUADS);
//		drawEmbedded(x,y,x2,y2,srcx,srcy,srcx2,srcy2);
//		GL.glEnd();
	}
	
	/**
	 * Draw a section of this image at a particular location and scale on the screen, while this
	 * is image is "in use", i.e. between calls to startUse and endUse.
	 * 
	 * @param x The x position to draw the image
	 * @param y The y position to draw the image
	 * @param x2 The x position of the bottom right corner of the drawn image
	 * @param y2 The y position of the bottom right corner of the drawn image
	 * @param srcx The x position of the rectangle to draw from this image (i.e. relative to this image)
	 * @param srcy The y position of the rectangle to draw from this image (i.e. relative to this image)
	 * @param srcx2 The x position of the bottom right cornder of rectangle to draw from this image (i.e. relative to this image)
	 * @param srcy2 The t position of the bottom right cornder of rectangle to draw from this image (i.e. relative to this image)
	 */
	public void drawEmbedded(float x, float y, float x2, float y2, float srcx, float srcy, float srcx2, float srcy2) {
		drawEmbedded(x,y,x2,y2,srcx,srcy,srcx2,srcy2,null);
	}
	
	/**
	 * Draw a section of this image at a particular location and scale on the screen, while this
	 * is image is "in use", i.e. between calls to startUse and endUse.
	 * 
	 * @param x The x position to draw the image
	 * @param y The y position to draw the image
	 * @param x2 The x position of the bottom right corner of the drawn image
	 * @param y2 The y position of the bottom right corner of the drawn image
	 * @param srcx The x position of the rectangle to draw from this image (i.e. relative to this image)
	 * @param srcy The y position of the rectangle to draw from this image (i.e. relative to this image)
	 * @param srcx2 The x position of the bottom right cornder of rectangle to draw from this image (i.e. relative to this image)
	 * @param srcy2 The t position of the bottom right cornder of rectangle to draw from this image (i.e. relative to this image)
	 * @param filter The colour filter to apply when drawing
	 */
	public void drawEmbedded(float x, float y, float x2, float y2, float srcx, float srcy, float srcx2, float srcy2, Color filter) {
		if (filter != null) {
			filter.bind();
		}
		
		float mywidth = x2 - x;
		float myheight = y2 - y;
		float texwidth = srcx2 - srcx;
		float texheight = srcy2 - srcy;

		float newTextureOffsetX = (((srcx) / (width)) * textureWidth)
				+ textureOffsetX;
		float newTextureOffsetY = (((srcy) / (height)) * textureHeight)
				+ textureOffsetY;
		float newTextureWidth = ((texwidth) / (width))
				* textureWidth;
		float newTextureHeight = ((texheight) / (height))
				* textureHeight;

		GL.glTexCoord2f(newTextureOffsetX, newTextureOffsetY);
		GL.glVertex3f(x,y, 0.0f);
		GL.glTexCoord2f(newTextureOffsetX, newTextureOffsetY
				+ newTextureHeight);
		GL.glVertex3f(x,(y + myheight), 0.0f);
		GL.glTexCoord2f(newTextureOffsetX + newTextureWidth,
				newTextureOffsetY + newTextureHeight);
		GL.glVertex3f((x + mywidth),(y + myheight), 0.0f);
		GL.glTexCoord2f(newTextureOffsetX + newTextureWidth,
				newTextureOffsetY);
		GL.glVertex3f((x + mywidth),y, 0.0f);
	}
	
	/**
	 * Draw the image in a warper rectangle. The effects this can 
	 * have are many and varied, might be interesting though.
	 * 
	 * @param x1 The top left corner x coordinate
	 * @param y1 The top left corner y coordinate
	 * @param x2 The top right corner x coordinate
	 * @param y2 The top right corner y coordinate
	 * @param x3 The bottom right corner x coordinate
	 * @param y3 The bottom right corner y coordinate
	 * @param x4 The bottom left corner x coordinate
	 * @param y4 The bottom left corner y coordinate
	 */
	public void drawWarped(float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4) {
        Color.white.bind();
        texture.bind();

        GL.glTranslatef(x1, y1, 0);
        if (angle != 0) {
            GL.glTranslatef(centerX, centerY, 0.0f);
            GL.glRotatef(angle, 0.0f, 0.0f, 1.0f);
            GL.glTranslatef(-centerX, -centerY, 0.0f);
        }

        GL.glBegin(SGL.GL_QUADS);
        init();

        GL.glTexCoord2f(textureOffsetX, textureOffsetY);
        GL.glVertex3f(0, 0, 0);
        GL.glTexCoord2f(textureOffsetX, textureOffsetY + textureHeight);
        GL.glVertex3f(x2 - x1, y2 - y1, 0);
        GL.glTexCoord2f(textureOffsetX + textureWidth, textureOffsetY
                + textureHeight);
        GL.glVertex3f(x3 - x1, y3 - y1, 0);
        GL.glTexCoord2f(textureOffsetX + textureWidth, textureOffsetY);
        GL.glVertex3f(x4 - x1, y4 - y1, 0);
        GL.glEnd();

        if (angle != 0) {
            GL.glTranslatef(centerX, centerY, 0.0f);
            GL.glRotatef(-angle, 0.0f, 0.0f, 1.0f);
            GL.glTranslatef(-centerX, -centerY, 0.0f);
        }
        GL.glTranslatef(-x1, -y1, 0);
    }
	
	/**
	 * Get the width of this image
	 * 
	 * @return The width of this image
	 */
	public int getWidth() {
		init();
		return width;
	}

	/**
	 * Get the height of this image
	 * 
	 * @return The height of this image
	 */
	public int getHeight() {
		init();
		return height;
	}
	
	/**
	 * Get a copy of this image. This is a shallow copy and does not 
	 * duplicate image adata.
	 * 
	 * @return The copy of this image
	 */
	public Image copy() {
		init();
		return getSubImage(0,0,width,height);
	}

	/**
	 * Get a scaled copy of this image with a uniform scale
	 * 
	 * @param scale The scale to apply
	 * @return The new scaled image
	 */
	public Image getScaledCopy(float scale) {
		init();
		return getScaledCopy((int) (width*scale),(int) (height*scale));
	}
	
	/**
	 * Get a scaled copy of this image
	 * 
	 * @param width The width of the copy
	 * @param height The height of the copy
	 * @return The new scaled image
	 */
	public Image getScaledCopy(int width, int height) {
		init();
		Image image = copy();
		image.width = width;
		image.height = height;
		image.centerX = width / 2f;
		image.centerY = height / 2f;
		return image;
	}
	
	/**
	 * Make sure the texture cordinates are inverse on the y axis
	 */
	public void ensureInverted() {
		if (textureHeight > 0) {
			textureOffsetY = textureOffsetY + textureHeight;
			textureHeight = -textureHeight;
		}
	}
	
	/**
	 * Get a copy image flipped on potentially two axis
	 * 
	 * @param flipHorizontal True if we want to flip the image horizontally
	 * @param flipVertical True if we want to flip the image vertically
	 * @return The flipped image instance
	 */
	public Image getFlippedCopy(boolean flipHorizontal, boolean flipVertical) {
		init();
		Image image = copy();
		
		if (flipHorizontal) {
			image.textureOffsetX = textureOffsetX + textureWidth;
			image.textureWidth = -textureWidth;
		}
		if (flipVertical) {
			image.textureOffsetY = textureOffsetY + textureHeight;
			image.textureHeight = -textureHeight;
		}
		
		return image;
	}

	/**
	 * End the use of this sprite sheet and release the lock. 
	 * 
	 * @see #startUse
	 */
	public void endUse() {
		if (inUse != this) {
			throw new RuntimeException("The sprite sheet is not currently in use");
		}
		inUse = null;
		GL.glEnd();
	}
	
	/**
	 * Start using this sheet. This method can be used for optimal rendering of a collection 
	 * of sprites from a single sprite sheet. First, startUse(). Then render each sprite by
	 * calling renderInUse(). Finally, endUse(). Between start and end there can be no rendering
	 * of other sprites since the rendering is locked for this sprite sheet.
	 */
	public void startUse() {
		if (inUse != null) {
			throw new RuntimeException("Attempt to start use of a sprite sheet before ending use with another - see endUse()");
		}
		inUse = this;
		init();

		Color.white.bind();
		texture.bind();
		GL.glBegin(SGL.GL_QUADS);
	}
	
	@Override
	public String toString() {
		init();
		
		return "[Image "+ref+" "+width+"x"+height+"  "+textureOffsetX+","+textureOffsetY+","+textureWidth+","+textureHeight+"]";
	}
	
	/**
	 * Get the OpenGL texture holding this image
	 * 
	 * @return The OpenGL texture holding this image
	 */
	public Texture getTexture() {
		return texture;
	}
	
	/**
	 * Set the texture used by this image
	 * 
	 * @param texture The texture used by this image
	 */
	public void setTexture(Texture texture) {
		this.texture = texture;
		reinit();
	}

	/**
	 * Translate an unsigned int into a signed integer
	 * 
	 * @param b The byte to convert
	 * @return The integer value represented by the byte
	 */
	private int translate(byte b) {
		if (b < 0) {
			return 256 + b;
		}
		
		return b;
	}
	
	/**
	 * Get the colour of a pixel at a specified location in this image
	 * 
	 * @param x The x coordinate of the pixel
	 * @param y The y coordinate of the pixel
	 * @return The Color of the pixel at the specified location
	 */
	public Color getColor(int x, int y) {
		if (pixelData == null) {
			pixelData = texture.getTextureData();
		}
		
		int xo = (int) (textureOffsetX * texture.getTextureWidth());
		int yo = (int) (textureOffsetY * texture.getTextureHeight());
		
		if (textureWidth < 0) {
			x = xo - x;
		} else {
			x = xo + x;
		} 
		
		if (textureHeight < 0) {
			y = yo - y;
		} else {
			y = yo + y;
		}
		
		int offset = x + (y * texture.getTextureWidth());
		offset *= texture.hasAlpha() ? 4 : 3;
		
		if (texture.hasAlpha()) {
			return new Color(translate(pixelData[offset]),translate(pixelData[offset+1]),
							 translate(pixelData[offset+2]),translate(pixelData[offset+3]));
		} else {
			return new Color(translate(pixelData[offset]),translate(pixelData[offset+1]),
					 	     translate(pixelData[offset+2]));
		}
	}

	/**
	 * Get the alpha value of a pixel at a specified location in this image,
	 * or 1f if the image does not support transparency.
	 * 
	 * @param x The x coordinate of the pixel
	 * @param y The y coordinate of the pixel
	 * @return The alpha level of the pixel at the specified location
	 */
	public float getAlphaAt(int x, int y) {
		if (!texture.hasAlpha())
			return 1f;

		if (pixelData == null)
			pixelData = texture.getTextureData();

		// scale coordinates based on the image scale
		x = x * texture.getImageWidth() / width;
		y = y * texture.getImageHeight() / height;

		int xo = (int) (textureOffsetX * texture.getTextureWidth());
		int yo = (int) (textureOffsetY * texture.getTextureHeight());

		x = (textureWidth < 0) ? xo - x : xo + x;
		y = (textureHeight < 0) ? yo - y : yo + y;

		int offset = x + (y * texture.getTextureWidth());
		offset *= 4;
		return (offset + 3 >= pixelData.length) ? 1f : translate(pixelData[offset + 3]) / 255f;
	}
	
	/**
	 * Check if this image has been destroyed
	 * 
	 * @return True if this image has been destroyed
	 */
	public boolean isDestroyed() {
		return destroyed;
	}
	
	/**
	 * Destroy the image and release any native resources. 
	 * Calls on a destroyed image have undefined results
	 * 
	 * @throws SlickException Indicates a failure to release resources on the graphics card
	 */
	public void destroy() throws SlickException {
		if (isDestroyed()) {
			return;
		}
		
		destroyed = true;
		texture.release();
		GraphicsFactory.releaseGraphicsForImage(this);
	}
	
	/**
	 * Flush the current pixel data to force a re-read next update
	 */
	public void flushPixelData() {
		pixelData = null;
	}
}
