/* 
 * Copyright (c) 2011, 2013, Oracle and/or its affiliates. All rights reserved.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; version 2 of the
 * License.
 * 
 * This program 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 for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301  USA
 */

#include "stdafx.h"

#include <boost/signals2/signal.hpp>
#include <pcrecpp.h>

#include "query_side_palette.h"
#include "wb_sql_editor_snippets.h"
#include "wb_sql_editor_help.h"
#include "grt/tree_model.h"
#include "snippet_popover.h"
#include "snippet_list.h"
#include "grt/grt_manager.h"

#include "base/geometry.h"
#include "base/notifications.h"
#include "base/drawing.h"

#include "mforms/toolbar.h"
#include "mforms/hypertext.h"
#include "mforms/scrollpanel.h"
#include "mforms/drawbox.h"
#include "mforms/utilities.h"
#include "mforms/code_editor.h"


#define USER_SNIPPETS "My Snippets"

using namespace mforms;
using namespace base;

//----------------- SnippetList --------------------------------------------------------------------

/**
  * Internal class containing a list of snippet entries in a scrollable list.
  */
class SnippetList : public BaseSnippetList
{
private:
  wb::SnippetPopover* _snippet_popover;
  bool _user_snippets_active;

private:

  DbSqlEditorSnippets* model() { return dynamic_cast<DbSqlEditorSnippets*>(_model); }

  void popover_closed()
  {
    if (_snippet_popover->has_changed())
    {
      std::string title = _snippet_popover->get_heading();
      model()->set_field(bec::NodeId(_selected_index), DbSqlEditorSnippets::Description, title);
      std::string sub_title = _snippet_popover->get_text();
      model()->set_field(bec::NodeId(_selected_index), DbSqlEditorSnippets::Script, sub_title);
      if (_selected_snippet)
        set_snippet_info(_selected_snippet, title, sub_title);
      model()->save();

      set_needs_repaint();
    }
  }

  //------------------------------------------------------------------------------------------------

  void prepare_context_menu()
  {
    _context_menu = manage(new Menu());
    _context_menu->set_handler(boost::bind(&SnippetList::on_action, this, _1));
    _context_menu->signal_will_show()->connect(boost::bind(&SnippetList::menu_will_show, this));

    _context_menu->add_item(_("Insert Snippet at Cursor"), "insert_text");
    _context_menu->add_item(_("Replace Editor Content with Snippet"), "replace_text");
    _context_menu->add_item(_("Execute Snippet"), "exec_snippet");
    _context_menu->add_separator();
    _context_menu->add_item(_("Copy Snippet to Clipboard"), "copy_to_clipboard");
    _context_menu->add_separator();
    _context_menu->add_item(_("Edit Snippet"), "edit_snippet");
    _context_menu->add_item(_("Add Snippet from Editor Content"), "add_snippet");
    _context_menu->add_item(_("Delete Snippet"), "del_snippet");
    _context_menu->add_separator();
    _context_menu->add_item(_("Restore Original Snippet List"), "restore_snippets");
  }

  //------------------------------------------------------------------------------------------------

  void menu_will_show()
  {
    _context_menu->set_item_enabled(0, _selected_index > -1);
    _context_menu->set_item_enabled(1, _selected_index > -1);
    _context_menu->set_item_enabled(2, _selected_index > -1);
    _context_menu->set_item_enabled(4, _selected_index > -1);
    _context_menu->set_item_enabled(6, _selected_index > -1);
    _context_menu->set_item_enabled(8, _selected_index > -1);
    _context_menu->set_item_enabled(10, !_user_snippets_active);
  }

  //------------------------------------------------------------------------------------------------

  void on_action(const std::string& action)
  {
    if (action == "edit_snippet")
    {
      if (_selected_snippet)
        edit_snippet(_selected_snippet);
    }
    else
      model()->activate_toolbar_item(bec::NodeId(_selected_index), action);

    // Refresh display if we added or removed a snippet.
    if (action == "add_snippet" || action == "del_snippet" || action == "restore_snippets")
      refresh_snippets();
  }

  //------------------------------------------------------------------------------------------------

public:
  SnippetList(const std::string& icon_name)
  : BaseSnippetList(icon_name, DbSqlEditorSnippets::get_instance())
  {
    _user_snippets_active = false;

    _snippet_popover = new wb::SnippetPopover();
    _snippet_popover->set_size(376, 257);
    _snippet_popover->signal_closed()->connect(boost::bind(&SnippetList::popover_closed, this));

    prepare_context_menu();
  }

  //------------------------------------------------------------------------------------------------

  ~SnippetList()
  {
    delete _snippet_popover;
    _context_menu->release();
  }

  //------------------------------------------------------------------------------------------------

  std::string selected_category() { return model()->selected_category(); }

  //------------------------------------------------------------------------------------------------

  /**
   * Updates the content depending on the selected snippet group.
   */
  void show_category(std::string category)
  {
    _user_snippets_active = (category == USER_SNIPPETS);
    model()->select_category(category);
    refresh_snippets();
  }

  //------------------------------------------------------------------------------------------------
  
  void edit_snippet(Snippet *snippet)
  {
    base::Rect bounds = snippet_bounds(snippet);

    int left = (int) bounds.left();
    int dummy = left;
    int top = (int) bounds.top();
    int bottom = (int) bounds.bottom();
    client_to_screen(left, top);
    client_to_screen(dummy, bottom);
    top = (top + bottom) / 2; // Don't change this to top + height / 2, on OS X coordinates are flipped.

    std::string title, description;
    get_snippet_info(snippet, title, description);

    _snippet_popover->set_heading(title);
    _snippet_popover->set_read_only(false);
    _snippet_popover->set_text(description);
    _snippet_popover->set_read_only(true);
    _snippet_popover->show(left, top, mforms::Left);
  }

  //------------------------------------------------------------------------------------------------

  virtual void mouse_double_click(int button, int x, int y)
  {
    BaseSnippetList::mouse_double_click(button, x, y);

    switch (button)
    {
    case MouseButtonLeft:
      {
        Snippet* snippet = snippet_from_point(x, y);
        if (snippet != NULL && snippet == _selected_snippet)
        {
          edit_snippet(snippet);
        }
        break;
      }
    }
  }

  //------------------------------------------------------------------------------------------------

  void close_popover()
  {
    _snippet_popover->close();
  }

  //------------------------------------------------------------------------------------------------

  // End of internal class SnippetList.
};

//----------------- QuerySidePalette ---------------------------------------------------------------

QuerySidePalette::QuerySidePalette(const SqlEditorForm::Ref &owner)
  : 
#ifdef _WIN32
    TabView(mforms::TabViewPalette),
#else
    TabView(mforms::TabViewSelectorSecondary),
#endif
    _owner(owner)
{
  _help_timer = NULL;
  _grtm = owner->grt_manager();
  _no_help = true;
  _enable_help = true;
  _switching_help = false;

  scoped_connect(signal_tab_changed(), boost::bind(&QuerySidePalette::tab_changed, this));

  mforms::Box *help_page = manage(new Box(false));
  _help_toolbar = prepare_help_toolbar();
  _help_text = manage(new mforms::HyperText());

  // Separate box since we need a border around the content, but not the toolbar.
  Box* content_border = manage(new Box(false));

  // No scoped_connect needed since _help_text is member variable.
  scoped_connect(_help_text->signal_link_click(), boost::bind(&QuerySidePalette::click_link, this, _1));

#if _WIN32
  _help_text->set_back_color(base::Color::get_application_color_as_string(AppColorPanelContentArea, false));
  _help_text->set_font("Tahoma 8");
  content_border->set_padding(3, 3, 3, 3);
#else
  _help_text->set_back_color("#ebebeb");
#endif

  _help_text->set_markup_text("");
  _current_topic = -1;

  help_page->add(_help_toolbar, false, true);
  content_border->add(_help_text, true, true);
  help_page->add(content_border, true, true);
  add_page(help_page, _("Context Help"));

  Box* snippet_page = manage(new Box(false));

  content_border = manage(new Box(false));
  _snippet_list = manage(new SnippetList("snippet_sql.png"));
#ifdef _WIN32
  content_border->set_padding(3, 3, 3, 3);
  _snippet_list->set_back_color(base::Color::get_application_color_as_string(AppColorPanelContentArea, false));
#else
  _snippet_list->set_back_color("#f2f2f2");
#endif
  _snippet_box = manage(new ScrollPanel());
  _snippet_box->add(_snippet_list);

  DbSqlEditorSnippets* snippets_model = DbSqlEditorSnippets::get_instance();
  std::vector<std::string> snippet_categories = snippets_model->get_category_list();
  if (snippet_categories.size() > 0)
    _snippet_list->show_category(snippet_categories[0]);
  else
    _snippet_list->show_category(USER_SNIPPETS);

  _snippet_toolbar = prepare_snippet_toolbar();

  snippet_page->add(_snippet_toolbar, false, true);
  content_border->add(_snippet_box, true, true);
  snippet_page->add(content_border, true, true);
  add_page(snippet_page, "Snippets");

  scoped_connect(_snippet_list->signal_selection_changed(), boost::bind(&QuerySidePalette::snippet_selection_changed, this));

  std::string old_category = _grtm->get_app_option_string("DbSqlEditor:SelectedSnippetCategory");
  if (!old_category.empty())
  {
    mforms::ToolBarItem *item = _snippet_toolbar->find_item("select_category");
    item->set_text(old_category);
    snippet_toolbar_item_activated(item);
  }

  snippet_selection_changed();

  base::NotificationCenter::get()->add_observer(this, "GNTextSelectionChanged");
}

//--------------------------------------------------------------------------------------------------

QuerySidePalette::~QuerySidePalette()
{
  base::NotificationCenter::get()->remove_observer(this);

  if (_help_timer != NULL)
    _grtm->cancel_timer(_help_timer);
  _back_item->release();
  _forward_item->release();
  _quick_jump_item->release();
}

void QuerySidePalette::cancel_timer()
{
  if (_help_timer != NULL)
    _grtm->cancel_timer(_help_timer);
}

//--------------------------------------------------------------------------------------------------

void QuerySidePalette::handle_notification(const std::string &name, void *sender, base::NotificationInfo &info)
{
  if (_enable_help)
  {
    if (name == "GNTextSelectionChanged") // Selection and caret changes.
    {
      mforms::Object *object = (mforms::Object*)sender;
      mforms::CodeEditor *code_editor = dynamic_cast<mforms::CodeEditor*>(object);
      if (code_editor == NULL)
        return;

      Sql_editor *editor = static_cast<Sql_editor*>(code_editor->get_host());
      // XXX This event is sent for any code editor in the app, not only the ones in this editor.. so we need a better check here
      if (editor && editor->grtobj().is_valid())
      {
        check_format_structures(editor);
        if (_help_timer != NULL)
          _grtm->cancel_timer(_help_timer);
        _help_timer = _grtm->run_every(boost::bind(&QuerySidePalette::update_context_help, this, editor), 0.7);
      }
    }
  }
} 

//--------------------------------------------------------------------------------------------------

void QuerySidePalette::tab_changed()
{
  _enable_help = get_active_tab() == 0; // Only show help if the help page is actually active.
}

//--------------------------------------------------------------------------------------------------

void QuerySidePalette::show_help_topic(std::string topic)
{
  std::string title, text, html_text;

  topic = base::toupper(topic);
  if (!topic.empty())
  {
    if (_topic_cache.find(topic) != _topic_cache.end())
    {
      std::pair<std::string, std::string> entry = _topic_cache[topic];
      html_text = entry.second;
      _help_text->set_markup_text(html_text);
      _no_help = false;
    }
  }

  if (html_text.empty() && !topic.empty())
  {
    SqlEditorForm::Ref form = _owner.lock();
    if (DbSqlEditorContextHelp::get_help_text(form, topic, title, text))
    {
#ifdef _WIN32
      std::string additional_space = "<div style=\"font-size:4pt\"> </div>";
#else
      std::string additional_space;
#endif
      html_text = std::string("<html><body style=\"font-family:") + DEFAULT_FONT_FAMILY + "; font-size: 8pt\">"
        "<b style=\"font-size: 11pt\">Topic: <span style=\"color:#688b5e\">" + title +
        "</span></b><br>" + additional_space + format_help_as_html(text) + "</body></html>";
      _help_text->set_markup_text(html_text);
      _no_help = false;
      _topic_cache[topic] = std::make_pair(text, html_text);
    }
  }

  if (html_text.empty())
  {
    _no_help = true;
    _help_text->set_markup_text(
      std::string("<hmtl><body style=\"font-family:") + DEFAULT_FONT_FAMILY + "; font-size: 8pt\">"
      "<div style=\"width: 100%\"><b style=\"font-size: 10pt; color:#B0B0B0\">"
      "No Context Help<b><br><br><hr></div></body></html>");
  }

  _switching_help = true;
  _quick_jump_item->set_text(topic);
  _switching_help = false;
}

//--------------------------------------------------------------------------------------------------

bool QuerySidePalette::update_context_help(Sql_editor *editor)
{
  _help_timer = NULL;

  std::string text, title;

   // Caret position as <column, row>.
  std::pair<int, int> caret = editor->cursor_pos_row_column(true);

  SqlEditorForm::Ref form = _owner.lock();
  std::string topic = DbSqlEditorContextHelp::find_help_topic_from_position(form,
    editor->current_statement(), caret);
  if (update_help_history(topic))
    show_help_topic(topic);
  
  return false; // Don't re-run this task, it's a single-shot.
}

//--------------------------------------------------------------------------------------------------

/**
 * Adds the given topic to the topic history if the current topic is not the same and updates
 * the forward/backward buttons.
 *
 * @returns True if the topic was added to the history, otherwise false.
 */
bool QuerySidePalette::update_help_history(std::string topic)
{
  topic = base::toupper(topic);
  if (_current_topic > 0 && _topic_history[_current_topic] == topic)
    return _no_help;

  if (!topic.empty())
  {
    _current_topic++;

    // Remove everything after the new topic position.
    _topic_history.erase(_topic_history.begin() + _current_topic, _topic_history.end());
    _topic_history.push_back(topic);
    _back_item->set_enabled(_current_topic > 0);
    _forward_item->set_enabled(false);
  }
  return true;
}

//--------------------------------------------------------------------------------------------------

void QuerySidePalette::click_link(const std::string &link)
{
  if (link.find("local:") == 0)
  {
    // Internal link.
    std::string topic = base::trim(link.substr(6, link.size() - 6));
    base::replace(topic, "%20", " ");
    while (topic.find("  ") != std::string::npos)
      base::replace(topic, "  ", " ");
    if (update_help_history(topic))
      show_help_topic(topic);
  }
  else
    mforms::Utilities::open_url(link);
}

//--------------------------------------------------------------------------------------------------

ToolBar* QuerySidePalette::prepare_snippet_toolbar()
{
  ToolBar* toolbar = manage(new ToolBar(mforms::PaletteToolBar));
  toolbar->set_name("snippet_toolbar");
#ifndef __APPLE__
  toolbar->set_padding(0, 0, 0, 0);
  toolbar->set_size(-1, 27);
#endif
  ToolBarItem* item = manage(new ToolBarItem(mforms::ActionItem));
  item->set_name("add_snippet");
  item->set_icon(App::get()->get_resource_path("snippet_add.png"));
  item->set_tooltip(_("Add the current editor content as new snippet"));
  scoped_connect(item->signal_activated(), boost::bind(&QuerySidePalette::snippet_toolbar_item_activated, this, _1));
  toolbar->add_item(item);
  item->release();

  /* not needed in toolbar
  item = manage(new ToolBarItem(mforms::ActionItem));
  item->set_name("del_snippet");
  item->set_icon(App::get()->get_resource_path("snippet_del.png"));
  item->set_tooltip(_("Delete the currently selected snippet"));
  scoped_connect(item->signal_activated(), boost::bind(&QuerySidePalette::snippet_toolbar_item_activated, this, _1));
  toolbar->add_item(item);
  item->release();
   */

  item = manage(new ToolBarItem(mforms::SeparatorItem));
  toolbar->add_item(item);
  item->release();

  item = manage(new ToolBarItem(mforms::SelectorItem));
  item->set_name("select_category");

  DbSqlEditorSnippets* snippets_model = DbSqlEditorSnippets::get_instance();
  item->set_selector_items(snippets_model->get_category_list());
  scoped_connect(item->signal_activated(), boost::bind(&QuerySidePalette::snippet_toolbar_item_activated, this, _1));
  item->set_text(USER_SNIPPETS);
  item->set_tooltip(_("Select a snippet category for display"));
  toolbar->add_item(item);
  item->release();

  item = manage(new ToolBarItem(mforms::SeparatorItem));
  toolbar->add_item(item);
  item->release();


  item = manage(new ToolBarItem(mforms::ActionItem));
  item->set_name("replace_text");
  item->set_icon(App::get()->get_resource_path("snippet_use.png"));
  item->set_tooltip(_("Replace the current text by this snippet"));
  scoped_connect(item->signal_activated(), boost::bind(&QuerySidePalette::snippet_toolbar_item_activated, this, _1));
  toolbar->add_item(item);
  item->release();

  item = manage(new ToolBarItem(mforms::ActionItem));
  item->set_name("insert_text");
  item->set_icon(App::get()->get_resource_path("snippet_insert.png"));
  item->set_tooltip(_("Insert the snippet text at the current caret position replacing selected text if there is any"));
  scoped_connect(item->signal_activated(), boost::bind(&QuerySidePalette::snippet_toolbar_item_activated, this, _1));
  toolbar->add_item(item);
  item->release();

  item = manage(new ToolBarItem(mforms::ActionItem));
  item->set_name("copy_to_clipboard");
  item->set_icon(App::get()->get_resource_path("snippet_clipboard.png"));
  item->set_tooltip(_("Copy the snippet text to the clipboard"));
  scoped_connect(item->signal_activated(), boost::bind(&QuerySidePalette::snippet_toolbar_item_activated, this, _1));
  toolbar->add_item(item);
  item->release();

  return toolbar;
}

//--------------------------------------------------------------------------------------------------

void QuerySidePalette::snippet_toolbar_item_activated(ToolBarItem* item)
{
  std::string action = item->get_name();
  if (action == "select_category")
  {
    _snippet_list->show_category(item->get_text());
    _grtm->set_app_option("DbSqlEditor:SelectedSnippetCategory", grt::StringRef(item->get_text()));
  }
  else
  {
    DbSqlEditorSnippets* snippets_model = DbSqlEditorSnippets::get_instance();
    snippets_model->activate_toolbar_item(bec::NodeId(_snippet_list->selected_index()), action);

    // Refresh display if we added or removed a snippet.
    if (action == "add_snippet" || action == "del_snippet")
      _snippet_list->refresh_snippets();
  }
}


//--------------------------------------------------------------------------------------------------

void QuerySidePalette::snippet_selection_changed()
{
  bool has_selection = _snippet_list->selected_index() > -1;
//  bool user_snippets_active = _snippet_list->selected_category() == USER_SNIPPETS;
 // _snippet_toolbar->set_item_enabled("add_snippet", true);
  _snippet_toolbar->set_item_enabled("del_snippet", has_selection);
  _snippet_toolbar->set_item_enabled("copy_to_clipboard", has_selection);
  _snippet_toolbar->set_item_enabled("replace_text", has_selection);
  _snippet_toolbar->set_item_enabled("insert_text", has_selection);
}

//--------------------------------------------------------------------------------------------------

/**
 * Called by the owning form if the main form changes to remove the popover.
 */
void QuerySidePalette::close_popover()
{
  _snippet_list->close_popover();
}

//--------------------------------------------------------------------------------------------------

ToolBar* QuerySidePalette::prepare_help_toolbar()
{
  ToolBar* toolbar = manage(new ToolBar(mforms::PaletteToolBar));
  toolbar->set_name("help_toolbar");
#ifndef __APPLE__
  toolbar->set_padding(0, 0, 0, 0);
  toolbar->set_size(-1, 27);
#endif

  _back_item = manage(new ToolBarItem(mforms::ActionItem));
  _back_item->set_name("back");
  _back_item->set_icon(App::get()->get_resource_path("wb-toolbar_nav-back.png"));
  _back_item->set_tooltip(_("One topic back"));
  _back_item->set_enabled(false);
  scoped_connect(_back_item->signal_activated(), boost::bind(&QuerySidePalette::help_toolbar_item_activated, this, _1));
  toolbar->add_item(_back_item);

  _forward_item = manage(new ToolBarItem(mforms::ActionItem));
  _forward_item->set_name("forward");
  _forward_item->set_icon(App::get()->get_resource_path("wb-toolbar_nav-forward.png"));
  _forward_item->set_tooltip(_("One topic forward"));
  _forward_item->set_enabled(false);
  scoped_connect(_forward_item->signal_activated(), boost::bind(&QuerySidePalette::help_toolbar_item_activated, this, _1));
  toolbar->add_item(_forward_item);

  ToolBarItem* item = manage(new ToolBarItem(mforms::SeparatorItem));
  toolbar->add_item(item);
  item->release();

  _quick_jump_item = manage(new ToolBarItem(mforms::SelectorItem));
  _quick_jump_item->set_name("quick_jump");

  std::vector<std::string> topic_entries;
  topic_entries.push_back(_("Jump to"));
  topic_entries.push_back("SELECT");
  topic_entries.push_back("UPDATE");
  topic_entries.push_back("INSERT");
  topic_entries.push_back("DELETE");
  topic_entries.push_back("CREATE TABLE");
  topic_entries.push_back("CREATE VIEW");
  topic_entries.push_back("CREATE PROCEDURE");
  topic_entries.push_back("CREATE FUNCTION");
  topic_entries.push_back("ALTER TABLE");
  _quick_jump_item->set_selector_items(topic_entries);
  _quick_jump_item->set_text(_("Jump To"));
  scoped_connect(_quick_jump_item->signal_activated(), boost::bind(&QuerySidePalette::help_toolbar_item_activated, this, _1));
  toolbar->add_item(_quick_jump_item);

  return toolbar;
}

//--------------------------------------------------------------------------------------------------

void QuerySidePalette::help_toolbar_item_activated(ToolBarItem* item)
{
  if (_switching_help)
    return;

  std::string action = item->get_name();
  if (action == "back" && _current_topic > 0)
  {
    std::string topic = _topic_history[--_current_topic];
    _back_item->set_enabled(_current_topic > 0);
    _forward_item->set_enabled(true);
    show_help_topic(topic);
    return;
  }

  if (action == "forward" && _current_topic < (int)_topic_history.size() - 1)
  {
    std::string topic = _topic_history[++_current_topic];
    _back_item->set_enabled(true);
    _forward_item->set_enabled(_current_topic < (int)_topic_history.size() - 1);
    show_help_topic(topic);
    return;
  }

  if (action == "quick_jump")
  {
    std::string topic = _quick_jump_item->get_text();
    if (update_help_history(topic))
      show_help_topic(topic);
    return;
  }

  if (_current_topic >= 0)
  {
    if (action == "copy_to_clipboard")
    {
      std::pair<std::string, std::string> entry = _topic_cache[_topic_history[_current_topic]];
      mforms::Utilities::set_clipboard_text(entry.first);
    }
    if (action == "copy_html_to_clipboard")
    {
      std::pair<std::string, std::string> entry = _topic_cache[_topic_history[_current_topic]];
      mforms::Utilities::set_clipboard_text(entry.second);
    }
  }
}

//--------------------------------------------------------------------------------------------------

bool compare_lengths(const std::pair<std::string, std::string> &l, const std::pair<std::string, std::string> &r)
{
  return l.first.size() > r.first.size();
}

//--------------------------------------------------------------------------------------------------

void QuerySidePalette::check_format_structures(Sql_editor *editor)
{
  mforms::CodeEditorConfig *config = editor->get_editor_settings();

  if (_keyword_color.empty())
  {
    // We use the major keyword color for all keywords in the help. Just to simplify things.
    std::map<std::string, std::string> major_style = config->get_styles()[7]; // Major keyword.
    _keyword_color = major_style["fore-color"];
    if (_keyword_color.empty())
      _keyword_color = "#000000";
  }

  // Initialize pattern -> color associations if not yet done.
  // We don't consider background colors atm.
  if (_pattern_and_colors.empty())
  {
    std::map<int, std::map<std::string, std::string> > styles = config->get_styles();
    _pattern_and_colors.push_back(std::make_pair("([^A-Za-z])([0-9]+(\\.[0-9]+)?((e|E)[0-9]+)?)", styles[6]["fore-color"]));
    _pattern_and_colors.push_back(std::make_pair("/\\*.*?\\*/", styles[1]["fore-color"]));
    _pattern_and_colors.push_back(std::make_pair("--\\s.*", styles[2]["fore-color"]));
    _pattern_and_colors.push_back(std::make_pair("@@?([A-Za-z]+|'.*?')", styles[3]["fore-color"])); // user and system variable
    _pattern_and_colors.push_back(std::make_pair("'.*?'", styles[12]["fore-color"]));
    _pattern_and_colors.push_back(std::make_pair("`.*?`", styles[17]["fore-color"]));
    // _pattern_and_colors.push_back(std::make_pair("/\\*!.*\\*/", styles[21]["fore-color"])); don't convert hidden command.
    //_pattern_and_colors.push_back(std::make_pair("\".*?\"", styles[13]["fore-color"])); collides with already done markup
  }
}

//--------------------------------------------------------------------------------------------------

std::string QuerySidePalette::format_help_as_html(const std::string &text)
{
  // Start by extracting the syntax part which is always at the top. This will be embedded into
  // <pre> tags to stay as formatted.
  std::string result, line, block;
  std::istringstream stream(text);

  // Line was read already for the syntax block check (if there's any input remaining yet).
  while (std::getline(stream, line))
  {
    block = "";
    
    if (line.empty())
    {
      result += "<br><br>";
      continue;
    }

    // First look for list entries es they are similarly formatted like code sections, so we catch lists first.
    if (line.find("o ") == 0)
    {
      // A list starts. End is reached if we find two linebreaks followed by anything not indented
      // and which is not a list starter.
      block = "<ul>";
      std::string pending = line.substr(2, line.size());
      while (std::getline(stream, line))
      {
        if (line.empty())
        {
          // List item might be done (or just has an empty line in it).
          // We might even be done with the entire list.
          if (!std::getline(stream, line))
            break;

          if (line.find("  ") == 0)
          {
            // Just another item part.
            pending += "<br><br>" + line;
            continue;
          }

          if (line.find("o ") == 0)
          {
            // Found a new list item. Add the pending one and start over.
            block += "<li>" + pending + "</li>";
            pending = line.substr(2, line.size());
          }
          else
            break; // We are done.
        }
        else
          pending += line; // Another part of the current list item.
      }

      // Last pending list element.
      result += block + "<li>" + pending + "</li></ul><br>" + line;
      
      continue;
    }

    // Syntax lines consist usually of a lead line followed by indented trail lines.
    // A code block ends on an empty line.
    if (line.find("mysql>") == 0)
      block = "mysql&gt;" + line.substr(6, line.size()) + '\n';
    else
      if (line[line.size() - 1] == ';')
        block = line;
    {
      std::istream::pos_type position = stream.tellg();
      std::string pending;
      if (std::getline(stream, pending) && pending.find("  ") == 0)
        block = line + '\n' + pending + '\n';
      else
      {
        // Track back and start over with the next line.
        stream.seekg(position);
      }
    }

    if (!block.empty())
    {
      while (std::getline(stream, line))
      {
        block += line + '\n';
        if (line.empty())
          break;
      }

      // Done with the code block. Colorize elements.
      // Note: this has some limitations because certain text can be catched by several patterns.
      for (size_t i = 0; i < _pattern_and_colors.size(); i++)
      {
        pcrecpp::RE pattern(_pattern_and_colors[i].first);
        if (i == 0)
          pattern.GlobalReplace("\\1<span style=\"color:" + _pattern_and_colors[i].second + "\">\\2</span>", &block);
        else
          pattern.GlobalReplace("<span style=\"color:" + _pattern_and_colors[i].second + "\">\\0</span>", &block);
      }


      result += "<pre style=\"font-family: " + std::string(DEFAULT_MONOSPACE_FONT_FAMILY) +
      "; font-size: 8pt;\">" + block + "</pre>";

      continue;
    }

    // Wrap emphasized text by <b>.
    pcrecpp::RE pattern = "\\*([A-Za-z]+)\\*";
    pattern.GlobalReplace("<b>\\1</b>", &line);

    // Replace triple hyphen by typographic dash.
    pattern = "---";
    pattern.GlobalReplace("&mdash;", &line);
    
    result += " " + line;
  }

  // Replace "URL: <link>" by "See Also: <link>"
  pcrecpp::RE pattern = "URL:";
  pattern.Replace("<b>See also:</b>", &result);

  // Color all upper case words with at least 2 chars (which should mostly cover keywords).
  // This step will mess up internal links, so we have to correct them.
  pattern = "[A-Z]{2,}";
  pattern.GlobalReplace("<span style=\"color:" + _keyword_color + "\">\\0</span>", &result);

  // Internal help links. Original format: [HELP THE HELP TOPIC]. But due to the previous step each word
  // is now surrounded by markup. Because of the variable number of keywords in a link we need several runs.
  pattern = "\\[<span style=\"color:[^\"]+\">HELP</span> +<span style=\"color:[^\"]+\">([A-Z]+)</span>\\]";
  pattern.GlobalReplace("<a href=\"local:\\1\">\\1</a>", &result);
  pattern = "\\[<span style=\"color:[^\"]+\">HELP</span> +<span style=\"color:[^\"]+\">([A-Z]+)</span> +"
    "<span style=\"color:[^\"]+\">([A-Z]+)</span>\\]";
  pattern.GlobalReplace("<a href=\"local:\\1 \\2\">\\1 \\2</a>", &result);
  pattern = "\\[<span style=\"color:[^\"]+\">HELP</span> +<span style=\"color:[^\"]+\">([A-Z]+)</span> +"
    "<span style=\"color:[^\"]+\">([A-Z]+)</span> +<span style=\"color:[^\"]+\">([A-Z]+)</span>\\]";
  pattern.GlobalReplace("<a href=\"local:\\1 \\2 \\3\">\\1 \\2 \\3</a>", &result);

  // Change references to become real links. A problem here is that due to hard line breaks
  // in the original text we can now have space chars in links. We cannot fix any possible combination
  // but we do so for certain cases we know.
  pattern = ". ?h ?t ?m ?l ?";
  pattern.GlobalReplace(".html", &result);

  // Not a general URL pattern, but one that fits exactly the help urls.
  pattern = "https?://[A-Za-z]+(?:\\.[A-Za-z]+)+(?:/[A-Za-z0-9-.]+)+/([A-Za-z0-9-]+)\\.html";
  pattern.GlobalReplace(" <a href=\"\\0\">Online help \\1</a> ", &result);

  return result;
}

//--------------------------------------------------------------------------------------------------
