/*
    SPDX-FileCopyrightText: 2003-2008 Cies Breijs <cies AT kde DOT nl>

    SPDX-License-Identifier: GPL-2.0-or-later
*/

#include "editor.h"

#include <QBuffer>
#include <QFileDialog>
#include <QFontDatabase>
#include <QHBoxLayout>
#include <QSaveFile>
#include <QTemporaryFile>
#include <QTextStream>

#include <KFind>
#include <KIO/StoredTransferJob>
#include <KLocalizedString>
#include <KMessageBox>

static const int CURSOR_WIDTH = 2; // in pixels
static const int TAB_WIDTH = 2; // in character widths

Editor::Editor(QWidget *parent)
    : QFrame(parent)
{
    setFrameStyle(QFrame::StyledPanel | QFrame::Sunken);
    setLineWidth(CURSOR_WIDTH);
    setCurrentUrl();
    currentRow = 1;
    currentCol = 1;

    // setup the main view
    editor = new TextEdit(this);
    editor->document()->setDefaultFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
    editor->setFrameStyle(QFrame::NoFrame);
    editor->installEventFilter(this);
    editor->setLineWrapMode(QTextEdit::WidgetWidth);
    editor->setTabStopDistance(editor->fontMetrics().boundingRect(QStringLiteral("0")).width() * TAB_WIDTH);
    editor->setAcceptRichText(false);
    setFocusProxy(editor);
    connect(editor->document(), &QTextDocument::contentsChange, this, &Editor::textChanged);
    connect(editor->document(), &QTextDocument::modificationChanged, this, &Editor::setModified);
    connect(editor, &TextEdit::cursorPositionChanged, this, &Editor::updateOnCursorPositionChange);

    // setup the line number pane
    numbers = new LineNumbers(this, editor);
    numbers->setFont(editor->document()->defaultFont());
    numbers->setWidth(1);
    connect(editor->document()->documentLayout(), SIGNAL(update(QRectF)), numbers, SLOT(update()));
    connect(editor->verticalScrollBar(), SIGNAL(valueChanged(int)), numbers, SLOT(update()));

    // let the line numbers and the editor coexist
    box = new QHBoxLayout(this);
    box->setSpacing(0);
    box->setContentsMargins(0, 0, 0, 0);
    box->addWidget(numbers);
    box->addWidget(editor);

    // calculate the bg color for the highlighted line
    QColor bgColor = this->palette().brush(this->backgroundRole()).color();
    highlightedLineBackgroundColor.setHsv(LINE_HIGHLIGHT_COLOR.hue(), bgColor.saturation() + EXTRA_SATURATION, bgColor.value());

    // our syntax highlighter (this does not do any markings)
    highlighter = new Highlighter(editor->document());

    // create a find dialog
    fdialog = new KFindDialog();
    fdialog->setSupportsRegularExpressionFind(false);
    fdialog->setHasSelection(false);
    fdialog->setHasCursor(false);

    // sets some more default values
    newFile();
    tokenizer = new Tokenizer();
}

Editor::~Editor()
{
    delete highlighter;
    delete tokenizer;
}

void Editor::enable()
{
    editor->viewport()->setEnabled(true);
    editor->setReadOnly(false);
}

void Editor::disable()
{
    editor->viewport()->setEnabled(false);
    editor->setReadOnly(true);
}

void Editor::setContent(const QString &s)
{
    editor->document()->setPlainText(s);
    editor->document()->setModified(false);
}

void Editor::openExample(const QString &example, const QString &exampleName)
{
    if (newFile()) {
        setContent(example);
        editor->document()->setModified(false);
        setCurrentUrl(QUrl::fromLocalFile(exampleName));
    }
}

void Editor::textChanged(int pos, int removed, int added)
{
    Q_UNUSED(pos);
    if (removed == 0 && added == 0)
        return; // save some cpu cycles
    removeMarkings(); // removes the character markings if there are any
    int lineCount = 1;
    for (QTextBlock block = editor->document()->begin(); block.isValid(); block = block.next())
        lineCount++;
    numbers->setWidth(qMax(1, 1 + static_cast<int>(std::floor(std::log10(static_cast<double>(lineCount) - 1)))));

    Q_EMIT contentChanged();
}

bool Editor::newFile()
{
    if (maybeSave()) {
        editor->document()->clear();
        setCurrentUrl();
        return true;
    }
    return false;
}

bool Editor::openFile(const QUrl &_url)
{
    QUrl url = _url;
    if (maybeSave()) {
        if (url.isEmpty()) {
            url = QFileDialog::getOpenFileUrl(this,
                                              i18n("Open"),
                                              QUrl(),
                                              QStringLiteral("%1 (*.turtle);;%2 (*)").arg(i18n("Turtle code files")).arg(i18n("All files")));
        }
        if (!url.isEmpty()) {
            KIO::StoredTransferJob *job = KIO::storedGet(url);
            if (job->exec()) {
                QByteArray data = job->data();
                QBuffer buffer(&data);
                if (!buffer.open(QIODevice::ReadOnly | QIODevice::Text)) {
                    return false; // can't happen
                }
                QTextStream in(&buffer);
                // check for our magic identifier
                QString s;
                s = in.readLine();
                if (s != KTURTLE_MAGIC_1_0) {
                    KMessageBox::error(
                        this,
                        i18n("The file you try to open is not a valid KTurtle script, or is incompatible with this version of KTurtle.\nCannot open %1",
                             url.toDisplayString(QUrl::PreferLocalFile)));
                    return false;
                }
                QString localizedScript = Translator::instance()->localizeScript(in.readAll());
                setContent(localizedScript);
                setCurrentUrl(url);
                editor->document()->setModified(false);
                Q_EMIT fileOpened(url);
                return true;
            } else {
                KMessageBox::error(this, job->errorString());
                return false;
            }
        }
    }
    //	statusbar "Nothing opened"
    return false;
}

bool Editor::saveFile(const QUrl &targetUrl)
{
    QUrl url(targetUrl);
    bool result = false;
    if (url.isEmpty() && currentUrl().isEmpty()) {
        result = saveFileAs();
    } else {
        if (url.isEmpty())
            url = currentUrl();
        QTemporaryFile tmp; // only used for network export
        tmp.setAutoRemove(false);
        tmp.open();
        QString filename = url.isLocalFile() ? url.toLocalFile() : tmp.fileName();

        QSaveFile *savefile = new QSaveFile(filename);
        if (savefile->open(QIODevice::WriteOnly)) {
            QTextStream outputStream(savefile);
            // store commands in their generic @(...) notation format, to be translatable when reopened
            // this allows sharing of scripts written in different languages
            Tokenizer tokenizer;
            tokenizer.initialize(editor->document()->toPlainText());
            const QStringList localizedLooks(Translator::instance()->allLocalizedLooks());
            QString untranslated;
            Token *t;
            bool pendingEOL = false; // to avoid writing a final EOL token
            while ((t = tokenizer.getToken())->type() != Token::EndOfInput) {
                if (pendingEOL) {
                    untranslated.append(QLatin1Char('\n'));
                    pendingEOL = false;
                }
                if (localizedLooks.contains(t->look())) {
                    QString defaultLook(Translator::instance()->defaultLook(t->look()));
                    untranslated.append(QStringLiteral("@(%1)").arg(defaultLook));
                } else {
                    if (t->type() == Token::EndOfLine)
                        pendingEOL = true;
                    else
                        untranslated.append(t->look());
                }
            }
            outputStream << KTURTLE_MAGIC_1_0 << '\n';
            outputStream << untranslated;
            outputStream.flush();
            if (savefile->commit()) {
                result = true;
                if (!url.isLocalFile()) {
                    KIO::StoredTransferJob *job = KIO::storedPut(savefile, url, -1, KIO::DefaultFlags);
                    if (!job->exec()) {
                        result = false;
                        KMessageBox::error(this, i18n("Could not save file."));
                    }
                }
            } else {
                // Error.
                result = false;
                KMessageBox::error(this, i18n("Could not save file."));
            }
            if (result) {
                setCurrentUrl(url);
                editor->document()->setModified(false);
                // MainWindow will add us to the recent file list
                Q_EMIT fileSaved(url);
            }
        }
        delete savefile;
    }
    return result;
}

bool Editor::saveFileAs()
{
    QUrl url = QFileDialog::getSaveFileUrl(this,
                                           i18n("Save As"),
                                           QUrl(),
                                           QStringLiteral("%1 (*.turtle);;%2 (*)").arg(i18n("Turtle code files")).arg(i18n("All files")));
    if (url.isEmpty())
        return false;
    bool result = saveFile(url);
    return result;
}

bool Editor::maybeSave()
{
    if (!editor->document()->isModified())
        return true;
    int result = KMessageBox::warningContinueCancel(this,
                                                    i18n("The program you are currently working on is not saved. "
                                                         "By continuing you may lose the changes you have made."),
                                                    i18n("Unsaved File"),
                                                    KGuiItem(i18n("&Discard Changes")),
                                                    KStandardGuiItem::cancel(),
                                                    i18n("&Discard Changes"));
    if (result == KMessageBox::Continue)
        return true;
    return false;
}

void Editor::setModified(bool b)
{
    editor->document()->setModified(b);
    Q_EMIT modificationChanged();
}

// TODO: improve find to be able to search within a selection
void Editor::find()
{
    // find selection, etc
    if (editor->textCursor().hasSelection()) {
        QString selectedText = editor->textCursor().selectedText();
        // If the selection is too big, then we don't want to automatically
        // populate the search text box with the selection text
        if (selectedText.length() < 30) {
            fdialog->setPattern(selectedText);
        }
    }
    if (fdialog->exec() == QDialog::Accepted && !fdialog->pattern().isEmpty()) {
        long kOpts = fdialog->options();
        QTextDocument::FindFlags qOpts = {};
        if (kOpts & KFind::CaseSensitive) {
            qOpts |= QTextDocument::FindCaseSensitively;
        }
        if (kOpts & KFind::FindBackwards) {
            qOpts |= QTextDocument::FindBackward;
        }
        if (kOpts & KFind::WholeWordsOnly) {
            qOpts |= QTextDocument::FindWholeWords;
        }
        editor->find(fdialog->pattern(), qOpts);
    }
}

void Editor::findNext()
{
    if (!fdialog->pattern().isEmpty()) {
        long kOpts = fdialog->options();
        QTextDocument::FindFlags qOpts = {};
        if (kOpts & KFind::CaseSensitive) {
            qOpts |= QTextDocument::FindCaseSensitively;
        }
        if (kOpts & KFind::FindBackwards) {
            qOpts |= QTextDocument::FindBackward;
        }
        if (kOpts & KFind::WholeWordsOnly) {
            qOpts |= QTextDocument::FindWholeWords;
        }
        editor->find(fdialog->pattern(), qOpts);
    }
}

void Editor::findPrev()
{
    if (!fdialog->pattern().isEmpty()) {
        long kOpts = fdialog->options();
        QTextDocument::FindFlags qOpts = {};
        if (kOpts & KFind::CaseSensitive) {
            qOpts |= QTextDocument::FindCaseSensitively;
        }
        // search in the opposite direction as findNext()
        if (!(kOpts & KFind::FindBackwards)) {
            qOpts |= QTextDocument::FindBackward;
        }
        if (kOpts & KFind::WholeWordsOnly) {
            qOpts |= QTextDocument::FindWholeWords;
        }
        editor->find(fdialog->pattern(), qOpts);
    }
}

void Editor::setCurrentUrl(const QUrl &url)
{
    m_currentUrl = url;
    Q_EMIT contentNameChanged(m_currentUrl.fileName());
}

void Editor::setOverwriteMode(bool b)
{
    editor->setOverwriteMode(b);
    editor->setCursorWidth(b ? editor->fontMetrics().boundingRect(QStringLiteral("0")).width() : 2);
}

void Editor::updateOnCursorPositionChange()
{
    // convert the absolute pos into a row/col pair, and set current line aswell
    QString s = editor->toPlainText();
    int pos = editor->textCursor().position();
    int row = 1;
    int last_break = -1;
    int next_break = 0;
    for (int i = 0; i < s.length(); i++) {
        if (s.at(i) == QLatin1Char('\n') && i < pos) {
            last_break = i;
            row++;
        } else if (s.at(i) == QLatin1Char('\n') && i >= pos) {
            next_break = i;
            break;
        }
    }
    if (next_break == 0)
        next_break = s.length();
    if (currentRow != row) {
        currentRow = row;
        highlightCurrentLine();
        editor->highlightCurrentLine();
    }
    currentCol = pos - last_break;
    currentLine = s.mid(last_break + 1, next_break - last_break - 1);
    Q_EMIT cursorPositionChanged();
}

Token *Editor::currentToken()
{
    tokenizer->initialize(currentLine);
    Token *token = tokenizer->getToken();
    while (token->type() != Token::EndOfInput) {
        if (currentCol >= token->startCol() && currentCol <= token->endCol())
            return token;
        delete token;
        token = tokenizer->getToken();
    }
    delete token;
    return nullptr;
}

void Editor::insertPlainText(const QString &txt)
{
    editor->textCursor().insertText(txt);
}

void Editor::paintEvent(QPaintEvent *event)
{
    QRect rect = editor->currentLineRect();
    rect.setWidth(this->width() - EDITOR_MARGIN); // don't draw too much
    rect.translate(0, EDITOR_MARGIN); // small hack to nicely align the line highlighting
    // QColor bgColor = this->palette().brush(this->backgroundRole()).color();
    QPainter painter(this);
    const QBrush brush(highlightedLineBackgroundColor);
    painter.fillRect(rect, brush);
    painter.end();
    QFrame::paintEvent(event);
}

QString Editor::toHtml(const QString &title, const QString &lang)
{
    Tokenizer *tokenizer = new Tokenizer();
    tokenizer->initialize(editor->document()->toPlainText());
    QString html = QString();
    QTextCharFormat *format;
    Token *token = tokenizer->getToken();
    while (token->type() != Token::EndOfInput) {
        QString escaped;
        switch (token->type()) {
        case Token::EndOfLine:
            escaped = QStringLiteral("<br />");
            break;
        case Token::WhiteSpace:
            escaped = QLatin1String("");
            for (int n = 0; n < token->look().length(); n++) {
                escaped += QLatin1String("&nbsp;");
            }
            break;
        default:
            escaped = token->look().toHtmlEscaped();
            break;
        }
        format = highlighter->tokenToFormat(token);
        if (format) {
            bool bold = format->fontWeight() > 50;
            html += QStringLiteral("<span style=\"color: %1;%2\">%3</span>")
                        .arg(format->foreground().color().name())
                        .arg(bold ? QStringLiteral(" font-weight: bold;") : QString())
                        .arg(escaped);
        } else {
            html += escaped;
        }
        token = tokenizer->getToken();
    }
    delete tokenizer;
    return QStringLiteral(
               "<html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"%1\" lang=\"%1\">"
               "<head><meta http-equiv=\"content-type\" content=\"text/html;charset=utf-8\" />"
               "<title>%2</title></head>"
               "<body style=\"font-family: monospace;\">%3</body></html>")
        .arg(lang)
        .arg(title)
        .arg(html);
}

// bool Editor::eventFilter(QObject *obj, QEvent *event)
// {
// 	if (obj != editor) return QFrame::eventFilter(obj, event);
//
// 	if (event->type() == QEvent::ToolTip) {
// 		QHelpEvent *helpEvent = static_cast<QHelpEvent *>(event);
//
// 		QTextCursor cursor = editor->cursorForPosition(helpEvent->pos());
// 		cursor.movePosition(QTextCursor::StartOfWord, QTextCursor::MoveAnchor);
// 		cursor.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor);
//
// 		QString word = cursor.selectedText();
// 		Q_EMIT mouseHover(word);
// 		Q_EMIT mouseHover(helpEvent->pos(), word);
//
// 		// QToolTip::showText(helpEvent->globalPos(), word); // For testing
// 	}
//
// 	return false;
// }

#include "moc_editor.cpp"
