/***************************************************************************
 *   copyright       : (C) 2009-2022 by Pascal Brachet                     *
 *   https://www.xm1math.net/algobox/                                      *
 *                                                                         *
 *   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.                                   *
 *                                                                         *
 *   @license GPL-2.0+ <https://spdx.org/licenses/GPL-2.0+.html>           *
 ***************************************************************************/

#include "jseditor.h"

#include <QPainter>
#include <QTextLayout>
#include <QMetaProperty>
#include <QDebug>
#include <QAction>
#include <QMenu>
#include <QApplication>
#include <QMimeData>
#include <QClipboard>
#include <QPalette>
#include <QKeyEvent>
#include <QAbstractItemView>
#include <QApplication>
#include <QModelIndex>
#include <QAbstractItemModel>
#include <QScrollBar>
#include <QTextCodec>
#include <QFile>
#include "blockdata.h"

JSEditor::JSEditor(QWidget *parent,QFont & efont): QTextEdit(parent)
{
QPalette p = palette();
p.setColor(QPalette::Inactive, QPalette::Highlight,p.color(QPalette::Active, QPalette::Highlight));
p.setColor(QPalette::Inactive, QPalette::HighlightedText,p.color(QPalette::Active, QPalette::HighlightedText));
setPalette(p);
setAcceptRichText(false);
setLineWidth(0);
setFrameShape(QFrame::NoFrame);
for (int i = 0; i < 3; ++i) UserBookmark[i]=0;
encoding="";
setFont(efont);
setTabChangesFocus(false);
highlighter = new JSHighlighter(document());
setWordWrapMode(QTextOption::WordWrap);
setCursorWidth(2);
#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0)
setTabStopWidth(fontMetrics().width("  "));
#else
setTabStopDistance(fontMetrics().horizontalAdvance("  "));
#endif
connect(this, SIGNAL(cursorPositionChanged()), this, SLOT(matchAll()));
setFocus();
}
JSEditor::~JSEditor(){

}


void JSEditor::paintEvent(QPaintEvent *event)
{
QRect rect = cursorRect();
rect.setX(0);
rect.setWidth(viewport()->width());
QPainter painter(viewport());
const QBrush brush(QColor("#ececec"));
painter.fillRect(rect, brush);
painter.end();
QTextEdit::paintEvent(event);
}

void JSEditor::contextMenuEvent(QContextMenuEvent *e)
{
QMenu *menu=new QMenu(this);
QAction *a;
// a = menu->addAction(QString::fromUtf8("Défaire"), this, SLOT(undo()));
// a->setShortcut(Qt::CTRL+Qt::Key_Z);
// a->setEnabled(document()->isUndoAvailable());
// a = menu->addAction(QString::fromUtf8("Refaire") , this, SLOT(redo()));
// a->setShortcut(Qt::CTRL+Qt::Key_Y);
// a->setEnabled(document()->isRedoAvailable());
// menu->addSeparator();
// a = menu->addAction(QString::fromUtf8("Couper"), this, SLOT(cut()));
// a->setShortcut(Qt::CTRL+Qt::Key_X);
// a->setEnabled(textCursor().hasSelection());
// a = menu->addAction(QString::fromUtf8("Copier"), this, SLOT(copy()));
// a->setShortcut(Qt::CTRL+Qt::Key_C);
// a->setEnabled(textCursor().hasSelection());
// a = menu->addAction(QString::fromUtf8("Coller") , this, SLOT(paste()));
// a->setShortcut(Qt::CTRL+Qt::Key_P);
// const QMimeData *md = QApplication::clipboard()->mimeData();
// a->setEnabled(md && canInsertFromMimeData(md));
// menu->addSeparator();
// a = menu->addAction(QString::fromUtf8("Chercher"), this, SLOT(wantFind()));
// a->setEnabled(!document()->isEmpty());
// a = menu->addAction(QString::fromUtf8("Remplacer"), this, SLOT(wantReplace()));
// a->setEnabled(!document()->isEmpty());
// menu->addSeparator();
// a = menu->addAction(QString::fromUtf8("Sélectionner tout"), this, SLOT(selectAll()));
// a->setShortcut(Qt::CTRL+Qt::Key_A);
// a->setEnabled(!document()->isEmpty());
// menu->addSeparator();
// a = menu->addAction(QString::fromUtf8("Indenter sélection"), this, SLOT(indentSelection()));
// a->setEnabled(textCursor().hasSelection());
// a = menu->addAction(QString::fromUtf8("Désindenter sélection"), this, SLOT(unindentSelection()));
// a->setEnabled(textCursor().hasSelection());
// menu->addSeparator();
// a = menu->addAction(QString::fromUtf8("Commenter sélection"), this, SLOT(commentSelection()));
// a->setEnabled(textCursor().hasSelection());
// a = menu->addAction(QString::fromUtf8("Décommenter sélection"), this, SLOT(uncommentSelection()));
// a->setEnabled(textCursor().hasSelection());
// menu->addSeparator();
a = menu->addAction(QString::fromUtf8("Aller au signet1"), this, SLOT(gotoBookmark1()));
a->setEnabled(!document()->isEmpty());
a = menu->addAction(QString::fromUtf8("Aller au signet2"), this, SLOT(gotoBookmark2()));
a->setEnabled(!document()->isEmpty());
a = menu->addAction(QString::fromUtf8("Aller au signet3"), this, SLOT(gotoBookmark3()));
a->setEnabled(!document()->isEmpty());
menu->exec(e->globalPos());
delete menu;
}

bool JSEditor::search( const QString &expr, bool cs, bool wo, bool forward, bool startAtCursor )
{
QTextDocument::FindFlags flags = QTextDocument::FindFlags(0);
if (cs) flags |= QTextDocument::FindCaseSensitively;
if (wo) flags |= QTextDocument::FindWholeWords;
QTextCursor c = textCursor();
QTextDocument::FindFlags options;
if (! startAtCursor) 
	{
	c.movePosition(QTextCursor::Start);
	setTextCursor(c);
	}
if (forward == false) flags |= QTextDocument::FindBackward;
QTextCursor found = document()->find(expr, c, flags);

if (found.isNull()) return false;
else 
	{
	setTextCursor(found);
	return true;
	}
}

void JSEditor::replace( const QString &r)
{
int start;
QTextCursor c = textCursor();
if (c.hasSelection()) 
	{
	start=c.selectionStart();
	c.removeSelectedText();
	c.insertText(r);
	c.setPosition(start,QTextCursor::MoveAnchor);
	c.setPosition(start+r.length(),QTextCursor::KeepAnchor);
	setTextCursor(c);
	}
}

void JSEditor::gotoLine( int line )
{
if (line<=numoflines()) setCursorPosition( line, 0 );
}

void JSEditor::commentSelection()
{
bool go=true;
QTextCursor cur=textCursor();
if (cur.hasSelection())
	{
	int start=cur.selectionStart();
	int end=cur.selectionEnd();
	cur.setPosition(start,QTextCursor::MoveAnchor);
	cur.movePosition(QTextCursor::StartOfBlock,QTextCursor::MoveAnchor);
	while ( cur.position() < end && go)
		{
		cur.insertText("//");
		end++;
		end++;
		go=cur.movePosition(QTextCursor::NextBlock,QTextCursor::MoveAnchor);
		}
}	
}

void JSEditor::indentSelection()
{
bool go=true;
QTextCursor cur=textCursor();
if (cur.hasSelection())
	{
	int start=cur.selectionStart();
	int end=cur.selectionEnd();
	cur.setPosition(start,QTextCursor::MoveAnchor);
	cur.movePosition(QTextCursor::StartOfBlock,QTextCursor::MoveAnchor);
	while ( cur.position() < end && go)
		{
		cur.insertText("\t");
		end++;
		go=cur.movePosition(QTextCursor::NextBlock,QTextCursor::MoveAnchor);
		}
	}
}

void JSEditor::uncommentSelection()
{
bool go=true;
QTextCursor cur=textCursor();
if (cur.hasSelection())
	{
	int start=cur.selectionStart();
	int end=cur.selectionEnd();
	cur.setPosition(start,QTextCursor::MoveAnchor);
	cur.movePosition(QTextCursor::StartOfBlock,QTextCursor::MoveAnchor);
	while ( cur.position() < end && go)
		{
		cur.movePosition(QTextCursor::NextCharacter,QTextCursor::KeepAnchor);
		cur.movePosition(QTextCursor::NextCharacter,QTextCursor::KeepAnchor);
		if (cur.selectedText()=="//") 
			{
			cur.removeSelectedText();
			end--;
			end--;
			}
		go=cur.movePosition(QTextCursor::NextBlock,QTextCursor::MoveAnchor);
		}
	}
}

void JSEditor::unindentSelection()
{
bool go=true;
QTextCursor cur=textCursor();
if (cur.hasSelection())
	{
	int start=cur.selectionStart();
	int end=cur.selectionEnd();
	cur.setPosition(start,QTextCursor::MoveAnchor);
	cur.movePosition(QTextCursor::StartOfBlock,QTextCursor::MoveAnchor);
	while ( cur.position() < end && go)
		{
		cur.movePosition(QTextCursor::NextCharacter,QTextCursor::KeepAnchor);
		if (cur.selectedText()=="\t") 
			{
			cur.removeSelectedText();
			end--;
			}
		go=cur.movePosition(QTextCursor::NextBlock,QTextCursor::MoveAnchor);
		}
	}
}

void JSEditor::changeFont(QFont & new_font)
{
setFont(new_font);
}

QString JSEditor::getEncoding()
{
 return encoding;
}

void JSEditor::setEncoding(QString enc)
{
 encoding=enc;
}  

int JSEditor::getCursorPosition(int para, int index)
{
return document()->findBlockByNumber(para).position()+index;
}

void JSEditor::setCursorPosition(int para, int index)
{
int pos=getCursorPosition(para,index);
QTextCursor cur=textCursor();
cur.setPosition(pos,QTextCursor::MoveAnchor);
setTextCursor(cur);
ensureCursorVisible();
setFocus();
}

int JSEditor::numoflines()
{
return document()->blockCount();
}

int JSEditor::linefromblock(const QTextBlock& p)
{
return p.blockNumber()+1;
}

void JSEditor::selectword(int line, int col, QString word)
{
QTextCursor cur=textCursor();
int i = 0;
QTextBlock p = document()->begin();
while ( p.isValid() ) 
	{
	if (line==i) break;
	i++;
	p = p.next();
	}
int pos=p.position();
int offset=word.length();
cur.setPosition(pos+col,QTextCursor::MoveAnchor);
cur.setPosition(pos+col+offset,QTextCursor::KeepAnchor);
setTextCursor(cur);
ensureCursorVisible();
}

QString JSEditor::textUnderCursor() const
 {
QTextCursor tc = textCursor();
int oldpos=tc.position();
tc.select(QTextCursor::WordUnderCursor);
int newpos = tc.selectionStart();
tc.setPosition(newpos, QTextCursor::MoveAnchor);
tc.setPosition(oldpos, QTextCursor::KeepAnchor);
QString word=tc.selectedText();
QString sword=word.trimmed();
if (word.right(1)!=sword.right(1)) word="";
return word;
 }

void JSEditor::keyPressEvent ( QKeyEvent * e ) 
{
if ( e->key()==Qt::Key_Tab) 
    {
    QTextCursor cursor=textCursor();
    QTextBlock block=cursor.block();
    if (block.isValid()) 
	{
	QString txt=block.text();
	if (txt.contains(QString(QChar(0x2022))))
	    {
	    //e->ignore();
	    search(QString(QChar(0x2022)) ,true,true,true,true);
	    return;		
	    }
	}
    QTextEdit::keyPressEvent(e);
    }
else if ( e->key()==Qt::Key_Backtab) 
    {
    QTextCursor cursor=textCursor();
    QTextBlock block=cursor.block();
    if (block.isValid()) 
	{
	QString txt=block.text();
	if (txt.contains(QString(QChar(0x2022))))
	    {
	    //e->ignore();
	    search(QString(QChar(0x2022)) ,true,true,false,true);
	    return;		
	    }
	}
    cursor.movePosition(QTextCursor::PreviousCharacter,QTextCursor::KeepAnchor);
    if (cursor.selectedText()=="\t") 
	    {
	    cursor.removeSelectedText();
	    }
    }
// if (((e->modifiers() & ~Qt::ShiftModifier) == Qt::ControlModifier) && e->key()==Qt::Key_Tab) 
//   {
//   e->ignore();
//   search(QString(0x2022) ,true,true,true,true);
//   return;
//   }
// if (((e->modifiers() & ~Qt::ShiftModifier) == Qt::ControlModifier) && e->key()==Qt::Key_Backtab) 
//   {
//   e->ignore();
//   search(QString(0x2022) ,true,true,false,true);
//   return;
//   }
// if ((e->key()==Qt::Key_Backtab))
// 	{
// 	QTextCursor cursor=textCursor();
// 	cursor.movePosition(QTextCursor::PreviousCharacter,QTextCursor::KeepAnchor);
// 	if (cursor.selectedText()=="\t") 
// 			{
// 			cursor.removeSelectedText();
// 			}
// 	}

else if ((e->key()==Qt::Key_Enter)||(e->key()==Qt::Key_Return))
	{
	QTextEdit::keyPressEvent(e);
	QTextCursor cursor=textCursor();
	cursor.joinPreviousEditBlock();
	QTextBlock block=cursor.block();
	QTextBlock blockprev=block.previous();
	if (blockprev.isValid()) 
		{
		QString txt=blockprev.text();
		int j=0;
		while ( (j<txt.count()) && ((txt[j]==' ') || txt[j]=='\t') ) 
			{
			cursor.insertText(QString(txt[j]));
			j++;
			}
		}
	cursor.endEditBlock();
	}
else QTextEdit::keyPressEvent(e);
}



void JSEditor::insertTag(QString Entity, int dx, int dy)
{
QTextCursor cur=textCursor();
int pos=cur.position();
insertPlainText(Entity);
cur.setPosition(pos,QTextCursor::MoveAnchor);
if (dy>0) cur.movePosition(QTextCursor::Down,QTextCursor::MoveAnchor,dy);
if (dx>0) cur.movePosition(QTextCursor::NextCharacter,QTextCursor::MoveAnchor,dx);
setTextCursor(cur);
setFocus();
}

void JSEditor::insertNewLine()
{
QKeyEvent e(QEvent::KeyPress,Qt::Key_Enter,Qt::NoModifier);
QTextEdit::keyPressEvent(&e);
QTextCursor cursor=textCursor();
cursor.joinPreviousEditBlock();
QTextBlock block=cursor.block();
QTextBlock blockprev=block.previous();
if (blockprev.isValid()) 
	{
	QString txt=blockprev.text();
	int j=0;
	while ( (j<txt.count()) && ((txt[j]==' ') || txt[j]=='\t') ) 
		{
		cursor.insertText(QString(txt[j]));
		j++;
		}
	}
cursor.endEditBlock();  
}

void JSEditor::focusInEvent(QFocusEvent *e)
{
QTextEdit::focusInEvent(e);
}

void JSEditor::wantFind()
{
emit dofind();  
}

void JSEditor::wantReplace()
{
emit doreplace();
}

void JSEditor::gotoBookmark1()
{
int l=UserBookmark[0];
if (l>0) gotoLine(l-1);
}

void JSEditor::gotoBookmark2()
{
int l=UserBookmark[1];
if (l>0) gotoLine(l-1);
}

void JSEditor::gotoBookmark3()
{
int l=UserBookmark[2];
if (l>0) gotoLine(l-1);
}

bool JSEditor::isWordSeparator(QChar c) const
{
    switch (c.toLatin1()) {
    case '.':
    case ',':
    case '?':
    case '!':
    case ':':
    case ';':
    case '-':
    case '<':
    case '>':
    case '[':
    case ']':
//    case '(':
//    case ')':
    case '{':
    case '}':
    case '=':
    case '/':
    case '+':
    case '%':
    case '&':
    case '^':
    case '*':
    case '\'':
    case '"':
    case '~':
        return true;
    default:
        return false;
    }
}

bool JSEditor::isSpace(QChar c) const
{
    return c == QLatin1Char(' ')
        || c == QChar::Nbsp
        || c == QChar::LineSeparator
        || c == QLatin1Char('\t')
        ;
}

void JSEditor::matchAll() 
{
viewport()->update();
QList<QTextEdit::ExtraSelection> selections;
setExtraSelections(selections);
matchPar();
matchBrack();
}

void JSEditor::matchPar() 
{

//QList<QTextEdit::ExtraSelection> selections;
//setExtraSelections(selections);
QTextBlock textBlock = textCursor().block();
BlockData *data = static_cast<BlockData *>( textBlock.userData() );
if ( data ) {
	QVector<ParenthesisInfo *> infos = data->parentheses();
	int pos = textCursor().block().position();

	for ( int i=0; i<infos.size(); ++i ) {
		ParenthesisInfo *info = infos.at(i);
		int curPos = textCursor().position() - textBlock.position();
		// Clicked on a left parenthesis?
		if ( info->position == curPos-1 && info->character == '(' ) {
			if ( matchLeftPar( textBlock, i+1, 0 ) )
				createParSelection( pos + info->position );
		}

		// Clicked on a right parenthesis?
		if ( info->position == curPos-1 && info->character == ')' ) {
			if ( matchRightPar( textBlock, i-1, 0 ) )
				createParSelection( pos + info->position );
		}
	}
}
}

bool JSEditor::matchLeftPar(QTextBlock currentBlock, int index, int numLeftPar ) 
{

BlockData *data = static_cast<BlockData *>( currentBlock.userData() );
QVector<ParenthesisInfo *> infos = data->parentheses();
int docPos = currentBlock.position();

// Match in same line?
for ( ; index<infos.size(); ++index ) {
	ParenthesisInfo *info = infos.at(index);

	if ( info->character == '(' ) {
		++numLeftPar;
		continue;
	}

	if ( info->character == ')' && numLeftPar == 0 ) {
		createParSelection( docPos + info->position );
		return true;
	} else
		--numLeftPar;
}

// No match yet? Then try next block
currentBlock = currentBlock.next();
if ( currentBlock.isValid() )
	return matchLeftPar( currentBlock, 0, numLeftPar );

// No match at all
return false;
}

bool JSEditor::matchRightPar(QTextBlock currentBlock, int index, int numRightPar) 
{

BlockData *data = static_cast<BlockData *>( currentBlock.userData() );
QVector<ParenthesisInfo *> infos = data->parentheses();
int docPos = currentBlock.position();

// Match in same line?
for (int j=index; j>=0; --j ) {
	ParenthesisInfo *info = infos.at(j);

	if ( info->character == ')' ) {
		++numRightPar;
		continue;
	}

	if ( info->character == '(' && numRightPar == 0 ) {
		createParSelection( docPos + info->position );
		return true;
	} else
		--numRightPar;
}

// No match yet? Then try previous block
currentBlock = currentBlock.previous();
if ( currentBlock.isValid() ) {

	// Recalculate correct index first
	BlockData *data = static_cast<BlockData *>( currentBlock.userData() );
	QVector<ParenthesisInfo *> infos = data->parentheses();

	return matchRightPar( currentBlock, infos.size()-1, numRightPar );
}

// No match at all
return false;
}

void JSEditor::matchBrack() 
{

//QList<QTextEdit::ExtraSelection> selections;
//setExtraSelections(selections);
QTextBlock textBlock = textCursor().block();
BlockData *data = static_cast<BlockData *>( textBlock.userData() );
if ( data ) {
	QVector<ParenthesisInfo *> infos = data->brackets();
	int pos = textCursor().block().position();

	for ( int i=0; i<infos.size(); ++i ) {
		ParenthesisInfo *info = infos.at(i);
		int curPos = textCursor().position() - textBlock.position();
		// Clicked on a left parenthesis?
		if ( info->position == curPos-1 && info->character == '{' ) {
			if ( matchLeftBrack( textBlock, i+1, 0 ) )
				createBrackSelection( pos + info->position );
		}

		// Clicked on a right parenthesis?
		if ( info->position == curPos-1 && info->character == '}' ) {
			if ( matchRightBrack( textBlock, i-1, 0 ) )
				createBrackSelection( pos + info->position );
		}
	}
}
}

bool JSEditor::matchLeftBrack(QTextBlock currentBlock, int index, int numLeftBrack ) 
{

BlockData *data = static_cast<BlockData *>( currentBlock.userData() );
QVector<ParenthesisInfo *> infos = data->brackets();
int docPos = currentBlock.position();

// Match in same line?
for ( ; index<infos.size(); ++index ) {
	ParenthesisInfo *info = infos.at(index);

	if ( info->character == '{' ) {
		++numLeftBrack;
		continue;
	}

	if ( info->character == '}' && numLeftBrack == 0 ) {
		createBrackSelection( docPos + info->position );
		return true;
	} else
		--numLeftBrack;
}

// No match yet? Then try next block
currentBlock = currentBlock.next();
if ( currentBlock.isValid() )
	return matchLeftBrack( currentBlock, 0, numLeftBrack );

// No match at all
return false;
}

bool JSEditor::matchRightBrack(QTextBlock currentBlock, int index, int numRightBrack) 
{

BlockData *data = static_cast<BlockData *>( currentBlock.userData() );
QVector<ParenthesisInfo *> infos = data->brackets();
int docPos = currentBlock.position();

// Match in same line?
for (int j=index; j>=0; --j ) {
	ParenthesisInfo *info = infos.at(j);

	if ( info->character == '}' ) {
		++numRightBrack;
		continue;
	}

	if ( info->character == '{' && numRightBrack == 0 ) {
		createBrackSelection( docPos + info->position );
		return true;
	} else
		--numRightBrack;
}

// No match yet? Then try previous block
currentBlock = currentBlock.previous();
if ( currentBlock.isValid() ) {

	// Recalculate correct index first
	BlockData *data = static_cast<BlockData *>( currentBlock.userData() );
	QVector<ParenthesisInfo *> infos = data->brackets();

	return matchRightBrack( currentBlock, infos.size()-1, numRightBrack );
}

// No match at all
return false;
}

void JSEditor::createParSelection( int pos ) 
{
QList<QTextEdit::ExtraSelection> selections = extraSelections();
QTextEdit::ExtraSelection selection;
QTextCharFormat format = selection.format;
format.setBackground( QColor("#FFFF99") );
format.setForeground( QColor("#FF0000") );
selection.format = format;

QTextCursor cursor = textCursor();
cursor.setPosition( pos );
cursor.movePosition( QTextCursor::NextCharacter, QTextCursor::KeepAnchor );
selection.cursor = cursor;
selections.append( selection );
setExtraSelections( selections );
}

void JSEditor::createBrackSelection( int pos ) 
{
QList<QTextEdit::ExtraSelection> selections = extraSelections();
QTextEdit::ExtraSelection selection;
QTextCharFormat format = selection.format;
format.setBackground( QColor("#FFFF99") );
format.setForeground( QColor("#FF0000") );
selection.format = format;

QTextCursor cursor = textCursor();
cursor.setPosition( pos );
cursor.movePosition( QTextCursor::NextCharacter, QTextCursor::KeepAnchor );
selection.cursor = cursor;
selections.append( selection );
setExtraSelections( selections );
}
