// This may look like C code, but it's really -*- C++ -*-
/*
 * Copyright (C) 2008 Emweb bvba, Kessel-Lo, Belgium.
 *
 * See the LICENSE file for terms of use.
 */
#ifndef WSUGGESTION_POPUP_H_
#define WSUGGESTION_POPUP_H_

#include <Wt/WCompositeWidget>
#include <Wt/WJavaScript>

namespace Wt {

class WAbstractItemModel;
class WModelIndex;
class WFormWidget;
class WTemplate;

/*! \class WSuggestionPopup Wt/WSuggestionPopup Wt/WSuggestionPopup
 *  \brief A widget which popups to assist in editing a textarea or lineedit.
 *
 * This widget may be associated with one or more \link WFormWidget
 * WFormWidgets\endlink (typically a WLineEdit or a WTextArea).
 *
 * When the user starts editing one of the associated widgets, this
 * popup will show just below it, offering a list of suggestions that
 * match in some way with the current edit. The mechanism for
 * filtering the total list of suggestions must be specified through a
 * separate JavaScript function. This function may also highlight part(s)
 * of the suggestions to provide feed-back on how they match.
 *
 * %WSuggestionPopup is an MVC view class, using a simple
 * WStringListModel by default. You can set a custom model using
 * setModel(). The member methods clearSuggestions() and
 * addSuggestion() manipulate the model.
 *
 * The class is initialized with two JavaScript functions, one for
 * filtering the suggestions, and one for editing the value of the
 * textarea when a suggestion is selected. Two static methods,
 * generateMatcherJS() and generateReplacerJS() may be used to
 * generate these functions based on a set of options (in the Options
 * struct). If the flexibility provided in this way is not sufficient,
 * and writing JavaScript does not give you an instant heart-attack,
 * you may provide your own implementations.
 *
 * The matcherJS function block must have the following JavaScript signature:
 * 
 * \code
 * function (editElement) {
 *   // fetch the location of cursor and current text in the editElement.
 *
 *   // return a function that matches a given suggestion with the current value of the editElement.
 *   return function(suggestion) {
 *
 *     // 1) remove markup from the suggestion
 *     // 2) check suggestion if it matches
 *     // 3) add markup to suggestion
 *
 *     return { match : ...,      // does the suggestion match ? (boolean)
 *              suggestion : ...  // modified suggestion markup
 *             };
 *   }
 * }
 * \endcode
 *
 * The replacerJS function block that edits the value has the following
 * JavaScript signature.
 *
 * \code
 * function (editElement, suggestionText, suggestionValue) {
 *   // editElement is the form element which must be edited.
 *   // suggestionText is the displayed text for the matched suggestion.
 *   // suggestionValue is the stored value for the matched suggestion.
 *
 *   // computed modifiedEditValue and modifiedPos ...
 *
 *   editElement.value = modifiedEditValue;
 *   editElement.selectionStart = edit.selectionEnd = modifiedPos;
 * }
 * \endcode
 *
 * To style the suggestions, you should style the \<span\> element inside
 * this widget, and the \<span\>."sel" element to style the current selection.
 *
 * Usage example:
 * \if cpp
 * \code
 * // options for email address suggestions
 * Wt::WSuggestionPopup::Options contactOptions
 * = { "<b>",         // highlightBeginTag
 *     "</b>",        // highlightEndTag
 *     ',',           // listSeparator      (for multiple addresses)
 *     " \\n",        // whitespace
 *     "-., \"@\\n;", // wordSeparators     (within an address)
 *     ", "           // appendReplacedText (prepare next email address)
 *    };
 *
 * Wt::WSuggestionPopup *popup
 *   = new Wt::WSuggestionPopup(Wt::WSuggestionPopup::generateMatcherJS(contactOptions),
 *	                      Wt::WSuggestionPopup::generateReplacerJS(contactOptions),
 *                              this);
 * Wt::WTextArea *textEdit = new Wt::WTextArea(this);
 * popup->forEdit(textEdit);
 *
 * // load popup data
 * for (unsigned i = 0; i < contacts.size(); ++i)
 *   popup->addSuggestion(contacts[i].formatted(), contacts[i].formatted());
 * \endcode
 * \elseif java
 * \code
 * // options for email address suggestions
 * WSuggestionPopup.Options contactOptions = new WSuggestionPopup.Options(); 
 * contactOptions.highlightBeginTag = "<b>";
 * contactOptions.highlightEndTag = "</b>";
 * contactOptions.listSeparator = ','; //for multiple addresses)
 * contactOptions.whitespace = " \\n";
 * contactOptions.wordSeparators = "-., \"@\\n;"; //within an address
 * contactOptions.appendReplacedText = ", "; //prepare next email address
 *	
 * WSuggestionPopup popup
 *  = new WSuggestionPopup(WSuggestionPopup.generateMatcherJS(contactOptions),
 * WSuggestionPopup.generateReplacerJS(contactOptions),
 * this);
 * 
 * WTextArea textEdit = new WTextArea(this);
 * popup.forEdit(textEdit);
 *		 
 * // load popup data
 * for (int i = 0; i < contacts.size(); ++i)
 * popup.addSuggestion(contacts.get(i).formatted(), contacts.get(i).formatted());
 * \endcode
 * \endif
 *
 * A screenshot of this example:
 * <TABLE border="0" align="center"> <TR> <TD> 
 * \image html WSuggestionPopup-default-1.png "An example WSuggestionPopup (default)"
 * </TD> <TD>
 * \image html WSuggestionPopup-polished-1.png "An example WSuggestionPopup (polished)"
 * </TD> </TR> </TABLE>
 *
 * <h3>CSS</h3>
 *
 * The suggestion popup is styled by the current CSS theme. The look can be
 * overridden using the <tt>Wt-suggest</tt> CSS class and the following
 * selectors:
 *
 * \verbatim
.Wt-suggest span : A suggestion element
.Wt-suggest .sel : A selected suggestion element
\endverbatim
 *
 * \note This widget requires JavaScript support.
 *
 * \ingroup modelview
 */
class WT_API WSuggestionPopup : public WCompositeWidget
{
public:
  /*! \brief Creates a suggestion popup with given matcherJS and replacerJS.
   */
  WSuggestionPopup(const std::string& matcherJS,
		   const std::string& replacerJS,
		   WContainerWidget *parent = 0);

  /*! \brief Lets this suggestion popup assist in editing the given edit field.
   *
   * A single suggestion popup may assist in several edits by repeated calls
   * of this method.
   */
  void forEdit(WFormWidget *edit);

  /*! \brief Clears the list of suggestions.
   */
  void clearSuggestions();

  /*! \brief Adds a new suggestion.
   */
  void addSuggestion(const WString& suggestionText,
		     const WString& suggestionValue);

  /*! \brief Sets the model to be used for the suggestions.
   *
   * The \p model may not be \c 0, and ownership of the model is not
   * transferred.
   *
   * The default value is a WStringListModel that is owned by the
   * suggestion popup.
   *
   * The Wt::DisplayRole is used for the suggestion text. The
   * Wt::UserRole is used for the suggestion value, unless empty, in
   * which case the suggestion text is used as value.
   *
   * Note that since the default %WStringListModel does not support
   * UserRole data, you will want to change it to a more general model
   * (e.g. WStandardItemModel) if you want suggestion values that are
   * different from display values.
   *
   * \sa setModelColumn(int)
   */
  void setModel(WAbstractItemModel *model);

  /*! \brief Sets the column in the model to be used for the items.
   *
   * The column \p index in the model will be used to retrieve data.
   *
   * The default value is 0.
   *
   * \sa setModel()
   */
  void setModelColumn(int index);

  /*! \brief Returns the data model.
   *
   * \sa setModel()
   */
  WAbstractItemModel *model() const { return model_; }

  /*! \brief A configuration object to generate a matcher and replacer
   *         JavaScript function.
   */
  struct Options {
    /*! \brief Open tag to highlight a match in a suggestion.
     *
     * Must be an opening markup tag, such as &lt;B&gt;. The tag
     * name must be all uppercase! (really?)
     */
    std::string highlightBeginTag;

    /*! \brief Close tag to highlight a match in a suggestion.
     *
     * Must be a closing markup tag, such as &lt;/B&gt;. The tag
     * name must be all uppercase!
     */
    std::string highlightEndTag;

    /*! \brief When editing a list of values, the separator used
     *         for different items.
     *
     * For example, ',' to separate different values on komma. Specify
     * 0 ('\\0') for no list separation.
     */
    char listSeparator;

    /*! \brief When editing a value, the whitespace characters ignored
     *         before the current value.
     *
     * For example, " \\n" to ignore spaces and newlines.
     */
    std::string whitespace;

    /*! \brief To show suggestions based on matches of the edited
     *         value with parts of the suggestion.
     *
     * For example, " .@" will also match with suggestion text after a space,
     * a dot (.) or an at-symbol (@).
     */
    std::string wordSeparators;

    /*! \brief When replacing the current edited value with suggestion value,
     *         append the following string as well.
     */
    std::string appendReplacedText;
  };

  /*! \brief Creates a matcher JavaScript function based on some
   *         generic options.
   */
  static std::string generateMatcherJS(const Options& options);

  /*! \brief Creates a replacer JavaScript function based on some
   *         generic options.
   */
  static std::string generateReplacerJS(const Options& options);

private:
  WTemplate *impl_;
  WAbstractItemModel *model_;
  int modelColumn_;

  std::string       matcherJS_;
  std::string       replacerJS_;
  WContainerWidget *content_;

  std::vector<boost::signals::connection> modelConnections_;

  JSlot editKeyDown_;
  JSlot editKeyUp_;
  JSlot suggestionClicked_;
  JSlot delayHide_;

  void setJavaScript(JSlot& slot, const std::string& methodName);

  void modelRowsInserted(const WModelIndex& parent, int start, int end);
  void modelRowsRemoved(const WModelIndex& parent, int start, int end);
  void modelDataChanged(const WModelIndex& topLeft,
			const WModelIndex& bottomRight);
  void modelLayoutChanged();

  void defineJavaScript();

protected:
  virtual void render(WFlags<RenderFlag> flags);
};

}

#endif // WSUGGESTION_POPUP_H_
