/****************************************************************
 * This file is distributed under the following license:
 *
 * Copyright (c) 2008, defnax
 * Copyright (c) 2008, Matt Edman, Justin Hipple
 *
 *  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; either version 2
 *  of the License, or (at your option) any later version.
 *
 *  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 Street, Fifth Floor, 
 *  Boston, MA  02110-1301, USA.
 ****************************************************************/

/*
** \file helpbrowser.cpp
** \version $Id: helpbrowser.cpp 2362 2008-02-29 04:30:11Z edmanm $
** \brief Displays a list of help topics and content
*/

#include <QDomDocument>
#include <QDir>
#include <rshare.h>
#include "gui/settings/rsharesettings.h"

#include "helpbrowser.h"


#define LEFT_PANE_INDEX     0
#define NO_STRETCH          0
#define MINIMUM_PANE_SIZE   1

/* Names of elements and attributes in the XML file */
#define ELEMENT_CONTENTS        "Contents"
#define ELEMENT_TOPIC           "Topic"
#define ATTRIBUTE_TOPIC_ID      "id"
#define ATTRIBUTE_TOPIC_HTML    "html"
#define ATTRIBUTE_TOPIC_NAME    "name"
#define ATTRIBUTE_TOPIC_SECTION "section"

/* Define two roles used to store data associated with a topic item */
#define ROLE_TOPIC_ID        Qt::UserRole
#define ROLE_TOPIC_QRC_PATH (Qt::UserRole+1)

static HelpBrowser *helpBrowser = NULL;

/** Constuctor. This will probably do more later */
HelpBrowser::HelpBrowser(QWidget *parent)
 : RWindow("HelpBrowser", parent)
{
  /* Invoke Qt Designer generated QObject setup routine */
  ui.setupUi(this);
#if defined(Q_OS_MAC)
  ui.actionHome->setShortcut(QString("Shift+Ctrl+H"));
#endif
#if !defined(Q_OS_WIN)
  ui.actionClose->setShortcut(QString("Ctrl+W"));
#endif

  helpBrowser = this;

  setAttribute(Qt::WA_DeleteOnClose, true);

  /* Hide Search frame */
  ui.frmFind->setHidden(true);
 
  /* Set the splitter pane sizes so that only the txtBrowser pane expands
   * and set to arbitrary sizes (the minimum sizes will take effect */
  QList<int> sizes;
  sizes.append(MINIMUM_PANE_SIZE); 
  sizes.append(MINIMUM_PANE_SIZE);
  ui.splitter->setSizes(sizes);
  ui.splitter->setStretchFactor(LEFT_PANE_INDEX, NO_STRETCH);

  connect(ui.treeContents,
          SIGNAL(currentItemChanged(QTreeWidgetItem*,QTreeWidgetItem*)),
          this, SLOT(contentsItemChanged(QTreeWidgetItem*,QTreeWidgetItem*)));

  connect(ui.treeSearch,
          SIGNAL(currentItemChanged(QTreeWidgetItem*,QTreeWidgetItem*)),
          this, SLOT(searchItemChanged(QTreeWidgetItem*,QTreeWidgetItem*)));

  /* Connect the navigation actions to their slots */
  connect(ui.actionHome, SIGNAL(triggered()), ui.txtBrowser, SLOT(home()));
  connect(ui.actionBack, SIGNAL(triggered()), ui.txtBrowser, SLOT(backward()));
  connect(ui.actionForward, SIGNAL(triggered()), ui.txtBrowser, SLOT(forward()));
  connect(ui.txtBrowser, SIGNAL(backwardAvailable(bool)), 
          ui.actionBack, SLOT(setEnabled(bool)));
  connect(ui.txtBrowser, SIGNAL(forwardAvailable(bool)),
          ui.actionForward, SLOT(setEnabled(bool)));
  connect(ui.btnFindNext, SIGNAL(clicked()), this, SLOT(findNext()));
  connect(ui.btnFindPrev, SIGNAL(clicked()), this, SLOT(findPrev()));
  connect(ui.btnSearch, SIGNAL(clicked()), this, SLOT(search()));
  
  /* Load the help topics from XML */
  loadContentsFromXml(":/help/" + language() + "/contents.xml");

  /* Show the first help topic in the tree */
  ui.treeContents->setCurrentItem(ui.treeContents->topLevelItem(0));
  ui.treeContents->setItemExpanded(ui.treeContents->topLevelItem(0), true);
}

HelpBrowser::~HelpBrowser()
{
  helpBrowser = NULL;
}

/** Returns the language in which help topics should appear, or English
 * ("en") if no translated help files exist for the current GUI language. */
QString
HelpBrowser::language()
{
  QString lang = Rshare::language();
  if (!QDir(":/help/" + lang).exists())
    lang = "en";
  return lang;
}

/** Load the contents of the help topics tree from the specified XML file. */
void
HelpBrowser::loadContentsFromXml(QString xmlFile)
{
  QString errorString;
  QFile file(xmlFile);
  QDomDocument document;
  
  /* Load the XML contents into the DOM document */
  if (!document.setContent(&file, true, &errorString)) {
    ui.txtBrowser->setPlainText(tr("Error Loading Help Contents:")+" "+errorString);
    return;
  }
  /* Load the DOM document contents into the tree view */
  if (!loadContents(&document, errorString)) {
    ui.txtBrowser->setPlainText(tr("Error Loading Help Contents:")+" "+errorString);
    return;
  }
}

/** Load the contents of the help topics tree from the given DOM document. */
bool
HelpBrowser::loadContents(const QDomDocument *document, QString &errorString)
{
  /* Grab the root document element and make sure it's the right one */
  QDomElement root = document->documentElement();
  if (root.tagName() != ELEMENT_CONTENTS) {
    errorString = tr("Supplied XML file is not a valid Contents document.");
    return false;
  }
  _elementList << root;

  /* Create the home item */
  QTreeWidgetItem *home = createTopicTreeItem(root, 0);
  ui.treeContents->addTopLevelItem(home);
  
  /* Process all top-level help topics */
  QDomElement child = root.firstChildElement(ELEMENT_TOPIC);
  while (!child.isNull()) {
    parseHelpTopic(child, home);
    child = child.nextSiblingElement(ELEMENT_TOPIC);
  }
  return true;
}

/** Parse a Topic element and handle all its children recursively. */
void
HelpBrowser::parseHelpTopic(const QDomElement &topicElement, 
                            QTreeWidgetItem *parent)
{
  /* Check that we have a valid help topic */
  if (isValidTopicElement(topicElement)) {
    /* Save this element for later (used for searching) */
    _elementList << topicElement;

    /* Create and populate the new topic item in the tree */
    QTreeWidgetItem *topic = createTopicTreeItem(topicElement, parent);

    /* Process all its child elements */
    QDomElement child = topicElement.firstChildElement(ELEMENT_TOPIC);
    while (!child.isNull()) {
      parseHelpTopic(child, topic);
      child = child.nextSiblingElement(ELEMENT_TOPIC);
    }
  }
}

/** Returns true if the given Topic element has the necessary attributes. */
bool
HelpBrowser::isValidTopicElement(const QDomElement &topicElement)
{
  return (topicElement.hasAttribute(ATTRIBUTE_TOPIC_ID) &&
          topicElement.hasAttribute(ATTRIBUTE_TOPIC_NAME) &&
          topicElement.hasAttribute(ATTRIBUTE_TOPIC_HTML));
}

/** Builds a resource path to an html file associated with the given help
 * topic. If the help topic needs an achor, the anchor will be formatted and
 * appended. */
QString
HelpBrowser::getResourcePath(const QDomElement &topicElement)
{
  QString link = language() + "/" + topicElement.attribute(ATTRIBUTE_TOPIC_HTML);
  if (topicElement.hasAttribute(ATTRIBUTE_TOPIC_SECTION)) {
    link += "#" + topicElement.attribute(ATTRIBUTE_TOPIC_SECTION);
  }
  return link;
}

/** Creates a new element to be inserted into the topic tree. */
QTreeWidgetItem*
HelpBrowser::createTopicTreeItem(const QDomElement &topicElement, 
                                 QTreeWidgetItem *parent)
{
  QTreeWidgetItem *topic = new QTreeWidgetItem(parent);
  topic->setText(0, topicElement.attribute(ATTRIBUTE_TOPIC_NAME));
  topic->setData(0, ROLE_TOPIC_ID, topicElement.attribute(ATTRIBUTE_TOPIC_ID));
  topic->setData(0, ROLE_TOPIC_QRC_PATH, getResourcePath(topicElement));
  return topic;
}

/** Called when the user selects a different item in the content topic tree */
void
HelpBrowser::contentsItemChanged(QTreeWidgetItem *current, QTreeWidgetItem *prev)
{
  QList<QTreeWidgetItem *> selected = ui.treeSearch->selectedItems();
  /* Deselect the selection in the search tree */
  if (!selected.isEmpty()) {
    ui.treeSearch->setItemSelected(selected[0], false);
  }
  currentItemChanged(current, prev);
}

/** Called when the user selects a different item in the content topic tree */
void
HelpBrowser::searchItemChanged(QTreeWidgetItem *current, QTreeWidgetItem *prev)
{
  QList<QTreeWidgetItem *> selected = ui.treeContents->selectedItems();
  /* Deselect the selection in the contents tree */
  if (!selected.isEmpty()) {
    ui.treeContents->setItemSelected(selected[0], false);
  }

  /* Change to selected page */
  currentItemChanged(current, prev);

  /* Highlight search phrase */
  QTextCursor found;
  QTextDocument::FindFlags flags = QTextDocument::FindWholeWords;
  found = ui.txtBrowser->document()->find(_lastSearch, 0, flags);
  if (!found.isNull()) {
    ui.txtBrowser->setTextCursor(found);
  }
}

/** Called when the user selects a different item in the tree. */
void
HelpBrowser::currentItemChanged(QTreeWidgetItem *current, QTreeWidgetItem *prev)
{
  Q_UNUSED(prev);
  if (current) {
    ui.txtBrowser->setSource(QUrl(current->data(0, 
                                              ROLE_TOPIC_QRC_PATH).toString()));
  }
  _foundBefore = false;
}

/** Searches for a topic in the topic tree. Returns a pointer to that topics
 * item in the topic tree if it is found, 0 otherwise. */
QTreeWidgetItem*
HelpBrowser::findTopicItem(QTreeWidgetItem *startItem, QString topic)
{
  /* If startItem is null, then we don't know where to start searching. */
  if (!startItem)
    return 0;

  /* Parse the first subtopic in the topic id. */
  QString subtopic = topic.mid(0, topic.indexOf(".")).toLower();

  /* Search through all children of startItem and look for a subtopic match */
  for (int i = 0; i < startItem->childCount(); i++) {
    QTreeWidgetItem *item = startItem->child(i);
    
    if (subtopic == item->data(0, ROLE_TOPIC_ID).toString().toLower()) {
      /* Found a subtopic match, so expand this item */
      ui.treeContents->setItemExpanded(item, true);
      if (!topic.contains(".")) {
        /* Found the exact topic */
        return item;
      }
      /* Search recursively for the next subtopic */
      return findTopicItem(item, topic.mid(topic.indexOf(".")+1));
    }
  }
  return 0;
}

/** Shows the help browser. If a sepcified topic was given, the search for
 * that topic's ID (e.g., "log.basic") and display the appropriate page. */
void
HelpBrowser::showTopic(QString topic)
{
  /* Search for the topic in the contents tree */
  QTreeWidgetItem *item =
    findTopicItem(ui.treeContents->topLevelItem(0), topic);
  
  if (item) {
    /* Item was found, so show its location in the hierarchy and select its
     * tree item. */
    QTreeWidgetItem* selected = ui.treeContents->selectedItems()[0];
    if (selected) {
      ui.treeContents->setItemSelected(selected, false);
    }
    ui.treeContents->setItemExpanded(ui.treeContents->topLevelItem(0), true);
    ui.treeContents->setItemSelected(item, true);
    currentItemChanged(item, selected);
  }
}

/** Called when the user clicks "Find Next". */
void
HelpBrowser::findNext()
{
  find(true);
}

/** Called when the user clicks "Find Previous". */
void
HelpBrowser::findPrev()
{
  find(false);
}

/** Searches the current page for the phrase in the Find box.
 *  Highlights the first instance found in the document
 *  \param forward true search forward if true, backward if false
 **/
void
HelpBrowser::find(bool forward)
{
  /* Don't bother searching if there is no search phrase */
  if (ui.lineFind->text().isEmpty()) {
    return;
  }
  
  QTextDocument::FindFlags flags = 0;
  QTextCursor cursor = ui.txtBrowser->textCursor();
  QString searchPhrase = ui.lineFind->text();
  
  /* Clear status bar */
  this->statusBar()->clearMessage();
  
  /* Set search direction and other flags */
  if (!forward) {
    flags |= QTextDocument::FindBackward;
  }
  if (ui.chkbxMatchCase->isChecked()) {
    flags |= QTextDocument::FindCaseSensitively;
  }
  if (ui.chkbxWholePhrase->isChecked()) {
    flags |= QTextDocument::FindWholeWords;
  }
  
  /* Check if search phrase is the same as the previous */
  if (searchPhrase != _lastFind) {
    _foundBefore = false;
  }
  _lastFind = searchPhrase;
  
  /* Set the cursor to the appropriate start location if necessary */
  if (!cursor.hasSelection()) {
    if (forward) {
      cursor.movePosition(QTextCursor::Start);
    } else {
      cursor.movePosition(QTextCursor::End);
    }
    ui.txtBrowser->setTextCursor(cursor);
  }

  /* Search the page */
  QTextCursor found;
  found = ui.txtBrowser->document()->find(searchPhrase, cursor, flags);
  
  /* If found, move the cursor to the location */
  if (!found.isNull()) {
    ui.txtBrowser->setTextCursor(found);
  /* If not found, display appropriate error message */
  } else {
    if (_foundBefore) {
      if (forward) 
        this->statusBar()->showMessage(tr("Search reached end of document"));
      else 
        this->statusBar()->showMessage(tr("Search reached start of document"));
    } else {
      this->statusBar()->showMessage(tr("Text not found in document"));
    }
  }
  
  /* Even if not found this time, may have been found previously */
  _foundBefore |= !found.isNull();
}
 
/** Searches all help pages for the phrase the Search box.
 *  Fills treeSearch with documents containing matches and sets the
 *  status bar text appropriately.
 */
void
HelpBrowser::search()
{
  /* Clear the list */
  ui.treeSearch->clear();
  
  /* Don't search if invalid document or blank search phrase */
  if (ui.lineSearch->text().isEmpty()) {
    return;
  }
    
  HelpTextBrowser browser;
  QTextCursor found;
  QTextDocument::FindFlags flags = QTextDocument::FindWholeWords;

  _lastSearch = ui.lineSearch->text();

  /* Search through all the pages looking for the phrase */
  for (int i=0; i < _elementList.size(); ++i) {
    /* Load page data into browser */
    browser.setSource(QUrl(getResourcePath(_elementList[i])));
      
    /* Search current document */
    found = browser.document()->find(ui.lineSearch->text(), 0, flags);

    /* If found, add page to tree */
    if (!found.isNull()) {
      ui.treeSearch->addTopLevelItem(createTopicTreeItem(_elementList[i], 0));
    }
  }

  /* Set the status bar text */
  this->statusBar()->showMessage(tr("Found %1 results")
                                .arg(ui.treeSearch->topLevelItemCount()));
}

/** Overrides the default show method */
void
HelpBrowser::showWindow(const QString &topic)
{
  /* Bring the window to the top */
  if (helpBrowser == NULL) {
    /*helpBrowser = */new HelpBrowser();
  }
  helpBrowser->show();

  /* If a topic was specified, then go ahead and display it. */
  if (!topic.isEmpty()) {
    helpBrowser->showTopic(topic);
  }
}

