/*
 * The contents of this file are subject to the terms of the Common Development
 * and Distribution License (the License). You may not use this file except in
 * compliance with the License.
 *
 * You can obtain a copy of the License at http://www.netbeans.org/cddl.html
 * or http://www.netbeans.org/cddl.txt.
 * 
 * When distributing Covered Code, include this CDDL Header Notice in each file
 * and include the License file at http://www.netbeans.org/cddl.txt.
 * If applicable, add the following below the CDDL Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 *
 * The Original Software is NetBeans. The Initial Developer of the Original
 * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
 * Microsystems, Inc. All Rights Reserved.
 */
package org.openide.windows;

import org.openide.awt.StatusDisplayer;
import org.openide.util.*;

import java.beans.*;


/** Simple support for an openable objects.
* Can be used either as an {@link org.openide.cookies.OpenCookie},
* {@link org.openide.cookies.ViewCookie}, or {@link org.openide.cookies.CloseCookie},
* depending on which cookies the subclass implements.
*
* @author Jaroslav Tulach
*/
public abstract class CloneableOpenSupport extends Object {
    private static java.awt.Container container;

    /** the environment that provides connection to outside world */
    protected Env env;

    /** All opened editors on this file.
     * <em>Warning:</em> Treat this field like <code>final</code>.
     * Internally the instance is used as <code>WeakListener</code>
     * on <code>Env</code> validity changes.
     * Changing the instance in subclasses would lead to breaking
     * of that listening, thus to errorneous behaviour. */
    protected CloneableTopComponent.Ref allEditors;

    /** New support for a given environment.
    * @param env environment to take all date from/to
    */
    public CloneableOpenSupport(Env env) {
        this.env = env;

        Listener l = new Listener(env);
        this.allEditors = l;

        // attach property change listener to be informed about loosing validity
        env.addPropertyChangeListener(org.openide.util.WeakListeners.propertyChange(l, env));

        // attach vetoable change listener to be cancel loosing validity when modified
        env.addVetoableChangeListener(org.openide.util.WeakListeners.vetoableChange(l, env));
    }

    /** Opens and focuses or just focuses already opened
     * <code>CloneableTopComponent</code>.
     * <p><b>Note: The actual processing of this method is scheduled into AWT thread
     * in case it is called from other than the AWT thread.</b></p>
     * @see org.openide.cookies.OpenCookie#open
     * @see #openCloneableTopComponent
     */
    public void open() {
        //Bugfix #10688 open() is now run in AWT thread
        Mutex.EVENT.writeAccess(
            new Runnable() {
                public void run() {
                    CloneableTopComponent editor = openCloneableTopComponent();
                    editor.requestActive();
                }
            }
        );
    }

    /** Focuses existing component to view, or if none exists creates new.
    * The default implementation simply calls {@link #open}.
    * @see org.openide.cookies.ViewCookie#view
    */
    public void view() {
        open();
    }

    /** Focuses existing component to view, or if none exists creates new.
    * The default implementation simply calls {@link #open}.
    * @see org.openide.cookies.EditCookie#edit
    */
    public void edit() {
        open();
    }

    /** Closes all components.
    * @return <code>true</code> if every component is successfully closed or <code>false</code> if the user cancelled the request
    * @see org.openide.cookies.CloseCookie#close
    */
    public boolean close() {
        return close(true);
    }

    /** Closes all opened windows.
    * @param ask true if we should ask user
    * @return true if sucesfully closed
    */
    protected boolean close(final boolean ask) {
        if (allEditors.isEmpty()) {
            return true;
        }

        //Bugfix #10688 close() is now run in AWT thread
        //also bugfix of 10714 - whole close (boolean) is run in AWT thread
        Boolean ret = (Boolean) Mutex.EVENT.writeAccess(
                new Mutex.Action() {
                    public Object run() {
                        //synchronized (allEditors) {
                        synchronized (getLock()) {
                            // user canceled the action
                            if (ask && !canClose()) {
                                return Boolean.FALSE;
                            }

                            java.util.Enumeration en = allEditors.getComponents();

                            while (en.hasMoreElements()) {
                                TopComponent c = (TopComponent) en.nextElement();

                                if (!c.close()) {
                                    return Boolean.FALSE;
                                }
                            }
                        }

                        return Boolean.TRUE;
                    }
                }
            );

        return ret.booleanValue();
    }

    /** Should test whether all data is saved, and if not, prompt the user
    * to save.
    * The default implementation returns <code>true</code>.
    *
    * @return <code>true</code> if everything can be closed
    */
    protected boolean canClose() {
        return true;
    }

    /** Simply open for an editor. */
    protected final CloneableTopComponent openCloneableTopComponent() {
        //synchronized (allEditors) {
        synchronized (getLock()) {
            CloneableTopComponent ret = allEditors.getArbitraryComponent();

            if (ret != null) {
                ret.open();

                return ret;
            } else {
                // no opened editor
                String msg = messageOpening();

                if (msg != null) {
                    StatusDisplayer.getDefault().setStatusText(msg);
                }

                CloneableTopComponent editor = createCloneableTopComponent();
                editor.setReference(allEditors);
                editor.open();

                msg = messageOpened();

                if (msg == null) {
                    msg = ""; // NOI18N
                }

                StatusDisplayer.getDefault().setStatusText(msg);

                return editor;
            }
        }
    }

    /** Creates lock object used in close and openCloneableTopComponent. */
    private Object getLock() {
        if (container == null) {
            container = new java.awt.Container();
        }

        return container.getTreeLock();
    }

    /** A method to create a new component. Must be overridden in subclasses.
    * @return the cloneable top component for this support
    */
    protected abstract CloneableTopComponent createCloneableTopComponent();

    /** Message to display when an object is being opened.
    * @return the message or null if nothing should be displayed
    */
    protected abstract String messageOpening();

    /** Message to display when an object has been opened.
    * @return the message or null if nothing should be displayed
    */
    protected abstract String messageOpened();

    /** Abstract interface that is used by CloneableOpenSupport to
    * talk to outside world.
    */
    public static interface Env extends java.io.Serializable {
        /** that is fired when the objects wants to mark itself as
        * invalid, so all components should be closed.
        */
        public static final String PROP_VALID = "valid"; // NOI18N

        /** that is fired when the objects wants to mark itself modified
        * or not modified.
        */
        public static final String PROP_MODIFIED = "modified"; // NOI18N

        /** Adds property listener.
        */
        public void addPropertyChangeListener(PropertyChangeListener l);

        /** Removes property listener.
        */
        public void removePropertyChangeListener(PropertyChangeListener l);

        /** Adds veto listener.
        */
        public void addVetoableChangeListener(VetoableChangeListener l);

        /** Removes veto listener.
        */
        public void removeVetoableChangeListener(VetoableChangeListener l);

        /** Test whether the support is in valid state or not.
        * It could be invalid after deserialization when the object it
        * referenced to does not exist anymore.
        *
        * @return true or false depending on its state
        */
        public boolean isValid();

        /** Test whether the object is modified or not.
        * @return true if the object is modified
        */
        public boolean isModified();

        /** Support for marking the environement modified.
        * @exception IOException if the environment cannot be marked modified
        *    (for example when the file is readonly), when such exception
        *    is the support should discard all previous changes
        */
        public void markModified() throws java.io.IOException;

        /** Reverse method that can be called to make the environment
        * unmodified.
        */
        public void unmarkModified();

        /** Method that allows environment to find its
        * cloneable open support.
        */
        public CloneableOpenSupport findCloneableOpenSupport();
    }

    /** Property change & veto listener. To react to dispose/delete of
    * the data object.
    */
    private static final class Listener extends CloneableTopComponent.Ref implements PropertyChangeListener,
        VetoableChangeListener, Runnable {
        /** generated Serialized Version UID */
        static final long serialVersionUID = -1934890789745432531L;

        /** environment to use as connection to outside world */
        private Env env;

        /** Constructor.
        */
        public Listener(Env env) {
            this.env = env;
        }

        /** Getter for the associated CloneableOpenSupport
        * @return the support or null if none was found
        */
        private CloneableOpenSupport support() {
            return env.findCloneableOpenSupport();
        }

        public void propertyChange(PropertyChangeEvent ev) {
            if (Env.PROP_VALID.equals(ev.getPropertyName())) {
                // do not check it if old value is not true
                if (Boolean.FALSE.equals(ev.getOldValue())) {
                    return;
                }

                Mutex.EVENT.readAccess(this);
            }
        }

        /** Closes the support in AWT thread.
         */
        public void run() {
            // loosing validity
            CloneableOpenSupport os = support();

            if (os != null) {
                // mark the object as not being modified, so nobody
                // will ask for save
                env.unmarkModified();

                os.close(false);
            }
        }

        /** Forbids setValid (false) on data object when there is an
        * opened editor.
        *
        * @param ev PropertyChangeEvent
        */
        public void vetoableChange(PropertyChangeEvent ev)
        throws PropertyVetoException {
            if (Env.PROP_VALID.equals(ev.getPropertyName())) {
                // do not check it if old value is not true
                if (Boolean.FALSE.equals(ev.getOldValue())) {
                    return;
                }

                if (env.isModified()) {
                    // if the object is modified 
                    CloneableOpenSupport os = support();

                    if ((os != null) && !os.canClose()) {
                        // is modified and has not been sucessfully closed
                        throw new PropertyVetoException(
                        // [PENDING] this is not a very good detail message!
                        "", ev // NOI18N
                        );
                    }
                }
            }
        }

        /** Resolvable to connect to the right data object. This
        * method is used for connectiong CloneableTopComponents via
        * their CloneableTopComponent.Ref
        */
        public Object readResolve() {
            CloneableOpenSupport os = support();

            if (os == null) {
                // problem! no replace!?
                return this;
            }

            // use the editor support's CloneableTopComponent.Ref
            return os.allEditors;
        }
    }
}
