/***
   This file is part of fly-ocr.

   Copyright (C) 2016-2020 Sandro Mani <manisandro@gmail.com>
   Copyright (C) 2021 LLC "RusBITech-Astra" (https://astralinux.ru)

    Based on gImageReader source code by Sandro Mani under the GNU
    General Public License (GPL) version 3 or later

   Foobar is free software: you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation, either version 3 of the License, or
   (at your option) any later version.

   Foobar is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with fly-ocr. If not, see <https://www.gnu.org/licenses/>.
***/

#include <QApplication>
#include <QDir>
#include <QDomDocument>
#include <QFileInfo>
#include <QFontComboBox>
#include <QImage>
#include <QInputDialog>
#include <QShortcut>
#include <QStyledItemDelegate>
#include <QMessageBox>
#include <QPointer>
#include <QStandardItemModel>
#include <QSyntaxHighlighter>
#include <QtSpell.hpp>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <fly-ocr/ocrengine.h>
#include "configsettings.h"
#include "displayertoolhocr.h"
#include "filedialogs.h"
#include "hocrdocument.h"
#include "hocrodtexporter.h"
#include "hocrpdfexporter.h"
#include "hocrproofreadwidget.h"
#include "hocrtextexporter.h"
#include "mainwindow.h"
#include "outputeditorhocr.h"
#include "recognizer.h"
#include "sourcemanager.h"
#include "utils.h"


class OutputEditorHOCR::HTMLHighlighter : public QSyntaxHighlighter
{
public:
    HTMLHighlighter(QTextDocument* document) : QSyntaxHighlighter(document)
    {
        mFormatMap[NormalState].setForeground(QColor(Qt::black));
        mFormatMap[InTag].setForeground(QColor(75, 75, 255));
        mFormatMap[InAttrKey].setForeground(QColor(75, 200, 75));
        mFormatMap[InAttrValue].setForeground(QColor(255, 75, 75));
        mFormatMap[InAttrValueDblQuote].setForeground(QColor(255, 75, 75));

        mStateMap[NormalState].append({QRegExp("<"), InTag, false});
        mStateMap[InTag].append({QRegExp(">"), NormalState, true});
        mStateMap[InTag].append({QRegExp("\\w+="), InAttrKey, false});
        mStateMap[InAttrKey].append({QRegExp("'"), InAttrValue, false});
        mStateMap[InAttrKey].append({QRegExp("\""), InAttrValueDblQuote, false});
        mStateMap[InAttrKey].append({QRegExp("\\s"), NormalState, false});
        mStateMap[InAttrValue].append({QRegExp("'[^']*'"), InTag, true});
        mStateMap[InAttrValueDblQuote].append({QRegExp("\"[^\"]*\""), InTag, true});
    }

private:
    enum State { NormalState = -1, InComment, InTag, InAttrKey, InAttrValue, InAttrValueDblQuote };
    struct Rule {
        QRegExp pattern;
        State nextState;
        bool addMatched; // add matched length to pos
    };

    QMap<State, QTextCharFormat> mFormatMap;
    QMap<State, QList<Rule>> mStateMap;

    void highlightBlock(const QString& text) override
    {
        int pos = 0;
        int len = text.length();
        State state = static_cast<State>(previousBlockState());
        while (pos < len) {
            State minState = state;
            int minPos = -1;
            for (const Rule& rule : mStateMap.value(state)) {
                int matchPos = rule.pattern.indexIn(text, pos);
                if (matchPos != -1 && (minPos < 0 || matchPos < minPos)) {
                    minPos = matchPos + (rule.addMatched ? rule.pattern.matchedLength() : 0);
                    minState = rule.nextState;
                }
            }
            if (minPos == -1) {
                setFormat(pos, len - pos, mFormatMap[state]);
                pos = len;
            } else {
                setFormat(pos, minPos - pos, mFormatMap[state]);
                pos = minPos;
                state = minState;
            }
        }
        setCurrentBlockState(state);
    }
};

///////////////////////////////////////////////////////////////////////////////

HOCRAttributeEditor::HOCRAttributeEditor(const QString& value, HOCRDocument* doc, const QModelIndex& itemIndex, const QString& attrName, const QString& attrItemClass)
    : QLineEdit(value), m_doc(doc), m_itemIndex(itemIndex), m_attrName(attrName), m_origValue(value), m_attrItemClass(attrItemClass)
{
    setFrame(false);
    connect(m_doc, &HOCRDocument::itemAttributeChanged, this, &HOCRAttributeEditor::updateValue);
    connect(this, &HOCRAttributeEditor::textChanged, this, [this] { validateChanges(); });
    connect(this, &QLineEdit::returnPressed, this, [this] { validateChanges(true); });
}

void HOCRAttributeEditor::focusOutEvent(QFocusEvent* ev)
{
    QLineEdit::focusOutEvent(ev);
    validateChanges();
}

void HOCRAttributeEditor::updateValue(const QModelIndex& itemIndex, const QString& name, const QString& value)
{
    if (itemIndex == m_itemIndex && name == m_attrName) {
        blockSignals(true);
        setText(value);
        blockSignals(false);
    }
}

void HOCRAttributeEditor::validateChanges(bool force)
{
    if (!hasFocus() || force) {
        int pos;
        QString newValue = text();
        if (newValue == m_origValue) {
            return;
        }
        if (validator() && validator()->validate(newValue, pos) != QValidator::Acceptable) {
            setText(m_origValue);
        } else {
            m_doc->editItemAttribute(m_itemIndex, m_attrName, newValue, m_attrItemClass);
            m_origValue = newValue;
        }
    }
}

///////////////////////////////////////////////////////////////////////////////

HOCRAttributeCheckbox::HOCRAttributeCheckbox(Qt::CheckState value, HOCRDocument* doc, const QModelIndex& itemIndex, const QString& attrName, const QString& attrItemClass)
    : m_doc(doc), m_itemIndex(itemIndex), m_attrName(attrName), m_attrItemClass(attrItemClass)
{
    setCheckState(value);
    connect(m_doc, &HOCRDocument::itemAttributeChanged, this, &HOCRAttributeCheckbox::updateValue);
    connect(this, &HOCRAttributeCheckbox::stateChanged, this, &HOCRAttributeCheckbox::valueChanged);
}

void HOCRAttributeCheckbox::updateValue(const QModelIndex& itemIndex, const QString& name, const QString& value)
{
    if (itemIndex == m_itemIndex && name == m_attrName) {
        blockSignals(true);
        setChecked(value == "1");
        blockSignals(false);
    }
}

void HOCRAttributeCheckbox::valueChanged()
{
    m_doc->editItemAttribute(m_itemIndex, m_attrName, isChecked() ? "1" : "0", m_attrItemClass);
}

///////////////////////////////////////////////////////////////////////////////


HOCRAttributeLangCombo::HOCRAttributeLangCombo(const QString& value, bool multiple, HOCRDocument* doc, const QModelIndex& itemIndex, const QString& attrName, const QString& attrItemClass)
    : m_doc(doc), m_itemIndex(itemIndex), m_attrName(attrName), m_attrItemClass(attrItemClass)
{
    if (multiple) {
        addItem(tr("Multiple values"));
        setCurrentIndex(0);
        QStandardItemModel* itemModel = qobject_cast<QStandardItemModel*>(model());
        itemModel->item(0)->setFlags(itemModel->item(0)->flags() & ~Qt::ItemIsEnabled);
    }
    for (const QString& code : QtSpell::Checker::getLanguageList()) {
        QString text = QtSpell::Checker::decodeLanguageCode(code);
        addItem(text, code);
    }
    if (!multiple) {
        setCurrentIndex(findData(value));
    }
    connect(m_doc, &HOCRDocument::itemAttributeChanged, this, &HOCRAttributeLangCombo::updateValue);
    connect(this, qOverload<int>(&HOCRAttributeLangCombo::currentIndexChanged), this, &HOCRAttributeLangCombo::valueChanged);
}

void HOCRAttributeLangCombo::updateValue(const QModelIndex& itemIndex, const QString& name, const QString& value)
{
    if (itemIndex == m_itemIndex && name == m_attrName) {
        blockSignals(true);
        setCurrentIndex(findData(value));
        blockSignals(false);
    }
}

void HOCRAttributeLangCombo::valueChanged()
{
    m_doc->editItemAttribute(m_itemIndex, m_attrName, currentData().toString(), m_attrItemClass);
}

///////////////////////////////////////////////////////////////////////////////

class HOCRTextDelegate : public QStyledItemDelegate
{
public:
    using QStyledItemDelegate::QStyledItemDelegate;

    QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& /* option */, const QModelIndex& /* index */) const override
    {
        return new QLineEdit(parent);
    }
    void setEditorData(QWidget* editor, const QModelIndex& index) const override
    {
        m_currentIndex = index;
        m_currentEditor = static_cast<QLineEdit*>(editor);
        static_cast<QLineEdit*>(editor)->setText(index.model()->data(index, Qt::EditRole).toString());
    }
    void setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const override
    {
        model->setData(index, static_cast<QLineEdit*>(editor)->text(), Qt::EditRole);
    }
    const QModelIndex& getCurrentIndex() const
    {
        return m_currentIndex;
    }
    QLineEdit* getCurrentEditor() const
    {
        return m_currentEditor;
    }

private:
    mutable QModelIndex m_currentIndex;
    mutable QPointer<QLineEdit> m_currentEditor;
};

///////////////////////////////////////////////////////////////////////////////

void OutputEditorHOCR::HOCRBatchProcessor::writeHeader(QIODevice* dev, const PageInfo& pageInfo) const
{

    OcrEngineFactory* factory = OcrEngine::selected();

    QString header = QString(
                         "<!DOCTYPE html>\n"
                         "<html>\n"
                         "<head>\n"
                         " <title>%1</title>\n"
                         " <meta charset=\"utf-8\" /> \n"
                         " <meta name='ocr-system' content='%2 %3' />\n"
                         " <meta name='ocr-capabilities' content='ocr_page ocr_carea ocr_par ocr_line ocrx_word'/>\n"
                         "</head><body>\n").arg(QFileInfo(pageInfo.filename).fileName())
                     .arg(factory->name()).arg(factory->version());
    dev->write(header.toUtf8());
}

void OutputEditorHOCR::HOCRBatchProcessor::writeFooter(QIODevice* dev) const
{
    dev->write("</body></html>\n");
}

void OutputEditorHOCR::HOCRBatchProcessor::appendOutput(QIODevice* dev, OcrEngine* engine, const PageInfo& pageInfos, bool /*firstArea*/) const
{
    QDomDocument doc;
    doc.setContent(engine->getPageData(pageInfos.page, OcrEngineFactory::HOCR));

    QDomElement pageDiv = doc.firstChildElement("div");
    QMap<QString, QString> attrs = HOCRItem::deserializeAttrGroup(pageDiv.attribute("title"));
    // This works because in batch mode the output is created next to the source image
    attrs["image"] = QString("'./%1'").arg(QFileInfo(pageInfos.filename).fileName());
    attrs["ppageno"] = QString::number(pageInfos.page);
    attrs["rot"] = QString::number(pageInfos.angle);
    attrs["res"] = QString::number(pageInfos.resolution);
    pageDiv.setAttribute("title", HOCRItem::serializeAttrGroup(attrs));
    dev->write(doc.toByteArray());
}

///////////////////////////////////////////////////////////////////////////////

OutputEditorHOCR::OutputEditorHOCR(DisplayerToolHOCR* tool, QWidget* parent) : OutputEditor(parent)
{
    static int reg = qRegisterMetaType<QList<QRect>>("QList<QRect>");
    Q_UNUSED(reg);

    m_tool = tool;
    m_ui.setupUi(this);
    m_highlighter = new HTMLHighlighter(m_ui.plainTextEditOutput->document());

    m_preview = new QGraphicsPixmapItem();
    m_preview->setTransformationMode(Qt::SmoothTransformation);
    m_preview->setZValue(2);
    MAIN->displayer()->scene()->addItem(m_preview);
    m_previewTimer.setSingleShot(true);

    m_ui.actionOutputReplace->setShortcut(Qt::CTRL + Qt::Key_F);
    m_ui.actionNavigateNext->setShortcut(Qt::Key_F3);
    m_ui.actionNavigatePrev->setShortcut(Qt::SHIFT + Qt::Key_F3);

    m_document = new HOCRDocument(m_ui.treeViewHOCR);
    m_ui.treeViewHOCR->setModel(m_document);
    m_ui.treeViewHOCR->setContextMenuPolicy(Qt::CustomContextMenu);
    m_ui.treeViewHOCR->header()->setStretchLastSection(false);
    m_ui.treeViewHOCR->header()->setSectionResizeMode(0, QHeaderView::Stretch);
    m_ui.treeViewHOCR->setColumnWidth(1, 32);
    m_ui.treeViewHOCR->setColumnHidden(1, true);
    m_ui.treeViewHOCR->setItemDelegateForColumn(0, new HOCRTextDelegate(m_ui.treeViewHOCR));

    m_proofReadWidget = new HOCRProofReadWidget(m_ui.treeViewHOCR, MAIN->displayer());

    m_ui.comboBoxNavigate->addItem(tr("Page"), "ocr_page");
    m_ui.comboBoxNavigate->addItem(tr("Block"), "ocr_carea");
    m_ui.comboBoxNavigate->addItem(tr("Paragraph"), "ocr_par");
    m_ui.comboBoxNavigate->addItem(tr("Line"), "ocr_line");
    m_ui.comboBoxNavigate->addItem(tr("Word"), "ocrx_word");
    m_ui.comboBoxNavigate->addItem(tr("Misspelled word"), "ocrx_word_bad");

    QShortcut* shortcut = new QShortcut(QKeySequence(Qt::Key_Delete), this);
    connect(shortcut, &QShortcut::activated, this, &OutputEditorHOCR::removeItem);
//    connect(m_ui.actionOutputExportODT, &QAction::triggered, this, &OutputEditorHOCR::exportToODT);
//    connect(m_ui.actionOutputExportPDF, &QAction::triggered, this, &OutputEditorHOCR::exportToPDF);
//    connect(m_ui.actionOutputExportText, &QAction::triggered, this, &OutputEditorHOCR::exportToText);
//    connect(m_ui.actionOutputExportDOCX, &QAction::triggered, this, &OutputEditorHOCR::exportToDOCX);
    connect(m_ui.actionOutputClear, &QAction::triggered, this, &OutputEditorHOCR::clear);
    connect(m_ui.actionOutputReplace, &QAction::triggered, m_ui.searchFrame, &SearchReplaceFrame::setVisible);
    connect(m_ui.actionOutputReplace, &QAction::triggered, m_ui.searchFrame, &SearchReplaceFrame::clear);
    connect(m_ui.actionToggleWConf, &QAction::triggered, this, &OutputEditorHOCR::toggleWConfColumn);
    connect(m_ui.actionPreview, &QAction::toggled, this, &OutputEditorHOCR::previewToggled);
    connect(&m_previewTimer, &QTimer::timeout, this, &OutputEditorHOCR::updatePreview);
    connect(m_ui.searchFrame, &SearchReplaceFrame::findReplace, this, &OutputEditorHOCR::findReplace);
    connect(m_ui.searchFrame, &SearchReplaceFrame::replaceAll, this, &OutputEditorHOCR::replaceAll);
    connect(m_ui.searchFrame, &SearchReplaceFrame::applySubstitutions, this, &OutputEditorHOCR::applySubstitutions);
    connect(ConfigSettings::get<FontSetting>("customoutputfont"), &FontSetting::changed, this, &OutputEditorHOCR::setFont);
    connect(ConfigSettings::get<SwitchSetting>("systemoutputfont"), &FontSetting::changed, this, &OutputEditorHOCR::setFont);
    connect(m_ui.treeViewHOCR->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &OutputEditorHOCR::showItemProperties);
    connect(m_ui.treeViewHOCR, &QTreeView::customContextMenuRequested, this, &OutputEditorHOCR::showTreeWidgetContextMenu);
    connect(m_ui.tabWidgetProps, &QTabWidget::currentChanged, this, &OutputEditorHOCR::updateSourceText);
    connect(m_tool, &DisplayerToolHOCR::bboxChanged, this, &OutputEditorHOCR::updateCurrentItemBBox);
    connect(m_tool, &DisplayerToolHOCR::bboxDrawn, this, &OutputEditorHOCR::bboxDrawn);
    connect(m_tool, &DisplayerToolHOCR::positionPicked, this, &OutputEditorHOCR::pickItem);
    connect(m_document, &HOCRDocument::dataChanged, this, &OutputEditorHOCR::setModified);
    connect(m_document, &HOCRDocument::rowsInserted, this, &OutputEditorHOCR::setModified);
    connect(m_document, &HOCRDocument::rowsRemoved, this, &OutputEditorHOCR::setModified);
    connect(m_document, &HOCRDocument::itemAttributeChanged, this, &OutputEditorHOCR::setModified);
    connect(m_document, &HOCRDocument::itemAttributeChanged, this, &OutputEditorHOCR::updateSourceText);
    connect(m_document, &HOCRDocument::itemAttributeChanged, this, &OutputEditorHOCR::itemAttributeChanged);
    connect(m_ui.comboBoxNavigate, qOverload<int>(&QComboBox::currentIndexChanged), this, &OutputEditorHOCR::navigateTargetChanged);
    connect(m_ui.actionNavigateNext, &QAction::triggered, this, &OutputEditorHOCR::navigateNext);
    connect(m_ui.actionNavigatePrev, &QAction::triggered, this, &OutputEditorHOCR::navigatePrev);
    connect(m_ui.actionExpandAll, &QAction::triggered, this, &OutputEditorHOCR::expandItemClass);
    connect(m_ui.actionCollapseAll, &QAction::triggered, this, &OutputEditorHOCR::collapseItemClass);
    connect(MAIN->displayer(), &Displayer::imageChanged, this, &OutputEditorHOCR::sourceChanged);

    setFont();

    //m_ui.actionOutputExportDOCX->setVisible(OcrEngine::supportedExportFormats() & OcrEngineFactory::DOCX);
}

OutputEditorHOCR::~OutputEditorHOCR()
{
    m_previewTimer.stop();
    delete m_preview;
    delete m_proofReadWidget;
}

void OutputEditorHOCR::read(int pageNumber, const SourceDocumentPage* page)
{
    if (!engine())
        return;

    QByteArray data = engine()->getPageData(pageNumber, OcrEngineFactory::HOCR);

    QDomDocument doc;
    doc.setContent(QString::fromUtf8(data));
    QDomElement pageDiv = doc.firstChildElement("div");
    QMap<QString, QString> attrs = HOCRItem::deserializeAttrGroup(pageDiv.attribute("title"));
    attrs["image"] = QString("'%1'").arg(page->path);
    attrs["ppageno"] = QString::number(page->page);
    attrs["rot"] = QString::number(page->angle);
    attrs["res"] = QString::number(page->resolution);
    pageDiv.setAttribute("title", HOCRItem::serializeAttrGroup(attrs));

    QModelIndex index = m_document->insertPage(0, pageDiv, true);

    expandCollapseChildren(index, true);
    MAIN->setOutputPaneVisible(true);
    m_modified = true;
    m_page = pageNumber;
}

void OutputEditorHOCR::setFont()
{
    if (ConfigSettings::get<SwitchSetting>("systemoutputfont")->getValue()) {
        m_ui.plainTextEditOutput->setFont(QFont());
    } else {
        m_ui.plainTextEditOutput->setFont(ConfigSettings::get<FontSetting>("customoutputfont")->getValue());
    }
}

void OutputEditorHOCR::setModified()
{
    m_ui.toolBarNavigate->setEnabled(m_document->pageCount() > 0);
    m_preview->setVisible(false);
    m_previewTimer.start(100); // Use a timer because setModified is potentially called a large number of times when the HOCR tree changes
    m_modified = true;
}

bool OutputEditorHOCR::isModified() const
{
    return m_modified;
}

void OutputEditorHOCR::commit()
{
    if (engine() && m_page >= 0) {
        //engine()->setPageData(m_page, OcrEngineFactory::HOCR, m_document->toHTML().toUtf8());
        m_modified = false;
    }
}

void OutputEditorHOCR::clear()
{
    m_previewTimer.stop();
    m_preview->setVisible(false);
    m_proofReadWidget->clear();
    m_document->clear();
    m_ui.tableWidgetProperties->setRowCount(0);
    m_ui.plainTextEditOutput->clear();
    m_tool->clearSelection();
    m_modified = false;
    m_filebasename.clear();
}

void OutputEditorHOCR::navigateTargetChanged()
{
    QString target = m_ui.comboBoxNavigate->itemData(m_ui.comboBoxNavigate->currentIndex()).toString();
    bool allowExpandCollapse = !target.startsWith("ocrx_word");
    m_ui.actionExpandAll->setEnabled(allowExpandCollapse);
    m_ui.actionCollapseAll->setEnabled(allowExpandCollapse);
}

void OutputEditorHOCR::navigateNext()
{
    navigateNextPrev(true);
}

void OutputEditorHOCR::navigatePrev()
{
    navigateNextPrev(false);
}

void OutputEditorHOCR::expandCollapseItemClass(bool expand)
{
    QString target = m_ui.comboBoxNavigate->itemData(m_ui.comboBoxNavigate->currentIndex()).toString();
    QModelIndex start = m_document->index(0, 0);
    QModelIndex next = start;
    do {
        const HOCRItem* item = m_document->itemAtIndex(next);
        if (item && item->itemClass() == target) {
            if (expand) {
                m_ui.treeViewHOCR->setExpanded(next, expand);
                for (QModelIndex parent = next.parent(); parent.isValid(); parent = parent.parent()) {
                    m_ui.treeViewHOCR->setExpanded(parent, true);
                }
                for (QModelIndex child = m_document->index(0, 0, next); child.isValid(); child = child.sibling(child.row() + 1, 0)) {
                    expandCollapseChildren(child, false);
                }
            } else {
                expandCollapseChildren(next, false);
            }
        }
        next = m_document->nextIndex(next);
    } while (next != start);
    m_ui.treeViewHOCR->scrollTo(m_ui.treeViewHOCR->currentIndex());
}

void OutputEditorHOCR::navigateNextPrev(bool next)
{
    QString target = m_ui.comboBoxNavigate->itemData(m_ui.comboBoxNavigate->currentIndex()).toString();
    bool misspelled = false;
    if (target == "ocrx_word_bad") {
        target = "ocrx_word";
        misspelled = true;
    }
    QModelIndex start = m_ui.treeViewHOCR->currentIndex();
    m_ui.treeViewHOCR->setCurrentIndex(m_document->prevOrNextIndex(next, start, target, misspelled));
}

void OutputEditorHOCR::expandCollapseChildren(const QModelIndex& index, bool expand) const
{
    int nChildren = m_document->rowCount(index);
    if (nChildren > 0) {
        m_ui.treeViewHOCR->setExpanded(index, expand);
        for (int i = 0; i < nChildren; ++i) {
            expandCollapseChildren(m_document->index(i, 0, index), expand);
        }
    }
}

bool OutputEditorHOCR::showPage(const HOCRPage* page)
{
    m_blockSourceChanged = true;
    bool success = page && MAIN->displayer()->setup(&page->pageNr(), &page->resolution(), &page->angle());
    m_blockSourceChanged = false;
    if (success) {
        sourceChanged();
    }
    return success;
}

int OutputEditorHOCR::currentPage()
{
    QModelIndexList selected = m_ui.treeViewHOCR->selectionModel()->selectedIndexes();
    if (selected.isEmpty()) {
        return m_document->pageCount();
    }
    QModelIndex index = selected.front();
    if (!index.isValid()) {
        return m_document->pageCount();
    }
    while (index.parent().isValid()) {
        index = index.parent();
    }
    return index.row();
}

void OutputEditorHOCR::showItemProperties(const QModelIndex& index, const QModelIndex& prev)
{
    m_tool->setAction(DisplayerToolHOCR::ACTION_NONE);
    const HOCRItem* prevItem = m_document->itemAtIndex(prev);
    m_ui.tableWidgetProperties->setRowCount(0);
    m_ui.plainTextEditOutput->clear();

    const HOCRItem* currentItem = m_document->itemAtIndex(index);
    if (!currentItem) {
        m_tool->clearSelection();
        return;
    }
    const HOCRPage* page = currentItem->page();

    int row = -1;
    QMap<QString, QString> attrs = currentItem->getAllAttributes();
    for (auto it = attrs.begin(), itEnd = attrs.end(); it != itEnd; ++it) {
        QString attrName = it.key();
        if (attrName == "class" || attrName == "id") {
            continue;
        }
        QStringList parts = attrName.split(":");
        m_ui.tableWidgetProperties->insertRow(++row);
        QTableWidgetItem* attrNameItem = new QTableWidgetItem(parts.last());
        attrNameItem->setFlags(attrNameItem->flags() & ~Qt::ItemIsEditable);
        m_ui.tableWidgetProperties->setItem(row, 0, attrNameItem);
        m_ui.tableWidgetProperties->setCellWidget(row, 1, createAttrWidget(index, attrName, it.value()));
    }

    // ocr_class:attr_key:attr_values
    QMap<QString, QMap<QString, QSet<QString>>> occurrences;
    currentItem->getPropagatableAttributes(occurrences);
    for (auto it = occurrences.begin(), itEnd = occurrences.end(); it != itEnd; ++it) {
        m_ui.tableWidgetProperties->insertRow(++row);
        QTableWidgetItem* sectionItem = new QTableWidgetItem(it.key());
        sectionItem->setFlags(sectionItem->flags() & ~(Qt::ItemIsEditable | Qt::ItemIsSelectable));
        sectionItem->setBackground(Qt::lightGray);
        QFont sectionFont = sectionItem->font();
        sectionFont.setBold(true);
        sectionItem->setFont(sectionFont);
        m_ui.tableWidgetProperties->setItem(row, 0, sectionItem);
        m_ui.tableWidgetProperties->setSpan(row, 0, 1, 2);
        for (auto attrIt = it.value().begin(), attrItEnd = it.value().end(); attrIt != attrItEnd; ++attrIt) {
            const QString& attrName = attrIt.key();
            const QSet<QString>& attrValues = attrIt.value();
            int attrValueCount = attrValues.size();
            m_ui.tableWidgetProperties->insertRow(++row);
            QStringList parts = attrName.split(":");
            QTableWidgetItem* attrNameItem = new QTableWidgetItem(parts.last());
            attrNameItem->setFlags(attrNameItem->flags() & ~Qt::ItemIsEditable);
            m_ui.tableWidgetProperties->setItem(row, 0, attrNameItem);
            m_ui.tableWidgetProperties->setCellWidget(row, 1, createAttrWidget(index, attrName, attrValueCount == 1 ? * (attrValues.begin()) : "", it.key(), attrValueCount > 1));
        }
    }

    m_ui.plainTextEditOutput->setPlainText(currentItem->toHtml());

    if (showPage(page)) {
        // Update preview if necessary
        if (!prevItem || prevItem->page() != page) {
            updatePreview();
        }
        // Minimum bounding box
        QRect minBBox;
        if (currentItem->itemClass() == "ocr_page") {
            minBBox = currentItem->bbox();
        } else {
            for (const HOCRItem* child : currentItem->children()) {
                minBBox = minBBox.united(child->bbox());
            }
        }
        m_tool->setSelection(currentItem->bbox(), minBBox);
    }
}

QWidget* OutputEditorHOCR::createAttrWidget(const QModelIndex& itemIndex, const QString& attrName, const QString& attrValue, const QString& attrItemClass, bool multiple)
{
    static QMap<QString, QString> attrLineEdits = {
        {"title:bbox", "\\d+\\s+\\d+\\s+\\d+\\s+\\d+"},
        {"title:x_fsize", "\\d+"},
        {"title:baseline", "[-+]?\\d+\\.?\\d*\\s[-+]?\\d+\\.?\\d*"}
    };
    auto it = attrLineEdits.find(attrName);
    if (it != attrLineEdits.end()) {
        QLineEdit* lineEdit = new HOCRAttributeEditor(attrValue, m_document, itemIndex, attrName, attrItemClass);
        lineEdit->setValidator(new QRegExpValidator(QRegExp(it.value())));
        if (multiple) {
            lineEdit->setPlaceholderText(tr("Multiple values"));
        }
        return lineEdit;
    } else if (attrName == "title:x_font") {
        QFontComboBox* combo = new QFontComboBox();
        combo->setCurrentIndex(-1);
        QLineEdit* edit = new HOCRAttributeEditor(attrValue, m_document, itemIndex, attrName, attrItemClass);
        edit->blockSignals(true); // Because the combobox alters the text as soon as setLineEdit is called...
        combo->setLineEdit(edit);
        edit->setText(attrValue);
        edit->blockSignals(false);
        if (multiple) {
            combo->lineEdit()->setPlaceholderText(tr("Multiple values"));
        }
        return combo;
    } else if (attrName == "lang") {
        HOCRAttributeLangCombo* combo = new HOCRAttributeLangCombo(attrValue, multiple, m_document, itemIndex, attrName, attrItemClass);
        return combo;
    } else if (attrName == "bold" || attrName == "italic") {
        Qt::CheckState value = multiple ? Qt::PartiallyChecked : attrValue == "1" ? Qt::Checked : Qt::Unchecked;
        return new HOCRAttributeCheckbox(value, m_document, itemIndex, attrName, attrItemClass);
    } else {
        QLineEdit* lineEdit = new QLineEdit(attrValue);
        lineEdit->setFrame(false);
        lineEdit->setReadOnly(true);
        return lineEdit;
    }
}

void OutputEditorHOCR::updateCurrentItemBBox(QRect bbox)
{
    QModelIndex current = m_ui.treeViewHOCR->selectionModel()->currentIndex();
    QString bboxstr = QString("%1 %2 %3 %4").arg(bbox.left()).arg(bbox.top()).arg(bbox.right()).arg(bbox.bottom());
    m_document->editItemAttribute(current, "title:bbox", bboxstr);
}

void OutputEditorHOCR::updateSourceText()
{
    if (m_ui.tabWidgetProps->currentWidget() == m_ui.plainTextEditOutput) {
        QModelIndex current = m_ui.treeViewHOCR->selectionModel()->currentIndex();
        const HOCRItem* currentItem = m_document->itemAtIndex(current);
        if (currentItem) {
            m_ui.plainTextEditOutput->setPlainText(currentItem->toHtml());
        }
    }
}

void OutputEditorHOCR::itemAttributeChanged(const QModelIndex& itemIndex, const QString& name, const QString& /*value*/)
{
    const HOCRItem* currentItem = m_document->itemAtIndex(itemIndex);
    if (name == "title:bbox" && currentItem) {
        // Minimum bounding box
        QRect minBBox;
        if (currentItem->itemClass() == "ocr_page") {
            minBBox = currentItem->bbox();
        } else {
            for (const HOCRItem* child : currentItem->children()) {
                minBBox = minBBox.united(child->bbox());
            }
        }
        m_tool->setSelection(currentItem->bbox(), minBBox);
    }
}

void OutputEditorHOCR::bboxDrawn(const QRect& bbox, int action)
{
    QDomDocument doc;
    QModelIndex current = m_ui.treeViewHOCR->selectionModel()->currentIndex();
    const HOCRItem* currentItem = m_document->itemAtIndex(current);
    if (!currentItem) {
        return;
    }
    QMap<QString, QMap<QString, QSet<QString>>> propAttrs;
    currentItem->getPropagatableAttributes(propAttrs);
    QDomElement newElement;
    if (action == DisplayerToolHOCR::ACTION_DRAW_GRAPHIC_RECT) {
        newElement = doc.createElement("div");
        newElement.setAttribute("class", "ocr_graphic");
        newElement.setAttribute("title", QString("bbox %1 %2 %3 %4").arg(bbox.left()).arg(bbox.top()).arg(bbox.right()).arg(bbox.bottom()));
    } else if (action == DisplayerToolHOCR::ACTION_DRAW_CAREA_RECT) {
        newElement = doc.createElement("div");
        newElement.setAttribute("class", "ocr_carea");
        newElement.setAttribute("title", QString("bbox %1 %2 %3 %4").arg(bbox.left()).arg(bbox.top()).arg(bbox.right()).arg(bbox.bottom()));
    } else if (action == DisplayerToolHOCR::ACTION_DRAW_PAR_RECT) {
        newElement = doc.createElement("p");
        newElement.setAttribute("class", "ocr_par");
        newElement.setAttribute("title", QString("bbox %1 %2 %3 %4").arg(bbox.left()).arg(bbox.top()).arg(bbox.right()).arg(bbox.bottom()));
    } else if (action == DisplayerToolHOCR::ACTION_DRAW_LINE_RECT) {
        newElement = doc.createElement("span");
        newElement.setAttribute("class", "ocr_line");
        QSet<QString> propLineBaseline = propAttrs["ocrx_line"]["baseline"];
        // Tesseract does as follows:
        // row_height = x_height + ascenders - descenders
        // font_pt_size = row_height * 72 / dpi (72 = pointsPerInch)
        // As a first approximation, assume x_size = bbox.height() and ascenders = descenders = bbox.height() / 4
        QMap<QString, QString> titleAttrs;
        titleAttrs["bbox"] = QString("%1 %2 %3 %4").arg(bbox.left()).arg(bbox.top()).arg(bbox.right()).arg(bbox.bottom());
        titleAttrs["x_ascenders"] = QString("%1").arg(0.25 * bbox.height());
        titleAttrs["x_descenders"] = QString("%1").arg(0.25 * bbox.height());
        titleAttrs["x_size"] = QString("%1").arg(bbox.height());
        titleAttrs["baseline"] = propLineBaseline.size() == 1 ? *propLineBaseline.begin() : QString("0 0");
        newElement.setAttribute("title", HOCRItem::serializeAttrGroup(titleAttrs));
    } else if (action == DisplayerToolHOCR::ACTION_DRAW_WORD_RECT) {
        QString text = QInputDialog::getText(this, tr("Add Word"), tr("Enter word:"));
        if (text.isEmpty()) {
            return;
        }
        newElement = doc.createElement("span");
        newElement.setAttribute("class", "ocrx_word");
        QMap<QString, QSet<QString>> propWord = propAttrs["ocrx_word"];

        newElement.setAttribute("lang", propWord["lang"].size() == 1 ? *propWord["lang"].begin() : m_document->defaultLanguage());
        QMap<QString, QString> titleAttrs;
        titleAttrs["bbox"] = QString("%1 %2 %3 %4").arg(bbox.left()).arg(bbox.top()).arg(bbox.right()).arg(bbox.bottom());
        titleAttrs["x_wconf"] = "100";
        titleAttrs["x_font"] = propWord["title:x_font"].size() == 1 ?
                               *propWord["title:x_font"].begin() : QFont().family();
        titleAttrs["x_fsize"] = propWord["title:x_fsize"].size() == 1 ?
                                *propWord["title:x_fsize"].begin() : QString("%1").arg(qRound(bbox.height() * 72. / currentItem->page()->resolution()));
        newElement.setAttribute("title", HOCRItem::serializeAttrGroup(titleAttrs));
        if (propWord["bold"].size() == 1) { newElement.setAttribute("bold", *propWord["bold"].begin()); }
        if (propWord["italic"].size() == 1) { newElement.setAttribute("italic", *propWord["italic"].begin()); }
        newElement.appendChild(doc.createTextNode(text));
    } else {
        return;
    }
    QModelIndex index = m_document->addItem(current, newElement);
    if (index.isValid()) {
        m_ui.treeViewHOCR->selectionModel()->setCurrentIndex(index, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
    }
}

void OutputEditorHOCR::expandItemClass()
{
    expandCollapseItemClass(true);
}

void OutputEditorHOCR::collapseItemClass()
{
    expandCollapseItemClass(false);
}

void OutputEditorHOCR::showTreeWidgetContextMenu(const QPoint& point)
{
    QModelIndexList indices = m_ui.treeViewHOCR->selectionModel()->selectedRows();
    int nIndices = indices.size();
    if (nIndices > 1) {
        // Check if merging or swapping is allowed (items are valid siblings)
        const HOCRItem* firstItem = m_document->itemAtIndex(indices.first());
        if (!firstItem) {
            return;
        }
        QSet<QString> classes;
        classes.insert(firstItem->itemClass());
        QVector<int> rows = {indices.first().row()};
        for (int i = 1; i < nIndices; ++i) {
            const HOCRItem* item = m_document->itemAtIndex(indices[i]);
            if (!item || item->parent() != firstItem->parent()) {
                return;
            }
            classes.insert(item->itemClass());
            rows.append(indices[i].row());
        }

        QMenu menu;
        std::sort(rows.begin(), rows.end());
        bool consecutive = (rows.last() - rows.first()) == nIndices - 1;
        bool graphics = firstItem->itemClass() == "ocr_graphic";
        bool pages = firstItem->itemClass() == "ocr_page";
        bool sameClass = classes.size() == 1;

        QAction* actionMerge = nullptr;
        QAction* actionSplit = nullptr;
        QAction* actionSwap = nullptr;
        if (consecutive && !graphics && !pages && sameClass) { // Merging allowed
            actionMerge = menu.addAction(tr("Merge"));
            if (firstItem->itemClass() != "ocr_carea") {
                actionSplit = menu.addAction(tr("Split from parent"));
            }
        }
        if (nIndices == 2) { // Swapping allowed
            actionSwap = menu.addAction(tr("Swap"));
        }

        QAction* clickedAction = menu.exec(m_ui.treeViewHOCR->mapToGlobal(point));
        if (!clickedAction) {
            return;
        }
        m_ui.treeViewHOCR->selectionModel()->blockSignals(true);
        QModelIndex newIndex;
        if (clickedAction == actionMerge) {
            newIndex = m_document->mergeItems(indices.first().parent(), rows.first(), rows.last());
        } else if (clickedAction == actionSplit) {
            newIndex = m_document->splitItem(indices.first().parent(), rows.first(), rows.last());
            expandCollapseChildren(newIndex, true);
        } else if (clickedAction == actionSwap) {
            newIndex = m_document->swapItems(indices.first().parent(), rows.first(), rows.last());
        }
        if (newIndex.isValid()) {
            m_ui.treeViewHOCR->selectionModel()->blockSignals(true);
            m_ui.treeViewHOCR->selectionModel()->clear();
            m_ui.treeViewHOCR->selectionModel()->blockSignals(false);
            m_ui.treeViewHOCR->selectionModel()->setCurrentIndex(newIndex, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
        }
        m_ui.treeViewHOCR->selectionModel()->blockSignals(false);
        // Nothing else is allowed with multiple items selected
        return;
    }
    QModelIndex index = m_ui.treeViewHOCR->indexAt(point);
    const HOCRItem* item = m_document->itemAtIndex(index);
    if (!item) {
        return;
    }

    QMenu menu;
    QAction* actionAddGraphic = nullptr;
    QAction* actionAddCArea = nullptr;
    QAction* actionAddPar = nullptr;
    QAction* actionAddLine = nullptr;
    QAction* actionAddWord = nullptr;
    QAction* actionSplit = nullptr;
    QAction* actionRemove = nullptr;
    QAction* actionExpand = nullptr;
    QAction* actionCollapse = nullptr;
    QString itemClass = item->itemClass();
    if (itemClass == "ocr_page") {
        actionAddGraphic = menu.addAction(tr("Add graphic region"));
        actionAddCArea = menu.addAction(tr("Add text block"));
    } else if (itemClass == "ocr_carea") {
        actionAddPar = menu.addAction(tr("Add paragraph"));
    } else if (itemClass == "ocr_par") {
        actionAddLine = menu.addAction(tr("Add line"));
    } else if (itemClass == "ocr_line") {
        actionAddWord = menu.addAction(tr("Add word"));
    } else if (itemClass == "ocrx_word") {
        m_document->addSpellingActions(&menu, index);
    }
    if (!menu.actions().isEmpty()) {
        menu.addSeparator();
    }
    if (itemClass == "ocr_par" || itemClass == "ocr_line" || itemClass == "ocrx_word") {
        actionSplit = menu.addAction(tr("Split from parent"));
    }
    actionRemove = menu.addAction(tr("Remove"));
    actionRemove->setShortcut(QKeySequence(Qt::Key_Delete));
    if (m_document->rowCount(index) > 0) {
        actionExpand = menu.addAction(tr("Expand all"));
        actionCollapse = menu.addAction(tr("Collapse all"));
    }

    QAction* clickedAction = menu.exec(m_ui.treeViewHOCR->mapToGlobal(point));
    if (!clickedAction) {
        return;
    }
    if (clickedAction == actionAddGraphic) {
        m_tool->setAction(DisplayerToolHOCR::ACTION_DRAW_GRAPHIC_RECT);
    } else if (clickedAction == actionAddCArea) {
        m_tool->setAction(DisplayerToolHOCR::ACTION_DRAW_CAREA_RECT);
    } else if (clickedAction == actionAddPar) {
        m_tool->setAction(DisplayerToolHOCR::ACTION_DRAW_PAR_RECT);
    } else if (clickedAction == actionAddLine) {
        m_tool->setAction(DisplayerToolHOCR::ACTION_DRAW_LINE_RECT);
    } else if (clickedAction == actionAddWord) {
        m_tool->setAction(DisplayerToolHOCR::ACTION_DRAW_WORD_RECT);
    } else if (clickedAction == actionSplit) {
        QModelIndex newIndex = m_document->splitItem(index.parent(), index.row(), index.row());
        m_ui.treeViewHOCR->selectionModel()->setCurrentIndex(newIndex, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
        expandCollapseChildren(newIndex, true);
    } else if (clickedAction == actionRemove) {
        m_document->removeItem(m_ui.treeViewHOCR->selectionModel()->currentIndex());
    } else if (clickedAction == actionExpand) {
        expandCollapseChildren(index, true);
    } else if (clickedAction == actionCollapse) {
        expandCollapseChildren(index, false);
    }
}

void OutputEditorHOCR::pickItem(const QPoint& point)
{
    Q_UNUSED(point);

//    int pageNr;
//    QString filename = MAIN->displayer()->currentImage();
//    QModelIndex pageIndex = m_document->searchPage(filename, pageNr);
//    const HOCRItem* pageItem = m_document->itemAtIndex(pageIndex);
//    if (!pageItem) {
//        return;
//    }



//    const HOCRPage* page = pageItem->page();
//    // Transform point in coordinate space used when page was OCRed
//    double alpha = (page->angle() - MAIN->displayer()->currentAngle()) / 180. * M_PI;
//    double scale = double(page->resolution()) / double(MAIN->displayer()->currentResolution());
//    QPoint newPoint( scale * (point.x() * std::cos(alpha) - point.y() * std::sin(alpha)) + 0.5 * page->bbox().width(),
//                     scale * (point.x() * std::sin(alpha) + point.y() * std::cos(alpha)) + 0.5 * page->bbox().height());
//    QModelIndex index = m_document->searchAtCanvasPos(pageIndex, newPoint);
//    m_ui.treeViewHOCR->setCurrentIndex(index);
//    const HOCRItem* item = m_document->itemAtIndex(index);
//    if (item->itemClass() != "ocrx_word" && item->itemClass() != "ocr_line") {
//        m_ui.treeViewHOCR->setFocus();
//    }
}

void OutputEditorHOCR::toggleWConfColumn(bool active)
{
    m_ui.treeViewHOCR->setColumnHidden(1, !active);
}

//bool OutputEditorHOCR::open(InsertMode mode, QStringList files)
//{
////    if (mode == InsertMode::Replace && !clear(false)) {
////        return false;
////    }
//    if (files.isEmpty()) {
//        files = FileDialogs::openDialog(tr("Open hOCR File"), "", "outputdir", QString("%1 (*.html)").arg(tr("hOCR HTML Files")), true);
//    }
//    if (files.isEmpty()) {
//        return false;
//    }
//    int pos = mode == InsertMode::InsertBefore ? currentPage() : m_document->pageCount();
//    QStringList failed;
//    QStringList invalid;
//    int added = 0;
//    for (const QString& filename : files) {
//        QFile file(filename);
//        if (!file.open(QIODevice::ReadOnly)) {
//            failed.append(filename);
//            continue;
//        }
//        QDomDocument doc;
//        doc.setContent(&file);
//        QDomElement div = doc.firstChildElement("html").firstChildElement("body").firstChildElement("div");
//        if (div.isNull() || div.attribute("class") != "ocr_page") {
//            invalid.append(filename);
//            continue;
//        }
//        while (!div.isNull()) {
//            // Need to query next before adding page since the element is reparented
//            QDomElement nextDiv = div.nextSiblingElement("div");
//            m_document->insertPage(pos++, div, false, QFileInfo(filename).absolutePath());
//            div = nextDiv;
//            ++added;
//        }
//    }
//    if (added > 0) {
//        m_modified = mode != InsertMode::Replace;
//        if (mode == InsertMode::Replace && m_filebasename.isEmpty()) {
//            QFileInfo finfo(files.front());
//            m_filebasename = finfo.absoluteDir().absoluteFilePath(finfo.completeBaseName());
//        }
//        MAIN->setOutputPaneVisible(true);
//    }
//    QStringList errorMsg;
//    if (!failed.isEmpty()) {
//        errorMsg.append(tr("The following files could not be opened:\n%1").arg(failed.join("\n")));
//    }
//    if (!invalid.isEmpty()) {
//        errorMsg.append(tr("The following files are not valid hOCR HTML:\n%1").arg(invalid.join("\n")));
//    }
//    if (!errorMsg.isEmpty()) {
//        QMessageBox::critical(MAIN, tr("Unable to open files"), errorMsg.join("\n\n"));
//    }
//    return added > 0;
//}

//bool OutputEditorHOCR::selectPage(int nr)
//{
//    if (!m_document || nr >= m_document->pageCount()) {
//        return false;
//    }
//    QModelIndex index = m_document->indexAtItem(m_document->page(nr));
//    if (index.isValid()) {
//        m_ui.treeViewHOCR->setCurrentIndex(index);
//    }
//    return index.isValid();
//}

//bool OutputEditorHOCR::save(const QString& filename)
//{
//    ui.treeViewHOCR->setFocus(); // Ensure any item editor loses focus and commits its changes
//    QString outname = filename;
//    if (outname.isEmpty()) {
//        QString suggestion = m_filebasename;
//        if (suggestion.isEmpty()) {
//            auto page = MAIN->sourceManager()->selectedPage();
//            if (page) {
//                QFileInfo finfo(page->path);
//                suggestion = finfo.absoluteDir().absoluteFilePath(finfo.completeBaseName());
//            } else {
//                suggestion = tr("output");
//            }
//        }
//        outname = FileDialogs::saveDialog(tr("Save hOCR Output..."), suggestion + ".html", "outputdir", QString("%1 (*.html)").arg(tr("hOCR HTML Files")));
//        if (outname.isEmpty()) {
//            return false;
//        }
//    }
//    QFile file(outname);
//    if (!file.open(QIODevice::WriteOnly)) {
//        QMessageBox::critical(MAIN, tr("Failed to save output"), tr("Check that you have writing permissions in the selected folder."));
//        return false;
//    }

//    OcrEngineFactory* factory = OcrEngine::selected();

//    QString header = QString(
//                         "<!DOCTYPE html>\n"
//                         "<html>\n"
//                         "<head>\n"
//                         " <title>%1</title>\n"
//                         " <meta charset=\"utf-8\" /> \n"
//                         " <meta name='ocr-system' content='%2 %3' />\n"
//                         " <meta name='ocr-capabilities' content='ocr_page ocr_carea ocr_par ocr_line ocrx_word'/>\n"
//                         "</head>\n").arg(QFileInfo(outname).fileName())
//                     .arg(factory->shortName())
//                     .arg(factory->version());
//    file.write(header.toUtf8());
//    m_document->convertSourcePaths(QFileInfo(outname).absolutePath(), false);
//    file.write(m_document->toHTML().toUtf8());
//    m_document->convertSourcePaths(QFileInfo(outname).absolutePath(), true);
//    file.write("</html>\n");
//    m_modified = false;
//    QFileInfo finfo(outname);
//    m_filebasename = finfo.absoluteDir().absoluteFilePath(finfo.completeBaseName());
//    return true;
//}

//bool OutputEditorHOCR::exportToODT()
//{
//    QString suggestion = m_filebasename;
//    if (suggestion.isEmpty()) {
//        auto page = MAIN->sourceManager()->selectedPage();
//        suggestion = page ? QFileInfo(page->displayName).baseName() : tr("output");
//    }

//    QString outname = FileDialogs::saveDialog(tr("Save ODT Output..."), suggestion + ".odt", "outputdir", QString("%1 (*.odt)").arg(tr("OpenDocument Text Documents")));
//    if (outname.isEmpty()) {
//        return false;
//    }

//    m_ui.treeViewHOCR->setFocus(); // Ensure any item editor loses focus and commits its changes
//    MAIN->displayer()->setBlockAutoscale(true);

//    if (OcrEngine::supportedExportFormats() & OcrEngineFactory::ODT) {
//        return MAIN->recognizer()->exportAs(OcrEngineFactory::ODT, outname);
//    }

//    bool success = HOCROdtExporter().run(m_document, outname);
//    MAIN->displayer()->setBlockAutoscale(false);
//    return success;
//}

//bool OutputEditorHOCR::exportToPDF()
//{
//    m_ui.treeViewHOCR->setFocus(); // Ensure any item editor loses focus and commits its changes
//    m_ui.actionPreview->setChecked(false); // Disable preview because if conflicts with preview from PDF dialog
//    QModelIndex current = m_ui.treeViewHOCR->selectionModel()->currentIndex();
//    const HOCRItem* item = m_document->itemAtIndex(current);
//    const HOCRPage* page = item ? item->page() : m_document->page(0);
//    bool success = false;
//    if (!showPage(page)) {
//        return false;
//    }

//    QString suggestion = m_filebasename;
//    if (suggestion.isEmpty()) {
//        auto page = MAIN->sourceManager()->selectedPage();
//        suggestion = page ? QFileInfo(page->displayName).baseName() : tr("output");
//    }

//    if (OcrEngine::supportedExportFormats() & OcrEngineFactory::PDF) {
//        QString outname = FileDialogs::saveDialog(tr("Save PDF Output..."), suggestion + ".pdf", "outputdir", QString("%1 (*.pdf)").arg(tr("PDF Files")));
//        if (outname.isEmpty()) {
//            return false;
//        }

//        return MAIN->recognizer()->exportAs(OcrEngineFactory::PDF, outname);
//    }

//    HOCRPdfExportDialog dialog(m_tool, m_document, page, MAIN);
//    if (dialog.exec() != QDialog::Accepted) {
//        return false;
//    }
//    HOCRPdfExporter::PDFSettings settings = dialog.getPdfSettings();

//    QString outname;
//    while (true) {
//        outname = FileDialogs::saveDialog(tr("Save PDF Output..."), suggestion + ".pdf", "outputdir", QString("%1 (*.pdf)").arg(tr("PDF Files")));
//        if (outname.isEmpty()) {
//            break;
//        }
//        if (m_document->referencesSource(outname)) {
//            QMessageBox::warning(MAIN, tr("Invalid Output"), tr("Cannot overwrite a file which is a source image of this document."));
//            continue;
//        }
//        break;
//    }
//    if (outname.isEmpty()) {
//        return false;
//    }

//    MAIN->displayer()->setBlockAutoscale(true);
//    success = HOCRPdfExporter().run(m_document, outname, &settings);
//    MAIN->displayer()->setBlockAutoscale(false);
//    return success;
//}

//bool OutputEditorHOCR::exportToText()
//{
//    QString suggestion = m_filebasename;
//    if (suggestion.isEmpty()) {
//        auto page = MAIN->sourceManager()->selectedPage();
//        suggestion = page ? QFileInfo(page->displayName).baseName() : tr("output");
//    }

//    QString outname = FileDialogs::saveDialog(tr("Save Text Output..."), suggestion + ".txt", "outputdir", QString("%1 (*.txt)").arg(tr("Text Files")));
//    if (outname.isEmpty()) {
//        return false;
//    }

//    m_ui.treeViewHOCR->setFocus(); // Ensure any item editor loses focus and commits its changes
//    MAIN->displayer()->setBlockAutoscale(true);
//    bool success = HOCRTextExporter().run(m_document, outname);
//    MAIN->displayer()->setBlockAutoscale(false);
//    return success;
//}

//bool OutputEditorHOCR::exportToDOCX()
//{
//    QString suggestion = m_filebasename;
//    if (suggestion.isEmpty()) {
//        auto page = MAIN->sourceManager()->selectedPage();
//        suggestion = page ? QFileInfo(page->displayName).baseName() : tr("output");
//    }

//    QString outname = FileDialogs::saveDialog(tr("Save DOCX Output..."), suggestion + ".docx", "outputdir", QString("%1 (*.docx)").arg(tr("Microsoft Word Open Documents")));
//    if (outname.isEmpty()) {
//        return false;
//    }

//    return MAIN->recognizer()->exportAs(OcrEngineFactory::DOCX, outname);
//}

//bool OutputEditorHOCR::clear(bool hide)
//{
//    m_previewTimer.stop();
//    m_preview->setVisible(false);
//    if (!m_widget->isVisible()) {
//        return true;
//    }
//    /*if (isModified()) {
//        int response = QMessageBox::question(MAIN, tr("Output not saved"), tr("Save output before proceeding?"), QMessageBox::Save, QMessageBox::Discard, QMessageBox::Cancel);
//        if (response == QMessageBox::Save) {
//            if (!save()) {
//                return false;
//            }
//        } else if (response != QMessageBox::Discard) {
//            return false;
//        }
//    }*/
//    m_proofReadWidget->clear();
//    m_document->clear();
//    ui.tableWidgetProperties->setRowCount(0);
//    ui.plainTextEditOutput->clear();
//    m_tool->clearSelection();
//    m_modified = false;
//    m_filebasename.clear();
//    if (hide) {
//        MAIN->setOutputPaneVisible(false);
//    }
//    return true;
//}

void OutputEditorHOCR::setLanguage(const QString& lang)
{
    m_document->setDefaultLanguage(lang);
}

//void OutputEditorHOCR::onVisibilityChanged(bool /*visible*/)
//{
//    ui.searchFrame->hideSubstitutionsManager();
//}

bool OutputEditorHOCR::findReplaceInItem(const QModelIndex& index, const QString& searchstr, const QString& replacestr, bool matchCase, bool backwards, bool replace, bool& currentSelectionMatchesSearch)
{
    // Check that the item is a word
    const HOCRItem* item = m_document->itemAtIndex(index);
    if (!item || item->itemClass() != "ocrx_word") {
        return false;
    }
    Qt::CaseSensitivity cs = matchCase ? Qt::CaseSensitive : Qt::CaseInsensitive;
    HOCRTextDelegate* delegate = static_cast<HOCRTextDelegate*>(m_ui.treeViewHOCR->itemDelegateForColumn(0));
    // If the item is already in edit mode, continue searching inside the text
    if (delegate->getCurrentIndex() == index && delegate->getCurrentEditor()) {
        QLineEdit* editor = delegate->getCurrentEditor();
        bool matchesSearch = editor->selectedText().compare(searchstr, cs) == 0;
        int selStart = editor->selectionStart();
        if (matchesSearch && replace) {
            QString oldText = editor->text();
            editor->setText(oldText.left(selStart) + replacestr + oldText.mid(selStart + searchstr.length()));
            editor->setSelection(selStart, replacestr.length());
            return true;
        }
        bool matchesReplace = editor->selectedText().compare(replacestr, cs) == 0;
        int pos = -1;
        if (backwards) {
            pos = selStart - 1;
            pos = pos < 0 ? -1 : editor->text().lastIndexOf(searchstr, pos, cs);
        } else {
            pos = matchesSearch ? selStart + searchstr.length() : matchesReplace ? selStart + replacestr.length() : selStart;
            pos = editor->text().indexOf(searchstr, pos, cs);
        }
        if (pos != -1) {
            editor->setSelection(pos, searchstr.length());
            return true;
        }
        currentSelectionMatchesSearch = matchesSearch;
        return false;
    }
    // Otherwise, if item contains text, set it in edit mode
    int pos = backwards ? item->text().lastIndexOf(searchstr, -1, cs) : item->text().indexOf(searchstr, 0, cs);
    if (pos != -1) {
        m_ui.treeViewHOCR->setCurrentIndex(index);
        m_ui.treeViewHOCR->edit(index);
        Q_ASSERT(delegate->getCurrentIndex() == index && delegate->getCurrentEditor());
        delegate->getCurrentEditor()->setSelection(pos, searchstr.length());
        return true;
    }
    return false;
}

void OutputEditorHOCR::findReplace(const QString& searchstr, const QString& replacestr, bool matchCase, bool backwards, bool replace)
{
    m_ui.searchFrame->clearErrorState();
    QModelIndex current = m_ui.treeViewHOCR->currentIndex();
    if (!current.isValid()) {
        current = m_document->index(backwards ? (m_document->rowCount() - 1) : 0, 0);
    }
    QModelIndex neww = current;
    bool currentSelectionMatchesSearch = false;
    while (!findReplaceInItem(neww, searchstr, replacestr, matchCase, backwards, replace, currentSelectionMatchesSearch)) {
        neww = backwards ? m_document->prevIndex(neww) : m_document->nextIndex(neww);
        if (!neww.isValid() || neww == current) {
            // Break endless loop
            if (!currentSelectionMatchesSearch) {
                m_ui.searchFrame->setErrorState();
            }
            return;
        }
    }
}

void OutputEditorHOCR::replaceAll(const QString& searchstr, const QString& replacestr, bool matchCase)
{
    MAIN->pushState(MainWindow::State::Busy, tr("Replacing..."));
    QModelIndex start = m_document->index(0, 0);
    QModelIndex curr = start;
    Qt::CaseSensitivity cs = matchCase ? Qt::CaseSensitive : Qt::CaseInsensitive;
    int count = 0;
    do {
        const HOCRItem* item = m_document->itemAtIndex(curr);
        if (item && item->itemClass() == "ocrx_word") {
            if (item->text().contains(searchstr, cs)) {
                ++count;
                m_document->setData(curr, item->text().replace(searchstr, replacestr, cs), Qt::EditRole);
            }
        }
        curr = m_document->nextIndex(curr);
        QApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
    } while (curr != start);
    if (count == 0) {
        m_ui.searchFrame->setErrorState();
    }
    MAIN->popState();
}

void OutputEditorHOCR::applySubstitutions(const QMap<QString, QString>& substitutions, bool matchCase)
{
    MAIN->pushState(MainWindow::State::Busy, tr("Applying substitutions..."));
    QModelIndex start = m_document->index(0, 0);
    Qt::CaseSensitivity cs = matchCase ? Qt::CaseSensitive : Qt::CaseInsensitive;
    for (auto it = substitutions.begin(), itEnd = substitutions.end(); it != itEnd; ++it) {
        QString search = it.key();
        QString replace = it.value();
        QModelIndex curr = start;
        do {
            const HOCRItem* item = m_document->itemAtIndex(curr);
            if (item && item->itemClass() == "ocrx_word") {
                m_document->setData(curr, item->text().replace(search, replace, cs), Qt::EditRole);
            }
            curr = m_document->nextIndex(curr);
            QApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
        } while (curr != start);
    }
    MAIN->popState();
}

void OutputEditorHOCR::removeItem()
{
    m_document->removeItem(m_ui.treeViewHOCR->selectionModel()->currentIndex());
}

void OutputEditorHOCR::sourceChanged()
{
//    if (m_blockSourceChanged) {
//        return;
//    }
//    int page;
//    QString path = MAIN->displayer()->currentImage(&page);
//    // Check if source is in document tree
//    QModelIndex pageIndex = m_document->searchPage(path, page);
//    if (!pageIndex.isValid()) {
//        m_ui.actionPreview->setChecked(false);
//        m_ui.treeViewHOCR->setCurrentIndex(QModelIndex());
//    } else {
//        QModelIndex curIndex = m_ui.treeViewHOCR->currentIndex();
//        while (curIndex != pageIndex && curIndex.parent().isValid()) {
//            curIndex = curIndex.parent();
//        }
//        if (curIndex != pageIndex) {
//            m_ui.treeViewHOCR->setCurrentIndex(pageIndex);
//        }
//    }
}

void OutputEditorHOCR::previewToggled(bool active)
{
    QModelIndex index = m_ui.treeViewHOCR->currentIndex();
    if (active && !index.isValid() && m_document->pageCount() > 0) {
        m_ui.treeViewHOCR->setCurrentIndex(m_document->indexAtItem(m_document->page(0)));
    } else {
        updatePreview();
    }
}

void OutputEditorHOCR::updatePreview()
{
    const HOCRItem* item = m_document->itemAtIndex(m_ui.treeViewHOCR->currentIndex());
    if (!m_ui.actionPreview->isChecked() || !item) {
        m_preview->setVisible(false);
        return;
    }

    const HOCRPage* page = item->page();
    const QRect& bbox = page->bbox();
    int pageDpi = page->resolution();

    QImage image(bbox.size(), QImage::Format_ARGB32);
    image.fill(QColor(255, 255, 255, 127));
    image.setDotsPerMeterX(pageDpi / 0.0254); // 1 in = 0.0254 m
    image.setDotsPerMeterY(pageDpi / 0.0254);
    QPainter painter(&image);
    painter.setRenderHint(QPainter::Antialiasing);

    drawPreview(painter, page);

    m_preview->setPixmap(QPixmap::fromImage(image));
    m_preview->setPos(-0.5 * bbox.width(), -0.5 * bbox.height());
    m_preview->setVisible(true);
}

void OutputEditorHOCR::drawPreview(QPainter& painter, const HOCRItem* item)
{
    if (!item->isEnabled()) {
        return;
    }
    QString itemClass = item->itemClass();
    if (itemClass == "ocr_line") {
        QPair<double, double> baseline = item->baseLine();
        const QRect& lineRect = item->bbox();
        for (HOCRItem* wordItem : item->children()) {
            if (!wordItem->isEnabled()) {
                continue;
            }
            const QRect& wordRect = wordItem->bbox();
            QFont font;
            if (!wordItem->fontFamily().isEmpty()) {
                font.setFamily(wordItem->fontFamily());
            }
            font.setBold(wordItem->fontBold());
            font.setItalic(wordItem->fontItalic());
            font.setPointSizeF(wordItem->fontSize());
            painter.setFont(font);
            // See https://github.com/kba/hocr-spec/issues/15
            double y = lineRect.bottom() + (wordRect.center().x() - lineRect.x()) * baseline.first + baseline.second;
            painter.drawText(wordRect.x(), y, wordItem->text());
        }
    } else if (itemClass == "ocr_graphic") {
        painter.drawImage(item->bbox(), m_tool->getSelection(item->bbox()));
    } else {
        for (HOCRItem* childItem : item->children()) {
            drawPreview(painter, childItem);;
        }
    }
}
