/*

Copyright 2014 S. Razi Alavizadeh
Copyright 2012-2018 Adam Reichold
Copyright 2018 Pavel Sanda
Copyright 2014 Dorian Scholz

This file is part of qpdfview.

qpdfview 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 2 of the License, or
(at your option) any later version.

qpdfview 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 qpdfview.  If not, see <http://www.gnu.org/licenses/>.

*/

#include "miscellaneous.h"

#include <QApplication>
#include <QDebug>
#include <QDialog>
#include <QDialogButtonBox>
#include <QLabel>
#include <QMenu>
#include <QMouseEvent>
#include <QProcess>
#include <QScrollBar>
#include <QTimer>
#include <QToolTip>
#include <QVBoxLayout>

#include "searchmodel.h"
#include "compatibility.h"

namespace qpdfview
{

namespace
{

inline bool isPrintable(const QString& string)
{
    foreach(const QChar& character, string)
    {
        if(!character.isPrint())
        {
            return false;
        }
    }

    return true;
}

inline QModelIndex firstIndex(const QModelIndexList& indexes)
{
    return !indexes.isEmpty() ? indexes.first() : QModelIndex();
}

} // anonymous

GraphicsCompositionModeEffect::GraphicsCompositionModeEffect(QPainter::CompositionMode compositionMode, QObject* parent) : QGraphicsEffect(parent),
    m_compositionMode(compositionMode)
{
}

void GraphicsCompositionModeEffect::draw(QPainter* painter)
{
    painter->save();

    painter->setCompositionMode(m_compositionMode);

    drawSource(painter);

    painter->restore();
}

ProxyStyle::ProxyStyle() : QProxyStyle(),
    m_scrollableMenus(false)
{
}

bool ProxyStyle::scrollableMenus() const
{
    return m_scrollableMenus;
}

void ProxyStyle::setScrollableMenus(bool scrollableMenus)
{
    m_scrollableMenus = scrollableMenus;
}

int ProxyStyle::styleHint(StyleHint hint, const QStyleOption* option, const QWidget* widget, QStyleHintReturn* returnData) const
{
    if(m_scrollableMenus && hint == QStyle::SH_Menu_Scrollable)
    {
        return 1;
    }

    return QProxyStyle::styleHint(hint, option, widget, returnData);
}

ToolTipMenu::ToolTipMenu(QWidget* parent) : QMenu(parent)
{
}

ToolTipMenu::ToolTipMenu(const QString& title, QWidget* parent) : QMenu(title, parent)
{
}

bool ToolTipMenu::event(QEvent* event)
{
    const QAction* const action = activeAction();

    if(event->type() == QEvent::ToolTip && action != 0 && !action->data().isNull())
    {
        QToolTip::showText(static_cast< QHelpEvent* >(event)->globalPos(), action->toolTip());
    }
    else
    {
        QToolTip::hideText();
    }

    return QMenu::event(event);
}

SearchableMenu::SearchableMenu(const QString& title, QWidget* parent) : ToolTipMenu(title, parent),
    m_searchable(false),
    m_text()
{
}

bool SearchableMenu::isSearchable() const
{
    return m_searchable;
}

void SearchableMenu::setSearchable(bool searchable)
{
    m_searchable = searchable;
}

void SearchableMenu::hideEvent(QHideEvent* event)
{
    QMenu::hideEvent(event);

    if(m_searchable && !event->spontaneous())
    {
        m_text = QString();

        foreach(QAction* action, actions())
        {
            action->setVisible(true);
        }
    }
}

void SearchableMenu::keyPressEvent(QKeyEvent* event)
{
    if(!m_searchable)
    {
        QMenu::keyPressEvent(event);
        return;
    }

    const QString text = event->text();

    if(!text.isEmpty() && isPrintable(text))
    {
        m_text.append(text);
    }
    else if(event->key() == Qt::Key_Backspace || event->key() == Qt::Key_Delete)
    {
        m_text.chop(1);
    }
    else
    {
        QMenu::keyPressEvent(event);
        return;
    }

    QAction* firstVisibleAction = 0;

    foreach(QAction* action, actions())
    {
        if(action->data().isNull()) // Modify only flagged actions
        {
            continue;
        }

        const bool visible = action->text().contains(m_text, Qt::CaseInsensitive);

        action->setVisible(visible);

        if(visible && firstVisibleAction == 0)
        {
            firstVisibleAction = action;
        }
    }

    setActiveAction(firstVisibleAction);

    QToolTip::showText(mapToGlobal(rect().topLeft()), tr("Search for '%1'...").arg(m_text), this);
}

TabBar::TabBar(QWidget* parent) : QTabBar(parent),
    m_dragIndex(-1)
{
}

QSize TabBar::tabSizeHint(int index) const
{
    QSize size = QTabBar::tabSizeHint(index);

    const TabWidget* tabWidget = qobject_cast< TabWidget* >(parentWidget());

    if(tabWidget != 0 && tabWidget->spreadTabs())
    {
        switch(tabWidget->tabPosition())
        {
        default:
        case QTabWidget::North:
        case QTabWidget::South:
            size.setWidth(qMax(width() / count(), size.width()));
            break;
        case QTabWidget::East:
        case QTabWidget::West:
            size.setHeight(qMax(height() / count(), size.height()));
            break;
        }
    }

    return size;
}

void TabBar::mousePressEvent(QMouseEvent* event)
{
    if(event->button() == Qt::MiddleButton)
    {
        const int index = tabAt(event->pos());

        if(index != -1)
        {
            emit tabCloseRequested(index);

            event->accept();
            return;
        }
    }
    else if(event->modifiers() == Qt::ShiftModifier && event->button() == Qt::LeftButton)
    {
        const int index = tabAt(event->pos());

        if(index != -1)
        {
            m_dragIndex = index;
            m_dragPos = event->pos();

            event->accept();
            return;
        }
    }

    QTabBar::mousePressEvent(event);
}

void TabBar::mouseMoveEvent(QMouseEvent* event)
{
    QTabBar::mouseMoveEvent(event);

    if(m_dragIndex != -1)
    {
        if((event->pos() - m_dragPos).manhattanLength() >= QApplication::startDragDistance())
        {
            emit tabDragRequested(m_dragIndex);

            m_dragIndex = -1;
        }
    }
}

void TabBar::mouseReleaseEvent(QMouseEvent* event)
{
    QTabBar::mouseReleaseEvent(event);

    m_dragIndex = -1;
}

TabWidget::TabWidget(QWidget* parent) : QTabWidget(parent),
    m_tabBarPolicy(TabBarAsNeeded),
    m_spreadTabs(false)
{
    TabBar* tabBar = new TabBar(this);

    tabBar->setContextMenuPolicy(Qt::CustomContextMenu);

    connect(tabBar, SIGNAL(tabDragRequested(int)), SIGNAL(tabDragRequested(int)));
    connect(tabBar, SIGNAL(customContextMenuRequested(QPoint)), SLOT(on_tabBar_customContextMenuRequested(QPoint)));

    setTabBar(tabBar);
}

int TabWidget::addTab(QWidget* const widget, const bool nextToCurrent,
                      const QString& label, const QString& toolTip)
{
    const int index = nextToCurrent
            ? insertTab(currentIndex() + 1, widget, label)
            : QTabWidget::addTab(widget, label);

    setTabToolTip(index, toolTip);
    setCurrentIndex(index);

    return index;
}

TabWidget::TabBarPolicy TabWidget::tabBarPolicy() const
{
    return m_tabBarPolicy;
}

void TabWidget::setTabBarPolicy(TabWidget::TabBarPolicy tabBarPolicy)
{
    m_tabBarPolicy = tabBarPolicy;

    switch(m_tabBarPolicy)
    {
    case TabBarAsNeeded:
        tabBar()->setVisible(count() > 1);
        break;
    case TabBarAlwaysOn:
        tabBar()->setVisible(true);
        break;
    case TabBarAlwaysOff:
        tabBar()->setVisible(false);
        break;
    }
}

bool TabWidget::spreadTabs() const
{
    return m_spreadTabs;
}

void TabWidget::setSpreadTabs(bool spreadTabs)
{
    if(m_spreadTabs != spreadTabs)
    {
        m_spreadTabs = spreadTabs;

        QResizeEvent resizeEvent(tabBar()->size(), tabBar()->size());
        QApplication::sendEvent(tabBar(), &resizeEvent);
    }
}

void TabWidget::previousTab()
{
    int index = currentIndex() - 1;

    if(index < 0)
    {
        index = count() - 1;
    }

    setCurrentIndex(index);
}

void TabWidget::nextTab()
{
    int index = currentIndex() + 1;

    if(index >= count())
    {
        index = 0;
    }

    setCurrentIndex(index);
}

void TabWidget::on_tabBar_customContextMenuRequested(QPoint pos)
{
    const int index = tabBar()->tabAt(pos);

    if(index != -1)
    {
        emit tabContextMenuRequested(tabBar()->mapToGlobal(pos), tabBar()->tabAt(pos));
    }
}

void TabWidget::tabInserted(int index)
{
    QTabWidget::tabInserted(index);

    if(m_tabBarPolicy == TabBarAsNeeded)
    {
        tabBar()->setVisible(count() > 1);
    }
}

void TabWidget::tabRemoved(int index)
{
    QTabWidget::tabRemoved(index);

    if(m_tabBarPolicy == TabBarAsNeeded)
    {
        tabBar()->setVisible(count() > 1);
    }
}

TreeView::TreeView(int expansionRole, QWidget* parent) : QTreeView(parent),
    m_expansionRole(expansionRole)
{
    connect(this, SIGNAL(expanded(QModelIndex)), SLOT(on_expanded(QModelIndex)));
    connect(this, SIGNAL(collapsed(QModelIndex)), SLOT(on_collapsed(QModelIndex)));
}

void TreeView::expandAbove(const QModelIndex& child)
{
    for(QModelIndex index = child.parent(); index.isValid(); index = index.parent())
    {
        expand(index);
    }
}

void TreeView::expandAll(const QModelIndex& index)
{
    if(index.isValid())
    {
        if(!isExpanded(index))
        {
            expand(index);
        }

        const QAbstractItemModel* const model = this->model();

        for(int row = 0, rowCount = model->rowCount(index); row < rowCount; ++row)
        {
            expandAll(model->index(row, 0, index));
        }
    }
    else
    {
        QTreeView::expandAll();
    }
}

void TreeView::collapseAll(const QModelIndex& index)
{
    if(index.isValid())
    {
        if(isExpanded(index))
        {
            collapse(index);
        }

        const QAbstractItemModel* const model = this->model();

        for(int row = 0, rowCount = model->rowCount(index); row < rowCount; ++row)
        {
            collapseAll(model->index(row, 0, index));
        }
    }
    else
    {
        QTreeView::collapseAll();
    }
}

int TreeView::expandedDepth(const QModelIndex& index)
{
    const QAbstractItemModel* const model = this->model();

    if(index.isValid())
    {
        if(!isExpanded(index) || !model->hasChildren(index))
        {
            return 0;
        }

        int depth = 0;

        for(int row = 0, rowCount = model->rowCount(index); row < rowCount; ++row)
        {
            depth = qMax(depth, expandedDepth(model->index(row, 0, index)));
        }

        return 1 + depth;
    }
    else
    {
        int depth = 0;

        for(int row = 0, rowCount = model->rowCount(); row < rowCount; ++row)
        {
            depth = qMax(depth, expandedDepth(model->index(row, 0)));
        }

        return depth;
    }
}

void TreeView::expandToDepth(const QModelIndex& index, int depth)
{
    const QAbstractItemModel* const model = this->model();

    if(index.isValid())
    {
        if(depth > 0)
        {
            if(!isExpanded(index))
            {
                expand(index);
            }
        }

        if(depth > 1)
        {
            for(int row = 0, rowCount = model->rowCount(index); row < rowCount; ++row)
            {
                expandToDepth(model->index(row, 0, index), depth - 1);
            }
        }
    }
    else
    {
        for(int row = 0, rowCount = model->rowCount(); row < rowCount; ++row)
        {
            expandToDepth(model->index(row, 0), depth);
        }
    }
}

void TreeView::collapseFromDepth(const QModelIndex& index, int depth)
{
    const QAbstractItemModel* const model = this->model();

    if(index.isValid())
    {
        if(depth <= 0)
        {
            if(isExpanded(index))
            {
                collapse(index);
            }
        }

        for(int row = 0, rowCount = model->rowCount(index); row < rowCount; ++row)
        {
            collapseFromDepth(model->index(row, 0, index), depth - 1);
        }
    }
    else
    {
        for(int row = 0, rowCount = model->rowCount(); row < rowCount; ++row)
        {
            collapseFromDepth(model->index(row, 0), depth);
        }
    }
}

void TreeView::restoreExpansion(const QModelIndex& index)
{
    const QAbstractItemModel* const model = this->model();

    if(index.isValid())
    {
        const bool expanded = index.data(m_expansionRole).toBool();

        if(isExpanded(index) != expanded)
        {
            setExpanded(index, expanded);
        }
    }

    for(int row = 0, rowCount = model->rowCount(index); row < rowCount; ++row)
    {
        restoreExpansion(model->index(row, 0, index));
    }
}

void TreeView::keyPressEvent(QKeyEvent* event)
{
    const bool verticalKeys = event->key() == Qt::Key_Up || event->key() == Qt::Key_Down;
    const bool horizontalKeys = event->key() == Qt::Key_Left || event->key() == Qt::Key_Right;

    const QModelIndex selection = firstIndex(selectedIndexes());

    // If Shift is pressed, the view is scrolled up or down.
    if(event->modifiers().testFlag(Qt::ShiftModifier) && verticalKeys)
    {
        QScrollBar* scrollBar = verticalScrollBar();

        if(event->key() == Qt::Key_Up && scrollBar->value() > scrollBar->minimum())
        {
            scrollBar->triggerAction(QAbstractSlider::SliderSingleStepSub);

            event->accept();
            return;
        }
        else if(event->key() == Qt::Key_Down && scrollBar->value() < scrollBar->maximum())
        {
            scrollBar->triggerAction(QAbstractSlider::SliderSingleStepAdd);

            event->accept();
            return;
        }
    }

    // If Control is pressed, all children of the selected item are expanded or collapsed.
    if(event->modifiers().testFlag(Qt::ControlModifier) && horizontalKeys)
    {
        if(event->key() == Qt::Key_Left)
        {
            collapseAll(selection);
        }
        else if(event->key() == Qt::Key_Right)
        {
            expandAll(selection);
        }

        event->accept();
        return;
    }

    // If Shift is pressed, one level of children of the selected item are expanded or collapsed.
    if(event->modifiers().testFlag(Qt::ShiftModifier) && horizontalKeys)
    {
        const int depth = expandedDepth(selection);

        if(event->key() == Qt::Key_Left)
        {
            collapseFromDepth(selection, depth - 1);
        }
        else if(event->key() == Qt::Key_Right)
        {
            expandToDepth(selection, depth + 1);
        }

        event->accept();
        return;
    }

    QTreeView::keyPressEvent(event);
}

void TreeView::wheelEvent(QWheelEvent* event)
{
    const QModelIndex selection = firstIndex(selectedIndexes());

    // If Control is pressed, expand or collapse the selected entry.
    if(event->modifiers().testFlag(Qt::ControlModifier) && selection.isValid())
    {
        if(rotatedForward(event))
        {
            collapse(selection);
        }
        else
        {
            expand(selection);
        }

        // Fall through when Shift is also pressed.
        if(!event->modifiers().testFlag(Qt::ShiftModifier))
        {
            event->accept();
            return;
        }
    }

    // If Shift is pressed, move the selected entry up and down.
    if(event->modifiers().testFlag(Qt::ShiftModifier) && selection.isValid())
    {
        QModelIndex sibling;

        if(rotatedForward(event))
        {
            sibling = indexAbove(selection);
        }
        else
        {
            sibling = indexBelow(selection);
        }

        if(sibling.isValid())
        {
            setCurrentIndex(sibling);
        }

        event->accept();
        return;
    }

    QTreeView::wheelEvent(event);
}

void TreeView::contextMenuEvent(QContextMenuEvent* event)
{
    QTreeView::contextMenuEvent(event);

    if(!event->isAccepted())
    {
        QMenu menu;

        const QAction* expandAllAction = menu.addAction(tr("&Expand all"));
        const QAction* collapseAllAction = menu.addAction(tr("&Collapse all"));

        const QAction* action = menu.exec(event->globalPos());

        if(action == expandAllAction)
        {
            expandAll(indexAt(event->pos()));
        }
        else if(action == collapseAllAction)
        {
            collapseAll(indexAt(event->pos()));
        }
    }
}

void TreeView::on_expanded(const QModelIndex& index)
{
    model()->setData(index, true, m_expansionRole);
}

void TreeView::on_collapsed(const QModelIndex& index)
{
    model()->setData(index, false, m_expansionRole);
}

LineEdit::LineEdit(QWidget* parent) : QLineEdit(parent)
{
}

void LineEdit::mousePressEvent(QMouseEvent* event)
{
    QLineEdit::mousePressEvent(event);

    selectAll();
}

ComboBox::ComboBox(QWidget* parent) : QComboBox(parent)
{
    setLineEdit(new LineEdit(this));
}

SpinBox::SpinBox(QWidget* parent) : QSpinBox(parent)
{
    setLineEdit(new LineEdit(this));
}

void SpinBox::keyPressEvent(QKeyEvent* event)
{
    QSpinBox::keyPressEvent(event);

    if(event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter)
    {
        emit returnPressed();
    }
}

MappingSpinBox::MappingSpinBox(TextValueMapper* mapper, QWidget* parent) : SpinBox(parent),
    m_mapper(mapper)
{
}

QString MappingSpinBox::textFromValue(int val) const
{
    bool ok = false;
    QString text = m_mapper->textFromValue(val, ok);

    if(!ok)
    {
        text = SpinBox::textFromValue(val);
    }

    return text;
}

int MappingSpinBox::valueFromText(const QString& text) const
{
    bool ok = false;
    int value = m_mapper->valueFromText(text, ok);

    if(!ok)
    {
        value = SpinBox::valueFromText(text);
    }

    return value;
}

QValidator::State MappingSpinBox::validate(QString& input, int& pos) const
{
    Q_UNUSED(input);
    Q_UNUSED(pos);

    return QValidator::Acceptable;
}

int getMappedNumber(MappingSpinBox::TextValueMapper* mapper,
                    QWidget* parent, const QString& title, const QString& caption,
                    int value, int min, int max, bool* ok, Qt::WindowFlags flags)
{
    QDialog* dialog = new QDialog(parent, flags | Qt::MSWindowsFixedSizeDialogHint);
    dialog->setWindowTitle(title);

    QLabel* label = new QLabel(dialog);
    label->setText(caption);

    MappingSpinBox* mappingSpinBox = new MappingSpinBox(mapper, dialog);
    mappingSpinBox->setRange(min, max);
    mappingSpinBox->setValue(value);
    mappingSpinBox->selectAll();

    QDialogButtonBox* dialogButtonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, Qt::Horizontal, dialog);
    QObject::connect(dialogButtonBox, SIGNAL(accepted()), dialog, SLOT(accept()));
    QObject::connect(dialogButtonBox, SIGNAL(rejected()), dialog, SLOT(reject()));

    dialog->setLayout(new QVBoxLayout(dialog));
    dialog->layout()->addWidget(label);
    dialog->layout()->addWidget(mappingSpinBox);
    dialog->layout()->addWidget(dialogButtonBox);

    dialog->setFocusProxy(mappingSpinBox);

    const int dialogResult = dialog->exec();
    const int number = mappingSpinBox->value();

    delete dialog;

    if(ok)
    {
        *ok = dialogResult == QDialog::Accepted;
    }

    return number;
}

ProgressLineEdit::ProgressLineEdit(QWidget* parent) : QLineEdit(parent),
    m_progress(0)
{    
}

int ProgressLineEdit::progress() const
{
    return m_progress;
}

void ProgressLineEdit::setProgress(int progress)
{
    if(m_progress != progress && progress >= 0 && progress <= 100)
    {
        m_progress = progress;

        update();
    }
}

void ProgressLineEdit::paintEvent(QPaintEvent* event)
{
    QLineEdit::paintEvent(event);

    QPainter painter(this);

    QRect highlightedRect = rect();
    highlightedRect.setWidth(m_progress * highlightedRect.width() / 100);

    painter.setCompositionMode(QPainter::CompositionMode_Multiply);
    painter.fillRect(highlightedRect, palette().highlight());
}

void ProgressLineEdit::keyPressEvent(QKeyEvent* event)
{
    QLineEdit::keyPressEvent(event);

    if(event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter)
    {
        emit returnPressed(event->modifiers());
    }
}

SearchLineEdit::SearchLineEdit(QWidget* parent) : ProgressLineEdit(parent)
{
    m_timer = new QTimer(this);

    m_timer->setInterval(2000);
    m_timer->setSingleShot(true);

    connect(this, SIGNAL(textEdited(QString)), m_timer, SLOT(start()));
    connect(this, SIGNAL(returnPressed(Qt::KeyboardModifiers)), SLOT(on_returnPressed(Qt::KeyboardModifiers)));
    connect(m_timer, SIGNAL(timeout()), SLOT(on_timeout()));
}

void SearchLineEdit::startSearch()
{
    QTimer::singleShot(0, this, SLOT(on_timeout()));
}

void SearchLineEdit::startTimer()
{
    m_timer->start();
}

void SearchLineEdit::stopTimer()
{
    m_timer->stop();
}

void SearchLineEdit::on_timeout()
{
    emit searchInitiated(text());
}

void SearchLineEdit::on_returnPressed(Qt::KeyboardModifiers modifiers)
{
    stopTimer();

    emit searchInitiated(text(), modifiers == Qt::ShiftModifier);
}

Splitter::Splitter(Qt::Orientation orientation, QWidget* parent) : QSplitter(orientation, parent),
    m_currentIndex(0)
{
    connect(qApp, SIGNAL(focusChanged(QWidget*,QWidget*)), this, SLOT(on_focusChanged(QWidget*,QWidget*)));
}

QWidget* Splitter::currentWidget() const
{
    return widget(m_currentIndex);
}

void Splitter::setCurrentWidget(QWidget* const currentWidget)
{
    for(int index = 0, count = this->count(); index < count; ++index)
    {
        QWidget* const widget = this->widget(index);

        if(currentWidget == widget)
        {
            if(m_currentIndex != index)
            {
                m_currentIndex = index;

                emit currentWidgetChanged(currentWidget);
            }

            return;
        }
    }
}

void Splitter::setUniformSizes()
{
    int size;

    switch(orientation())
    {
    default:
    case Qt::Horizontal:
        size = width();
        break;
    case Qt::Vertical:
        size = height();
        break;
    }

    QList< int > sizes;

    for(int index = 0, count = this->count(); index < count; ++index)
    {
        sizes.append(size / count);
    }

    setSizes(sizes);
}

void Splitter::on_focusChanged(QWidget* /* old */, QWidget* now)
{
    for(QWidget* currentWidget = now; currentWidget != 0; currentWidget = currentWidget->parentWidget())
    {
        if(currentWidget->parentWidget() == this)
        {
            setCurrentWidget(currentWidget);

            return;
        }
    }
}

void openInNewWindow(const QString& filePath, int page)
{
    QProcess::startDetached(
        QApplication::applicationFilePath(),
        QStringList() << QString("%2#%1").arg(page).arg(filePath)
    );
}

} // qpdfview
