/*
 * Dillo Widget
 *
 * Copyright 2005-2007 Sebastian Geerken <sgeerken@dillo.org>
 *
 * 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 3 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, see <http://www.gnu.org/licenses/>.
 */



#include "core.hh"
#include "../lout/debug.hh"

#include <string.h>

using namespace lout;

/*
 * strndup() is a GNU extension.
 */
extern "C" char *strndup(const char *s, size_t size)
{
   char *r = (char *) malloc (size + 1);

   if (r) {
      strncpy (r, s, size);
      r[size] = 0;
   }

   return r;
}

namespace dw {
namespace core {

SelectionState::SelectionState ()
{
   DBG_OBJ_CREATE ("dw::core::SelectionState");

   layout = NULL;

   selectionState = NONE;
   from = NULL;
   to = NULL;

   linkState = LINK_NONE;
   link = NULL;
}

SelectionState::~SelectionState ()
{
   reset ();
   DBG_OBJ_DELETE ();
}

void SelectionState::reset ()
{
   resetSelection ();
   resetLink ();
}

void SelectionState::resetSelection ()
{
   if (from)
      delete from;
   from = NULL;
   if (to)
      delete to;
   to = NULL;
   selectionState = NONE;
}


void SelectionState::resetLink ()
{
   if (link)
      delete link;
   link = NULL;
   linkState = LINK_NONE;
}

bool SelectionState::buttonPress (Iterator *it, int charPos, int linkNo,
                                  EventButton *event)
{
   DBG_IF_RTFL {
      misc::StringBuffer sb;
      it->intoStringBuffer (&sb);
      DBG_OBJ_ENTER ("events", 0, "buttonPress", "[%s], %d, %d, ...",
                     sb.getChars (), charPos, linkNo);
   }

   Widget *itWidget = it->getWidget ();
   bool ret = false;

   if (event) {
      if (event->button == 3) {
         // menu popup
         layout->emitLinkPress (itWidget, linkNo, -1, -1, -1, event);
         ret = true;
      } else if (linkNo != -1) {
         // link handling
         (void) layout->emitLinkPress (itWidget, linkNo, -1, -1, -1, event);
         resetLink ();
         linkState = LINK_PRESSED;
         linkButton = event->button;
         DeepIterator *newLink = new DeepIterator (it);
         if (newLink->isEmpty ()) {
            delete newLink;
            resetLink ();
         } else {
            link = newLink;
            // It may be that the user has pressed on something activatable
            // (linkNo != -1), but there is no contents, e.g. with images
            // without ALTernative text.
            if (link) {
               linkChar = correctCharPos (link, charPos);
               linkNumber = linkNo;
            }
         }
         // We do not return the value of the signal method,
         // but we do actually process this event.
         ret = true;
      } else if (event->button == 1) {
         // normal selection handling
         highlight (false, 0);
         resetSelection ();

         selectionState = SELECTING;
         DeepIterator *newFrom = new DeepIterator (it);
         if (newFrom->isEmpty ()) {
            delete newFrom;
            resetSelection ();
         } else {
            from = newFrom;
            fromChar = correctCharPos (from, charPos);
            to = from->cloneDeepIterator ();
            toChar = correctCharPos (to, charPos);
         }
         ret = true;
      }
   }

   DBG_OBJ_MSGF ("events", 1, "=> %s", ret ? "true" : "false");
   DBG_OBJ_LEAVE ();
   return ret;
}

bool SelectionState::buttonRelease (Iterator *it, int charPos, int linkNo,
                                    EventButton *event)
{
   DBG_IF_RTFL {
      misc::StringBuffer sb;
      it->intoStringBuffer (&sb);
      DBG_OBJ_ENTER ("events", 0, "buttonRelease", "[%s], %d, %d, ...",
                     sb.getChars (), charPos, linkNo);
   }

   Widget *itWidget = it->getWidget ();
   bool ret = false;

   if (linkState == LINK_PRESSED && event && event->button == linkButton) {
      // link handling
      ret = true;
      if (linkNo != -1)
         (void) layout->emitLinkRelease (itWidget, linkNo, -1, -1, -1, event);

      // The link where the user clicked the mouse button?
      if (linkNo == linkNumber) {
         resetLink ();
         (void) layout->emitLinkClick (itWidget, linkNo, -1, -1, -1, event);
      } else {
         if (event->button == 1)
            // Reset links and switch to selection mode. The selection
            // state will be set to SELECTING, which is handled some lines
            // below.
            switchLinkToSelection (it, charPos);
      }
   }

   if (selectionState == SELECTING && event && event->button == 1) {
      // normal selection
      ret = true;
      adjustSelection (it, charPos);

      if (from->compareTo (to) == 0 && fromChar == toChar)
         // nothing selected
         resetSelection ();
      else {
         copy ();
         selectionState = SELECTED;
      }
   }

   DBG_OBJ_MSGF ("events", 1, "=> %s", ret ? "true" : "false");
   DBG_OBJ_LEAVE ();
   return ret;
}

bool SelectionState::buttonMotion (Iterator *it, int charPos, int linkNo,
                                   EventMotion *event)
{
   if (linkState == LINK_PRESSED) {
      //link handling
      if (linkNo != linkNumber)
         // No longer the link where the user clicked the mouse button.
         // Reset links and switch to selection mode.
         switchLinkToSelection (it, charPos);
      // Still in link: do nothing.
   } else if (selectionState == SELECTING) {
      // selection
      adjustSelection (it, charPos);
   }

   return true;
}

/**
 * \brief General form of dw::core::SelectionState::buttonPress,
 *    dw::core::SelectionState::buttonRelease and
 *    dw::core::SelectionState::buttonMotion.
 */
bool SelectionState::handleEvent (EventType eventType, Iterator *it,
                                  int charPos, int linkNo,
                                  MousePositionEvent *event)
{
   switch (eventType) {
   case BUTTON_PRESS:
      return buttonPress (it, charPos, linkNo, (EventButton*)event);

   case BUTTON_RELEASE:
      return buttonRelease (it, charPos, linkNo, (EventButton*)event);

   case BUTTON_MOTION:
      return buttonMotion (it, charPos, linkNo, (EventMotion*)event);


   default:
      misc::assertNotReached ();
   }

   return false;
}


/**
 * \brief This method is called when the user decides not to activate a link,
 *    but instead select text.
 */
void SelectionState::switchLinkToSelection (Iterator *it, int charPos)
{
   // It may be that selection->link is NULL, see a_Selection_button_press.
   if (link) {
      // Reset old selection.
      highlight (false, 0);
      resetSelection ();

      // Transfer link state into selection state.
      from = link->cloneDeepIterator ();
      fromChar = linkChar;
      to = from->createVariant (it);
      toChar = correctCharPos (to, charPos);
      selectionState = SELECTING;

      // Reset link status.
      resetLink ();

      highlight (true, 0);

   } else {
      // A link was pressed on, but there is nothing to select. Reset
      // everything.
      resetSelection ();
      resetLink ();
   }
}

/**
 * \brief This method is used by core::dw::SelectionState::buttonMotion and
 *    core::dw::SelectionState::buttonRelease, and changes the second limit of
 *    the already existing selection region.
 */
void SelectionState::adjustSelection (Iterator *it, int charPos)
{
   DeepIterator *newTo;
   int newToChar, cmpOld, cmpNew, cmpDiff, len;
   bool bruteHighlighting = false;

   newTo = to->createVariant (it);
   newToChar = correctCharPos (newTo, charPos);

   cmpOld = to->compareTo (from);
   cmpNew = newTo->compareTo (from);

   if (cmpOld == 0 || cmpNew == 0) {
      // Either before, or now, the limits differ only by the character
      // position.
      bruteHighlighting = true;
   } else if (cmpOld * cmpNew < 0) {
      // The selection order has changed, i.e. the user moved the selection
      // end again beyond the position he started.
      bruteHighlighting = true;
   } else {
      // Here, cmpOld and cmpNew are equivalent and != 0.
      cmpDiff = newTo->compareTo (to);

      if (cmpOld * cmpDiff > 0) {
         // The user has enlarged the selection. Highlight the difference.
         if (cmpDiff < 0) {
            len = correctCharPos (to, END_OF_WORD);
            highlight0 (true, newTo, newToChar, to, len + 1, 1);
         } else {
            highlight0 (true, to, 0, newTo, newToChar, -1);
         }
      } else {
         if (cmpOld * cmpDiff < 0) {
            // The user has reduced the selection. Unhighlight the difference.
            highlight0 (false, to, 0, newTo, 0, cmpDiff);
         }

         // Otherwise, the user has changed the position only slightly.
         // In both cases, re-highlight the new position.
         if (cmpOld < 0) {
            len = correctCharPos (newTo, END_OF_WORD);
            newTo->highlight (newToChar, len + 1, HIGHLIGHT_SELECTION);
         } else
            newTo->highlight (0, newToChar, HIGHLIGHT_SELECTION);
      }
   }

   if (bruteHighlighting)
      highlight (false, 0);

   delete to;
   to = newTo;
   toChar = newToChar;

   if (bruteHighlighting)
      highlight (true, 0);
}

/**
 * \brief This method deals especially with the case that a widget passes
 *    dw::core::SelectionState::END_OF_WORD.
 */
int SelectionState::correctCharPos (DeepIterator *it, int charPos)
{
   Iterator *top = it->getTopIterator ();
   int len;

   if (top->getContent()->type == Content::TEXT)
      len = strlen(top->getContent()->text);
   else
      len = 1;

   return misc::min(charPos, len);
}

void SelectionState::highlight0 (bool fl, DeepIterator *from, int fromChar,
                                 DeepIterator *to, int toChar, int dir)
{
   DeepIterator *a, *b, *i;
   int cmp, aChar, bChar;
   bool start;

   if (from && to) {
      cmp = from->compareTo (to);
      if (cmp == 0) {
         if (fl) {
            if (fromChar < toChar)
               from->highlight (fromChar, toChar, HIGHLIGHT_SELECTION);
            else
               from->highlight (toChar, fromChar, HIGHLIGHT_SELECTION);
         } else
            from->unhighlight (0, HIGHLIGHT_SELECTION);
         return;
      }

      if (cmp < 0) {
         a = from;
         aChar = fromChar;
         b = to;
         bChar = toChar;
      } else {
         a = to;
         aChar = toChar;
         b = from;
         bChar = fromChar;
      }

      for (i = a->cloneDeepIterator (), start = true;
           (cmp = i->compareTo (b)) <= 0;
           i->next (), start = false) {
         if (i->getContent()->type == Content::TEXT) {
            if (fl) {
               if (start) {
                  i->highlight (aChar, strlen (i->getContent()->text) + 1,
                                HIGHLIGHT_SELECTION);
               } else if (cmp == 0) {
                  // the end
                  i->highlight (0, bChar, HIGHLIGHT_SELECTION);
               } else {
                  i->highlight (0, strlen (i->getContent()->text) + 1,
                                HIGHLIGHT_SELECTION);
               }
            } else {
               i->unhighlight (dir, HIGHLIGHT_SELECTION);
            }
         }
      }
      delete i;
   }
}

void SelectionState::copy()
{
   if (from && to) {
      Iterator *si;
      DeepIterator *a, *b, *i;
      int cmp, aChar, bChar;
      bool start;
      char *tmp;
      misc::StringBuffer strbuf;

      cmp = from->compareTo (to);
      if (cmp == 0) {
         if (from->getContent()->type == Content::TEXT) {
            si = from->getTopIterator ();
            if (fromChar < toChar)
               tmp = strndup (si->getContent()->text + fromChar,
                              toChar - fromChar);
            else
               tmp = strndup (si->getContent()->text + toChar,
                              fromChar - toChar);
            strbuf.appendNoCopy (tmp);
         }
      } else {
         if (cmp < 0) {
            a = from;
            aChar = fromChar;
            b = to;
            bChar = toChar;
         } else {
            a = to;
            aChar = toChar;
            b = from;
            bChar = fromChar;
         }

         for (i = a->cloneDeepIterator (), start = true;
              (cmp = i->compareTo (b)) <= 0;
              i->next (), start = false) {
            si = i->getTopIterator ();
            switch (si->getContent()->type) {
            case Content::TEXT:
               if (start) {
                  tmp = strndup (si->getContent()->text + aChar,
                                 strlen (i->getContent()->text) - aChar);
                  strbuf.appendNoCopy (tmp);
               } else if (cmp == 0) {
                  // the end
                  tmp = strndup (si->getContent()->text, bChar);
                  strbuf.appendNoCopy (tmp);
               } else
                  strbuf.append (si->getContent()->text);

               if (si->getContent()->space && cmp != 0)
                  strbuf.append (" ");

               break;

            case Content::BREAK:
               if (si->getContent()->breakSpace > 0)
                  strbuf.append ("\n\n");
               else
                  strbuf.append ("\n");
               break;
            default:
               // Make pedantic compilers happy. Especially
               // DW_CONTENT_WIDGET is never returned by a DwDeepIterator.
               break;
            }
         }
         delete i;
      }

      layout->copySelection(strbuf.getChars());
   }
}

} // namespace core
} // namespace dw
