// Copyright (C) 2011-2013, Gabriel Dos Reis.
// All rights reserved.
// Written by Gabriel Dos Reis.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
//     - Redistributions of source code must retain the above copyright
//       notice, this list of conditions and the following disclaimer.
//
//     - Redistributions in binary form must reproduce the above copyright
//       notice, this list of conditions and the following disclaimer in
//       the documentation and/or other materials provided with the
//       distribution.
//
//     - Neither the name of OpenAxiom. nor the names of its contributors
//       may be used to endorse or promote products derived from this
//       software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
// IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
// TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
// OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

#include <cmath>
#include <string>
#include <sstream>
#include <iostream>

#include <QScrollBar>
#include <QApplication>
#include "conversation.h"
#include "debate.h"

namespace OpenAxiom {
   // Largest width and line spacing, in pixel, of the font metrics
   // associated with `w'.
   static QSize font_units(const QWidget* w) {
      const QFontMetrics fm = w->fontMetrics();
      const auto h = fm.lineSpacing();
      if (auto w = fm.maxWidth())
         return { w, h };
      return { fm.width('W'), h };
   }

   // Return true if the QString `s' is morally an empty string.
   // QT makes a difference between a null string and an empty string.
   // That distinction is largely pedantic and without difference
   // for most of our practical purposes.
   static bool
   empty_string(const QString& s) {
      return s.isNull() or s.isEmpty();
   }
   
   // Return a resonable margin for this frame.
   static int our_margin(const QFrame* f) {
      return 2 + f->frameWidth();
   }

   // --------------------
   // -- OutputTextArea --
   // --------------------
   OutputTextArea::OutputTextArea(QWidget* p)
         : Base(p), cur(document()) {
      get_cursor().movePosition(QTextCursor::End);
      setReadOnly(true);          // this is a output only area.
      setLineWrapMode(NoWrap);    // for the time being, mess with nothing.
      setFont(p->font());
      setViewportMargins(0, 0, 0, 0);
      setSizePolicy(QSizePolicy::Preferred, QSizePolicy::MinimumExpanding);
      // We do not want to see scroll bars.  Usually disallowing vertical
      // scroll bars and allocating enough horizontal space is sufficient
      // to ensure that we don't see any horizontal scrollbar.
      setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
      setLineWidth(1);
   }

   // This overriding implementation is so that we can control the
   // amount of vertical space in the read-only editor viewport allocated
   // for the display of output text.  In particular we do not want
   // scrollbars.
   QSize
   OutputTextArea::sizeHint() const {
      const QSize s = font_units(this);
      return QSize(width(), (1 + document()->lineCount()) * s.height());
   }

   void OutputTextArea::add_paragraph(const QString& s) {
      if (not document()->isEmpty())
         get_cursor().insertBlock();
      get_cursor().insertText(s);
      QSize sz = sizeHint();
      sz.setWidth(parentWidget()->width() - our_margin(this));
      resize(sz);
      show();
      updateGeometry();
   }

   void OutputTextArea::add_text(const QString& s) {
      setPlainText(toPlainText() + s);
      QSize sz = sizeHint();
      const int w = parentWidget()->width() - 2 * frameWidth();
      if (w > sz.width())
         sz.setWidth(w);
      resize(sz);
      show();
      updateGeometry();
   }

   OutputTextArea&
   OutputTextArea::insert_block(const QString& s) {
      if (not document()->isEmpty())
         get_cursor().insertBlock();
      get_cursor().insertText(s);
      resize(sizeHint());
      updateGeometry();
      return *this;
   }

   // --------------
   // -- Question --
   // --------------
   Question::Question(Exchange* e) : QLineEdit(e) {
      setBackgroundRole(QPalette::AlternateBase);
      setFrame(true);
   }

   void Question::focusInEvent(QFocusEvent* e) {
      setFrame(true);
      update();
      QLineEdit::focusInEvent(e);
   }

   void Question::enterEvent(QEvent* e) {
      setFrame(true);
      update();
      QLineEdit::enterEvent(e);
   }

   // ------------
   // -- Answer --
   // ------------
   Answer::Answer(Exchange* e) : OutputTextArea(e) {
      setFrameStyle(StyledPanel | Raised);
   }

   // --------------
   // -- Exchange --
   // --------------
   // Amount of pixel spacing between the query and reply areas.
   const int spacing = 2;

   // Return a monospace font
   static QFont monospace_font() {
      QFont f("Monaco", 11);
      f.setStyleHint(QFont::TypeWriter);
      return f;
   }

   // The layout within an exchange is as follows:
   //   -- input area (an editor) with its own decoation accounted for.
   //   -- an optional spacing
   //   -- an output area with its own decoration accounted for.
   QSize Exchange::sizeHint() const {
      const int m = our_margin(this);
      QSize sz = question()->size() + QSize(2 * m, 2 * m);
      if (not answer()->isHidden())
         sz.rheight() += answer()->height() + spacing;
      return sz;
   }

   Server* Exchange::server() const {
      return win->debate()->server();
   }

   // Dress the query area with initial properties.
   static void
   prepare_query_widget(Conversation* conv, Exchange* e) {
      Question* q = e->question();
      q->setFrame(false);
      q->setFont(conv->font());
      const int m = our_margin(e);
      q->setGeometry(m, m, conv->width() - 2 * m, q->height());
   }

   // Dress the reply aread with initial properties.
   // Place the reply widget right below the frame containing
   // the query widget; make both of the same width, of course.
   static void
   prepare_reply_widget(Conversation* conv, Exchange* e) {
      Answer* a = e->answer();
      Question* q = e->question();
      const QPoint pt = e->question()->geometry().bottomLeft();
      const int m = our_margin(a);
      a->setGeometry(pt.x(), pt.y() + spacing, 
		     conv->width() - 2 * m, q->height());
      a->setBackgroundRole(q->backgroundRole());
      a->hide();                // nothing to show yet
   }

   static void
   finish_exchange_make_up(Conversation* conv, Exchange* e) {
      e->setAutoFillBackground(true);
      e->move(conv->bottom_left());
   }
   
   Exchange::Exchange(Conversation* conv, int n)
         : QFrame(conv), win(conv), no(n), query(this), reply(this) {
      setLineWidth(1);
      setFont(conv->font());
      prepare_query_widget(conv, this);
      prepare_reply_widget(conv, this);
      finish_exchange_make_up(conv, this);
      connect(question(), SIGNAL(returnPressed()),
              this, SLOT(reply_to_query()));
   }

   static void ensure_visibility(Debate* debate, Exchange* e) {
      const int y = e->y() + e->height();
      QScrollBar* vbar = debate->verticalScrollBar();
      const int value = vbar->value();
      int new_value = y - vbar->pageStep();
      if (y < value)
         vbar->setValue(std::max(new_value, 0));
      else if (new_value > value)
         vbar->setValue(std::min(new_value, vbar->maximum()));
      e->question()->setFocus(Qt::OtherFocusReason);
   }
   
   void
   Exchange::reply_to_query() {
      QString input = question()->text().trimmed();
      if (empty_string(input))
         return;
      question()->setReadOnly(true); // Make query area read only.
      question()->clearFocus();
      question()->setFocusPolicy(Qt::NoFocus);
      server()->input(input);
      QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
   }

   void Exchange::resizeEvent(QResizeEvent* e) {
      QFrame::resizeEvent(e);
      const int w = width() - 2 * our_margin(this);
      if (w > question()->width()) {
         question()->resize(w, question()->height());
         answer()->resize(w, answer()->height());
      }
   }

   // ------------
   // -- Banner --
   // ------------
   Banner::Banner(Conversation* conv) :  Base(conv) {
      setFrameStyle(StyledPanel | Raised);
      setBackgroundRole(QPalette::Base);
   }

   // ------------------
   // -- Conversation --
   // -------------------
   
   // Default number of characters per question line.
   const int columns = 80;
   const int lines = 40;

   static QSize
   minimum_preferred_size(const Conversation* conv) {
      const QSize s = font_units(conv);
      return QSize(columns * s.width(), lines * s.height());
   }

   // Set a minimum preferred widget size, so no layout manager
   // messes with it.  Indicate we can make use of more space.
   Conversation::Conversation(Debate* d)
         : QWidget(d),
           win(d),
           greatings(this),
           cur_ex(),
           cur_out(&greatings),
           rx("\\(\\d+\\)\\s->"),
           tx("\\sType: ") {
      setFont(monospace_font());
      setBackgroundRole(QPalette::Base);
      greatings.setFont(font());
      setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
   }

   Conversation::~Conversation() {
      for (int i = children.size() -1 ; i >= 0; --i)
         delete children[i];
   }

   QPoint Conversation::bottom_left() const {
      if (length() == 0)
         return greatings.geometry().bottomLeft();
      return children.back()->geometry().bottomLeft();
   }

   static QSize
   round_up_height(const QSize& sz, int height) {
      if (height < 1)
         height = 1;
      const int n = (sz.height() + height) / height;
      return QSize(sz.width(), n * height);
   }
   
   QSize Conversation::sizeHint() const {
      const int n = length();
      if (n == 0)
         return minimum_preferred_size(this);
      const int view_height = debate()->viewport()->height();
      QSize sz = greatings.size();
      for (int i = 0; i < n; ++i)
         sz.rheight() += children[i]->height();
      return round_up_height(sz, view_height);
   }

   void Conversation::resizeEvent(QResizeEvent* e) {
      QWidget::resizeEvent(e);
      setMinimumSize(size());
      const QSize sz = size();
      if (e->oldSize() == sz)
         return;
      greatings.resize(sz.width(), greatings.height());
      for (int i = 0; i < length(); ++i) {
         Exchange* e = children[i];
         e->resize(sz.width(), e->height());
      }
   }

   void Conversation::paintEvent(QPaintEvent* e) {
      QWidget::paintEvent(e);
      if (length() == 0)
         greatings.update();
   }

   Exchange*
   Conversation::new_topic() {
      Exchange* w = new Exchange(this, length() + 1);
      w->show();
      children.push_back(w);
      adjustSize();
      updateGeometry();
      cur_out = w->answer();
      return cur_ex = w;
   }

   Exchange*
   Conversation::next(Exchange* w) {
      if (w == 0 or w->number() == length())
         return new_topic();
      return cur_ex = children[w->number()];
   }

   static QTextCharFormat
   get_type_format(OutputTextArea* area) {
      auto format = area->get_cursor().charFormat();
      format.setFontWeight(QFont::Bold);
      format.setToolTip("domain of result");
      return format;
   }

   static void
   display_type(OutputTextArea* area, QString& text, int n) {
      area->insert_block(QString(n, ' '));
      area->get_cursor().insertText(text.mid(n), get_type_format(area));
      area->resize(area->sizeHint());
      area->updateGeometry();
   }

   void
   Conversation::read_reply() {
      auto data = debate()->server()->readAll();
      QStringList strs = QString::fromLocal8Bit(data).split('\n');
      QString prompt;
      for (auto& s : strs) {
         if (rx.indexIn(s) != -1) {
            prompt = s;
            continue;
         }
         auto tpos = tx.indexIn(s);
         if (tpos != -1)
            display_type(cur_out, s, tpos + tx.matchedLength());
         else
            cur_out->add_paragraph(s);
      }
      if (length() == 0) {
         if (not empty_string(prompt))
            ensure_visibility(debate(), new_topic());
      }
      else {
         exchange()->adjustSize();
         exchange()->update();
         exchange()->updateGeometry();
         if (empty_string(prompt))
            ensure_visibility(debate(), exchange());
         else {
            ensure_visibility(debate(), next(exchange()));
            QApplication::restoreOverrideCursor();
	 }
      }
   }
}