/*
 * $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;

import java.net.URL;
import java.text.MessageFormat;
import java.util.Map;

/**
 * Questions are the primary constituent elements of {@link Interview interviews}.
 * They provide text and an optional graphic to be presented to the user,
 * and they provide a place to store the user's response.
 * Various subtypes are provided, according to the type of response they
 * request and store.
 * Questions are identified internally by a unique tag, which is used to identify
 * the question in contexts such as resource bundles, dictionaries, and so on.
 */
public abstract class Question
{

    /**
     * Create a question with a nominated tag.
     * @param interview The interview containing this question.
     * @param baseTag A name to uniquely identify this question within its interview.
     */
    protected Question(Interview interview, String baseTag) {
        if (baseTag == null || baseTag.length() == 0)
            throw new IllegalArgumentException("No tag specified");

        this.interview = interview;
        this.baseTag = baseTag;

        updateTag();

        interview.add(this);

        String c = interview.getClass().getName();
        int dot = c.lastIndexOf(".");
        if (dot != -1)
            c = c.substring(dot+1);
        c = c.replace('$', '.');
        key = c + "." + baseTag;
    }

    /**
     * Create a question with no identifying tag.
     * @param interview The interview containing this question.
     */
    protected Question(Interview interview) {
        this.interview = interview;
        baseTag = null;
        tag = null;
        key = null;
    }

    /**
     * Return the interview of which this question is a part.
     * @return the interview of which this question is a part
     */
    public Interview getInterview() {
        return interview;
    }

    /**
     * Get the <em>key</em> for a question. The key is the class name of the parent
     * interview followed by a period followed by the tag given to the constructor.
     * It therefore reasonably identifies the question relative to the class of
     * its parent interview. The key is normally used to identify resources
     * such as the question text in a resource file or help information in a
     * help set.
     * @return the key for this question
     * @see #getTag
     */
    public String getKey() {
        return key;
    }

    /**
     * Get the <em>tag</em> for a question. The tag is the tag of the parent
     * interview, followed by a dot, followed by the tag given to the constructor.
     * It therefore reasonably identifies the question relative to the instance of
     * its parent interview. The tag is normally used to identify instance-specific
     * responses in a saved interview file.
     * @return the tag for this question
     * @see #getKey
     */
    public String getTag() {
        return tag;
    }

    String getBaseTag() {
        return baseTag;
    }

    void updateTag() {
        if (interview.getTag() == null)
            tag = baseTag;
        else
            tag = (interview.getTag() + "." + baseTag);
    }

    /**
     * Set the text for this question. The text can also be provided
     * in the containing interview's resource bundle, using the resource name
     * <i>key</i>.<code>text</code>.
     * @param text the text for this question
     * @see #getText
     */
    protected void setText(String text) {
        this.text = text.trim();
    }

    /**
     * Get the text for this question. If not specified, the system will
     * try and locate the text in the containing interviews resource bundle,
     * using the resource name <i>key</i><code>.text</code>.
     * @return the text for this question
     * @see #setText
     */
    public String getText() {
        if (text == null) {
            text = getResourceString(interview, key + ".text", true);
            if (text == null)
                text = key + ".text";
        }
        return MessageFormat.format(text, getTextArgs());
    }

    /**
     * Get any arguments necessary to go with the question text.
     * The text is formatted using the rules specified for
     * {@link java.text.MessageFormat#format}. By default, this
     * method returns null. If the text for the question contains
     * variable entries (using the '{' '}' notation), you should
     * override this method to provide the corresponding values.
     * @return An array of objects suitable for formatting in the text
     * of the question.
     */
    protected Object[] getTextArgs() {
        return null;
    }

    /**
     * Set the summary text for this question. The text can also be provided
     * in the containing interview's resource bundle, using the resource name
     * <i>key</i>.<code>smry</code>.
     * @param summary a short summary of the text for this question
     * @see #getSummary
     */
    protected void setSummary(String summary) {
        this.summary = summary.trim();
    }

    /**
     * Set the summary text for this question.
     * @return a short summary of the text for this question
     * @see #setSummary
     */
    public String getSummary() {
        if (summary == null) {
            summary = getResourceString(interview, key + ".smry", true);
            if (summary == null)
                summary = key + ".smry";
            /* OLD, undocumented and mildly suspect (e.g. i18n)
            if (summary == null) {
                // search text for either a newline, or for whitespace
                // following a .
                String t = getText();
                if (t != null) {
                    for (int i = 0; i < t.length(); i++) {
                        char c = t.charAt(i);
                        if (c == '\n'
                            || (Character.isWhitespace(c)
                                && i > 0
                                && t.charAt(i-1) == '.' )) {
                            summary = t.substring(0, i).trim();
                            break;
                        }
                    }
                    if (summary == null)
                        summary = t;
                }
            }
            */
        }
        return summary;
    }

    /**
     * Set the URL for a specific graphic for this question.
     * @param resource The name of a resource containing the
     * desired image.
     * @see #getImage
     */
    public void setImage(String resource) {
        image = getClass().getResource(resource);
    }

    /**
     * Set the URL for a specific graphic for this question.
     * @param u The URL of a resource containing the
     * desired image.
     * @see #getImage
     */
    protected void setImage(URL u) {
        image = u;
    }

    /**
     * Get the graphic for this question. If {@link #setImage}
     * has been called to supply a specific image for this question,
     * that will be the result here; otherwise, the value defaults
     * first to a question-specific resource (<i>tag</i><code>.gif</code>)
     * and then to a {@link Interview#getDefaultImage default image}
     * for the interview.
     * @return a URL for the question.
     * @see #setImage
     */
    public URL getImage() {
        if (image == null)
            image = getClass().getResource(tag + ".gif");
        if (image == null)
            image = interview.getDefaultImage();
        return image;
    }

    /**
     * Get any items which should be added to the interview's
     * checklist.
     * @return any items which should be added to the interview's
     * checklist, or null if none.
     * @see Interview#createChecklist
     */
    public Checklist.Item[] getChecklistItems() {
        return null;
    }

    /**
     * Add a named marker to this question, if it has not already been added.
     * @param name the name of the marker to be added.
     * @throws NullPointerException if name is null
     */
    public void addMarker(String name) {
        interview.addMarker(this, name);
    }

    /**
     * Remove a named marker to this question.
     * @param name the name of the marker to be removed.
     */
    public void removeMarker(String name) {
        interview.removeMarker(this, name);
    }

    /**
     * Check if a marker has been added to this question.
     * @param name the name of the marker to be checked.
     * @return true if the marker has been added to this question, and false otherwise.
     */
    public boolean hasMarker(String name) {
        return interview.hasMarker(this, name);
    }


    /**
     * Determine if this question is currently "hidden".
     * Hidden questions do not appear on the current path
     * and so are skipped over by {@link Interview#prev} and
     * {@link Interview#next}, etc.
     * By default, questions are not hidden, and this method returns false.
     * Override this method if you want to hide a question.
     * It can be convenient to determine whether or not a question is
     * hidden using the values of earlier questions.
     * @return true if this question has been hidden.
     * @deprecated
     * @see #isEnabled
     */
    public boolean isHidden() {
        return false;
    }


    /**
     * Determine if this question is currently enabled.
     * Disabled questions do not appear on the current path
     * and so are skipped over by {@link Interview#prev} and
     * {@link Interview#next}, etc.
     * By default, all questions are enabled, and this method returns true.
     * Override this method if you want to hide a question.
     * It can be convenient to determine whether or not a question should be
     * disabled by using the values of earlier questions.
     * <em>Note:</em> for backwards compatibility, the default implementation
     * returns <code>!isHidden()</code>.
     * @return true if this question should appear on the current path,
     * and false otherwise.
     */
    public boolean isEnabled() {
        return (!isHidden());
    }

    /**
     * Get the next question to be asked.
     * @return the next question to be asked
     */
    protected abstract Question getNext();

    /**
     * Clear any state for this question, setting the value
     * to a question-specific default value. Most question
     * types also provide a way to set the default value.
     */
    public abstract void clear();

    /**
     * Get the response to this question as a string.
     * @return a string representing the current response to this question, or null.
     * @see #setValue(String)
     */
    public abstract String getStringValue();

    /**
     * Set the response to this question to the value represented by
     * a string-valued argument. Subtypes of Question will typically
     * have type-specific methods to set the value as well.
     * @param s A string containing a value value appropriate for the
     * particular type of question whose value is being set.
     * @throws Interview.Fault (retained for compatibility; should not be thrown)
     * @see #getStringValue
     */
    public abstract void setValue(String s) throws Interview.Fault;

    /**
     * Check if the question currently has a valid response.
     * @return true if the question currently has a valid response,
     * and false otheriwse.
     **/
    public abstract boolean isValueValid();

    /**
     * Check if the question always has a valid response.
     * This may be true, for example, for a choice question with a default response.
     * @return true if the question always has a valid response,
     * and false otheriwse.
     **/
    public abstract boolean isValueAlwaysValid();

    /**
     * Load any state for this question from a dictionary,
     * using the tag as the key.
     * @param data The map from which to load the value for this question.
     */
    protected abstract void load(Map data);

    /**
     * Save any state for this question in a dictionary,
     * using the tag as the key.
     * @param data The map in which to save the value for this question.
     */
    protected abstract void save(Map data);

    /**
     * Export any appropriate values to the dictionary, as part
     * of {@link Interview#export}. The default is to do nothing.
     * Note that only questions which are on the current path have
     * their export method called. Questions do not appear on the
     * current path if they are not accessible from any question on the
     * path, or if they have been disabled.
     * @param data The map in which to export any data for this question.
     * @see Interview#getPath
     * @see #isEnabled
     */
    protected void export(Map data) {
    }

    /**
     * Check if this question equals another.
     * By default, two questions are equal if they have equal tags,
     * and the string values are either both null
     * or are equal.
     */
    public boolean equals(Object other) {
        if ( !(other instanceof Question))
            return false;

        Question oq = (Question) other;

        return (equal(tag, oq.tag) && equal(getStringValue(), oq.getStringValue()));
    }

    private static boolean equal(String s1, String s2) {
        return (s1 == null || s2 == null ? s1 == s2 : s1.equals(s2));
    }

    /**
     * Get an entry from the interview's resource bundle.
     * The parent and other ancestors bundles will be checked first before
     * this interview's bundle, allowing the root interview a chance to override
     * the default value provided by this interview.
     * @param key the name of the entry to be returned
     * @return the value of the resource, or null if not found
     * @see Interview#getResourceString(String)
     */
    protected String getResourceString(String key) {
        return interview.getResourceString(key);
    }

    /**
     * Get an entry from the interview's resource bundle. If checkAncestorsFirst is true,
     * then the parent and other ancestor interviews' bundles will be checked first before
     * this interview's bundle, allowing the root interview a chance to override
     * the default value provided by this interview. Otherwise, the parent bundles
     * will only be checked if this bundle does not provide a value.
     * @param key the name of the entry to be returned
     * @param checkAncestorsFirst whether to recursively call this method on the
     * parent (if any) before checking this bundle, or only afterwards, if this
     * bundle does not provide a value
     * @return the value of the resource, or null if not found
     * @see Interview#getResourceString(String, boolean)
     */
    protected String getResourceString(String key, boolean checkAncestorsFirst) {
        return interview.getResourceString(key, checkAncestorsFirst);
    }

    private String getResourceString(Interview interview, String key, boolean checkAncestorsFirst) {
        return interview.getResourceString(key, checkAncestorsFirst);
        /*
         * the following code is now in Interview.getResourceString
        try {
            //System.err.println("QU: " + interview.getTag() + " " + key);
            String s = null;
            Interview p = interview.getParent();
            if (checkAncestorsFirst) {
                if (p != null)
                    s = getResourceString(p, key, checkAncestorsFirst);
                if (s == null) {
                    ResourceBundle b = interview.getResourceBundle();
                    if (b != null)
                        s = b.getString(key);
                }
            }
            else {
                ResourceBundle b = interview.getResourceBundle();
                if (b != null)
                    s = b.getString(key);
                if (s == null && p != null)
                    s = getResourceString(p, key, checkAncestorsFirst);
            }
            //System.err.println("QU: " + interview.getTag() + " " + key + " " + s);
            return s;
        }
        catch (MissingResourceException e) {
            //System.err.println("QU: " + interview.getTag() + " " + key + " -null-");
            return null;
        }
        */
    }

    /**
     * The interview to which this question belongs.
     */
    protected Interview interview;

    /**
     * A unique tag to identify this question.
     */
    protected String tag; // parent tag + baseTag

    private String baseTag; // tag relative to parent

    /**
     * A unique key to identify the resources for this question.
     */
    protected final String key;

    /**
     * The text of this question.
     */
    private String text;

    /**
     * The summary of this question.
     */
    private String summary;

    /**
     * A URL for a graphic for this question.
     */
    private URL image;
}
