/** Copyright (C) 2006, Ian Paul Larsen.
 **
 **  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 <https://www.gnu.org/licenses/>.
 **/



#include <iostream>
#include <QScrollBar>
#include <QTextCursor>
#include <QTextBlock>
#include <QFlags>
#include <QPainter>
#include <QResizeEvent>
#include <QPaintEvent>
#include <QRegularExpression>
#include <QtWidgets/QMessageBox>
#include <QtWidgets/QStatusBar>
#ifdef BASIC256_ENABLE_PRINTER
#include <QtPrintSupport/QPrinter>
#include <QtPrintSupport/QPrintDialog>
#endif
#include <QtWidgets/QFontDialog>

#include "MainWindow.h"
#include "BasicEdit.h"
#include "LineNumberArea.h"
#include "Settings.h"
#include "Constants.h"
#include "EditorTheme.h"
#include "EditSyntaxHighlighter.h"

extern int guiState;

BasicEdit::BasicEdit(const QString & defaulttitle) {
	currentLine = 1;
	runState = RUNSTATESTOP;
    rightClickBlockNumber = -1;
    breakPoints = new QList<int>;
    title = defaulttitle;
    windowtitle = defaulttitle;
    filename.clear();
    path.clear();
    undoButton = false;
    redoButton = false;
    copyButton = false;
    action = new QAction(windowtitle, this);
    action->setCheckable(true);
    fileChangedOnDiskFlag = false;

    // Paint the editor from EditorTheme rather than leaving it to the OS
    // colour scheme. The syntax highlighter sets only foreground colours and
    // never a background, so on Qt 6.5+ under a dark desktop theme the widget's
    // Base role turns near-black and the code becomes unreadable. Setting both
    // here keeps the pane, its gutter and the highlight bands consistent with
    // each other whichever theme is in force.
    setStyleSheet(EditorTheme::current().paneStyleSheet("QPlainTextEdit"));

    setReadOnly(guiState!=GUISTATENORMAL);
    if(guiState==GUISTATEAPP){
        startPos=0;
        lineNumberArea = NULL;
        setDisabled(true);
    }else{
        lineNumberArea = new LineNumberArea(this);
        this->setInputMethodHints(Qt::ImhNoPredictiveText);
        startPos = this->textCursor().position();
        updateLineNumberAreaWidth(0);
        connect(this, SIGNAL(blockCountChanged(int)), this, SLOT(updateLineNumberAreaWidth(int)));
        connect(this, SIGNAL(updateRequest(QRect,int)), this, SLOT(updateLineNumberArea(QRect,int)));
        connect(this, SIGNAL(cursorPositionChanged()), this, SLOT(cursorMove()));
        connect(this, SIGNAL(cursorPositionChanged()), this, SLOT(highlightCurrentLine()));
        connect(this, SIGNAL(modificationChanged(bool)), this, SLOT(updateTitle()));
        connect(this, SIGNAL(undoAvailable(bool)), this, SLOT(slotUndoAvailable(bool)));
        connect(this, SIGNAL(redoAvailable(bool)), this, SLOT(slotRedoAvailable(bool)));
        connect(this, SIGNAL(copyAvailable(bool)), this, SLOT(slotCopyAvailable(bool)));
        connect(action, SIGNAL(triggered()), this, SLOT(actionWasTriggered()));
        highlightCurrentLine();
    }
}


BasicEdit::~BasicEdit() {
    if (breakPoints) {
        delete breakPoints;
        breakPoints = NULL;
    }
    if (lineNumberArea) {
		delete lineNumberArea;
		lineNumberArea = NULL;
	}
}

void BasicEdit::setFont(QFont f) {
	QPlainTextEdit::setFont(f);
	QFontMetrics metrics(f);
	setTabStopDistance(metrics.horizontalAdvance(QLatin1Char(' ')) * EDITOR_TAB_WIDTH);
	updateLineNumberAreaWidth(blockCount());
}


void
BasicEdit::cursorMove() {
	QTextCursor t(textCursor());
	emit(changeStatusBar(tr("Line: ") + QString::number(t.blockNumber()+1)
		+ tr(" Character: ") + QString::number(t.positionInBlock())));
}

void
BasicEdit::seekLine(int newLine) {
    // go to a line number and set
	// the text cursor
	//
	// code should be proximal in that it should be closest to look at the curent
    // position than to go and search the entire program from the top
    QTextCursor t = textCursor();
    int line = t.blockNumber()+1;	// current line number for the block
    // go back or forward to the line from the current position
    if (line<newLine) {
        // advance forward
        while (line < newLine && t.movePosition(QTextCursor::NextBlock)) {
            line++;
        }
    } else if (line>newLine){
        // go back to find the line
        while (line > newLine && t.movePosition(QTextCursor::PreviousBlock)) {
            line--;
        }
    }
	setTextCursor(t);
}

void BasicEdit::slotWhitespace(bool checked) {
	// toggle the display of whitespace characters
	// http://www.qtcentre.org/threads/27245-Printing-white-spaces-in-QPlainTextEdit-the-QtCreator-way
	QTextOption option = document()->defaultTextOption();
	if (checked) {
		option.setFlags(option.flags() | QTextOption::ShowTabsAndSpaces);
	} else {
		option.setFlags(option.flags() & ~QTextOption::ShowTabsAndSpaces);
	}
	option.setFlags(option.flags() | QTextOption::AddSpaceForLineAndParagraphSeparators);
	document()->setDefaultTextOption(option);
}

void BasicEdit::slotWrap(bool checked) {
	if (checked) {
		setLineWrapMode(QPlainTextEdit::WidgetWidth);
	} else {
		setLineWrapMode(QPlainTextEdit::NoWrap);
	}
}

void
BasicEdit::goToLine(int newLine) {
    seekLine(newLine);
    setFocus();
}


void
BasicEdit::keyPressEvent(QKeyEvent *e) {
	e->accept();
    //Autoindent new line as previous one
	if (e->key() == Qt::Key_Return || e->key() == Qt::Key_Enter){
		QPlainTextEdit::keyPressEvent(e);
		QTextCursor cur = textCursor();
		cur.movePosition(QTextCursor::PreviousBlock);
		cur.movePosition(QTextCursor::StartOfBlock);
		cur.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
		QString str = cur.selectedText();
		QRegularExpression rx("^([\\t ]+)");
		QRegularExpressionMatch rxmatch = rx.match(str);
		if(rxmatch.hasMatch())
			textCursor().insertText(rxmatch.captured(1));
	}else if(e->key() == Qt::Key_Tab && e->modifiers() == Qt::NoModifier){
		if(!indentSelection())
			QPlainTextEdit::keyPressEvent(e);
	}else if((e->key() == Qt::Key_Tab && e->modifiers() & Qt::ShiftModifier) || e->key() == Qt::Key_Backtab){
		unindentSelection();
	}else{
		QPlainTextEdit::keyPressEvent(e);
	}
}


void BasicEdit::saveFile(bool overwrite) {
#ifdef Q_OS_WASM
	// No real filesystem path on WASM, so no getSaveFileName() prompt and
	// no overwrite concept (the "overwrite" parameter is meaningless here:
	// browsers can't silently overwrite a previously-downloaded file --
	// every save is a fresh download via saveFileContent(), same as a
	// desktop "Save As" every time). saveFileContent() gives no completion
	// callback (fire-and-forget from Qt's side), so setModified(false) is
	// optimistic, same as it would be if a desktop write silently failed.
	(void)overwrite;
	emit(setCurrentEditorTab(this));
	QString suggested = title;
	QRegularExpression rx("\\.[^\\/]*$");
	if (!suggested.contains(rx)) {
		suggested += ".kbs";
	}
	QFileDialog::saveFileContent(this->document()->toPlainText().toUtf8(), suggested);
	document()->setModified(false);
#else
	// BE SURE TO SET filename PROPERTY FIRST
	// or set it to '' to prompt for a new file name
	if (filename == "") {
        emit(setCurrentEditorTab(this)); //activate editor window
        filename = QFileDialog::getSaveFileName(this, tr("Save file as"), title+".kbs", tr("BASIC-256 File ") + "(*.kbs);;" + tr("Any File ")  + "(*.*)");
	}

	if (filename != "") {
		QRegularExpression rx("\\.[^\\/]*$");
		if (!filename.contains(rx)) {
			filename += ".kbs";
		}
		if (!overwrite && QFile::exists(filename)) {
			// Async (RULE 2): QMessageBox::warning()'s exec() never returns
			// on the WASM main thread without Asyncify -- show the prompt
			// non-modally and continue the write in the completion slot
			// instead of blocking here for the Yes/No answer.
			QMessageBox *msgBox = new QMessageBox(this);
			msgBox->setAttribute(Qt::WA_DeleteOnClose);
			msgBox->setWindowTitle(tr("Save File"));
			msgBox->setText(tr("The file ") + filename + tr(" already exists.")+ "\n" +tr("Do you want to overwrite?"));
			msgBox->setIcon(QMessageBox::Warning);
			msgBox->setStandardButtons(QMessageBox::Yes | QMessageBox::No);
			msgBox->setDefaultButton(QMessageBox::No);
			QObject::connect(msgBox, &QMessageBox::finished, this, [this](int result){
				if (result == QMessageBox::Yes) {
					writeFile();
				}
			});
			// open() forces window-modal, unlike exec()'s old application-modal
			// default, which on Windows leaves the main window's title bar
			// close button live while this dialog is up.
			msgBox->setWindowModality(Qt::ApplicationModal);
			msgBox->show();
		} else {
			writeFile();
		}
	}
#endif
}

#ifndef Q_OS_WASM
void BasicEdit::writeFile() {
	// Suspend the on-disk watch for the duration of our own write, or the
	// watcher reports it back to us as "the file changed outside the editor".
	emit(unwatchFile(filename));
	QFile f(filename);
	f.open(QIODevice::WriteOnly | QIODevice::Truncate);
	f.write(this->document()->toPlainText().toUtf8());
	f.close();
	QFileInfo fi(f);
	document()->setModified(false);
	setTitle(fi.fileName());
	QDir::setCurrent(fi.absolutePath());
	emit(addFileToRecentList(filename));
	emit(watchFile(filename));
}
#endif

void BasicEdit::saveAllStep(int s) {
    if(document()->isModified()){
        if(s==1){
            // Step 1 - save changes
            if(!filename.isEmpty()) saveFile(true);
        }else{
            // Step 2 - save unsaved files (need user interaction)
            if(filename.isEmpty()) saveFile(true);
        }
    }
}

void BasicEdit::saveProgram() {
    saveFile(false);
}

void
BasicEdit::saveAsProgram() {
#ifdef Q_OS_WASM
    // No real "save to a different path" concept on WASM -- every save is
    // already a fresh browser download regardless of which button is
    // clicked (see saveFile()'s Q_OS_WASM branch), so Save As reuses it.
    saveFile(false);
#else
    QString tempfilename = QFileDialog::getSaveFileName(this, tr("Save file as"), ".", tr("BASIC-256 File ")+ "(*.kbs);;" + tr("Any File ")+ "(*.*)");
    if (tempfilename != "") {
        filename = tempfilename;
        saveFile(false);
    }
#endif
}




void BasicEdit::slotPrint() {
#if !defined(BASIC256_ENABLE_PRINTER)
    QMessageBox::warning(this, tr("Print"),
        tr("Printing is not supported in this platform at this time."));
#else
    QTextDocument *document = this->document();
    QPrinter printer;

    QPrintDialog *dialog = new QPrintDialog(&printer, this);
    dialog->setWindowTitle(tr("Print Code"));

    if (dialog->exec() == QDialog::Accepted) {
        if ((printer.printerState() != QPrinter::Error) && (printer.printerState() != QPrinter::Aborted)) {

            QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
            document->print(&printer);
            QApplication::restoreOverrideCursor();
        } else {
            QMessageBox::warning(this, tr("Print"),
                tr("Unable to carry out printing.\nPlease check your printer settings."));
        }

    }
    
    delete dialog;
#endif
}



void BasicEdit::beautifyProgram() {
	QString program;
	QStringList lines;
	int indent = 0;
	bool indentThisLine = true;
	bool increaseIndent = false;
	bool decreaseIndent = false;
	bool increaseIndentDouble = false;
	bool decreaseIndentDouble = false;
	QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
	program = this->document()->toPlainText();
	lines = program.split(QRegularExpression("\\n"));
	for (int i = 0; i < lines.size(); i++) {
		QString line = lines.at(i);
		line = line.trimmed();
        if(line.isEmpty()){
            // label - empty line no indent
            indentThisLine = false;
        } else if (line.contains(QRegularExpression("^\\S+[:]"))) {
			// label - one line no indent
			indentThisLine = false;
		} else if (line.contains(QRegularExpression("^(for)|(foreach)\\s", QRegularExpression::CaseInsensitiveOption))) {
			// for - indent next (block of code)
			increaseIndent = true;
		} else if (line.contains(QRegularExpression("^next(\\s)", QRegularExpression::CaseInsensitiveOption))) {
			// next var - come out of block - reduce indent
			decreaseIndent = true;
		} else if (line.contains(QRegularExpression("^next$", QRegularExpression::CaseInsensitiveOption))) {
			// next - come out of block - reduce indent
			decreaseIndent = true;
		} else if (line.contains(QRegularExpression("^if\\s.+\\sthen\\s*((#|(rem\\s)).*)?$", QRegularExpression::CaseInsensitiveOption))) {
			// if/then (NOTHING FOLLOWING) - indent next (block of code)
			increaseIndent = true;
		} else if (line.contains(QRegularExpression("^else\\s*((#|(rem\\s)).*)?$", QRegularExpression::CaseInsensitiveOption))) {
			// else - come out of block and start new block
			decreaseIndent = true;
			increaseIndent = true;
		} else if (line.contains(QRegularExpression("^end\\s*if\\s*((#|(rem\\s)).*)?$", QRegularExpression::CaseInsensitiveOption))) {
			// end if - come out of block - reduce indent
			decreaseIndent = true;
		} else if (line.contains(QRegularExpression("^while\\s", QRegularExpression::CaseInsensitiveOption))) {
			// while - indent next (block of code)
			increaseIndent = true;
		} else if (line.contains(QRegularExpression("^end\\s*while\\s*((#|(rem\\s)).*)?$", QRegularExpression::CaseInsensitiveOption))) {
			// endwhile - come out of block
			decreaseIndent = true;
		} else if (line.contains(QRegularExpression("^function\\s", QRegularExpression::CaseInsensitiveOption))) {
			// function - indent next (block of code)
			increaseIndent = true;
		} else if (line.contains(QRegularExpression("^end\\s*function\\s*((#|(rem\\s)).*)?$", QRegularExpression::CaseInsensitiveOption))) {
			// endfunction - come out of block
			decreaseIndent = true;
		} else if (line.contains(QRegularExpression("^subroutine\\s", QRegularExpression::CaseInsensitiveOption))) {
			// function - indent next (block of code)
			increaseIndent = true;
		} else if (line.contains(QRegularExpression("^end\\s*subroutine\\s*((#|(rem\\s)).*)?$", QRegularExpression::CaseInsensitiveOption))) {
			// endfunction - come out of block
			decreaseIndent = true;
		} else if (line.contains(QRegularExpression("^do\\s*((#|(rem\\s)).*)?$", QRegularExpression::CaseInsensitiveOption))) {
			// do - indent next (block of code)
			increaseIndent = true;
		} else if (line.contains(QRegularExpression("^until\\s", QRegularExpression::CaseInsensitiveOption))) {
			// until - come out of block
			decreaseIndent = true;
		} else if (line.contains(QRegularExpression("^try\\s*((#|(rem\\s)).*)?$", QRegularExpression::CaseInsensitiveOption))) {
			// try indent next (block of code)
			increaseIndent = true;
		} else if (line.contains(QRegularExpression("^catch\\s*((#|(rem\\s)).*)?$", QRegularExpression::CaseInsensitiveOption))) {
			// catch - come out of block and start new block
			decreaseIndent = true;
			increaseIndent = true;
		} else if (line.contains(QRegularExpression("^end\\s*try\\s*((#|(rem\\s)).*)?$", QRegularExpression::CaseInsensitiveOption))) {
			// end try - come out of block - reduce indent
			decreaseIndent = true;
		} else if (line.contains(QRegularExpression("^begin\\s*case\\s*((#|(rem\\s)).*)?$", QRegularExpression::CaseInsensitiveOption))) {
			// begin case double indent next (block of code)
			increaseIndentDouble = true;
		} else if (line.contains(QRegularExpression("^end\\s*case\\s*((#|(rem\\s)).*)?$", QRegularExpression::CaseInsensitiveOption))) {
			// end case double reduce
			decreaseIndentDouble = true;
		} else if (line.contains(QRegularExpression("^case\\s.+\\s*((#|(rem\\s)).*)?$", QRegularExpression::CaseInsensitiveOption))) {
			// case expression - indent one line
			decreaseIndent = true;
			increaseIndent = true;
		}
		//
		if (decreaseIndent) {
			indent--;
			if (indent<0) indent=0;
			decreaseIndent = false;
		}
		if (decreaseIndentDouble) {
			indent-=2;
			if (indent<0) indent=0;
			decreaseIndentDouble = false;
		}
		if (indentThisLine) {
			line = QString(indent, QChar('\t')) + line;
		} else {
			indentThisLine = true;
		}
		if (increaseIndent) {
			indent++;
			increaseIndent = false;
		}
		if (increaseIndentDouble) {
			indent+=2;
			increaseIndentDouble = false;
		}
		//
		lines.replace(i, line);
	}
    //this->setPlainText(lines.join("\n"));
    QTextCursor cursor = this->textCursor();
    cursor.select(QTextCursor::Document);
    cursor.insertText(lines.join("\n"));
	QApplication::restoreOverrideCursor();
}

void BasicEdit::findString(QString s, bool reverse, bool casesens, bool words)
{
	if(s.length()==0) return;
	QTextDocument::FindFlags flag;
	if (reverse) flag |= QTextDocument::FindBackward;
	if (casesens) flag |= QTextDocument::FindCaseSensitively;
	if (words) flag |= QTextDocument::FindWholeWords;

	QTextCursor cursor = this->textCursor();
	// here we save the cursor position and the verticalScrollBar value
    QTextCursor cursorSaved = cursor;
    int scroll = verticalScrollBar()->value();

    if (!find(s, flag))
	{
        //nothing is found | jump to start/end
        setUpdatesEnabled(false);
        cursor.movePosition(reverse?QTextCursor::End:QTextCursor::Start);
        setTextCursor(cursor);

        if (!find(s, flag))
        {
            // word not found : we set the cursor back to its initial position and restore verticalScrollBar value
			setTextCursor(cursorSaved);
            verticalScrollBar()->setValue(scroll);
            setUpdatesEnabled(true);
            QMessageBox::information(this, tr("Find"),
                tr("String not found."),
                QMessageBox::Ok, QMessageBox::Ok);
        }else{
            //start from current screen position -> no scroll or scroll only as needed
            //if next search is in the same view
            verticalScrollBar()->setValue(scroll);
            // we can use ensureCursorVisible() but we want to make sure that entire selection is visible
            cursor = this->textCursor();
            cursorSaved = cursor;
            cursor.setPosition(cursor.selectionStart());
            setTextCursor(cursor);
            setTextCursor(cursorSaved);
            setUpdatesEnabled(true);
        }
    }
}

void BasicEdit::replaceString(QString from, QString to, bool reverse, bool casesens, bool words, bool doall) {
	if(from.length()==0) return;

	// Replace one time.
	if(!doall){
		//Replace if text is selected - use the cursor from the last find not the copy
        QTextCursor cursor = this->textCursor();
        if (from.compare(cursor.selectedText(),(casesens ? Qt::CaseSensitive : Qt::CaseInsensitive))==0){
			cursor.insertText(to);
		}

		//Make a search
        findString(from, reverse, casesens, words);

    //Replace all
    }else{
		QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
		setUpdatesEnabled(false);
		QTextCursor cursorSaved = textCursor();
		int scroll = verticalScrollBar()->value();
		QTextCursor cursor = textCursor();
		cursor.beginEditBlock();
		int n = 0;
		cursor.movePosition(QTextCursor::Start);
		setTextCursor(cursor);
		QTextDocument::FindFlags flag;
		if (casesens) flag |= QTextDocument::FindCaseSensitively;
		if (words) flag |= QTextDocument::FindWholeWords;
		while (find(from, flag)){
			if (textCursor().hasSelection()){
				textCursor().insertText(to);
				n++;
			}
		}
		cursor.endEditBlock();
		setUpdatesEnabled(true);
		QApplication::restoreOverrideCursor();
		// set the cursor back to its initial position and restore verticalScrollBar value
		setTextCursor(cursorSaved);
		verticalScrollBar()->setValue(scroll);
		if(n==0)
			QMessageBox::information(this, tr("Replace"),
				tr("String not found."),
				QMessageBox::Ok, QMessageBox::Ok);
		else
			QMessageBox::information(this, tr("Replace"),
				tr("Replace completed.") + "\n" + QString::number(n) + " " + tr("occurrence(s) were replaced."),
				QMessageBox::Ok, QMessageBox::Ok);
	}
}

QString BasicEdit::getCurrentWord() {
    // get the cursor and find the current word on this line
    // anything but letters delimits
    QString w;
    QTextCursor t(textCursor());
    QTextBlock b(t.block());
    w = b.text();
    w = w.left(w.indexOf(QRegularExpression("[^a-zA-Z0-9]"),t.positionInBlock()));
    w = w.mid(w.lastIndexOf(QRegularExpression("[^a-zA-Z0-9]"))+1);
    return w;
}

int BasicEdit::lineNumberAreaWidth() {
    int digits = 1;
    int max = qMax(1, blockCount());
    while (max >= 10) {
        max /= 10;
        ++digits;
    }
    int space = 10 + fontMetrics().horizontalAdvance(QLatin1Char('9')) * digits;
    return space;
}

void BasicEdit::updateLineNumberAreaWidth(int /* newBlockCount */) {
    //update setViewportMargins only when lineNumberAreaWidth is changed - save CPU
    //(this signal is emitted even when user scroll up or scroll down code)
    int w = lineNumberAreaWidth();
    if(w!=lastLineNumberAreaWidth){
        lastLineNumberAreaWidth=w;
        setViewportMargins(w, 0, 0, 0);
    }
}

void BasicEdit::updateLineNumberArea(const QRect &rect, int dy) {
    if (dy) {
        lineNumberArea->scroll(0, dy);
    } else {
        lineNumberArea->update(0, rect.y(), lineNumberArea->width(), rect.height());
    }
}



void BasicEdit::applyTheme() {
    setStyleSheet(EditorTheme::current().paneStyleSheet("QPlainTextEdit"));
    // The highlighter is parented to the document by the QSyntaxHighlighter
    // base ctor, so nothing else holds the pointer (see MainWindow).
    EditSyntaxHighlighter *h = document()->findChild<EditSyntaxHighlighter*>();
    if (h) h->applyTheme();
    highlightCurrentLine();
    if (lineNumberArea) lineNumberArea->update();
}

void BasicEdit::highlightCurrentLine() {
    if(guiState==GUISTATEAPP) return;
    QList<QTextEdit::ExtraSelection> extraSelections;
    QTextEdit::ExtraSelection blockSelection;
    QTextEdit::ExtraSelection selection;
    QColor lineColor;
    QColor blockColor;

    if(guiState==GUISTATERUN || runState==RUNSTATERUN){
        // editor is in readOnly mode so line will be red (forbidden)
        lineColor = EditorTheme::current().readOnlyLine;
        blockColor = EditorTheme::current().readOnlyBlock;
    }else if (runState==RUNSTATERUNDEBUG || runState==RUNSTATEDEBUG) {
        // if we are executing in debug mode
        lineColor = EditorTheme::current().debugLine;
        blockColor = lineColor;
    }else{
        // in edit mode
        lineColor = EditorTheme::current().currentLine;
        blockColor = EditorTheme::current().currentBlock;
    }

    blockSelection.format.setBackground(blockColor);
    blockSelection.format.setProperty(QTextFormat::FullWidthSelection, true);
    blockSelection.cursor = textCursor();
    blockSelection.cursor.movePosition(QTextCursor::StartOfBlock,QTextCursor::MoveAnchor);
    blockSelection.cursor.movePosition(QTextCursor::EndOfBlock,QTextCursor::KeepAnchor);
    extraSelections.append(blockSelection);

    selection.format.setBackground(lineColor);
    selection.format.setProperty(QTextFormat::FullWidthSelection, true);
    selection.cursor = textCursor();
    selection.cursor.clearSelection();
    extraSelections.append(selection);


    //mark brackets if we are editing
	if (runState==RUNSTATESTOP && !isReadOnly()) {
        QTextCursor cur = textCursor();
        int pos = cur.position();
        cur.movePosition(QTextCursor::EndOfBlock, QTextCursor::MoveAnchor);
        cur.movePosition(QTextCursor::StartOfBlock, QTextCursor::KeepAnchor);
        int posDoc = cur.position();
        pos = pos - posDoc -1;
        QString s = cur.selectedText(); //current line as QString
        int length = s.length();
        QChar cPrev, cNext;


        if(length>1){
            QChar open;
            QChar close;
            QChar c;
            bool lookBefore = false;
            bool lookAfter = false;
            int quote=0;
            int doubleQuote=0;
            int i = 0;
            const QColor bracketColor = EditorTheme::current().bracketMatch;
            QTextEdit::ExtraSelection selectionBracket;

            if(pos>0){
                cPrev = s.at(pos);
                if(cPrev==')'){
                    open = '(';
                    lookBefore = true;
                }else if(cPrev==']'){
                    open = '[';
                    lookBefore = true;
                }else if(cPrev=='}'){
                    open = '{';
                    lookBefore = true;
                }
            }

            if(pos<length-1){
                cNext = s.at(pos+1);
                if(cNext=='('){
                    close = ')';
                    lookAfter = true;
                }else if(cNext=='['){
                    close = ']';
                    lookAfter = true;
                }else if(cNext=='{'){
                    close = '}';
                    lookAfter = true;
                }
            }

            if(lookBefore){
                //check for open bracket
                QList<int> list;
                for(i=0;i<pos;i++){
                    c=s.at(i);
                    if(c=='\"' && quote==0){
                        doubleQuote^=1;
                    }else if(c=='\'' && doubleQuote==0){
                        quote^=1;
                    }else if(quote==0 && doubleQuote==0){
                        if(c==open){
                            list.append(i);
                        }else if(c==cPrev){
                            if(!list.isEmpty())
                                list.removeLast();
                        }
                    }
                }
                if(quote==0 && doubleQuote==0 && !list.isEmpty()){
                    //mark current bracket and the corresponding bracket
                    selectionBracket.format.setBackground(bracketColor);
                    selectionBracket.cursor = textCursor();
                    selectionBracket.cursor.setPosition(pos+posDoc+1);
                    selectionBracket.cursor.movePosition(QTextCursor::PreviousCharacter,QTextCursor::KeepAnchor);
                    extraSelections.append(selectionBracket);
                    selectionBracket.cursor.setPosition(list.last()+posDoc+1);
                    selectionBracket.cursor.movePosition(QTextCursor::PreviousCharacter,QTextCursor::KeepAnchor);
                    extraSelections.append(selectionBracket);
                }
            }else if(lookAfter){
                //else check only for quotes if lookAfter
                for(i=0;i<pos;i++){
                    c=s.at(i);
                    if(c=='\"' && quote==0){
                        doubleQuote^=1;
                    }else if(c=='\'' && doubleQuote==0){
                        quote^=1;
                    }
                }
            }

            if(lookAfter && quote==0 && doubleQuote==0){
                //check for bracket if current bracket is not quoted
                int count = 1;
                for(i=pos+2;i<length;i++){
                    c=s.at(i);
                    if(c=='\"' && quote==0){
                        doubleQuote^=1;
                    }else if(c=='\'' && doubleQuote==0){
                        quote^=1;
                    }else if(quote==0 && doubleQuote==0){
                        if(c==cNext){
                            count++;
                        }else if(c==close){
                            count--;
                            if(count==0){
                                //mark current bracket and the corresponding bracket
                                selectionBracket.format.setBackground(bracketColor);
                                selectionBracket.cursor = textCursor();
                                selectionBracket.cursor.setPosition(pos+posDoc+1);
                                selectionBracket.cursor.movePosition(QTextCursor::NextCharacter,QTextCursor::KeepAnchor);
                                extraSelections.append(selectionBracket);
                                selectionBracket.cursor.setPosition(i+posDoc+1);
                                selectionBracket.cursor.movePosition(QTextCursor::PreviousCharacter,QTextCursor::KeepAnchor);
                                extraSelections.append(selectionBracket);
                                break;
                            }
                        }
                    }
                }
            }
        }
    }
    setExtraSelections(extraSelections);
}


void BasicEdit::resizeEvent(QResizeEvent *e) {
    QPlainTextEdit::resizeEvent(e);
    if(lineNumberArea){
        QRect cr = contentsRect();
        lineNumberArea->setGeometry(QRect(cr.left(), cr.top(), lineNumberAreaWidth(), cr.height()));
    }
}


void BasicEdit::lineNumberAreaPaintEvent(QPaintEvent *event) {
    //We need lastBlockNumber to adjust Qt bug (see below)
    QTextCursor t(textCursor());
    int currentBlockNumber = t.blockNumber();
    t.movePosition(QTextCursor::End);
    int lastBlockNumber = t.block().blockNumber();

    QPainter painter(lineNumberArea);
    painter.fillRect(event->rect(), EditorTheme::current().gutterBackground);

    QTextBlock block = firstVisibleBlock();
    int blockNumber = block.blockNumber();
    int top = (int) blockBoundingGeometry(block).translated(contentOffset()).top();
    int bottom = top + (int) blockBoundingRect(block).height();
    int y = -1;

    if(lineNumberArea->underMouse())
        y = lineNumberArea->mapFromGlobal(QCursor::pos()).y();

    while (block.isValid() && top <= event->rect().bottom()) {
        if (block.isVisible() && bottom > event->rect().top()) {
            QString number = QString::number(blockNumber + 1);

            //Draw lighter background for current paragraph
            //For this to work (and to correctly change the color of current paragraph number) highlightCurrentLine()
            //must set a specialSelection for entire current block. If not, QPaintEvent will update only
            //a small portion of lineNumberAreaPaint (just current line, not entire paragraph), acording with Automatic Clipping.
            //Also there is a bug with last block. See http://stackoverflow.com/questions/37890906/qt-linenumberarea-example-blockboundingrectlastblock
            //or http://www.forum.crossplatform.ru/index.php?showtopic=3963
            if (blockNumber==currentBlockNumber)
                painter.fillRect(0,top,lineNumberArea->width(),bottom-top-(blockNumber==lastBlockNumber?3:0), EditorTheme::current().gutterCurrent);
            else if(top <= y && bottom > y && y>=0 && runState != RUNSTATERUN)
                painter.fillRect(0,top,lineNumberArea->width(),bottom-top-(blockNumber==lastBlockNumber?3:0), EditorTheme::current().gutterHover);

            // Draw breakpoints
            if (block.userState()==STATEBREAKPOINT) {
                    painter.setBrush(EditorTheme::current().breakpoint);
                    painter.setPen(EditorTheme::current().breakpoint);
                    int w = lineNumberArea->width();
                    int bh = blockBoundingRect(block).height();
                    int fh = fontMetrics().height();
                    painter.drawEllipse((w-(fh-6))/2, top+(bh-(fh-6))/2, fh-6, fh-6);
            }
            // draw text
            if (blockNumber==currentBlockNumber) {
                painter.setPen(EditorTheme::current().gutterTextCurrent);
            } else {
                painter.setPen(EditorTheme::current().gutterText);
            }
            painter.drawText(0, top, lineNumberArea->width()-5, fontMetrics().height(), Qt::AlignRight, number);
        }

        block = block.next();
        top = bottom;
        bottom = top + (int) blockBoundingRect(block).height();
        ++blockNumber;
    }
}


void BasicEdit::lineNumberAreaMouseWheelEvent(QWheelEvent *event) {
    QPlainTextEdit::wheelEvent(event);
}


//Breakpoints are stick to each paragraph by using setUserState to avoid messing up while delete/edit/drop/paste large sections of text.
//breakPoints list is also needed by interpretor. Before running in debug mode breakPoints list is update by calling updateBreakPointsList().
//Because setting/removing breakpoints is enabled in debug mode, we set/unset breakpoints in breakPoints list too.
void BasicEdit::lineNumberAreaMouseClickEvent(QMouseEvent *event) {
    if (runState == RUNSTATERUN)
        return;
    // based on mouse click - set the breakpoint in the map/block and highlight the line
	int line;
	QTextBlock block = firstVisibleBlock();
    int bottom = (int) blockBoundingGeometry(block).translated(contentOffset()).top(); //bottom from previous block
    line = block.blockNumber();
	// line 0 ... (n-1) of what was clicked
	while(block.isValid()) {
		bottom += blockBoundingRect(block).height();
		if (event->y() < bottom) {
            if(event->button() == Qt::LeftButton){
                //keep breakPoints list update for debug running mode
                if(block.userState()==STATEBREAKPOINT){
                    block.setUserState(STATECLEAR);
                    if (breakPoints->contains(line))
                        breakPoints->removeAt(breakPoints->indexOf(line,0));
                }else{
                    block.setUserState(STATEBREAKPOINT);
                    breakPoints->append(line);
                }
                lineNumberArea->repaint();
            }else if(event->button() == Qt::RightButton){
                rightClickBlockNumber = line;
                QMenu contextMenu(this);
                if(block.userState()==STATEBREAKPOINT)
                    contextMenu.addAction ( tr("Remove breakpoint from line") + " " + QString::number(line+1), this , SLOT (toggleBreakPoint()) );
                else
                    contextMenu.addAction ( tr("Set breakpoint at line") + " " + QString::number(line+1), this , SLOT (toggleBreakPoint()) );
                QAction *act = contextMenu.addAction ( tr("Clear all breakpoints") , this , SLOT (clearBreakPoints()) );
                act->setEnabled(isBreakPoint());
                contextMenu.exec (event->globalPos());
            }
            return;
        }
		block = block.next();
		line++;
	}
    QMenu contextMenu(this);
    contextMenu.addAction ( tr("Clear all breakpoints") , this , SLOT (clearBreakPoints()) );
    contextMenu.exec (event->globalPos());
}

void BasicEdit::toggleBreakPoint(){
    if(rightClickBlockNumber<0)
        return;
    QTextBlock block = firstVisibleBlock();
    int n = block.blockNumber();
    if(n<rightClickBlockNumber){
        for(;n<rightClickBlockNumber;n++){
            block = block.next();
        }
    }else if(n>rightClickBlockNumber){
        for(;n>rightClickBlockNumber;n--){
            block = block.previous();
        }
    }
    if(block.isValid()){
        if(block.blockNumber()==rightClickBlockNumber){
            //keep breakPoints list update for debug running mode
            if(block.userState()==STATEBREAKPOINT){
                block.setUserState(STATECLEAR);
                if (breakPoints->contains(rightClickBlockNumber))
                    breakPoints->removeAt(breakPoints->indexOf(rightClickBlockNumber,0));
            }else{
                block.setUserState(STATEBREAKPOINT);
                breakPoints->append(rightClickBlockNumber);
            }
            lineNumberArea->repaint();
        }
    }
    rightClickBlockNumber=-1;
}

void BasicEdit::clearBreakPoints() {
    // remove all breakpoints
    QTextBlock b(document()->firstBlock());
    while (b.isValid()){
        b.setUserState(STATECLEAR);
        b = b.next();
    }
    breakPoints->clear();
    lineNumberArea->repaint();
}

bool BasicEdit::isBreakPoint() {
    // check if there are breakpoints to be cleared (usefull to update menu)
    QTextBlock b(document()->firstBlock());
    while (b.isValid()){
        if(b.userState()==STATEBREAKPOINT)
            return true;
        b = b.next();
    }
    return false;
}

void BasicEdit::updateBreakPointsList() {
    breakPoints->clear();
    QTextBlock b(document()->firstBlock());
    while (b.isValid()){
        if(b.userState()==STATEBREAKPOINT)
            breakPoints->append(b.blockNumber());
        b = b.next();
    }
}


int BasicEdit::indentSelection() {
	QTextCursor cur = textCursor();
	if(!cur.hasSelection())
			return false;
	int a = cur.anchor();
	int p = cur.position();
	int start = (a<=p?a:p);
	int end = (a>p?a:p);

	cur.beginEditBlock();
	cur.setPosition(end);
	int eblock = cur.block().blockNumber();
	cur.setPosition(start);
	int sblock = cur.block().blockNumber();

	for(int i = sblock; i <= eblock; i++)
	{
		cur.movePosition(QTextCursor::StartOfBlock, QTextCursor::MoveAnchor);
		cur.insertText("\t");
		cur.movePosition(QTextCursor::NextBlock, QTextCursor::MoveAnchor);
	}
	cur.endEditBlock();
return true;
}


void BasicEdit::unindentSelection() {
	QTextCursor cur = textCursor();
	int a = cur.anchor();
	int p = cur.position();
	int start = (a<=p?a:p);
	int end = (a>p?a:p);

	cur.beginEditBlock();
	cur.setPosition(end);
	int eblock = cur.block().blockNumber();
	cur.setPosition(start);
	int sblock = cur.block().blockNumber();
	QString s;

	for(int i = sblock; i <= eblock; i++)
	{
		cur.movePosition(QTextCursor::EndOfBlock, QTextCursor::MoveAnchor);
		cur.movePosition(QTextCursor::StartOfBlock, QTextCursor::KeepAnchor);
		s = cur.selectedText();
		if(!s.isEmpty()){
			if(s.startsWith("    ") || s.startsWith("   \t")){
				cur.movePosition(QTextCursor::StartOfBlock, QTextCursor::MoveAnchor);
				cur.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, 4);
				cur.removeSelectedText();
			}else if(s.startsWith("   ") || s.startsWith("  \t")){
				cur.movePosition(QTextCursor::StartOfBlock, QTextCursor::MoveAnchor);
				cur.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, 3);
				cur.removeSelectedText();
			}else if(s.startsWith("  ") || s.startsWith(" \t")){
				cur.movePosition(QTextCursor::StartOfBlock, QTextCursor::MoveAnchor);
				cur.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, 2);
				cur.removeSelectedText();
			}else if(s.startsWith(" ") || s.startsWith("\t")){
				cur.movePosition(QTextCursor::StartOfBlock, QTextCursor::MoveAnchor);
				cur.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, 1);
				cur.removeSelectedText();
			}
		}
		cur.movePosition(QTextCursor::NextBlock, QTextCursor::MoveAnchor);
	}
	cur.endEditBlock();
}

void BasicEdit::setTitle(QString newTitle){
    title = newTitle;
    updateTitle();
}

void BasicEdit::updateTitle(){
    windowtitle = (document()->isModified()?"*":"") + title;
    action->setText(windowtitle);
    emit(updateWindowTitle(this));
}

void BasicEdit::dragEnterEvent(QDragEnterEvent *event){
    if (event->mimeData()->hasFormat("text/plain")){
        event->acceptProposedAction();
        QPlainTextEdit::dragEnterEvent(event);
    }else{
        event->ignore();
    }
}

void BasicEdit::dropEvent(QDropEvent *event){
    if (event->mimeData()->hasFormat("text/plain")){
        event->acceptProposedAction();
        QPlainTextEdit::dropEvent(event);
    }else{
        event->ignore();
    }
}

void BasicEdit::setEditorRunState(int state){
    if(runState == state) return;
    runState = state;
    if(guiState!=GUISTATEAPP){
        if(state==RUNSTATESTOP&&guiState==GUISTATENORMAL){
            setReadOnly(false);
            setTextInteractionFlags(Qt::TextEditorInteraction);
            if(fileChangedOnDiskFlag) handleFileChangedOnDisk();
        } else if(state==RUNSTATERUN||guiState!=GUISTATENORMAL){
            setReadOnly(true);
        }else{
            //Just like readonly but user can not change current line and highlight another one (need in debug mode)
            setTextInteractionFlags(Qt::NoTextInteraction);
        }
        //setDisabled(!(state==RUNSTATESTOP&&guiState==GUISTATENORMAL));
        highlightCurrentLine();
    }
}

void BasicEdit::slotUndoAvailable(bool val){
    undoButton = val;
    emit(updateEditorButtons());
}

void BasicEdit::slotRedoAvailable(bool val){
    redoButton = val;
    emit(updateEditorButtons());
}
void BasicEdit::slotCopyAvailable(bool val){
    copyButton = val;
    emit(updateEditorButtons());
}

void BasicEdit::actionWasTriggered(){
    emit(setCurrentEditorTab(this));
}

void BasicEdit::fileChangedOnDiskSlot(QString fn){
    if(fn==filename){
        if(runState==RUNSTATESTOP){
            if(!fileChangedOnDiskFlag){
                //avoid fileChangedOnDiskSlot to be fired multiple times while program waits user confirmation
                fileChangedOnDiskFlag=true;
                handleFileChangedOnDisk();
            }
        }else{
            fileChangedOnDiskFlag=true;
        }
    }
}

void BasicEdit::handleFileChangedOnDisk(){
    fileChangedOnDiskFlag=false; // handled below; dialog is now dispatched
    QFileInfo check_file(filename);

    // Nothing to tell the user if what is on disk is already what is on screen.
    // writeFile() suspends the watch while it saves, but a notification queued
    // just before removePath() can still land afterwards, and a file replaced
    // rather than rewritten in place drops off QFileSystemWatcher entirely --
    // so re-arm the watch and stay quiet rather than prompting to reload the
    // very text the editor is showing.
    if(check_file.exists()){
        QFile f(filename);
        if(f.open(QIODevice::ReadOnly)){
            QByteArray ondisk = f.readAll();
            f.close();
            if(ondisk == this->document()->toPlainText().toUtf8()){
                emit(watchFile(filename));
                return;
            }
        }
    }

    document()->setModified(true);
    updateTitle();
    emit(setCurrentEditorTab(this));
    if(!check_file.exists()){
        QMessageBox *msgBox = new QMessageBox(this);
        msgBox->setAttribute(Qt::WA_DeleteOnClose);
        msgBox->setIcon(QMessageBox::Information);
        msgBox->setWindowTitle(tr("File has been removed."));
        msgBox->setText(tr("It seems that file %1 was removed from disk. Don't forget to save your work.").arg(filename));
        msgBox->setStandardButtons(QMessageBox::Ok);
        msgBox->setWindowModality(Qt::ApplicationModal);
        msgBox->show();
    }else{
        QMessageBox *msgBox = new QMessageBox(this);
        msgBox->setAttribute(Qt::WA_DeleteOnClose);
        msgBox->setIcon(QMessageBox::Warning);
        msgBox->setWindowTitle(tr("File changed"));
        msgBox->setText(tr("The file %1 has changed outside BASIC256 editor. Do you want to reload it?").arg(filename));
        msgBox->setStandardButtons(QMessageBox::Yes | QMessageBox::No);
        msgBox->setDefaultButton(QMessageBox::No);
        QObject::connect(msgBox, &QMessageBox::finished, this, [this](int result){
            if (result != QMessageBox::Yes) return;
            //reload changed file from disk but keep undo history to allow reversing this action
            QFile f(filename);
            if(f.open(QIODevice::ReadOnly)){
                QByteArray ba = f.readAll();
                f.close();
                QTextCursor cursor = this->textCursor();
                cursor.beginEditBlock();
                cursor.select(QTextCursor::Document);
                cursor.insertText(QString::fromUtf8(ba.data()));
                cursor.movePosition(QTextCursor::Start);
                setTextCursor(cursor);
                cursor.endEditBlock();
                document()->setModified(false);
                updateTitle();
                // A file replaced (rather than rewritten in place) is dropped by
                // QFileSystemWatcher -- make sure we are still watching it.
                emit(watchFile(filename));
            }else{
                QMessageBox *err = new QMessageBox(this);
                err->setAttribute(Qt::WA_DeleteOnClose);
                err->setIcon(QMessageBox::Critical);
                err->setWindowTitle(tr("Load File"));
                err->setText(tr("Unable to open program file") + " \"" + filename + "\".\n" + tr("File permissions problem or file open by another process."));
                err->setStandardButtons(QMessageBox::Ok);
                err->setWindowModality(Qt::ApplicationModal);
                err->show();
            }
        });
        msgBox->setWindowModality(Qt::ApplicationModal);
        msgBox->show();
    }
}
