/*
    SPDX-FileCopyrightText: 2007 Pino Toscano <pino@kde.org>
    SPDX-FileCopyrightText: 2018 Intevation GmbH <intevation@intevation.de>

    Work sponsored by the LiMux project of the city of Munich:
    SPDX-FileCopyrightText: 2017 Klarälvdalens Datakonsult AB a KDAB Group company <info@kdab.com>

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

#include "formwidgets.h"
#include "pageviewutils.h"
#include "revisionviewer.h"
#include "signaturepropertiesdialog.h"

#include <KLineEdit>
#include <KLocalizedString>
#include <KStandardAction>
#include <QAction>
#include <QButtonGroup>
#include <QEvent>
#include <QKeyEvent>
#include <QMenu>
#include <QPainter>
#include <QUrl>

// local includes
#include "core/action.h"
#include "core/document.h"
#include "debug_ui.h"

FormWidgetsController::FormWidgetsController(Okular::Document *doc)
    : QObject(doc)
    , m_doc(doc)
{
    // emit changed signal when a form has changed
    connect(this, &FormWidgetsController::formTextChangedByUndoRedo, this, &FormWidgetsController::changed);
    connect(this, &FormWidgetsController::formListChangedByUndoRedo, this, &FormWidgetsController::changed);
    connect(this, &FormWidgetsController::formComboChangedByUndoRedo, this, &FormWidgetsController::changed);

    // connect form modification signals to and from document
    connect(this, &FormWidgetsController::formTextChangedByWidget, doc, &Okular::Document::editFormText);
    connect(doc, &Okular::Document::formTextChangedByUndoRedo, this, &FormWidgetsController::formTextChangedByUndoRedo);

    connect(this, &FormWidgetsController::formListChangedByWidget, doc, &Okular::Document::editFormList);
    connect(doc, &Okular::Document::formListChangedByUndoRedo, this, &FormWidgetsController::formListChangedByUndoRedo);

    connect(this, &FormWidgetsController::formComboChangedByWidget, doc, &Okular::Document::editFormCombo);
    connect(doc, &Okular::Document::formComboChangedByUndoRedo, this, &FormWidgetsController::formComboChangedByUndoRedo);

    connect(this, &FormWidgetsController::formButtonsChangedByWidget, doc, &Okular::Document::editFormButtons);
    connect(doc, &Okular::Document::formButtonsChangedByUndoRedo, this, &FormWidgetsController::slotFormButtonsChangedByUndoRedo);

    // Connect undo/redo signals
    connect(this, &FormWidgetsController::requestUndo, doc, &Okular::Document::undo);
    connect(this, &FormWidgetsController::requestRedo, doc, &Okular::Document::redo);

    connect(doc, &Okular::Document::canUndoChanged, this, &FormWidgetsController::canUndoChanged);
    connect(doc, &Okular::Document::canRedoChanged, this, &FormWidgetsController::canRedoChanged);

    // Connect the generic formWidget refresh signal
    connect(doc, &Okular::Document::refreshFormWidget, this, &FormWidgetsController::refreshFormWidget);
}

FormWidgetsController::~FormWidgetsController()
{
}

void FormWidgetsController::signalAction(Okular::Action *a)
{
    emit action(a);
}

void FormWidgetsController::processScriptAction(Okular::Action *a, Okular::FormField *field, Okular::Annotation::AdditionalActionType type)
{
    // If it's not a Action Script or if the field is not a FormText, handle it normally
    if (a->actionType() != Okular::Action::Script || field->type() != Okular::FormField::FormText) {
        emit action(a);
        return;
    }
    switch (type) {
    // These cases are to be handled by the FormField text, so we let it happen.
    case Okular::Annotation::FocusIn:
    case Okular::Annotation::FocusOut:
        return;
    case Okular::Annotation::PageOpening:
    case Okular::Annotation::PageClosing:
    case Okular::Annotation::CursorEntering:
    case Okular::Annotation::CursorLeaving:
    case Okular::Annotation::MousePressed:
    case Okular::Annotation::MouseReleased:
        emit action(a);
    }
}

void FormWidgetsController::registerRadioButton(FormWidgetIface *fwButton, Okular::FormFieldButton *formButton)
{
    if (!fwButton)
        return;

    QAbstractButton *button = dynamic_cast<QAbstractButton *>(fwButton);
    if (!button) {
        qWarning() << "fwButton is not a QAbstractButton" << fwButton;
        return;
    }

    QList<RadioData>::iterator it = m_radios.begin(), itEnd = m_radios.end();
    const int id = formButton->id();
    m_buttons.insert(id, button);
    for (; it != itEnd; ++it) {
        const RadioData &rd = *it;
        const QList<int>::const_iterator idsIt = std::find(rd.ids.begin(), rd.ids.end(), id);
        if (idsIt != rd.ids.constEnd()) {
            qCDebug(OkularUiDebug) << "Adding id" << id << "To group including" << rd.ids;
            rd.group->addButton(button);
            rd.group->setId(button, id);
            return;
        }
    }

    const QList<int> siblings = formButton->siblings();

    RadioData newdata;
    newdata.ids = siblings;
    newdata.ids.append(id);
    newdata.group = new QButtonGroup();
    newdata.group->addButton(button);
    newdata.group->setId(button, id);

    // Groups of 1 (like checkboxes) can't be exclusive
    if (siblings.isEmpty())
        newdata.group->setExclusive(false);

    connect(newdata.group, QOverload<QAbstractButton *>::of(&QButtonGroup::buttonClicked), this, &FormWidgetsController::slotButtonClicked);
    m_radios.append(newdata);
}

void FormWidgetsController::dropRadioButtons()
{
    QList<RadioData>::iterator it = m_radios.begin(), itEnd = m_radios.end();
    for (; it != itEnd; ++it) {
        delete (*it).group;
    }
    m_radios.clear();
    m_buttons.clear();
}

bool FormWidgetsController::canUndo()
{
    return m_doc->canUndo();
}

bool FormWidgetsController::canRedo()
{
    return m_doc->canRedo();
}

bool FormWidgetsController::shouldFormWidgetBeShown(Okular::FormField *form)
{
    return !form->isReadOnly() || form->type() == Okular::FormField::FormSignature;
}

void FormWidgetsController::slotButtonClicked(QAbstractButton *button)
{
    int pageNumber = -1;
    CheckBoxEdit *check = qobject_cast<CheckBoxEdit *>(button);
    if (check) {
        // Checkboxes need to be uncheckable so if clicking a checked one
        // disable the exclusive status temporarily and uncheck it
        Okular::FormFieldButton *formButton = static_cast<Okular::FormFieldButton *>(check->formField());
        if (formButton->state()) {
            const bool wasExclusive = button->group()->exclusive();
            button->group()->setExclusive(false);
            check->setChecked(false);
            button->group()->setExclusive(wasExclusive);
        }
        pageNumber = check->pageItem()->pageNumber();
    } else if (RadioButtonEdit *radio = qobject_cast<RadioButtonEdit *>(button)) {
        pageNumber = radio->pageItem()->pageNumber();
    }

    const QList<QAbstractButton *> buttons = button->group()->buttons();
    QList<bool> checked;
    QList<bool> prevChecked;
    QList<Okular::FormFieldButton *> formButtons;

    for (QAbstractButton *button : buttons) {
        checked.append(button->isChecked());
        Okular::FormFieldButton *formButton = static_cast<Okular::FormFieldButton *>(dynamic_cast<FormWidgetIface *>(button)->formField());
        formButtons.append(formButton);
        prevChecked.append(formButton->state());
    }
    if (checked != prevChecked)
        emit formButtonsChangedByWidget(pageNumber, formButtons, checked);
    if (check) {
        // The formButtonsChangedByWidget signal changes the value of the underlying
        // Okular::FormField of the checkbox. We need to execute the activation
        // action after this.
        check->doActivateAction();
    }
}

void FormWidgetsController::slotFormButtonsChangedByUndoRedo(int pageNumber, const QList<Okular::FormFieldButton *> &formButtons)
{
    for (const Okular::FormFieldButton *formButton : formButtons) {
        int id = formButton->id();
        QAbstractButton *button = m_buttons[id];
        CheckBoxEdit *check = qobject_cast<CheckBoxEdit *>(button);
        if (check) {
            emit refreshFormWidget(check->formField());
        }
        // temporarily disable exclusiveness of the button group
        // since it breaks doing/redoing steps into which all the checkboxes
        // are unchecked
        const bool wasExclusive = button->group()->exclusive();
        button->group()->setExclusive(false);
        bool checked = formButton->state();
        button->setChecked(checked);
        button->group()->setExclusive(wasExclusive);
        button->setFocus();
    }
    emit changed(pageNumber);
}

FormWidgetIface *FormWidgetFactory::createWidget(Okular::FormField *ff, QWidget *parent)
{
    FormWidgetIface *widget = nullptr;

    switch (ff->type()) {
    case Okular::FormField::FormButton: {
        Okular::FormFieldButton *ffb = static_cast<Okular::FormFieldButton *>(ff);
        switch (ffb->buttonType()) {
        case Okular::FormFieldButton::Push:
            widget = new PushButtonEdit(ffb, parent);
            break;
        case Okular::FormFieldButton::CheckBox:
            widget = new CheckBoxEdit(ffb, parent);
            break;
        case Okular::FormFieldButton::Radio:
            widget = new RadioButtonEdit(ffb, parent);
            break;
        default:;
        }
        break;
    }
    case Okular::FormField::FormText: {
        Okular::FormFieldText *fft = static_cast<Okular::FormFieldText *>(ff);
        switch (fft->textType()) {
        case Okular::FormFieldText::Multiline:
            widget = new TextAreaEdit(fft, parent);
            break;
        case Okular::FormFieldText::Normal:
            widget = new FormLineEdit(fft, parent);
            break;
        case Okular::FormFieldText::FileSelect:
            widget = new FileEdit(fft, parent);
            break;
        }
        break;
    }
    case Okular::FormField::FormChoice: {
        Okular::FormFieldChoice *ffc = static_cast<Okular::FormFieldChoice *>(ff);
        switch (ffc->choiceType()) {
        case Okular::FormFieldChoice::ListBox:
            widget = new ListEdit(ffc, parent);
            break;
        case Okular::FormFieldChoice::ComboBox:
            widget = new ComboEdit(ffc, parent);
            break;
        }
        break;
    }
    case Okular::FormField::FormSignature: {
        Okular::FormFieldSignature *ffs = static_cast<Okular::FormFieldSignature *>(ff);
        if (ffs->isVisible() && ffs->signatureType() != Okular::FormFieldSignature::UnknownType)
            widget = new SignatureEdit(ffs, parent);
        break;
    }
    default:;
    }

    if (!FormWidgetsController::shouldFormWidgetBeShown(ff))
        widget->setVisibility(false);

    return widget;
}

FormWidgetIface::FormWidgetIface(QWidget *w, Okular::FormField *ff)
    : m_controller(nullptr)
    , m_ff(ff)
    , m_widget(w)
    , m_pageItem(nullptr)
{
}

FormWidgetIface::~FormWidgetIface()
{
}

Okular::NormalizedRect FormWidgetIface::rect() const
{
    return m_ff->rect();
}

void FormWidgetIface::setWidthHeight(int w, int h)
{
    m_widget->resize(w, h);
}

void FormWidgetIface::moveTo(int x, int y)
{
    m_widget->move(x, y);
}

bool FormWidgetIface::setVisibility(bool visible)
{
    bool hadfocus = m_widget->hasFocus();
    if (hadfocus)
        m_widget->clearFocus();
    m_widget->setVisible(visible);
    return hadfocus;
}

void FormWidgetIface::setCanBeFilled(bool fill)
{
    m_widget->setEnabled(fill);
}

void FormWidgetIface::setPageItem(PageViewItem *pageItem)
{
    m_pageItem = pageItem;
}

void FormWidgetIface::setFormField(Okular::FormField *field)
{
    m_ff = field;
}

Okular::FormField *FormWidgetIface::formField() const
{
    return m_ff;
}

PageViewItem *FormWidgetIface::pageItem() const
{
    return m_pageItem;
}

void FormWidgetIface::setFormWidgetsController(FormWidgetsController *controller)
{
    m_controller = controller;
    QObject *obj = dynamic_cast<QObject *>(this);
    QObject::connect(m_controller, &FormWidgetsController::refreshFormWidget, obj, [this](Okular::FormField *form) { slotRefresh(form); });
}

void FormWidgetIface::slotRefresh(Okular::FormField *form)
{
    if (m_ff != form) {
        return;
    }
    setVisibility(form->isVisible() && m_controller->shouldFormWidgetBeShown(form));

    m_widget->setEnabled(!form->isReadOnly());
}

PushButtonEdit::PushButtonEdit(Okular::FormFieldButton *button, QWidget *parent)
    : QPushButton(parent)
    , FormWidgetIface(this, button)
{
    setText(button->caption());

    if (button->caption().isEmpty()) {
        setFlat(true);
    }

    setVisible(button->isVisible());
    setCursor(Qt::ArrowCursor);
}

CheckBoxEdit::CheckBoxEdit(Okular::FormFieldButton *button, QWidget *parent)
    : QCheckBox(parent)
    , FormWidgetIface(this, button)
{
    setText(button->caption());

    setVisible(button->isVisible());
    setCursor(Qt::ArrowCursor);
}

void CheckBoxEdit::setFormWidgetsController(FormWidgetsController *controller)
{
    Okular::FormFieldButton *form = static_cast<Okular::FormFieldButton *>(m_ff);
    FormWidgetIface::setFormWidgetsController(controller);
    m_controller->registerRadioButton(this, form);
    setChecked(form->state());
}

void CheckBoxEdit::doActivateAction()
{
    Okular::FormFieldButton *form = static_cast<Okular::FormFieldButton *>(m_ff);
    if (form->activationAction())
        m_controller->signalAction(form->activationAction());
}

void CheckBoxEdit::slotRefresh(Okular::FormField *form)
{
    if (form != m_ff) {
        return;
    }
    FormWidgetIface::slotRefresh(form);

    Okular::FormFieldButton *button = static_cast<Okular::FormFieldButton *>(m_ff);
    bool oldState = isChecked();
    bool newState = button->state();
    if (oldState != newState) {
        setChecked(button->state());
        doActivateAction();
    }
}

RadioButtonEdit::RadioButtonEdit(Okular::FormFieldButton *button, QWidget *parent)
    : QRadioButton(parent)
    , FormWidgetIface(this, button)
{
    setText(button->caption());

    setVisible(button->isVisible());
    setCursor(Qt::ArrowCursor);
}

void RadioButtonEdit::setFormWidgetsController(FormWidgetsController *controller)
{
    Okular::FormFieldButton *form = static_cast<Okular::FormFieldButton *>(m_ff);
    FormWidgetIface::setFormWidgetsController(controller);
    m_controller->registerRadioButton(this, form);
    setChecked(form->state());
}

FormLineEdit::FormLineEdit(Okular::FormFieldText *text, QWidget *parent)
    : QLineEdit(parent)
    , FormWidgetIface(this, text)
{
    int maxlen = text->maximumLength();
    if (maxlen >= 0)
        setMaxLength(maxlen);
    setAlignment(text->textAlignment());
    setText(text->text());
    if (text->isPassword())
        setEchoMode(QLineEdit::Password);

    m_prevCursorPos = cursorPosition();
    m_prevAnchorPos = cursorPosition();
    m_editing = false;

    connect(this, &QLineEdit::textEdited, this, &FormLineEdit::slotChanged);
    connect(this, &QLineEdit::cursorPositionChanged, this, &FormLineEdit::slotChanged);

    setVisible(text->isVisible());
}

void FormLineEdit::setFormWidgetsController(FormWidgetsController *controller)
{
    FormWidgetIface::setFormWidgetsController(controller);
    connect(m_controller, &FormWidgetsController::formTextChangedByUndoRedo, this, &FormLineEdit::slotHandleTextChangedByUndoRedo);
}

bool FormLineEdit::event(QEvent *e)
{
    if (e->type() == QEvent::KeyPress) {
        QKeyEvent *keyEvent = static_cast<QKeyEvent *>(e);
        if (keyEvent == QKeySequence::Undo) {
            emit m_controller->requestUndo();
            return true;
        } else if (keyEvent == QKeySequence::Redo) {
            emit m_controller->requestRedo();
            return true;
        }
    } else if (e->type() == QEvent::FocusIn) {
        const auto fft = static_cast<Okular::FormFieldText *>(m_ff);
        if (text() != fft->text())
            setText(fft->text());
        m_editing = true;

        QFocusEvent *focusEvent = static_cast<QFocusEvent *>(e);
        if (focusEvent->reason() != Qt::ActiveWindowFocusReason) {
            if (const Okular::Action *action = m_ff->additionalAction(Okular::Annotation::FocusIn))
                emit m_controller->focusAction(action, fft);
        }
        setFocus();
    } else if (e->type() == QEvent::FocusOut) {
        m_editing = false;

        // Don't worry about focus events from other sources than the user FocusEvent to edit the field
        QFocusEvent *focusEvent = static_cast<QFocusEvent *>(e);
        if (focusEvent->reason() == Qt::OtherFocusReason || focusEvent->reason() == Qt::ActiveWindowFocusReason)
            return true;

        if (const Okular::Action *action = m_ff->additionalAction(Okular::Annotation::FocusOut)) {
            bool ok = false;
            emit m_controller->validateAction(action, static_cast<Okular::FormFieldText *>(m_ff), ok);
        }
        if (const Okular::Action *action = m_ff->additionalAction(Okular::FormField::FormatField)) {
            emit m_controller->formatAction(action, static_cast<Okular::FormFieldText *>(m_ff));
        }
    }
    return QLineEdit::event(e);
}

void FormLineEdit::contextMenuEvent(QContextMenuEvent *event)
{
    QMenu *menu = createStandardContextMenu();

    QList<QAction *> actionList = menu->actions();
    enum { UndoAct, RedoAct, CutAct, CopyAct, PasteAct, DeleteAct, SelectAllAct };

    QAction *kundo = KStandardAction::create(KStandardAction::Undo, m_controller, SIGNAL(requestUndo()), menu);
    QAction *kredo = KStandardAction::create(KStandardAction::Redo, m_controller, SIGNAL(requestRedo()), menu);
    connect(m_controller, &FormWidgetsController::canUndoChanged, kundo, &QAction::setEnabled);
    connect(m_controller, &FormWidgetsController::canRedoChanged, kredo, &QAction::setEnabled);
    kundo->setEnabled(m_controller->canUndo());
    kredo->setEnabled(m_controller->canRedo());

    QAction *oldUndo, *oldRedo;
    oldUndo = actionList[UndoAct];
    oldRedo = actionList[RedoAct];

    menu->insertAction(oldUndo, kundo);
    menu->insertAction(oldRedo, kredo);

    menu->removeAction(oldUndo);
    menu->removeAction(oldRedo);

    menu->exec(event->globalPos());
    delete menu;
}

void FormLineEdit::slotChanged()
{
    Okular::FormFieldText *form = static_cast<Okular::FormFieldText *>(m_ff);
    QString contents = text();
    int cursorPos = cursorPosition();

    if (form->additionalAction(Okular::FormField::FieldModified) && m_editing && !form->isReadOnly()) {
        bool ok = false;
        QString oldInputText = form->text();
        form->setText(text());
        emit m_controller->keystrokeAction(form->additionalAction(Okular::FormField::FieldModified), form, ok);
        form->setText(oldInputText);
        if (!ok) {
            setText(oldInputText);
            return;
        }
    }

    if (contents != form->text()) {
        emit m_controller->formTextChangedByWidget(pageItem()->pageNumber(), form, contents, cursorPos, m_prevCursorPos, m_prevAnchorPos);
    }

    m_prevCursorPos = cursorPos;
    m_prevAnchorPos = cursorPos;
    if (hasSelectedText()) {
        if (cursorPos == selectionStart()) {
            m_prevAnchorPos = selectionStart() + selectedText().size();
        } else {
            m_prevAnchorPos = selectionStart();
        }
    }
}

void FormLineEdit::slotHandleTextChangedByUndoRedo(int pageNumber, Okular::FormFieldText *textForm, const QString &contents, int cursorPos, int anchorPos)
{
    Q_UNUSED(pageNumber);
    if (textForm != m_ff || contents == text()) {
        return;
    }
    disconnect(this, &QLineEdit::cursorPositionChanged, this, &FormLineEdit::slotChanged);
    setText(contents);
    setCursorPosition(anchorPos);
    cursorForward(true, cursorPos - anchorPos);
    connect(this, &QLineEdit::cursorPositionChanged, this, &FormLineEdit::slotChanged);
    m_prevCursorPos = cursorPos;
    m_prevAnchorPos = anchorPos;
    setFocus();
}

void FormLineEdit::slotRefresh(Okular::FormField *form)
{
    if (form != m_ff) {
        return;
    }
    FormWidgetIface::slotRefresh(form);

    Okular::FormFieldText *text = static_cast<Okular::FormFieldText *>(form);
    setText(text->text());
}

TextAreaEdit::TextAreaEdit(Okular::FormFieldText *text, QWidget *parent)
    : KTextEdit(parent)
    , FormWidgetIface(this, text)
{
    setAcceptRichText(text->isRichText());
    setCheckSpellingEnabled(text->canBeSpellChecked());
    setAlignment(text->textAlignment());
    setPlainText(text->text());
    setUndoRedoEnabled(false);

    connect(this, &QTextEdit::textChanged, this, &TextAreaEdit::slotChanged);
    connect(this, &QTextEdit::cursorPositionChanged, this, &TextAreaEdit::slotChanged);
    connect(this, &KTextEdit::aboutToShowContextMenu, this, &TextAreaEdit::slotUpdateUndoAndRedoInContextMenu);
    m_prevCursorPos = textCursor().position();
    m_prevAnchorPos = textCursor().anchor();
    m_editing = false;
    setVisible(text->isVisible());
}

TextAreaEdit::~TextAreaEdit()
{
    // We need this because otherwise on destruction we destruct the syntax highlighter
    // that ends up calling text changed but then we go to slotChanged and we're already
    // half destructed and all is bad
    disconnect(this, &QTextEdit::textChanged, this, &TextAreaEdit::slotChanged);
}

bool TextAreaEdit::event(QEvent *e)
{
    if (e->type() == QEvent::KeyPress) {
        QKeyEvent *keyEvent = static_cast<QKeyEvent *>(e);
        if (keyEvent == QKeySequence::Undo) {
            emit m_controller->requestUndo();
            return true;
        } else if (keyEvent == QKeySequence::Redo) {
            emit m_controller->requestRedo();
            return true;
        }
    } else if (e->type() == QEvent::FocusIn) {
        const auto fft = static_cast<Okular::FormFieldText *>(m_ff);
        if (toPlainText() != fft->text())
            setText(fft->text());
        m_editing = true;
    } else if (e->type() == QEvent::FocusOut) {
        m_editing = false;
        if (const Okular::Action *action = m_ff->additionalAction(Okular::FormField::FormatField)) {
            emit m_controller->formatAction(action, static_cast<Okular::FormFieldText *>(m_ff));
        }
    }
    return KTextEdit::event(e);
}

void TextAreaEdit::slotUpdateUndoAndRedoInContextMenu(QMenu *menu)
{
    if (!menu)
        return;

    QList<QAction *> actionList = menu->actions();
    enum { UndoAct, RedoAct, CutAct, CopyAct, PasteAct, ClearAct, SelectAllAct, NCountActs };

    QAction *kundo = KStandardAction::create(KStandardAction::Undo, m_controller, SIGNAL(requestUndo()), menu);
    QAction *kredo = KStandardAction::create(KStandardAction::Redo, m_controller, SIGNAL(requestRedo()), menu);
    connect(m_controller, &FormWidgetsController::canUndoChanged, kundo, &QAction::setEnabled);
    connect(m_controller, &FormWidgetsController::canRedoChanged, kredo, &QAction::setEnabled);
    kundo->setEnabled(m_controller->canUndo());
    kredo->setEnabled(m_controller->canRedo());

    QAction *oldUndo, *oldRedo;
    oldUndo = actionList[UndoAct];
    oldRedo = actionList[RedoAct];

    menu->insertAction(oldUndo, kundo);
    menu->insertAction(oldRedo, kredo);

    menu->removeAction(oldUndo);
    menu->removeAction(oldRedo);
}

void TextAreaEdit::setFormWidgetsController(FormWidgetsController *controller)
{
    FormWidgetIface::setFormWidgetsController(controller);
    connect(m_controller, &FormWidgetsController::formTextChangedByUndoRedo, this, &TextAreaEdit::slotHandleTextChangedByUndoRedo);
}

void TextAreaEdit::slotHandleTextChangedByUndoRedo(int pageNumber, Okular::FormFieldText *textForm, const QString &contents, int cursorPos, int anchorPos)
{
    Q_UNUSED(pageNumber);
    if (textForm != m_ff) {
        return;
    }
    setPlainText(contents);
    QTextCursor c = textCursor();
    c.setPosition(anchorPos);
    c.setPosition(cursorPos, QTextCursor::KeepAnchor);
    m_prevCursorPos = cursorPos;
    m_prevAnchorPos = anchorPos;
    setTextCursor(c);
    setFocus();
}

void TextAreaEdit::slotChanged()
{
    Okular::FormFieldText *form = static_cast<Okular::FormFieldText *>(m_ff);
    QString contents = toPlainText();
    int cursorPos = textCursor().position();

    if (form->additionalAction(Okular::FormField::FieldModified) && m_editing && !form->isReadOnly()) {
        bool ok = false;
        QString oldInputText = form->text();
        form->setText(toPlainText());
        emit m_controller->keystrokeAction(form->additionalAction(Okular::FormField::FieldModified), form, ok);
        form->setText(oldInputText);
        if (!ok) {
            setText(oldInputText);
            return;
        }
    }

    if (contents != form->text()) {
        emit m_controller->formTextChangedByWidget(pageItem()->pageNumber(), form, contents, cursorPos, m_prevCursorPos, m_prevAnchorPos);
    }
    m_prevCursorPos = cursorPos;
    m_prevAnchorPos = textCursor().anchor();
}

void TextAreaEdit::slotRefresh(Okular::FormField *form)
{
    if (form != m_ff) {
        return;
    }
    FormWidgetIface::slotRefresh(form);

    Okular::FormFieldText *text = static_cast<Okular::FormFieldText *>(form);
    setPlainText(text->text());
}

FileEdit::FileEdit(Okular::FormFieldText *text, QWidget *parent)
    : KUrlRequester(parent)
    , FormWidgetIface(this, text)
{
    setMode(KFile::File | KFile::ExistingOnly | KFile::LocalOnly);
    setFilter(i18n("*|All Files"));
    setUrl(QUrl::fromUserInput(text->text()));
    lineEdit()->setAlignment(text->textAlignment());

    m_prevCursorPos = lineEdit()->cursorPosition();
    m_prevAnchorPos = lineEdit()->cursorPosition();

    connect(this, &KUrlRequester::textChanged, this, &FileEdit::slotChanged);
    connect(lineEdit(), &QLineEdit::cursorPositionChanged, this, &FileEdit::slotChanged);
    setVisible(text->isVisible());
}

void FileEdit::setFormWidgetsController(FormWidgetsController *controller)
{
    FormWidgetIface::setFormWidgetsController(controller);
    connect(m_controller, &FormWidgetsController::formTextChangedByUndoRedo, this, &FileEdit::slotHandleFileChangedByUndoRedo);
}

bool FileEdit::eventFilter(QObject *obj, QEvent *event)
{
    if (obj == lineEdit()) {
        if (event->type() == QEvent::KeyPress) {
            QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
            if (keyEvent == QKeySequence::Undo) {
                emit m_controller->requestUndo();
                return true;
            } else if (keyEvent == QKeySequence::Redo) {
                emit m_controller->requestRedo();
                return true;
            }
        } else if (event->type() == QEvent::ContextMenu) {
            QContextMenuEvent *contextMenuEvent = static_cast<QContextMenuEvent *>(event);

            QMenu *menu = ((QLineEdit *)lineEdit())->createStandardContextMenu();

            QList<QAction *> actionList = menu->actions();
            enum { UndoAct, RedoAct, CutAct, CopyAct, PasteAct, DeleteAct, SelectAllAct };

            QAction *kundo = KStandardAction::create(KStandardAction::Undo, m_controller, SIGNAL(requestUndo()), menu);
            QAction *kredo = KStandardAction::create(KStandardAction::Redo, m_controller, SIGNAL(requestRedo()), menu);
            connect(m_controller, &FormWidgetsController::canUndoChanged, kundo, &QAction::setEnabled);
            connect(m_controller, &FormWidgetsController::canRedoChanged, kredo, &QAction::setEnabled);
            kundo->setEnabled(m_controller->canUndo());
            kredo->setEnabled(m_controller->canRedo());

            QAction *oldUndo, *oldRedo;
            oldUndo = actionList[UndoAct];
            oldRedo = actionList[RedoAct];

            menu->insertAction(oldUndo, kundo);
            menu->insertAction(oldRedo, kredo);

            menu->removeAction(oldUndo);
            menu->removeAction(oldRedo);

            menu->exec(contextMenuEvent->globalPos());
            delete menu;
            return true;
        }
    }
    return KUrlRequester::eventFilter(obj, event);
}

void FileEdit::slotChanged()
{
    // Make sure line edit's text matches url expansion
    if (text() != url().toLocalFile())
        this->setText(url().toLocalFile());

    Okular::FormFieldText *form = static_cast<Okular::FormFieldText *>(m_ff);

    QString contents = text();
    int cursorPos = lineEdit()->cursorPosition();
    if (contents != form->text()) {
        emit m_controller->formTextChangedByWidget(pageItem()->pageNumber(), form, contents, cursorPos, m_prevCursorPos, m_prevAnchorPos);
    }

    m_prevCursorPos = cursorPos;
    m_prevAnchorPos = cursorPos;
    if (lineEdit()->hasSelectedText()) {
        if (cursorPos == lineEdit()->selectionStart()) {
            m_prevAnchorPos = lineEdit()->selectionStart() + lineEdit()->selectedText().size();
        } else {
            m_prevAnchorPos = lineEdit()->selectionStart();
        }
    }
}

void FileEdit::slotHandleFileChangedByUndoRedo(int pageNumber, Okular::FormFieldText *form, const QString &contents, int cursorPos, int anchorPos)
{
    Q_UNUSED(pageNumber);
    if (form != m_ff || contents == text()) {
        return;
    }
    disconnect(lineEdit(), &QLineEdit::cursorPositionChanged, this, &FileEdit::slotChanged);
    setText(contents);
    lineEdit()->setCursorPosition(anchorPos);
    lineEdit()->cursorForward(true, cursorPos - anchorPos);
    connect(lineEdit(), &QLineEdit::cursorPositionChanged, this, &FileEdit::slotChanged);
    m_prevCursorPos = cursorPos;
    m_prevAnchorPos = anchorPos;
    setFocus();
}

ListEdit::ListEdit(Okular::FormFieldChoice *choice, QWidget *parent)
    : QListWidget(parent)
    , FormWidgetIface(this, choice)
{
    addItems(choice->choices());
    setSelectionMode(choice->multiSelect() ? QAbstractItemView::ExtendedSelection : QAbstractItemView::SingleSelection);
    setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
    const QList<int> selectedItems = choice->currentChoices();
    if (choice->multiSelect()) {
        for (const int index : selectedItems)
            if (index >= 0 && index < count())
                item(index)->setSelected(true);
    } else {
        if (selectedItems.count() == 1 && selectedItems.at(0) >= 0 && selectedItems.at(0) < count()) {
            setCurrentRow(selectedItems.at(0));
            scrollToItem(item(selectedItems.at(0)));
        }
    }

    connect(this, &QListWidget::itemSelectionChanged, this, &ListEdit::slotSelectionChanged);

    setVisible(choice->isVisible());
    setCursor(Qt::ArrowCursor);
}

void ListEdit::setFormWidgetsController(FormWidgetsController *controller)
{
    FormWidgetIface::setFormWidgetsController(controller);
    connect(m_controller, &FormWidgetsController::formListChangedByUndoRedo, this, &ListEdit::slotHandleFormListChangedByUndoRedo);
}

void ListEdit::slotSelectionChanged()
{
    const QList<QListWidgetItem *> selection = selectedItems();
    QList<int> rows;
    for (const QListWidgetItem *item : selection) {
        rows.append(row(item));
    }
    Okular::FormFieldChoice *form = static_cast<Okular::FormFieldChoice *>(m_ff);
    if (rows != form->currentChoices()) {
        emit m_controller->formListChangedByWidget(pageItem()->pageNumber(), form, rows);
    }
}

void ListEdit::slotHandleFormListChangedByUndoRedo(int pageNumber, Okular::FormFieldChoice *listForm, const QList<int> &choices)
{
    Q_UNUSED(pageNumber);

    if (m_ff != listForm) {
        return;
    }

    disconnect(this, &QListWidget::itemSelectionChanged, this, &ListEdit::slotSelectionChanged);
    for (int i = 0; i < count(); i++) {
        item(i)->setSelected(choices.contains(i));
    }
    connect(this, &QListWidget::itemSelectionChanged, this, &ListEdit::slotSelectionChanged);

    setFocus();
}

ComboEdit::ComboEdit(Okular::FormFieldChoice *choice, QWidget *parent)
    : QComboBox(parent)
    , FormWidgetIface(this, choice)
{
    addItems(choice->choices());
    setEditable(true);
    setInsertPolicy(NoInsert);
    lineEdit()->setReadOnly(!choice->isEditable());
    QList<int> selectedItems = choice->currentChoices();
    if (selectedItems.count() == 1 && selectedItems.at(0) >= 0 && selectedItems.at(0) < count())
        setCurrentIndex(selectedItems.at(0));

    if (choice->isEditable() && !choice->editChoice().isEmpty())
        lineEdit()->setText(choice->editChoice());

    connect(this, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &ComboEdit::slotValueChanged);
    connect(this, &QComboBox::editTextChanged, this, &ComboEdit::slotValueChanged);
    connect(lineEdit(), &QLineEdit::cursorPositionChanged, this, &ComboEdit::slotValueChanged);

    setVisible(choice->isVisible());
    setCursor(Qt::ArrowCursor);
    m_prevCursorPos = lineEdit()->cursorPosition();
    m_prevAnchorPos = lineEdit()->cursorPosition();
}

void ComboEdit::setFormWidgetsController(FormWidgetsController *controller)
{
    FormWidgetIface::setFormWidgetsController(controller);
    connect(m_controller, &FormWidgetsController::formComboChangedByUndoRedo, this, &ComboEdit::slotHandleFormComboChangedByUndoRedo);
}

void ComboEdit::slotValueChanged()
{
    const QString text = lineEdit()->text();

    Okular::FormFieldChoice *form = static_cast<Okular::FormFieldChoice *>(m_ff);

    QString prevText;
    if (form->currentChoices().isEmpty()) {
        prevText = form->editChoice();
    } else {
        prevText = form->choices().at(form->currentChoices().constFirst());
    }

    int cursorPos = lineEdit()->cursorPosition();
    if (text != prevText) {
        emit m_controller->formComboChangedByWidget(pageItem()->pageNumber(), form, currentText(), cursorPos, m_prevCursorPos, m_prevAnchorPos);
    }
    prevText = text;
    m_prevCursorPos = cursorPos;
    m_prevAnchorPos = cursorPos;
    if (lineEdit()->hasSelectedText()) {
        if (cursorPos == lineEdit()->selectionStart()) {
            m_prevAnchorPos = lineEdit()->selectionStart() + lineEdit()->selectedText().size();
        } else {
            m_prevAnchorPos = lineEdit()->selectionStart();
        }
    }
}

void ComboEdit::slotHandleFormComboChangedByUndoRedo(int pageNumber, Okular::FormFieldChoice *form, const QString &text, int cursorPos, int anchorPos)
{
    Q_UNUSED(pageNumber);

    if (m_ff != form) {
        return;
    }

    // Determine if text corrisponds to an index choices
    int index = -1;
    for (int i = 0; i < count(); i++) {
        if (itemText(i) == text) {
            index = i;
        }
    }

    m_prevCursorPos = cursorPos;
    m_prevAnchorPos = anchorPos;

    disconnect(lineEdit(), &QLineEdit::cursorPositionChanged, this, &ComboEdit::slotValueChanged);
    const bool isCustomValue = index == -1;
    if (isCustomValue) {
        setEditText(text);
    } else {
        setCurrentIndex(index);
    }
    lineEdit()->setCursorPosition(anchorPos);
    lineEdit()->cursorForward(true, cursorPos - anchorPos);
    connect(lineEdit(), &QLineEdit::cursorPositionChanged, this, &ComboEdit::slotValueChanged);
    setFocus();
}

void ComboEdit::contextMenuEvent(QContextMenuEvent *event)
{
    QMenu *menu = lineEdit()->createStandardContextMenu();

    QList<QAction *> actionList = menu->actions();
    enum { UndoAct, RedoAct, CutAct, CopyAct, PasteAct, DeleteAct, SelectAllAct };

    QAction *kundo = KStandardAction::create(KStandardAction::Undo, m_controller, SIGNAL(requestUndo()), menu);
    QAction *kredo = KStandardAction::create(KStandardAction::Redo, m_controller, SIGNAL(requestRedo()), menu);
    connect(m_controller, &FormWidgetsController::canUndoChanged, kundo, &QAction::setEnabled);
    connect(m_controller, &FormWidgetsController::canRedoChanged, kredo, &QAction::setEnabled);
    kundo->setEnabled(m_controller->canUndo());
    kredo->setEnabled(m_controller->canRedo());

    QAction *oldUndo, *oldRedo;
    oldUndo = actionList[UndoAct];
    oldRedo = actionList[RedoAct];

    menu->insertAction(oldUndo, kundo);
    menu->insertAction(oldRedo, kredo);

    menu->removeAction(oldUndo);
    menu->removeAction(oldRedo);

    menu->exec(event->globalPos());
    delete menu;
}

bool ComboEdit::event(QEvent *e)
{
    if (e->type() == QEvent::KeyPress) {
        QKeyEvent *keyEvent = static_cast<QKeyEvent *>(e);
        if (keyEvent == QKeySequence::Undo) {
            emit m_controller->requestUndo();
            return true;
        } else if (keyEvent == QKeySequence::Redo) {
            emit m_controller->requestRedo();
            return true;
        }
    }
    return QComboBox::event(e);
}

SignatureEdit::SignatureEdit(Okular::FormFieldSignature *signature, QWidget *parent)
    : QAbstractButton(parent)
    , FormWidgetIface(this, signature)
    , m_widgetPressed(false)
    , m_dummyMode(false)
    , m_wasVisible(false)
{
    setCursor(Qt::PointingHandCursor);
    connect(this, &SignatureEdit::clicked, this, &SignatureEdit::slotViewProperties);
}

void SignatureEdit::setDummyMode(bool set)
{
    m_dummyMode = set;
    if (m_dummyMode) {
        m_wasVisible = isVisible();
        // if widget was hidden then show it.
        // even if it wasn't hidden calling this will still update the background.
        setVisibility(true);
    } else {
        // forms were not visible before this call so hide this widget.
        if (!m_wasVisible)
            setVisibility(false);
        // forms were visible even before this call so only update the background color.
        else
            update();
    }
}

bool SignatureEdit::event(QEvent *e)
{
    if (m_dummyMode && e->type() != QEvent::Paint) {
        e->accept();
        return true;
    }

    switch (e->type()) {
    case QEvent::MouseButtonPress: {
        QMouseEvent *ev = static_cast<QMouseEvent *>(e);
        if (ev->button() == Qt::LeftButton) {
            m_widgetPressed = true;
            update();
        }
        break;
    }
    case QEvent::MouseButtonRelease: {
        QMouseEvent *ev = static_cast<QMouseEvent *>(e);
        if (ev->button() == Qt::LeftButton) {
            m_widgetPressed = false;
            update();
        }
        break;
    }
    case QEvent::Leave: {
        m_widgetPressed = false;
        update();
    }
    default:
        break;
    }

    return QAbstractButton::event(e);
}

void SignatureEdit::contextMenuEvent(QContextMenuEvent *event)
{
    QMenu *menu = new QMenu(this);
    QAction *signatureProperties = new QAction(i18n("Signature Properties"), menu);
    connect(signatureProperties, &QAction::triggered, this, &SignatureEdit::slotViewProperties);
    menu->addAction(signatureProperties);
    menu->exec(event->globalPos());
    delete menu;
}

void SignatureEdit::paintEvent(QPaintEvent *)
{
    QPainter painter(this);
    // no borders when user hasn't allowed the forms to be shown
    if (m_dummyMode && !m_wasVisible) {
        painter.setPen(Qt::transparent);
    } else {
        painter.setPen(Qt::black);
    }

    if (m_widgetPressed || m_dummyMode) {
        QColor col = palette().color(QPalette::Active, QPalette::Highlight);
        col.setAlpha(50);
        painter.setBrush(col);
    } else {
        painter.setBrush(Qt::transparent);
    }
    painter.drawRect(0, 0, width() - 2, height() - 2);
}

void SignatureEdit::slotViewProperties()
{
    if (m_dummyMode)
        return;

    Okular::FormFieldSignature *formSignature = static_cast<Okular::FormFieldSignature *>(formField());
    SignaturePropertiesDialog propDlg(m_controller->m_doc, formSignature, this);
    propDlg.exec();
}

// Code for additional action handling.
// Challenge: Change preprocessor magic to C++ magic!
//
// The mouseRelease event is special because the PDF spec
// says that the activation action takes precedence over this.
// So the mouse release action is only signaled if no activation
// action exists.
//
// For checkboxes the activation action is not triggered as
// they are still triggered from the clicked signal and additionally
// when the checked state changes.

#define DEFINE_ADDITIONAL_ACTIONS(FormClass, BaseClass)                                                                                                                                                                                        \
    void FormClass::mousePressEvent(QMouseEvent *event)                                                                                                                                                                                        \
    {                                                                                                                                                                                                                                          \
        Okular::Action *act = m_ff->additionalAction(Okular::Annotation::MousePressed);                                                                                                                                                        \
        if (act) {                                                                                                                                                                                                                             \
            m_controller->signalAction(act);                                                                                                                                                                                                   \
        }                                                                                                                                                                                                                                      \
        BaseClass::mousePressEvent(event);                                                                                                                                                                                                     \
    }                                                                                                                                                                                                                                          \
    void FormClass::mouseReleaseEvent(QMouseEvent *event)                                                                                                                                                                                      \
    {                                                                                                                                                                                                                                          \
        if (!QWidget::rect().contains(event->localPos().toPoint())) {                                                                                                                                                                          \
            BaseClass::mouseReleaseEvent(event);                                                                                                                                                                                               \
            return;                                                                                                                                                                                                                            \
        }                                                                                                                                                                                                                                      \
        Okular::Action *act = m_ff->activationAction();                                                                                                                                                                                        \
        if (act && !qobject_cast<CheckBoxEdit *>(this)) {                                                                                                                                                                                      \
            m_controller->signalAction(act);                                                                                                                                                                                                   \
        } else if ((act = m_ff->additionalAction(Okular::Annotation::MouseReleased))) {                                                                                                                                                        \
            m_controller->signalAction(act);                                                                                                                                                                                                   \
        }                                                                                                                                                                                                                                      \
        BaseClass::mouseReleaseEvent(event);                                                                                                                                                                                                   \
    }                                                                                                                                                                                                                                          \
    void FormClass::focusInEvent(QFocusEvent *event)                                                                                                                                                                                           \
    {                                                                                                                                                                                                                                          \
        Okular::Action *act = m_ff->additionalAction(Okular::Annotation::FocusIn);                                                                                                                                                             \
        if (act) {                                                                                                                                                                                                                             \
            m_controller->processScriptAction(act, m_ff, Okular::Annotation::FocusIn);                                                                                                                                                         \
        }                                                                                                                                                                                                                                      \
        BaseClass::focusInEvent(event);                                                                                                                                                                                                        \
    }                                                                                                                                                                                                                                          \
    void FormClass::focusOutEvent(QFocusEvent *event)                                                                                                                                                                                          \
    {                                                                                                                                                                                                                                          \
        Okular::Action *act = m_ff->additionalAction(Okular::Annotation::FocusOut);                                                                                                                                                            \
        if (act) {                                                                                                                                                                                                                             \
            m_controller->processScriptAction(act, m_ff, Okular::Annotation::FocusOut);                                                                                                                                                        \
        }                                                                                                                                                                                                                                      \
        BaseClass::focusOutEvent(event);                                                                                                                                                                                                       \
    }                                                                                                                                                                                                                                          \
    void FormClass::leaveEvent(QEvent *event)                                                                                                                                                                                                  \
    {                                                                                                                                                                                                                                          \
        Okular::Action *act = m_ff->additionalAction(Okular::Annotation::CursorLeaving);                                                                                                                                                       \
        if (act) {                                                                                                                                                                                                                             \
            m_controller->signalAction(act);                                                                                                                                                                                                   \
        }                                                                                                                                                                                                                                      \
        BaseClass::leaveEvent(event);                                                                                                                                                                                                          \
    }                                                                                                                                                                                                                                          \
    void FormClass::enterEvent(QEvent *event)                                                                                                                                                                                                  \
    {                                                                                                                                                                                                                                          \
        Okular::Action *act = m_ff->additionalAction(Okular::Annotation::CursorEntering);                                                                                                                                                      \
        if (act) {                                                                                                                                                                                                                             \
            m_controller->signalAction(act);                                                                                                                                                                                                   \
        }                                                                                                                                                                                                                                      \
        BaseClass::enterEvent(event);                                                                                                                                                                                                          \
    }

DEFINE_ADDITIONAL_ACTIONS(PushButtonEdit, QPushButton)
DEFINE_ADDITIONAL_ACTIONS(CheckBoxEdit, QCheckBox)
DEFINE_ADDITIONAL_ACTIONS(RadioButtonEdit, QRadioButton)
DEFINE_ADDITIONAL_ACTIONS(FormLineEdit, QLineEdit)
DEFINE_ADDITIONAL_ACTIONS(TextAreaEdit, KTextEdit)
DEFINE_ADDITIONAL_ACTIONS(FileEdit, KUrlRequester)
DEFINE_ADDITIONAL_ACTIONS(ListEdit, QListWidget)
DEFINE_ADDITIONAL_ACTIONS(ComboEdit, QComboBox)
DEFINE_ADDITIONAL_ACTIONS(SignatureEdit, QAbstractButton)

#undef DEFINE_ADDITIONAL_ACTIONS
