/* 
 * 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.proxy;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Vector;
import java.util.jar.JarInputStream;
import java.util.zip.ZipEntry;

import org.gimp.jgimp.GimpApp;
import org.gimp.jgimp.JGimpBufferOverflowException;
import org.gimp.jgimp.JGimpData;
import org.gimp.jgimp.JGimpDrawable;
import org.gimp.jgimp.JGimpException;
import org.gimp.jgimp.JGimpImage;
import org.gimp.jgimp.JGimpInvalidDrawableException;
import org.gimp.jgimp.JGimpInvalidPixelRegionException;
import org.gimp.jgimp.JGimpPDBArgTypeConstants;
import org.gimp.jgimp.extension.JGimpExtension;
import org.gimp.jgimp.nativewrappers.JGimpInt32;
import org.gimp.jgimp.nativewrappers.JGimpPDBStatus;
import org.gimp.jgimp.nativewrappers.JGimpString;
import org.gimp.jgimp.plugin.JGimpMenuPathConstructor;
import org.gimp.jgimp.plugin.JGimpParamDescriptor;
import org.gimp.jgimp.plugin.JGimpParamDescriptorListBuilder;
import org.gimp.jgimp.plugin.JGimpPlugIn;
import org.gimp.jgimp.plugin.JGimpPlugInReturnValues;
import org.gimp.jgimp.plugin.JGimpProcedureDescriptor;


/**
 * JGimpPlugInProxy is the Java-based proxy between pure Java code 
 * and the native shared library that attaches to the GIMP.
 * This class is only instantiated by native code.
 * To communicate with the GIMP, create either a plug-in or
 * an extension, and install them in the proper locations.
 * <br>
 * To write a plug-in or an extension, see {@link org.gimp.jgimp.plugin.JGimpPlugIn}
 * or {@link org.gimp.jgimp.extension.JGimpExtension}, respectively.
 *
 * @see org.gimp.jgimp.plugin.JGimpPlugIn
 * @see org.gimp.jgimp.extension.JGimpExtension
 */
public class JGimpPlugInProxy implements JGimpProxy {

    private static JGimpPlugInProxy s_Instance = null;
    private File m_SourceJarFile = null; /* the jar file we're found in */
    private boolean m_LoadPlugIns = false;
    private boolean m_LoadExtensions = false;
    private File m_JavafilesDirectory = null;
    private HashMap m_ResidentPlugIns = new HashMap();                 /* key: plug-in name, value: reference to the actual plug-in */
    private HashMap m_NonResidentPlugIns = new HashMap();              /* key: plug-in name, value: java.lang.reflect.Constructor object for instantiating a new plug-in */
    private Vector m_ExecutingNonResidentPlugIns = new Vector();       /* non-resident plug-ins which are currently executing */
    private Vector m_Extensions = new Vector();
    private URLClassLoader m_LocalClassLoader = null;

    /**
     * Called by native code to start the plug-in proxy
     * @param jgimpJarfile The absolute path to the jgimp jarfile that we're part of
     * @param jgimpBaseDir The bas directory of the JGimp installation
     * @param loadPlugIns Whether to load plug-ins or not
     * @param loadExtensions Whether to load extensions or not
     */
    private static void _startProxy(String jgimpJarfile, String jgimpBaseDir, boolean loadPlugIns, boolean loadExtensions) {
        if (s_Instance == null) {
            /*
             * Apparently you need to set the context class loader for this initial thread since we
             * are being loaded through JNI; if we don't then some Java code won't load correctly
             * (such as JEditorPane, which uses the context class loader to load editor kits).
             * So before doing anything else, we set the context class loader. Only found this
             * out by JEditorPane code crashing, then inspecting where it crashed in Sun's code...
             * Here's the bug report on this "feature": http://developer.java.sun.com/developer/bugParade/bugs/4489399.html
             */
            Thread.currentThread().setContextClassLoader(JGimpPlugInProxy.class.getClassLoader());
            s_Instance = new JGimpPlugInProxy(jgimpJarfile, jgimpBaseDir, loadPlugIns, loadExtensions);
        }
    }
    /**
     * Called by native code to get an already-created instance.
     * Returns null if it hasn't been properly initialized.
     */
    private static JGimpPlugInProxy _getInstance() {
        return s_Instance;
    }

    protected JGimpPlugInProxy(String jgimpJarfile, String jgimpBaseDir, boolean loadPlugIns, boolean loadExtensions) {
        m_SourceJarFile = new File(jgimpJarfile);
        m_LoadPlugIns = loadPlugIns;
        m_LoadExtensions = loadExtensions;
        
        GimpApp gimpApp = new GimpApp(this);
        String javafilesDir = null;
        try {
            javafilesDir = gimpApp.getResourceFileData(JAVAFILES_DIR_KEY);
        } catch (Exception ignoredException) {}

        if ((javafilesDir == null) || (javafilesDir.length() < 1)) {
            javafilesDir = jgimpBaseDir + System.getProperty("file.separator") + "javafiles";
        }
        m_JavafilesDirectory = new File(javafilesDir);

        try {
            gimpApp.setResourceFileData(JGIMP_BASE_DIR_KEY, new File(jgimpBaseDir).getCanonicalPath());
        } catch (Exception e) {
System.err.println("JGimp internal error: Could not set JGimp base directory key." + e.getMessage());
e.printStackTrace();
        }
        if (m_LoadPlugIns) {
            reloadPlugIns();
        }
        if (m_LoadExtensions) {
            reloadExtensions();
        }
    }

    /**
     * Removes all plug-ins from the GIMP then rescans and reinstalls all Java-based
     * plug-ins
     */
    public synchronized void reloadPlugIns() {
        stopAllPlugIns();

        Iterator pluginIterator = m_NonResidentPlugIns.keySet().iterator();
        while (pluginIterator.hasNext()) {
            _unloadPlugInOrExtension(pluginIterator.next().toString());
        }
        pluginIterator = m_ResidentPlugIns.keySet().iterator();
        while (pluginIterator.hasNext()) {
            _unloadPlugInOrExtension(pluginIterator.next().toString());
        }

        m_NonResidentPlugIns.clear();
        m_ResidentPlugIns.clear();

        m_LocalClassLoader = new URLClassLoader(new URL[0], this.getClass().getClassLoader());
        loadDirectory(m_JavafilesDirectory, null, null);
        try {
            installPlugIn(new ReloadPlugIn());
        } catch (Exception e) {
System.err.println("Error installing reload plugin: " + e.getMessage());
e.printStackTrace();
        }
    }

    /**
     * Stops any running extensions and removes all extensions installed in the GIMP,
     * then rescans and reinstalls all Java-based extensions
     */
    public synchronized void reloadExtensions() {
        stopAllExtensions();

        m_Extensions.clear();

        m_LocalClassLoader = new URLClassLoader(new URL[0], this.getClass().getClassLoader());
        loadDirectory(m_JavafilesDirectory, null, null);
    }

    private native void _unloadPlugInOrExtension(String pluginName);

    private void stopAllPlugIns() {
        Iterator keys = m_ResidentPlugIns.keySet().iterator();
        while (keys.hasNext()) {
            JGimpPlugIn thisPlugIn = (JGimpPlugIn) m_ResidentPlugIns.get(keys.next());
            thisPlugIn.stop(new GimpApp(this));
        }

        synchronized (this) {
            Iterator pluginsIterator = m_ExecutingNonResidentPlugIns.iterator();
            while (pluginsIterator.hasNext()) {
                JGimpPlugIn thisPlugIn = (JGimpPlugIn) pluginsIterator.next();
                thisPlugIn.stop(new GimpApp(this));
            }
            m_ExecutingNonResidentPlugIns.clear();
        }
    }
    private void stopAllExtensions() {
        Iterator extensionIterator = m_Extensions.iterator();
        while (extensionIterator.hasNext()) {
            JGimpExtension thisExtension = (JGimpExtension) extensionIterator.next();
            thisExtension.stop(new GimpApp(this));
        }
    }

    
    /**
     * Collects a list of all files within a directory and its sub-directories,
     * then attempts to load each .jar and .class file as either an extension
     * or plug-in. Keeps track of directories it has visited to prevent
     * it from getting caught in a loop.
     */
    private void loadDirectory(File directory, Vector runningDirectoryList, Vector urlList) {

        boolean isFirstInvocation = false;

        if (directory == null) {
            return;
        }
        if (!directory.isDirectory()) {
            return;
        }
        if (runningDirectoryList == null) {
            runningDirectoryList = new Vector();
            urlList = new Vector();
            isFirstInvocation = true;
        }
        if (runningDirectoryList.contains(directory)) {
            return;
        } else {
            runningDirectoryList.add(directory);
        }

        // read the files, add to our list of URLs
        File[] files = directory.listFiles();
        for (int i = 0; i < files.length; i++) {
            File thisFile = files[i];

            if (m_SourceJarFile.equals(thisFile)) {
                continue;
            }

            if (thisFile.isDirectory()) {
                loadDirectory(thisFile, runningDirectoryList, urlList);
            } else if (thisFile.getName().toLowerCase().endsWith(".jar") || thisFile.getName().toLowerCase().endsWith(".class")) {
                try {
                    urlList.add(files[i].toURL());
                } catch (Exception e) {
                    System.err.println("Caught exception transforming file to URL: " + e.getMessage());
                    e.printStackTrace();
                }
            }
        }
        
        if (isFirstInvocation) {
            URL[] urlArray = (URL[]) urlList.toArray(new URL[0]);

            m_LocalClassLoader = new URLClassLoader(urlArray, m_LocalClassLoader);

            // now attempt to load each URL
            for (int i = 0; i < urlArray.length; i++) {
                try {
                    loadURL(urlArray[i], false);
                } catch (Exception e) {
                    System.err.println("Error loading file: " + urlArray[i].toString() + ": " + e.getMessage());
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * Loads the plugin or extension referred to by the URL
     */
    private void loadURL(URL inURL, boolean addToClassLoader)
        throws
            IOException,
            ClassNotFoundException,
            NoSuchMethodException,
            InvocationTargetException,
            IllegalAccessException,
            InstantiationException,
            JGimpProxyException {

        if (inURL == null) {
            return;
        }
        if (addToClassLoader) {
            m_LocalClassLoader = new URLClassLoader(new URL[] { inURL }, m_LocalClassLoader);
        }

        if (inURL.getFile().toLowerCase().endsWith(".jar")) {
            JarInputStream jis = new JarInputStream(inURL.openStream());

            for (ZipEntry thisEntry = jis.getNextEntry(); thisEntry != null; thisEntry = jis.getNextEntry()) {
                if (thisEntry.isDirectory() || (thisEntry.getName().indexOf(".class") < 0)) {
                    continue;
                }
                String className = thisEntry.getName().replace('/', '.');
                className = className.substring(0, className.indexOf(".class"));
                loadClass(className);
            }
        } else if (inURL.getFile().toLowerCase().endsWith(".class")) {
            String className = new File(inURL.getFile()).getName(); // hack to quickly get the file name
            className = className.substring(0, className.indexOf(".class"));
            loadClass(className);
        } else if (inURL.getProtocol().toLowerCase().startsWith("file")) {
            // Check if the URL is a directory, then try to load it that way
            File thisDir = new File(inURL.getPath());
            if (!thisDir.isDirectory()) {
                // not a directory. just exit
                return;
            }
            loadDirectory(thisDir, null, null);
        } else {
            ; // don't know what type of file it is, so ignore it
        }
    }
    
    /**
     * Loads the plugin or extension referred to by the class
     */
    private void loadClass(String className) {
		try {
			Class thisClass = m_LocalClassLoader.loadClass(className);
			if (m_LoadExtensions && JGimpExtension.class.isAssignableFrom(thisClass)) {
				Constructor theConstructor = thisClass.getConstructor(new Class[0]);
				JGimpExtension theExtension = (JGimpExtension) theConstructor.newInstance(new Object[0]);
				loadExtension(theExtension);
			}
			if (m_LoadPlugIns && JGimpPlugIn.class.isAssignableFrom(thisClass)) {
				Constructor theConstructor = thisClass.getConstructor(new Class[0]);
				JGimpPlugIn thePlugin = (JGimpPlugIn) theConstructor.newInstance(new Object[0]);
				installPlugIn(thePlugin);
			}
		} catch (Exception e) {
System.err.println("Caught exception " + e + " attempting to load class " + className + ": " + e.getMessage());
e.printStackTrace();
		} catch (NoClassDefFoundError e) {
System.err.println("Caught error " + e + " attempting to load class " + className + ": " + e.getMessage());
e.printStackTrace();
		}
    }

    /**
     * @see org.gimp.jgimp.proxy.JGimpProxy#loadExtension(URL)
     */
    public synchronized void loadExtension(java.net.URL inExtensionURL)
        throws
            JGimpProxyException,
            java.io.IOException,
            java.lang.ClassNotFoundException,
            java.lang.NoSuchMethodException,
            java.lang.InstantiationException,
            java.lang.IllegalAccessException,
            java.lang.reflect.InvocationTargetException {
        loadURL(inExtensionURL, true);
    }

    /**
     * @see org.gimp.jgimp.proxy.JGimpProxy#loadExtension(JGimpExtension)
     */
    public synchronized void loadExtension(JGimpExtension inExtension) throws JGimpProxyException {
        if (inExtension == null) {
            return;
        }
        if (!m_LoadExtensions) {
            throw new JGimpProxyException("Can't load extensions with this proxy");
        }
        m_Extensions.add(inExtension);
        inExtension.run(new GimpApp(this));
    }


    /**
     * @see org.gimp.jgimp.proxy.JGimpProxy#installPlugIn(URL)
     */
    public synchronized void installPlugIn(java.net.URL inPlugInURL)
        throws
            JGimpProxyException,
            java.io.IOException,
            java.lang.ClassNotFoundException,
            java.lang.NoSuchMethodException,
            java.lang.InstantiationException,
            java.lang.IllegalAccessException,
            java.lang.reflect.InvocationTargetException {
                
        loadURL(inPlugInURL, true);
    }

    /**
     * @see org.gimp.jgimp.proxy.JGimpProxy#installPlugIn(JGimpPlugIn)
     */
    public synchronized void installPlugIn(JGimpPlugIn inPlugIn) throws JGimpProxyException {
        /*
         * Algorithm:
         * - for plug-ins that don't remain resident, store their Class object
         *   in a hash table.
         * - for plug-ins that remain resident, store a reference to an instantiated
         *   object in a hash table.
         *
         *   Then, when calling a plug-in, search the resident hash table then
         *   the non-resident hash table
         */

        if (!m_LoadPlugIns) {
            throw new JGimpProxyException("Can't load plug-ins through this proxy");
        }
        if (inPlugIn == null) {
            return;
        }

        JGimpProcedureDescriptor[] plugInInfo = inPlugIn.getPlugInInfo(new GimpApp(this));
        if (plugInInfo == null) {
            return;
        }

        for (int i = 0; i < plugInInfo.length; i++) {
            JGimpProcedureDescriptor thisInfo = plugInInfo[i];
            String name = thisInfo.getName();

            if (inPlugIn.remainResident(new GimpApp(this))) {
                m_ResidentPlugIns.put(name, inPlugIn);
            } else {
                try {
                    m_NonResidentPlugIns.put(name, inPlugIn.getClass().getConstructor(new Class[0]));
                } catch (Exception e) {
                    throw new JGimpProxyException("Could not get constructor for plug-in named " + name + ": " + e.getMessage());
                }
            }
            int returnValue =
                _installPlugInNatively(
                    thisInfo.getName(),
                    thisInfo.getBlurb(),
                    thisInfo.getHelp(),
                    thisInfo.getAuthor(),
                    thisInfo.getCopyright(),
                    thisInfo.getDate(),
                    thisInfo.getMenuPath(),
                    thisInfo.getImageTypes(),
                    thisInfo.getParameterDescriptions(),
                    thisInfo.getReturnValueDescriptions());
            if (returnValue != 0) {
                throw new JGimpProxyException("Unable to install plug-in named " + thisInfo.getName());
            }
        }
    }

    private synchronized static native int _installPlugInNatively(
        String inName,
        String inBlurb,
        String inHelp,
        String inAuthor,
        String inCopyright,
        String inDate,
        String inMenuPath,
        String inImageTypes,
        JGimpParamDescriptor[] inParamDescriptions,
        JGimpParamDescriptor[] inReturnValueDescriptions);
    
    /**
     * Called from libjgimp (the C library) when a Java plug-in is called
     */
    private JGimpPlugInReturnValues _executeJavaPlugIn(String inName, JGimpData[] params) {
        JGimpPlugIn thePlugIn = null;
        if (m_ResidentPlugIns.containsKey(inName)) {
            thePlugIn = (JGimpPlugIn) m_ResidentPlugIns.get(inName);
            if (thePlugIn != null) {
                return thePlugIn.run(new GimpApp(this), inName, params);
            }
        } else if (m_NonResidentPlugIns.containsKey(inName)) {
            Constructor theConstructor = (Constructor) m_NonResidentPlugIns.get(inName);
            if (theConstructor != null) {
                try {
                    thePlugIn = (JGimpPlugIn) theConstructor.newInstance(new Object[0]);
                } catch (Exception e) {
                    System.err.println("Error: couldn't instantiate non-resident plug-in " + inName + ": " + e.getMessage());
                    e.printStackTrace();
                    return JGimpPlugInReturnValues.returnExecutionError();
                }
                if (thePlugIn != null) {
                    synchronized (this) {
                        m_ExecutingNonResidentPlugIns.add(thePlugIn);
                    }
                    JGimpPlugInReturnValues returnVals = thePlugIn.run(new GimpApp(this), inName, params);
                    synchronized (this) {
                        m_ExecutingNonResidentPlugIns.remove(thePlugIn);
                    }
                    return returnVals;
                }
            }
        }
        return JGimpPlugInReturnValues.returnExecutionError();
    }

    /*
     * Called by the GIMP when the application is quitting
     */
    private void shutdown() {
        stopAllPlugIns();
        stopAllExtensions();
    }
    /**
     * @see org.gimp.jgimp.proxy.JGimpProxy#runPDBProcedure(String, JGimpData[])
     */
    public JGimpData[] runPDBProcedure(String inProcedureName, JGimpData[] args) throws JGimpProxyException {
		if (m_ResidentPlugIns.containsKey(inProcedureName) || m_NonResidentPlugIns.containsKey(inProcedureName)) {
			// handle ourselves without going to the pdb...
			JGimpPlugInReturnValues returnVals = _executeJavaPlugIn(inProcedureName, args);
			if (!returnVals.getReturnStatus().equals(JGimpPDBStatus.getSuccessStatus())) {
				throw new JGimpProxyException("Non-success status returned: " + returnVals.getReturnStatus());
			}
			JGimpData[] returnArray = new JGimpData[returnVals.getReturnData().length + 1];
			returnArray[0] = JGimpPDBStatus.getSuccessStatus();
			System.arraycopy(returnVals.getReturnData(), 0, returnArray, 1, returnArray.length-1);
			return returnArray;
		}
		else {
			String procName = inProcedureName.replace('-', '_'); // make all -'s _'s for the plug-in call
			return _runPDBProcedureImpl(procName, args);
		}
    }
    public synchronized static native JGimpData[] _runPDBProcedureImpl(String inProcedureName, JGimpData[] args);

    /**
     * @see org.gimp.jgimp.proxy.JGimpProxy#readPixelRegionInNativeByteFormat(JGimpDrawable, int, int, int, int, byte[], int)
     */
    public void readPixelRegionInNativeByteFormat(
        JGimpDrawable inDrawable,
        int x,
        int y,
        int width,
        int height,
        byte[] buffer,
        int offset)
        throws JGimpProxyException, JGimpInvalidPixelRegionException, JGimpInvalidDrawableException, JGimpBufferOverflowException {
        if (buffer == null) {
            throw new NullPointerException();
        }
        long result = _readPixelRegionInNativeByteFormat(inDrawable.getID(), x, y, width, height, buffer, offset);
        if (result < 0) {
            throw new JGimpProxyException("Caught exception reading bytes in native call");
        }
    }

    /**
     * @see org.gimp.jgimp.proxy.JGimpProxy#readPixelRegionInNativeIntFormat(JGimpDrawable, int, int, int, int, int[], int)
     */
    public void readPixelRegionInNativeIntFormat(
        JGimpDrawable inDrawable,
        int x,
        int y,
        int width,
        int height,
        int[] buffer,
        int offset)
        throws JGimpProxyException, JGimpInvalidPixelRegionException, JGimpInvalidDrawableException, JGimpBufferOverflowException {
        if (buffer == null) {
            throw new NullPointerException();
        }
        long result = _readPixelRegionInNativeIntFormat(inDrawable.getID(), x, y, width, height, buffer, offset);
        if (result < 0) {
            throw new JGimpProxyException("Caught exception reading ints in native call");
        }
    }

    /**
     * @see org.gimp.jgimp.proxy.JGimpProxy#readPixelRegionInNativeByteFormat(JGimpDrawable, int, int, int, int)
     */
    public byte[] readPixelRegionInNativeByteFormat(JGimpDrawable inDrawable, 
                                                        int x, 
                                                        int y, 
                                                        int width, 
                                                        int height)
        throws JGimpProxyException, JGimpInvalidPixelRegionException, JGimpInvalidDrawableException, JGimpBufferOverflowException {
            
        byte[] buffer = null;
        try {
            buffer = new byte[inDrawable.getBytesPerPixel() * width * height];
        } catch (JGimpException e) {
            throw new JGimpProxyException(e.getMessage());
        }
        readPixelRegionInNativeByteFormat(inDrawable, x, y, width, height, buffer, 0);
        return buffer;
    }
    /**
     * @see org.gimp.jgimp.proxy.JGimpProxy#readPixelRegionInNativeIntFormat(JGimpDrawable, int, int, int, int)
     */
    public int[] readPixelRegionInNativeIntFormat(JGimpDrawable inDrawable, int x, int y, int width, int height)
        throws JGimpProxyException, JGimpInvalidPixelRegionException, JGimpInvalidDrawableException, JGimpBufferOverflowException {
        int bytesPerPixel = -1;
        try {
            bytesPerPixel = inDrawable.getBytesPerPixel();
        } catch (JGimpException e) {
            throw new JGimpProxyException(e.getMessage());
        }
        int numBytes = width * height * bytesPerPixel;
        int arraySize = numBytes / 4;
        if ((numBytes % 4) != 0) {
            arraySize++;
        }
        int[] buffer = new int[arraySize];
        readPixelRegionInNativeIntFormat(inDrawable, x, y, width, height, buffer, 0);
        return buffer;
    }

    /**
     * @see org.gimp.jgimp.proxy.JGimpProxy#readPixelRegionInJavaIntFormat(JGimpDrawable, int, int, int, int, int[], int)
     */
    public void readPixelRegionInJavaIntFormat(
        JGimpDrawable inDrawable,
        int x,
        int y,
        int width,
        int height,
        int[] buffer,
        int offset)
        throws JGimpProxyException, JGimpInvalidPixelRegionException, JGimpInvalidDrawableException, JGimpBufferOverflowException {
        if (buffer == null) {
            throw new NullPointerException();
        }
        long result = _readPixelRegionInJavaIntFormat(inDrawable.getID(), x, y, width, height, buffer, offset);
        if (result < 0) {
            throw new JGimpProxyException("Caught exception reading ints in native call");
        }
    }

    /**
     * @see org.gimp.jgimp.proxy.JGimpProxy#readPixelRegionInJavaIntFormat(JGimpDrawable, int, int, int, int)
     */
    public int[] readPixelRegionInJavaIntFormat(JGimpDrawable inDrawable, int x, int y, int width, int height)
        throws JGimpProxyException, JGimpInvalidPixelRegionException, JGimpInvalidDrawableException, JGimpBufferOverflowException {
        // Allocate the array and the pixel data object
        int bytesPerPixel = -1;
        try {
            bytesPerPixel = inDrawable.getBytesPerPixel();
        } catch (JGimpException e) {
            throw new JGimpProxyException(e.getMessage());
        }
        int numIntsPerPixel = bytesPerPixel / 4;
        if ((bytesPerPixel % 4) != 0) {
            numIntsPerPixel++;
        }
        int[] buffer = new int[numIntsPerPixel * width * height];
        readPixelRegionInJavaIntFormat(inDrawable, x, y, width, height, buffer, 0);
        return buffer;
    }

    /**
     * @see org.gimp.jgimp.proxy.JGimpProxy#writePixelRegionInNativeByteFormat(JGimpDrawable, int, int, int, int, byte[], int)
     */
    public void writePixelRegionInNativeByteFormat(
        JGimpDrawable inDrawable,
        int x,
        int y,
        int width,
        int height,
        byte[] buffer,
        int offset)
        throws JGimpProxyException, JGimpInvalidPixelRegionException, JGimpInvalidDrawableException {
        if (buffer == null) {
            throw new NullPointerException();
        }
        long result = _writePixelRegionInNativeByteFormat(inDrawable.getID(), x, y, width, height, buffer, offset);
        if (result < 0) {
            throw new JGimpProxyException("Failed to write pixel region using byte array");
        }
    }

    /**
     * @see org.gimp.jgimp.proxy.JGimpProxy#writePixelRegionInNativeIntFormat(JGimpDrawable, int, int, int, int, int[], int)
     */
    public void writePixelRegionInNativeIntFormat(
        JGimpDrawable inDrawable,
        int x,
        int y,
        int width,
        int height,
        int[] buffer,
        int offset)
        throws JGimpProxyException, JGimpInvalidPixelRegionException, JGimpInvalidDrawableException {
        if (buffer == null) {
            throw new NullPointerException();
        }
        long result = _writePixelRegionInNativeIntFormat(inDrawable.getID(), x, y, width, height, buffer, offset);
        if (result < 0) {
            throw new JGimpProxyException("Failed to write pixel region using byte array");
        }
    }

    /**
     * @see org.gimp.jgimp.proxy.JGimpProxy#writePixelRegionInJavaIntFormat(JGimpDrawable, int, int, int, int, int[], int)
     */
    public void writePixelRegionInJavaIntFormat(
        JGimpDrawable inDrawable,
        int x,
        int y,
        int width,
        int height,
        int[] buffer,
        int offset)
        throws JGimpProxyException, JGimpInvalidPixelRegionException, JGimpInvalidDrawableException {
        if (buffer == null) {
            throw new NullPointerException();
        }
        long result = _writePixelRegionInJavaIntFormat(inDrawable.getID(), x, y, width, height, buffer, offset);
        if (result < 0) {
            throw new JGimpProxyException("Failed to write pixel region using byte array");
        }
    }

    /**
     * Returns less than 0 on failure, otherwise, number of bytes read.
     */
    private synchronized static native long _readPixelRegionInNativeByteFormat(
        int inDrawableID,
        int x,
        int y,
        int width,
        int height,
        byte[] buffer,
        int offset);
    /**
     * Returns less than 0 on failure, otherwise, number of bytes read.
     */
    private synchronized static native long _readPixelRegionInNativeIntFormat(int inDrawableID, int x, int y, int width, int height, int[] buffer, int offset);
    /**
     * Returns less than 0 on failure, otherwise, number of bytes read.
     */
    private synchronized static native long _readPixelRegionInJavaIntFormat(int inDrawableID, int x, int y, int width, int height, int[] buffer, int offset);

    /**
     * Returns less than 0 on failure, otherwise, number of bytes written.
     */
    private synchronized static native long _writePixelRegionInNativeByteFormat(
        int inDrawableID,
        int x,
        int y,
        int width,
        int height,
        byte[] buffer,
        int offset);
    /**
     * Returns less than 0 on failure, otherwise, number of bytes written.
     */
    private synchronized static native long _writePixelRegionInNativeIntFormat(int inDrawableID, int x, int y, int width, int height, int[] buffer, int offset);
    /**
     * Returns less than 0 on failure, otherwise, number of bytes written.
     */
    private synchronized static native long _writePixelRegionInJavaIntFormat(int inDrawableID, int x, int y, int width, int height, int[] buffer, int offset);

    /**
     * @see org.gimp.jgimp.proxy.JGimpProxy#localLoadImage(URL)
     */
    public JGimpImage localLoadImage(java.net.URL image) throws JGimpProxyException, IOException {
        // Form our PDB call args for opening the local image
        JGimpData[] args = new JGimpData[3];

        args[0] = new JGimpInt32(1); // Don't run interactively

        // If the file is local, then just pass in a pointer to it from here
        // Otherwise, download it to a temporary file and load it up into GIMP that way
        File tempFile = null;
        if (image.getProtocol().toLowerCase().startsWith("file")) {
            // Throw it into a File because the URL's getFile() method
            // returns a leading forward slash in the file name, which is
            // problematic for Windows
            File thisFile = new File(image.getFile());
            args[1] = new JGimpString(thisFile.getCanonicalPath());
            args[2] = args[1];
        } else {
            // Need to download a copy to load into the GIMP
            String fileSuffix = null;
            String imageFileName = image.getFile();
            if (imageFileName.lastIndexOf(".") == -1) {
                throw new JGimpProxyException("Unable to load local image " + imageFileName + " because it does not contain a valid suffix");
            }
            fileSuffix = imageFileName.substring(imageFileName.lastIndexOf("."), imageFileName.length());

            tempFile = File.createTempFile("jgimp_plugin_proxy_temp_file", fileSuffix);
            // Mark our temp image for deletion, just in case it can't be deleted below
            tempFile.deleteOnExit();

            // Now copy the data to the file
            byte[] buffer = new byte[2048];
            InputStream is = image.openStream();
            OutputStream os = new FileOutputStream(tempFile);
            int amountRead = 0;

            do {
                amountRead = is.read(buffer);
                if (amountRead > 0) {
                    os.write(buffer, 0, amountRead);
                }
            } while (amountRead != -1);

            is.close();
            os.close();

            args[1] = new JGimpString(tempFile.getCanonicalPath());
            args[2] = args[1];
        }

        // Call the procedure, check the return vals
        JGimpData[] returnVals = runPDBProcedure("gimp_file_load", args);
        if (returnVals == null) {
            throw new JGimpProxyException("Unable to execute gimp-file-load in localLoadImage");
        }
        if (returnVals.length < 2) {
            throw new JGimpProxyException("Unable to get output image in localLoadImage");
        }
        if (returnVals[0] instanceof JGimpPDBStatus) {
            JGimpPDBStatus theStatus = (JGimpPDBStatus) returnVals[0];
            if (theStatus.getStatus() != JGimpPDBStatus.Type.GIMP_PDB_SUCCESS) {
                throw new JGimpProxyException("Procedure gimp-file-load failed to load image in localLoadImage. Status returned: " + theStatus.getStatus());
            }
        } else {
            throw new JGimpProxyException("Procedure gimp-file-load failed to load image in localLoadImage. No status returned.");
        }

        // Get the image ref
        JGimpImage returnImage = (JGimpImage) returnVals[1];

        // Delete our temp image
        if (tempFile != null) {
            try {
                tempFile.delete();
            } catch (Exception ignored) {
            }
        }

        return returnImage;
    }

    /**
     * @see org.gimp.jgimp.proxy.JGimpProxy#localSaveImage(JGimpImage, JGimpDrawable, java.io.File, String, JGimpData[])
     */
    public void localSaveImage(
        JGimpImage image,
        JGimpDrawable drawable,
        java.io.File targetFile,
        String imageType,
        JGimpData[] saveArguments)
        throws JGimpProxyException, IOException {
        // Form our PDB call args for saving the image locally

        JGimpData[] args = null;
        if (saveArguments != null) {
            args = new JGimpData[saveArguments.length + 5];
        } else {
            args = new JGimpData[5];
        }
        args[0] = new JGimpInt32(1); // don't run interactively
        args[1] = image; // the image
        args[2] = drawable; // the drawable
        args[3] = new JGimpString(targetFile.getCanonicalPath()); // the path to save to
        args[4] = new JGimpString(targetFile.getCanonicalPath()); // the path to save to

        if (saveArguments != null) {
            for (int i = 0; i < saveArguments.length; i++) {
                args[5 + i] = saveArguments[i];
            }
        }

        // Call the procedure, check the return vals
        String pdbName = null;

        if (imageType.toLowerCase().equals("xcf")) {
            pdbName = "gimp_xcf_save";
        } else {
            pdbName = "file_" + imageType.toLowerCase() + "_save";
        }
        JGimpData[] returnVals = runPDBProcedure(pdbName, args);
        if (returnVals == null) {
            throw new JGimpProxyException("Unable to execute " + pdbName + " in localSaveFile");
        }
        if (returnVals[0] instanceof JGimpPDBStatus) {
            JGimpPDBStatus theStatus = (JGimpPDBStatus) returnVals[0];
            if (theStatus.getStatus() != JGimpPDBStatus.Type.GIMP_PDB_SUCCESS) {
                throw new JGimpProxyException("Procedure " + pdbName + " failed to save image in localSaveFile. Status returned: " + theStatus.getStatus());
            }
        } else {
            throw new JGimpProxyException("Procedure " + pdbName + " failed to save image in localSaveFile. No status returned.");
        }

        // no status returned, instead, exceptions are thrown on failure
    }
    /**
     * @see org.gimp.jgimp.proxy.JGimpProxy#canInstallPlugIns()
     */
    public boolean canInstallPlugIns() {
        return m_LoadPlugIns;
    }

    /**
     * @see org.gimp.jgimp.proxy.JGimpProxy#canLoadExtensions()
     */
    public boolean canLoadExtensions() {
        return m_LoadExtensions;
    }
}

class ReloadPlugIn implements JGimpPlugIn {
    private final static String RELOAD_PLUGINS_PLUGIN    = "plugin_jgimp_reload_plugins";
    private final static String RELOAD_EXTENSIONS_PLUGIN = "plugin_jgimp_reload_extensions";

    public ReloadPlugIn() {
        ; // no-op
    }

	public boolean remainResident(GimpApp app) {
        return true;
    }

    public JGimpProcedureDescriptor[] getPlugInInfo(GimpApp app) {

		JGimpProcedureDescriptor[] plugInInfoArray = new JGimpProcedureDescriptor[1];
		JGimpParamDescriptorListBuilder paramBuilder = new JGimpParamDescriptorListBuilder();

		paramBuilder.append( JGimpPDBArgTypeConstants.PDB_INT32, "run_mode", "Interactive (0), non-interactive (1)" ); // Can run interactively, or non-interactively

		plugInInfoArray[0] = new JGimpProcedureDescriptor(
				/* Name of the plug-in as installed in the PDB */ RELOAD_PLUGINS_PLUGIN,
				/* Description of the plug-in                  */ "Reloads the JGimp and Jython plug-ins",
				/* The "help" message for this plug-in         */ "Reloads the JGimp and Jython plug-ins",
				/* Author                                      */ "Michael Terry",
				/* Copyright information                       */ "Copyright 2003, Michael Terry",
				/* Date of writing                             */ "May 2003",
				/* Menu path where it is installed             */ JGimpMenuPathConstructor.createToolboxMenuPath("Xtns/JGimp/Reload JGimp-Jython Plug-Ins"),
				/* Types of images this plug-in can work on    */ null,
				/* The parameters we accept                    */ paramBuilder.toArray(),
				/* Return value descriptions (there are none)  */ null);

//		plugInInfoArray[1] = new JGimpProcedureDescriptor(
//				/* Name of the plug-in as installed in the PDB */ RELOAD_EXTENSIONS_PLUGIN,
//				/* Description of the plug-in                  */ "Reloads the JGimp and Jython extensions",
//				/* The "help" message for this plug-in         */ "Reloads the JGimp and Jython extensions",
//				/* Author                                      */ "Michael Terry",
//				/* Copyright information                       */ "Copyright 2003, Michael Terry",
//				/* Date of writing                             */ "May 2003",
//				/* Menu path where it is installed             */ JGimpMenuPathConstructor.createToolboxMenuPath("Xtns/JGimp/Reload JGimp-Jython Extensions"),
//				/* Types of images this plug-in can work on    */ null,
//				/* The parameters we accept                    */ paramBuilder.toArray(),
//				/* Return value descriptions (there are none)  */ null);
        return plugInInfoArray;
    }

    public JGimpPlugInReturnValues run(GimpApp app, String inName, JGimpData[] inParams) {
        if (inName.equals(RELOAD_PLUGINS_PLUGIN)) {
            app.getProxy().reloadPlugIns();
        } else if (inName.equals(RELOAD_EXTENSIONS_PLUGIN)) {
            app.getProxy().reloadExtensions();
        }
        return JGimpPlugInReturnValues.returnSuccess();
    }

    public void stop(GimpApp app) {
        ; // no-op
    }
}
