/*
 * $Id$
 *
 * Copyright 1996-2008 Sun Microsystems, Inc.  All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Sun designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Sun in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
 * CA 95054 USA or visit www.sun.com if you need additional information or
 * have any questions.
 */
package com.sun.interview.wizard;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.KeyboardFocusManager;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import javax.accessibility.AccessibleContext;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.BorderFactory;
import javax.swing.FocusManager;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.InputMap;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.JViewport;
import javax.swing.KeyStroke;
import javax.swing.Scrollable;
import javax.swing.SwingConstants;
import javax.swing.event.AncestorEvent;
import javax.swing.event.AncestorListener;
import javax.swing.plaf.metal.MetalLookAndFeel;

import com.sun.interview.ChoiceArrayQuestion;
import com.sun.interview.ChoiceQuestion;
import com.sun.interview.ErrorQuestion;
import com.sun.interview.FileListQuestion;
import com.sun.interview.FileQuestion;
import com.sun.interview.FloatQuestion;
import com.sun.interview.InetAddressQuestion;
import com.sun.interview.IntQuestion;
import com.sun.interview.Interview;
import com.sun.interview.ListQuestion;
import com.sun.interview.NullQuestion;
import com.sun.interview.PropertiesQuestion;
import com.sun.interview.Question;
import com.sun.interview.StringQuestion;
import com.sun.interview.StringListQuestion;
import com.sun.interview.TreeQuestion;
import com.sun.interview.YesNoQuestion;
import java.util.EventListener;

/**
 * A panel which implements a {@link com.sun.interview.wizard.Wizard wizard}
 * that asks a series of {@link com.sun.interview.Question questions}
 * embodied in an {@link Interview interview}.
 */
class QuestionPanel extends JPanel 
    implements Scrollable 
{
    
    /**
     * Create a panel in which to display the questions of the interview.
     * The interview to be run.
     */
    QuestionPanel(Interview i) {
	interview = i;

	initRenderers();
	initGUI();

	addAncestorListener(listener);
     
    }

    
    // ---------- Component stuff ---------------------------------------

    public Dimension getPreferredSize() {
	Dimension d = super.getPreferredSize();
	d.height = Math.max(d.height, PREFERRED_HEIGHT * DOTS_PER_INCH);
	d.width = Math.max(d.width, PREFERRED_WIDTH * DOTS_PER_INCH);
	return d;
    }

    // ---------- Scrollable stuff ---------------------------------------

    public Dimension getPreferredScrollableViewportSize() {
        Dimension maxD = new Dimension(PREFERRED_WIDTH * DOTS_PER_INCH, PREFERRED_HEIGHT * DOTS_PER_INCH);
        Dimension ps = getPreferredSize();
        ps.width = Math.min(ps.width, maxD.width);
        ps.height = Math.min(ps.height, maxD.height);
        return ps;
    }

    public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
        switch(orientation) {
        case SwingConstants.VERTICAL:
            return visibleRect.height / 10;
        case SwingConstants.HORIZONTAL:
            return visibleRect.width / 10;
        default:
            throw new IllegalArgumentException("Invalid orientation: " + orientation);
        }
    }

    public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
        switch(orientation) {
        case SwingConstants.VERTICAL:
            return visibleRect.height;
        case SwingConstants.HORIZONTAL:
            return visibleRect.width;
        default:
            throw new IllegalArgumentException("Invalid orientation: " + orientation);
        }
    } 

    public boolean getScrollableTracksViewportHeight() {
        // We can't use getPreferred size here, because of situation, when
        // getPreferredSize() gives default values. In this case, and if horizontal scroll exists, 
        // viewport height becomes less than getPreferredSize().height, but may be enough for components
        // without any scrolling
        if(getParent() instanceof JViewport)
                return (((JViewport)getParent()).getHeight() > getPreferredSize().height);
	else
	    return false;
    }

    public final boolean getScrollableTracksViewportWidth() {
 
//        if (currentRenderer instanceof SizeSensitiveQuestionRenderer) {
//            Dimension d;
//            int visWidth = ((JViewport)getParent()).getExtentSize().width;
//            int requiredWidth = valuePanel.getPreferredSize().width + TEXT_AREA_INSETS_LEFT_RIGHT*2;
//            if(requiredWidth < visWidth) {
////                d = this.getPreferredSize();
////                this.setPreferredSize(new Dimension(visWidth, 
////                        super.getPreferredSize().height));
////                d = this.getPreferredSize();
//                setHorScrollStatus(false);
//                return true;
//            }
//            else {
////                d = this.getPreferredSize();
////                this.setPreferredSize(new Dimension(requiredWidth,
////                        super.getPreferredSize().height));
////                d = this.getPreferredSize();
//                setHorScrollStatus(true);
//                return false;
//            }
////            setHorScrollStatus(true);
////            return false;
//        }
//        else {      
////            this.setPreferredSize(this.getSize());
//            setHorScrollStatus(false);
            return true;
//        }
    }

    // ---------- end of Scrollable stuff -----------------------------------

    void setNextAction(Action nextAction) {
	this.nextAction = nextAction;
    }

    void saveCurrentResponse() /*throws Interview.Fault*/ {
	if (valueSaver != null)
	    valueSaver.run();
    }

    boolean isTagVisible() {
	return propsPanel.isVisible();
    }

    void setTagVisible(boolean v) {
	propsPanel.setVisible(v);
    }

    private void initRenderers() {
	renderers = new HashMap();
	renderers.put(ChoiceQuestion.class, new ChoiceQuestionRenderer());
	renderers.put(ChoiceArrayQuestion.class, new ChoiceArrayQuestionRenderer());
	renderers.put(FileQuestion.class, new FileQuestionRenderer());
	renderers.put(FileListQuestion.class, new FileListQuestionRenderer());
	renderers.put(FloatQuestion.class, new FloatQuestionRenderer());
	renderers.put(IntQuestion.class, new IntQuestionRenderer());
	renderers.put(InetAddressQuestion.class, new InetAddressQuestionRenderer());
	renderers.put(ListQuestion.class, new ListQuestionRenderer());
	renderers.put(NullQuestion.class, new NullQuestionRenderer());
	renderers.put(PropertiesQuestion.class, new PropertiesQuestionRenderer());
	renderers.put(StringQuestion.class, new StringQuestionRenderer());
	renderers.put(StringListQuestion.class, new StringListQuestionRenderer());
	renderers.put(TreeQuestion.class, new TreeQuestionRenderer());
        renderers.put(YesNoQuestion.class, new YesNoQuestionRenderer());
        setCustomRenderers(new HashMap());
    }

    /**
     * Create the basic GUI infrastructure. The content of the
     * various areas are filled in from the questions that are
     * asked.
     */
    private void initGUI() {
	/*
	  	+---------------+-------------------------------+
		|		|	title			|
		|		+-------------------------------+
		|   		|	text area		|
		|   graphic	|				|
		|		+-------------------------------+
	  	|	       	|				|
		|   		|       value			|
	  	|	       	|				|
		|		+-------------------------------+
		|		|	msg			|
		|		+-------------------------------+
		|		|	tag info (optional)     |
	  	+---------------+-------------------------------+
	*/
	setInfo(this, "qu", false);
	setLayout(new GridBagLayout());
	GridBagConstraints c = new GridBagConstraints();

	graphicLabel = new JLabel();
	setInfo(graphicLabel, "qu.icon", false);
	graphicLabel.setFocusable(false);
	if (interview != null) {
	    URL u = interview.getDefaultImage();
	    if (u != null) 
		graphicLabel.setIcon(new ImageIcon(u));
	}
	c.anchor = GridBagConstraints.CENTER;
	c.gridheight = GridBagConstraints.REMAINDER;
	add(graphicLabel, c);

	titleField = new JTextField();
	setInfo(titleField, "qu.title", true);
	titleField.setEditable(false);
	titleField.setBackground(MetalLookAndFeel.getPrimaryControlDarkShadow());
	titleField.setForeground(MetalLookAndFeel.getWindowBackground());
	Font f = MetalLookAndFeel.getSystemTextFont();
	titleField.setFont(f.deriveFont(f.getSize() * 1.5f));
	c.fill = GridBagConstraints.HORIZONTAL;
	c.gridwidth = GridBagConstraints.REMAINDER;
	c.gridheight = 1;
	c.weightx = 1;
	add(titleField, c);

	textArea = new JTextArea(3, 30);
	setInfo(textArea, "qu.text", true);
	textArea.setEditable(false);
	textArea.setLineWrap(true);
	textArea.setOpaque(false);
	textArea.setWrapStyleWord(true);
	// override JTextArea focus traversal keys, resetting them to 
	// the Component default (i.e. the same as for the parent.)
	textArea.setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, null);
	textArea.setFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, null);
	// set enter to be same as Next
	{
	    InputMap im = textArea.getInputMap();
	    im.put(enterKey, "next");
	    ActionMap am = textArea.getActionMap();
	    am.put("next", valueAction);
	}
	c.insets.top = TEXT_AREA_INSETS_TOP;
	c.insets.left = c.insets.right = TEXT_AREA_INSETS_LEFT_RIGHT;
	c.insets.bottom = TEXT_AREA_INSETS_BOTTOM;
	c.fill = GridBagConstraints.BOTH;
	add(textArea, c);


	valuePanel = new JPanel(new BorderLayout());
	setInfo(valuePanel, "qu.vp", false);
	valuePanel.setOpaque(true);
	// set default action for enter to be same as Next
	{
	    InputMap im = valuePanel.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
	    im.put(enterKey, "next");
	    ActionMap am = valuePanel.getActionMap();
	    am.put("next", valueAction);
	}

	c.insets.top = VALUE_PANEL_INSETS_TOP;
	c.weighty = 1;
	c.fill = GridBagConstraints.BOTH;
	c.insets.bottom = VALUE_PANEL_INSETS_BOTTOM;
	add(valuePanel, c);

        c.fill = GridBagConstraints.BOTH;
	valueMessageField = new JTextField();
	setInfo(valueMessageField, "qu.vmsg", false);
	valueMessageField.setEditable(false);
	valueMessageField.setOpaque(false);
	valueMessageField.setFont(valueMessageField.getFont().deriveFont(Font.BOLD));
	valueMessageField.setBorder(null);
        c.insets.top = VALUE_MESSAGE_FIELD_INSETS_TOP;
	c.insets.bottom = VALUE_MESSAGE_FIELD_INSETS_BOTTOM;
	c.weighty = 0;
	add(valueMessageField, c);	

	propsPanel = new JPanel(new BorderLayout());
	propsPanel.setBorder(BorderFactory.createEtchedBorder()); 
		// replace by titled border "properties" if we get more than one...
	propsPanel.setName("qu.props.pnl");
	propsPanel.setFocusable(false);
	JLabel tagLabel = new JLabel(i18n.getString("qu.tag.lbl"));
	setInfo(tagLabel, "qu.tag.lbl", true);
	tagLabel.setDisplayedMnemonic(i18n.getString("qu.tag.mne").charAt(0));
	tagLabel.setBorder(BorderFactory.createEmptyBorder(0, 5, 0, 7));
	propsPanel.add(tagLabel, BorderLayout.WEST);
	tagField = new JTextField();
	tagField.setName("qu.tag.fld");
	tagField.setEditable(false);
	tagField.setBorder(null);
	tagLabel.setLabelFor(tagField);
	propsPanel.add(tagField, BorderLayout.CENTER);
	propsPanel.setVisible(false);
        c.insets.top = PROPS_PANEL_INSETS_TOP;
	c.insets.bottom = PROPS_PANEL_INSETS_BOTTOM;
	add(propsPanel, c);

	ActionMap actionMap = getActionMap();

	actionMap.put("hideProps", new AbstractAction() {
		public void actionPerformed(ActionEvent e) {
		    propsPanel.setVisible(false);
		}
	    });

	actionMap.put("showProps", new AbstractAction() {
		public void actionPerformed(ActionEvent e) {
		    propsPanel.setVisible(true);
		}
	    });

	actionMap.put("toggleProps", new AbstractAction() {
		public void actionPerformed(ActionEvent e) {
		    propsPanel.setVisible(!propsPanel.isVisible());
		}
	    });

	InputMap inputMap = getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
	inputMap.put(KeyStroke.getKeyStroke("ctrl T"), "toggleProps");
        
    }
    
    private void setInfo(JComponent jc, String uiKey, boolean addToolTip) {
	jc.setName(uiKey);
	AccessibleContext ac = jc.getAccessibleContext();
	ac.setAccessibleName(i18n.getString(uiKey + ".name"));
	if (addToolTip) {
	    String tip = i18n.getString(uiKey + ".tip");
	    jc.setToolTipText(tip);
	    ac.setAccessibleDescription(tip);
	}
	else
	    ac.setAccessibleDescription(i18n.getString(uiKey + ".desc"));
    }
    
    /**
     * Show a question. The appropriate showXXX method
     * is called, depending on the question, and then a response
     * is awaited.
     */
    public void showQuestion(Question q) {
	//System.err.println("QP.showQuestion " + q.getTag() + " " + q);
	if (q instanceof ErrorQuestion) {
	    JTextArea txt = new JTextArea(q.getText());
	    txt.setOpaque(false);
	    txt.setEditable(false);
	    txt.setLineWrap(true);
	    txt.setWrapStyleWord(true);
	    // The height is effectively ignored in the next line (just don't use 0.)
	    // The text will be layed out, wrapping lines, for the width, and the
	    // preferred height will thereby be determined accordingly.
	    txt.setSize(new Dimension(5 * DOTS_PER_INCH, Integer.MAX_VALUE));

	    JOptionPane.showMessageDialog(this, 
					  txt,
					  q.getSummary(),
					  JOptionPane.ERROR_MESSAGE);
	    try {
		// no current response to save :-)
		interview.prev();
	    }
	    catch (Interview.Fault ignore) {
	    }
	    return;
	}
	    
	URL u = q.getImage();
	final Icon icon = (u == null ? null : new ImageIcon(u));
	
	if (icon != null)
	    graphicLabel.setIcon(icon);
	
	titleField.setText(q.getSummary());
	textArea.setText(q.getText());
	tagField.setText(q.getTag());
	
	boolean focus = anyChildHasFocus(valuePanel);
	valuePanel.removeAll();
	
	QuestionRenderer r = getRenderer(q);

        //------------------------------------
        
	if (r == null) {
	    //System.err.println("no renderer for " + q.getTag() + " [" + q.getClass().getName() + "]");
	    valueSaver = null;
	}
	else {
	    JComponent rc = r.getQuestionRendererComponent(q, valueAction);
	    if (rc == null) {
		valueSaver = null;
		if (focus) {
		    // no response area, so put focus back on question text
		    textArea.requestFocus();
		}
	    }
	    else {
		if (rc.getName() == null)
		    rc.setName(r.getClass().getName());

		valueSaver = (Runnable) (rc.getClientProperty(QuestionRenderer.VALUE_SAVER));
		//System.err.println("QP.showQuestion valueSaver=" + valueSaver);
		valuePanel.add(rc);
		if (focus) {
		    //System.err.println("QP.showQuestion: setFocus");
		    FocusManager fm = FocusManager.getCurrentManager();
		    //fm.focusNextComponent(valuePanel);
		    fm.focusNextComponent(textArea);
		}
	    }
	}

	if (q.isValueAlwaysValid())
	    valueMessageField.setVisible(false);
	else {
	    showValueMessage(null);
	    valueMessageField.setVisible(true);
	}
          
	// relayout the GUI
	revalidate();
        
	currentRenderer = r;
	currentQuestion = q;
    }

    /**
     * This method invokes when config editor is going to be closed.
     * This made to allow components handle feature closing
     */    
    public void prepareClosing() {
        if(currentRenderer instanceof PropertiesQuestionRenderer) {
            AncestorEvent e = new AncestorEvent(this, AncestorEvent.ANCESTOR_REMOVED,
                                    valuePanel, valuePanel.getParent());
            Component[] childs = valuePanel.getComponents();
            for(int i = 0; i < childs.length; i++) {
                EventListener[] l = childs[i].getListeners(AncestorListener.class);
                for(int j = 0; j < l.length; j++) {
                    if(l[i] instanceof AncestorListener) {
                        ((AncestorListener)l[i]).ancestorRemoved(e);
                    }
                }
            }
        }
    }

    public void showValueInvalidMessage() {
	String msg = (currentRenderer == null 
		      ? null 
		      : currentRenderer.getInvalidValueMessage(currentQuestion));
	showValueMessage((msg == null ? INVALID_VALUE : msg), INVALID_VALUE_COLOR);
    }

    private void showValueMessage(String msg) {
	showValueMessage(msg, MetalLookAndFeel.getBlack());
    }

    private void showValueMessage(String msg, Color c) {
	if (msg == null || msg.length() == 0) {
	    valueMessageField.setText("");
	    valueMessageField.setEnabled(false);
	}
	else {
	    valueMessageField.setForeground(c);
	    valueMessageField.setText(msg);
	    valueMessageField.setEnabled(true);
	}
    }

    private QuestionRenderer getRenderer(Question q) {
        QuestionRenderer result = null;
        if (customRenderers != null) {
            result = getRenderer(q, customRenderers);
        }
        if (result == null) {
            result = getRenderer(q, renderers);
        }
	return result;
    }

    private QuestionRenderer getRenderer(Question q, Map rendMap) {
        for (Class c = q.getClass(); c != null; c = c.getSuperclass()) {
	    QuestionRenderer r = (QuestionRenderer) (rendMap.get(c));
	    if (r != null)
		return r;
	}
	return null;
    }


    private boolean anyChildHasFocus(JPanel p) {
	if (p.hasFocus())
	    return true;

	for (int i = 0; i < p.getComponentCount(); i++) {
	    Component c = (p.getComponent(i));
	    if ((c instanceof JComponent) && ((JComponent)c).hasFocus()
		|| (c instanceof JPanel && anyChildHasFocus((JPanel)c)))
		return true;
	}
	return false;
    }

    private Interview interview;
    private Question currentQuestion;
    private QuestionRenderer currentRenderer;
    private JLabel graphicLabel;
    private JTextField titleField;
    private JTextArea textArea;
    private JPanel valuePanel;
    private Runnable valueSaver;
    private JTextField valueMessageField;
    private JPanel propsPanel;
    private JTextField tagField;
    private Map renderers;
    private Map customRenderers;
    private Listener listener = new Listener();

    private static final I18NResourceBundle i18n = I18NResourceBundle.getDefaultBundle();
    private static String INVALID_VALUE = i18n.getString("qu.invalidValue.txt");
    private static Color INVALID_VALUE_COLOR = i18n.getErrorColor();

    private KeyStroke enterKey = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0);
    private Action valueAction = new AbstractAction() {
	    public void actionPerformed(ActionEvent e) {
		String cmd = e.getActionCommand();
		if (cmd.equals(QuestionRenderer.EDITED)) 
		    showValueMessage(null);
		else {
		    if (nextAction != null) {
			nextAction.actionPerformed(e);
		    }
		    else {
			// default old behavior
			try {
			    saveCurrentResponse();
			    interview.next();
			}
			catch (Interview.Fault ex) {
			    // exception normally means no more questions, 
			    // which should only be  because the value of the current 
			    // question is invalid
			    // e.printStackTrace();
			    // QuestionPanel.this.getToolkit().beep();
			    showValueInvalidMessage();
			}
		    }
		}
	    }
	    
    };

    private Action nextAction; // optionally settable

    private static final int PREFERRED_HEIGHT = 3; // inches
    private static final int PREFERRED_WIDTH = 4; // inches
    private static final int DOTS_PER_INCH = Toolkit.getDefaultToolkit().getScreenResolution();
    private static final int TEXT_AREA_INSETS_TOP = 20;
    
    private static final int TEXT_AREA_INSETS_LEFT_RIGHT = 10;
    private static final int TEXT_AREA_INSETS_BOTTOM = 10;
    
    private static final int VALUE_PANEL_INSETS_TOP = 0;
    private static final int VALUE_PANEL_INSETS_BOTTOM = 10;
    
    private static final int VALUE_MESSAGE_FIELD_INSETS_TOP = 0;
    private static final int VALUE_MESSAGE_FIELD_INSETS_BOTTOM = 0;
    
    private static final int PROPS_PANEL_INSETS_TOP = 0;
    private static final int PROPS_PANEL_INSETS_BOTTOM = 10;

    public void setCustomRenderers(Map customRenderers) {
        this.customRenderers = customRenderers;
    }


    private class Listener
        implements AncestorListener, Interview.Observer
    {

        // ---------- AncestorListener

        public void ancestorAdded(AncestorEvent e) {
            interview.addObserver(this);
            showQuestion(interview.getCurrentQuestion());
        }

        public void ancestorMoved(AncestorEvent e) { }

        public void ancestorRemoved(AncestorEvent e) {
            interview.removeObserver(this);
        }

        // ---------- Interview.Observer ----------

        public void pathUpdated() {
            // if path is updated as a result of refresh, clear the error message
            showValueMessage(null);
        }

        public void currentQuestionChanged(Question q) {
            showQuestion(q);
        }

        public void finished() { }

    }
    
}
