#include "vtextedit.h"

#include <QDebug>
#include <QScrollBar>
#include <QPainter>
#include <QResizeEvent>

#include "vimageresourcemanager2.h"

#define VIRTUAL_CURSOR_BLOCK_WIDTH 8

enum class BlockState
{
    Normal = 0,
    CodeBlockStart,
    CodeBlock,
    CodeBlockEnd,
    Comment
};


VTextEdit::VTextEdit(QWidget *p_parent)
    : QTextEdit(p_parent),
      m_imageMgr(nullptr)
{
    init();
}

VTextEdit::VTextEdit(const QString &p_text, QWidget *p_parent)
    : QTextEdit(p_text, p_parent),
      m_imageMgr(nullptr)
{
    init();
}

VTextEdit::~VTextEdit()
{
    delete m_imageMgr;
}

void VTextEdit::init()
{
    setAcceptRichText(false);

    m_defaultCursorWidth = 1;

    m_lineNumberType = LineNumberType::None;

    m_blockImageEnabled = false;

    m_cursorBlockMode = CursorBlock::None;

    m_highlightCursorLineBlock = false;

    m_enableExtraBuffer = false;

    m_imageMgr = new VImageResourceManager2();

    QTextDocument *doc = document();
    VTextDocumentLayout *docLayout = new VTextDocumentLayout(doc, m_imageMgr);
    docLayout->setBlockImageEnabled(m_blockImageEnabled);
    doc->setDocumentLayout(docLayout);

    docLayout->setVirtualCursorBlockWidth(VIRTUAL_CURSOR_BLOCK_WIDTH);

    docLayout->setCursorWidth(m_defaultCursorWidth);

    connect(docLayout, &VTextDocumentLayout::cursorBlockWidthUpdated,
            this, [this](int p_width) {
                if (p_width != cursorWidth()
                    && p_width > VIRTUAL_CURSOR_BLOCK_WIDTH) {
                    setCursorWidth(p_width);
                }
            });

    m_lineNumberArea = new VLineNumberArea(this,
                                           document(),
                                           fontMetrics().width(QLatin1Char('8')),
                                           this);
    connect(doc, &QTextDocument::blockCountChanged,
            this, &VTextEdit::updateLineNumberAreaMargin);
    connect(this, &QTextEdit::textChanged,
            this, &VTextEdit::updateLineNumberArea);
    connect(verticalScrollBar(), &QScrollBar::valueChanged,
            this, &VTextEdit::updateLineNumberArea);
    connect(this, &QTextEdit::cursorPositionChanged,
            this, [this]() {
                if (m_highlightCursorLineBlock) {
                    QTextCursor cursor = textCursor();
                    getLayout()->setCursorLineBlockNumber(cursor.block().blockNumber());
                }

                updateLineNumberArea();
            });
}

VTextDocumentLayout *VTextEdit::getLayout() const
{
    return qobject_cast<VTextDocumentLayout *>(document()->documentLayout());
}

void VTextEdit::setLineLeading(qreal p_leading)
{
    getLayout()->setLineLeading(p_leading);
}

void VTextEdit::resizeEvent(QResizeEvent *p_event)
{
    QTextEdit::resizeEvent(p_event);

    QRect rect = contentsRect();
    if (m_lineNumberType != LineNumberType::None) {
        m_lineNumberArea->setGeometry(QRect(rect.left(),
                                            rect.top(),
                                            m_lineNumberArea->calculateWidth(),
                                            rect.height()));
    }

    if (m_enableExtraBuffer) {
        getLayout()->setExtraBufferHeight(rect.height() / 2);
    }
}

void VTextEdit::paintLineNumberArea(QPaintEvent *p_event)
{
    if (m_lineNumberType == LineNumberType::None) {
        updateLineNumberAreaMargin();
        m_lineNumberArea->hide();
        return;
    }

    QPainter painter(m_lineNumberArea);
    painter.fillRect(p_event->rect(), m_lineNumberArea->getBackgroundColor());

    QTextBlock block = firstVisibleBlock();
    if (!block.isValid()) {
        return;
    }

    VTextDocumentLayout *layout = getLayout();
    Q_ASSERT(layout);

    int blockNumber = block.blockNumber();
    QRectF rect = layout->blockBoundingRect(block);
    int top = contentOffsetY() + (int)rect.y();
    int bottom = top + (int)rect.height();
    int eventTop = p_event->rect().top();
    int eventBtm = p_event->rect().bottom();
    const int digitHeight = painter.fontMetrics().height();
    const int curBlockNumber = textCursor().block().blockNumber();
    painter.setPen(m_lineNumberArea->getForegroundColor());
    const int leading = (int)layout->getLineLeading();

    // Display line number only in code block.
    if (m_lineNumberType == LineNumberType::CodeBlock) {
        int number = 0;
        while (block.isValid() && top <= eventBtm) {
            int blockState = block.userState();
            switch (blockState) {
            case (int)BlockState::CodeBlockStart:
                Q_ASSERT(number == 0);
                number = 1;
                break;

            case (int)BlockState::CodeBlockEnd:
                number = 0;
                break;

            case (int)BlockState::CodeBlock:
                if (number == 0) {
                    // Need to find current line number in code block.
                    QTextBlock startBlock = block.previous();
                    while (startBlock.isValid()) {
                        if (startBlock.userState() == (int)BlockState::CodeBlockStart) {
                            number = block.blockNumber() - startBlock.blockNumber();
                            break;
                        }

                        startBlock = startBlock.previous();
                    }
                }

                break;

            default:
                break;
            }

            if (blockState == (int)BlockState::CodeBlock) {
                if (block.isVisible() && bottom >= eventTop) {
                    QString numberStr = QString::number(number);
                    painter.drawText(0,
                                     top + leading,
                                     m_lineNumberArea->width(),
                                     digitHeight,
                                     Qt::AlignRight | Qt::AlignTop,
                                     numberStr);
                }

                ++number;
            }

            block = block.next();
            top = bottom;
            bottom = top + (int)layout->blockBoundingRect(block).height();
        }

        return;
    }

    // Handle m_lineNumberType 1 and 2.
    Q_ASSERT(m_lineNumberType == LineNumberType::Absolute
             || m_lineNumberType == LineNumberType::Relative);
    while (block.isValid() && top <= eventBtm) {
        if (block.isVisible() && bottom >= eventTop) {
            bool currentLine = false;
            int number = blockNumber + 1;
            if (m_lineNumberType == LineNumberType::Relative) {
                number = blockNumber - curBlockNumber;
                if (number == 0) {
                    currentLine = true;
                    number = blockNumber + 1;
                } else if (number < 0) {
                    number = -number;
                }
            } else if (blockNumber == curBlockNumber) {
                currentLine = true;
            }

            QString numberStr = QString::number(number);

            if (currentLine) {
                QFont font = painter.font();
                font.setBold(true);
                painter.setFont(font);
            }

            painter.drawText(0,
                             top + leading,
                             m_lineNumberArea->width(),
                             digitHeight,
                             Qt::AlignRight | Qt::AlignTop,
                             numberStr);

            if (currentLine) {
                QFont font = painter.font();
                font.setBold(false);
                painter.setFont(font);
            }
        }

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

void VTextEdit::updateLineNumberAreaMargin()
{
    int width = 0;
    if (m_lineNumberType != LineNumberType::None) {
        width = m_lineNumberArea->calculateWidth();
    }

    if (width != viewportMargins().left()) {
        setViewportMargins(width, 0, 0, 0);
    }
}

void VTextEdit::updateLineNumberArea()
{
    if (m_lineNumberType != LineNumberType::None) {
        if (!m_lineNumberArea->isVisible()) {
            updateLineNumberAreaMargin();
            m_lineNumberArea->show();
        }

        m_lineNumberArea->update();
    } else if (m_lineNumberArea->isVisible()) {
        updateLineNumberAreaMargin();
        m_lineNumberArea->hide();
    }
}

int VTextEdit::firstVisibleBlockNumber() const
{
    VTextDocumentLayout *layout = getLayout();
    Q_ASSERT(layout);
    return layout->findBlockByPosition(QPointF(0, -contentOffsetY()));
}

QTextBlock VTextEdit::firstVisibleBlock() const
{
    VTextDocumentLayout *layout = getLayout();
    Q_ASSERT(layout);
    int blockNumber = layout->findBlockByPosition(QPointF(0, -contentOffsetY()));
    return document()->findBlockByNumber(blockNumber);
}

QTextBlock VTextEdit::lastVisibleBlock() const
{
    VTextDocumentLayout *layout = getLayout();
    Q_ASSERT(layout);
    int blockNumber = layout->findBlockByPosition(QPointF(0, -contentOffsetY() + contentsRect().height()));
    return document()->findBlockByNumber(blockNumber);
}

void VTextEdit::visibleBlockRange(int &p_first, int &p_last) const
{
    VTextDocumentLayout *layout = getLayout();
    Q_ASSERT(layout);
    p_first = layout->findBlockByPosition(QPointF(0, -contentOffsetY()));
    p_last = layout->findBlockByPosition(QPointF(0, -contentOffsetY() + contentsRect().height()));
}

int VTextEdit::contentOffsetY() const
{
    QScrollBar *sb = verticalScrollBar();
    return -(sb->value());
}

void VTextEdit::clearBlockImages()
{
    m_imageMgr->clear();

    getLayout()->relayout();
}

void VTextEdit::relayout(const OrderedIntSet &p_blocks)
{
    if (p_blocks.isEmpty()) {
        return;
    }

    getLayout()->relayout(p_blocks);

    updateLineNumberArea();
}

void VTextEdit::relayoutVisibleBlocks()
{
    int first, last;
    visibleBlockRange(first, last);
    OrderedIntSet blocks;

    for (int i = first; i <= last; ++i) {
        blocks.insert(i, QMapDummyValue());
    }

    getLayout()->relayout(blocks);

    updateLineNumberArea();
}

bool VTextEdit::containsImage(const QString &p_imageName) const
{
    return m_imageMgr->contains(p_imageName);
}

QSize VTextEdit::imageSize(const QString &p_imageName) const
{
    const QPixmap *img = m_imageMgr->findImage(p_imageName);
    if (img) {
        return img->size();
    }

    return QSize();
}

const QPixmap *VTextEdit::findImage(const QString &p_name) const
{
    return m_imageMgr->findImage(p_name);
}

void VTextEdit::addImage(const QString &p_imageName, const QPixmap &p_image)
{
    if (m_blockImageEnabled) {
        m_imageMgr->addImage(p_imageName, p_image);
    }
}

void VTextEdit::removeImage(const QString &p_imageName)
{
    m_imageMgr->removeImage(p_imageName);
}

void VTextEdit::setBlockImageEnabled(bool p_enabled)
{
    if (m_blockImageEnabled == p_enabled) {
        return;
    }

    m_blockImageEnabled = p_enabled;

    getLayout()->setBlockImageEnabled(m_blockImageEnabled);

    if (!m_blockImageEnabled) {
        clearBlockImages();
    }
}

void VTextEdit::setImageWidthConstrainted(bool p_enabled)
{
    getLayout()->setImageWidthConstrainted(p_enabled);
}

void VTextEdit::setImageLineColor(const QColor &p_color)
{
    getLayout()->setImageLineColor(p_color);
}

void VTextEdit::setCursorBlockMode(CursorBlock p_mode)
{
    VTextDocumentLayout *layout = getLayout();

    if (p_mode != m_cursorBlockMode) {
        m_cursorBlockMode = p_mode;
        layout->setCursorBlockMode(m_cursorBlockMode);
        layout->clearLastCursorBlockWidth();
        setCursorWidth(m_cursorBlockMode != CursorBlock::None ? VIRTUAL_CURSOR_BLOCK_WIDTH
                                                              : m_defaultCursorWidth);
        layout->updateBlockByNumber(textCursor().blockNumber());
    }
}

void VTextEdit::setHighlightCursorLineBlockEnabled(bool p_enabled)
{
    if (m_highlightCursorLineBlock != p_enabled) {
        auto layout = getLayout();
        m_highlightCursorLineBlock = p_enabled;
        layout->setHighlightCursorLineBlockEnabled(p_enabled);
        if (m_highlightCursorLineBlock) {
            QTextCursor cursor = textCursor();
            layout->setCursorLineBlockNumber(cursor.block().blockNumber());
        }
    }
}

void VTextEdit::setCursorLineBlockBg(const QColor &p_bg)
{
    getLayout()->setCursorLineBlockBg(p_bg);
}

void VTextEdit::relayout()
{
    getLayout()->relayout();

    updateLineNumberArea();
}

void VTextEdit::setDisplayScaleFactor(qreal p_factor)
{
    m_defaultCursorWidth = p_factor + 0.5;

    setCursorWidth(m_cursorBlockMode != CursorBlock::None ? VIRTUAL_CURSOR_BLOCK_WIDTH
                                                          : m_defaultCursorWidth);
    getLayout()->setCursorWidth(m_defaultCursorWidth);
}

void VTextEdit::updateLineNumberAreaWidth(const QFontMetrics &p_metrics)
{
    m_lineNumberArea->setDigitWidth(p_metrics.width(QLatin1Char('8')));
    updateLineNumberAreaMargin();
}

void VTextEdit::dragMoveEvent(QDragMoveEvent *p_event)
{
    QTextEdit::dragMoveEvent(p_event);

    // We need to update the cursor rect to show the cursor while dragging text.
    // This is a work-around. We do not know why VTextEdit won't update the cursor
    // rect to show the cursor.
    // TODO: find out the rect of current cursor to update that rect only.
    update();
}

int VTextEdit::lineNumberAreaWidth() const
{
    return m_lineNumberArea->width();
}
