/****************************************************************************
**
** Copyright (C) 2001-2006 Frank Hemer <frank@hemer.org>,
**                         Tilo Riemer <riemer@crossvc.com>,
**                         Jose Hernandez <joseh@tesco.net>
**
**
**----------------------------------------------------------------------------
**
**----------------------------------------------------------------------------
**
** CrossVC is available under two different licenses:
**
** If CrossVC is linked against the GPLed version of Qt 
** CrossVC is released under the terms of GPL also.
**
** If CrossVC is linked against a nonGPLed version of Qt 
** CrossVC is released under the terms of the 
** CrossVC License for non-Unix platforms (CLNU)
**
**
** CrossVC License for non-Unix platforms (CLNU):
**
** Redistribution and use in binary form, without modification, 
** are permitted provided that the following conditions are met:
**
** 1. 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.
** 2. It is not permitted to distribute the binary package under a name
**    different than CrossVC.
** 3. The name of the authors may not be used to endorse or promote
**    products derived from this software without specific prior written
**    permission.
** 4. The source code is the creative property of the authors.
**    Extensions and development under the terms of the Gnu Public License
**    are limited to the Unix platform. Any distribution or compilation of 
**    the source code against libraries licensed other than gpl requires 
**    the written permission of the authors.
**
**
** THIS SOFTWARE IS PROVIDED BY THE AUTHOR "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 AUTHOR 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.
**
**
**
** CrossVC License for Unix platforms:
**
** 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 version 2 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
**
*****************************************************************************/

#include "config.h"

#include <stdio.h>
#include <qlayout.h>
#include <qlabel.h>
#include <qfont.h>
#include <qpushbutton.h>
#include <qtimer.h>
#include <qpainter.h>
#include <qregexp.h>
#include <qwhatsthis.h>
#include <qsplitter.h>
#include <qsizegrip.h>
#include <assert.h>

#include "DiffDialogImpl.h"
#include "globals.h"
#include "pixmapcache.h"

//The width of the splitter
const unsigned int DiffDialogImpl::SplitterStyle::SPLITTER_WIDTH = 4;

int containsWChar(const QString s) {
   int count = 0;
   char * c = const_cast<char *>(s.ascii());
   if (c) {
      unsigned int i;
      unsigned int len = strlen(c);
      for (i=0; i<len; ++i) {
	 if (c[i] < 0) ++count;
      }
   }
   return count;
}

/** 
 * Constructs a DiffDialogImpl which is a child of 'parent', with the 
 * name 'name' and widget flags set to 'f' 
 *
 * The dialog will by default be modeless, unless you set 'modal' to
 * TRUE to construct a modal dialog.
 */
DiffDialogImpl::DiffDialogImpl(const QIconSet &whatsThisIconSet,
      QWidget* parent,  const char* name, WFlags fl) 
   : DiffDialog(LookAndFeel::g_b0AsParent ? 0 : parent, name, fl)
{
   Splitter->setStyle(new SplitterStyle(this));
   Splitter->setOpaqueResize(true);
   m_pWhatsThis->setIconSet(whatsThisIconSet);
#ifdef Q_WS_MAC
  m_pWhatsThis->setMaximumWidth(m_pWhatsThis->height() * 2);
#else
  m_pWhatsThis->setMaximumWidth(m_pWhatsThis->height());
#endif
   ButtonLayout->addWidget(new QSizeGrip(this),0,Qt::AlignRight|Qt::AlignBottom);

   m_pDiffA->setPartner(m_pDiffB);
   m_pDiffA->setLineNumbers(true);
   m_pDiffA->setMarkers(true);

   m_pDiffB->setPartner(m_pDiffA);
   m_pDiffB->setLineNumbers(true);
   m_pDiffB->setMarkers(true);

   if (!Font::g_diff.isEmpty()) {
      QFont f;
      if (f.fromString(Font::g_diff)) {
	 m_pDiffA->setFont(f);
	 m_pDiffB->setFont(f);
      } else Font::g_diff = QString::null;
   }
        
   QFontMetrics fm (m_pDiffA->fontMetrics ());
   int dialogWidth = QMIN (QApplication::desktop ()->width (), fm.width ("0123456789")*12);
   setMinimumSize (dialogWidth, fm.lineSpacing ()*30);
}


/**
 * Destroys the object and frees any allocated resources
 */
DiffDialogImpl::~DiffDialogImpl()
{
   // no need to delete child widgets, Qt does it all for us
}


/** 
 * Toggles scrollbar synchronisation on and off.
 */
void DiffDialogImpl::toggleSynchronize(bool b)
{
   m_pDiffA->setPartner(b? m_pDiffB : 0);
   m_pDiffB->setPartner(b? m_pDiffA : 0);
}

#define COLUMNWIDTH ((SIDEWIDTH-3)>>1)
#define DELIMITERWIDTH (SIDEWIDTH-(COLUMNWIDTH<<1))

void DiffDialogImpl::parseCvsDiff(CvsBuffer  *pCvsBuffer, QString name,
      QString revA, QString revB)
{
   setCaption ("CVS Diff - " + name);

   revlabel1->setText(revA.isEmpty() ? QString(tr("Repository"))
	 : tr("Revision ") + revA);

   revlabel2->setText(revB.isEmpty() ? QString(tr("Sandbox"))
	 : tr("Revision ") + revB);

   int lineno1, lineno2;
   int skiplines = outputLineOffset + 5;
   int blockChanges = 0;
   int lineChanges = 0;
   int blockStart = -1;
   DiffView::DiffType type = DiffView::Unchanged;

   m_linesTotal = 0;

   DiffView::DiffType prevType = DiffView::Unchanged;

   lineno1 = lineno2 = 0;
   QString line;

   unsigned int len = (*pCvsBuffer).numLines();

   // must skip some lines
   bool skip = true;
   unsigned int i;
   for (i = skiplines; i < len; i++)
      {
	 line = (*pCvsBuffer).textLine(i);
	 if (skip) {
	    if (line.startsWith("diff ")) skip = false;
	    continue;
	 }

	 //ugly hack to work around cvs-server ignoring the client-locale
	 int colWidth = COLUMNWIDTH;
// 	 int wcCount = line.left(colWidth).contains(195);
	 int wcCount = containsWChar(line.left(colWidth));
// 	 qDebug("line ->"+line+", wcCount: "+QString::number(wcCount)+", wc: "+QString::number(containsWChar(line)));

	 int offset = colWidth;
	 int deltaCount = wcCount;
	 int tCount;
// 	 while ( (tCount=line.mid(offset,deltaCount).contains(195)) > 0 ) {
	 while ( (tCount=containsWChar(line.mid(offset,deltaCount))) > 0 ) {
	    offset += deltaCount;
	    wcCount += tCount;
	    deltaCount = tCount;
	 }
// 	 colWidth += (2*wcCount);
	 colWidth += wcCount;

	 QString line1 = line.left(COLUMNWIDTH);
	 QString line2 = line.mid(colWidth + DELIMITERWIDTH, COLUMNWIDTH);
	 QChar marker = line.at(colWidth+1);

	 type = 
	    (marker == '|') ? DiffView::Change : 
	    (marker == '<') ? DiffView::Delete : 
	    (marker == '>') ? DiffView::Insert : 
	    DiffView::Unchanged;

	 if (type != DiffView::Unchanged) {
	    lineChanges++;
	 }
	 if (type != prevType) {//start of changed block
	    if ( type != DiffView::Unchanged) {
	       if (blockStart==-1) {//new changed block
		  blockStart = m_linesTotal;
	       } else {//next changed block
		  m_changes[blockStart] = ChangedBlock(m_linesTotal-blockStart,prevType);
		  blockStart = m_linesTotal;
	       }
	       blockChanges++;
	    } else if (blockStart!=-1){//end of changed block
	       m_changes[blockStart] = ChangedBlock(m_linesTotal-blockStart,prevType);
	       blockStart = -1;
	    }
	    prevType = type;
	 }

	 //remove trailing space
	 line1.truncate(line1.findRev(QRegExp("\\S"))+1);
	 line2.truncate(line2.findRev(QRegExp("\\S"))+1);

	 DiffView::DiffList diffList1;
	 DiffView::DiffList diffList2;
	 if (type == DiffView::Change) {
	    analyzeChangeDiff(line1,diffList1,line2,diffList2);
	 } else {
	    DiffView::Diff diff1;
	    diff1.pos = 0;
	    diff1.len = line1.length();
	    diff1.type = DiffView::Unchanged;//type; -->don't change char color on add or remove
	    DiffView::Diff diff2;
	    diff2.pos = 0;
	    diff2.len = line2.length();
	    diff2.type = DiffView::Unchanged;//type; -->don't change char color on add or remove
	    diffList1.append(diff1);
	    diffList2.append(diff2);
	 }

	 m_pDiffA->addLine(line1, diffList1,
	       (type != DiffView::Unchanged ) ? DiffView::Neutral : type,
	       (type!=DiffView::Insert) ? ++lineno1 : -1);

	 m_pDiffB->addLine(line2,  diffList2, type,
	       (type!=DiffView::Delete)? ++lineno2 : -1);

	 LineChangeBox->setNum(lineChanges);
	 BlockChangeBox->setNum(blockChanges);
		
	 m_linesTotal++;
      }
   if (blockStart!=-1){//end of changed block
      m_changes[blockStart] = ChangedBlock(m_linesTotal-blockStart,type);
   }
}

void DiffDialogImpl::analyzeChangeDiff(QString &line1, DiffView::DiffList &diffList1, QString &line2, DiffView::DiffList &diffList2) {

   //   static int test = 0;if (test++ != 2) return;
   QMap<int,MatchMark> matchMarks;

   int offset1 = 0;
   int offset2 = 0;
   while (line1.at(offset1).isSpace()) offset1++;
   while (line2.at(offset2).isSpace()) offset2++;

   markBestMatch(line1,offset1,line1.length()-1,line2,offset2,line2.length()-1,matchMarks);
  
   if (matchMarks.empty()) {//no match found
      DiffView::Diff diff1;
      DiffView::Diff diff2;
      diff1.pos = 0;
      diff1.len = line1.length();
      diff1.type = DiffView::Change;
      diff2.pos = 0;
      diff2.len = line2.length();
      diff2.type = DiffView::Change;
      diffList1.append(diff1);
      diffList2.append(diff2);
   } else {//analyze added/removed/changed
      QMap<int,MatchMark>::iterator it = matchMarks.begin();

      DiffView::Diff diff1;
      DiffView::Diff diff2;

      //insert offset (spaces)
      diff1.pos = 0;
      diff1.len = offset1;
      diff1.type = DiffView::Unchanged;
      diff2.pos = 0;
      diff2.len = offset2;
      diff2.type = DiffView::Unchanged;
      diffList1.append(diff1);
      diffList2.append(diff2);
      //     qDebug("appending offset(blank): "+line1.mid(diff1.pos,diff1.len)+", line2: "+line2.mid(diff2.pos,diff2.len));

      int lastSPos = offset1;
      int lastTPos = offset2;

      while (it != matchMarks.end()) {

	 int sLen = it.key()-lastSPos;
	 int tLen = it.data().pos-lastTPos;

	 if (sLen == 0 && tLen == 0) {//caused by the algorithm
	    ;
	 } else if ( sLen == 0) {//added

	    diff2.pos = lastTPos;
	    diff2.len = tLen;
	    diff2.type = DiffView::Insert;
	    diffList2.append(diff2);
	    // 	qDebug("added: "+line2.mid(lastTPos,tLen));

	 } else if ( tLen == 0) {//removed
	  
	    diff1.pos = lastSPos;
	    diff1.len = sLen;
	    diff1.type = DiffView::Delete;
	    diffList1.append(diff1);
	    // 	qDebug("removed: "+line1.mid(lastSPos,sLen));

	 } else {//changed

	    diff1.pos = lastSPos;
	    diff1.len = sLen;
	    diff1.type = DiffView::Change;
	    diff2.pos = lastTPos;
	    diff2.len = tLen;
	    diff2.type = DiffView::Change;
	    diffList1.append(diff1);
	    diffList2.append(diff2);
	    // 	qDebug("changed, line1: "+line1.mid(lastSPos,sLen)+", line2: "+line2.mid(lastTPos,tLen));

	 }

	 //append the matching area
	 diff1.pos = it.key();
	 diff1.len = it.data().len;
	 diff1.type = DiffView::Unchanged;
	 diff2.pos = it.data().pos;
	 diff2.len = it.data().len;
	 diff2.type = DiffView::Unchanged;
	 diffList1.append(diff1);
	 diffList2.append(diff2);
	 //       qDebug("appending unchanged line1: "+line1.mid(diff1.pos,diff1.len)+", line2: "+line2.mid(diff2.pos,diff2.len));
	 lastSPos = it.key()+it.data().len;
	 lastTPos = it.data().pos+it.data().len;
	 ++it;
      }
      int rest1 = line1.length()-lastSPos;
      int rest2 = line2.length()-lastTPos;
      if ( (rest1==0) && (rest2==0)) return;
      if (rest1==0) {//added
	 diff2.pos = lastTPos;
	 diff2.len = rest2;
	 diff2.type = DiffView::Insert;
	 diffList2.append(diff2);
	 //       qDebug("rest added: "+line2.mid(lastTPos,rest2));
      } else if (rest2==0) {//removed
	 diff1.pos = lastSPos;
	 diff1.len = rest1;
	 diff1.type = DiffView::Delete;
	 diffList1.append(diff1);
	 //       qDebug("rest removed: "+line1.mid(lastSPos,rest1));
      } else {//changed
	 diff1.pos = lastSPos;
	 diff1.len = rest1;
	 diff1.type = DiffView::Change;
	 diff2.pos = lastTPos;
	 diff2.len = rest2;
	 diff2.type = DiffView::Change;
	 diffList1.append(diff1);
	 diffList2.append(diff2);
	 //       qDebug("rest changed, line1: "+line1.mid(lastSPos,rest1)+", line2: "+line2.mid(lastTPos,rest2));
      }
   }
}

void DiffDialogImpl::markBestMatch(QString &line1, int offset1, int end1,
      QString &line2, int offset2, int end2,
      QMap<int,MatchMark> &matchMarks) {

   end1 = end1-offset1;
   end2 = end2-offset2;
   QString str1 = line1.mid(offset1,end1+1);
   QString str2 = line2.mid(offset2,end2+1);

   //   qDebug("\nanalyzeChangeDiff(->"+str1+"<-,->"+str2+"<-)\n");
   //   qDebug("end1: "+QString::number(end1)+", end2: "+QString::number(end2));

   int tokenStart = 0;
   int tokenLen = 0;
   int matchStart = 0;

   //set minimum match size
   int maxMatchLen = CvsOptions::g_precision-1;

   int maxMatchSStart = -1;
   int maxMatchTStart = -1;

   QChar c;
   QChar cn;

   bool isLetterToken = FALSE;

   int pos = 0;
   while ( (pos <= end1) ) {
      //     qDebug("outer loop pos: "+QString::number(pos));

      //get token ('letters,numbers,_' in any length or single char)
      c = str1.at(pos);
      tokenLen = 1;
      if ( (c.isLetterOrNumber() || c=='_') && (isLetterToken = TRUE) && (pos+tokenLen <= end1) ) {
	 do {
	    c = str1.at(pos+tokenLen);
	 } while ( ( c.isLetterOrNumber() || c=='_') && (pos+(++tokenLen) <= end1) );
      } else isLetterToken = FALSE;

      tokenStart = pos;
      int firstTokenLen = tokenLen;
      QString token = str1.mid(tokenStart,firstTokenLen);
      //     qDebug("use token: ->"+token+"<-, len: "+QString::number(tokenLen));

      matchStart = str2.find(token);
      //check if char after token (in target str) belongs to the token, i.e. the compared part is longer, and so doesn't match
      if (isLetterToken && (matchStart+tokenLen<=end2) ) {
	 c = str2.at(matchStart+tokenLen);
	 if ( c.isLetterOrNumber() || c=='_') matchStart = -1;
      }
      if (matchStart>-1) {
	 //       qDebug("found token at pos: "+QString::number(matchStart));
	 int afterMatchEnd = matchStart + tokenLen;
	 int tmpTokenLen = 0;
	 pos += tokenLen;

	 while ( (pos <= end1) && (afterMatchEnd <= end2) ) {
	    // 	qDebug("inner loop pos: "+QString::number(pos)+", afterMatchEnd: "+QString::number(afterMatchEnd) );
	    //get next token ('letters,numbers,_' in any length or single char
	    c = str1.at(pos);
	    tmpTokenLen = 1;
	    if ( (c.isLetterOrNumber() || c=='_') && (isLetterToken = TRUE) && (pos+tmpTokenLen <= end1) ) {
	       do {
		  c = str1.at(pos+tmpTokenLen);
	       } while ( ( c.isLetterOrNumber() || c=='_') && (pos+(++tmpTokenLen) <= end1) );
	    } else isLetterToken = FALSE;
	    QString tmpToken = str1.mid(pos,tmpTokenLen);
	    // 	qDebug("intern use token: ->"+tmpToken+"<-, len: "+QString::number(tmpTokenLen));
	    if ( str2.find(tmpToken,afterMatchEnd) == afterMatchEnd) {
	       if (isLetterToken && (afterMatchEnd+tmpTokenLen<=end2) ) {
		  c = str2.at(afterMatchEnd+tmpTokenLen);
		  if ( c.isLetterOrNumber() || c=='_') {//compared part is longer, and so doesn't match
		     // 	      qDebug("intern didn't find token: ->"+tmpToken+"<-, val: "+QString::number(afterMatchEnd));
		     break;
		  }
	       }
	       // 	  qDebug("intern found token: ->"+tmpToken+"<- at: "+QString::number(afterMatchEnd));
	       tokenLen += tmpTokenLen;
	       afterMatchEnd += tmpTokenLen;
	       pos += tmpTokenLen;
	    } else {
	       // 	  qDebug("intern didn't find token: ->"+token+"<-, val: "+QString::number(afterMatchEnd));
	       break;
	    }
	 }
	 if ( tokenLen > maxMatchLen ) {
	    maxMatchSStart = tokenStart;
	    maxMatchTStart = matchStart;
	    maxMatchLen = tokenLen;
	    // 	qDebug("inner found max match: ->"+str1.mid(maxMatchSStart,maxMatchLen)
	    // 	       +"<- at: "+QString::number(maxMatchTStart)+", len: "+QString::number(tokenLen));
	 }
      }
      pos = tokenStart+firstTokenLen;
   }

   if (maxMatchSStart > -1) {
      //     qDebug("found max match: ->"+str1.mid(maxMatchSStart,maxMatchLen)
      // 	   +"<- at: "+QString::number(maxMatchTStart)+", len: "+QString::number(maxMatchLen)+"\n");
      MatchMark mark;
      mark.pos = offset2+maxMatchTStart;
      mark.len = maxMatchLen;

      matchMarks[offset1+maxMatchSStart] = mark;
   } else {
      //     qDebug("\n********** no max match found, returning **********\n");
      return;
   }

   //right half comes first!!
   if ( (maxMatchSStart+maxMatchLen<=end1) && (maxMatchTStart+maxMatchLen<=end2) ) {
      //     qDebug("process right half: ");
      markBestMatch(line1,offset1+maxMatchSStart+maxMatchLen,offset1+end1,line2,offset2+maxMatchTStart+maxMatchLen,offset2+end2,matchMarks);
   }

   //left half is next!!
   if (maxMatchSStart>0 && maxMatchTStart>0) {
      //     qDebug("process left half: ");
      markBestMatch(line1,offset1,offset1+maxMatchSStart-1,line2,offset2,offset2+maxMatchTStart-1,matchMarks);
   }

   //   qDebug("\n********** end of func, line1: ->"+line1+"<- line2: ->"+line2+"<- **********\n");

}

void DiffDialogImpl::enterWhatsThisMode()
{
   QWhatsThis::enterWhatsThisMode();
}

void DiffDialogImpl::SplitterStyle::drawPrimitive(PrimitiveElement pe, QPainter *p,
      const QRect &r, const QColorGroup &cg, SFlags flags /*= Style_Default*/,
      const QStyleOption &StyleOption /*= QStyleOption::Default*/) const
{ 
   if (pe != PE_Splitter) {
      QApplication::style().drawPrimitive(pe, p, r, cg, flags, StyleOption);
   } else {

      int top = dlg->revlabel1->height();
      int h = 0;
      dlg->m_pDiffA->adjustScrollBarOffsets(top,h);

      double ppl = (double)h/(double)dlg->m_linesTotal;

      ChangesMap::Iterator it;
      for ( it = dlg->m_changes.begin(); it != dlg->m_changes.end(); ++it ) {
	 QColor c;
	 switch(it.data().type()) {
	    case DiffView::Change: {//red
	       c.setRgb(255, 0, 0);
	       break;
	    }
	    case DiffView::Insert: {//blue
	       c.setRgb(0, 65, 255);
	       break;
	    }
	    case DiffView::Delete: {//green
	       c.setRgb(43, 190, 65);
	       break;
	    }
	    default: {
	       continue;
	    }
	 }
	 int y = (int)(it.key()*ppl);
	 int height = (int)(it.data().lines()*ppl);
	 if (height<1) {
	    height = 1;
	 }
	 p->setPen(c);
	 p->setBrush(c);
	 p->drawRect(0,top+y,r.width(),height);
      }
   }
}

