/***************************************************************************
             SignalWidget.cpp  -  Widget for displaying the signal
                             -------------------
    begin                : 1999
    copyright            : (C) 1999 by Martin Wilz
    email                : Martin Wilz <mwilz@ernie.mi.uni-koeln.de>
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   This program 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.                                   *
 *                                                                         *
 ***************************************************************************/


#include <errno.h>
#include <math.h>
#include <stdlib.h>

#include <new>

#include <QApplication>
#include <QBitmap>
#include <QContextMenuEvent>
#include <QDragEnterEvent>
#include <QDragLeaveEvent>
#include <QDragMoveEvent>
#include <QDropEvent>
#include <QEvent>
#include <QFrame>
#include <QIcon>
#include <QMenu>
#include <QMouseEvent>
#include <QPaintEvent>
#include <QResizeEvent>
#include <QTime>
#include <QToolTip>

#include <KCursor>
#include <KLocalizedString>

#include "libkwave/ClipBoard.h"
#include "libkwave/LabelList.h"
#include "libkwave/Track.h"
#include "libkwave/Utils.h"

#include "libgui/SignalWidget.h"
#include "libgui/TrackPixmap.h"
#include "libgui/TrackView.h"
// #include "libgui/ShortcutWrapper.h"

// /** table of keyboard shortcuts 0...9 */
// static const int tbl_keys[10] = {
//     Qt::Key_1,
//     Qt::Key_2,
//     Qt::Key_3,
//     Qt::Key_4,
//     Qt::Key_5,
//     Qt::Key_6,
//     Qt::Key_7,
//     Qt::Key_8,
//     Qt::Key_9,
//     Qt::Key_0
// };

/** vertical zoom factor: minimum value */
#define VERTICAL_ZOOM_MIN 1.0

/** vertical zoom factor: maximum value */
#define VERTICAL_ZOOM_MAX 100.0

/** vertical zoom factor: increment/decrement factor */
#define VERTICAL_ZOOM_STEP_FACTOR 1.5

/** interval for limiting the number of repaints per second [ms] */
#define REPAINT_INTERVAL 50

//***************************************************************************
Kwave::SignalWidget::SignalWidget(QWidget *parent,
                                  Kwave::SignalManager *signal_manager,
                                  QVBoxLayout *upper_dock,
                                  QVBoxLayout *lower_dock)
    :QWidget(parent),
     m_signal_manager(signal_manager),
     m_views(),
     m_layout(this),
     m_upper_dock(upper_dock),
     m_lower_dock(lower_dock),
     m_offset(0),
     m_zoom(0.0),
     m_vertical_zoom(1.0),
     m_repaint_timer()
{
//    qDebug("SignalWidget::SignalWidget()");

    // connect to the signal manager's signals
    Kwave::SignalManager *sig = m_signal_manager;

    connect(sig,  SIGNAL(sigTrackInserted(uint,Kwave::Track*)),
            this, SLOT(slotTrackInserted(uint,Kwave::Track*)));
    connect(sig,  SIGNAL(sigTrackDeleted(uint,Kwave::Track*)),
            this, SLOT(slotTrackDeleted(uint,Kwave::Track*)));

    // use a timer for limiting the repaint rate
    m_repaint_timer.setSingleShot(true);
    connect(&m_repaint_timer, SIGNAL(timeout()),
            this,             SLOT(repaintTimerElapsed()));

//     // -- accelerator keys for 1...9 --
//     for (int i = 0; i < 10; i++) {
//      Kwave::ShortcutWrapper *shortcut =
//          new(std::nothrow) Kwave::ShortcutWrapper(this, tbl_keys[i], i);
//      connect(shortcut, SIGNAL(activated(int)),
//              this, SLOT(parseKey(int)));
//     }

    setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);

    m_layout.setColumnStretch(0,   0);
    m_layout.setColumnStretch(1, 100);
    m_layout.setContentsMargins(0, 0, 0, 0);
    m_layout.setVerticalSpacing(3);
    setLayout(&m_layout);
    m_layout.activate();
}

//***************************************************************************
Kwave::SignalWidget::~SignalWidget()
{
}

//***************************************************************************
void Kwave::SignalWidget::setZoomAndOffset(double zoom, sample_index_t offset)
{
    Q_ASSERT(zoom >= 0.0);
    foreach (QPointer<Kwave::SignalView> view, m_views)
        view->setZoomAndOffset(zoom, offset);
}

//***************************************************************************
void Kwave::SignalWidget::forwardCommand(const QString &command)
{
    emit sigCommand(command);
}

//***************************************************************************
void Kwave::SignalWidget::requestRepaint(Kwave::SignalView *view)
{
    // add the view to the repaint queue (if not already there)
    if (!m_repaint_queue.contains(view))
        m_repaint_queue.enqueue(view);

    if (!m_repaint_timer.isActive()) {
        // start the repaint timer
        m_repaint_timer.start(REPAINT_INTERVAL);
    }
    // else: repainting is inhibited -> wait until the
    // repaint timer is elapsed
}

//***************************************************************************
void Kwave::SignalWidget::repaintTimerElapsed()
{
    while (!m_repaint_queue.isEmpty())
    {
        Kwave::SignalView *view = m_repaint_queue.dequeue();
        if (!view) continue;
        view->refresh();
    }
}

//***************************************************************************
void Kwave::SignalWidget::contextMenuEvent(QContextMenuEvent *e)
{
    Q_ASSERT(e);

    bool have_signal = m_signal_manager && !m_signal_manager->isEmpty();
    if (!have_signal)return;
    bool have_selection = (m_signal_manager->selection().length() > 1);
    bool have_labels =
        !(Kwave::LabelList(m_signal_manager->metaData()).isEmpty());

    QMenu *context_menu = new(std::nothrow) QMenu(this);
    Q_ASSERT(context_menu);
    if (!context_menu) return;

    /* menu items common to all cases */

    // undo
    QAction *action;
    action = context_menu->addAction(
        QIcon::fromTheme(_("edit-undo")),
        i18n("&Undo"),
        QKeySequence::StandardKey::Undo,
        this,
        SLOT(contextMenuEditUndo()));
    Q_ASSERT(action);
    if (!action) return;
    if (!m_signal_manager->canUndo())
        action->setEnabled(false);

    // redo
    action = context_menu->addAction(
        QIcon::fromTheme(_("edit-redo")),
        i18n("&Redo"),
        QKeySequence::StandardKey::Redo,
        this,
        SLOT(contextMenuEditRedo()));
    Q_ASSERT(action);
    if (!action) return;
    if (!m_signal_manager->canRedo())
        action->setEnabled(false);
    context_menu->addSeparator();

    // cut/copy/paste
    QAction *action_cut = context_menu->addAction(
        QIcon::fromTheme(_("edit-cut")),
        i18n("Cu&t"),
        QKeySequence::StandardKey::Cut,
        this,
        SLOT(contextMenuEditCut()));
    QAction *action_copy = context_menu->addAction(
        QIcon::fromTheme(_("edit-copy")),
        i18n("&Copy"),
        QKeySequence::StandardKey::Copy,
        this,
        SLOT(contextMenuEditCopy()));
    QAction *action_paste = context_menu->addAction(
        QIcon::fromTheme(_("edit-paste")),
        i18n("&Paste"),
        QKeySequence::StandardKey::Paste,
        this,
        SLOT(contextMenuEditPaste()));
    context_menu->addSeparator();
    if (action_cut)   action_cut->setEnabled(have_selection);
    if (action_copy)  action_copy->setEnabled(have_selection);
    if (action_paste)
        action_paste->setEnabled(!Kwave::ClipBoard::instance().isEmpty());

    int mouse_x = mapFromGlobal(e->globalPos()).x();
    int mouse_y = mapFromGlobal(e->globalPos()).y();
    if (mouse_x < 0) mouse_x = 0;
    if (mouse_y < 0) mouse_y = 0;
    if (mouse_x >= width())   mouse_x = width()   - 1;
    if (mouse_y >= height())  mouse_y = height()  - 1;


    QMenu *submenu_select = context_menu->addMenu(i18n("Selection"));
    Q_ASSERT(submenu_select);
    if (!submenu_select) return;

    // Selection / &Save
    QAction *action_select_save = submenu_select->addAction(
        QIcon::fromTheme(_("document-save")),
        i18n("&Save..."), this, SLOT(contextMenuSaveSelection()));
    Q_ASSERT(action_select_save);
    if (!action_select_save) return;
    action_select_save->setEnabled(have_selection);

    // Selection / &Expand to labels
    QAction *action_select_expand_to_labels = submenu_select->addAction(
        i18n("&Expand to Labels"),
        Qt::Key_E,
        this,
        SLOT(contextMenuSelectionExpandToLabels()));
    Q_ASSERT(action_select_expand_to_labels);
    if (!action_select_expand_to_labels) return;
    action_select_expand_to_labels->setEnabled(have_labels);

    // Selection / to next labels
    QAction *action_select_next_labels = submenu_select->addAction(
        i18n("To Next Labels"),
        Qt::SHIFT | Qt::CTRL | Qt::Key_N,
        this,
        SLOT(contextMenuSelectionNextLabels()));
    Q_ASSERT(action_select_next_labels);
    if (!action_select_next_labels) return;
    action_select_next_labels->setEnabled(have_labels);

    // Selection / to previous labels
    QAction *action_select_prev_labels = submenu_select->addAction(
        i18n("To Previous Labels"),
        Qt::SHIFT | Qt::CTRL | Qt::Key_P,
        this,
        SLOT(contextMenuSelectionPrevLabels()));
    Q_ASSERT(action_select_prev_labels);
    if (!action_select_prev_labels) return;
    action_select_prev_labels->setEnabled(have_labels);

    // find out whether there was a click within a signal view
    QSharedPointer<Kwave::ViewItem> item(nullptr);
    foreach (QPointer<Kwave::SignalView> view, m_views) {
        // map the rect of the view to our coordinate system
        const QRect view_rect = QRect(
            view->mapToParent(view->rect().topLeft()),
            view->mapToParent(view->rect().bottomRight()));

        // check: mouse click was into that view?
        if (view_rect.contains(mouse_x, mouse_y)) {
            // map mouse click position to coordinate system of the view
            QPoint pos = view->mapFromParent(QPoint(mouse_x, mouse_y));

            // give the view the chance to extend the context menu
            view->handleContextMenu(pos, context_menu);

            // try to find a view item at these coordinates
            item = view->findItem(pos);

            // if found, give the item the chance to extend the context menu
            if (!item.isNull()) {
                connect(item.data(), SIGNAL(sigCommand(QString)),
                        this,        SLOT(forwardCommand(QString)));
                item->appendContextMenu(context_menu);
            }

            // we process only one view, views cannot overlap!
            break;
        }
    }

    context_menu->exec(QCursor::pos());
    delete context_menu;
}

//***************************************************************************
void Kwave::SignalWidget::wheelEvent(QWheelEvent *event)
{
    if (!event) return;

    // we currently are only interested in <Alt> + <WheelUp/Down>
    if (event->modifiers() != Qt::AltModifier) {
        event->ignore();
        return;
    }

    if (event->angleDelta().ry() > 0) {
        // zoom in
        setVerticalZoom(m_vertical_zoom  * VERTICAL_ZOOM_STEP_FACTOR);
        event->accept();
    } else if (event->angleDelta().ry() < 0) {
        // zoom out
        setVerticalZoom(m_vertical_zoom  / VERTICAL_ZOOM_STEP_FACTOR);
        event->accept();
    } else {
        // no change
        event->ignore();
    }
}

//***************************************************************************
void Kwave::SignalWidget::setVerticalZoom(double zoom)
{
    if (zoom > VERTICAL_ZOOM_MAX) zoom = VERTICAL_ZOOM_MAX;
    if (zoom < VERTICAL_ZOOM_MIN) zoom = VERTICAL_ZOOM_MIN;
    if (qFuzzyCompare(zoom, m_vertical_zoom)) return; // no change

    // take over the zoom factor
    m_vertical_zoom = zoom;

    // propagate the zoom to all views
    foreach (QPointer<Kwave::SignalView> view, m_views)
        if (view) view->setVerticalZoom(m_vertical_zoom);

    // get back the maximum zoom set by the views
    foreach (QPointer<Kwave::SignalView> view, m_views)
        if (view && view->verticalZoom() > m_vertical_zoom)
            m_vertical_zoom = view->verticalZoom();
}

//***************************************************************************
int Kwave::SignalWidget::mapToViewPort(const QPoint &pos) const
{
    if (m_views.isEmpty()) return mapFromGlobal(pos).x(); // if empty
    return mapFromGlobal(pos).x() - m_layout.cellRect(0, 1).left();
}

//***************************************************************************
int Kwave::SignalWidget::visibleWidth() const
{
    if (m_views.isEmpty()) return width(); // if empty
    return m_layout.cellRect(0, 1).width();
}

//***************************************************************************
void Kwave::SignalWidget::insertRow(int index, Kwave::SignalView *view,
                                    QWidget *controls)
{
    const int rows = m_layout.rowCount();
    const int cols = m_layout.columnCount();

    // update the layout: move all items from the index on to the next row
    for (int row = rows; row > index; row--) {
        for (int col = 0; col < cols; col++) {
            QLayoutItem *item = m_layout.itemAtPosition(row - 1, col);
            if (item) {
                m_layout.removeItem(item);
                m_layout.addItem(item, row, col);
            }
        }
    }

    // add the widget to the layout
    m_layout.addWidget(view, index, 1);

    if (controls) {
        // add the controls to the layout
        m_layout.addWidget(controls, index, 0);

        // associate the controls to the view, so that when the view
        // gets removed/deleted, the controls get removed as well
        view->addSibling(controls);

        m_layout.activate();
        // NOTE: QLayout::activate() does not really activate the layout
        //       synchronously as expected. Instead it posts an event to
        //       invalidate the layout of it's parent widget.
        //       Unfortunately we need reliable geometry information
        //       already *now*, which forces us to do the event loop
        //       right here.
        qApp->processEvents(QEventLoop::ExcludeUserInputEvents |
                            QEventLoop::ExcludeSocketNotifiers);
    }
}

//***************************************************************************
void Kwave::SignalWidget::deleteRow(int index)
{
    const int rows = m_layout.rowCount();
    const int cols = m_layout.columnCount();

    if (index >= rows)
        return;

    // update the layout: move all items from this row to the previous
    for (int row = index; row < (rows - 1); row++) {
        for (int col = 0; col < cols; col++) {
            QLayoutItem *item = m_layout.itemAtPosition(row + 1, col);
            if (item) {
                m_layout.removeItem(item);
                m_layout.addItem(item, row, col);
            }
        }
    }
}

//***************************************************************************
void Kwave::SignalWidget::insertView(Kwave::SignalView *view,
                                     QWidget *controls)
{
    Q_ASSERT(m_upper_dock);
    Q_ASSERT(m_lower_dock);
    Q_ASSERT(view);
    if (!m_upper_dock || !m_lower_dock) return;
    if (!view) return;

    // set initial vertical zoom
    view->setVerticalZoom(m_vertical_zoom);

    // find the proper row to insert the track view
    int index = 0;
    int track = (view) ? view->track() : -1;
    const Kwave::SignalView::Location where = view->preferredLocation();
    switch (where) {
        case Kwave::SignalView::UpperDockTop: {
            // upper dock area, top
            index = 0;
            m_upper_dock->insertWidget(0, view);
            Q_ASSERT(!controls);
            break;
        }
        case Kwave::SignalView::UpperDockBottom: {
            // upper dock area, bottom
            index = m_upper_dock->count();
            m_upper_dock->addWidget(view);
            Q_ASSERT(!controls);
            break;
        }
        case Kwave::SignalView::Top: {
            // central layout, above all others
            index = m_upper_dock->count();
            int row = 0;
            insertRow(row, view, controls);
            break;
        }
        case Kwave::SignalView::AboveTrackTop: {
            // above the corresponding track, start of group
            index = m_upper_dock->count();
            int row = 0;
            for (;index < m_views.count(); ++row, ++index) {
                if (m_views[index]->track() >= track) break; // reached top
                if (m_views[index]->preferredLocation() >=
                        Kwave::SignalView::Bottom) break;
            }
            insertRow(row, view, controls);
            break;
        }
        case Kwave::SignalView::AboveTrackBottom: {
            // above the corresponding track, end of group
            index = m_upper_dock->count();
            int row = 0;
            for (;index < m_views.count(); ++row, ++index) {
                if (m_views[index]->track() < track) continue; // too early
                if (m_views[index]->track() != track) break; // next track
                if (m_views[index]->preferredLocation() !=
                        Kwave::SignalView::AboveTrackTop) break;
            }
            insertRow(row, view, controls);
            break;
        }
        case Kwave::SignalView::BelowTrackTop: {
            // below the corresponding track, start of group
            index = m_upper_dock->count();
            int row = 0;
            for (;index < m_views.count(); ++row, ++index) {
                if (m_views[index]->track() < track) continue; // too early
                if (m_views[index]->track() != track) break; // next track
                if (m_views[index]->preferredLocation() >=
                        Kwave::SignalView::BelowTrackTop) break;
            }
            insertRow(row, view, controls);
            break;
        }
        case Kwave::SignalView::BelowTrackBottom: {
            // below the corresponding track, end of group
            index = m_upper_dock->count();
            int row = 0;
            for (;index < m_views.count(); ++row, ++index) {
                if (m_views[index]->track() < track) continue; // too early
                if (m_views[index]->track() != track) break; // next track
                if (m_views[index]->preferredLocation() >=
                        Kwave::SignalView::Bottom) break;
            }
            insertRow(row, view, controls);
            break;
        }
        case Kwave::SignalView::Bottom: {
            // below all others
            int row = m_layout.rowCount();
            index = m_upper_dock->count() + row;
            insertRow(row, view, controls);
            break;
        }
        case Kwave::SignalView::LowerDockTop: {
            // lower dock area, top
            index = m_upper_dock->count() + m_layout.rowCount();
            m_lower_dock->insertWidget(0, view);
            Q_ASSERT(!controls);
            break;
        }
        case Kwave::SignalView::LowerDockBottom:
            // lower dock area, bottom
            index = m_upper_dock->count() + m_layout.rowCount() +
                    m_lower_dock->count();
            m_lower_dock->addWidget(view);
            Q_ASSERT(!controls);
            break;
        DEFAULT_IMPOSSIBLE;
    }

    // insert the view into the list of views
    Q_ASSERT(index >= 0);
    Q_ASSERT(index < m_upper_dock->count() + m_layout.rowCount() +
             m_lower_dock->count());
    m_views.insert(index, view);

    // initially set the current view info
    view->setZoomAndOffset(m_zoom, m_offset);

    // connect all signals
    connect(view,       SIGNAL(sigCommand(QString)),
            this,       SIGNAL(sigCommand(QString)),
            Qt::QueuedConnection);

    connect(view,       SIGNAL(sigNeedRepaint(Kwave::SignalView*)),
            this,       SLOT(requestRepaint(Kwave::SignalView*)));

    connect(view,       SIGNAL(contentSizeChanged()),
            this,       SLOT(updateMinimumHeight()));
    connect(view,       SIGNAL(destroyed(QObject*)),
            this,       SLOT(updateMinimumHeight()));
    connect(view,       SIGNAL(sigCursorChanged(sample_index_t)),
            this,       SIGNAL(sigCursorChanged(sample_index_t)));
    connect(this,       SIGNAL(sigCursorChanged(sample_index_t)),
            view,       SLOT(showCursor(sample_index_t)));

    updateMinimumHeight();
}

//***************************************************************************
void Kwave::SignalWidget::slotTrackInserted(unsigned int index,
                                            Kwave::Track *track)
{
    Q_ASSERT(track);
    if (!track) return;

    // create a container widget for the track controls
    QWidget *controls = new(std::nothrow) QWidget(nullptr);
    Q_ASSERT(controls);
    if (!controls) return;

    // create a new view for the track's signal
    Kwave::SignalView *new_view = new(std::nothrow) Kwave::TrackView(
        this, controls, m_signal_manager, track);
    Q_ASSERT(new_view);
    if (!new_view) {
        delete controls;
        return;
    }

    // loop over all views and adjust the track index of the following ones
    foreach (QPointer<Kwave::SignalView> view, m_views) {
        if (view->track() >= Kwave::toInt(index))
            view->setTrack(view->track() + 1);
    }

    // assign the view to the new track
    new_view->setTrack(index);

    insertView(new_view, controls);
}

//***************************************************************************
void Kwave::SignalWidget::slotTrackDeleted(unsigned int index,
                                           Kwave::Track *track)
{
    Q_UNUSED(track)

    // loop over all views, delete those that are bound to this track
    // and adjust the index of the following ones
    bool empty = true;
    QMutableListIterator<QPointer<Kwave::SignalView> > it(m_views);
    while (it.hasNext()) {
        Kwave::SignalView *view = it.next();
        if (view->track() == Kwave::toInt(index)) {
            it.remove();
            delete view;
        } else if (view->track() > Kwave::toInt(index)) {
            view->setTrack(view->track() - 1);
            empty = false;
        } else if (view->track() != -1) {
            empty = false;
        }
    }

    // find out if there are any empty rows in the grid now
    const int rows = m_layout.rowCount();
    const int cols = m_layout.columnCount();
    for (int row = 0; row < rows; row++) {
        bool row_is_empty = true;
        for (int col = 0; col < cols; col++) {
            QLayoutItem *item = m_layout.itemAtPosition(row, col);
            if (item) {
                row_is_empty = false;
                break;
            }
        }
        if (row_is_empty) deleteRow(row);
    }

    // if there are only views with track() == -1, we are empty,
    // in that case delete the rest (all views)
    if (empty) {
        while (!m_views.isEmpty())
            delete m_views.takeFirst();
    }
}

//***************************************************************************
void Kwave::SignalWidget::updateMinimumHeight()
{
    int height = 0;

    const int rows = m_layout.rowCount();
    const int cols = m_layout.columnCount();
    for (int row = 0; row < rows; row++) {
        int h = 0;
        for (int col = 0; col < cols; col++) {
            QLayoutItem *item = m_layout.itemAtPosition(row, col);
            QWidget *widget = (item) ? item->widget() : nullptr;
            if (widget) {
                int min_height = widget->minimumSize().height();
                if (min_height > h) h = min_height;
            }
        }
        height += h;
    }
    if (rows > 1) height += (rows - 1) * m_layout.verticalSpacing();

    setMinimumHeight(height);
}

//***************************************************************************
// void Kwave::SignalWidget::parseKey(int key)
// {
//     if ((key < 0) || (key >= m_lamps.count()))
//      return;
//     if (m_lamps.at(key)) m_lamps.at(key)->nextState();
// }

//***************************************************************************
//***************************************************************************

#include "moc_SignalWidget.cpp"
