/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*-
 *
 * The contents of this file are subject to the Netscape Public License
 * Version 1.0 (the "NPL"); you may not use this file except in
 * compliance with the NPL.  You may obtain a copy of the NPL at
 * http://www.mozilla.org/NPL/
 *
 * Software distributed under the NPL is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the NPL
 * for the specific language governing rights and limitations under the
 * NPL.
 *
 * The Initial Developer of this code under the NPL is Netscape
 * Communications Corporation.  Portions created by Netscape are
 * Copyright (C) 1998 Netscape Communications Corporation.  All Rights
 * Reserved.
 */

package netscape.plugin.composer;

import java.util.*;
import java.awt.image.ImageProducer;
import java.awt.image.MemoryImageSource;
import java.io.*;
import java.net.URL;
import netscape.javascript.JSObject;

/* This class is private to the composer. It manages the plugins.
 */

class PluginManager {
    public PluginManager(){
    }

    public void registerPlugin(String pluginFileName, String iniContents){
        Properties properties = new Properties();
        File pluginFile = new File(pluginFileName);
        try {
            InputStream stream = new StringBufferInputStream(iniContents);
            properties.load(stream);
            stream.close();
        } catch ( IOException e) {
            System.err.println("Caught exception while parsing .ini contents:\n" + iniContents);
            e.printStackTrace();
        }
        registerPlugins(pluginFile, properties);
        registerEncoders(pluginFile, properties);
        registerEvents(properties);
    }

    public void registerPlugins(File pluginFile, Properties properties){
        Enumeration plugins = null;
        String factoryName = null;
        try {
            factoryName = trimWhitespace(properties.getProperty("netscape.plugin.composer.Factory",
                "netscape.plugin.composer.Factory"));
            Factory factory = (Factory) Class.forName(factoryName).newInstance();
            plugins = factory.getPlugins(pluginFile, properties);
        } catch ( Throwable t ) {
            System.err.println("Caught exception while instantiating " + factoryName);
            t.printStackTrace();
        }
        registerPlugins(categories, plugins);
    }

    public static Enumeration instantiateClassList(String classNames){
        Vector result = new Vector();
        if ( classNames != null ) {
            StringTokenizer tokenizer = new StringTokenizer(classNames, ":");
            while( tokenizer.hasMoreTokens() ){
                String className = PluginManager.trimWhitespace(tokenizer.nextToken());
                try {
                    Object object = Class.forName(className).newInstance();
                    result.addElement(object);
                } catch (Throwable t) {
                    System.err.println("Caught exception while instantiating " + className);
                    t.printStackTrace();
                }
            }
        }
        return result.elements();
    }

    protected void registerEvents(Properties properties){
        String eventHandlers =
            properties.getProperty("netscape.plugin.composer.eventHandlers", "");
        // System.err.println("registerEvents " + eventHandlers);
        registerPlugins(events, eventHandlers);
    }

    protected static void registerPlugins(SortedStringTable table, String classNames) {
        registerPlugins(table, instantiateClassList(classNames));
    }

    protected static void registerPlugins(SortedStringTable table, Enumeration plugins){
        if ( plugins != null ) {
            while(plugins.hasMoreElements()){
                Plugin plugin = (Plugin) plugins.nextElement();
                if ( plugin != null ) {
                    try {
                        table.getOrCreateTable(plugin.getCategory()).put(
                            plugin.getName(), plugin);
                    } catch(Throwable t){
                        System.err.println("Trouble registering plugin:" + plugin);
                        t.printStackTrace();
                    }
                }
            }
        }
    }
    public static String trimWhitespace(String string){
        int length = string.length();
        StringBuffer out = new StringBuffer(length);
        for(int i = 0; i < length; i++ ){
            char c = string.charAt(i);
            if ( !Character.isSpace(c) ) {
                out.append(c);
            }
        }
        return out.toString();
    }
    public void registerEncoders(File pluginFile, Properties properties){
        Enumeration encoderList = null;
        String factoryName = null;
        try {
            factoryName = trimWhitespace(properties.getProperty("netscape.plugin.composer.ImageEncoderFactory",
                "netscape.plugin.composer.ImageEncoderFactory"));
            ImageEncoderFactory factory = (ImageEncoderFactory) Class.forName(factoryName).newInstance();
            encoderList = factory.getImageEncoders(pluginFile, properties);
        } catch ( Throwable t ) {
            System.err.println("Caught exception while instantiating " + factoryName);
            t.printStackTrace();
        }
        if ( encoderList != null ) {
            while(encoderList.hasMoreElements()){
                Object object = null;
                try {
                    object = encoderList.nextElement();
                    ImageEncoder encoder = (ImageEncoder) object;
                    registerEncoderInstance(encoder);
                } catch ( Throwable t ) {
                    System.err.println("Caught exception while processing "
                        + object);
                    t.printStackTrace();
                }
            }
        }
    }

    private void registerEncoderInstance(ImageEncoder encoder){
        ImageEncoder.register(encoder);
        String name = encoder.getName();
        encoders.put(name, encoder);
    }

    public int getNumberOfCategories(){
        return categories.length();
    }

    public int getNumberOfPlugins(int category){
        SortedStringTable categoryTable = (SortedStringTable) categories.get(category);
        if ( categoryTable == null ) {
            return 0;
        }
        return categoryTable.length();
    }
    public String getCategoryName(int category){
        return categories.getKey(category);
    }
    public String getPluginName(int type, int index){
        return getPlugin(type, index).getName();
    }
    public String getPluginHint(int type, int index){
        return getPlugin(type, index).getHint();
    }

    /** Start a plugin operation.
     */
    public boolean performPlugin(Composer composer, int category, int index,
        String in, String base, String workDirectory, String workDirectoryURL,
        JSObject jsobject){
        return performPlugin2(composer, getPlugin(category, index), in, base,
            workDirectory, workDirectoryURL, null, null,jsobject);
    }

    public boolean performPluginByName(Composer composer, String pluginName,
        String in, String base, String workDirectory, String workDirectoryURL,
        JSObject jsobject){
        Plugin plugin = null;
        String destinationURL = null;
        String eventName = null;
        try {
            int eventMarker = pluginName.indexOf(':');
            if ( eventMarker >= 0 ) {
                eventName = pluginName.substring(0,eventMarker);
                destinationURL = pluginName.substring(eventMarker + 1);
                if ( destinationURL.length() == 0 ) {
                    destinationURL = null;
                }
                SortedStringTable table = events;
                // System.err.println("Executing plug-ins for event " + eventName + " " + table);
                if ( table != null ) {
                    plugin = new GroupPlugin(table);
                }
            }
            else {
                plugin = (Plugin) Class.forName(pluginName).newInstance();
            }
            if ( plugin != null ) {
                return performPlugin2(composer, plugin, in, base,
                    workDirectory, workDirectoryURL, eventName, destinationURL, jsobject);
            }
        }
        catch(Throwable t){
            t.printStackTrace();
        }
        return false;
    }

    public boolean performPlugin2(Composer composer, Plugin plugin,
        String in, String base, String workDirectory, String workDirectoryURL,
        String eventName, String destinationURL, JSObject jsobject){
        String result = in;
        if ( plugin != null ){
            try {
                URL baseURL = base != null ? new URL(base) : null;
                URL workDirectoryURLURL = null;
                URL destinationURLURL = null;
                if ( workDirectoryURL != null ) {
                    String slash = "/";
                    // The workDirectoryURL needs to end in a "/" so that
                    // relative URLs work.
                    if ( ! workDirectoryURL.endsWith(slash) ){
                        workDirectoryURL = workDirectoryURL + slash;
                    }
                    // Netscape on Windows likes urls of the form
                    // file:///C|/temp/
                    // But this freaks out the URL parser in java.
                    // Try to work around this by matching the file:
                    // part.
                    String prefix = "file:";
                    if ( workDirectoryURL.indexOf(prefix) == 0 ){
                        workDirectoryURLURL = new URL("file","",
                            workDirectoryURL.substring(prefix.length()));
                    }
                    else {
                       workDirectoryURLURL = new URL(workDirectoryURL);
                    }
                    //System.err.println("Source: " + workDirectoryURL);
                    //System.err.println("Simple result: " + workDirectoryURLURL );
                    //System.err.println("URL(file,,///C|/temp) = "+ new URL("file","","///C|/temp"));
                    //System.err.println("Relative URL: " + new URL(workDirectoryURLURL, "Floop.jpg"));
                }

                if ( destinationURL != null ) {
                    // Netscape on Windows likes urls of the form
                    // file:///C|/temp/
                    // But this freaks out the URL parser in java.
                    // Try to work around this by matching the file:
                    // part.
                    String prefix = "file:";
                    if ( destinationURL.indexOf(prefix) == 0 ){
                        destinationURLURL = new URL("file","",
                            destinationURL.substring(prefix.length()));
                    }
                    else {
                       destinationURLURL = new URL(destinationURL);
                    }
                }
                ComposerDocument document = new ComposerDocument(composer, in, baseURL,
                    workDirectory, workDirectoryURLURL, eventName, destinationURLURL,
                    jsobject);
                SecurityManager.enablePrivilege("SuperUser");
                ThreadGroup threadGroup = new ThreadGroup(
                    Thread.currentThread().getThreadGroup(),
                    plugin.getName());
                Thread thread = new Thread(threadGroup, new PluginRunner(plugin, document, this),
                    plugin.getName());
                thread.start();
                pluginThreads.put(composer, threadGroup);
            } catch (IOException e) {
              System.err.println("Composer plugin runner threw this exception:");
               e.printStackTrace();
                return false;
            }
        }
        else {
            return false;
        }
        return true;
    }

    /** Stop a plugin operation
     */

    public void stopPlugin(Composer composer){
        killGroup(composer);
    }

    public int getNumberOfEncoders(){
        return encoders.length();
    }
    public String getEncoderName(int index){
        return getEncoder(index).getName();
    }
    public String getEncoderFileType(int index){
        byte[] fileType = new byte[4];
        getEncoder(index).getFileType(fileType);
        return new String(fileType, 0);
    }
    public boolean getEncoderNeedsQuantizedSource(int index){
        return getEncoder(index).needsQuantizedSource();
    }
    public String getEncoderFileExtension(int index){
        return getEncoder(index).getFileExtension();
    }
    public String getEncoderHint(int index){
        return getEncoder(index).getHint();
    }

    protected ImageEncoder getEncoder(int index) {
        return (ImageEncoder) encoders.get(index);
    }

    /** Start an encoder operation.
     */
    public boolean startEncoder(Composer composer, int index,
        int width, int height, byte[][] pixels,
        String fileName){
        Plugin encoderPlugin = new ImageEncoderPlugin(getEncoder(index),
            width, height, pixels, fileName);
        return performPlugin2(composer, encoderPlugin, null, null, null, null, null, null,null);
    }

    protected Plugin getPlugin(int category, int index){
        Plugin result = null;
        SortedStringTable categoryTable = (SortedStringTable) categories.get(category);
        if ( categoryTable != null ) {
            result = (Plugin) categoryTable.get(index);
        }
        return result;
    }

    void pluginFinished(Composer composer, int status, Object argument){
        composer.pluginFinished(status, argument);
        killGroup(composer);
    }

    void killGroup(Composer composer){
        ThreadGroup group = (ThreadGroup) pluginThreads.remove(composer);
        if ( group != null ) {
            pluginKiller.kill(group);
        }
    }
    private SortedStringTable categories = new SortedStringTable();
    private SortedStringTable encoders = new SortedStringTable();
    private SortedStringTable events = new SortedStringTable();
    private Hashtable pluginThreads = new Hashtable(); // Composer composer, Thread
    private PluginKiller pluginKiller = new PluginKiller();
}

class PluginRunner implements Runnable {
    public PluginRunner(Plugin plugin, ComposerDocument document,
        PluginManager manager){
        this.plugin = plugin;
        this.document = document;
        this.manager = manager;
    }
    public void run() {
        Composer composer = document.getComposer();
        // Take this out someday when security manager works better.
        // SecurityManager.enablePrivilege("SuperUser");
        try {
            boolean result = plugin.perform(document);
            manager.pluginFinished(composer, result ? Composer.PLUGIN_OK : Composer.PLUGIN_CANCEL, null);
        } catch( ThreadDeath t) {
            System.err.println("Composer plugin " + plugin + " was killed.");
        } catch (Throwable t){
            System.err.println("Composer plugin " + plugin + " threw this exception:");
            t.printStackTrace();
            manager.pluginFinished(composer, Composer.PLUGIN_FAIL, t.toString());
        }
    }
    private Plugin plugin;
    private ComposerDocument document;
    private PluginManager manager;
}

class ImageEncoderPlugin extends Plugin {
    public ImageEncoderPlugin(ImageEncoder encoder,
        int width, int height, byte[][] pixels,
        String fileName) {
        this.encoder = encoder;
        this.width = width;
        this.height = height;
        this.pixels = pixels;
        this.fileName = fileName;
    }
    public boolean perform(Document document) throws IOException {
        // Create the image.
        int length = width * height;
        int[] pixels2 = new int[length];
        int i = 0;
        for(int y = 0; y < height; y++) {
            int j = 0;
            byte[] line = pixels[y];
            for(int x = 0; x < width; x++ ) {
                pixels2[i++] = (0xff << 24)
                    | ((0xff & line[j]) << 16)
                    | ((0xff & line[j+1]) << 8)
                    | ((0xff & line[j+2]));
                j += 3;
            }
        }
        ImageProducer source = new MemoryImageSource(width, height, pixels2, 0, width);
        ByteArrayOutputStream temp = new ByteArrayOutputStream(width*height/2);
        boolean result = encoder.encode(source, temp);
        if ( result ) {
           // Create the output
           SecurityManager.enablePrivilege("SuperUser");
           OutputStream output = new FileOutputStream(new File(fileName));
           output.write(temp.toByteArray());
           output.close();
        }
        return result;
    }
    private ImageEncoder encoder;
    private int width;
    private int height;
    private byte[][] pixels;
    private String fileName;
}

/** Processes a sequence of plugins.
 */

class GroupPlugin extends Plugin {
    public GroupPlugin(SortedStringTable table){
        table_ = table;
    }
    public boolean perform(Document document) throws IOException {
        // Perform each of the plug-ins in order
        boolean result = true;
        // System.err.println("GroupPlugin ready to go! " + table_);
        if ( table_ != null ) {
            Enumeration categories = table_.values();
            while ( categories.hasMoreElements() && result ){
                try {
                    SortedStringTable category =
                        (SortedStringTable) categories.nextElement();
                    Enumeration plugins = category.values();
                    while ( plugins.hasMoreElements() && result ){
                        Plugin plugin = null;
                        try {
                            plugin = (Plugin) plugins.nextElement();
                            result = plugin.perform(document);
                        }
                        catch(Throwable t){
                            System.err.print("Exception while executing " + plugin);
                            t.printStackTrace();
                            result = false;
                        }
                    }

                }
                catch(Throwable t){
                    t.printStackTrace();
                    result = false;
                }
            }
        }
        return result;
    }
    SortedStringTable table_;
}

class PluginKiller implements Runnable {
    PluginKiller() {
        list = new Vector();
        // Temporary security work-around for pre-4.0PR3
        SecurityManager.enablePrivilege("SuperUser");
        Thread killer = new Thread(this, "Composer Plug-in Killer");
        killer.start();
    }

    synchronized void kill(ThreadGroup group) {
        list.addElement(group);
        notifyAll();
    }

    synchronized ThreadGroup getPluginThreadGroup() {
        while (list.isEmpty()) {
            try {
                wait();
            } catch (InterruptedException e) {
            }
        }

        int last = list.size() - 1;
        ThreadGroup threadGroup = (ThreadGroup) list.elementAt(last);
        list.removeElementAt(last);
        return threadGroup;
    }

    public void run() {
        while (true) {

            // this thread cannot die for any reason.
            // Catch any exception and keep killing
            try {
                ThreadGroup group = getPluginThreadGroup();

                synchronized (group) {
                    //System.err.println("killing threadgroup " + group);

                    // Give the thread some time to exit. If it exits
                    // sooner then we will be notified when the group runs
                    // out of threads.
                    try {
                        group.wait(300);
                    } catch (InterruptedException e) {
                    }

                    // Check and see if thread is really gone
                    if (group.activeCount() > 0) {
                        // Gun down the remaining threads.
                        try {
                            SecurityManager.enablePrivilege("SuperUser");
                            group.stop();
                            group.wait(5000);
                        } catch (InterruptedException e) {
                        }
                        finally {
                            SecurityManager.revertPrivilege();
                        }
                    }

                    // Finally, destroy the thread group
                    try {
                        SecurityManager.enablePrivilege("SuperUser");
                        group.destroy();
                    } catch (Exception e) {
                    }
                    finally {
                        SecurityManager.revertPrivilege();
                    }
                }
            } catch (Exception e) {
                System.out.println("Exception occurred while deleting Composer Plug-in thread group: ");
                e.printStackTrace();
            }
        }
    }

    public String toString() {
        return new String("Plugin thread killer! Composer plugins to be disposed of: " + list);
    }

    // the list of composer plug-in groups to finish
    private Vector list; // Of ThreadGroup

}
