/* class Window
 *
 * Copyright (C) 2001-2003  R M Pitman
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package charva.awt;

import charva.awt.event.*;

import java.io.File;
import java.util.Enumeration;
import java.util.Vector;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * The Window class represents a "toplevel" window with
 * no decorative frame. The window is initially invisible; you must use
 * the show() method to make it visible.
 */
public class Window
        extends Container
        implements Runnable {

    private static final Log LOG = LogFactory.getLog(Window.class);

    public Window(Window owner_) {
        _owner = owner_;
        init();
    }

    public Window(Frame owner_) {
        _owner = (Window) owner_;
        init();
    }

    private void init() {
        _term = Toolkit.getDefaultToolkit();
        super._layoutMgr = new BorderLayout();
        _visible = false;

        // The window inherits the colors of its parent if there is one,
        // otherwise use the default colors as set in charva.awt.Toolkit.
        if (_owner != null) {
            super.setForeground(_owner.getForeground());
            super.setBackground(_owner.getBackground());
        } else {
            super.setForeground(Toolkit.getDefaultForeground());
            super.setBackground(Toolkit.getDefaultBackground());
        }
    }

    /**
     * Return the Window that is the "owner" of this Window.
     */
    public Window getOwner() {
        return _owner;
    }

    /**
     * Register a WindowListener object for this window.
     */
    public void addWindowListener(WindowListener listener_) {
        if (_windowListeners == null)
            _windowListeners = new Vector();

        _windowListeners.add(listener_);
    }

    /**
     * Process window events occurring on this window by dispatching them
     * to any registered WindowListener objects.
     */
    protected void processWindowEvent(WindowEvent evt_) {
        if (_windowListeners == null)
            return;

        Enumeration e = _windowListeners.elements();
        while (e.hasMoreElements()) {
            WindowListener wl = (WindowListener) e.nextElement();
            switch (evt_.getID()) {

                case AWTEvent.WINDOW_CLOSING:
                    wl.windowClosing(evt_);
                    break;

                case AWTEvent.WINDOW_OPENED:
                    wl.windowOpened(evt_);
                    break;
            }
        }
    }

    /**
     * Returns true if this Window is currently displayed.
     */
    public boolean isDisplayed() {
        return _term.isWindowDisplayed(this);
    }

    /**
     * Causes this Window to be sized to fit the preferred sizes and
     * layouts of its contained components.
     */
    public void pack() {
        setSize(minimumSize());
        super.doLayout();   // call method in Container superclass
    }

    /**
     * Lay out the contained components, draw the window and its contained
     * components, and then read input events off the EventQueue and send
     * them to the component that has the input focus.
     */
    public void show() {

        if (_visible)
            return;    // This window is already visible.

        _visible = true;
        _term.addWindow(this);

        /* call doLayout in Container superclass.  This will lay out all of
         * the contained components (i.e. children) and their descendants,
         * and in the process will set the valid flag of all descendants.
         */
        super.doLayout();    // call method in Container superclass

        this.adjustLocation();   // ensure it fits inside the screen

        this.draw();

        /* Rather than call Toolkit.sync() directly here, we put a SyncEvent
         * onto the SyncQueue. The SyncThread will read it off the SyncQueue
         * and then sleep for 50msec before putting the SyncEvent onto
         * the EventQueue, from which it will be picked up by the active
         * Window (i.e. an instance of this class).  The active Window
         * will then call Toolkit.sync() directly.  This slight delay speeds
         * up the case where a window opens and then immediately opens a
         * new window (and another...).
         */
        SyncQueue.getInstance().postEvent(new SyncEvent(this));

        if (_dispatchThreadRunning)
            run();
        else {
            _dispatchThreadRunning = true;
            Thread dispatchThread = new Thread(this);
            dispatchThread.setName("event dispatcher");
            dispatchThread.start();

            /* If "charva.script.playback" is defined, we start up
             * a thread for playing back the script. Keys from both the
             * script and the keyboard will cause "fireKeystroke()" to be
             * invoked.
             * The playback thread is started _after_ "addWindow()" is
             * called for the first time, to make sure that _windowList
             * is non-empty when the playback thread calls "fireKeystroke()".
             */
            startPlayback();

        }
    }

    public void run() {
        /* Loop and process input events until the window closes.
         */
        EventQueue evtQueue = _term.getSystemEventQueue();
        try {
            for (_windowClosed = false; _windowClosed != true;) {

                // This returns immediately if there is a keystroke available,
                // otherwise it blocks for up to 100 msec waiting for one.
                _term.checkForKeystroke();

                while (!evtQueue.queuesAreEmpty()) {

                    java.util.EventObject evt = evtQueue.getNextEvent();

                    /* The event object should always be an AWTEvent. If not,
                     * we will get a ClassCastException.
                     */
                    processEvent((AWTEvent) evt);
                }
            }    // end FOR loop
        } catch (Exception e) {
            e.printStackTrace();
            System.exit(1);
        }
    }

    /**
     * Process an event off the event queue.  This method can be extended by
     * subclasses of Window to deal with application-specific events.
     */
    protected void processEvent(AWTEvent evt_) {
        Object source = evt_.getSource();

        if (evt_ instanceof AdjustmentEvent)
            ((Adjustable) source).processAdjustmentEvent((AdjustmentEvent) evt_);

        else if (evt_ instanceof ScrollEvent) {
            ((Scrollable) source).processScrollEvent((ScrollEvent) evt_);
            requestFocus();
            super.requestSync();
        } else if (evt_ instanceof PaintEvent) {

            /* Unless the affected component is totally obscured by
             * windows that are stacked above it, we must redraw its
             * window and all the windows above it.
             */
            if (!((Component) source).isTotallyObscured()) {

                Vector windowlist = _term.getWindowList();
                synchronized (windowlist) {

                    /* We have to draw the window rather than just the affected
                     * component, because setVisible(false) may have been set on
                     * the component.
                     */
                    Window ancestor = ((Component) source).getAncestorWindow();
                    ancestor.draw();

                    /* Ignore windows that are underneath the window that
                     * contains the component that generated the PaintEvent.
                     */
                    Window w = null;
                    int i;
                    for (i = 0; i < windowlist.size(); i++) {
                        w = (Window) windowlist.elementAt(i);
                        if (w == ancestor)
                            break;
                    }

                    /* Redraw all the windows _above_ the one that generated
                     * the PaintEvent.
                     */
                    for (; i < windowlist.size(); i++) {
                        w = (Window) windowlist.elementAt(i);
                        w.draw();
                    }
                }

                super.requestSync();
            }
        } else if (evt_ instanceof SyncEvent) {
            _term.sync();
        } else if (evt_ instanceof WindowEvent) {
            WindowEvent we = (WindowEvent) evt_;
            we.getWindow().processWindowEvent(we);

            /* Now, having given the WindowListener objects a chance to
             * process the WindowEvent, we must check if it was a
             * WINDOW_CLOSING event sent to this window.
             */
            if (we.getID() == AWTEvent.WINDOW_CLOSING) {

                we.getWindow()._windowClosed = true;

                /* Remove this window from the list of those displayed,
                 * and blank out the screen area where the window was
                 * displayed.
                 */
                _term.removeWindow(we.getWindow());
                _term.blankBox(_origin, _size);

                /* Now redraw all of the windows, from the bottom to the
                 * top.
                 */
                Vector winlist = _term.getWindowList();
                Window window = null;
                synchronized (winlist) {
                    for (int i = 0; i < winlist.size(); i++) {
                        window = (Window) winlist.elementAt(i);
                        window.draw();
                    }
                    if (window != null)        // (there may be no windows left)
                        window.requestFocus();
                }

                /* Put a SyncEvent onto the SyncQueue.  The SyncThread will
                 * sleep for 50 msec before putting it onto the EventQueue,
                 * from which it will be picked up by the active Window
                 * (i.e. an instance of this class), which will then call
                 * Toolkit.sync() directly.  This is done to avoid calling
                 * sync() after the close of a window and again after the
                 * display of a new window which might be displayed
                 * immediately afterwards.
                 */
                if (window != null)
                    SyncQueue.getInstance().postEvent(new SyncEvent(window));
            }
        }    // end if WindowEvent
        else if (evt_ instanceof GarbageCollectionEvent) {
            SyncQueue.getInstance().postEvent(evt_);
        } else if (evt_ instanceof InvocationEvent) {
            ((InvocationEvent) evt_).dispatch();
        } else {
            /* It is a KeyEvent, MouseEvent, ActionEvent, ItemEvent,
             * FocusEvent or a custom type of event.
             */
            ((Component) source).processEvent(evt_);
        }
    }

    /**
     * Hide this window and all of its contained components.
     * This is done by putting a WINDOW_CLOSING event onto the queue.
     */
    public void hide() {

        if (!_visible) {
            LOG.warn("Trying to hide window " + this + " that is already hidden!");
            return;    // This window is already hidden.
        }

        _visible = false;
        WindowEvent we = new WindowEvent(this, AWTEvent.WINDOW_CLOSING);
        _term.getSystemEventQueue().postEvent(we);
    }

    /**
     * Draw all the components in this window, and request the keyboard focus.
     */
    public void draw() {
        super.draw();
        requestFocus();
    }

    /**
     * Overrides the method in the Component superclass, because a Window
     * has no parent container.
     * Note that we return a COPY of the origin, not a reference to it, so
     * that the caller cannot modify our location via the returned value.
     */
    public Point getLocationOnScreen() {
        return new Point(_origin);
    }

    /**
     * A Window component will not receive input focus during keyboard focus
     * traversal using Tab and Shift-Tab.
     */
    public boolean isFocusTraversable() {
        return false;
    }

    /**
     * Adjust the position of the window so that it fits inside the screen.
     */
    public void adjustLocation() {
        int bottom = _origin.y + getHeight();
        if (bottom > _term.getScreenRows())
            _origin.y -= bottom - _term.getScreenRows();

        if (_origin.y < 0)
            _origin.y = 0;

        int right = _origin.x + getWidth();
        if (right > _term.getScreenColumns())
            _origin.x -= right - _term.getScreenColumns();

        if (_origin.x < 0)
            _origin.x = 0;
    }

    public void debug(int level_) {
        LOG.debug("Window origin=" + _origin + " size=" + _size);
        super.debug(1);
    }

    private void startPlayback() {
        String scriptfilename;
        if ((scriptfilename = System.getProperty("charva.script.playbackFile")) == null)
            return;

        File scriptFile = new File(scriptfilename);
        if (!scriptFile.canRead()) {
            LOG.warn("Cannot read script tile \"" + scriptfilename + "\"");
            return;
        }


        PlaybackThread thr = new PlaybackThread(scriptFile);
        thr.setDaemon(true);
        thr.setName("playback thread");
        thr.start();
    }

    //====================================================================
    // INSTANCE VARIABLES

    private Window _owner;
    protected Toolkit _term;
    private boolean _windowClosed = false;

    private Vector _windowListeners = null;

    private static boolean _dispatchThreadRunning = false;

}
