/* 
 * JGimp - A Java extension for the GIMP
 * Copyright (C) 2002  Georgia Tech Research Corp.
 * Written by Michael Terry, mterry@cc.gatech.edu
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

package org.gimp.jgimp;

import java.awt.Color;
import java.awt.Dimension;
import java.util.Vector;

import org.gimp.jgimp.nativewrappers.JGimpInt32;
import org.gimp.jgimp.proxy.JGimpExtendedProxy;
import org.gimp.jgimp.proxy.JGimpProxy;
import org.gimp.jgimp.proxy.JGimpProxyException;

/**
 * A Java-friendly wrapper class for GIMP images.
 */
public class JGimpImage extends JGimpData {

    private GimpApp m_App = null;
    private int m_ID = -1;

    JGimpImage(GimpApp app, int ID) {
        m_App = app;
        m_ID = ID;
    }
    JGimpImage(JGimpProxy proxy, int ID) {
        m_App = new GimpApp(proxy);
        m_ID = ID;
    }
    
    public final static int MERGE_EXPAND_AS_NECESSARY  = 0;
    public final static int MERGE_CLIP_TO_IMAGE        = 1;
    public final static int MERGE_CLIP_TO_BOTTOM_LAYER = 2; 

    /**
     * @see JGimpData#getGimpPDBArgType()
     */
    public int getGimpPDBArgType() {
        return JGimpPDBArgTypeConstants.PDB_IMAGE;
    }

    /**
     * Returns the GIMP's internal ID for this image.
     */
    public int getID() {
        return m_ID;
    }

    public String toString() {
        return "JGimpImage, value: " + m_ID;
    }

    public boolean equals(Object inRight) {
        if (inRight == null) {
            return false;
        }

        if (inRight instanceof JGimpImage) {
            return (((JGimpImage) inRight).getID() == this.getID());
        }
        return false;
    }

    /**
     * Returns the GIMP's internal ID for this drawable.
     */
    public int convertToInt() throws JGimpInvalidDataCoercionException {
        return m_ID;
    }

    /**
     * Returns whether this version of JGimp can render the entire
     * image (all layers) into a single pixel buffer. This capability
     * is useful for displaying GIMP images within Java.
     */
    public boolean canRenderEntireImage() {
        return (m_App.getProxy() instanceof JGimpExtendedProxy);
    }
    /**
     * Renders the specified region of the image (including all visible
     * layers) into the given buffer. Throws a JGimpException if this version
     * of JGimp does not support this operation.
     * @throws JGimpProxyException
     * @throws JGimpInvalidPixelRegionException
     * @throws JGimpInvalidImageException
     * @throws JGimpBufferOverflowException
     * @throws JGimpException
     * @see JGimpImage#canRenderEntireImage()
     * @see JGimpProxy#readPixelRegionInJavaIntFormat(JGimpDrawable, int, int, int, int, int[], int)
     */
    public void renderImageInJavaIntFormat(int x, int y, int width, int height, int[] buffer, int offset)
        throws JGimpProxyException, JGimpInvalidPixelRegionException, JGimpInvalidImageException, JGimpBufferOverflowException, JGimpException {
        if (!(m_App.getProxy() instanceof JGimpExtendedProxy)) {
            throw new JGimpException("Operation not supported by this proxy. Needs embedded version of JGimp");
        }
        JGimpExtendedProxy theProxy = (JGimpExtendedProxy) m_App.getProxy();
        theProxy.renderImageInJavaIntFormat(this, x, y, width, height, buffer, offset);
    }

    /**
     * Renders the specified region of the image (including all visible
     * layers) into a new buffer. Throws a JGimpException if this version
     * of JGimp does not support this operation.
     * @throws JGimpProxyException
     * @throws JGimpInvalidPixelRegionException
     * @throws JGimpInvalidImageException
     * @throws JGimpBufferOverflowException
     * @throws JGimpException
     * @see JGimpImage#canRenderEntireImage()
     * @see JGimpProxy#readPixelRegionInJavaIntFormat(JGimpDrawable, int, int, int, int)
     */
    public int[] renderImageInJavaIntFormat(int x, int y, int width, int height)
        throws JGimpProxyException, JGimpInvalidPixelRegionException, JGimpInvalidImageException, JGimpBufferOverflowException, JGimpException {
        if (!(m_App.getProxy() instanceof JGimpExtendedProxy)) {
            throw new JGimpException("Operation not supported by this proxy. Needs embedded version of JGimp");
        }
        JGimpExtendedProxy theProxy = (JGimpExtendedProxy) m_App.getProxy();
        return theProxy.renderImageInJavaIntFormat(this, x, y, width, height);
    }

    /**
     * Creates and inserts a new layer in the image at the specified
     * location. Layers are bottom-up ordering, counting begins at 0.
     * @param width The width of the new layer.
     * @param height The height of the new layer.
     * @param includeAlphaChannel Whether to create an alpha channel for the new layer.
     * @param name The name of the new layer.
     * @param opacity The initial opacity of the new layer.
     * @param layerCombinationMode The layer combination mode for the new layer, where the value is one from {@link JGimpLayerCombinationModeConstants}.
     * @param position Where to place the new layer; layer numbering begins at 0; ordering is bottom-up (layer 0 is the bottom layer).
     * @return A reference to the new layer.
     * @throws JGimpException
     * @pdb gimp-image-add-layer
     */
    public JGimpLayer insertNewLayer(int width, int height, boolean includeAlphaChannel, String name, float opacity, int layerCombinationMode, int position) throws JGimpException {
        int type = this.getType();
        type *= 2;
        if (includeAlphaChannel) {
            type++;
        }
        JGimpLayer theLayer =
            (JGimpLayer) m_App.callProcedure(
                "gimp_layer_new",
                this,
                new JGimpInt32(width),
                new JGimpInt32(height),
                new JGimpInt32(type),
                name,
                new Double(opacity),
                new JGimpInt32(layerCombinationMode))[0];
        m_App.callProcedure("gimp_image_add_layer", this, theLayer, new JGimpInt32(position));
        return theLayer;
    }
    
    /**
     * Creates and adds a new layer to the image at the top of all
     * other layers. Layers are bottom-up ordering, counting begins at 0.
     * @param width The width of the new layer.
     * @param height The height of the new layer.
     * @param includeAlphaChannel Whether to create an alpha channel for the new layer.
     * @param name The name of the new layer.
     * @param opacity The initial opacity of the new layer.
     * @param layerCombinationMode The layer combination mode for the new layer, where the value is one from {@link JGimpLayerCombinationModeConstants}.
     * @return A reference to the new layer.
     * @throws JGimpException
     * @pdb gimp-image-add-layer
     */
    public JGimpLayer appendNewLayer(int width, int height, boolean includeAlphaChannel, String name, float opacity, int layerCombinationMode) throws JGimpException {
        return this.insertNewLayer(width, height, includeAlphaChannel, name, opacity, layerCombinationMode, -1);
    }

    /**
     * Removes the given layer from this image.
     * @param layer The layer to remove.
     * @throws JGimpException
     * @pdb gimp-image-remove-layer
     */
    public void removeLayer(JGimpLayer layer) throws JGimpException {
        m_App.callProcedure("gimp_image_remove_layer", this, layer);
    }

    /**
     * Removes the layer at the given position.
     * Layers are bottom-up ordering, counting begins at 0.
     * @param layerPosition The layer number, with the bottom layer numbered 0.
     * @pdb gimp-image-remove-layer
     */
    public void removeLayer(int layerPosition) throws JGimpException, JGimpInvalidLayerException {
        removeLayer(this.getLayerAtPosition(layerPosition));
    }

    /**
     * Returns the active layer in the image, null if no layer
     * is currently active.
     * @pdb gimp-image-get-active-layer
     */
    public JGimpLayer getActiveLayer() throws JGimpException {
        JGimpLayer theLayer = (JGimpLayer) m_App.callProcedure("gimp_image_get_active_layer", this)[0];
        if (theLayer.getID() == -1) {
            return null;
        }
        return theLayer;
    }
    
    /**
     * Sets the active layer in this image.
     * @param layer The layer to set active.
     * @throws JGimpException
     * @pdb gimp-image-set-active-layer
     */
    public void setActiveLayer(JGimpLayer layer) throws JGimpException {
        m_App.callProcedure("gimp_image_set_active_layer", this, layer);
    }
    
    /**
     * Sets the active layer in this image.
     * @param layerPosition The index of the layer to set active. Layer numbering
     *                      begins at 0 at the bottom.
     * @throws JGimpException
     * @throws JGimpInvalidLayerException
     * @pdb gimp-image-set-active-layer
     */
    public void setActiveLayer(int layerPosition) throws JGimpException, JGimpInvalidLayerException {
        this.setActiveLayer(this.getLayerAtPosition(layerPosition));
    }

    /**
     * Returns an array of the layers of this image, ordered from
     * the bottom-up.
     * @pdb gimp-image-get-layers
     */
    public JGimpLayer[] getLayers() throws JGimpException {
        int[] layers = m_App.callProcedure("gimp_image_get_layers", this)[0].convertToIntArray();
        int[] tempLayers = new int[layers.length];
        for (int i = 0; i < layers.length; i++) {
            tempLayers[i] = layers[layers.length - 1 - i];
        }
        layers = tempLayers;
        JGimpLayer[] returnArray = new JGimpLayer[layers.length];
        for (int i = 0; i < returnArray.length; i++) {
            returnArray[i] = new JGimpLayer(m_App, layers[i]);
        }
        return returnArray;
    }
    
    /**
     * Returns the number of layers in this image.
     * @throws JGimpException
     * @pdb gimp-image-get-layers
     */
    public int getNumLayers() throws JGimpException {
        return this.getLayers().length;
    }
    
    /**
     * Returns a reference to the layer at the given index
     * @param position The index of the layer. Layer numbering is
     *                 bottom-up and starts at 0.
     * @return A reference to the layer at the given index.
     * @throws JGimpException
     * @throws JGimpInvalidLayerException
     */
    public JGimpLayer getLayerAtPosition(int position) throws JGimpException, JGimpInvalidLayerException {
        JGimpLayer[] theLayers = this.getLayers();
        if ((position < 0) || (position >= theLayers.length)) {
            throw new JGimpInvalidLayerException(-1);
        }
        return theLayers[position];
    }
    
    /**
     * Returns the position (index) of the given layer within
     * the image.
     * @param layer A layer within the image.
     * @return The layer's position (index) in the image.
     * @throws JGimpException
     * @throws JGimpInvalidLayerException
     */
    public int getLayerPosition(JGimpLayer layer) throws JGimpException, JGimpInvalidLayerException {
        JGimpLayer[] layers = this.getLayers();
        for (int i = 0; i < layers.length; i++) {
            JGimpLayer thisLayer = layers[i];
            if (thisLayer.equals(layer)) {
                return i;
            }
        }
        throw new JGimpInvalidLayerException(layer.getID());
    }
    
    /**
     * Lowers the position of the given layer to make it closer
     * to the bottom of the stack of layers.
     * @param layer The layer to move.
     * @throws JGimpException
     * @pdb gimp-image-lower-layer
     */
    public void lowerLayer(JGimpLayer layer) throws JGimpException {
        m_App.callProcedure("gimp_image_lower_layer", this, layer);
    }
	/**
	 * Lowers the position of the layer at the given index,
     * moving it one closer to the bottom of the stack of layers.
     * @param layerPosition The index of the layer to move, <em>not</em>
     *                      the layer's ID. Layer numbering is bottom-up,
     *                      with counting starting at 0.
     * @pdb gimp-image-lower-layer
	 */
	public void lowerLayer(int layerPosition) throws JGimpException, JGimpInvalidLayerException {
		this.lowerLayer(this.getLayerAtPosition(layerPosition));
	}
    /**
     * Moves the given layer to the bottom of the stack of layers.
     * @param layer The layer to move to the bottom.
     * @throws JGimpException
     * @pdb gimp-image-lower-layer-to-bottom
     */
	public void lowerLayerToBottom(JGimpLayer layer) throws JGimpException {
		m_App.callProcedure("gimp_image_lower_layer_to_bottom", this, layer);
	}
    /**
     * Moves the layer at the given index to the bottom of the stack of layers.
     * @param layerPosition The index of the layer to move to the bottom,
     *                      <em>not</em> the layer's ID. Layer numbering is
     *                      bottom-up, with counting starting at 0.
     * @throws JGimpException
     * @throws JGimpInvalidLayerException
     * @pdb gimp-image-lower-layer-to-bottom
     */
	public void lowerLayerToBottom(int layerPosition) throws JGimpException, JGimpInvalidLayerException {
		this.lowerLayerToBottom(this.getLayerAtPosition(layerPosition));
	}
    /**
     * Merges the given layer down on to the layer below it.
     * @param layer The layer to merge down.
     * @param mergeType The merge type. Should be one of JGimpImage.MERGE_EXPAND_AS_NECESSARY, JGimpImage.MERGE_CLIP_TO_IMAGE,
     *                  or JGimpImage.MERGE_CLIP_TO_BOTTOM_LAYER.
     * @throws JGimpException
     * @pdb gimp-image-merge-down
     */
	private void mergeLayerDown(JGimpLayer layer, int mergeType) throws JGimpException {
		m_App.callProcedure("gimp_image_merge_down", this, layer, new JGimpInt32(mergeType));
	}
    /**
     * Merges the given layer down on to the layer below it, expanding
     * to include the area of both layers.
     * @throws JGimpException
     * @pdb gimp_image_merge_down
     */
	public void mergeLayerDownExpandAsNecessary(JGimpLayer layer) throws JGimpException {
		this.mergeLayerDown(layer, MERGE_EXPAND_AS_NECESSARY);
	}
    /**
     * Merges the given layer down on to the layer below it, clipping the final
     * layer to the extents of the image.
     * @throws JGimpException
     * @pdb gimp_image_merge_down
     */
    public void mergeLayerDownClipToImage(JGimpLayer layer) throws JGimpException {
        this.mergeLayerDown(layer, MERGE_CLIP_TO_IMAGE);
    }
    /**
     * Merges the given layer down on to the layer below it, clipping to
     * the lower (bottom) layer's extents.
     * @param layer The layer to merge.
     * @throws JGimpException
     * @pdb gimp_image_merge_down
     */
    public void mergeLayerDownClipToBottomLayer(JGimpLayer layer) throws JGimpException {
        this.mergeLayerDown(layer, MERGE_CLIP_TO_BOTTOM_LAYER);
    }
    /**
     * Merges the layer at the given index down on to the layer below it, expanding
     * to include the area of both layers.
     * @param layerPosition The index of the layer to merge down, <em>not</em> the
     *                      ID of the layer. Layer numbering is bottom-up, with
     *                      numbering starting at 0.
     * @throws JGimpException
     * @pdb gimp_image_merge_down
     */
    public void mergeLayerDownExpandAsNecessary(int layerPosition) throws JGimpException, JGimpInvalidLayerException {
        this.mergeLayerDownExpandAsNecessary(this.getLayerAtPosition(layerPosition));
    }
    /**
     * Merges the layer at the given index down on to the layer below it, clipping the final
     * layer to the extents of the image.
     * @param layerPosition The index of the layer to merge down, <em>not</em> the
     *                      ID of the layer. Layer numbering is bottom-up, with
     *                      numbering starting at 0.
     * @throws JGimpException
     * @pdb gimp_image_merge_down
     */
    public void mergeLayerDownClipToImage(int layerPosition) throws JGimpException, JGimpInvalidLayerException {
        this.mergeLayerDownClipToImage(this.getLayerAtPosition(layerPosition));
    }
    /**
     * Merges the given layer down on to the layer below it, clipping to
     * the lower (bottom) layer's extents.
     * @param layerPosition The index of the layer to merge down, <em>not</em> the
     *                      ID of the layer. Layer numbering is bottom-up, with
     *                      numbering starting at 0.
     * @throws JGimpException
     * @pdb gimp_image_merge_down
     */
    public void mergeLayerDownClipToBottomLayer(int layerPosition) throws JGimpException, JGimpInvalidLayerException {
        this.mergeLayerDownClipToBottomLayer(this.getLayerAtPosition(layerPosition));
    }
    /**
     * Combines all visible layers of the image into a single layer.
     * @return The resultant, final layer after merging all layers.
     * @throws JGimpException
     * @pdb gimp-image-flatten
     */
    public JGimpLayer flatten() throws JGimpException {
        return (JGimpLayer) m_App.callProcedure("gimp_image_flatten", this)[0];
    }

    /**
     * Returns the top-most layer that intersects the given point, or null
     * if no layer intersects the given point.
     * @throws JGimpException
     * @pdb gimp-image-pick-correlate-layer
     */
    public JGimpLayer getLayerAtPoint(int x, int y) throws JGimpException {
        int layer = m_App.callProcedure("gimp_image_pick_correlate_layer", this, new JGimpInt32(x), new JGimpInt32(y))[0].convertToInt();
        if (layer == -1) {
            return null;
        }
        return new JGimpLayer(m_App, layer);
    }
    
    /**
     * Raises the position of the given layer to make it closer
     * to the top of the stack of layers.
     * @param layer The layer to move.
     * @throws JGimpException
     * @pdb gimp-image-raise-layer
     */
    public void raiseLayer(JGimpLayer layer) throws JGimpException {
        m_App.callProcedure("gimp_image_raise_layer", this, layer);
    }
    /**
     * Raises the position of the layer at the given index,
     * moving it one closer to the top of the stack of layers.
     * @param layerPosition The index of the layer to move, <em>not</em>
     *                      the layer's ID. Layer numbering is bottom-up,
     *                      with counting starting at 0.
     * @pdb gimp-image-raise-layer
     */
    public void raiseLayer(int layerPosition) throws JGimpException, JGimpInvalidLayerException {
        this.raiseLayer(this.getLayerAtPosition(layerPosition));
    }
    /**
     * Moves the given layer to the top of the stack of layers.
     * @param layer The layer to move to the top.
     * @throws JGimpException
     * @pdb gimp-image-raise-layer-to-top
     */
    public void raiseLayerToTop(JGimpLayer layer) throws JGimpException {
        m_App.callProcedure("gimp_image_raise_layer_to_top", this, layer);
    }
    /**
     * Moves the layer at the given index to the top of the stack of layers.
     * @param layerPosition The index of the layer to move to the top,
     *                      <em>not</em> the layer's ID. Layer numbering is
     *                      bottom-up, with counting starting at 0.
     * @throws JGimpException
     * @throws JGimpInvalidLayerException
     * @pdb gimp-image-raise-layer-to-top
     */
    public void raiseLayerToTop(int layerPosition) throws JGimpException, JGimpInvalidLayerException {
        this.raiseLayerToTop(this.getLayerAtPosition(layerPosition));
    }

    /**
     * Creates and adds a layer mask to the given layer in the image.
     * @param layer    The layer to add a mask to.
     * @param maskName The name of the mask.
     * @param opacity  The initial opacity of the mask.
     * @param channelCompositingColor The channel compositing color for this mask.
     * @return A reference to the newly created mask.
     * @throws JGimpException
     * @pdb gimp-channel-new
     * @pdb gimp-image-add-layer-mask
     */
    public JGimpChannel addLayerMask(JGimpLayer layer, String maskName, double opacity, Color channelCompositingColor) throws JGimpException {
        JGimpChannel theMask =
                    (JGimpChannel) m_App.callProcedure(
                        "gimp_channel_new",
                        this,
                        new JGimpInt32(layer.getWidth()),
                        new JGimpInt32(layer.getHeight()),
                        maskName,
                        new Double(opacity),
                        channelCompositingColor)[0];
        m_App.callProcedure("gimp_image_add_layer_mask", this, layer, theMask);
        return theMask;
    }
    /**
     * Applies the layer's layer mask, and removes it once applied.
     * @param layer The layer to operate on .
     * @throws JGimpException
     * @pdb gimp-image-remove-layer-mask
     */
    public void applyLayerMask(JGimpLayer layer) throws JGimpException {
        m_App.callProcedure("gimp_image_remove_layer_mask", this, layer, new JGimpInt32(0));
    }
    /**
     * Discard's the layer's layer mask, removing it without applying it to the layer.
     * @param layer The layer to operate on.
     * @throws JGimpException
     * @pdb gimp-image-remove-layer-mask
     */
    public void discardLayerMask(JGimpLayer layer) throws JGimpException {
        m_App.callProcedure("gimp_image_remove_layer_mask", this, layer, new JGimpInt32(1));
    }

    /**
     * Creates and appends a new channel to this image. Currently, all channels
     * are inserted at the top of the channel stack (GIMP implementation detail, not JGimp).
     * @param width The width of the new channel.
     * @param height The height of the new channel.
     * @param name The name of the new channel.
     * @param opacity The channel's initial opacity.
     * @param channelCompositingColor The channel's compositing color.
     * @return A reference to the newly created channel.
     * @throws JGimpException
     * @pdb gimp-channel-new
     * @pdb gimp-image-add-channel
     */
    public JGimpChannel appendNewChannel(int width, int height, String name, double opacity, Color channelCompositingColor) throws JGimpException {
        return this.insertNewChannel(width, height, name, opacity, channelCompositingColor, -1);
    }
    /**
     * Creates and inserts a new channel for this image. Currently, all channels
     * are inserted at the top of the channel stack (GIMP implementation detail, not JGimp),
     * so the position parameter is ignored.
     * @param width The width of the new channel.
     * @param height The height of the new channel.
     * @param name The name of the new channel.
     * @param opacity The channel's initial opacity.
     * @param channelCompositingColor The channel's compositing color.
     * @param position The position to insert the channel (currently ignored -- see above).
     * @return A reference to the newly created channel.
     * @throws JGimpException
     * @pdb gimp-channel-new
     * @pdb gimp-image-add-channel
     */
    public JGimpChannel insertNewChannel(int width, int height, String name, double opacity, Color channelCompositingColor, int position)
        throws JGimpException {
        JGimpChannel theChannel =
            (JGimpChannel) m_App.callProcedure(
                "gimp_channel_new",
                this,
                new JGimpInt32(width),
                new JGimpInt32(height),
                name,
                new Double(opacity),
                channelCompositingColor)[0];
        m_App.callProcedure("gimp_image_add_channel", this, theChannel, new JGimpInt32(position));
        return theChannel;
    }
    
    /**
     * Removes the given channel from this image.
     * @param channel The channel to remove.
     * @throws JGimpException
     * @pdb gimp-image-remove-channel
     */
    public void removeChannel(JGimpChannel channel) throws JGimpException {
        m_App.callProcedure("gimp_image_remove_channel", this, channel);
    }
    /**
     * Lowers the given channel in the channel stack.
     * @param channel The channel to lower.
     * @throws JGimpException
     * @pdb gimp-image-lower-channel
     */
    public void lowerChannel(JGimpChannel channel) throws JGimpException {
        m_App.callProcedure("gimp_image_lower_channel", this, channel);
    }
    /**
     * Raises the given channel in the channel stack.
     * @param channel The channel to raise.
     * @throws JGimpException
     * @pdb gimp-image-raise-channel
     */
    public void raiseChannel(JGimpChannel channel) throws JGimpException {
        m_App.callProcedure("gimp_image_raise_channel", this, channel);
    }
    /**
     * Returns the active channel for the image, null if no channel
     * is currently active.
     * @throws JGimpException
     * @pdb gimp-image-get-active-channel
     */
    public JGimpChannel getActiveChannel() throws JGimpException {
        JGimpChannel activeChannel = (JGimpChannel) m_App.callProcedure("gimp_image_get_active_channel", this)[0];
        if (activeChannel.getID() == -1) {
            return null;
        }
        return activeChannel;
    }
    /**
     * Sets the active channel to the given channel.
     * @param channel The channel to set as active.
     * @throws JGimpException
     * @pdb gimp-image-set-active-channel
     */
    public void setActiveChannel(JGimpChannel channel) throws JGimpException {
        m_App.callProcedure("gimp_image_set_active_channel", this, channel);
    }
    /**
     * Clears the active channel, so no channel is currently active .
     * @throws JGimpException
     * @pdb gimp-image-unset-active-channel
     */
    public void clearActiveChannel() throws JGimpException {
        m_App.callProcedure("gimp_image_unset_active_channel", this);
    }
    /**
     * Retrieves a list of the channels for this image. Channel
     * ordering is bottom-up, so the lowest channel is in the first
     * array position.
     * @pdb gimp-image-get-channels
     */
    public JGimpChannel[] getChannels() throws JGimpException {
        int[] channels = m_App.callProcedure("gimp_image_get_channels", this)[1].convertToIntArray();
        int[] tempChannels = new int[channels.length];
        for (int i = 0; i < channels.length; i++) {
            tempChannels[i] = channels[channels.length - 1 - i];
        }
        channels = tempChannels;
        JGimpChannel[] returnArray = new JGimpChannel[channels.length];
        for (int i = 0; i < returnArray.length; i++) {
            returnArray[i] = new JGimpChannel(m_App, channels[i]);
        }
        return returnArray;
    }

    /**
     * Adds a horizontal guide for this image (applies to GIMP application
     * windows only).
     * @param y_position The position on the y-axis for this guide.
     * @return An integer that serves as a reference to the new gudie.
     * @throws JGimpException
     * @pdb gimp-image-add-hguide
     */
    public Integer addHorizontalGuide(int y_position) throws JGimpException {
        return new Integer(m_App.callProcedure("gimp_image_add_hguide", this, new JGimpInt32(y_position))[0].convertToInt());
    }
    /**
     * Adds a vertical guide for this image (applies to GIMP application
     * windows only).
     * @param x_position The position on the x-axis for this guide.
     * @return An integer that serves as a reference to the new gudie.
     * @throws JGimpException
     * @pdb gimp-image-add-vguide
     */
    public Integer addVerticalGuide(int x_position) throws JGimpException {
        return new Integer(m_App.callProcedure("gimp_image_add_vguide", this, new JGimpInt32(x_position))[0].convertToInt());
    }
    /**
     * Deletes the guide referenced by the given integer.
     * @param guideNumber The integer referencing a pre-existing guide.
     * @throws JGimpException
     * @pdb gimp-image-delete-guide
     */
    public void deleteGuide(Integer guideNumber) throws JGimpException {
        m_App.callProcedure("gimp_image_delete_guide", this, guideNumber);
    }
    /**
     * Returns a list of all the guides associated with this image.
     * @throws JGimpException
     * @pdb gimp-image-find-next-guide
     */
    public Integer[] getGuides() throws JGimpException {
        Vector guides = new Vector();
        int thisResult = m_App.callProcedure("gimp_image_find_next_guide", this, new JGimpInt32(0))[0].convertToInt();
        while (thisResult != 0) {
            guides.add(new JGimpInt32(thisResult));
            thisResult = m_App.callProcedure("gimp_image_find_next_guide", this, new JGimpInt32(thisResult))[0].convertToInt();
        }
        Integer[] returnArray = new Integer[guides.size()];
        return (Integer[]) guides.toArray(returnArray);
    }
    /**
     * Returns true if the guide represented by the given integer
     * is a vertical guide.
     * @param guideNumber The guide's reference number.
     * @return True if the guide is a vertical guide, false otherwise.
     * @throws JGimpException
     * @pdb gimp-image-get-guide-orientation
     */
    public boolean isVerticalGuide(Integer guideNumber) throws JGimpException {
        final int VERTICAL = 1;
        int thisOrientation = m_App.callProcedure("gimp_image_get_guide_orientation", this, guideNumber)[0].convertToInt();
        return (thisOrientation == VERTICAL);
    }
    /**
     * Returns true if the guide represented by the given integer
     * is a horizontal guide.
     * @param guideNumber The guide's reference number.
     * @return True if the guide is a horizontal guide, false otherwise.
     * @throws JGimpException
     * @pdb gimp-image-get-guide-orientation
     */
    public boolean isHorizontalGuide(Integer guideNumber) throws JGimpException {
        return !this.isVerticalGuide(guideNumber);
    }
    
    /**
     * Returns the position (coordinate) of the given guide .
     * @param guideNumber The guide's reference number.
     * @return The coordinate (x or y, depending on the guide's orientation) of the guide.
     * @throws JGimpException
     * @pdb gimp-image-get-guide-position
     */
    public int getGuidePosition(Integer guideNumber) throws JGimpException {
        return m_App.callProcedure("gimp_image_get_guide_position", this, guideNumber)[0].convertToInt();
    }
    /**
     * Displays this image in the GIMP (not in Java).
     * @pdb gimp-display-new
     */
    public JGimpDisplay display() throws JGimpException {
        return (JGimpDisplay)m_App.callProcedure("gimp_display_new", this)[0];
    }
    /**
     * Returns the height of this image.
     * @throws JGimpException
     * @pdb gimp-image-height
     */
    public int getHeight() throws JGimpException {
        return m_App.callProcedure("gimp_image_height", this)[0].convertToInt();
    }
    /**
     * Returns the width of this image.
     * @throws JGimpException
     * @pdb gimp-image-width
     */
    public int getWidth() throws JGimpException {
        return m_App.callProcedure("gimp_image_width", this)[0].convertToInt();
    }
    /**
     * Returns the size (width/height) of this image as a Dimension.
     * @throws JGimpException
     * @pdb gimp-image-width
     * @pdb gimp-image-height
     */
    public Dimension getSize() throws JGimpException {
        return new Dimension(this.getWidth(), this.getHeight());
    }
    /**
     * Resizes the image.
     * @param newWidth The new width of the image.
     * @param newHeight The new height of the image.
     * @param x_offset The x offset for the old content relative to the new image (old x + x_offset).
     * @param y_offset The y offset for the old content relative to the new image (old y + y_offset).
     * @throws JGimpException
     * @pdb gimp-image-resize
     */
    public void resize(int newWidth, int newHeight, int x_offset, int y_offset) throws JGimpException {
        m_App.callProcedure("gimp_image_resize", this, new JGimpInt32(newWidth), new JGimpInt32(newHeight), new JGimpInt32(x_offset), new JGimpInt32(y_offset));
    }
    /**
     * Scales the image to the given width and height.
     * @param newWidth The new width of the image.
     * @param newHeight The new height of the image.
     * @throws JGimpException
     * @pdb gimp-image-scale
     */
    public void scale(int newWidth, int newHeight) throws JGimpException {
        m_App.callProcedure("gimp_image_scale", this, new JGimpInt32(newWidth), new JGimpInt32(newHeight));
    }
    /**
     * Returns the active drawable for this image.
     * @throws JGimpException
     * @pdb gimp-image-active-drawable
     */
    public JGimpDrawable getActiveDrawable() throws JGimpException {
        return (JGimpDrawable) m_App.callProcedure("gimp_image_active_drawable", this)[0];
    }
    /**
     * Returns the GIMP's integer code for the type of this image (RGB - 0, 
     * Grayscale - 1, Indexed - 2).
     * @throws JGimpException
     * @pdb gimp-image-base-type
     */
    public int getType() throws JGimpException {
        return m_App.callProcedure("gimp_image_base_type", this)[0].convertToInt();
    }

    /**
     * Clears the "dirty count" of this image, or the record that indicates
     * changes have been made to the image. Essentially, this indicates to the
     * GIMP that the image has not been modified, and thus the user will not be
     * prompted to save the image when closing it.
     * @throws JGimpException
     * @pdb gimp-image-clean-all
     */
    public void clearDirtyCount() throws JGimpException {
        m_App.callProcedure("gimp_image_clean_all", this);
    }

    /**
     * Deletes the image, releasing all resources associated with it in the GIMP.
     * However, if there are GIMP displays open for the image, this will not delete it.
     * @throws JGimpException
     * @pdb gimp-image-delete
     */
    public void delete() throws JGimpException {
        m_App.callProcedure("gimp_image_delete", this);
    }

    /**
     * Returns true if the image currently has a floating selection.
     * @throws JGimpException
     * @pdb gimp-image-floating-selection
     */
    public boolean hasFloatingSelection() throws JGimpException {
        return (m_App.callProcedure("gimp_image_floating_selection", this)[0].convertToInt() != -1);
    }
    
    /**
     * Retrieves the floating selection, if one exists. If no
     * floating selection exists, returns null.
     * @throws JGimpException
     * @pdb gimp-image-floating-selection
     */
    public JGimpLayer getFloatingSelection() throws JGimpException {
        JGimpLayer selection = (JGimpLayer) m_App.callProcedure("gimp_image_floating_selection", this)[0];
        if (selection.getID() == -1) {
            return null;
        }
        return selection;
    }
    
    /**
     * Returns the drawable the floating selection is attached to.
     * If no floating selection exists, returns null.
     * @throws JGimpException
     * @pdb gimp-image-floating-sel-attached-to
     */
    public JGimpDrawable getDrawableFloatingSelectionAttachedTo() throws JGimpException {
        JGimpDrawable drawable = (JGimpDrawable) m_App.callProcedure("gimp_image_floating_sel_attached_to", this)[0];
        if (drawable.getID() == -1) {
            return null;
        }
        return drawable;
    }

    /**
     * Retrieves the color map of this image if it is an indexed image. If no
     * color map exists, it returns an empty array.
     * @throws JGimpException
     * @pdb gimp-image-get-cmap
     */    
    public Color[] getColorMap() throws JGimpException {
        byte[] rawColors = m_App.callProcedure("gimp_image_get_cmap", this)[0].convertToByteArray();
        Color[] returnColors = new Color[rawColors.length / 3];
        for (int i = 0; i < rawColors.length; i += 3) {
            returnColors[i / 3] = new Color(rawColors[i], rawColors[i + 1], rawColors[i + 2]);
        }
        return returnColors;
    }
    
    /**
     * Sets the color map for this image, if it is an indexed image.
     * @param colorMap The new color map.
     * @throws JGimpException
     * @pdb gimp-image-set-cmap
     */
    public void setColorMap(Color[] colorMap) throws JGimpException {
        byte[] rawColors = new byte[colorMap.length * 3];
        for (int i = 0; i < colorMap.length; i++) {
            rawColors[i * 3] = (byte) colorMap[i].getRed();
            rawColors[i * 3 + 1] = (byte) colorMap[i].getGreen();
            rawColors[i * 3 + 2] = (byte) colorMap[i].getBlue();
        }
        m_App.callProcedure("gimp_image_set_cmap", this, new JGimpInt32(rawColors.length), rawColors);
    }

    /**
     * Returns the filename for this image, if it exists. If no
     * file name exists, returns either an empty String or null.
     * @throws JGimpException
     * @pdb gimp-image-get-filename
     */
    public String getFilename() throws JGimpException {
        // TODO: figure out if this will crash if no filename exists, or return a null String, or a String of zero-length
        return m_App.callProcedure("gimp_image_get_filename", this)[0].convertToString();
    }
    
    /**
     * Sets the filename for this image.
     * @param filename The new filename for this image.
     * @throws JGimpException
     * @pdb gimp-image-set-filename
     */
    public void setFilename(String filename) throws JGimpException {
        m_App.callProcedure("gimp_image_set_filename", this, filename);
    }

    /**
     * Returns the dots-per-inch resolution of the x-axis for this image.
     * @throws JGimpException
     * @pdb gimp-image-get-resolution
     */
    public double getXResolution() throws JGimpException {
        return m_App.callProcedure("gimp_image_get_resolution", this)[0].convertToDouble();
    }
    /**
     * Returns the dots-per-inch resolution of the y-axis for this image.
     * @throws JGimpException
     * @pdb gimp-image-get-resolution
     */
    public double getYResolution() throws JGimpException {
        return m_App.callProcedure("gimp_image_get_resolution", this)[1].convertToDouble();
    }
    /**
     * Sets the dots-per-inch resolution of the x- and y-axes for this image.
     * @throws JGimpException
     * @pdb gimp-image-set-resolution
     */
    public void setResolution(double x_res, double y_res) throws JGimpException {
        m_App.callProcedure("gimp_image_set_resolution", this, new Double(x_res), new Double(y_res));
    }

    /**
     * A stub...
     * @throws JGimpException
     */
    public JGimpUnit getUnit() throws JGimpException {
        throw new JGimpException("Stub");
    }

    /**
     * A stub...
     * @throws JGimpException
     */
    public JGimpSelection getSelection() throws JGimpException {
        throw new JGimpException("Stub");
    }

    private boolean isComponentActive(int channel) throws JGimpException {
        return (m_App.callProcedure("gimp_image_get_component_active", this, new JGimpInt32(channel))[0].convertToInt() == 1);
    }
    /**
     * Returns whether the red channel is currently active for this image.
     * @throws JGimpException
     * @pdb gimp-image-get-component-active
     */
    public boolean isRedChannelActive() throws JGimpException {
        return isComponentActive(0);
    }
    /**
     * Returns whether the green channel is currently active for this image.
     * @throws JGimpException
     * @pdb gimp-image-get-component-active
     */
    public boolean isGreenChannelActive() throws JGimpException {
        return isComponentActive(1);
    }
    /**
     * Returns whether the blue channel is currently active for this image.
     * @throws JGimpException
     * @pdb gimp-image-get-component-active
     */
    public boolean isBlueChannelActive() throws JGimpException {
        return isComponentActive(2);
    }
    /**
     * Returns whether the gray channel is currently active for this image.
     * @throws JGimpException
     * @pdb gimp-image-get-component-active
     */
    public boolean isGrayChannelActive() throws JGimpException {
        return isComponentActive(3);
    }
    /**
     * Returns whether the indexed channel is currently active for this image.
     * @throws JGimpException
     * @pdb gimp-image-get-component-active
     */
    public boolean isIndexedChannelActive() throws JGimpException {
        return isComponentActive(4);
    }
    private void setComponentActive(int channel, boolean active) throws JGimpException {
        m_App.callProcedure("gimp_image_set_component_active", this, new JGimpInt32(channel), new Boolean(active));
    }
    /**
     * Sets whether the red channel is active.
     * @throws JGimpException
     * @pdb gimp-image-set-component-active
     */
    public void setRedChannelActive(boolean active) throws JGimpException {
        setComponentActive(0, active);
    }
    /**
     * Sets whether the green channel is active.
     * @throws JGimpException
     * @pdb gimp-image-set-component-active
     */
    public void setGreenChannelActive(boolean active) throws JGimpException {
        setComponentActive(1, active);
    }
    /**
     * Sets whether the blue channel is active.
     * @throws JGimpException
     * @pdb gimp-image-set-component-active
     */
    public void setBlueChannelActive(boolean active) throws JGimpException {
        setComponentActive(2, active);
    }
    /**
     * Sets whether the gray channel is active.
     * @throws JGimpException
     * @pdb gimp-image-set-component-active
     */
    public void setGrayChannelActive(boolean active) throws JGimpException {
        setComponentActive(3, active);
    }
    /**
     * Sets whether the indexed channel is active.
     * @throws JGimpException
     * @pdb gimp-image-set-component-active
     */
    public void setIndexedChannelActive(boolean active) throws JGimpException {
        setComponentActive(4, active);
    }
    private boolean isComponentVisible(int channel) throws JGimpException {
        return (m_App.callProcedure("gimp_image_get_component_visible", this, new JGimpInt32(channel))[0].convertToInt() == 1);
    }
    /**
     * Returns whether the red channel is active.
     * @throws JGimpException
     * @pdb gimp-image-get-component-visible
     */
    public boolean isRedChannelVisible() throws JGimpException {
        return this.isComponentVisible(0);
    }
    /**
     * Returns whether the green channel is active.
     * @throws JGimpException
     * @pdb gimp-image-get-component-visible
     */
    public boolean isGreenChannelVisible() throws JGimpException {
        return this.isComponentVisible(1);
    }
    /**
     * Returns whether the blue channel is active.
     * @throws JGimpException
     * @pdb gimp-image-get-component-visible
     */
    public boolean isBlueChannelVisible() throws JGimpException {
        return this.isComponentVisible(2);
    }
    /**
     * Returns whether the gray channel is active.
     * @throws JGimpException
     * @pdb gimp-image-get-component-visible
     */
    public boolean isGrayChannelVisible() throws JGimpException {
        return this.isComponentVisible(3);
    }
    /**
     * Returns whether the indexed channel is active.
     * @throws JGimpException
     * @pdb gimp-image-get-component-visible
     */
    public boolean isIndexedChannelVisible() throws JGimpException {
        return this.isComponentVisible(4);
    }
    private void setComponentVisible(int channel, boolean visible) throws JGimpException {
        m_App.callProcedure("gimp_image_set_component_visible", this, new JGimpInt32(channel), new Boolean(visible));
    }
    /**
     * Sets the visibility of the red channel.
     * @throws JGimpException
     * @pdb gimp-image-set-component-visible
     */
    public void setRedChannelVisible(boolean visible) throws JGimpException {
        setComponentVisible(0, visible);
    }
    /**
     * Sets the visibility of the green channel.
     * @throws JGimpException
     * @pdb gimp-image-set-component-visible
     */
    public void setGreenChannelVisible(boolean visible) throws JGimpException {
        setComponentVisible(1, visible);
    }
    /**
     * Sets the visibility of the blue channel.
     * @throws JGimpException
     * @pdb gimp-image-set-component-visible
     */
    public void setBlueChannelVisible(boolean visible) throws JGimpException {
        setComponentVisible(2, visible);
    }
    /**
     * Sets the visibility of the gray channel.
     * @throws JGimpException
     * @pdb gimp-image-set-component-visible
     */
    public void setGrayChannelVisible(boolean visible) throws JGimpException {
        setComponentVisible(3, visible);
    }
    /**
     * Sets the visibility of the indexed channel.
     * @throws JGimpException
     * @pdb gimp-image-set-component-visible
     */
    public void setIndexedChannelVisible(boolean visible) throws JGimpException {
        setComponentVisible(4, visible);
    }
    /**
     * Toggles whether operations are recorded for undo or not.
     * @param enabled Set to true to enable recording of actions for the
     *                undo stack, false to temporarily turn off undo for
     *                this image.
     * @throws JGimpException
     * @pdb gimp-image-undo-enable
     * @pdb gimp-image-undo-disabled
     */
    public void setUndoEnabled(boolean enabled) throws JGimpException {
        if (enabled) {
            m_App.callProcedure("gimp_image_undo_enable", this);
        } else {
            m_App.callProcedure("gimp_image_undo_disabled", this);
        }
    }
    /**
     * Returns whether undo is currently enabled.
     * @throws JGimpException
     * @pdb gimp-image-undo-is-enabled
     */
    public boolean isUndoEnabled() throws JGimpException {
        return m_App.callProcedure("gimp_image_undo_is_enabled", this)[0].convertToBoolean();
    }

    /**
     * "Freezes" the undo stack, but maintains it so it is the same after
     * undo is "thawed".
     * @param frozen Whether to freeze or unfreeze (thaw) the undo stack.
     * @throws JGimpException
     * @pdb gimp-image-undo-freeze
     * @pdb gimp-image-undo-thaw
     */
    public void setUndoFrozen(boolean frozen) throws JGimpException {
        if (frozen) {
            m_App.callProcedure("gimp_image_undo_freeze", this);
        } else {
            m_App.callProcedure("gimp_image_undo_thaw", this);
        }
    }

    /**
     * Duplicates the entire image, returns a reference to the duplicate.
     * @return A reference to the duplicated image.
     * @throws JGimpException
     * @pdb gimp-channel-ops-duplicate
     */
    public JGimpImage duplicate() throws JGimpException {
        JGimpImage theImage = (JGimpImage)m_App.callProcedure("gimp_channel_ops_duplicate", this)[0];
/* Remove me when this (potential) bug in the GIMP in fixed: In gimpdrawable.c, gimp_drawable_mask_bounds will not set x1 and y1 if gimage_mask_bounds(...) returns false, which will screw up some plug-ins. This next call somehow fixes this problem (don't have time to investigate it fully now*/
theImage.getActiveLayer().getSelectionBounds();
/* End workaround code */
        return theImage;
    }

    /**
     * Frees any "shadow" memory currently held by the image.
     * @throws JGimpException
     * @pdb gimp-image-free-shadow
     */
    public void freeShadowMemory() throws JGimpException {
        m_App.callProcedure("gimp_image_free_shadow", this);
    }
}
