/*
 * Copyright (c) 2007 BUSINESS OBJECTS SOFTWARE LIMITED
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 *     * Redistributions of source code must retain the above copyright notice,
 *       this list of conditions and the following disclaimer.
 *  
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *  
 *     * Neither the name of Business Objects nor the names of its contributors
 *       may be used to endorse or promote products derived from this software
 *       without specific prior written permission.
 *  
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */


/*
 * NavigatorAdapter.java
 * Creation date: Jul 23, 2003
 * By: Frank Worsley
 */
package org.openquark.gems.client;

import java.util.Hashtable;
import java.util.List;
import java.util.Locale;
import java.util.Set;

import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;
import javax.swing.event.UndoableEditListener;
import javax.swing.tree.TreeNode;
import javax.swing.undo.StateEdit;
import javax.swing.undo.StateEditable;

import org.openquark.cal.compiler.ScopedEntity;
import org.openquark.cal.compiler.TypeChecker;
import org.openquark.cal.metadata.ArgumentMetadata;
import org.openquark.cal.metadata.CALFeatureMetadata;
import org.openquark.cal.metadata.FunctionalAgentMetadata;
import org.openquark.cal.services.GemEntity;
import org.openquark.cal.services.MetaModule;
import org.openquark.cal.services.Perspective;
import org.openquark.gems.client.Gem.PartInput;
import org.openquark.gems.client.explorer.TableTopExplorer;
import org.openquark.gems.client.navigator.NavAddress;
import org.openquark.gems.client.navigator.NavAddressHelper;
import org.openquark.gems.client.navigator.NavFrame;
import org.openquark.gems.client.navigator.NavFrameOwner;
import org.openquark.gems.client.navigator.NavAddress.NavAddressMethod;
import org.openquark.gems.client.utilities.ExtendedUndoableEditSupport;
import org.openquark.util.Pair;


/**
 * The GemCutter's implementation of the NavFrameOwner interface. This class is basically
 * a thin wrapper around the NavFrame class. It delegates to an instance of NavFrame for all
 * functionality and simply adds undo support on top of NavFrame.
 * 
 * @author Frank Worsley
 */
class NavigatorAdapter implements NavFrameOwner {

    /**
     * Implements a state editable for metadata objects.
     * @author Frank Worsley
     */
    private class MetadataStateEditable implements StateEditable {

        /** The metadata key in the state maps. */
        private static final String METADATA_STATE_KEY = "MetadataStateKey"; 

        /** The address of the metadata object this editable is for. */
        private final NavAddress address;

        /**
         * Constructs a new state editable for the given metadata object.
         * @param address the address of the metadata object
         */
        public MetadataStateEditable(NavAddress address) {

            if (address == null) {
                throw new NullPointerException();
            }
        
            this.address = address;
        }

        /**
         * @see javax.swing.undo.StateEditable#storeState(java.util.Hashtable)
         */
        public void storeState(Hashtable<Object, Object> state) {
            CALFeatureMetadata metadata = NavAddressHelper.getMetadata(NavigatorAdapter.this, address);            
            state.put(getKey(), metadata);
        }

        /**
         * @see javax.swing.undo.StateEditable#restoreState(java.util.Hashtable)
         */
        public void restoreState(Hashtable<?, ?> state) {

            CALFeatureMetadata savedState = (CALFeatureMetadata) state.get(getKey());
        
            if (savedState != null) {
            
                boolean success = NavAddressHelper.saveMetadata(NavigatorAdapter.this, address, savedState);
                
                if (!success) {
                    throw new IllegalStateException("failed to restore saved metadata state");
                }

                // Update the state edit if the currently edited metadata was changed.
                if (address.equals(currentEditAddress)) {
                    stateEdit = new StateEdit(new MetadataStateEditable(address));
                }

                // Check the table top names once the undo completes.
                // We need to do it after the undo so GemGraph checking is enabled again.
                SwingUtilities.invokeLater(new Runnable() {
                    public void run() {
                        updateInputNames(address);
                        nameCheckTableTop();
                        navFrame.refreshMetadata(address);
                    }
                });
            }
        }
    
        /**
         * @return the key to use in the state maps for the metadata
         */
        private Object getKey() {
            return new Pair<NavAddress, String>(address, METADATA_STATE_KEY);
        }
    }

    
    /** The undoable edit support for the navigator adapter. */
    private final ExtendedUndoableEditSupport undoableEditSupport;

    /** The GemCutter this adapter is for. */
    private final GemCutter gemCutter;

    /** The navigator frame this adapter is using. */
    private NavFrame navFrame = null;

    /** The state edit for the metadata object being edited. */
    private StateEdit stateEdit = null;
    
    /** The address of the metadata object currently being edited. */
    private NavAddress currentEditAddress = null;

    /**
     * Constructor for a new navigator adapter for a GemCutter.
     * @param gemCutter the GemCutter this adapter is for
     */
    public NavigatorAdapter(GemCutter gemCutter) {
        
        if (gemCutter == null) {
            throw new NullPointerException();
        }
        
        this.gemCutter = gemCutter;
        this.undoableEditSupport = new ExtendedUndoableEditSupport(this);
    }

    /**
     * Add an undoable edit listener.
     * @param listener the listener to add
     */
    public void addUndoableEditListener(UndoableEditListener listener) {
        undoableEditSupport.addUndoableEditListener(listener);
    }
    
    /**
     * Remove an undoable edit listener.
     * @param listener the listener to remove
     */
    public void removeUndoableEditListener(UndoableEditListener listener) {
        undoableEditSupport.removeUndoableEditListener(listener);
    }

    /**
     * @return whether or not the navigator instance if visible on screen
     */
    private boolean navigatorCanView() {
        return navFrame.isVisible() && !navFrame.isEditing();
    }

    /**
     * Refreshed the navigatior tree by reloading the CAL entities from the perspective.
     * This has to be done after a gem is saved so that the new gem shows up in the tree.
     */
    public void refresh() {
        
        if (navFrame == null) {
            navFrame = new NavFrame(this);
        }
        
        navFrame.refreshTree();
    }
    
    /**
     * @see org.openquark.gems.client.navigator.NavFrameOwner#getParent()
     */
    public JFrame getParent() {
        return gemCutter;
    }

    /**
     * @see org.openquark.gems.client.navigator.NavFrameOwner#getTypeChecker()
     */
    public TypeChecker getTypeChecker() {
        return gemCutter.getTypeChecker();
    }

    /**
     * @see org.openquark.gems.client.navigator.NavFrameOwner#getValueRunner()
     */
    public ValueRunner getValueRunner() {
        return gemCutter.getValueRunner();
    }

    /**
     * @see org.openquark.gems.client.navigator.NavFrameOwner#getPerspective()
     */
    public Perspective getPerspective() {
        return gemCutter.getPerspective();
    }

    /**
     * @see org.openquark.gems.client.navigator.NavFrameOwner#getCollector(java.lang.String)
     */
    public CollectorGem getCollector(String name) {
        Set<CollectorGem> collectors = gemCutter.getTableTop().getGemGraph().getCollectorsForName(name);
        return (collectors.size() == 0) ? null : collectors.iterator().next();
    }

    /**
     * @see org.openquark.gems.client.navigator.NavFrameOwner#displayNavigator(boolean)
     */
    public void displayNavigator(boolean newNavigator) {
        navFrame.setVisible(true);
        navFrame.refreshViewer();
        navFrame.requestFocus();
    }

    /**
     * @see org.openquark.gems.client.navigator.NavFrameOwner#displayMetadata(org.openquark.gems.client.navigator.NavAddress, boolean)
     */
    public void displayMetadata(NavAddress address, boolean newViewer) {
        if (navigatorCanView() || newViewer) {
            navFrame.displayMetadata(address);
        }
    }

    /**
     * Display metadata for the given entity.
     * @param entity the entity to display metadata for
     * @param newViewer true if a new window should be opened
     */
    public void displayMetadata(GemEntity entity, boolean newViewer) {
        displayMetadata(NavAddress.getAddress(entity), newViewer);
    }

    /**
     * Display metadata for the given entity.
     * @param entity the entity to display metadata for
     * @param newViewer true if a new window should be opened
     */
    public void displayMetadata(ScopedEntity entity, boolean newViewer) {
        displayMetadata(NavAddress.getAddress(entity), newViewer);
    }

    /**
     * Display metadata for the given module.
     * @param module the module to display metadata for
     * @param newViewer true if a new window should be opened
     */
    public void displayMetadata(MetaModule module, boolean newViewer) {
        displayMetadata(NavAddress.getAddress(module), newViewer);
    }
    
    /**
     * Display metadata for the given collector.
     * @param collector the collector to display metadata for
     * @param newViewer true if a new window should be opened
     */
    public void displayMetadata(CollectorGem collector, boolean newViewer) {
        displayMetadata(NavAddress.getAddress(collector), newViewer);
    }

    /**
     * @see org.openquark.gems.client.navigator.NavFrameOwner#editMetadata(org.openquark.gems.client.navigator.NavAddress)
     */
    public void editMetadata(NavAddress address) {
        navFrame.editMetadata(address);
        currentEditAddress = address;
        stateEdit = new StateEdit(new MetadataStateEditable(address));
    }

    /**
     * Edit metadata for the given entity.
     * @param entity the entity to edit metadata for
     */
    public void editMetadata(ScopedEntity entity) {
        editMetadata(NavAddress.getAddress(entity));
    }

    /**
     * Edit metadata for the given module.
     * @param module the module to edit metadata for
     */
    public void editMetadata(MetaModule module) {
        editMetadata(NavAddress.getAddress(module));
    }

    /**
     * Edit metadata for the given collector.
     * @param collector the collector to edit metadata for
     */
    public void editMetadata(CollectorGem collector) {
        editMetadata(NavAddress.getAddress(collector));
    }
    
    /**
     * Edit argument metadata for the given entity.
     * @param entity the entity to edit argument metadata for
     * @param argNum the number of the argument whose metadata to edit
     */
    public void editMetadata(GemEntity entity, int argNum) {
        NavAddress address = NavAddress.getAddress(entity).withParameter(NavAddress.ARGUMENT_PARAMETER, Integer.toString(argNum));
        editMetadata(address);
    }
    
    /**
     * Edit argument metadata for the given collector.
     * @param collector the collector to edit argument metadata for
     * @param argNum the number of the argument whose metadata to edit
     */    
    public void editMetadata(CollectorGem collector, int argNum) {
        NavAddress address = NavAddress.getAddress(collector).withParameter(NavAddress.ARGUMENT_PARAMETER, Integer.toString(argNum));
        editMetadata(address);
    }
    
    /**
     * @see org.openquark.gems.client.navigator.NavFrameOwner#editComplete(org.openquark.gems.client.navigator.NavAddress)
     */
    public void editComplete(NavAddress address) {

        if (!address.equals(currentEditAddress)) {
            throw new IllegalStateException("address for completed edit does not equal current edit address");
        }
        
        currentEditAddress = null;
        stateEdit = null;
    }
    
    /**
     * @see org.openquark.gems.client.navigator.NavFrameOwner#metadataChanged(org.openquark.gems.client.navigator.NavAddress)
     */
    public void metadataChanged(NavAddress address) {

        if (!address.equals(currentEditAddress)) {
            throw new IllegalStateException("address for changed edit does not equal current edit address");
        }
        
        stateEdit.end();
        
        undoableEditSupport.beginUpdate();
        undoableEditSupport.setEditName(GemCutter.getResourceString("UndoText_ChangeGemProperties"));
        undoableEditSupport.postEdit(stateEdit);
        undoableEditSupport.endUpdate();

        stateEdit = new StateEdit(new MetadataStateEditable(address));
        
        updateInputNames(address);
        nameCheckTableTop();
        
        navFrame.refreshMetadata(currentEditAddress);
    }
    
    /**
     * Updates the names of any PartInputs affected by the metadata changing for the 
     * entity with the given address. Does nothing if no updates are required.
     * @param address the address of the entity whose metadata changed
     */
    private void updateInputNames(NavAddress address) {

        NavAddressMethod method = address.getMethod();
        
        if (method != NavAddress.FUNCTION_METHOD &&
            method != NavAddress.CLASS_METHOD_METHOD &&
            method != NavAddress.DATA_CONSTRUCTOR_METHOD) {
            
            return;
        }
        
        // If this was a functional agent, then we need to update its gems on the table top.
        GemEntity entity = getPerspective().getWorkspace().getGemEntity(address.toFeatureName().toQualifiedName());
        FunctionalAgentMetadata metadata = (FunctionalAgentMetadata) NavAddressHelper.getMetadata(this, address.withAllStripped());
        ArgumentMetadata[] argMetadata = metadata.getArguments();
        
        for (final Gem gem : gemCutter.getTableTop().getGemGraph().getGems()) {
            
            if (gem instanceof FunctionalAgentGem) {
                
                GemEntity gemEntity = ((FunctionalAgentGem) gem).getGemEntity();
                
                if (gemEntity.equals(entity)) {

                    // We need to update the inputs of this gem
                    PartInput[] inputs = gem.getInputParts();
                    
                    for (int i = 0; i < inputs.length; i++) {
                        
                        // Update the original name for this input.
                        String displayName = i < argMetadata.length ? argMetadata[i].getDisplayName() : null;
                        String nameFromCode = i < entity.getNNamedArguments() ? entity.getNamedArgument(i) : null;
                        
                        inputs[i].setOriginalInputName(displayName != null ? displayName : nameFromCode);
                    }
                }
            }
        }
    }
    
    /**
     * Name checks the table top to disambiguate any input names that may have become ambiguous
     * as a result of the metadata changing. Also updates the TableTopExplorer to correctly
     * display the new input names.
     */
    private void nameCheckTableTop() {

        // Update the tabletop for the new gem graph state.  Among other things, this will ensure arg name disambiguation.
        gemCutter.getTableTop().updateForGemGraph();
        
        // Since we don't know which input names actually changed as a result of
        // name checking the table top, we simply have to update all nodes in the explorer.
        
        TableTopExplorer tableTopExplorer = gemCutter.getTableTopExplorer();
        Set<DisplayedGem> displayedGemSet = gemCutter.getTableTop().getDisplayedGems();
        
        for (final DisplayedGem gem : displayedGemSet) {
            
            PartInput[] inputs = gem.getGem().getInputParts();
            
            for (final PartInput input : inputs) {
                TreeNode node = tableTopExplorer.getInputNode(input);
                
                if (node != null) {
                    tableTopExplorer.getExplorerTreeModel().nodeChanged(node);
                }
            }
        }
    }
    
    /**
     * @see org.openquark.gems.client.navigator.NavFrameOwner#searchMetadata(java.lang.String)
     */
    public List<NavAddress> searchMetadata(String searchString) {
        return navFrame.searchMetadata(searchString);
    }
    
    /**
     * Tests all metadata examples.
     */
    public void testMetadataExamples() {        
        String conciseTitle = "Concise Output?";
        String conciseMessage = "Would you like to suppress the output of successful metadata tests?";
        int conciseResponse = JOptionPane.showOptionDialog(gemCutter,
                conciseMessage,
                conciseTitle,
                JOptionPane.YES_NO_CANCEL_OPTION,
                JOptionPane.QUESTION_MESSAGE,
                null, null, null); 
        
        // If the user closed the dialog or hit cancel, do nothing
        if (conciseResponse == JOptionPane.CANCEL_OPTION || conciseResponse == JOptionPane.CLOSED_OPTION) {
            return;
        }

        String autoTitle = "Skip non-automatic examples?";
        String autoMessage = "Would you like to run only those examples set to run automatically?";
        int autoResponse = JOptionPane.showOptionDialog(gemCutter,
                autoMessage,
                autoTitle,
                JOptionPane.YES_NO_CANCEL_OPTION,
                JOptionPane.QUESTION_MESSAGE,
                null, null, null); 
        
        // If the user closed the dialog or hit cancel, do nothing
        if (autoResponse == JOptionPane.CANCEL_OPTION || autoResponse == JOptionPane.CLOSED_OPTION) {
            return;
        }
        
        navFrame.testMetadataExamples(conciseResponse == JOptionPane.YES_OPTION, autoResponse==JOptionPane.YES_OPTION);
    }

    /**
     * {@inheritDoc}
     */
    public Locale getLocaleForMetadata() {
        return GemCutter.getLocaleFromPreferences();
    }
}
