/*
 * 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.netbeans.core.windows.model.ModelElement;
import org.openide.windows.TopComponent;

import java.awt.*;
import java.util.*;
import java.util.List;


/**
 * Snapshot of split structure in model.
 *
 * @author  Peter Zavadsky
 */
public class ModeStructureSnapshot {

    private final ElementSnapshot splitRootSnapshot;

    private final Set<ModeSnapshot> separateModeSnapshots;

    private final Set<SlidingModeSnapshot> slidingModeSnapshots;

    /** Creates a new instance of ModesModelSnapshot. */
    public ModeStructureSnapshot(ElementSnapshot splitRootSnapshot, 
            Set<ModeSnapshot> separateModeSnapshots,
            Set<SlidingModeSnapshot> slidingModeSnapshots) {
        this.splitRootSnapshot = splitRootSnapshot;
        this.separateModeSnapshots = separateModeSnapshots;
        this.slidingModeSnapshots = slidingModeSnapshots;
    }

    public ElementSnapshot getSplitRootSnapshot() {
        return splitRootSnapshot;
    }
    
    public ModeSnapshot[] getSeparateModeSnapshots() {
        return separateModeSnapshots.toArray(new ModeSnapshot[0]);
    }
    
    public SlidingModeSnapshot[] getSlidingModeSnapshots() {
        return slidingModeSnapshots.toArray(new SlidingModeSnapshot[0]);
    }

    /** @param name name of mode */
    public ModeSnapshot findModeSnapshot(String name) {
        ModeSnapshot ma = findModeSnapshotOfName(splitRootSnapshot, name);
        if(ma != null) {
            return ma;
        }
        
        for(Iterator it = separateModeSnapshots.iterator(); it.hasNext(); ) {
            ma = (ModeSnapshot)it.next();
            if(name.equals(ma.getName())) {
                return ma;
            }
        }
        
        for(Iterator it = slidingModeSnapshots.iterator(); it.hasNext(); ) {
            ma = (SlidingModeSnapshot)it.next();
            if(name.equals(ma.getName())) {
                return ma;
            }
        }
        
        return null;
    }
    
    private static ModeSnapshot findModeSnapshotOfName(ElementSnapshot snapshot, String name) {
        if(snapshot instanceof ModeSnapshot) {
            ModeSnapshot ma = (ModeSnapshot)snapshot;
            if(name.equals(ma.getName())) {
                return ma;
            }
        } else if(snapshot instanceof SplitSnapshot) {
            SplitSnapshot split = (SplitSnapshot)snapshot; 
            for(Iterator it = split.getChildSnapshots().iterator(); it.hasNext(); ) {
                ElementSnapshot child = (ElementSnapshot)it.next();
                ModeSnapshot ma = findModeSnapshotOfName(child, name);
                if(ma != null) {
                    return ma;
                }
            }
        } else if(snapshot instanceof EditorSnapshot) {
            EditorSnapshot editorSnapshot = (EditorSnapshot)snapshot;
            ModeSnapshot ma = findModeSnapshotOfName(editorSnapshot.getEditorAreaSnapshot(), name);
            if(ma != null) {
                return ma;
            }
        }
        
        return null;
    }
    
    /** Superclass for snapshot of model element.
     * There are three types, split, mode, and editor (represents editor area) type. */
    public static abstract class ElementSnapshot {
        // PENDING revise
        /** Corresponding object in model (SplitNode or ModeNode or ModeImpl for separate mode). */
        private final ModelElement originator;
        
        private /*final*/ SplitSnapshot parent;

        
        public ElementSnapshot(ModelElement originator, SplitSnapshot parent) {
            this.originator = originator;
            setParent(parent);
        }

        /** Gets originator object. Used only in model. */
        public ModelElement getOriginator() {
            return originator;
        }
        
        public void setParent(SplitSnapshot parent) {
            if(this.parent == null) {
                this.parent = parent;
            } else {
                throw new IllegalStateException("Parent can be set only once," // NOI18N
                    + " this.parent=" + this.parent + ", parent=" + parent); // NOI18N
            }
        }
        
        public SplitSnapshot getParent() {
            return parent;
        }
        
        public boolean originatorEquals(ElementSnapshot o) {
            return getClass().equals(o.getClass()) // To prevent mismatch between split and mode snapshot.
                                                   // Split has now originator corresponding to first child.
                && ((ElementSnapshot)o).originator == originator;
        }

        public abstract double getResizeWeight();
        
        /** Indicates whether component represented by this element is visible or not. */
        public abstract boolean isVisibleInSplit();

        /** Indicates whether there is at least one visible descendant (split relevant). */
        public abstract boolean hasVisibleDescendant();
        
        public String toString() {
            return "Snapshot[originatorHash=" + (originator != null ? Integer.toHexString(originator.hashCode()) : "null" ) + "]"; // NOI18N
        }
    }
    
    /** */ 
    public static class SplitSnapshot extends ElementSnapshot {
        private final int orientation;
        private final List<ElementSnapshot> childSnapshots = new ArrayList<ElementSnapshot>();
        private final Map<ElementSnapshot, Double> childSnapshot2splitWeight = new HashMap<ElementSnapshot, Double>();
        private final double resizeWeight;
        
        public SplitSnapshot(ModelElement originator, SplitSnapshot parent, int orientation,
                List<ElementSnapshot> childSnapshots, 
                Map<ElementSnapshot, Double> childSnapshot2splitWeight, double resizeWeight) {
            super(originator, parent); // XXX PENDING originator corresponds to the first child model element.
            
            this.orientation = orientation;
            this.childSnapshots.addAll(childSnapshots);
            this.childSnapshot2splitWeight.putAll(childSnapshot2splitWeight);
            this.resizeWeight = resizeWeight;
        }

        public int getOrientation() {
            return orientation;
        }
        
        public List getVisibleChildSnapshots() {
            List<ElementSnapshot> l = getChildSnapshots();
            for(Iterator<ElementSnapshot> it = l.iterator(); it.hasNext(); ) {
                ElementSnapshot child = it.next();
                if(!child.hasVisibleDescendant()) {
                    it.remove();
                }
            }
            
            return l;
        }
        
        public List<ElementSnapshot> getChildSnapshots() {
            return new ArrayList<ElementSnapshot>(childSnapshots);
        }
        
        public double getChildSnapshotSplitWeight(ElementSnapshot childSnapshot) {
            Double d = (Double)childSnapshot2splitWeight.get(childSnapshot);
            return d == null ? -1 : d.doubleValue();
        }
        
        public double getResizeWeight() {
            return resizeWeight;
        }
        
        /** Indicates whether component represented by this node is visible or not. */
        public boolean isVisibleInSplit() {
            int count = 0;
            for(Iterator it = getChildSnapshots().iterator(); it.hasNext(); ) {
                ElementSnapshot child = (ElementSnapshot)it.next();
                if(child.hasVisibleDescendant()) {
                    count++;
                    // At leas two are needed so the split is showing.
                    if(count >= 2 
                            //#124847
                            || (Constants.SWITCH_HIDE_EMPTY_DOCUMENT_AREA 
                                && getParent() == null && count==1)) {
                        return true;
                    }
                }
            }
            
            return false;
        }

        /** Indicates whether there is at least one visible descendant. */
        public boolean hasVisibleDescendant() {
            for(Iterator it = getChildSnapshots().iterator(); it.hasNext(); ) {
                ElementSnapshot child = (ElementSnapshot)it.next();
                if(child.hasVisibleDescendant()) {
                    return true;
                }
            }
            
            return false;
        }
        
        public String toString() {
            return super.toString() + "[orientation=" // NOI18N
                + (orientation == Constants.HORIZONTAL ? "horizontal" : "vertical") // NOI18N
                + "]"; // NOI18N
        }
       
    }

    /** */
    public static class ModeSnapshot extends ElementSnapshot { 
        private final ModeImpl mode;
        
        private final String name;
        private final int state;
        private final int kind;
        private final Rectangle bounds;
        private final int frameState;
        private final TopComponent selectedTopComponent;
        private final TopComponent[] openedTopComponents;
        private final double resizeWeight;
        
        public ModeSnapshot(ModelElement originator, SplitSnapshot parent, ModeImpl mode, double resizeWeight) {
            super(originator, parent);
            
            this.mode = mode;

            this.name = mode.getName();
            this.state = mode.getState();
            this.kind = mode.getKind();
            this.bounds = mode.getBounds();
            this.frameState = mode.getFrameState();
            this.selectedTopComponent = mode.getSelectedTopComponent();
            this.openedTopComponents = mode.getOpenedTopComponents().toArray(new TopComponent[0]);
            this.resizeWeight = resizeWeight;
        }
        
        
        public boolean originatorEquals(ElementSnapshot o) {
            if(!super.originatorEquals(o)) {
                return false;
            }
            
            // XXX Even if originators are same, they differ if their states are different.
            // Difference -> split vs. separate representations.
            ModeSnapshot me = (ModeSnapshot)o;
            return getState() == me.getState();
        }
        
        public ModeImpl getMode() {
            return mode;
        }
        
        public String getName() {
            return name;
        }

        public int getState() {
            return state;
        }

        public int getKind() {
            return kind;
        }

        public Rectangle getBounds() {
            return bounds;
        }

        public int getFrameState() {
            return frameState;
        }

        public TopComponent getSelectedTopComponent() {
            return selectedTopComponent;
        }

        public TopComponent[] getOpenedTopComponents() {
            return openedTopComponents;
        }
        
        public double getResizeWeight() {
            return resizeWeight;
        }

        public boolean isVisibleInSplit() {
            if(getOpenedTopComponents().length == 0) {
                return false;
            }

            if(getState() == Constants.MODE_STATE_SEPARATED) {
                return false;
            }
            
            if(mode.getKind() == Constants.MODE_KIND_EDITOR ) {
                WindowManagerImpl wm = WindowManagerImpl.getInstance();
                if( null != wm.getEditorMaximizedMode() && wm.getEditorMaximizedMode() != mode )
                    return false;
            }

            return true;
        }
        
        public boolean isVisibleSeparate() {
            if(getOpenedTopComponents().length == 0) {
                return false;
            }
            
            if(getState() == Constants.MODE_STATE_JOINED) {
                return false;
            }
            
            return true;
        }
        
        public boolean hasVisibleDescendant() {
            return isVisibleInSplit();
        }
        
        public String toString() {
            return super.toString() + "[name=" + mode.getName() + ", permanent=" + mode.isPermanent() // NOI18N
                + ", constraints=" + Arrays.asList(mode.getConstraints()) + "]"; // NOI18N
        }

    }
    
    public static class SlidingModeSnapshot extends ModeSnapshot { 

        private final String side;
        private final Map<TopComponent,Integer> slideInSizes;
        
        public SlidingModeSnapshot(ModeImpl mode, String side, Map<TopComponent,Integer> slideInSizes) {
            super(null, null, mode, 0D);
            
            this.side = side;
            this.slideInSizes = slideInSizes;
        }
        
        public String getSide () {
            return side;
        }
        
        public Map<TopComponent,Integer> getSlideInSizes() {
            return slideInSizes;
        }
    } // end of SlidingModeSnapshot
    
    
    /** */
    public static class EditorSnapshot extends ElementSnapshot {
        private final ModeStructureSnapshot.ElementSnapshot editorAreaSnapshot;
        private final double resizeWeight;
        
        public EditorSnapshot(ModelElement originator, SplitSnapshot parent,
        ElementSnapshot editorAreaSnapshot, double resizeWeight) {
            super(originator, parent);
            
            this.editorAreaSnapshot = editorAreaSnapshot;
            this.resizeWeight = resizeWeight;
        }
        
        public double getResizeWeight() {
            return resizeWeight;
        }
        
        public ElementSnapshot getEditorAreaSnapshot() {
            return editorAreaSnapshot;
        }

        /** Indicates whether component represented by this node is visible or not. */
        public boolean isVisibleInSplit() {
            if(Constants.SWITCH_HIDE_EMPTY_DOCUMENT_AREA) {
                return null != editorAreaSnapshot && editorAreaSnapshot.isVisibleInSplit();
            } else {
                return true;
            }
        }

        /** Indicates whether there is at least one visible descendant. */
        public boolean hasVisibleDescendant() {
            return isVisibleInSplit();
        }
        
        public String toString() {
            return super.toString() + "\n" + editorAreaSnapshot; // NOI18N
        }
    }

    
    public String toString() {
        StringBuffer sb = new StringBuffer();
        sb.append("\nModesSnapshot hashCode=" + Integer.toHexString(hashCode())); // NOI18N
        sb.append("\nSplit modes:\n"); // NOI18N
        sb.append(dumpSnapshot(splitRootSnapshot, 0)); 
        sb.append("\nSeparate Modes:"); // NOI18N
        sb.append(dumpSet(separateModeSnapshots));
        return sb.toString();
    }
    
    private static String dumpSnapshot(ElementSnapshot snapshot, int indent) {
        StringBuffer sb = new StringBuffer();
        String indentString = createIndentString(indent);
        
        if(snapshot instanceof SplitSnapshot) {
            SplitSnapshot splitSnapshot = (SplitSnapshot)snapshot;
            sb.append(indentString + "split="+splitSnapshot); // NOI18N
            indent++;
            for(Iterator it = splitSnapshot.getChildSnapshots().iterator(); it.hasNext(); ) {
                ElementSnapshot child = (ElementSnapshot)it.next();
                sb.append("\n" + dumpSnapshot(child, indent)); // NOI18N
            }
        } else if(snapshot instanceof ModeSnapshot) {
            sb.append(indentString + "mode=" + snapshot); // NOI18N
        } else if(snapshot instanceof EditorSnapshot) {
            sb.append(indentString + "editor=" + snapshot); // NOI18N
            sb.append(dumpSnapshot(((EditorSnapshot)snapshot).getEditorAreaSnapshot(), ++indent));
        }
        
        return sb.toString();
    }
    
    private static String createIndentString(int indent) {
        StringBuffer sb = new StringBuffer(indent);
        for(int i = 0; i < indent; i++) {
            sb.append(' ');
        }
        
        return sb.toString();
    }
    
    private static String dumpSet(Set separateModes) {
        StringBuffer sb = new StringBuffer();
        
        for(java.util.Iterator it = separateModes.iterator(); it.hasNext(); ) {
            sb.append("\nmode=" + it.next());
        }
        
        return sb.toString();
    }
    
}

