/*
 * 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.modules.palette;

import java.awt.event.ActionEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeSupport;
import java.util.ArrayList;
import javax.swing.Action;
import javax.swing.SwingUtilities;
import org.netbeans.spi.palette.DragAndDropHandler;
import org.netbeans.spi.palette.PaletteController;
import org.openide.nodes.*;
import org.openide.util.*;
import org.netbeans.modules.palette.ui.Customizer;
import org.netbeans.spi.palette.PaletteActions;

/**
 * Default implementation of PaletteModel interface based on Nodes.
 *
 * @author S. Aubrecht
 */
public class DefaultModel implements Model, NodeListener {
    
    /**
     * Palette's root node. Its subnodes are palette categories.
     */
    private RootNode rootNode;
    
    /**
     * Item currently selected in the palette or null.
     */
    private Item selectedItem;
    /**
     * Category that owns the selected item or null.
     */
    private Category selectedCategory;
    private PropertyChangeSupport propertySupport;
    private ArrayList<ModelListener> modelListeners = new ArrayList<ModelListener>( 3 );
    private boolean categoriesNeedRefresh = true;
    /**
     * Cached categories
     */
    private Category[] categories;
    
    /** 
     * Creates a new instance of DefaultPaletteModel 
     *
     * @param rootNode Palette's root node.
     */
    public DefaultModel( RootNode rootNode ) {
        this.rootNode = rootNode;
        
        propertySupport = new PropertyChangeSupport( this );
        this.rootNode.addNodeListener( this );
    }

    public void setSelectedItem( Lookup category, Lookup item ) {
        Category cat = null;
        Item it = null;
        if( null != category ) {
            Node catNode = category.lookup( Node.class );
            if( null != catNode ) {
                cat = findCategory( catNode );
            }
        }
        if( null != item && null != cat ) {
            Node itNode = item.lookup( Node.class );
            if( null != itNode ) {
                it = findItem( cat, itNode );//new DefaultItem( itNode );
            }
        }
        
        Item oldValue = selectedItem;
        this.selectedItem = it;
        this.selectedCategory = cat;
        propertySupport.firePropertyChange( Model.PROP_SELECTED_ITEM, oldValue, selectedItem );
    }
    
    public void clearSelection() {
        setSelectedItem( null, null );
    }
    public Action[] getActions() {
        return rootNode.getActions( false );
    }

    public Item getSelectedItem() {
        return selectedItem;
    }
    
    public Category getSelectedCategory() {
        return selectedCategory;
    }

    public void addModelListener( ModelListener listener ) {
        synchronized( modelListeners ) {
            modelListeners.add( listener );
            propertySupport.addPropertyChangeListener( listener );
        }
    }

    public void removeModelListener( ModelListener listener ) {
        synchronized( modelListeners ) {
            modelListeners.remove( listener );
            propertySupport.removePropertyChangeListener( listener );
        }
    }

    
    public synchronized Category[] getCategories() {
        if( null == categories || categoriesNeedRefresh ) {
            Node[] nodes = rootNode.getChildren().getNodes( canBlock() );
            categories = nodes2categories( nodes );
            categoriesNeedRefresh = false;
        }
        return categories;
    }
    
    public static boolean canBlock() {
        return !Children.MUTEX.isReadAccess() && !Children.MUTEX.isWriteAccess ();
    }

    /** Fired when a set of new children is added.
    * @param ev event describing the action
    */
    public void childrenAdded(NodeMemberEvent ev) {
        categoriesNeedRefresh = true;
        if( isRefreshingChildren )
            return;
        final Node[] nodes = ev.getDelta();
        SwingUtilities.invokeLater( new Runnable() {
            public void run() {
                Category[] addedCategories = findCategories( nodes );
                fireCategoriesChanged( addedCategories, true );
            }
        });
    }

    /** Fired when a set of children is removed.
    * @param ev event describing the action
    */
    public void childrenRemoved(NodeMemberEvent ev) {
        categoriesNeedRefresh = true;
        final Node[] nodes = ev.getDelta();
        SwingUtilities.invokeLater( new Runnable() {
            public void run() {
                Category[] removedCategories = findCategories( nodes );
                fireCategoriesChanged( removedCategories, false );
            }
        });
    }

    /** Fired when the order of children is changed.
    * @param ev event describing the change
    */
    public void childrenReordered(NodeReorderEvent ev) {
        categoriesNeedRefresh = true;
        fireCategoriesChanged( null, false );
    }

    /** Fired when the node is deleted.
    * @param ev event describing the node
    */
    public synchronized void nodeDestroyed(NodeEvent ev) {
        this.rootNode.removeNodeListener( this );
    }

    public void propertyChange(PropertyChangeEvent evt) {
    }

    private void fireCategoriesChanged( Category[] changedCategories, boolean added ) {
        ModelListener[] listeners;
        synchronized( modelListeners ) {
            listeners = new ModelListener[modelListeners.size()];
            listeners = modelListeners.toArray( listeners );
        }
        for( int i=0; i<listeners.length; i++ ) {
            if( null != changedCategories ) {
                if( added ) {
                    listeners[i].categoriesAdded( changedCategories );
                } else {
                    listeners[i].categoriesRemoved( changedCategories );
                }
            } else {
                listeners[i].categoriesReordered();
            }
        }
    }

    /**
     * Wrap the given Nodes in PaletteCategory instances.
     */
    private Category[] nodes2categories( Node[] nodes ) {
        Category[] res = new Category[ nodes.length ];
        
        for( int i=0; i<res.length; i++ ) {
            res[i] = new DefaultCategory( nodes[i] );
        }
        
        return res;
    }
    
    private Category[] findCategories( Node[] nodes ) {
        Category[] res = new Category[ nodes.length ];

        Category[] current = getCategories();
        for( int i=0; i<res.length; i++ ) {
            boolean found = false;
            for( int j=0; !found && null != current && j<current.length; j++ ) {
                Node catNode = current[j].getLookup().lookup( Node.class );
                if( nodes[i].equals( catNode ) ) {
                    res[i] = current[j];
                    found = true;
                }
            }
            if( !found ) {
                res[i] = new DefaultCategory( nodes[i] );
            }
        }

        return res;
    }

    private boolean isRefreshingChildren = false;
    public void refresh() {
        synchronized( rootNode ) {
            PaletteActions customActions = rootNode.getLookup().lookup( PaletteActions.class );
            Action customRefreshAction = customActions.getRefreshAction();
            if( null != customRefreshAction ) {
                customRefreshAction.actionPerformed( new ActionEvent( getRoot(), 0, "refresh" ) ); //NOI18N
            }
            clearSelection();
            categoriesNeedRefresh = true;
            isRefreshingChildren = true;
            try {
                rootNode.refreshChildren();
            } finally {
                isRefreshingChildren = false;
            }
            fireCategoriesChanged( null, false );
        }
    }
    
    public void showCustomizer( PaletteController controller, Settings settings ) {
        Customizer.show( rootNode, controller, settings );
    }
    
    public Lookup getRoot() {
        return rootNode.getLookup();
    }
    
    public boolean moveCategory( Category source, Category target, boolean moveBefore ) {
        int targetIndex = categoryToIndex( target );
        if( !moveBefore ) {
            targetIndex++;
        }
        DragAndDropHandler handler = getDragAndDropHandler();
        return handler.moveCategory( source.getLookup(), targetIndex );
    }

    private int categoryToIndex( Category category ) {
        Node node = category.getLookup().lookup( Node.class );
        if( null != node ) {
            Index order = rootNode.getCookie( Index.class );
            if( null != order ) {
                return order.indexOf( node );
            }
        }
        return -1;
    }

    public String getName() {
        return rootNode.getName();
    }

    private Category findCategory( Node node ) {
        Category[] cats = getCategories();
        for( int i=0; i<cats.length; i++ ) {
            Node catNode = cats[i].getLookup().lookup( Node.class );
            if( null != catNode && catNode.equals( node ) )
                return cats[i];
        }
        return null;
    }
    
    private Item findItem( Category category, Node node ) {
        Item[] items = category.getItems();
        for( int i=0; i<items.length; i++ ) {
            Node itNode = items[i].getLookup().lookup( Node.class );
            if( null != itNode && itNode.equals( node ) )
                return items[i];
        }
        return null;
    }

    public boolean canReorderCategories() {
        return getDragAndDropHandler().canReorderCategories( rootNode.getLookup() );
    }
    
    private DragAndDropHandler getDragAndDropHandler() {
        return rootNode.getLookup().lookup( DragAndDropHandler.class );
    }
}
