#include "HexWidget.h"
#include "Cutter.h"
#include "Configuration.h"
#include "dialogs/WriteCommandsDialogs.h"
#include "dialogs/CommentsDialog.h"

#include <QPainter>
#include <QPaintEvent>
#include <QResizeEvent>
#include <QMouseEvent>
#include <QKeyEvent>
#include <QWheelEvent>
#include <QtEndian>
#include <QScrollBar>
#include <QMenu>
#include <QClipboard>
#include <QApplication>
#include <QInputDialog>
#include <QPushButton>
#include <QJsonObject>
#include <QJsonArray>
#include <QRegularExpression>
#include <QToolTip>
#include <QActionGroup>

static constexpr uint64_t MAX_COPY_SIZE = 128 * 1024 * 1024;
static constexpr int MAX_LINE_WIDTH_PRESET = 32;
static constexpr int MAX_LINE_WIDTH_BYTES = 128 * 1024;
static constexpr int WARNING_TIME_MS = 500;

HexWidget::HexWidget(QWidget *parent)
    : QScrollArea(parent),
      cursorEnabled(true),
      cursorOnAscii(false),
      updatingSelection(false),
      itemByteLen(1),
      itemGroupSize(1),
      rowSizeBytes(16),
      columnMode(ColumnMode::PowerOf2),
      itemFormat(ItemFormatHex),
      itemBigEndian(false),
      addrCharLen(AddrWidth64),
      showHeader(true),
      showAscii(true),
      showExHex(true),
      showExAddr(true),
      ioModesController(parent),
      warningTimer(this)
{
    setMouseTracking(true);
    setFocusPolicy(Qt::FocusPolicy::StrongFocus);
    connect(horizontalScrollBar(), &QScrollBar::valueChanged, this,
            [this]() { viewport()->update(); });

    connect(Config(), &Configuration::colorsUpdated, this, &HexWidget::updateColors);
    connect(Config(), &Configuration::fontsUpdated, this,
            [this]() { setMonospaceFont(Config()->getFont()); });

    auto sizeActionGroup = new QActionGroup(this);
    for (int i = 1; i <= 8; i *= 2) {
        QAction *action = new QAction(QString::number(i), this);
        action->setCheckable(true);
        action->setActionGroup(sizeActionGroup);
        connect(action, &QAction::triggered, this, [=]() { setItemSize(i); });
        actionsItemSize.append(action);
    }
    actionsItemSize.at(0)->setChecked(true);

    /* Follow the order in ItemFormat enum */
    QStringList names;
    names << tr("Hexadecimal");
    names << tr("Octal");
    names << tr("Decimal");
    names << tr("Signed decimal");
    names << tr("Float");

    auto formatActionGroup = new QActionGroup(this);
    for (int i = 0; i < names.length(); ++i) {
        QAction *action = new QAction(names.at(i), this);
        action->setCheckable(true);
        action->setActionGroup(formatActionGroup);
        connect(action, &QAction::triggered, this,
                [=]() { setItemFormat(static_cast<ItemFormat>(i)); });
        actionsItemFormat.append(action);
    }
    actionsItemFormat.at(0)->setChecked(true);
    actionsItemFormat.at(ItemFormatFloat)->setEnabled(false);

    rowSizeMenu = new QMenu(tr("Bytes per row"), this);
    auto columnsActionGroup = new QActionGroup(this);
    for (int i = 1; i <= MAX_LINE_WIDTH_PRESET; i *= 2) {
        QAction *action = new QAction(QString::number(i), rowSizeMenu);
        action->setCheckable(true);
        action->setActionGroup(columnsActionGroup);
        connect(action, &QAction::triggered, this, [=]() { setFixedLineSize(i); });
        rowSizeMenu->addAction(action);
    }
    rowSizeMenu->addSeparator();
    actionRowSizePowerOf2 = new QAction(tr("Power of 2"), this);
    actionRowSizePowerOf2->setCheckable(true);
    actionRowSizePowerOf2->setActionGroup(columnsActionGroup);
    connect(actionRowSizePowerOf2, &QAction::triggered, this,
            [=]() { setColumnMode(ColumnMode::PowerOf2); });
    rowSizeMenu->addAction(actionRowSizePowerOf2);

    actionItemBigEndian = new QAction(tr("Big Endian"), this);
    actionItemBigEndian->setCheckable(true);
    actionItemBigEndian->setEnabled(false);
    connect(actionItemBigEndian, &QAction::triggered, this, &HexWidget::setItemEndianness);

    actionHexPairs = new QAction(tr("Bytes as pairs"), this);
    actionHexPairs->setCheckable(true);
    connect(actionHexPairs, &QAction::triggered, this, &HexWidget::onHexPairsModeEnabled);

    actionCopy = new QAction(tr("Copy"), this);
    addAction(actionCopy);
    actionCopy->setShortcutContext(Qt::ShortcutContext::WidgetWithChildrenShortcut);
    actionCopy->setShortcut(QKeySequence::Copy);
    connect(actionCopy, &QAction::triggered, this, &HexWidget::copy);

    actionCopyAddress = new QAction(tr("Copy address"), this);
    actionCopyAddress->setShortcutContext(Qt::ShortcutContext::WidgetWithChildrenShortcut);
    actionCopyAddress->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_C);
    connect(actionCopyAddress, &QAction::triggered, this, &HexWidget::copyAddress);
    addAction(actionCopyAddress);

    // Add comment option
    actionComment = new QAction(tr("Add Comment"), this);
    actionComment->setShortcutContext(Qt::ShortcutContext::WidgetWithChildrenShortcut);
    actionComment->setShortcut(Qt::Key_Semicolon);
    connect(actionComment, &QAction::triggered, this, &HexWidget::onActionAddCommentTriggered);
    addAction(actionComment);

    // delete comment option
    actionDeleteComment = new QAction(tr("Delete Comment"), this);
    actionDeleteComment->setShortcutContext(Qt::ShortcutContext::WidgetWithChildrenShortcut);
    connect(actionDeleteComment, &QAction::triggered, this,
            &HexWidget::onActionDeleteCommentTriggered);
    addAction(actionDeleteComment);

    actionSelectRange = new QAction(tr("Select range"), this);
    connect(actionSelectRange, &QAction::triggered, this,
            [this]() { rangeDialog.open(cursor.address); });
    addAction(actionSelectRange);
    connect(&rangeDialog, &QDialog::accepted, this, &HexWidget::onRangeDialogAccepted);

    actionsWriteString.reserve(5);
    QAction *actionWriteString = new QAction(tr("Write string"), this);
    connect(actionWriteString, &QAction::triggered, this, &HexWidget::w_writeString);
    actionsWriteString.append(actionWriteString);

    QAction *actionWriteLenString = new QAction(tr("Write length and string"), this);
    connect(actionWriteLenString, &QAction::triggered, this, &HexWidget::w_writePascalString);
    actionsWriteString.append(actionWriteLenString);

    QAction *actionWriteWideString = new QAction(tr("Write wide string"), this);
    connect(actionWriteWideString, &QAction::triggered, this, &HexWidget::w_writeWideString);
    actionsWriteString.append(actionWriteWideString);

    QAction *actionWriteCString = new QAction(tr("Write zero terminated string"), this);
    connect(actionWriteCString, &QAction::triggered, this, &HexWidget::w_writeCString);
    actionsWriteString.append(actionWriteCString);

    QAction *actionWrite64 = new QAction(tr("Write a decoded or encoded Base64 string"), this);
    connect(actionWrite64, &QAction::triggered, this, &HexWidget::w_write64);
    actionsWriteString.append(actionWrite64);

    actionsWriteOther.reserve(5);
    QAction *actionWriteBytes = new QAction(tr("Write hex bytes"), this);
    connect(actionWriteBytes, &QAction::triggered, this, &HexWidget::w_writeBytes);
    actionsWriteOther.append(actionWriteBytes);

    QAction *actionWriteZeros = new QAction(tr("Write zeros"), this);
    connect(actionWriteZeros, &QAction::triggered, this, &HexWidget::w_writeZeros);
    actionsWriteOther.append(actionWriteZeros);

    QAction *actionWriteRandom = new QAction(tr("Write random bytes"), this);
    connect(actionWriteRandom, &QAction::triggered, this, &HexWidget::w_writeRandom);
    actionsWriteOther.append(actionWriteRandom);

    QAction *actionDuplicateFromOffset = new QAction(tr("Duplicate from offset"), this);
    connect(actionDuplicateFromOffset, &QAction::triggered, this, &HexWidget::w_duplFromOffset);
    actionsWriteOther.append(actionDuplicateFromOffset);

    QAction *actionIncDec = new QAction(tr("Increment/Decrement"), this);
    connect(actionIncDec, &QAction::triggered, this, &HexWidget::w_increaseDecrease);
    actionsWriteOther.append(actionIncDec);

    actionKeyboardEdit = new QAction(tr("Edit with keyboard"), this);
    actionKeyboardEdit->setCheckable(true);
    connect(actionKeyboardEdit, &QAction::triggered, this, &HexWidget::onKeyboardEditTriggered);
    connect(actionKeyboardEdit, &QAction::toggled, this, &HexWidget::onKeyboardEditChanged);

    connect(this, &HexWidget::selectionChanged, this,
            [this](Selection newSelection) { actionCopy->setEnabled(!newSelection.empty); });

    updateMetrics();
    updateItemLength();

    startAddress = 0ULL;
    cursor.address = 0ULL;
    data.reset(new MemoryData());
    oldData.reset(new MemoryData());

    fetchData();
    updateCursorMeta();

    connect(&cursor.blinkTimer, &QTimer::timeout, this, &HexWidget::onCursorBlinked);
    cursor.setBlinkPeriod(1000);
    cursor.startBlinking();

    updateColors();

    warningTimer.setSingleShot(true);
    connect(&warningTimer, &QTimer::timeout, this, &HexWidget::hideWarningRect);
}

void HexWidget::setMonospaceFont(const QFont &font)
{
    if (!(font.styleHint() & QFont::Monospace)) {
        /* FIXME: Use default monospace font
        setFont(XXX); */
    }
    QScrollArea::setFont(font);
    monospaceFont = font.resolve(this->font());
    updateMetrics();
    fetchData();
    updateCursorMeta();

    viewport()->update();
}

void HexWidget::setItemSize(int nbytes)
{
    static const QVector<int> values({ 1, 2, 4, 8 });

    if (!values.contains(nbytes))
        return;

    finishEditingWord();

    itemByteLen = nbytes;
    if (itemByteLen > rowSizeBytes) {
        rowSizeBytes = itemByteLen;
    }

    actionsItemFormat.at(ItemFormatFloat)->setEnabled(nbytes >= 4);
    actionItemBigEndian->setEnabled(nbytes != 1);

    refreshWordEditState();

    updateItemLength();
    if (!cursorOnAscii && cursor.address % itemByteLen) {
        moveCursor(-int(cursor.address % itemByteLen));
    }
    fetchData();
    updateCursorMeta();

    viewport()->update();
}

void HexWidget::setItemFormat(ItemFormat format)
{
    finishEditingWord();

    itemFormat = format;

    bool sizeEnabled = true;
    if (format == ItemFormatFloat)
        sizeEnabled = false;
    actionsItemSize.at(0)->setEnabled(sizeEnabled);
    actionsItemSize.at(1)->setEnabled(sizeEnabled);

    refreshWordEditState();

    updateItemLength();
    fetchData();
    updateCursorMeta();

    viewport()->update();
}

void HexWidget::setItemGroupSize(int size)
{
    itemGroupSize = size;

    updateCounts();
    fetchData();
    updateCursorMeta();

    viewport()->update();
}

/**
 * @brief Checks if Item at the address changed compared to the last read data.
 * @param address Address of Item to be compared.
 * @return True if Item is different, False if Item is equal or last read didn't contain the
 * address.
 * @see HexWidget#readItem
 *
 * Checks if current Item at the address changed compared to the last read data.
 * It is assumed that the current read data buffer contains the address.
 */
bool HexWidget::isItemDifferentAt(uint64_t address)
{
    char oldItem[sizeof(uint64_t)] = {};
    char newItem[sizeof(uint64_t)] = {};
    if (data->copy(newItem, address, static_cast<size_t>(itemByteLen))
        && oldData->copy(oldItem, address, static_cast<size_t>(itemByteLen))) {
        return memcmp(oldItem, newItem, sizeof(oldItem)) != 0;
    }
    return false;
}

void HexWidget::updateCounts()
{
    actionHexPairs->setEnabled(rowSizeBytes > 1 && itemByteLen == 1
                               && itemFormat == ItemFormat::ItemFormatHex);
    actionHexPairs->setChecked(Core()->getConfigb("hex.pairs"));
    if (actionHexPairs->isChecked() && actionHexPairs->isEnabled()) {
        itemGroupSize = 2;
    } else {
        itemGroupSize = 1;
    }

    if (columnMode == ColumnMode::PowerOf2) {
        int last_good_size = itemGroupByteLen();
        for (int i = itemGroupByteLen(); i <= MAX_LINE_WIDTH_BYTES; i *= 2) {
            rowSizeBytes = i;
            itemColumns = rowSizeBytes / itemGroupByteLen();
            updateAreasPosition();
            if (horizontalScrollBar()->maximum() == 0) {
                last_good_size = rowSizeBytes;
            } else {
                break;
            }
        }
        rowSizeBytes = last_good_size;
    }

    itemColumns = rowSizeBytes / itemGroupByteLen();

    // ensure correct action is selected when changing line size programmatically
    if (columnMode == ColumnMode::Fixed) {
        int w = 1;
        const auto &actions = rowSizeMenu->actions();
        for (auto action : actions) {
            action->setChecked(false);
        }
        for (auto action : actions) {
            if (w > MAX_LINE_WIDTH_PRESET) {
                break;
            }
            if (rowSizeBytes == w) {
                action->setChecked(true);
            }
            w *= 2;
        }
    } else if (columnMode == ColumnMode::PowerOf2) {
        actionRowSizePowerOf2->setChecked(true);
    }

    updateAreasPosition();
}

void HexWidget::setFixedLineSize(int lineSize)
{
    if (lineSize < 1 || lineSize < itemGroupByteLen() || lineSize % itemGroupByteLen()) {
        updateCounts();
        return;
    }
    rowSizeBytes = lineSize;
    columnMode = ColumnMode::Fixed;

    updateCounts();
    fetchData();
    updateCursorMeta();

    viewport()->update();
}

void HexWidget::setColumnMode(ColumnMode mode)
{
    columnMode = mode;

    updateCounts();
    fetchData();
    updateCursorMeta();

    viewport()->update();
}

void HexWidget::selectRange(RVA start, RVA end)
{
    BasicCursor endCursor(end);
    endCursor += 1;
    setCursorAddr(endCursor);
    selection.set(start, end);
    cursorEnabled = false;
    emit selectionChanged(getSelection());
}

void HexWidget::clearSelection()
{
    setCursorAddr(BasicCursor(cursor.address), false);
    emit selectionChanged(getSelection());
}

HexWidget::Selection HexWidget::getSelection()
{
    return Selection { selection.isEmpty(), selection.start(), selection.end() };
}

void HexWidget::seek(uint64_t address)
{
    if (!cursorOnAscii) {
        // when other widget causes seek to the middle of word
        // switch to ascii column which operates with byte positions
        auto viewOffset = startAddress % itemByteLen;
        auto addrOffset = address % itemByteLen;
        if ((addrOffset + itemByteLen - viewOffset) % itemByteLen) {
            setCursorOnAscii(true);
        }
    }
    setCursorAddr(BasicCursor(address));
}

void HexWidget::refresh()
{
    fetchData();
    viewport()->update();
}

void HexWidget::setItemEndianness(bool bigEndian)
{
    finishEditingWord();
    itemBigEndian = bigEndian;

    updateCursorMeta(); // Update cached item character

    viewport()->update();
}

void HexWidget::updateColors()
{
    borderColor = Config()->getColor("gui.border");
    backgroundColor = Config()->getColor("gui.background");
    b0x00Color = Config()->getColor("b0x00");
    b0x7fColor = Config()->getColor("b0x7f");
    b0xffColor = Config()->getColor("b0xff");
    printableColor = Config()->getColor("ai.write");
    defColor = Config()->getColor("btext");
    addrColor = Config()->getColor("func_var_addr");
    diffColor = Config()->getColor("graph.diff.unmatch");
    warningColor = QColor("red");

    updateCursorMeta();
    viewport()->update();
}

void HexWidget::paintEvent(QPaintEvent *event)
{
    QPainter painter(viewport());
    painter.setFont(monospaceFont);

    int xOffset = horizontalScrollBar()->value();
    if (xOffset > 0)
        painter.translate(QPoint(-xOffset, 0));

    if (event->rect() == cursor.screenPos.toAlignedRect()) {
        /* Cursor blink */
        drawCursor(painter);
        return;
    }

    painter.fillRect(event->rect().translated(xOffset, 0), backgroundColor);

    drawHeader(painter);

    drawAddrArea(painter);
    drawItemArea(painter);
    drawAsciiArea(painter);

    if (warningRectVisible) {
        painter.setPen(warningColor);
        painter.drawRect(warningRect);
    }

    if (!cursorEnabled)
        return;

    drawCursor(painter, true);
}

void HexWidget::updateWidth()
{
    int max = (showAscii ? asciiArea.right() : itemArea.right()) - viewport()->width();
    if (max < 0)
        max = 0;
    else
        max += charWidth;
    horizontalScrollBar()->setMaximum(max);
    horizontalScrollBar()->setSingleStep(charWidth);
}

bool HexWidget::isFixedWidth() const
{
    return itemFormat == ItemFormatHex || itemFormat == ItemFormatOct;
}

void HexWidget::resizeEvent(QResizeEvent *event)
{
    int oldByteCount = bytesPerScreen();
    updateCounts();

    if (event->oldSize().height() == event->size().height() && oldByteCount == bytesPerScreen())
        return;

    updateAreasHeight();
    fetchData(); // rowCount was changed
    updateCursorMeta();

    viewport()->update();
}

void HexWidget::mouseMoveEvent(QMouseEvent *event)
{
    QPoint pos = event->pos();
    pos.rx() += horizontalScrollBar()->value();

    auto mouseAddr = mousePosToAddr(pos).address;

    QString metaData = getFlagsAndComment(mouseAddr);
    if (!metaData.isEmpty() && itemArea.contains(pos)) {
        QToolTip::showText(mapToGlobal(event->pos()), metaData.replace(",", ", "), this);
    } else {
        QToolTip::hideText();
    }

    if (!updatingSelection) {
        if (itemArea.contains(pos) || asciiArea.contains(pos))
            setCursor(Qt::IBeamCursor);
        else
            setCursor(Qt::ArrowCursor);
        return;
    }

    auto &area = currentArea();
    if (pos.x() < area.left())
        pos.setX(area.left());
    else if (pos.x() > area.right())
        pos.setX(area.right());
    auto addr = currentAreaPosToAddr(pos, true);
    setCursorAddr(addr, true);

    /* Stop blinking */
    cursorEnabled = false;

    viewport()->update();
}

void HexWidget::mousePressEvent(QMouseEvent *event)
{
    QPoint pos(event->pos());
    pos.rx() += horizontalScrollBar()->value();

    if (event->button() == Qt::LeftButton) {
        bool selectingData = itemArea.contains(pos);
        bool selecting = selectingData || asciiArea.contains(pos);
        bool holdingShift = event->modifiers() == Qt::ShiftModifier;

        // move cursor within actively edited item
        if (selectingData && !holdingShift && editWordState >= EditWordState::WriteNotEdited) {

            auto editWordArea = itemRectangle(cursor.address - startAddress);
            if (editWordArea.contains(pos)) {
                int wordOffset = 0;
                auto cursorPosition = screenPosToAddr(pos, false, &wordOffset);
                if (cursorPosition.address == cursor.address
                    // allow selecting after last character only when cursor limited to current word
                    && (wordOffset < editWord.length()
                        || navigationMode == HexNavigationMode::WordChar)) {
                    editWordPos = std::max(0, wordOffset);
                    editWordPos = std::min<int>(editWordPos, editWord.length());

                    if (isFixedWidth()) {
                        updatingSelection = true;
                        auto selectionCursor = cursorPosition;
                        if (editWordPos > itemCharLen / 2) {
                            selectionCursor += itemByteLen;
                        }
                        selection.init(selectionCursor);
                    }

                    viewport()->update();
                    return;
                }
            }
        }

        // cursor within any item if the mode allows
        if (selectingData && !holdingShift && editWordState >= EditWordState::WriteNotStarted
            && navigationMode == HexNavigationMode::AnyChar) {
            updatingSelection = true;
            setCursorOnAscii(false);
            int wordOffset = 0;
            auto cursorPosition = screenPosToAddr(pos, false, &wordOffset);
            finishEditingWord();
            if (isFixedWidth() && wordOffset >= itemCharLen - itemPrefixLen) {
                wordOffset = 0;
                cursorPosition += itemByteLen;
            }
            setCursorAddr(cursorPosition, holdingShift);
            auto selectionPosition = currentAreaPosToAddr(pos, true);
            selection.init(selectionPosition);
            emit selectionChanged(getSelection());

            if (wordOffset > 0) {
                startEditWord();
                editWordPos = std::min<int>(wordOffset, editWord.length() - 1);
            }
            viewport()->update();
            return;
        }

        if (selecting) {
            finishEditingWord();

            updatingSelection = true;
            setCursorOnAscii(!selectingData);
            auto cursorPosition = currentAreaPosToAddr(pos, true);
            setCursorAddr(cursorPosition, holdingShift);
            viewport()->update();
        }
    }
}

void HexWidget::mouseDoubleClickEvent(QMouseEvent *event)
{
    QPoint pos(event->pos());
    pos.rx() += horizontalScrollBar()->value();

    if (event->button() == Qt::LeftButton && !isFixedWidth()
        && editWordState == EditWordState::WriteNotStarted && itemArea.contains(pos)) {
        int wordOffset = 0;
        auto cursorPosition = screenPosToAddr(pos, false, &wordOffset);
        setCursorAddr(cursorPosition, false);
        startEditWord();
        int padding = std::max<int>(0, itemCharLen - editWord.length());
        editWordPos = std::max(0, wordOffset - padding);
        editWordPos = std::min<int>(editWordPos, editWord.length());
    }
}

void HexWidget::mouseReleaseEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton) {
        if (selection.isEmpty()) {
            selection.init(BasicCursor(cursor.address));
            cursorEnabled = true;
            viewport()->update();
        }
        updatingSelection = false;
    }
}

void HexWidget::wheelEvent(QWheelEvent *event)
{
    // according to Qt doc 1 row per 5 degrees, angle measured in 1/8 of degree
    int dy = event->angleDelta().y() / (8 * 5);
    int64_t delta = -dy * itemRowByteLen();

    if (dy == 0)
        return;

    if (delta < 0 && startAddress < static_cast<uint64_t>(-delta)) {
        startAddress = 0;
    } else if (delta > 0 && data->maxIndex() < static_cast<uint64_t>(bytesPerScreen())) {
        startAddress = 0;
    } else if ((data->maxIndex() - startAddress)
               <= static_cast<uint64_t>(bytesPerScreen() + delta - 1)) {
        startAddress = (data->maxIndex() - bytesPerScreen()) + 1;
    } else {
        startAddress += delta;
    }

    fetchData();
    if (cursor.address >= startAddress && cursor.address <= lastVisibleAddr()) {
        /* Don't enable cursor blinking if selection isn't empty */
        cursorEnabled = selection.isEmpty();
        updateCursorMeta();
    } else {
        cursorEnabled = false;
    }
    viewport()->update();
}

bool HexWidget::validCharForEdit(QChar digit)
{
    switch (itemFormat) {
    case ItemFormatHex:
        return (digit >= '0' && digit <= '9') || (digit >= 'a' && digit <= 'f')
                || (digit >= 'A' && digit <= 'F');
    case ItemFormatOct: {
        if (editWordPos > 0) {
            return (digit >= '0' && digit <= '7');
        } else {
            int bitsInMSD = (itemByteLen * 8) % 3;
            int biggestDigit = (1 << bitsInMSD) - 1;
            return digit >= '0' && digit <= char('0' + biggestDigit);
        }
    }
    case ItemFormatDec:
        return (digit >= '0' && digit <= '9');
    case ItemFormatSignedDec:
        return (digit >= '0' && digit <= '9') || digit == '-';
    case ItemFormatFloat:
        return (digit >= '0' && digit <= '9') || digit == '-' || digit == '+' || digit == '.'
                || digit == ',' || digit == '+' || digit == 'e' || digit == 'E' || digit == 'i'
                || digit == 'n' || digit == 'f' || digit == 'I' || digit == 'N' || digit == 'F'
                || digit == 'a' || digit == 'A';
    }

    return false;
}

void HexWidget::movePrevEditCharAny()
{
    if (!selection.isEmpty()) {
        clearSelection();
    }
    editWordPos -= 1;
    if (editWordPos < 0) {
        finishEditingWord();
        if (moveCursor(-itemByteLen, false, OverflowMove::Ignore)) {
            startEditWord();
            editWordPos = editWord.length() - 1;
        }
    }
    viewport()->update();
}

void HexWidget::typeOverwriteModeChar(QChar c)
{
    if (editWordState < EditWordState::WriteNotEdited || !isFixedWidth()) {
        return;
    }
    editWord[editWordPos] = c;
    editWordPos++;
    editWordState = EditWordState::WriteEdited;
    if (editWordPos >= editWord.length()) {
        finishEditingWord();
        bool moved = moveCursor(itemByteLen, false, OverflowMove::Ignore);
        startEditWord();
        if (!moved) {
            editWordPos = editWord.length() - 1;
        }
    }
}

HexWidget::HexNavigationMode HexWidget::defaultNavigationMode()
{
    switch (editWordState) {
    case EditWordState::Read:
        return HexNavigationMode::Words;
    case EditWordState::WriteNotStarted:
        return isFixedWidth() ? HexNavigationMode::AnyChar : HexNavigationMode::Words;
    case EditWordState::WriteNotEdited:
    case EditWordState::WriteEdited:
        return isFixedWidth() ? HexNavigationMode::AnyChar : HexNavigationMode::WordChar;
    }
    return HexNavigationMode::Words;
}

void HexWidget::refreshWordEditState()
{
    navigationMode = defaultNavigationMode();
}

bool HexWidget::handleAsciiWrite(QKeyEvent *event)
{
    if (!cursorOnAscii || !canKeyboardEdit()) {
        return false;
    }
    if (event->key() == Qt::Key_Backspace || event->matches(QKeySequence::Backspace)) {
        if (!selection.isEmpty()) {
            writeZeros(selection.start(), selection.size());
        } else {
            moveCursor(-1, false);
            writeZeros(cursor.address, 1);
        }
        return true;
    }
    if (event->key() == Qt::Key_Delete || event->matches(QKeySequence::Delete)) {
        if (!selection.isEmpty()) {
            writeZeros(selection.start(), selection.size());
        } else {
            writeZeros(cursor.address, 1);
            moveCursor(1, false);
        }
        return true;
    }
    QString text;
    if (event->matches(QKeySequence::Paste)) {
        text = QApplication::clipboard()->text();
        if (text.length() <= 0) {
            return false;
        }
    } else {
        text = event->text();
        if (text.length() <= 0) {
            return false;
        }
        QChar c = text[0];
        if (c <= '\x1f' || c == '\x7f') {
            return false;
        }
    }

    auto bytes = text.toUtf8(); // TODO:#3028 use selected text encoding
    auto address = getLocationAddress();
    clearSelection();
    data->write(reinterpret_cast<const uint8_t *>(bytes.data()), address, bytes.length());
    seek(address + bytes.length());
    viewport()->update();
    return true;
}

bool HexWidget::handleNumberWrite(QKeyEvent *event)
{
    if (editWordState < EditWordState::WriteNotStarted) {
        return false;
    }
    bool overwrite = isFixedWidth();
    auto keyText = event->text();
    bool editingWord = editWordState >= EditWordState::WriteNotEdited;
    if (keyText.length() > 0 && validCharForEdit(keyText[0])) {
        if (!selection.isEmpty()) {
            setCursorAddr(BasicCursor(selection.start()));
        }
        if (!editingWord) {
            startEditWord();
        }
        if (overwrite) {
            typeOverwriteModeChar(keyText[0]);
        } else if (!editingWord /* && !overwrite */) {
            editWord = keyText;
            editWordPos = editWord.length();
            editWordState = EditWordState::WriteEdited;
        } else if (itemFormat == ItemFormatFloat || editWord.length() < itemCharLen) {
            editWord.insert(editWordPos, keyText);
            editWordPos += keyText.length();
            editWordState = EditWordState::WriteEdited;
        }
        maybeFlushCharEdit();
        return true;
    }
    if (event->matches(QKeySequence::Paste) && (editingWord || overwrite)) {
        QString text = QApplication::clipboard()->text();
        if (text.length() > 0) {
            if (overwrite) {
                startEditWord();
                for (QChar c : text) {
                    if (validCharForEdit(c)) {
                        typeOverwriteModeChar(c);
                    }
                }
            } else {
                editWord.insert(editWordPos, text);
                editWordPos += text.length();
                editWordState = EditWordState::WriteEdited;
            }
        }
        maybeFlushCharEdit();
        return true;
    }
    if (editingWord) {
        if (event->matches(QKeySequence::Cancel)) {
            cancelEditedWord();
            return true;
        } else if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) {
            bool needToAdvance =
                    !(editWordPos == 0 && overwrite && editWordState < EditWordState::WriteEdited);
            if (finishEditingWord(false) && needToAdvance) {
                moveCursor(itemByteLen);
            }
            return true;
        }
    }
    if (event->matches(QKeySequence::Delete)) {
        if (!selection.isEmpty()) {
            writeZeros(selection.start(), selection.size());
            clearSelection();
        } else {
            startEditWord();
            if (overwrite) {
                typeOverwriteModeChar('0');
            } else if (editWordPos < editWord.length()) {
                editWord.remove(editWordPos, 1);
                editWordState = EditWordState::WriteEdited;
            }
        }
        maybeFlushCharEdit();
        return true;
    }
    if (event->matches(QKeySequence::DeleteEndOfWord) && selection.isEmpty()) {
        startEditWord();
        if (overwrite) {
            for (int i = editWordPos; i < editWord.length(); i++) {
                typeOverwriteModeChar('0');
            }
        } else if (editWordPos < editWord.length()) {
            editWord.remove(editWordPos, editWord.length());
            editWordState = EditWordState::WriteEdited;
        }
        maybeFlushCharEdit();
        return true;
    }
    if (event->matches(QKeySequence::DeleteStartOfWord) && selection.isEmpty()) {
        if (!editingWord || (overwrite && editWordPos == 0)) {
            if (!moveCursor(-itemByteLen, false, OverflowMove::Ignore)) {
                return false;
            }
            startEditWord();
            editWordPos = editWord.length();
        }
        if (overwrite) {
            while (editWordPos > 0) {
                editWordPos--;
                editWord[editWordPos] = '0';
            }
            editWordState = EditWordState::WriteEdited;
            maybeFlushCharEdit();
            return true;
        } else {
            if (editWordPos > 0) {
                editWord.remove(0, editWordPos);
                editWordState = EditWordState::WriteEdited;
                editWordPos = 0;
                maybeFlushCharEdit();
                return true;
            }
        }
        return false;
    }

    if (event->key() == Qt::Key_Backspace) {
        if (!selection.isEmpty()) {
            writeZeros(selection.start(), selection.size());
            clearSelection();
            setCursorAddr(BasicCursor(selection.start()), false);
            return true;
        } else {
            if (!editingWord || (overwrite && editWordPos == 0)) {
                if (!moveCursor(-itemByteLen, false, OverflowMove::Ignore)) {
                    return false;
                }
                startEditWord();
                editWordPos = editWord.length();
            }
            if (editWordPos > 0) {
                editWordPos -= 1;
                if (overwrite) {
                    editWord[editWordPos] = '0';
                } else {
                    editWord.remove(editWordPos, 1);
                }
                editWordState = EditWordState::WriteEdited;
                maybeFlushCharEdit();
                return true;
            }
        }
        return false;
    }

    return false;
}

bool HexWidget::event(QEvent *event)
{
    // prefer treating keys like 's' 'g' '.' as typing input instead of global shortcuts
    if (event->type() == QEvent::ShortcutOverride) {
        auto keyEvent = static_cast<QKeyEvent *>(event);
        auto modifiers = keyEvent->modifiers();
        if ((modifiers == Qt::NoModifier || modifiers == Qt::ShiftModifier
             || modifiers == Qt::KeypadModifier)
            && keyEvent->key() < Qt::Key_Escape && canKeyboardEdit()) {
            keyEvent->accept();
            return true;
        }
    }

    return QScrollArea::event(event);
}

void HexWidget::keyPressEvent(QKeyEvent *event)
{
    bool select = false;
    auto moveOrSelect = [event, &select](QKeySequence::StandardKey moveSeq,
                                         QKeySequence::StandardKey selectSeq) -> bool {
        if (event->matches(moveSeq)) {
            select = false;
            return true;
        } else if (event->matches(selectSeq)) {
            select = true;
            return true;
        }
        return false;
    };

    if (canKeyboardEdit()) {
        if (handleAsciiWrite(event)) {
            viewport()->update();
            return;
        }
        if (editWordState >= EditWordState::WriteNotStarted && !cursorOnAscii) {
            if (handleNumberWrite(event)) {
                viewport()->update();
                return;
            }
        }
    }

    if (cursorOnAscii || navigationMode == HexNavigationMode::Words
        || navigationMode == HexNavigationMode::AnyChar) {
        if (moveOrSelect(QKeySequence::MoveToNextPage, QKeySequence::SelectNextPage)) {
            moveCursor(bytesPerScreen(), select);
        } else if (moveOrSelect(QKeySequence::MoveToPreviousPage,
                                QKeySequence::SelectPreviousPage)) {
            moveCursor(-bytesPerScreen(), select);
        } else if (moveOrSelect(QKeySequence::MoveToStartOfLine, QKeySequence::SelectStartOfLine)) {
            int linePos =
                    int((cursor.address % itemRowByteLen()) - (startAddress % itemRowByteLen()));
            moveCursor(-linePos, select);
        } else if (moveOrSelect(QKeySequence::MoveToEndOfLine, QKeySequence::SelectEndOfLine)) {
            int linePos =
                    int((cursor.address % itemRowByteLen()) - (startAddress % itemRowByteLen()));
            moveCursor(itemRowByteLen() - linePos, select);
        }
    }

    if (navigationMode == HexNavigationMode::Words || cursorOnAscii) {
        if (moveOrSelect(QKeySequence::MoveToNextLine, QKeySequence::SelectNextLine)) {
            moveCursor(itemRowByteLen(), select, OverflowMove::Ignore);
        } else if (moveOrSelect(QKeySequence::MoveToPreviousLine,
                                QKeySequence::SelectPreviousLine)) {
            moveCursor(-itemRowByteLen(), select, OverflowMove::Ignore);
        } else if (moveOrSelect(QKeySequence::MoveToNextChar, QKeySequence::SelectNextChar)
                   || moveOrSelect(QKeySequence::MoveToNextWord, QKeySequence::SelectNextWord)) {
            moveCursor(cursorOnAscii ? 1 : itemByteLen, select);
        } else if (moveOrSelect(QKeySequence::MoveToPreviousChar, QKeySequence::SelectPreviousChar)
                   || moveOrSelect(QKeySequence::MoveToPreviousWord,
                                   QKeySequence::SelectPreviousWord)) {
            moveCursor(cursorOnAscii ? -1 : -itemByteLen, select);
        }
    } else if (navigationMode == HexNavigationMode::AnyChar && !cursorOnAscii) {
        if (moveOrSelect(QKeySequence::MoveToNextLine, QKeySequence::SelectNextLine)) {
            moveCursorKeepEditOffset(itemRowByteLen(), select, OverflowMove::Ignore);
        } else if (moveOrSelect(QKeySequence::MoveToPreviousLine,
                                QKeySequence::SelectPreviousLine)) {
            moveCursorKeepEditOffset(-itemRowByteLen(), select, OverflowMove::Ignore);
        } else if (moveOrSelect(QKeySequence::MoveToNextChar, QKeySequence::SelectNextChar)) {
            if (select) {
                moveCursor(itemByteLen, select);
            } else {
                if (!selection.isEmpty()) {
                    clearSelection();
                }
                if (editWordState == EditWordState::WriteNotStarted) {
                    startEditWord();
                }
                editWordPos += 1;
                if (editWordPos >= editWord.length()) {
                    bool moved = moveCursor(itemByteLen, false, OverflowMove::Ignore);
                    startEditWord();
                    if (!moved) {
                        editWordPos = editWord.length() - 1;
                    }
                }
            }
            viewport()->update();
        } else if (event->matches(QKeySequence::MoveToPreviousChar)) {
            movePrevEditCharAny();
        } else if (event->matches(QKeySequence::SelectPreviousChar)) {
            moveCursor(-itemByteLen, true);
        } else if (moveOrSelect(QKeySequence::MoveToNextWord, QKeySequence::SelectNextWord)) {
            moveCursor(itemByteLen, select);
        } else if (event->matches(QKeySequence::MoveToPreviousWord)) {
            if (editWordPos > 0) {
                editWordPos = 0;
                viewport()->update();
            } else {
                moveCursor(-itemByteLen, false);
            }
        } else if (event->matches(QKeySequence::SelectPreviousWord)) {
            moveCursor(-itemByteLen, true);
        }
    } else if (navigationMode == HexNavigationMode::WordChar) {
        if (event->matches(QKeySequence::MoveToNextChar)) {
            editWordPos = std::min<int>(editWord.length(), editWordPos + 1);
            viewport()->update();
        } else if (event->matches(QKeySequence::MoveToPreviousChar)) {
            editWordPos = std::max(0, editWordPos - 1);
            viewport()->update();
        } else if (event->matches(QKeySequence::MoveToStartOfLine)) {
            editWordPos = 0;
            viewport()->update();
        } else if (event->matches(QKeySequence::MoveToEndOfLine)) {
            editWordPos = editWord.length();
            viewport()->update();
        } else if (event->matches(QKeySequence::MoveToPreviousWord)) {
            if (editWordPos > 0) {
                editWordPos = 0;
            } else {
                moveCursor(-itemByteLen, select);
                startEditWord();
            }
            viewport()->update();
        } else if (event->matches(QKeySequence::MoveToNextWord)) {
            if (editWordPos < editWord.length()) {
                editWordPos = editWord.length();
            } else {
                moveCursor(itemByteLen, select);
                startEditWord();
                editWordPos = editWord.length();
            }
            viewport()->update();
        }
    }
}

void HexWidget::contextMenuEvent(QContextMenuEvent *event)
{
    QPoint pt = event->pos();
    bool mouseOutsideSelection = false;
    if (event->reason() == QContextMenuEvent::Mouse) {
        auto mouseAddr = mousePosToAddr(pt).address;
        if (asciiArea.contains(pt)) {
            cursorOnAscii = true;
        } else if (itemArea.contains(pt)) {
            cursorOnAscii = false;
        }
        if (selection.isEmpty()) {
            seek(mouseAddr);
        } else {
            mouseOutsideSelection = !selection.contains(mouseAddr);
        }
    }

    auto disableOutsideSelectionActions = [this](bool disable) {
        actionCopyAddress->setDisabled(disable);
    };

    QString comment = Core()->getCommentAt(cursor.address);

    if (comment.isNull() || comment.isEmpty()) {
        actionDeleteComment->setVisible(false);
        actionComment->setText(tr("Add Comment"));
    } else {
        actionDeleteComment->setVisible(true);
        actionComment->setText(tr("Edit Comment"));
    }

    if (!ioModesController.canWrite()) {
        actionKeyboardEdit->setChecked(false);
    }

    auto *menu = new QMenu(this);
    QMenu *sizeMenu = menu->addMenu(tr("Item size:"));
    sizeMenu->addActions(actionsItemSize);
    QMenu *formatMenu = menu->addMenu(tr("Item format:"));
    formatMenu->addActions(actionsItemFormat);
    menu->addMenu(rowSizeMenu);
    menu->addAction(actionHexPairs);
    menu->addAction(actionItemBigEndian);
    QMenu *writeMenu = menu->addMenu(tr("Edit"));
    writeMenu->addActions(actionsWriteString);
    writeMenu->addSeparator();
    writeMenu->addActions(actionsWriteOther);
    menu->addAction(actionKeyboardEdit);

    menu->addSeparator();
    menu->addAction(actionCopy);
    disableOutsideSelectionActions(mouseOutsideSelection);
    menu->addAction(actionCopyAddress);
    menu->addActions(this->actions());
    menu->exec(mapToGlobal(pt));
    disableOutsideSelectionActions(false);
    menu->deleteLater();
}

void HexWidget::onCursorBlinked()
{
    if (!cursorEnabled)
        return;
    cursor.blink();
    QRect cursorRect = cursor.screenPos.toAlignedRect();
    viewport()->update(cursorRect.translated(-horizontalScrollBar()->value(), 0));
}

void HexWidget::onHexPairsModeEnabled(bool enable)
{
    // Sync configuration
    Core()->setConfig("hex.pairs", enable);
    if (enable) {
        setItemGroupSize(2);
    } else {
        setItemGroupSize(1);
    }
}

void HexWidget::copy()
{
    if (selection.isEmpty() || selection.size() > MAX_COPY_SIZE)
        return;

    auto x = cursorOnAscii
            ? Core()->getString(selection.start(), selection.size(), RZ_STRING_ENC_8BIT, true)
            : Core()->ioRead(selection.start(), (int)selection.size()).toHex();
    QApplication::clipboard()->setText(x);
}

void HexWidget::copyAddress()
{
    uint64_t addr = getLocationAddress();
    QClipboard *clipboard = QApplication::clipboard();
    clipboard->setText(RzAddressString(addr));
}

// slot for add comment action
void HexWidget::onActionAddCommentTriggered()
{
    uint64_t addr = cursor.address;
    CommentsDialog::addOrEditComment(addr, this);
}

// slot for deleting comment action
void HexWidget::onActionDeleteCommentTriggered()
{
    uint64_t addr = cursor.address;
    Core()->delComment(addr);
}

void HexWidget::onRangeDialogAccepted()
{
    if (rangeDialog.empty()) {
        seek(rangeDialog.getStartAddress());
        return;
    }
    selectRange(rangeDialog.getStartAddress(), rangeDialog.getEndAddress());
}

void HexWidget::writeZeros(uint64_t address, uint64_t length)
{
    const uint64_t MAX_BUFFER = 1024;
    std::vector<uint8_t> zeroes(std::min(MAX_BUFFER, length), 0);
    while (length > zeroes.size()) {
        data->write(zeroes.data(), address, zeroes.size());
        address += zeroes.size();
        length -= zeroes.size();
    }
    if (length > 0) {
        data->write(zeroes.data(), address, length);
    }
}

void HexWidget::w_writeString()
{
    if (!ioModesController.prepareForWriting()) {
        return;
    }
    bool ok = false;
    QString str = QInputDialog::getText(this, tr("Write string"), tr("String:"), QLineEdit::Normal,
                                        "", &ok);
    if (!ok || str.isEmpty()) {
        return;
    }
    {
        RzCoreLocked core(Core());
        rz_core_write_string_at(core, getLocationAddress(), str.toUtf8().constData());
    }
    refresh();
}

void HexWidget::w_increaseDecrease()
{
    if (!ioModesController.prepareForWriting()) {
        return;
    }
    IncrementDecrementDialog d;
    int ret = d.exec();
    if (ret == QDialog::Rejected) {
        return;
    }
    int64_t value = (int64_t)d.getValue();
    uint8_t sz = d.getNBytes();
    if (d.getMode() == IncrementDecrementDialog::Decrease) {
        value *= -1;
    }
    {
        RzCoreLocked core(Core());
        rz_core_write_value_inc_at(core, getLocationAddress(), value, sz);
    }
    refresh();
}

void HexWidget::w_writeBytes()
{
    if (!ioModesController.prepareForWriting()) {
        return;
    }
    bool ok = false;

    int size = INT_MAX;
    if (!selection.isEmpty() && selection.size() <= INT_MAX) {
        size = static_cast<int>(selection.size());
    }

    QByteArray bytes = QInputDialog::getText(this, tr("Write hex bytes"), tr("Hex byte string:"),
                                             QLineEdit::Normal, "", &ok)
                               .toUtf8();
    const int offset = bytes.startsWith("\\x") ? 2 : 0;
    const int incr = offset + 2;
    const int bytes_size = qMin(bytes.size() / incr, size);
    if (!ok || !bytes_size) {
        return;
    }
    {
        auto *buf = (uint8_t *)malloc(static_cast<size_t>(bytes_size));
        if (!buf) {
            return;
        }
        for (int i = 0, j = 0, sz = bytes.size(); i < sz; i += incr, j++) {
            buf[j] = static_cast<uint8_t>(bytes.mid(i + offset, 2).toInt(nullptr, 16));
        }
        RzCoreLocked core(Core());
        rz_core_write_at(core, getLocationAddress(), buf, bytes_size);
        free(buf);
    }
    refresh();
}

void HexWidget::w_writeZeros()
{
    if (!ioModesController.prepareForWriting()) {
        return;
    }

    int size = 1;
    if (!selection.isEmpty() && selection.size() <= INT_MAX) {
        size = static_cast<int>(selection.size());
    }

    bool ok = false;
    int len = QInputDialog::getInt(this, tr("Write zeros"), tr("Number of zeros:"), size, 1,
                                   0x7FFFFFFF, 1, &ok);
    if (!ok) {
        return;
    }
    {
        writeZeros(getLocationAddress(), len);
    }
    refresh();
}

void HexWidget::w_write64()
{
    if (!ioModesController.prepareForWriting()) {
        return;
    }
    Base64EnDecodedWriteDialog d;
    int ret = d.exec();
    if (ret == QDialog::Rejected) {
        return;
    }
    QByteArray str = d.getData();

    if (d.getMode() == Base64EnDecodedWriteDialog::Decode
        && (QString(str).contains(QRegularExpression("[^a-zA-Z0-9+/=]")) || str.length() % 4 != 0
            || str.isEmpty())) {
        QMessageBox::critical(
                this, tr("Error"),
                tr("Error occured during decoding your input.\n"
                   "Please, make sure, that it is a valid base64 string and try again."));
        return;
    }

    {
        RzCoreLocked core(Core());
        if (d.getMode() == Base64EnDecodedWriteDialog::Encode) {
            rz_core_write_base64_at(core, getLocationAddress(), str.toHex().constData());
        } else {
            rz_core_write_base64d_at(core, getLocationAddress(), str.constData());
        }
    }
    refresh();
}

void HexWidget::w_writeRandom()
{
    if (!ioModesController.prepareForWriting()) {
        return;
    }

    int size = 1;
    if (!selection.isEmpty() && selection.size() <= INT_MAX) {
        size = static_cast<int>(selection.size());
    }

    bool ok = false;
    int nbytes = QInputDialog::getInt(this, tr("Write random bytes"), tr("Number of bytes:"), size,
                                      1, 0x7FFFFFFF, 1, &ok);
    if (!ok) {
        return;
    }

    {
        RzCoreLocked core(Core());
        rz_core_write_random_at(core, getLocationAddress(), nbytes);
    }
    refresh();
}

void HexWidget::w_duplFromOffset()
{
    if (!ioModesController.prepareForWriting()) {
        return;
    }
    DuplicateFromOffsetDialog d;
    int ret = d.exec();
    if (ret == QDialog::Rejected) {
        return;
    }
    RVA src = d.getOffset();
    int len = (int)d.getNBytes();
    {
        RzCoreLocked core(Core());
        rz_core_write_duplicate_at(core, getLocationAddress(), src, len);
    }
    refresh();
}

void HexWidget::w_writePascalString()
{
    if (!ioModesController.prepareForWriting()) {
        return;
    }
    bool ok = false;
    QString str = QInputDialog::getText(this, tr("Write Pascal string"), tr("String:"),
                                        QLineEdit::Normal, "", &ok);
    if (!ok || str.isEmpty()) {
        return;
    }
    {
        RzCoreLocked core(Core());
        rz_core_write_length_string_at(core, getLocationAddress(), str.toUtf8().constData());
    }
    refresh();
}

void HexWidget::w_writeWideString()
{
    if (!ioModesController.prepareForWriting()) {
        return;
    }
    bool ok = false;
    QString str = QInputDialog::getText(this, tr("Write wide string"), tr("String:"),
                                        QLineEdit::Normal, "", &ok);
    if (!ok || str.isEmpty()) {
        return;
    }
    {
        RzCoreLocked core(Core());
        rz_core_write_string_wide_at(core, getLocationAddress(), str.toUtf8().constData());
    }
    refresh();
}

void HexWidget::w_writeCString()
{
    if (!ioModesController.prepareForWriting()) {
        return;
    }
    bool ok = false;
    QString str = QInputDialog::getText(this, tr("Write zero-terminated string"), tr("String:"),
                                        QLineEdit::Normal, "", &ok);
    if (!ok || str.isEmpty()) {
        return;
    }
    {
        RzCoreLocked core(Core());
        rz_core_write_string_zero_at(core, getLocationAddress(), str.toUtf8().constData());
    }
    refresh();
}

void HexWidget::onKeyboardEditTriggered(bool enabled)
{
    if (!enabled) {
        return;
    }
    if (!ioModesController.prepareForWriting()) {
        actionKeyboardEdit->setChecked(false);
    }
}

void HexWidget::onKeyboardEditChanged(bool enabled)
{
    if (!enabled) {
        finishEditingWord();
        navigationMode = HexNavigationMode::Words;
        editWordState = EditWordState::Read;
    } else {
        editWordState = EditWordState::WriteNotStarted;
        navigationMode = defaultNavigationMode();
    }
    updateCursorMeta();
    viewport()->update();
}

void HexWidget::updateItemLength()
{
    itemPrefixLen = 0;
    itemPrefix.clear();

    switch (itemFormat) {
    case ItemFormatHex:
        itemCharLen = 2 * itemByteLen;
        if (itemByteLen > 1 && showExHex) {
            itemPrefixLen = hexPrefix.length();
            itemPrefix = hexPrefix;
        }
        break;
    case ItemFormatOct:
        itemCharLen = (itemByteLen * 8 + 3) / 3;
        break;
    case ItemFormatDec:
        switch (itemByteLen) {
        case 1:
            itemCharLen = 3;
            break;
        case 2:
            itemCharLen = 5;
            break;
        case 4:
            itemCharLen = 10;
            break;
        case 8:
            itemCharLen = 20;
            break;
        }
        break;
    case ItemFormatSignedDec:
        switch (itemByteLen) {
        case 1:
            itemCharLen = 4;
            break;
        case 2:
            itemCharLen = 6;
            break;
        case 4:
            itemCharLen = 11;
            break;
        case 8:
            itemCharLen = 20;
            break;
        }
        break;
    case ItemFormatFloat:
        if (itemByteLen < 4)
            itemByteLen = 4;
        // FIXME
        itemCharLen = 3 * itemByteLen;
        break;
    }

    itemCharLen += itemPrefixLen;

    updateCounts();
}

void HexWidget::drawHeader(QPainter &painter)
{
    if (!showHeader)
        return;

    int offset = 0;
    QRectF rect(itemArea.left(), 0, itemWidth(), lineHeight);

    painter.setPen(addrColor);

    for (int j = 0; j < itemColumns; ++j) {
        for (int k = 0; k < itemGroupSize; ++k, offset += itemByteLen) {
            painter.drawText(rect, Qt::AlignVCenter | Qt::AlignRight,
                             QString::number(offset, 16).toUpper());
            rect.translate(itemWidth(), 0);
        }
        rect.translate(columnSpacingWidth(), 0);
    }

    rect.moveLeft(asciiArea.left());
    rect.setWidth(charWidth);
    for (int j = 0; j < itemRowByteLen(); ++j) {
        painter.drawText(rect, Qt::AlignVCenter | Qt::AlignRight,
                         QString::number(j % 16, 16).toUpper());
        rect.translate(charWidth, 0);
    }
}

void HexWidget::drawCursor(QPainter &painter, bool shadow)
{
    if (shadow) {
        QPen pen(Qt::gray);
        pen.setStyle(Qt::DashLine);
        painter.setPen(pen);
        qreal shadowWidth = charWidth;
        if (cursorOnAscii) {
            shadowWidth = itemWidth();
        } else if (editWordState >= EditWordState::WriteNotEdited) {
            shadowWidth = itemByteLen * charWidth;
        }
        shadowCursor.screenPos.setWidth(shadowWidth);
        painter.drawRect(shadowCursor.screenPos);
        painter.setPen(Qt::SolidLine);
    }

    painter.setPen(cursor.cachedColor);
    QRectF charRect(cursor.screenPos);
    charRect.setWidth(charWidth);
    painter.fillRect(charRect, backgroundColor);
    painter.drawText(charRect, Qt::AlignVCenter, cursor.cachedChar);
    if (cursor.isVisible) {
        painter.setCompositionMode(QPainter::RasterOp_SourceXorDestination);
        painter.fillRect(cursor.screenPos, QColor(0xff, 0xff, 0xff));
    }
}

void HexWidget::drawAddrArea(QPainter &painter)
{
    uint64_t offset = startAddress;
    QString addrString;
    QSizeF areaSize((addrCharLen + (showExAddr ? 2 : 0)) * charWidth, lineHeight);
    QRectF strRect(addrArea.topLeft(), areaSize);

    painter.setPen(addrColor);
    for (int line = 0; line < visibleLines && offset <= data->maxIndex();
         ++line, strRect.translate(0, lineHeight), offset += itemRowByteLen()) {
        addrString = QString("%1").arg(offset, addrCharLen, 16, QLatin1Char('0'));
        if (showExAddr)
            addrString.prepend(hexPrefix);
        painter.drawText(strRect, Qt::AlignVCenter, addrString);
    }

    painter.setPen(borderColor);

    qreal vLineOffset = itemArea.left() - charWidth;
    painter.drawLine(QLineF(vLineOffset, 0, vLineOffset, viewport()->height()));
}

void HexWidget::drawItemArea(QPainter &painter)
{
    QRectF itemRect(itemArea.topLeft(), QSizeF(itemWidth(), lineHeight));
    QColor itemColor;
    QString itemString;

    fillSelectionBackground(painter);

    bool haveEditWord = false;
    QRectF editWordRect;
    QColor editWordColor;

    uint64_t itemAddr = startAddress;
    for (int line = 0; line < visibleLines; ++line) {
        itemRect.moveLeft(itemArea.left());
        for (int j = 0; j < itemColumns; ++j) {
            for (int k = 0; k < itemGroupSize && itemAddr <= data->maxIndex();
                 ++k, itemAddr += itemByteLen) {

                itemString = renderItem(itemAddr - startAddress, &itemColor);

                if (!getFlagsAndComment(itemAddr).isEmpty()) {
                    QColor markerColor(borderColor);
                    markerColor.setAlphaF(0.5);
                    painter.setPen(markerColor);
                    for (const auto &shape : rangePolygons(itemAddr, itemAddr, false)) {
                        painter.drawPolyline(shape);
                    }
                }
                if (selection.contains(itemAddr) && !cursorOnAscii) {
                    itemColor = palette().highlightedText().color();
                }
                if (isItemDifferentAt(itemAddr)) {
                    itemColor.setRgb(diffColor.rgb());
                }

                if (editWordState <= EditWordState::WriteNotStarted || cursor.address != itemAddr) {
                    painter.setPen(itemColor);
                    painter.drawText(itemRect, Qt::AlignVCenter, itemString);
                    itemRect.translate(itemWidth(), 0);
                    if (cursor.address == itemAddr) {
                        auto &itemCursor = cursorOnAscii ? shadowCursor : cursor;
                        int itemCharPos = 0;
                        if (editWordState > EditWordState::Read) {
                            itemCharPos += itemPrefixLen;
                        }
                        if (itemCharPos < itemString.length()) {
                            itemCursor.cachedChar = itemString.at(itemCharPos);
                        } else {
                            itemCursor.cachedChar = ' ';
                        }
                        itemCursor.cachedColor = itemColor;
                    }
                } else {
                    haveEditWord = true;
                    editWordRect = itemRect;
                    editWordColor = itemColor;

                    auto &itemCursor = cursor;
                    itemCursor.cachedChar =
                            editWordPos < editWord.length() ? editWord[editWordPos] : QChar(' ');
                    itemCursor.cachedColor = itemColor;
                    itemCursor.screenPos.moveTopLeft(itemRect.topLeft());
                    itemCursor.screenPos.translate(charWidth * (editWordPos + itemPrefixLen), 0);

                    itemRect.translate(itemWidth(), 0);
                }
            }
            itemRect.translate(columnSpacingWidth(), 0);
        }
        itemRect.translate(0, lineHeight);
    }
    if (haveEditWord) {
        auto length = std::max<int>(itemCharLen, editWord.length());
        auto rect = editWordRect;
        rect.setWidth(length * charWidth);
        painter.fillRect(rect, backgroundColor);

        painter.setPen(editWordColor);
        editWordRect.setWidth(4000);
        painter.drawText(editWordRect, Qt::AlignVCenter | Qt::AlignLeft, itemPrefix + editWord);
    }

    painter.setPen(borderColor);

    qreal vLineOffset = asciiArea.left() - charWidth;
    painter.drawLine(QLineF(vLineOffset, 0, vLineOffset, viewport()->height()));
}

void HexWidget::drawAsciiArea(QPainter &painter)
{
    QRectF charRect(asciiArea.topLeft(), QSizeF(charWidth, lineHeight));

    fillSelectionBackground(painter, true);

    uint64_t address = startAddress;
    QChar ascii;
    QColor color;
    for (int line = 0; line < visibleLines; ++line, charRect.translate(0, lineHeight)) {
        charRect.moveLeft(asciiArea.left());
        for (int j = 0; j < itemRowByteLen() && address <= data->maxIndex(); ++j, ++address) {
            ascii = renderAscii(address - startAddress, &color);
            if (selection.contains(address) && cursorOnAscii) {
                color = palette().highlightedText().color();
            }
            if (isItemDifferentAt(address)) {
                color.setRgb(diffColor.rgb());
            }
            painter.setPen(color);
            /* Dots look ugly. Use fillRect() instead of drawText(). */
            if (ascii == '.') {
                qreal a = cursor.screenPos.width();
                QPointF p = charRect.bottomLeft();
                p.rx() += (charWidth - a) / 2 + 1;
                p.ry() += -2 * a;
                painter.fillRect(QRectF(p, QSizeF(a, a)), color);
            } else {
                painter.drawText(charRect, Qt::AlignVCenter, ascii);
            }
            charRect.translate(charWidth, 0);
            if (cursor.address == address) {
                auto &itemCursor = cursorOnAscii ? cursor : shadowCursor;
                itemCursor.cachedChar = ascii;
                itemCursor.cachedColor = color;
            }
        }
    }
}

void HexWidget::fillSelectionBackground(QPainter &painter, bool ascii)
{
    if (selection.isEmpty()) {
        return;
    }
    const auto parts = rangePolygons(selection.start(), selection.end(), ascii);
    for (const auto &shape : parts) {
        QColor highlightColor = palette().color(QPalette::Highlight);
        if (ascii == cursorOnAscii) {
            painter.setBrush(highlightColor);
            painter.drawPolygon(shape);
        } else {
            painter.setPen(highlightColor);
            painter.drawPolyline(shape);
        }
    }
}

QVector<QPolygonF> HexWidget::rangePolygons(RVA start, RVA last, bool ascii)
{
    if (last < startAddress || start > lastVisibleAddr()) {
        return {};
    }

    QRectF rect;
    const QRectF area = QRectF(ascii ? asciiArea : itemArea);

    /* Convert absolute values to relative */
    int startOffset = std::max(uint64_t(start), startAddress) - startAddress;
    int endOffset = std::min(uint64_t(last), lastVisibleAddr()) - startAddress;

    QVector<QPolygonF> parts;

    auto getRectangle = [&](int offset) {
        return QRectF(ascii ? asciiRectangle(offset) : itemRectangle(offset));
    };

    auto startRect = getRectangle(startOffset);
    auto endRect = getRectangle(endOffset);
    bool startJagged = false;
    bool endJagged = false;
    if (!ascii) {
        if (int startFraction = startOffset % itemByteLen) {
            startRect.setLeft(startRect.left() + startFraction * startRect.width() / itemByteLen);
            startJagged = true;
        }
        if (int endFraction = itemByteLen - 1 - (endOffset % itemByteLen)) {
            endRect.setRight(endRect.right() - endFraction * endRect.width() / itemByteLen);
            endJagged = true;
        }
    }
    if (endOffset - startOffset + 1 <= rowSizeBytes) {
        if (startOffset / rowSizeBytes == endOffset / rowSizeBytes) { // single row
            rect = startRect;
            rect.setRight(endRect.right());
            parts.push_back(QPolygonF(rect));
        } else {
            // two separate rectangles
            rect = startRect;
            rect.setRight(area.right());
            parts.push_back(QPolygonF(rect));
            rect = endRect;
            rect.setLeft(area.left());
            parts.push_back(QPolygonF(rect));
        }
    } else {
        // single multiline shape
        QPolygonF shape;
        shape << startRect.topLeft();
        rect = getRectangle(startOffset + rowSizeBytes - 1 - startOffset % rowSizeBytes);
        shape << rect.topRight();
        if (endOffset % rowSizeBytes != rowSizeBytes - 1) {
            rect = getRectangle(endOffset - endOffset % rowSizeBytes - 1);
            shape << rect.bottomRight() << endRect.topRight();
        }
        shape << endRect.bottomRight();
        shape << getRectangle(endOffset - endOffset % rowSizeBytes).bottomLeft();
        if (startOffset % rowSizeBytes) {
            rect = getRectangle(startOffset - startOffset % rowSizeBytes + rowSizeBytes);
            shape << rect.topLeft() << startRect.bottomLeft();
        }
        shape << shape.first(); // close the shape
        parts.push_back(shape);
    }
    if (!ascii && (startJagged || endJagged) && parts.length() >= 1) {

        QPolygonF top;
        top.reserve(3);
        top << QPointF(0, 0) << QPointF(charWidth, lineHeight / 3) << QPointF(0, lineHeight / 2);
        QPolygonF bottom;
        bottom.reserve(3);
        bottom << QPointF(0, lineHeight / 2) << QPointF(-charWidth, 2 * lineHeight / 3)
               << QPointF(0, lineHeight);

        // small adjustment to make sure that edges don't overlap with rect edges, QPolygonF doesn't
        // handle it properly
        QPointF adjustment(charWidth / 16, 0);
        top.translate(-adjustment);
        bottom.translate(adjustment);

        if (startJagged) {
            auto movedTop = top.translated(startRect.topLeft());
            auto movedBottom = bottom.translated(startRect.topLeft());
            parts[0] = parts[0].subtracted(movedTop).united(movedBottom);
        }
        if (endJagged) {
            auto movedTop = top.translated(endRect.topRight());
            auto movedBottom = bottom.translated(endRect.topRight());
            parts.last() = parts.last().subtracted(movedBottom).united(movedTop);
        }
    }
    return parts;
}

void HexWidget::updateMetrics()
{
    QFontMetricsF fontMetrics(this->monospaceFont);
    lineHeight = fontMetrics.height();
#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0)
    charWidth = fontMetrics.width('A');
#else
    charWidth = fontMetrics.horizontalAdvance('A');
#endif

    updateCounts();
    updateAreasHeight();

    qreal cursorWidth = std::max(charWidth / 3, 1.);
    cursor.screenPos.setHeight(lineHeight);
    shadowCursor.screenPos.setHeight(lineHeight);

    cursor.screenPos.setWidth(cursorWidth);
    if (cursorOnAscii) {
        cursor.screenPos.moveTopLeft(asciiArea.topLeft());

        shadowCursor.screenPos.setWidth(itemWidth());
        shadowCursor.screenPos.moveTopLeft(itemArea.topLeft());
    } else {
        cursor.screenPos.moveTopLeft(itemArea.topLeft());
        shadowCursor.screenPos.setWidth(charWidth);
        shadowCursor.screenPos.moveTopLeft(asciiArea.topLeft());
    }
}

void HexWidget::updateAreasPosition()
{
    const qreal spacingWidth = areaSpacingWidth();

    qreal yOffset = showHeader ? lineHeight : 0;

    addrArea.setTopLeft(QPointF(0, yOffset));
    addrArea.setWidth((addrCharLen + (showExAddr ? 2 : 0)) * charWidth);

    itemArea.setTopLeft(QPointF(addrArea.right() + spacingWidth, yOffset));
    itemArea.setWidth(itemRowWidth());

    asciiArea.setTopLeft(QPointF(itemArea.right() + spacingWidth, yOffset));
    asciiArea.setWidth(asciiRowWidth());

    updateWidth();
}

void HexWidget::updateAreasHeight()
{
    visibleLines = static_cast<int>((viewport()->height() - itemArea.top()) / lineHeight);

    qreal height = visibleLines * lineHeight;
    addrArea.setHeight(height);
    itemArea.setHeight(height);
    asciiArea.setHeight(height);
}

bool HexWidget::moveCursor(int offset, bool select, OverflowMove overflowMove)
{
    BasicCursor addr(cursor.address);
    if (overflowMove == OverflowMove::Ignore) {
        if (addr.moveChecked(offset)) {
            if (addr.address > data->maxIndex()) {
                addr.address = data->maxIndex();
                addr.pastEnd = true;
            }
            setCursorAddr(addr, select);
            return true;
        }
        return false;
    } else {
        addr += offset;
        if (addr.address > data->maxIndex()) {
            addr.address = data->maxIndex();
        }
        setCursorAddr(addr, select);
        return true;
    }
}

void HexWidget::moveCursorKeepEditOffset(int byteOffset, bool select, OverflowMove overflowMove)
{
    int wordOffset = editWordPos;
    moveCursor(byteOffset, select, overflowMove);
    // preserve position within word when moving vertically in hex or oct modes
    if (!cursorOnAscii && !select && wordOffset > 0 && navigationMode == HexNavigationMode::AnyChar
        && editWordState > EditWordState::Read) {
        startEditWord();
        editWordPos = wordOffset;
    }
}

void HexWidget::setCursorAddr(BasicCursor addr, bool select)
{
    finishEditingWord();
    if (!select) {
        bool clearingSelection = !selection.isEmpty();
        selection.init(addr);
        if (clearingSelection)
            emit selectionChanged(getSelection());
    }
    emit positionChanged(addr.address);

    cursor.address = addr.address;
    if (!cursorOnAscii) {
        cursor.address -= cursor.address % itemByteLen;
    }

    /* Pause cursor repainting */
    cursorEnabled = false;

    if (select) {
        selection.update(addr);
        emit selectionChanged(getSelection());
    }

    uint64_t addressValue = cursor.address;
    /* Update data cache if necessary */
    if (!(addressValue >= startAddress && addressValue <= lastVisibleAddr())) {
        /* Align start address */
        addressValue -= (addressValue % itemRowByteLen());

        /* FIXME: handling Page Up/Down */
        uint64_t rowAfterVisibleAddress = startAddress + bytesPerScreen();
        if (addressValue == rowAfterVisibleAddress && addressValue > startAddress) {
            // when pressing down add only one new row
            startAddress += itemRowByteLen();
        } else {
            startAddress = addressValue;
        }

        fetchData();

        if (startAddress > (data->maxIndex() - bytesPerScreen()) + 1) {
            startAddress = (data->maxIndex() - bytesPerScreen()) + 1;
        }
    }

    updateCursorMeta();

    /* Draw cursor */
    cursor.isVisible = !select;
    viewport()->update();

    /* Resume cursor repainting */
    cursorEnabled = selection.isEmpty();
}

void HexWidget::updateCursorMeta()
{
    QPointF point;
    QPointF pointAscii;

    int offset = cursor.address - startAddress;
    int itemOffset = offset;
    int asciiOffset;

    /* Calc common Y coordinate */
    point.ry() = (itemOffset / itemRowByteLen()) * lineHeight;
    pointAscii.setY(point.y());
    itemOffset %= itemRowByteLen();
    asciiOffset = itemOffset;

    /* Calc X coordinate on the item area */
    point.rx() = (itemOffset / itemGroupByteLen()) * columnExWidth();
    itemOffset %= itemGroupByteLen();
    point.rx() += (itemOffset / itemByteLen) * itemWidth();

    /* Calc X coordinate on the ascii area */
    pointAscii.rx() = asciiOffset * charWidth;

    point += itemArea.topLeft();
    pointAscii += asciiArea.topLeft();

    if (editWordState > EditWordState::Read && !cursorOnAscii) {
        point.rx() += itemPrefixLen * charWidth;
    }

    cursor.screenPos.moveTopLeft(cursorOnAscii ? pointAscii : point);
    shadowCursor.screenPos.moveTopLeft(cursorOnAscii ? point : pointAscii);
}

void HexWidget::setCursorOnAscii(bool ascii)
{
    cursorOnAscii = ascii;
}

QColor HexWidget::itemColor(uint8_t byte)
{
    QColor color(defColor);

    if (byte == 0x00)
        color = b0x00Color;
    else if (byte == 0x7f)
        color = b0x7fColor;
    else if (byte == 0xff)
        color = b0xffColor;
    else if (IS_PRINTABLE(byte)) {
        color = printableColor;
    }

    return color;
}

template<class T>
static T fromBigEndian(const void *src)
{
#if QT_VERSION >= QT_VERSION_CHECK(5, 12, 0)
    return qFromBigEndian<T>(src);
#else
    T result;
    memcpy(&result, src, sizeof(T));
    return qFromBigEndian<T>(result);
#endif
}

template<class T>
static T fromLittleEndian(const void *src)
{
#if QT_VERSION >= QT_VERSION_CHECK(5, 12, 0)
    return qFromLittleEndian<T>(src);
#else
    T result;
    memcpy(&result, src, sizeof(T));
    return qFromLittleEndian<T>(result);
#endif
}

QVariant HexWidget::readItem(int offset, QColor *color)
{
    quint8 byte;
    quint16 word;
    quint32 dword;
    quint64 qword;
    float float32;
    double float64;

    quint8 bytes[sizeof(uint64_t)];
    data->copy(bytes, startAddress + offset, static_cast<size_t>(itemByteLen));
    const bool signedItem = itemFormat == ItemFormatSignedDec;

    if (color) {
        *color = defColor;
    }

    switch (itemByteLen) {
    case 1:
        byte = bytes[0];
        if (color)
            *color = itemColor(byte);
        if (!signedItem)
            return QVariant(static_cast<quint64>(byte));
        return QVariant(static_cast<qint64>(static_cast<qint8>(byte)));
    case 2:
        if (itemBigEndian)
            word = fromBigEndian<quint16>(bytes);
        else
            word = fromLittleEndian<quint16>(bytes);

        if (!signedItem)
            return QVariant(static_cast<quint64>(word));
        return QVariant(static_cast<qint64>(static_cast<qint16>(word)));
    case 4:
        if (itemBigEndian)
            dword = fromBigEndian<quint32>(bytes);
        else
            dword = fromLittleEndian<quint32>(bytes);

        if (itemFormat == ItemFormatFloat) {
            memcpy(&float32, &dword, sizeof(float32));
            return QVariant(float32);
        }
        if (!signedItem)
            return QVariant(static_cast<quint64>(dword));
        return QVariant(static_cast<qint64>(static_cast<qint32>(dword)));
    case 8:
        if (itemBigEndian)
            qword = fromBigEndian<quint64>(bytes);
        else
            qword = fromLittleEndian<quint64>(bytes);
        if (itemFormat == ItemFormatFloat) {
            memcpy(&float64, &qword, sizeof(float64));
            return QVariant(float64);
        }
        if (!signedItem)
            return QVariant(qword);
        return QVariant(static_cast<qint64>(qword));
    }

    return QVariant();
}

QString HexWidget::renderItem(int offset, QColor *color)
{
    QString item;
    QVariant itemVal = readItem(offset, color);
    int itemLen = itemCharLen - itemPrefixLen; /* Reserve space for prefix */

    // FIXME: handle broken itemVal ( QVariant() )
    switch (itemFormat) {
    case ItemFormatHex:
        item = QString("%1").arg(itemVal.toULongLong(), itemLen, 16, QLatin1Char('0'));
        if (itemByteLen > 1 && showExHex)
            item.prepend(hexPrefix);
        break;
    case ItemFormatOct:
        item = QString("%1").arg(itemVal.toULongLong(), itemLen, 8, QLatin1Char('0'));
        break;
    case ItemFormatDec:
        item = QString("%1").arg(itemVal.toULongLong(), itemLen, 10);
        break;
    case ItemFormatSignedDec:
        item = QString("%1").arg(itemVal.toLongLong(), itemLen, 10);
        break;
    case ItemFormatFloat:
        item = QString("%1").arg(itemVal.toDouble(), itemLen, 'g', itemByteLen == 4 ? 6 : 15);
        break;
    }

    return item;
}

QChar HexWidget::renderAscii(int offset, QColor *color)
{
    uchar byte;
    data->copy(&byte, startAddress + offset, sizeof(byte));
    if (color) {
        *color = itemColor(byte);
    }
    if (!IS_PRINTABLE(byte)) {
        byte = '.';
    }
    return QChar(byte);
}

/**
 * @brief Gets the available flags and comment at a specific address.
 * @param address Address of Item to be checked.
 * @return String containing the flags and comment available at the address.
 */
QString HexWidget::getFlagsAndComment(uint64_t address)
{
    QString flagNames = Core()->listFlagsAsStringAt(address);
    QString metaData = flagNames.isEmpty() ? "" : "Flags: " + flagNames.trimmed();

    QString comment = Core()->getCommentAt(address);
    if (!comment.isEmpty()) {
        if (!metaData.isEmpty()) {
            metaData.append("\n");
        }
        metaData.append("Comment: " + comment.trimmed());
    }

    return metaData;
}

bool HexWidget::canKeyboardEdit()
{
    return ioModesController.canWrite() && actionKeyboardEdit->isChecked();
}

template<class T, class BigValue>
static bool checkRange(BigValue v)
{
    return v >= std::numeric_limits<T>::min() && v <= std::numeric_limits<T>::max();
}

template<class T, class BigInteger>
static bool checkAndWrite(BigInteger value, uint8_t *buf, bool littleEndian)
{
    if (!checkRange<T>(value)) {
        return false;
    }
    if (littleEndian) {
        qToLittleEndian((T)value, buf);
    } else {
        qToBigEndian((T)value, buf);
    }
    return true;
}

template<class UType, class SType>
static bool checkAndWriteWithSign(const QVariant &value, uint8_t *buf, bool isSigned,
                                  bool littleEndian)
{
    if (isSigned) {
        return checkAndWrite<SType>(value.toLongLong(), buf, littleEndian);
    } else {
        return checkAndWrite<UType>(value.toULongLong(), buf, littleEndian);
    }
}

bool HexWidget::parseWord(QString word, uint8_t *buf, size_t bufferSize) const
{
    bool parseOk = false;
    if (bufferSize < size_t(itemByteLen)) {
        return false;
    }
    if (itemFormat == ItemFormatFloat) {
        if (itemByteLen == 4) {
            float value = word.toFloat(&parseOk);
            if (!parseOk) {
                return false;
            }
            if (itemBigEndian) {
                rz_write_be_float(buf, value);
            } else {
                rz_write_le_float(buf, value);
            }
            return true;
        } else if (itemByteLen == 8) {
            double value = word.toDouble(&parseOk);
            if (!parseOk) {
                return false;
            }
            if (itemBigEndian) {
                rz_write_be_double(buf, value);
            } else {
                rz_write_le_double(buf, value);
            }
            return true;
        }
        return false;
    } else {
        QVariant value;
        bool isSigned = false;
        switch (itemFormat) {
        case ItemFormatHex:
            value = word.toULongLong(&parseOk, 16);
            break;
        case ItemFormatOct:
            value = word.toULongLong(&parseOk, 8);
            break;
        case ItemFormatDec:
            value = word.toULongLong(&parseOk, 10);
            break;
        case ItemFormatSignedDec:
            isSigned = true;
            value = word.toLongLong(&parseOk, 10);
            break;
        default:
            break;
        }
        if (!parseOk) {
            return false;
        }

        switch (itemByteLen) {
        case 1:
            return checkAndWriteWithSign<uint8_t, int8_t>(value, buf, isSigned, !itemBigEndian);
        case 2:
            return checkAndWriteWithSign<uint16_t, int16_t>(value, buf, isSigned, !itemBigEndian);
        case 4:
            return checkAndWriteWithSign<quint32, qint32>(value, buf, isSigned, !itemBigEndian);
        case 8:
            return checkAndWriteWithSign<quint64, qint64>(value, buf, isSigned, !itemBigEndian);
        }
    }
    return false;
}

bool HexWidget::flushCurrentlyEditedWord()
{
    if (editWordState < EditWordState::WriteEdited) {
        return true;
    }
    uint8_t buf[16];
    if (parseWord(editWord, buf, sizeof(buf))) {
        data->write(buf, cursor.address, itemByteLen);
        return true;
    }
    editWordState = EditWordState::WriteNotEdited;
    return false;
}

bool HexWidget::finishEditingWord(bool force)
{
    if (editWordState == EditWordState::WriteEdited) {
        if (!flushCurrentlyEditedWord() && !force) {
            qWarning() << "Not a valid number in current format or size" << editWord;
            showWarningRect(itemRectangle(cursor.address - startAddress).adjusted(-1, -1, 1, 1));
            return false;
        }
    }
    editWord.clear();
    editWordPos = 0;
    editWordState = canKeyboardEdit() ? EditWordState::WriteNotStarted : EditWordState::Read;
    navigationMode = defaultNavigationMode();
    return true;
}

void HexWidget::cancelEditedWord()
{
    editWordPos = 0;
    editWordState = canKeyboardEdit() ? EditWordState::WriteNotStarted : EditWordState::Read;
    editWord.clear();
    navigationMode = defaultNavigationMode();
    updateCursorMeta();
    viewport()->update();
}

void HexWidget::maybeFlushCharEdit()
{
    if (editWordState < EditWordState::WriteEdited) {
        return;
    }
    if ((itemFormat == ItemFormatHex && earlyEditFlush >= EarlyEditFlush::EditNibble)
        || (isFixedWidth() && earlyEditFlush >= EarlyEditFlush::EditFixedWidthChar)) {
        flushCurrentlyEditedWord();
        if (!flushCurrentlyEditedWord()) {
            showWarningRect(itemRectangle(cursor.address - startAddress).adjusted(-1, -1, 1, 1));
        }
    }
    viewport()->update();
}

void HexWidget::startEditWord()
{
    if (!canKeyboardEdit()) {
        return;
    }
    if (editWordState >= EditWordState::WriteNotEdited) {
        return;
    }
    editWordPos = 0;
    editWordState = EditWordState::WriteNotEdited;
    navigationMode = defaultNavigationMode();
    editWord = renderItem(cursor.address - startAddress).trimmed();
    if (itemPrefixLen > 0) {
        editWord = editWord.mid(itemPrefixLen);
    }
    viewport()->update();
}

void HexWidget::fetchData()
{
    data.swap(oldData);
    data->fetch(startAddress, bytesPerScreen());
}

BasicCursor HexWidget::screenPosToAddr(const QPoint &point, bool middle, int *wordOffset) const
{
    QPointF pt = point - itemArea.topLeft();

    int relativeAddress = 0;
    int line = static_cast<int>(pt.y() / lineHeight);
    relativeAddress += line * itemRowByteLen();
    int column = static_cast<int>(pt.x() / columnExWidth());
    relativeAddress += column * itemGroupByteLen();
    pt.rx() -= column * columnExWidth();
    auto roundingOffset = middle ? itemWidth() / 2 : 0;
    int posInGroup = static_cast<int>((pt.x() + roundingOffset) / itemWidth());
    if (!middle) {
        posInGroup = std::min(posInGroup, itemGroupSize - 1);
    }
    relativeAddress += posInGroup * itemByteLen;
    pt.rx() -= posInGroup * itemWidth();
    BasicCursor result(startAddress);
    result += relativeAddress;

    if (!middle && wordOffset != nullptr) {
        int charPos = static_cast<int>((pt.x() / charWidth) + 0.5);
        charPos -= itemPrefixLen;
        charPos = std::max(0, charPos);
        *wordOffset = charPos;
    }
    return result;
}

BasicCursor HexWidget::asciiPosToAddr(const QPoint &point, bool middle) const
{
    QPointF pt = point - asciiArea.topLeft();

    int relativeAddress = 0;
    relativeAddress += static_cast<int>(pt.y() / lineHeight) * itemRowByteLen();
    auto roundingOffset = middle ? (charWidth / 2) : 0;
    relativeAddress += static_cast<int>((pt.x() + (roundingOffset)) / charWidth);
    BasicCursor result(startAddress);
    result += relativeAddress;
    return result;
}

BasicCursor HexWidget::currentAreaPosToAddr(const QPoint &point, bool middle) const
{
    return cursorOnAscii ? asciiPosToAddr(point, middle) : screenPosToAddr(point, middle);
}

BasicCursor HexWidget::mousePosToAddr(const QPoint &point, bool middle) const
{
    return asciiArea.contains(point) ? asciiPosToAddr(point, middle)
                                     : screenPosToAddr(point, middle);
}

QRectF HexWidget::itemRectangle(int offset)
{
    qreal x;
    qreal y;

    qreal width = itemWidth();
    y = (offset / itemRowByteLen()) * lineHeight;
    offset %= itemRowByteLen();

    x = (offset / itemGroupByteLen()) * columnExWidth();
    offset %= itemGroupByteLen();
    x += (offset / itemByteLen) * itemWidth();
    if (offset == 0) {
        x -= charWidth / 2;
        width += charWidth / 2;
    }
    if (static_cast<int>(offset) == itemGroupByteLen() - 1) {
        width += charWidth / 2;
    }

    x += itemArea.x();
    y += itemArea.y();

    return QRectF(x, y, width, lineHeight);
}

QRectF HexWidget::asciiRectangle(int offset)
{
    QPointF p;

    p.ry() = (offset / itemRowByteLen()) * lineHeight;
    offset %= itemRowByteLen();

    p.rx() = offset * charWidth;

    p += asciiArea.topLeft();

    return QRectF(p, QSizeF(charWidth, lineHeight));
}

RVA HexWidget::getLocationAddress()
{
    return !selection.isEmpty() ? selection.start() : cursor.address;
}

void HexWidget::hideWarningRect()
{
    warningRectVisible = false;
    viewport()->update();
}

void HexWidget::showWarningRect(QRectF rect)
{
    warningRect = rect;
    warningRectVisible = true;
    warningTimer.start(WARNING_TIME_MS);
    viewport()->update();
}
