/*
 * 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 java.awt.Dimension;
import java.awt.Frame;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.io.IOException;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.netbeans.core.windows.persistence.*;
import org.openide.awt.ToolbarPool;
import org.openide.filesystems.FileObject;
import org.openide.util.Exceptions;
import org.openide.util.Utilities;
import org.openide.windows.*;


/**
 * Class which handles loading, saving of window system and persistence events.
 *
 * @author  Peter Zavadsky, Marek Slama
 */
final public class PersistenceHandler implements PersistenceObserver {

    // Persistence data
    /** Maps mode config name to mode instance. */
    private final Map<String, ModeImpl> name2mode = new WeakHashMap<String, ModeImpl>(10);
    /** Maps group config name to group instance. */
    private final Map<String, TopComponentGroupImpl> name2group = new WeakHashMap<String, TopComponentGroupImpl>(10);

    private static PersistenceHandler defaultInstance;

    private boolean loaded = false;

    /** Debugging flag. */
    private static final boolean DEBUG = Debug.isLoggable(PersistenceHandler.class);

    private final LazyLoader lazyLoader = new LazyLoader();


    /** Creates a new instance of PersistenceHanlder */
    private PersistenceHandler() {
    }

    /**
     * Clears cached data.
     */
    public void clear() {
        name2mode.clear();
        name2group.clear();
        TopComponentTracker.getDefault().clear();
    }

    public static synchronized PersistenceHandler getDefault() {
        if(defaultInstance == null) {
            defaultInstance = new PersistenceHandler();
        }

        return defaultInstance;
    }

    // XXX helper method
    public static boolean isTopComponentPersistentWhenClosed(TopComponent tc) {
        return PersistenceManager.isTopComponentPersistentWhenClosed(tc);
    }

    boolean isLoaded() {
        return loaded;
    }

    public void load() {
        if(DEBUG) {
            debugLog("## PersistenceHandler.load"); // NOI18N
        }

        TopComponentTracker.getDefault().load();

        WindowManagerConfig wmc = null;
        try {
            wmc = PersistenceManager.getDefault().loadWindowSystem();
        } catch (IOException exc) {
            // Serious persistence problem -> try to reset
            Exceptions.attachLocalizedMessage(exc, "Cannot load window system persistent data, user directory content is broken. Resetting to default layout..."); //NOI18N
            Logger.getLogger(PersistenceHandler.class.getName()).log(Level.WARNING, null, exc); // NOI18N
            // try to delete local winsys data and try once more
            try {
                FileObject rootFolder = PersistenceManager.getDefault().getRootLocalFolder();
                if (null != rootFolder) {
                    rootFolder.delete();
                    wmc = PersistenceManager.getDefault().loadWindowSystem();
                } else {
                    Logger.getLogger(PersistenceHandler.class.getName()).log(Level.WARNING, "Cannot even get access to local winsys configuration, using internally predefined configuration."); // NOI18N
                    wmc = ConfigFactory.createDefaultConfig();
                }
            } catch (IOException ioE) {
                Exceptions.attachLocalizedMessage(ioE, "Cannot load even default layout, using internally predefined configuration."); //NOI18N
                Logger.getLogger(PersistenceHandler.class.getName()).log(Level.WARNING, null, ioE);
                wmc = ConfigFactory.createDefaultConfig();
            }
        }

        ToolbarPool.getDefault().setPreferredIconSize(wmc.preferredToolbarIconSize);

        WindowManagerImpl wm = WindowManagerImpl.getInstance();
        if (wmc.tcIdViewList.length > 0) {
            wm.setRecentViewList(wmc.tcIdViewList);
        } else {
            //No recent view list is saved, fill it by opened TopComponents
            List<String> idList = new ArrayList<>();
            for (int i = 0; i < wmc.modes.length; i++) {
                ModeConfig mc = wmc.modes[i];
                for (int j = 0; j < mc.tcRefConfigs.length; j++) {
                    //Only opened
                    if (mc.tcRefConfigs[j].opened) {
                        idList.add(mc.tcRefConfigs[j].tc_id);
                    }
                }
            }
            wm.setRecentViewList(idList.toArray( new String[idList.size()]));
        }

        wm.setEditorAreaConstraints(wmc.editorAreaConstraints);
        wm.setEditorAreaState(wmc.editorAreaState);

        ModeImpl activeMode    = null;
        ModeImpl editorMaximizedMode = null;
        ModeImpl viewMaximizedMode = null;

        // First create empty modes.
        Map<ModeImpl, ModeConfig> mode2config = new HashMap<ModeImpl, ModeConfig>();

        for (int i = 0; i < wmc.modes.length; i++) {
            ModeConfig mc = (ModeConfig) wmc.modes[i];
            ModeImpl mode = getModeFromConfig(mc);

            mode2config.put(mode, mc);

            if(mc.name.equals(wmc.activeModeName)) {
                activeMode = mode;
            }
            if(mc.name.equals(wmc.editorMaximizedModeName)) {
                editorMaximizedMode = mode;
            } else if(mc.name.equals(wmc.viewMaximizedModeName)) {
                viewMaximizedMode = mode;
            }
        }

        TopComponentTracker tcTracker = TopComponentTracker.getDefault();
        TopComponent activeTopComponentOverride = null;
        // Then fill them with TopComponents.
        for(Iterator it = mode2config.keySet().iterator(); it.hasNext(); ) {
            ModeImpl mode = (ModeImpl)it.next();
            ModeConfig mc = (ModeConfig)mode2config.get(mode);
            initModeFromConfig(mode, mc, false);
            initPreviousModes(mode, mc, mode2config);

            // Set selected TopComponent.
            if(mc.selectedTopComponentID != null) {
                mode.setUnloadedSelectedTopComponent(mc.selectedTopComponentID);
            }
            if(mc.previousSelectedTopComponentID != null) {
                mode.setUnloadedPreviousSelectedTopComponent(mc.previousSelectedTopComponentID);
            }
            for( String tcId : mode.getTopComponentsIDs() ) {
                tcTracker.add( tcId, mode );
            }
            //some TopComponents want to be always active when the window system starts (e.g. welcome screen)
            for( TopComponent tc : mode.getOpenedTopComponents() ) {
                Object val = tc.getClientProperty( Constants.ACTIVATE_AT_STARTUP );
                if( null != val && val instanceof Boolean && ((Boolean)val).booleanValue() ) {
                    activeTopComponentOverride = tc;
                    break;
                }
            }
        }

        //Initialize top component groups
        for (int i = 0; i < wmc.groups.length; i++) {
            GroupConfig groupCfg = wmc.groups[i];
            createTopComponentGroupFromConfig(groupCfg);
        }

        // XXX #37188, 40237 Hot fix for the problem with initing active
        // component (even it is not on screen yet).
        if(activeMode != null) {
            TopComponent active = activeMode.getSelectedTopComponent();
            if(active != null) {
                WindowManagerImpl.getInstance().specialPersistenceCompShow(active);
            }
        }
        // active mode can be null, Active mode info is stored in winsys config (system layer) and modes in
        // project layer, that can cause out of synch state when switching projects...
        // setting null is however considered a valid state.
        wm.setActiveMode(activeMode);
        wm.setEditorMaximizedMode(editorMaximizedMode);
        wm.setViewMaximizedMode(viewMaximizedMode);

        Rectangle joinedBounds = computeBounds(
            wmc.centeredHorizontallyJoined,
            wmc.centeredVerticallyJoined,
            wmc.xJoined,
            wmc.yJoined,
            wmc.widthJoined,
            wmc.heightJoined,
            wmc.relativeXJoined,
            wmc.relativeYJoined,
            wmc.relativeWidthJoined,
            wmc.relativeHeightJoined);
        if(joinedBounds != null) {
            wm.setMainWindowBoundsJoined(joinedBounds);
        }
        // PENDING else { ...some default value?

        Rectangle separatedBounds = computeBounds(
            wmc.centeredHorizontallySeparated,
            wmc.centeredVerticallySeparated,
            wmc.xSeparated,
            wmc.ySeparated,
            wmc.widthSeparated,
            wmc.heightSeparated,
            wmc.relativeXSeparated,
            wmc.relativeYSeparated,
            wmc.relativeWidthSeparated,
            wmc.relativeHeightSeparated);
        if(separatedBounds != null) {
            wm.setMainWindowBoundsSeparated(separatedBounds);
        }
        // PENDING else { ...some default value?

        wm.setMainWindowFrameStateJoined(wmc.mainWindowFrameStateJoined);
        wm.setMainWindowFrameStateSeparated(wmc.mainWindowFrameStateSeparated);

        Rectangle absBounds = wmc.editorAreaBounds == null ? new Rectangle() : wmc.editorAreaBounds;
        Rectangle relBounds = wmc.editorAreaRelativeBounds == null ? new Rectangle() : wmc.editorAreaRelativeBounds;
        Rectangle bounds = computeBounds(false, false,
            absBounds.x,
            absBounds.y,
            absBounds.width,
            absBounds.height,
            relBounds.x / 100.0F,
            relBounds.y / 100.0F,
            relBounds.width / 100.0F,
            relBounds.height / 100.0F);
        wm.setEditorAreaBounds(bounds);
        wm.setEditorAreaFrameState(wmc.editorAreaFrameState);
        wm.setToolbarConfigName(wmc.toolbarConfiguration);
        if( null != activeTopComponentOverride )
            activeTopComponentOverride.requestActive();

        loaded = true;
    }


    /** Implements <code>WindowSystem</code> interface method. */
    public synchronized void save() {
        if( !loaded )
            return; //nothing to save
        if(DEBUG) {
            debugLog("## PersistenceHandler.save"); // NOI18N
        }
        TopComponentTracker.getDefault().save();

        ToolbarPool.getDefault().waitFinished();
        WindowManagerConfig wmc = getConfig();
        PersistenceManager.getDefault().saveWindowSystem(wmc);
    }

    private ModeImpl getModeFromConfig(ModeConfig mc) {
        if(DEBUG) {
            debugLog("Getting mode name=" + mc.name);
        }

        ModeImpl mode = (ModeImpl)WindowManagerImpl.getInstance().findMode(mc.name);
        if(mode == null) {
            mode = createModeFromConfig(mc);
        }
        return mode;
    }

    private ModeImpl createModeFromConfig(ModeConfig mc) {
        if(DEBUG) {
            debugLog(""); // NOI18N
            debugLog("Creating mode name=\"" + mc.name + "\""); // NOI18N
        }

        ModeImpl mode;
        if (mc.kind == Constants.MODE_KIND_SLIDING) {
            mode = WindowManagerImpl.getInstance().createSlidingMode(
                    mc.name, mc.permanent, mc.side, mc.slideInSizes);
        } else {
             mode = WindowManagerImpl.getInstance().createMode(
                mc.name, mc.kind, mc.state, mc.permanent, mc.constraints);
        }
        name2mode.put(mc.name, mode);
        if( mc.minimized )
            mode.setMinimized( mc.minimized );
        if( null != mc.otherNames ) {
            for( String s : mc.otherNames ) {
                mode.addOtherName( s );
            }
        }

        return mode;
    }

    /**
     * find the the previous mode for tc if exists and set it in the model..
     */
    private void initPreviousModes(ModeImpl mode, ModeConfig mc, Map modes) {
        for (int j = 0; j < mc.tcRefConfigs.length; j++) {
            TCRefConfig tcRefConfig = mc.tcRefConfigs[j];
            if(DEBUG) {
                debugLog("\tTopComponent[" + j + "] id=\"" // NOI18N
                    + tcRefConfig.tc_id + "\", \topened=" + tcRefConfig.opened); // NOI18N
            }
            if (tcRefConfig.previousMode != null) {
                Iterator it = modes.keySet().iterator();
                ModeImpl previous = null;
                while (it.hasNext()) {
                    ModeImpl md = (ModeImpl)it.next();

                    if (tcRefConfig.previousMode.equals(md.getName())
                            || md.getOtherNames().contains( tcRefConfig.previousMode ) ) {
                        previous = md;
                        break;
                    }
                }
                if (previous != null) {
                    WindowManagerImpl.getInstance().setPreviousModeForTopComponent(tcRefConfig.tc_id, mode, previous, tcRefConfig.previousIndex);
                } else {
                    Logger.getLogger(PersistenceHandler.class.getName()).log(Level.INFO, null,
                                      new java.lang.NullPointerException("Cannot find previous mode named \'" +
                                                                         tcRefConfig.previousMode +
                                                                         "\'"));

                }
            }
        }
    }

    /**
     * find the the previous mode for tc if exists and set it in the model..
     */
    private void initPreviousMode (ModeImpl mode, TCRefConfig tcRefConfig) {
        if(DEBUG) {
            debugLog("\tTopComponent id=\"" // NOI18N
                + tcRefConfig.tc_id + "\", \topened=" + tcRefConfig.opened); // NOI18N
        }
        if (tcRefConfig.previousMode == null) {
            return;
        }
        Set<? extends ModeImpl> modes = WindowManagerImpl.getInstance().getModes();
        Iterator it = modes.iterator();
        ModeImpl previous = null;
        while (it.hasNext()) {
            ModeImpl md = (ModeImpl) it.next();

            if (tcRefConfig.previousMode.equals(md.getName())
                    || md.getOtherNames().contains( tcRefConfig.previousMode ) ) {
                previous = md;
                break;
            }
        }
        if (previous != null) {
            WindowManagerImpl.getInstance().setPreviousModeForTopComponent
            (tcRefConfig.tc_id, mode, previous, tcRefConfig.previousIndex);
        } else {
            Logger.getLogger(PersistenceHandler.class.getName()).log(Level.INFO, null,
                              new java.lang.NullPointerException("Cannot find previous mode named \'" +
                                                                 tcRefConfig.previousMode +
                                                                 "\'"));
        }
    }

    private ModeImpl initModeFromConfig(ModeImpl mode, ModeConfig mc, boolean initPrevModes) {
        WindowManagerImpl wm = WindowManagerImpl.getInstance();
        if( null != mc.otherNames ) {
            for( String s : mc.otherNames ) {
                mode.addOtherName( s );
            }
        }
        for (int j = 0; j < mc.tcRefConfigs.length; j++) {
            TCRefConfig tcRefConfig = mc.tcRefConfigs[j];
            if(DEBUG) {
                debugLog("\tTopComponent[" + j + "] id=\"" // NOI18N
                    + tcRefConfig.tc_id + "\", \topened=" + tcRefConfig.opened); // NOI18N
            }

            // PENDING
            if (tcRefConfig.opened) {
                lazyLoader.lazyLoad(mode, mc.selectedTopComponentID, tcRefConfig, j );
            } else {
                mode.addUnloadedTopComponent(tcRefConfig.tc_id);
            }
            wm.setTopComponentDockedInMaximizedMode( tcRefConfig.tc_id, tcRefConfig.dockedInMaximizedMode );
            wm.setTopComponentSlidedInDefaultMode( tcRefConfig.tc_id, !tcRefConfig.dockedInDefaultMode );
            wm.setTopComponentMaximizedWhenSlidedIn( tcRefConfig.tc_id, tcRefConfig.slidedInMaximized );
            if (initPrevModes) {
                initPreviousMode(mode, tcRefConfig);
            }
        }

        // PENDING Refine the unneded computing.
        Rectangle absBounds = mc.bounds == null ? new Rectangle() : mc.bounds;
        Rectangle relBounds = mc.relativeBounds == null ? new Rectangle() : mc.relativeBounds;
        Rectangle bounds = computeBounds(false, false,
            absBounds.x,
            absBounds.y,
            absBounds.width,
            absBounds.height,
            relBounds.x / 100.0F,
            relBounds.y / 100.0F,
            relBounds.width / 100.0F,
            relBounds.height / 100.0F);
        mode.setBounds(bounds);
        mode.setFrameState(mc.frameState);
        mode.setMinimized( mc.minimized );

        return mode;
    }

    TopComponent getTopComponentForID(String tc_id, boolean deserialize) {
        if(tc_id == null || "".equals(tc_id)) {
            return null;
        }

//        long start = System.currentTimeMillis();
        TopComponent tc = PersistenceManager.getDefault().getTopComponentForID(tc_id, deserialize);
//        if(DEBUG) {
//            debugLog("***Getting TopComponent for ID=" + tc_id + " in " + (System.currentTimeMillis() - start) + " ms"); // NOI18N
//        }

        return tc;
    }

    private TopComponentGroupImpl createTopComponentGroupFromConfig(GroupConfig groupCfg) {
        if(DEBUG) {
            debugLog(""); // NOI18N
            debugLog("Creating group name=\"" + groupCfg.name + "\" \t[opened=" + groupCfg.opened + "]"); // NOI18N
        }

        TopComponentGroupImpl tcGroup = new TopComponentGroupImpl(groupCfg.name, groupCfg.opened);

        name2group.put(groupCfg.name, tcGroup);

        for (int j = 0; j < groupCfg.tcGroupConfigs.length; j++) {
            TCGroupConfig tcGroupCfg = groupCfg.tcGroupConfigs[j];
            if(DEBUG) {
                debugLog("\tTopComponent[" + j + "] id=\"" // NOI18N
                    + tcGroupCfg.tc_id + "\", \topen=" + tcGroupCfg.open + ", \tclose=" + tcGroupCfg.close
                    + ", \twasOpened=" + tcGroupCfg.wasOpened); // NOI18N
            }

            tcGroup.addUnloadedTopComponent(tcGroupCfg.tc_id);

            if (tcGroupCfg.open) {
                tcGroup.addUnloadedOpeningTopComponent(tcGroupCfg.tc_id);
            }
            if (tcGroupCfg.close) {
                tcGroup.addUnloadedClosingTopComponent(tcGroupCfg.tc_id);
            }

            // Handle also wasOpened flag.
            if(groupCfg.opened && tcGroupCfg.wasOpened) {
                tcGroup.addGroupUnloadedOpenedTopComponent(tcGroupCfg.tc_id);
            }
        }

        WindowManagerImpl.getInstance().addTopComponentGroup(tcGroup);

        return tcGroup;
    }



    private WindowManagerConfig getConfig() {
        WindowManagerConfig wmc = new WindowManagerConfig();

        wmc.preferredToolbarIconSize = ToolbarPool.getDefault().getPreferredIconSize();

        WindowManagerImpl wmi = WindowManagerImpl.getInstance();

        Rectangle joinedBounds = wmi.getMainWindowBoundsJoined();
        if(DEBUG) {
            debugLog("joinedBouds=" + joinedBounds); // NOI18N
        }
        wmc.xJoined      = joinedBounds.x;
        wmc.yJoined      = joinedBounds.y;
        wmc.widthJoined  = joinedBounds.width;
        wmc.heightJoined = joinedBounds.height;
        Rectangle separatedBounds = wmi.getMainWindowBoundsSeparated();
        if(DEBUG) {
            debugLog("separatedBounds=" + separatedBounds); // NOI18N
        }
        wmc.xSeparated      = separatedBounds.x;
        wmc.ySeparated      = separatedBounds.y;
        wmc.widthSeparated  = separatedBounds.width;
        wmc.heightSeparated = separatedBounds.height;

        if( Utilities.isMac() ) {
            //125881 - mac doesn't fire events when maximized window is resized by user
            wmc.mainWindowFrameStateJoined = wmi.getMainWindow().getExtendedState();
        } else {
            wmc.mainWindowFrameStateJoined = wmi.getMainWindowFrameStateJoined();
        }
        if (wmc.mainWindowFrameStateJoined == Frame.ICONIFIED) {
            // #46646 - don't save iconified state
            //mkleint - actually shoudn't we ignore the maximized states as well?
            wmc.mainWindowFrameStateJoined = Frame.NORMAL;
        }
        if(DEBUG) {
            debugLog("mainWindowFrameStateJoined=" + wmc.mainWindowFrameStateJoined); // NOI18N
        }
        wmc.mainWindowFrameStateSeparated = wmi.getMainWindowFrameStateSeparated();
        if (wmc.mainWindowFrameStateSeparated == Frame.ICONIFIED) {
            // #46646 - don't save iconified state
            wmc.mainWindowFrameStateSeparated = Frame.NORMAL;
        }
        if(DEBUG) {
            debugLog("mainWindowFrameStateSeparated=" + wmc.mainWindowFrameStateSeparated); // NOI18N
        }

        wmc.editorAreaState = wmi.getEditorAreaState();
        if(DEBUG) {
            debugLog("editorAreaState=" + wmc.editorAreaState); // NOI18N
        }
        wmc.editorAreaBounds = wmi.getEditorAreaBounds();
        if(DEBUG) {
            debugLog("editorAreaBounds=" + wmc.editorAreaBounds); // NOI18N
        }
        wmc.editorAreaConstraints = wmi.getEditorAreaConstraints();
        if(DEBUG) {
            debugLog("editorAreaConstraints=" + Arrays.toString(wmc.editorAreaConstraints)); // NOI18N
        }
        wmc.editorAreaFrameState = wmi.getEditorAreaFrameState();
        if(DEBUG) {
            debugLog("editorAreaFrameState=" + wmc.editorAreaFrameState); // NOI18N
        }
        wmc.screenSize = Toolkit.getDefaultToolkit().getScreenSize();

        ModeImpl mo = wmi.getActiveMode();
        if(DEBUG) {
            debugLog("active mode=" + mo); // NOI18N
        }
        if (mo != null) {
            wmc.activeModeName = mo.getName();
        }

        mo = wmi.getEditorMaximizedMode();
        if(DEBUG) {
            debugLog("editor maximized mode=" + mo); // NOI18N
        }
        if (mo != null) {
            wmc.editorMaximizedModeName = mo.getName();
        }

        mo = wmi.getViewMaximizedMode();
        if(DEBUG) {
            debugLog("view maximized mode=" + mo); // NOI18N
        }
        if (mo != null) {
            wmc.viewMaximizedModeName = mo.getName();
        }

        wmc.toolbarConfiguration = wmi.getToolbarConfigName();
        if(DEBUG) {
            debugLog("toolbarConfiguration=" + wmc.toolbarConfiguration); // NOI18N
        }

        // Modes.
        Set<? extends Mode> modeSet = wmi.getModes();
        List<ModeConfig> modeConfigs = new ArrayList<ModeConfig>(modeSet.size());
        for (Iterator<? extends Mode> it = modeSet.iterator(); it.hasNext(); ) {
            ModeImpl modeImpl = (ModeImpl)it.next();
            //Do not save empty non permanent mode
            ModeConfig mc = getConfigFromMode(modeImpl);
            if ((mc.tcRefConfigs.length == 0) && (!mc.permanent) && wmi.getCentral().doCheckSlidingModes(modeImpl)) {
                continue;
            }
            modeConfigs.add(mc);
        }
        wmc.modes = modeConfigs.toArray(new ModeConfig[0]);

        // TopComponent groups.
        Set<TopComponentGroupImpl> tcGroups = wmi.getTopComponentGroups();
        List<GroupConfig> groupConfigs = new ArrayList<GroupConfig>(tcGroups.size());
        for (Iterator<TopComponentGroupImpl> it = tcGroups.iterator(); it.hasNext(); ) {
            groupConfigs.add(getConfigFromGroup(it.next()));
        }
        wmc.groups = groupConfigs.toArray(new GroupConfig[0]);

        PersistenceManager pm = PersistenceManager.getDefault();
        //RecentViewList
        TopComponent [] tcs = wmi.getRecentViewList();
        List<String> tcIdList = new ArrayList<String>(tcs.length);
        for (int i = 0; i < tcs.length; i++) {
            if (pm.isTopComponentPersistent(tcs[i])) {
                String tc_id = WindowManager.getDefault().findTopComponentID(tcs[i]);
                tc_id = PersistenceManager.escapeTcId4XmlContent(tc_id);
                tcIdList.add(tc_id);
            }
        }
        wmc.tcIdViewList = tcIdList.toArray(new String [tcIdList.size()]);

        return wmc;
    }

    public ModeConfig getConfigFromMode(ModeImpl mode) {
        PersistenceManager pm = PersistenceManager.getDefault();
        WindowManagerImpl wm = WindowManagerImpl.getInstance();
        ModeConfig modeCfg = new ModeConfig();
        modeCfg.name = mode.getName();
        if(DEBUG) {
            debugLog(""); // NOI18N
            debugLog("mode name=" + modeCfg.name); // NOI18N
        }
        modeCfg.state = mode.getState();
        if(DEBUG) {
            debugLog("mode state=" + modeCfg.state); // NOI18N
        }

        modeCfg.kind = mode.getKind();
        if(DEBUG) {
            debugLog("mode kind=" + modeCfg.kind); // NOI18N
        }
        if (wm instanceof WindowManagerImpl) {
            modeCfg.side = wm.getCentral().getModeSide(mode);
            if( null != modeCfg.side ) {
                modeCfg.slideInSizes = wm.getCentral().getSlideInSizes( modeCfg.side );
            }
        }
        if(DEBUG) {
            debugLog("mode side=" + modeCfg.side); // NOI18N
        }

        modeCfg.constraints = mode.getConstraints();
        if(DEBUG) {
            debugLog("mode constraints=" + Arrays.toString(modeCfg.constraints)); // NOI18N
        }
        // PENDING Whether to save relative or absolute bounds.
        // In case of relative, they would need to be computed.
        Rectangle relBounds = null;
        if (relBounds != null) {
            modeCfg.relativeBounds = relBounds;
        } else {
            modeCfg.bounds = mode.getBounds();
            if(DEBUG) {
                debugLog("mode bounds=" + modeCfg.bounds); // NOI18N
            }
        }
        modeCfg.frameState = mode.getFrameState();
        if(DEBUG) {
            debugLog("mode frame state=" + modeCfg.frameState); // NOI18N
        }
        if( modeCfg.frameState == Frame.MAXIMIZED_BOTH && Utilities.isWindows() ) {
            //#200199 - when a window is maximized on MS Windows its bounds origin
            //are shifted -4 pixels horizontally and vertically for some reason
            //so the window location may seem to be out of screen on some multi-monitor setups
            if( modeCfg.bounds.x < 0 ) {
                modeCfg.bounds.x += 4;
                modeCfg.bounds.width -= 4;
            }
            if( modeCfg.bounds.y < 0 ) {
                modeCfg.bounds.y += 4;
                modeCfg.bounds.height -= 4;
            }

        }

        TopComponent selectedTC = mode.getSelectedTopComponent();
        if(selectedTC != null) {
            if (pm.isTopComponentPersistent(selectedTC)) {
                String tc_id = wm.findTopComponentID(selectedTC);
                if(DEBUG) {
                    debugLog("selected tc=" + selectedTC.getName()); // NOI18N
                }
                modeCfg.selectedTopComponentID = tc_id;
            }
        }
        modeCfg.permanent = mode.isPermanent();
        if(DEBUG) {
            debugLog("mode permanent=" + modeCfg.permanent); // NOI18N
        }

        String prevSelectedTCID = mode.getPreviousSelectedTopComponentID();
        if(prevSelectedTCID != null) {
            if(DEBUG) {
                debugLog("previous selected tc id=" + prevSelectedTCID); // NOI18N
            }
            modeCfg.previousSelectedTopComponentID = prevSelectedTCID;
        }

        modeCfg.minimized = mode.isMinimized();

        modeCfg.otherNames = mode.getOtherNames();

        // TopComponents:
        List<TCRefConfig> tcRefCfgList = new ArrayList<TCRefConfig>();
        List<String> openedTcIDs = mode.getOpenedTopComponentsIDs();
        for(Iterator it = mode.getTopComponentsIDs().iterator(); it.hasNext(); ) {
            String tcID = (String)it.next();
            //Filter tc we do not want to save
            boolean opened = openedTcIDs.contains(tcID);
            if (opened) {
                if (pm.isTopComponentNonPersistentForID(tcID)) {
                    continue;
                }
            } else {
                if (pm.isTopComponentNonPersistentForID(tcID)) {
                    continue;
                } else if (pm.isTopComponentPersistentOnlyOpenedForID(tcID)) {
                    continue;
                }
            }

            // #45981: save previous mode even for closed tcs
            String modeName = null;
            int prevIndex = -1;
            ModeImpl prev = wm.getPreviousModeForTopComponent(tcID, mode);
            if (prev != null) {
                modeName = prev.getName();
                prevIndex = wm.getPreviousIndexForTopComponent(tcID, mode);
            }

            if(DEBUG) {
                debugLog("tc ID=" + tcID + " opened=" + opened); // NOI18N
            }
            TCRefConfig tcRefCfg = new TCRefConfig();
            tcRefCfg.tc_id = tcID;
            tcRefCfg.opened = opened;
            tcRefCfg.previousMode = modeName;
            tcRefCfg.previousIndex = prevIndex;
            tcRefCfg.dockedInMaximizedMode = wm.isTopComponentDockedInMaximizedMode( tcID );
            tcRefCfg.dockedInDefaultMode = !wm.isTopComponentSlidedInDefaultMode( tcID );
            tcRefCfg.slidedInMaximized = wm.isTopComponentMaximizedWhenSlidedIn( tcID );
            tcRefCfgList.add(tcRefCfg);
        }

        modeCfg.tcRefConfigs = tcRefCfgList.toArray(new TCRefConfig[tcRefCfgList.size()]);
        return modeCfg;
    }

    private GroupConfig getConfigFromGroup(TopComponentGroupImpl tcGroup) {
        GroupConfig groupCfg = new GroupConfig();
        groupCfg.name = tcGroup.getName();
        groupCfg.opened = tcGroup.isOpened();
        if(DEBUG) {
            debugLog(""); // NOI18N
            debugLog("group name=" + groupCfg.name); // NOI18N
        }
        Set<String> openSet = tcGroup.getOpeningSetIDs();
        Set<String> closeSet = tcGroup.getClosingSetIDs();
        Set<String> wasOpenedSet = tcGroup.getGroupOpenedTopComponentsIDs();

        Map<String, TCGroupConfig> tcGroupCfgMap = new HashMap<String, TCGroupConfig>();

        for (String tcID: tcGroup.getTopComponentsIDs()) {

            TCGroupConfig tcGroupCfg;
            if (tcGroupCfgMap.containsKey(tcID)) {
                tcGroupCfg = tcGroupCfgMap.get(tcID);
            } else {
                tcGroupCfg = new TCGroupConfig();
                tcGroupCfg.tc_id = tcID;
                tcGroupCfgMap.put(tcID, tcGroupCfg);
            }

            tcGroupCfg.open  = openSet.contains(tcID);
            tcGroupCfg.close = closeSet.contains(tcID);
            if(groupCfg.opened) {
                tcGroupCfg.wasOpened = wasOpenedSet.contains(tcID);
            }
            if(DEBUG) {
                debugLog("tc id=" + tcGroupCfg.tc_id // NOI18N
                    + ", open=" + tcGroupCfg.open // NOI18N
                    + ", close=" + tcGroupCfg.close // NOI18N
                    + ", wasOpened=" + tcGroupCfg.wasOpened); // NOI18N
            }
        }

        groupCfg.tcGroupConfigs = tcGroupCfgMap.values().toArray(new TCGroupConfig[0]);
        return groupCfg;
    }


    /** Handles adding mode to model.
     * @param modeConfig configuration data of added mode
     */
    @Override
    public synchronized void modeConfigAdded(ModeConfig modeConfig) {
        if(DEBUG) {
            debugLog("WMI.modeConfigAdded mo:" + modeConfig.name); // NOI18N
        }
        ModeImpl mode = getModeFromConfig(modeConfig);
        initModeFromConfig(mode, modeConfig, true);
    }

    /** Handles removing mode from model.
     * @param modeName unique name of removed mode
     */
    @Override
    public synchronized void modeConfigRemoved(String modeName) {
        if(DEBUG) {
            debugLog("WMI.modeConfigRemoved mo:" + modeName); // NOI18N
        }
        ModeImpl mode = (ModeImpl)name2mode.remove(modeName);
        if(mode != null) {
            WindowManagerImpl.getInstance().removeMode(mode);
        } else {
            Logger.getLogger(PersistenceHandler.class.getName()).log(Level.WARNING, null,
                              new java.lang.NullPointerException("Mode for name=" +
                                                                 modeName +
                                                                 " was not created")); // NOI18N
        }
    }

    /** Handles adding tcRef to model.
     * @param modeName unique name of parent mode.
     * @param tcRefConfig configuration data of added tcRef
     * @param tcRefNames array of tcIds to pass ordering of new tcRef,
     * if there is no ordering defined tcRef is appended to end of array
     */
    @Override
    public synchronized void topComponentRefConfigAdded
    (String modeName, TCRefConfig tcRefConfig, String [] tcRefNames) {
        if(DEBUG) {
            debugLog("WMI.topComponentRefConfigAdded mo:" + modeName + " tcRef:" + tcRefConfig.tc_id); // NOI18N
        }

        WindowManagerImpl wm = WindowManagerImpl.getInstance();
        wm.setTopComponentDockedInMaximizedMode( tcRefConfig.tc_id, tcRefConfig.dockedInMaximizedMode );
        wm.setTopComponentSlidedInDefaultMode( tcRefConfig.tc_id, !tcRefConfig.dockedInDefaultMode );
        wm.setTopComponentMaximizedWhenSlidedIn( tcRefConfig.tc_id, tcRefConfig.slidedInMaximized );
        ModeImpl mode = (ModeImpl) name2mode.get(modeName);
        if( null == mode ) {
            for( ModeImpl m : name2mode.values() ) {
                if( m.getOtherNames().contains( modeName ) ) {
                    mode = m;
                    break;
                }
            }
        }
        if (mode != null) {
            initPreviousMode(mode, tcRefConfig);
        }

        TopComponent tc = getTopComponentForID(tcRefConfig.tc_id,true);
        if (tc != null) {
            if (mode != null) {
                if (tcRefConfig.opened) {
                    mode.addOpenedTopComponent(tc);
                } else {
                    mode.addClosedTopComponent(tc);
                }
            }
        }
    }

    /** Handles removing tcRef from model.
     * @param tc_id unique id of removed tcRef
     */
    @Override
    public synchronized void topComponentRefConfigRemoved(String tc_id) {
        if(DEBUG) {
            debugLog("WMI.topComponentRefConfigRemoved tcRef:" + tc_id); // NOI18N
        }

        WindowManagerImpl wm = WindowManagerImpl.getInstance();
        ModeImpl mode = wm.findModeForOpenedID(tc_id);
        if(mode != null) {
            TopComponent tc = getTopComponentForID(tc_id,true);
            if(tc != null) {
                mode.removeTopComponent(tc);
            }
        } else {
            mode = wm.findModeForClosedID(tc_id);
            if(mode != null) {
                mode.removeClosedTopComponentID(tc_id);
            }
        }
    }

    /** Handles adding group to model.
     * @param groupConfig configuration data of added group
     */
    @Override
    public synchronized void groupConfigAdded(GroupConfig groupConfig) {
        if(DEBUG) {
            debugLog("WMI.groupConfigAdded group:" + groupConfig.name); // NOI18N
        }
        createTopComponentGroupFromConfig(groupConfig);
    }

    /** Handles removing group from model.
     * @param groupName unique name of removed group
     */
    @Override
    public synchronized void groupConfigRemoved(String groupName) {
        if(DEBUG) {
            debugLog("WMI.groupConfigRemoved group:" + groupName); // NOI18N
        }
        TopComponentGroupImpl group = (TopComponentGroupImpl)name2group.remove(groupName);
        if(group != null) {
            WindowManagerImpl.getInstance().removeTopComponentGroup(group);
        } else {
            Logger.getLogger(PersistenceHandler.class.getName()).log(Level.WARNING, null,
                              new java.lang.NullPointerException("Null group for name=" +
                                                                 groupName));
        }
    }

    /** Handles adding tcGroup to model.
     * @param groupName unique name of parent group
     * @param tcGroupConfig configuration data of added tcGroup
     */
    @Override
    public synchronized void topComponentGroupConfigAdded(String groupName, TCGroupConfig tcGroupConfig) {
        if(DEBUG) {
            debugLog("WMI.topComponentGroupConfigAdded group:" + groupName + " tcGroup:" + tcGroupConfig.tc_id); // NOI18N
        }

        TopComponentGroupImpl group = (TopComponentGroupImpl)name2group.get(groupName);
        if(group != null) {
            group.addUnloadedTopComponent(tcGroupConfig.tc_id);
            if(tcGroupConfig.open) {
                group.addUnloadedOpeningTopComponent(tcGroupConfig.tc_id);
            }

            if(tcGroupConfig.close) {
                group.addUnloadedClosingTopComponent(tcGroupConfig.tc_id);
            }
        }
    }

    /** Handles removing tcGroup from model.
     * @param groupName unique name of parent group
     * @param tc_id unique id of removed tcGroup
     */
    @Override
    public synchronized void topComponentGroupConfigRemoved(String groupName, String tc_id) {
        if(DEBUG) {
            debugLog("WMI.topComponentGroupConfigRemoved group:" + groupName + " tcGroup:" + tc_id); // NOI18N
        }

        TopComponentGroupImpl group = (TopComponentGroupImpl)name2group.get(groupName);
        if(group != null) {
            group.removeUnloadedTopComponent(tc_id);
        }
    }

    /** Dump window manager configuration data to standard output. */
    private static String dumpConfig (WindowManagerConfig wmc) {
        StringBuffer buffer = new StringBuffer();
        buffer.append("\n-- wmc: [" + Integer.toHexString(System.identityHashCode(wmc)) + "]");
        buffer.append("\n-- JOINED --");
        buffer.append("\n-- x: " + wmc.xJoined);
        buffer.append("\n-- y: " + wmc.yJoined);
        buffer.append("\n--  width: " + wmc.widthJoined);
        buffer.append("\n-- height: " + wmc.heightJoined);
        buffer.append("\n--  relativeX: " + wmc.relativeXJoined);
        buffer.append("\n--  relativeY: " + wmc.relativeYJoined);
        buffer.append("\n--  relativeWidth: " + wmc.relativeWidthJoined);
        buffer.append("\n-- relativeHeight: " + wmc.relativeHeightJoined);
        buffer.append("\n-- centeredHorizontally: " + wmc.centeredHorizontallyJoined);
        buffer.append("\n--   centeredVertically: " + wmc.centeredVerticallyJoined);
        buffer.append("\n--    maximizeIfWidthBelowJoined: " + wmc.maximizeIfWidthBelowJoined);
        buffer.append("\n--   maximizeIfHeightBelowJoined: " + wmc.maximizeIfHeightBelowJoined);

        buffer.append("\n-- SEPARATED --");
        buffer.append("\n-- x: " + wmc.xSeparated);
        buffer.append("\n-- y: " + wmc.ySeparated);
        buffer.append("\n--  width: " + wmc.widthSeparated);
        buffer.append("\n-- height: " + wmc.heightSeparated);
        buffer.append("\n--  relativeX: " + wmc.relativeXSeparated);
        buffer.append("\n--  relativeY: " + wmc.relativeYSeparated);
        buffer.append("\n--  relativeWidth: " + wmc.relativeWidthSeparated);
        buffer.append("\n-- relativeHeight: " + wmc.relativeHeightSeparated);
        buffer.append("\n-- centeredHorizontally: " + wmc.centeredHorizontallySeparated);
        buffer.append("\n--   centeredVertically: " + wmc.centeredVerticallySeparated);

        buffer.append("\n-- editorAreaState: " + wmc.editorAreaState);
        if(wmc.editorAreaConstraints != null) {
            for (int i = 0; i < wmc.editorAreaConstraints.length; i++) {
                buffer.append("\n-- co[" + i + "]: " + wmc.editorAreaConstraints[i]);
            }
        }
        buffer.append("\n--         editorAreaBounds: " + wmc.editorAreaBounds);
        buffer.append("\n-- editorAreaRelativeBounds: " + wmc.editorAreaRelativeBounds);

        buffer.append("\n--     screenSize: " + wmc.screenSize);
        buffer.append("\n--    activeModeName: " + wmc.activeModeName);
        buffer.append("\n-- editorMaximizedModeName: " + wmc.editorMaximizedModeName);
        buffer.append("\n-- viewMaximizedModeName: " + wmc.viewMaximizedModeName);
        buffer.append("\n--     toolbarconfig: " + wmc.toolbarConfiguration);
        buffer.append("\n-- modes: " + Arrays.toString(wmc.modes) + " size " + (wmc.modes == null ? -1 : wmc.modes.length));
        for (int i = 0; i < wmc.modes.length; i++) {
            ModeConfig mc = wmc.modes[i];
            buffer.append("\n-- --");
            buffer.append("\n-- -- mode[" + i + "]: " + mc.name);
            buffer.append("\n-- -- state: " + mc.state + " "
            + ((mc.state == Constants.MODE_STATE_JOINED) ? "joined" : "separated"));
            if (mc.constraints != null) {
                buffer.append("\n-- -- constraints.sz: " + mc.constraints.length);
                for (int j = 0; j < mc.constraints.length; j++) {
                    buffer.append("\n-- -- co[" + j + "]: " + mc.constraints[j]);
                }
            }
            buffer.append("\n-- -- kind: " + mc.kind + " ");
            if (mc.kind == Constants.MODE_KIND_EDITOR) {
                buffer.append("editor");
            } else if (mc.kind == Constants.MODE_KIND_VIEW) {
                buffer.append("view");
            } else if (mc.kind == Constants.MODE_KIND_SLIDING) {
                buffer.append("sliding");
            } else {
                buffer.append("unknown");
            }
            buffer.append("\n-- --         bounds: " + mc.bounds);
            buffer.append("\n-- -- relativeBounds: " + mc.relativeBounds);
            buffer.append("\n-- --          state: " + mc.frameState);
            buffer.append("\n-- -- active-tc: " + mc.selectedTopComponentID);
            buffer.append("\n-- -- permanent: " + mc.permanent);
            if (mc.tcRefConfigs != null) {
                for (int k = 0; k < mc.tcRefConfigs.length; k++) {
                    TCRefConfig tcRefCfg = mc.tcRefConfigs[k];
                    buffer.append("\n++ ++ ++ tcRef[" + k + "]: " + tcRefCfg.tc_id);
                    buffer.append("\n++ ++ ++   opened: " + tcRefCfg.opened);
                }
            }
        }
        buffer.append("\n-- groups: " + Arrays.toString(wmc.groups) + " size " + (wmc.groups == null ? -1 : wmc.groups.length));
        for (int i = 0; i < wmc.groups.length; i++) {
            GroupConfig sc = wmc.groups[i];
            buffer.append("\n-- --");
            buffer.append("\n-- -- group[" + i + "]: " + sc.name);
            if (sc.tcGroupConfigs != null) {
                for (int k = 0; k < sc.tcGroupConfigs.length; k++) {
                    TCGroupConfig tcGroupCfg = sc.tcGroupConfigs[k];
                    buffer.append("\n++ ++ ++ tcGroup[" + k + "]: " + tcGroupCfg.tc_id);
                    buffer.append("\n++ ++ ++   open: " + tcGroupCfg.open);
                    buffer.append("\n++ ++ ++  close: " + tcGroupCfg.close);
                }
            }
        }

        return buffer.toString();
    }

    private static void debugLog(String message) {
        Debug.log(PersistenceHandler.class, message);
    }

    /**
     * @return False if the given point is not inside any screen device that are currently available.
     */
    private static boolean isOutOfScreen( int x, int y ) {
        GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
        GraphicsDevice[] gs = ge.getScreenDevices();
        for( int j=0; j<gs.length; j++ ) {
            GraphicsDevice gd = gs[j];
            if( gd.getType() != GraphicsDevice.TYPE_RASTER_SCREEN )
                continue;
            Rectangle bounds = gd.getDefaultConfiguration().getBounds();
            if( bounds.contains( x, y ) )
                return false;
        }
        return true;
    }

    public static Rectangle computeBounds(boolean centeredHorizontaly, boolean centeredVerticaly,
    int x, int y, int width, int height, float relativeX, float relativeY, float relativeWidth, float relativeHeight) {
        Rectangle bounds;
        if(width > 0 && height > 0) {
            // From absoute values.
            bounds = new Rectangle(x, y, width, height);
            // #33288 fix start- when screen resolution changes, some windows may get completely out of the screen.
            Rectangle screen = Utilities.getUsableScreenBounds();
            int xlimit = screen.x + screen.width - 20; // 20 = let's have some buffer area..
            int ylimit = screen.y + screen.height - 20; // 20 = let's have some buffer area..
            // will make sure that the out-of-screen windows get thrown in.
            if( isOutOfScreen( bounds.x, bounds.y ) ) {
                while (bounds.x > xlimit) {
                    bounds.x = Math.max(bounds.x - screen.width, screen.x);
                }
                while (bounds.y > ylimit) {
                    bounds.y = Math.max(bounds.y - ylimit, screen.y);
                }
            }
            // #33288 fix end
        } else if(relativeWidth > 0F && relativeHeight > 0F) {
            // From relative values.
            Rectangle screen = Utilities.getUsableScreenBounds();
            bounds = new Rectangle((int)(screen.width * relativeX), (int)(screen.height * relativeY),
                        (int)(screen.width * relativeWidth), (int)(screen.height * relativeHeight));
        } else {
            return null;
        }
        // Center the bounds if necessary.
        if(centeredHorizontaly || centeredVerticaly) {
            Rectangle centered = Utilities.findCenterBounds(new Dimension(bounds.width, bounds.height));
            if(centeredHorizontaly) {
                bounds.x = centered.x;
            }
            if(centeredVerticaly) {
                bounds.y = centered.y;
            }
        }

        return bounds;
    }

    void finishLazyLoading() {
        lazyLoader.loadAllNow();
    }

}
