// Application.java
// By Ned Etcode
// Copyright 1995, 1996 Netscape Communications Corp.  All rights reserved.

package netscape.application;

import netscape.util.*;

import java.lang.Thread;
import java.net.URL;
import java.applet.AppletContext;

/** Object subclass that represents the overall IFC-based Java
  * application. An Application instance maintains application-wide state and
  * manages access to resources or objects that provide resources
  * or services, such as RootViews.
  */

public class Application implements Runnable, EventProcessor {
    static Hashtable    groupToApplication = new Hashtable();
    static final String _releaseName = "IFC 1.0";

    java.applet.Applet    applet;
    AppletResources     _appResources;
    EventLoop           eventLoop = new EventLoop();
    Vector              _languageVector;
    Vector              rootViews = new Vector();
    RootView            mainRootView;
    boolean             didCreateApplet;
    Vector              _modalVector = new Vector();
    Vector              observers = new Vector();

    // This is application wide state kept on behalf of Bitmap, Sound, and
    // Font. Because we keep a hashtable of images, they will never go away
    // for the lifetime of an app. This code would get much nicer if each
    // application got its own static variables and would get better still if
    // the language had weak references.

    java.awt.MediaTracker tracker = null;
    int                   bitmapCount = 0;
    Hashtable             bitmapByName = new Hashtable();
    Hashtable             soundByName = new Hashtable();
    Hashtable             fontByName = new Hashtable();

    // This is information kept for graphics debugging
    DebugGraphicsInfo   debugGraphicsInfo;

    // This is the shared TimerQueue for the Application.
    TimerQueue timerQueue;

    // This lock is used to make the awt thread waiting for
    // the application cleanup
    Object cleanupLock;

    boolean isPaused;

    /** Constructs an Application. The Application will not begin processing
      * Events until it receives a <b>run()</b> message.  You will almost never
      * create an Application instance - the IFC machinery will create
      * one for you.
      * @see #run
      */
    public Application() {
        super();
        FoundationApplet ifcApplet;

        groupToApplication.put(Thread.currentThread().getThreadGroup(), this);
        ifcApplet = FoundationApplet.applet();
        if (ifcApplet == null) {
            ifcApplet = createApplet();
            ifcApplet.setApplication(this);
            didCreateApplet = true;
        } else {
            isPaused = true;
            ifcApplet.setupCanvas(this);
        }
        applet = ifcApplet;
        _appResources = new AppletResources(this, codeBase());
        timerQueue = new TimerQueue();
    }

    public static String releaseName()  {
        return _releaseName;
    }

    /** Constructs an Application for an existing Applet.  You should only
      * use this constructor when including IFC Views within an AWT-based
      * component hierarchy.  You must call <b>run()</b> on the returned
      * Application for your Views to draw and receive events.
      * @see FoundationPanel
      * @see #run
      */
    public Application(java.applet.Applet applet) {
        groupToApplication.put(Thread.currentThread().getThreadGroup(), this);
        this.applet = applet;
        _appResources = new AppletResources(this, codeBase());
        timerQueue = new TimerQueue();
        appletStarted();
    }

    /** Returns the application instance. */
    public static Application application() {
        ThreadGroup group = Thread.currentThread().getThreadGroup();
        Application app = (Application)groupToApplication.get(group);

        if(app == null) {   /// ddk - ADDED FOR JAVASCRIPT SUPPORT
            app = FoundationApplet.currentApplication();
        }

        /*
        if (app == null) {
            System.err.println("Not in Application thread group!");
            System.err.println("Thread: " + Thread.currentThread());
            Thread.currentThread().getThreadGroup().list();
            Thread.currentThread().getThreadGroup().stop();
            Thread.dumpStack();
        }
        */
        return app;
    }

    /** Initializes the Application.  Override to perform any initialization.
      */
    public void init() {
    }

    /** Called when the run loop has exited.
     */
    public void cleanup() {
        Enumeration groups = groupToApplication.keys();
        int iView;

        while (groups.hasMoreElements()) {
            ThreadGroup group = (ThreadGroup)groups.nextElement();
            Application app = (Application)groupToApplication.get(group);

            if (app == this) {
                groupToApplication.remove(group);
                break;
            }
        }

        /* This will not destroy the applet. This will remove the application
         * and thread groups
         */
        if (applet instanceof FoundationApplet) {
            ((FoundationApplet)applet).cleanup();
        }

        applicationDidStop();

        if (didCreateApplet) {
            ((FoundationApplet)applet).destroyFromIFC();
        }
    }

    /** Destroys the Application, including all ExternalWindows.
      * Stops the EventLoop and causes <b>run()</b> to return.
      */
    public void stopRunning() {
        eventLoop.stopRunning();
    }

    /** This method is called in the AWT thread. It will
     *  stop the event loop and wait until run is done
     */
    void stopRunningForAWT() {
        synchronized( cleanupLock ) {
            eventLoop.stopRunning();
            while( true ) {
                try {
                    cleanupLock.wait();
                    return;
                } catch ( java.lang.InterruptedException e ) {
                    /** ALERT what should we do? Can this really happen? */
                }
            }
        }
    }

    /** Starts the Application's EventLoop.
      */
    public void run() {
        // Having init() called here is kind of strange.  ALERT!
        cleanupLock = new Object();
        applicationDidStart();
        init();
        eventLoop.run();
        cleanup();
        /* Unblock the awt thread if necessary */
        synchronized( cleanupLock ) {
            cleanupLock.notify();
        }
    }

/* attributes */

    /** Returns the Application's EventLoop.
      * @see EventLoop
      */
    public EventLoop eventLoop() {
        return eventLoop;
    }

    /**
     * Returns a Vector containing the user's language preferences,
     * specified by Strings, in order of preference.  For example, for a user
     * preferring French over English, the Vector would contain the
     * Strings "French" and "English," in that order.<br><br>
     * <i><b>Note:</b> This method will return an empty Vector until such time
     * as Applets can save user preferences to their local machine.  If you
     * maintain user preferences at your site, you can request this
     * Vector and fill it with the appropriate strings.</I>
     */
    Vector languagePreferences() {
        if (_languageVector == null) {
            _languageVector = new Vector();
        }

        return _languageVector;
    }

/* actions */

    /*
     * Convenience method for forcing all pending drawing requests to appear
     * onscreen immediately.  Equivalent to
     * <pre>
     *     getToolkit().sync();
     * </pre>
     * <I><b>Note:</b> Due to bugs in the getToolkit().sync() native
     * implementation, this method currently does nothing.</i>
     */
    void syncGraphics() {
  /* awt */
// hangs the system sometimes with "unexpected asynch reply" messages
//      getToolkit().sync();
    }



/* interfaces */

    /*
     * Returns a java.io.InputStream containing the Velocity interface file
     * named
     * <b>interfaceName</b>.  Uses the Application's AppletResources
     * instance to
     * retrieve the correct localized interface file based on the user's
     * language preferences.
     * @see AppletResources
     */
    java.io.InputStream streamForInterface(String interfaceName) {
        return _appResources.streamForInterface(interfaceName);
    }

/* other resources */

    /** Returns a java.io.InputStream containing data for the file named
      * <b>resourceName</b> of type <b>type</b> (i.e. resides within a
      * directory named "<b>type</b>" at the same directory level as the
      * application's index.html file).
      */
    java.io.InputStream streamForResourceOfType(String resourceName,
                                                       String type) {
        return _appResources.streamForResourceOfType(resourceName, type);
    }

    /** Returns <b>true</b> if the Application was started as an Applet. */
    public boolean isApplet() {
        return !didCreateApplet;
    }

    java.io.InputStream streamForRelativePath(String relativePath) {
  /* awt */
        URL                     documentURL;
        java.io.InputStream     inputStream;

        try {
            documentURL = new URL(codeBase(), relativePath);
        } catch (Exception e) {
            System.err.println("Application.streamForRelativePath() - " + e);
            documentURL = null;
        }

        if (documentURL == null) {
            return null;
        }

        try {
            inputStream = documentURL.openStream();
        } catch (Exception e) {
            System.err.println(
                "Application.streamForURL() - Trouble retrieving URL " +
                documentURL + " : " + e);
            inputStream = null;
        }

        return inputStream;
    }

    AppletContext getAppletContext() {
        return applet == null ? null : applet.getAppletContext();
    }

    /** Returns the Application's codebase, the URL where the Applet's class
      * file originated.
      */
    public URL codeBase() {
        return applet.getCodeBase();
    }

    /** Returns the values for the parameter <b>name</b>.
      */
    public String parameterNamed(String name) {
        return applet == null ? null : applet.getParameter(name);
    }

    /** Returns the Application's "main" RootView.  In the case of an Applet,
      * this method returns the RootView associated with the Applet.  If
      * not an Applet and never set by the Application, this method returns
      * <b>null</b>.
      * @see #setMainRootView
      */
    public RootView mainRootView() {
        return mainRootView;
    }

    /** Sets the main RootView.
      * @see #mainRootView
      */
    public void setMainRootView(RootView view) {
        addRootView(view);
        mainRootView = view;
    }

    /** Returns the Applet's RootView, or the RootView of the ExternalWindow
      * that was most recently made the top-most window.
      */
    RootView firstRootView() {
        return (RootView)rootViews.lastElement();
    }

    /** Returns a Vector containing all the RootViews being displayed
      * by the Application.  This vector is for <i>reading only</i> - do not
      * modify.
      */
    public Vector rootViews() {
        return rootViews;
    }

    /** Returns a Vector containing all the ExternalWindows being displayed
      * by the Application.  This Vector is for <i>reading only</i> - do not
      * modify.
      */
    public Vector externalWindows() {
        int i, count = rootViews.count();
        Vector windows = new Vector();

        for (i = 0; i < count; i++) {
            RootView rootView = (RootView)rootViews.elementAt(i);
            ExternalWindow window = rootView.externalWindow();

            if (window != null) {
                windows.addElement(window);
            }
        }
        return windows;
    }

    /* We are playing this add/remove game to make sure that
     * we don't add the same rootview twice and that the
     * firstRootView is not changed while adding a new rootview.
     */
    void addRootView(RootView view) {
        if (!rootViews.contains(view)) {
            rootViews.insertElementAt(view, 0);
            view.setApplication(this);
        }
    }

    void removeRootView(RootView view) {
        rootViews.removeElement(view);
        view.setApplication(null);
    }

    void makeFirstRootView(RootView view) {
        rootViews.removeElement(view);
        rootViews.addElement(view);
    }

    java.awt.Frame frame() {
        java.awt.Component comp;
        for (comp = applet;
             comp != null && !(comp instanceof java.awt.Frame);
             comp = comp.getParent());
        if (comp != null)
            return (java.awt.Frame) comp;
        else
            return null;
    }

    synchronized java.awt.MediaTracker mediaTracker() {
        if (tracker == null) {
            tracker = new java.awt.MediaTracker(applet);
        }

        return tracker;
    }

    synchronized int nextBitmapNumber() {
        return bitmapCount++;
    }

    synchronized TimerQueue timerQueue() {
        return timerQueue;
    }


    /** Called before the EventLoop processes <b>anEvent</b>.
      * The default implementation does nothing.
      */
    public void willProcessEvent(Event anEvent) {
    }

    /** Called after the EventLoop has processed <b>anEvent</b>.
      * The default implementation draws all dirty Views.  If
      * you subclass this method, you should call
      * <b>super.didProcessEvent()</b> at the end of your implementation.
      */
    public void didProcessEvent(Event anEvent) {
        drawAllDirtyViews();
    }

    /** This method is called by InternalWindow and ExternalWindow
     *  in showModally before waiting for an event.
     */
    void drawAllDirtyViews() {
        int i, count;
        RootView rView;

        count = rootViews.count();
        for (i = 0; i < count; i++) {
            rView = (RootView)rootViews.elementAt(i);
            rView.drawDirtyViews();
            rView._updateCursorAndMoveView();
        }
    }

    boolean isMac() {
        String          osName;

        osName = System.getProperty("os.name");
        if (osName != null && osName.startsWith("Mac")) {
            return true;
        }
        return false;
    }

    /** Subclassers can override to catch key down Events when there's no
      * focused view.
      */
    public void keyDown(KeyEvent event) {
    }

    /** Subclassers can override to catch key up Events when there's no
      * focused view.
      */
    public void keyUp(KeyEvent event) {
    }


    /** Modal views */
    void beginModalSessionForView(View aView) {
        RootView        rootView;

        if (aView == null)
            throw new InconsistencyException(
                            "beginModalSessionForView called with null view");

        _modalVector.addElement(aView);

        rootView = aView.rootView();
        if (rootView != null) {
            rootView.updateCursor();
        }
    }

    void endModalSessionForView(View aView) {
        RootView        rootView;

        if( aView != _modalVector.lastElement()) {
            throw new InconsistencyException("endModalSessionForView called for"+
                                    " a view that is not the last modal view");
        }

        _modalVector.removeLastElement();

        rootView = aView.rootView();
        if (rootView != null) {
            rootView.updateCursor();
        }
    }

    View modalView() {
        if( _modalVector.count() > 0)
            return (View) _modalVector.lastElement();
        else
            return null;
    }

    boolean isModalViewShowing() {
        if (_modalVector.count() == 0) {
            return false;
        } else {
            return true;
        }
    }

    /** Causes <b>target</b> to receive a <b>performCommand()</b> message
      * with <b>command</b> and <b>object</b>, from the Application's
      * main thread using the Application's EventLoop.  This method will not
      * return until the command has been performed. It can only be called
      * from threads <i>other than</i> the main thread.
      * @see #eventLoop
      * @see EventLoop#mainThread
      */
    public void performCommandAndWait(Target target,
                                      String command,
                                      Object data) {
        CommandEvent commandEvent = new CommandEvent(target, command, data);

        eventLoop.addEventAndWait(commandEvent);
    }

    /** Causes <b>target</b> to receive a <b>performCommand()</b> message
      * with <b>command</b> and <b>object</b>, after the current Event and
      * others in the Application's EventLoop have been processed. If
      * <b>ignorePrevious</b> is <b>true</b>, this method disposes of all
      * pending requests with the same <b>target</b> and <b>command</b>.
      * @see #eventLoop
      */
    public void performCommandLater(Target target,
                                    String command,
                                    Object data, boolean ignorePrevious) {
        CommandEvent commandEvent = new CommandEvent(target, command, data);

        if (ignorePrevious) {
            eventLoop.filterEvents(
                new CommandFilter(commandEvent.target, commandEvent.command,
                                  data));
        }
        eventLoop.addEvent(commandEvent);
    }

    /** Causes <b>target</b> to receive a <b>performCommand()</b> message
      * with <b>command</b> and <b>object</b>, after the current Event and
      * others in the Application's EventLoop have been processed.
      * Equivalent to the code:
      * <pre>
      *     performCommandLater(target, command, data, false)
      * </pre>
      * @see #performCommandLater(Target, String, Object, boolean)
      */
    public void performCommandLater(Target target, String command,
                                    Object data) {
        performCommandLater(target, command, data, false);
    }

    /** Creates and returns the Application's Applet.
      * This method will only be called in a stand-alone application.
      * Application subclasses can override this method to provide a
      * custom subclass of FoundationApplet. This Applet must have been
      * added to a java.awt.Frame and have an AppletStub.
      * @see FoundationApplet
      * @see java.awt.Frame
      * @see java.applet.AppletStub
      */
    protected FoundationApplet createApplet() {
        java.awt.Frame awtFrame = new java.awt.Frame();
        FoundationApplet applet = new FoundationApplet();

        awtFrame.add(applet);
        awtFrame.addNotify();
        applet.addNotify();
        applet.setStub(new FoundationAppletStub());
        return applet;
    }

    String exceptionHeader() {
        return "Uncaught exception.  IFC release: " + _releaseName;
    }

    /** Adds <b>observer</b> as an object that will receive notifications
      * when the Application's running state changes. An observer
      * might be interested in learning when the HTML page containing the
      * Application is no longer visible, for example.  See the
      * ApplicationObserver interface for more information.
      * @see ApplicationObserver
      * @see #removeObserver
      */
    public void addObserver(ApplicationObserver observer) {
        observers.addElementIfAbsent(observer);
    }

    /** Removes <b>observer</b> from the group of objects interested in
      * notifications of changes in the Application's running state.
      * @see #addObserver
      */
    public void removeObserver(ApplicationObserver observer) {
        observers.removeElement(observer);
    }

    /** Called from an Applet's <b>start()</b> method.  Notifies all
      * ApplicationObservers that the Application has resumed.
      * Normally called from FoundationApplet.
      */
    public void appletStarted() {
        int i = observers.count();

        isPaused = false;
        while (i-- > 0) {
            ApplicationObserver observer;

            observer = (ApplicationObserver)observers.elementAt(i);
            observer.applicationDidResume(this);
        }
        i = rootViews.count();
        while (i-- > 0) {
            RootView rootView = (RootView)rootViews.elementAt(i);

            if (rootView.externalWindow() == null) {
                rootView.setVisible(true);
            }
        }
    }

    /** Called from an Applet's <b>stop()</b> method.  Notifies all
      * ApplicationObservers that the Application has paused.
      * Normally called from FoundationApplet.
      */
    public void appletStopped() {
        int i = observers.count();

        isPaused = true;
        while (i-- > 0) {
            ApplicationObserver observer;

            observer = (ApplicationObserver)observers.elementAt(i);
            observer.applicationDidPause(this);
        }
        i = rootViews.count();
        while (i-- > 0) {
            RootView rootView = (RootView)rootViews.elementAt(i);

            if (rootView.externalWindow() == null) {
                rootView.setVisible(false);
            }
        }
    }

    void applicationDidStart() {
        int i = observers.count();

        while (i-- > 0) {
            ApplicationObserver observer;

            observer = (ApplicationObserver)observers.elementAt(i);
            observer.applicationDidStart(this);
        }
    }

    void applicationDidStop() {
        int i = observers.count();

        while (i-- > 0) {
            ApplicationObserver observer;

            observer = (ApplicationObserver)observers.elementAt(i);
            observer.applicationDidStop(this);
        }
    }

    /** Processes Application-specific Events. */
    public void processEvent(Event event) {
        if (event instanceof ApplicationEvent) {
            if (event.type == ApplicationEvent.APPLET_STOPPED) {
                appletStopped();
            } else if (event.type == ApplicationEvent.APPLET_STARTED) {
                appletStarted();
            }
        }
    }

    /** Returns <b>true</b> if the Application's EventLoop is
      * currently running.
      */
    public boolean isRunning() {
        return eventLoop.isRunning();
    }

    /** Returns <b>true</b> if the Application is currently paused.
      * Applications pause when their Applet's HTML page becomes hidden.
      */
    public boolean isPaused() {
        return isPaused();
    }
}
