/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.netbeans.core.windows;


import org.openide.windows.Mode;
import org.openide.windows.TopComponent;
import org.openide.windows.Workspace;

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import javax.swing.SwingUtilities;
import java.awt.Image;
import java.awt.Rectangle;
import java.io.IOException;
import java.util.Collection;
import org.netbeans.core.windows.persistence.ModeConfig;
import org.netbeans.core.windows.persistence.PersistenceManager;
import org.openide.util.Exceptions;


/** This class is an implementation of Mode interface.
 * It designates 'place' on screen, at wich TopComponent can occure.
 *
 * @author Peter Zavadsky
 */
public final class ModeImpl implements Mode.Xml {

    /** Name constant as a base for nonamed modes. */
    private static final String MODE_ANONYMOUS_NAME = "anonymousMode"; // NOI18N
    
    /** asociated property change support for firing property changes */
    private final PropertyChangeSupport changeSupport = new PropertyChangeSupport(this);

    /** Debugging flag. */
    private static final boolean DEBUG = Debug.isLoggable(ModeImpl.class);
    
    
    /** Construct new mode with given properties */
    private ModeImpl(String name, int state, int kind, boolean permanent) {
        getCentral().createModeModel(this, name, state, kind, permanent);
    }
    
    
    /** Factory method which creates <code>ModeImpl</code> instances. */
    public static ModeImpl createModeImpl(String name, int state, int kind, boolean permanent) {
        return new ModeImpl(name, state, kind, permanent);
    }
    
    
    ///////////////////////////////////////////////////////////////////
    // Start of org.openide.windows.Mode interface implementation.
    ///////////////////////////////////////////////////////////////////
    /** Gets the programmatic name of this mode.
     * This name should be unique, as it is used to find modes etc.
     * Implements <code>Mode</code> interface method.
     * @return programmatic name of this mode */
    @Override
    public String getName () {
        WindowManagerImpl.warnIfNotInEDT();
        
        return getCentral().getModeName(this);
    }
    
    /**
     * @return A list of Mode's additional names.
     * @since 2.30
     */
    Collection<String> getOtherNames() {
        WindowManagerImpl.warnIfNotInEDT();
        
        return getCentral().getModeOtherNames(this);
    }
    
    /**
     * Add another mode name.
     * @param modeOtherName 
     * @since 2.30
     */
    void addOtherName( String modeOtherName ) {
        WindowManagerImpl.warnIfNotInEDT();
        
        getCentral().addModeOtherName(this, modeOtherName);
    }
    
    /** Gets display name of this mode.
     ** Implements <code>Mode</code> interface method.
     * @return Human presentable name of this mode implementation
     * @deprecated It is not used anymore. This impl delegated to {@link #getName} method.  */
    @Override
    public String getDisplayName () {
        WindowManagerImpl.warnIfNotInEDT();
        
        return getName();
    }

    /** Gets icon for this mode.
     * Implements <code>Mode</code> interface method. 
     * @return null
     * @deprecated It is not used anymore. */
    @Override
    public Image getIcon () {
        WindowManagerImpl.warnIfNotInEDT();
        
        return null;
    }

    /** Indicates whether specified <code>TopComponent</code> can be docked
     * into this <code>Mode</code>.
     * Implements <code>Mode</code> interface method. 
     * @return <code>true</code> */
    @Override
    public boolean canDock(TopComponent tc) {
        WindowManagerImpl.warnIfNotInEDT();
        
        return true;
    }
    
    /** Attaches a component to a mode for this workspace.
     * If the component is in different mode on this desktop, it is 
     * removed from the original and moved to this one.
     * Implements <code>Mode</code> interface method.
     *
     * @param tc top component to dock into this mode
     * @return true if top component was succesfully docked to this
     * mode, false otherwise */
    @Override
    public boolean dockInto(TopComponent tc) {
        WindowManagerImpl.warnIfNotInEDT();
        
        return dockIntoImpl(tc, true);
    }
    
    /** Sets bounds of this mode.
     * Implements <code>Mode</code> interface method.
     * @param rect bounds for the mode */
    @Override
    public void setBounds (Rectangle bounds) {
        WindowManagerImpl.warnIfNotInEDT();
        
        getCentral().setModeBounds(this, bounds);
    }

    /** Getter for current bounds of the mode.
     * Implements <code>Mode</code> interface method.
     * @return the bounds of the mode
     */
    @Override
    public Rectangle getBounds () {
        WindowManagerImpl.warnIfNotInEDT();
        
        return getCentral().getModeBounds(this);
    }
    

    /** Getter for asociated workspace.
     * Implements <code>Mode</code> interface method.
     * @return The workspace instance to which is this mode asociated.
     * @deprecated XXX Don't use anymore.
     */
    @Override
    public Workspace getWorkspace () {
        WindowManagerImpl.warnIfNotInEDT();
        
        // Here is the only fake workspace.
        return WindowManagerImpl.getInstance();
    }
    
    /** Gets array of <code>TopComponent</code>S in this mode.
     * Implements <code>Mode</code> interface method.
     * @return array of top components which are currently
     * docked in this mode. May return empty array if no top component
     * is docked in this mode.
     */
    @Override
    public TopComponent[] getTopComponents() {
        WindowManagerImpl.warnIfNotInEDT();
        
        return getCentral().getModeTopComponents(this).toArray(new TopComponent[0]);
    }

    /** Adds listener to the property changes.
     * Implements <code>Mode</code> interface support. */
    @Override
    public void addPropertyChangeListener (PropertyChangeListener pchl) {
        changeSupport.addPropertyChangeListener(pchl);
    }

    /** Removes listener to the property changes.
     * Implements <code>Mode</code> interface method. */
    @Override
    public void removePropertyChangeListener (PropertyChangeListener pchl) {
        changeSupport.removePropertyChangeListener(pchl);
    }
    ///////////////////////////////////////////////////////////////////
    // End of org.openide.windows.Mode interface implementation.
    ///////////////////////////////////////////////////////////////////

    
    /** Actually performs the docking operation.
     * @param tc top component to dock into this mode
     * @param orderWeight weight for ordering. Smaller weight number means
     * smaller position index, which means closer to the top or start in
     * visual representations 
     * @param select <code>true</code> if the docked <code>TopComponent</code>
     * will be selected afterwards
     * @return true if top component was succesfully docked to this */
    private boolean dockIntoImpl(final TopComponent tc, final boolean select) {
        if(DEBUG) {
            Debug.log(ModeImpl.class, "Docking tc=" + tc.getName() + " into mode=" + this); // NOI18N
            Debug.dumpStack(ModeImpl.class);
        }
        
        boolean opened = false;
        // PENDING
        // Preferably all in one step.
        ModeImpl mode = (ModeImpl)WindowManagerImpl.getInstance().findMode(tc);
        if(mode != null && mode != this) {
            // XXX if only closin (mode.close(tc)) there could happen,
            // there is the same TopComponent as closed in two modes. Revise.
            opened = tc.isOpened();
            mode.removeTopComponent(tc);
        }
        
        if( opened ) {
            //don't close the TopComponent if it was opened in the previous mode
            addOpenedTopComponent( tc );
        } else {
            addClosedTopComponent(tc);
        }
        return true;
    }
    
    /** Closes given top component. */
    public void close(TopComponent tc) {
        if(!getOpenedTopComponents().contains(tc)) {
            return;
        }
        if (PersistenceHandler.isTopComponentPersistentWhenClosed(tc)) {
            addClosedTopComponent(tc);
        } else {
            if (Boolean.TRUE.equals(tc.getClientProperty(Constants.KEEP_NON_PERSISTENT_TC_IN_MODEL_WHEN_CLOSED))) {
                addClosedTopComponent(tc);
            } else {
                removeTopComponent(tc);
            }
        }
    }

    /** Gets list of opened TopComponentS. */
    public List<TopComponent> getOpenedTopComponents() {
        return getCentral().getModeOpenedTopComponents(this);
    }
    
    /** Sets selected TopComponent. */
    public void setSelectedTopComponent(TopComponent tc) {
        if(!getOpenedTopComponents().contains(tc)) {
            return;
        }
        
        TopComponent old = getSelectedTopComponent();
        if(tc == old) {
            return;
        }
        
        getCentral().setModeSelectedTopComponent(this, tc);
    }
    
    /** Gets selected TopComponent. */
    public TopComponent getSelectedTopComponent() {
        WindowManagerImpl.assertEventDispatchThread();
        
        return getCentral().getModeSelectedTopComponent(this);
    }
    
    /**
     * Remember which top component was previously the selected one.
     * Used when switching to/from maximized mode.
     */
    public void setPreviousSelectedTopComponentID(String tcId) {
        String old = getPreviousSelectedTopComponentID();
        if(null != tcId && tcId.equals(old)) {
            return;
        }
        getCentral().setModePreviousSelectedTopComponentID(this, tcId);
    }
    
    /**
     * @return The top component that was the selected one before switching to/from 
     * the maximized mode.
     */
    public TopComponent getPreviousSelectedTopComponent() {
        String tcId = getPreviousSelectedTopComponentID();
        TopComponent res = null;
        if( null != tcId )
            res = WindowManagerImpl.getInstance().findTopComponent(tcId);
        WindowManagerImpl.assertEventDispatchThread();
        
        return res;
    }
    
    /**
     * @return The ID top component that was the selected one before switching to/from 
     * the maximized mode.
     */
    public String getPreviousSelectedTopComponentID() {
        WindowManagerImpl.assertEventDispatchThread();
        
        return getCentral().getModePreviousSelectedTopComponentID(this);
    }
    
    public void addOpenedTopComponent(TopComponent tc) {
        getCentral().addModeOpenedTopComponent(this, tc);
    }
    
    public void addOpenedTopComponentNoNotify(TopComponent tc) {
        getCentral().addModeOpenedTopComponentNoNotify(this, tc);
    }

    public void addOpenedTopComponent(TopComponent tc, int index) {
        getCentral().insertModeOpenedTopComponent( this, tc, index );
    }
    
    public void addClosedTopComponent(TopComponent tc) {
        getCentral().addModeClosedTopComponent(this, tc);
    }
    
    public void addUnloadedTopComponent(String tcID) {
        addUnloadedTopComponent( tcID, -1 );
    }
    
    public void addUnloadedTopComponent(String tcID, int index) {
        getCentral().addModeUnloadedTopComponent(this, tcID, index);
    }
    
    public void setUnloadedSelectedTopComponent(String tcID) {
        getCentral().setUnloadedSelectedTopComponent(this, tcID);
    }
    
    public void setUnloadedPreviousSelectedTopComponent(String tcID) {
        getCentral().setUnloadedPreviousSelectedTopComponent(this, tcID);
    }
    
    public List<String> getOpenedTopComponentsIDs() {
        return getCentral().getModeOpenedTopComponentsIDs(this);
    }
    
    public List<String> getClosedTopComponentsIDs() {
        return getCentral().getModeClosedTopComponentsIDs(this);
    }
    
    public List<String> getTopComponentsIDs() {
        return getCentral().getModeTopComponentsIDs(this);
    }
    
    /** Gets opened top component position in the mode */
    public int getTopComponentTabPosition(TopComponent tc) {
        return getCentral().getModeTopComponentTabPosition(this, tc);
    }
    
    /** Sets and updates the state of associated frame, if frame exists.
     * Otherwise remembers state for futher use
     */
    public void setFrameState(int state) {
        getCentral().setModeFrameState(this, state);
    }
    
    /** @return state of the frame
     * If frame exists, its real state is returned. 
     * Last remembered frame state is returned if frame currently
     * doesn't exist. FrameType.NORMAL is returned as default if state cannot be
     * obtained by mentioned procedures.
     */
    public int getFrameState () {
        return getCentral().getModeFrameState(this);
    }
    
    /** Indicates whether this mode is permanent, it means it is kept in model
     * even in case it becomes empty. */
    public boolean isPermanent () {
        return getCentral().isModePermanent(this);
    }
    
    /** Indicates whether this mode has no TopComponents. */
    public boolean isEmpty() {
        return getCentral().isModeEmpty(this);
    }

    public boolean containsTopComponent(TopComponent tc) {
        return getCentral().containsModeTopComponent(this, tc);
    }
    
    /** Gets state of mode. Either split or separate. */
    public int getState() {
        return getCentral().getModeState(this);
    }
    
    /** Gets kind, either editor or view. */
    public int getKind() {
        return getCentral().getModeKind(this);
    }
    /** Gets side, either null for view and editor kinds, a side constant for sliding kind.. */
    public String getSide() {
        return getCentral().getModeSide(this);
    }
    
    // Contstraints and split weights are saved in split structure at wm model level.
    /** Sets constraints for mode. */
    public void setConstraints(SplitConstraint[] constraints) {
        WindowManagerImpl.getInstance().setModeConstraints(this, constraints);
    }

    /** @return Current constraints of this mode, null by default */
    public SplitConstraint[] getConstraints() {
        return WindowManagerImpl.getInstance().getModeConstraints(this);
    }
    
    /**
     * @return True if this mode is minimized.
     * @since 2.30
     */
    public boolean isMinimized() {
        return getCentral().isModeMinimized( this );
    }
    
    /**
     * Mark this mode as minimized or docked.
     * @param minimized 
     * @since 2.30
     */
    public void setMinimized( boolean minimized ) {
        getCentral().setModeMinimized( this, minimized );
    }
    
    /** Removes TopComponent from this mode. */
    public void removeTopComponent(TopComponent tc) {
        getCentral().removeModeTopComponent(this, tc);
    }
    
    public void removeTopComponents(Set topComponentSet) {
        for(Iterator it = topComponentSet.iterator(); it.hasNext(); ) {
            TopComponent tc = (TopComponent)it.next();
            removeTopComponent(tc);
        }
    }
    
    // XXX Only use for yet unloaded components, for PersistenceHandler only.
    public void removeClosedTopComponentID(String tcID) {
        getCentral().removeModeClosedTopComponentID(this, tcID);
    }
    
    // XXX It is used for user actions only, to prohibit mixing
    // of view and editor components.
    /** Indicates whether this mode can contain specified TopComponent. */
    public boolean canContain(TopComponent tc) {
        if(Constants.SWITCH_MODE_ADD_NO_RESTRICT
        || WindowManagerImpl.getInstance().isTopComponentAllowedToMoveAnywhere(tc)
        || Switches.isMixingOfEditorsAndViewsEnabled()) {
            return true;
        }
        
        ModeImpl mode = (ModeImpl)WindowManagerImpl.getInstance().findMode(tc);
        if(mode == null) {
            return true;
        }
        // allow mixing of view and sliding modes
        int myKind = getKind();
        int otherKind = mode.getKind();
        
        return (myKind == otherKind) ||
               (myKind != Constants.MODE_KIND_EDITOR && otherKind != Constants.MODE_KIND_EDITOR);
    }
    
    void doFirePropertyChange(final String propName,
    final Object oldValue, final Object newValue) {
        // PENDING When #37529 finished, then uncomment the next row and move the
        // checks of AWT thread away.
        //  WindowManagerImpl.assertEventDispatchThread();
        if(SwingUtilities.isEventDispatchThread()) {
            changeSupport.firePropertyChange(propName, oldValue, newValue);
        } else {
            SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run() {
                    changeSupport.firePropertyChange(propName, oldValue, newValue);
                }
            });
        }
    }
    
    /** @return string description of this mode */
    @Override
    public String toString () {
        // #42995 - don't scream when toString called from non-AWT thread
        return super.toString () + "[" + getCentral().getModeName(this) + "]"; // NOI18N
    }
    
    /** Accessor to central unit. Helper method. */
    private static Central getCentral() {
        return WindowManagerImpl.getInstance().getCentral();
    }
    
    
    ////////////////////
    // Utility methods>>
    /*private*/ static String getUnusedModeName() {
        String base = MODE_ANONYMOUS_NAME;
        
        // don't allow base to be too long, because will act as file name too
        // PENDING Maximal length is 20.
        if (base.length() > 20) {
            base = base.substring(0, 20);
        }
        
        // add numbers to the name
        String result;
        int modeNumber = 1;
        WindowManagerImpl wm = WindowManagerImpl.getInstance();
        while(wm.findMode(result = base + "_" + modeNumber) != null) { // NOI18N
            modeNumber++;
        }
        return result;
    }
    // Utility methods<<
    ////////////////////

    public void setModeName(String text) {
        getCentral().setModeName(this, text);
    }

    @Override
    public String toXml() {
        ModeConfig config = PersistenceHandler.getDefault().getConfigFromMode(this);
        try {
            return PersistenceManager.getDefault().createXmlFromMode(config);
        } catch (IOException ex) {
            Exceptions.printStackTrace(ex);
            return "";
        }
    }

}

