/*
 * $Id$
 *
 * Copyright (c) 1996, 2011, Oracle and/or its affiliates. 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.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle 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 Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package com.sun.interview.wizard;

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.StringListQuestion;
import com.sun.interview.StringQuestion;
import com.sun.interview.TreeQuestion;
import com.sun.interview.YesNoQuestion;

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.JEditorPane;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
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 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.EventListener;
import java.util.HashMap;
import java.util.Map;

/**
 * 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 {

    private static final I18NResourceBundle i18n = I18NResourceBundle.getDefaultBundle();


    // ---------- Component stuff ---------------------------------------
    private static final int PREFERRED_HEIGHT = 3; // inches

    // ---------- Scrollable stuff ---------------------------------------
    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;

    // ---------- end of Scrollable stuff -----------------------------------
    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;
    private static String INVALID_VALUE = i18n.getString("qu.invalidValue.txt");
    private static Color INVALID_VALUE_COLOR = i18n.getErrorColor();
    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<Class<? extends Question>, QuestionRenderer> renderers;
    private Map<Class<? extends Question>, QuestionRenderer> customRenderers;
    private Listener listener = new Listener();
    private KeyStroke enterKey = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0);
    private Action nextAction; // optionally settable
    private Action valueAction = new AbstractAction() {
        @Override
        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();
                    }
                }
            }
        }

    };
    /**
     * 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);

    }

    @Override
    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;
    }

    @Override
    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;
    }

    @Override
    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);
        }
    }

    @Override
    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);
        }
    }

    @Override
    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 getParent().getHeight() > getPreferredSize().height;
        } else {
            return false;
        }
    }

    @Override
    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;
//        }
    }

    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(new Color(102, 102, 153));//titleField.setBackground(MetalLookAndFeel.getPrimaryControlDarkShadow());
        titleField.setForeground(Color.WHITE);//titleField.setForeground(MetalLookAndFeel.getWindowBackground());
        Font f = titleField.getFont();//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.setBackground(new Color(255, 255, 255, 0));
        textArea.setBorder(null);
        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() {
            @Override
            public void actionPerformed(ActionEvent e) {
                propsPanel.setVisible(false);
            }
        });

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

        actionMap.put("toggleProps", new AbstractAction() {
            @Override
            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) {
            showErrorQuestion((ErrorQuestion) q);
            try {
                // no current response to save :-)
                interview.prev();
            } catch (Interview.Fault ignore) {
            }
            return;
        }

        URL u = q.getImage();
        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();
        repaint();

        currentRenderer = r;
        currentQuestion = q;
    }

    private void showErrorQuestion(ErrorQuestion q) {

        JEditorPane ePane = new JEditorPane(q.getTextMimeType(), q.getText());
        ePane.setEditable(false);

        Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
        Dimension minS = new Dimension(350, 100);
        Dimension maxS = new Dimension(Math.min(2 * (screen.width / 3), 800), Math.min(2 * (screen.height / 3), 600));
        Dimension p = ePane.getPreferredSize();

        p.setSize(Math.max(p.width, minS.width), Math.max(p.height, minS.height));
        p.setSize(Math.min(p.width, maxS.width), Math.min(p.height, maxS.height));
        ePane.setPreferredSize(p);

        ePane.setCaretPosition(0);
        JScrollPane sPane = new JScrollPane(ePane);
        sPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
        sPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);

        JOptionPane.showMessageDialog(this, sPane, q.getSummary(), JOptionPane.ERROR_MESSAGE);

    }

    /**
     * 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 (EventListener aL : l) {
                    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, Color.BLACK);//showValueMessage(msg, MetalLookAndFeel.getBlack());
    }

    private void showValueMessage(String msg, Color c) {
        if (msg == null || msg.isEmpty()) {
            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<Class<? extends Question>, QuestionRenderer> rendMap) {
        for (Class<?> c = q.getClass(); c != null; c = c.getSuperclass()) {
            QuestionRenderer r = 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 && c.hasFocus())
                    || (c instanceof JPanel && anyChildHasFocus((JPanel) c))) {
                return true;
            }
        }
        return false;
    }

    public void setCustomRenderers(Map<Class<? extends Question>, QuestionRenderer> customRenderers) {
        this.customRenderers = customRenderers;
    }


    private class Listener
            implements AncestorListener, Interview.Observer {

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

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

        @Override
        public void ancestorMoved(AncestorEvent e) {
        }

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

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

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

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


    }

}
