/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */

/*
    Sonic Visualiser
    An audio file viewer and annotation editor.
    Centre for Digital Music, Queen Mary, University of London.
    
    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.  See the file
    COPYING included with this distribution for more information.
*/

/**
 * A rotary dial widget.
 *
 * Based on an original design by Thorsten Wilms.
 *
 * Implemented as a widget for the Rosegarden MIDI and audio sequencer
 * and notation editor by Chris Cannam.
 *
 * Extracted into a standalone Qt3 widget by Pedro Lopez-Cabanillas
 * and adapted for use in QSynth.
 * 
 * Ported to Qt4 by Chris Cannam.
 *
 * This file copyright 2003-2006 Chris Cannam, copyright 2005 Pedro
 * Lopez-Cabanillas, copyright 2006 Queen Mary, University of London.
 *
 * 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.  See the file
 * COPYING included with this distribution for more information.
 */

#include "AudioDial.h"

#include "base/RangeMapper.h"

#include <cmath>
#include <iostream>

#include <QTimer>
#include <QPainter>
#include <QPixmap>
#include <QColormap>
#include <QMouseEvent>
#include <QPaintEvent>
#include <QInputDialog>
#include <QMenu>

#include "base/Profiler.h"

#include "MenuTitle.h"





//!!! Pedro updated his version to use my up/down response code from RG -- need to grab that code in preference to this version from Rui


//-------------------------------------------------------------------------
// AudioDial - Instance knob widget class.
//

#define AUDIO_DIAL_MIN (0.25 * M_PI)
#define AUDIO_DIAL_MAX (1.75 * M_PI)
#define AUDIO_DIAL_RANGE (AUDIO_DIAL_MAX - AUDIO_DIAL_MIN)



// Constructor.
AudioDial::AudioDial(QWidget *parent) :
    QDial(parent),
    m_knobColor(Qt::black),  // shorthand for "background colour" in paint()
    m_meterColor(Qt::white), // shorthand for "foreground colour" in paint()
    m_defaultValue(0),
    m_defaultMappedValue(0),
    m_mappedValue(0),
    m_noMappedUpdate(false),
    m_showTooltip(true),
    m_provideContextMenu(true),
    m_lastContextMenu(nullptr),
    m_rangeMapper(nullptr)
{
    m_mouseDial = false;
    m_mousePressed = false;
    setContextMenuPolicy(Qt::CustomContextMenu);
    connect(this, SIGNAL(customContextMenuRequested(const QPoint &)),
            this, SLOT(contextMenuRequested(const QPoint &)));
}


// Destructor.
AudioDial::~AudioDial (void)
{
    delete m_rangeMapper;
    delete m_lastContextMenu;
}

void AudioDial::contextMenuRequested(const QPoint &pos)
{
    if (!m_provideContextMenu) {
        return;
    }

    delete m_lastContextMenu;
    m_lastContextMenu = new QMenu;
    auto m = m_lastContextMenu;

    if (m_title == "") {
        MenuTitle::addTitle(m, tr("Dial"));
    } else {        
        MenuTitle::addTitle(m, m_title);
    }

    m->addAction(tr("&Edit..."), this, SLOT(edit()));
    m->addAction(tr("&Reset to Default"), this, SLOT(setToDefault()));

    m->popup(mapToGlobal(pos));
}

void AudioDial::setRangeMapper(RangeMapper *mapper)
{
    if (m_rangeMapper == mapper) return;

    if (!m_rangeMapper && mapper) {
        connect(this, SIGNAL(valueChanged(int)),
                this, SLOT(updateMappedValue(int)));
    }

    delete m_rangeMapper;
    m_rangeMapper = mapper;

    updateMappedValue(value());
}


void AudioDial::paintEvent(QPaintEvent *)
{
    Profiler profiler("AudioDial::paintEvent");

    QPainter paint;

    double angle = AUDIO_DIAL_MIN // offset
        + (AUDIO_DIAL_RANGE *
           (double(QDial::value() - QDial::minimum()) /
            (double(QDial::maximum() - QDial::minimum()))));
    int degrees = int(angle * 180.0 / M_PI);

    int ns = notchSize();
    int numTicks = 1 + (maximum() + ns - minimum()) / ns;
        
    QColor knobColor(m_knobColor);
    if (knobColor == Qt::black) {
        knobColor = palette().window().color().lighter(150);
    }
    bool knobIsDark =
        (knobColor.red() + knobColor.green() + knobColor.blue() <= 384);

    QColor meterColor(m_meterColor);
    if (!isEnabled()) {
        meterColor = palette().mid().color();
    } else if (m_meterColor == Qt::white) {
        if (knobIsDark) {
            meterColor = palette().text().color();
        } else {
            meterColor = palette().highlight().color();
        }
    }

    QColor notchColor(palette().dark().color());
    if (knobIsDark) {
        notchColor = palette().text().color();
    }
    
    int m_size = width() < height() ? width() : height();
    int scale = 1;
    int width = m_size - 2*scale;

    paint.begin(this);
    paint.setRenderHint(QPainter::Antialiasing, true);
    paint.translate(1, 1);

    QPen pen;
    QColor c;

    // Knob body and face...

    c = knobColor;
    pen.setColor(knobColor);
    pen.setWidth(scale * 2);
    pen.setCapStyle(Qt::FlatCap);
        
    paint.setPen(pen);
    paint.setBrush(c);

    int indent = (int)(width * 0.15 + 1);

    paint.drawEllipse(indent-1, indent-1, width-2*indent, width-2*indent);

    pen.setWidth(3 * scale);
    int pos = indent-1 + (width-2*indent) / 20;
    int darkWidth = (width-2*indent) * 3 / 4;
    while (darkWidth) {
        if (knobIsDark) {
            c = c.darker(102);
        } else {
            c = c.lighter(102);
        }
        pen.setColor(c);
        paint.setPen(pen);
        paint.drawEllipse(pos, pos, darkWidth, darkWidth);
        if (!--darkWidth) break;
        paint.drawEllipse(pos, pos, darkWidth, darkWidth);
        if (!--darkWidth) break;
        paint.drawEllipse(pos, pos, darkWidth, darkWidth);
        ++pos; --darkWidth;
    }

    // Tick notches...

    if ( notchesVisible() ) {
        pen.setColor(notchColor);
        pen.setWidth(scale);
        paint.setPen(pen);
        for (int i = 0; i < numTicks; ++i) {
            int div = numTicks;
            if (div > 1) --div;
            drawTick(paint, AUDIO_DIAL_MIN + (AUDIO_DIAL_MAX - AUDIO_DIAL_MIN) * i / div,
                     width, true);
        }
    }

    // The bright metering bit...

    c = meterColor;
    pen.setColor(c);
    pen.setWidth(indent);
    paint.setPen(pen);

//    cerr << "degrees " << degrees << ", gives us " << -(degrees - 45) * 16 << endl;

    int arcLen = -(degrees - 45) * 16;
    if (arcLen == 0) arcLen = -16;

    paint.drawArc(indent/2, indent/2,
                  width-indent, width-indent, (180 + 45) * 16, arcLen);

    paint.setBrush(Qt::NoBrush);

    // Shadowing...

    pen.setWidth(scale);
    paint.setPen(pen);

    // Knob shadow...

    int shadowAngle = -720;
    if (knobIsDark) {
        c = knobColor.lighter();
    } else {
        c = knobColor.darker();
    }
    for (int arc = 120; arc < 2880; arc += 240) {
        pen.setColor(c);
        paint.setPen(pen);
        paint.drawArc(indent, indent,
                      width-2*indent, width-2*indent, shadowAngle + arc, 240);
        paint.drawArc(indent, indent,
                      width-2*indent, width-2*indent, shadowAngle - arc, 240);
        if (knobIsDark) {
            c = c.darker(110);
        } else {
            c = c.lighter(110);
        }
    }

    // Scale shadow, omitting the bottom part...

    shadowAngle = 2160;
    c = palette().shadow().color();
    for (int i = 0; i < 5; ++i) {
        pen.setColor(c);
        paint.setPen(pen);
        int arc = i * 240 + 120;
        paint.drawArc(scale/2, scale/2,
                      width-scale, width-scale, shadowAngle + arc, 240);
        c = c.lighter(110);
    }
    c = palette().shadow().color();
    for (int i = 0; i < 12; ++i) {
        pen.setColor(c);
        paint.setPen(pen);
        int arc = i * 240 + 120;
        paint.drawArc(scale/2, scale/2,
                      width-scale, width-scale, shadowAngle - arc, 240);
        c = c.lighter(110);
    }

    // Scale ends...

    if (knobIsDark) {
        pen.setColor(palette().mid().color());
    } else {
        pen.setColor(palette().shadow().color());
    }
    pen.setWidth(scale);
    paint.setPen(pen);
    for (int i = 0; i < numTicks; ++i) {
        if (i != 0 && i != numTicks - 1) continue;
        int div = numTicks;
        if (div > 1) --div;
        drawTick(paint, AUDIO_DIAL_MIN + (AUDIO_DIAL_MAX - AUDIO_DIAL_MIN) * i / div,
                 width, false);
    }

    // Pointer notch...

    double hyp = double(width) / 2.0;
    double len = hyp - indent;
    --len;

    double x0 = hyp;
    double y0 = hyp;

    double x = hyp - len * sin(angle);
    double y = hyp + len * cos(angle);

    c = notchColor;
    if (isEnabled()) {
        if (knobIsDark) {
            c = c.lighter(130);
        } else {
            c = c.darker(130);
        }
    }
    pen.setColor(c);
    pen.setWidth(scale * 2);
    paint.setPen(pen);
    paint.drawLine(int(x0), int(y0), int(x), int(y));

    paint.end();
}


void AudioDial::drawTick(QPainter &paint,
                         double angle, int size, bool internal)
{
    double hyp = double(size) / 2.0;
    double x0 = hyp - (hyp - 1) * sin(angle);
    double y0 = hyp + (hyp - 1) * cos(angle);

//    cerr << "drawTick: angle " << angle << ", size " << size << ", internal " << internal << endl;
    
    if (internal) {

        double len = hyp / 4;
        double x1 = hyp - (hyp - len) * sin(angle);
        double y1 = hyp + (hyp - len) * cos(angle);
                
        paint.drawLine(int(x0), int(y0), int(x1), int(y1));

    } else {

        double len = hyp / 4;
        double x1 = hyp - (hyp + len) * sin(angle);
        double y1 = hyp + (hyp + len) * cos(angle);

        paint.drawLine(int(x0), int(y0), int(x1), int(y1));
    }
}


void AudioDial::setKnobColor(const QColor& color)
{
    m_knobColor = color;
    update();
}


void AudioDial::setMeterColor(const QColor& color)
{
    m_meterColor = color;
    update();
}


void AudioDial::setMouseDial(bool mouseDial)
{
    m_mouseDial = mouseDial;
}


void AudioDial::setDefaultValue(int defaultValue)
{
    m_defaultValue = defaultValue;
    if (m_rangeMapper) {
        m_defaultMappedValue = m_rangeMapper->getValueForPosition(defaultValue);
    }
}

void AudioDial::setValue(int value)
{
    QDial::setValue(value);
    updateMappedValue(value);
}

void AudioDial::setDefaultMappedValue(double value)
{
    m_defaultMappedValue = value;
    if (m_rangeMapper) {
        m_defaultValue = m_rangeMapper->getPositionForValue(value);
    }
}

void AudioDial::setMappedValue(double mappedValue)
{
    if (m_rangeMapper) {
        int newPosition = m_rangeMapper->getPositionForValue(mappedValue);
        bool changed = (m_mappedValue != mappedValue);
        m_mappedValue = mappedValue;
        m_noMappedUpdate = true;
        SVDEBUG << "AudioDial::setMappedValue(" << mappedValue << "): new position is " << newPosition << endl;
        if (newPosition != value()) {
            setValue(newPosition);
        } else if (changed) {
            emit valueChanged(newPosition);
        }
        m_noMappedUpdate = false;
    } else {
        setValue(int(mappedValue));
    }
}


void AudioDial::setShowToolTip(bool show)
{
    m_showTooltip = show;
    m_noMappedUpdate = true;
    updateMappedValue(value());
    m_noMappedUpdate = false;
}


void AudioDial::setProvideContextMenu(bool provide)
{
    m_provideContextMenu = provide;
}


double AudioDial::mappedValue() const
{
    if (m_rangeMapper) {
//        SVDEBUG << "AudioDial::mappedValue(): value = " << value() << ", mappedValue = " << m_mappedValue << endl;
        return m_mappedValue;
    }
    return value();
}


void AudioDial::updateMappedValue(int value)
{
    if (!m_noMappedUpdate) {
        if (m_rangeMapper) {
            m_mappedValue = m_rangeMapper->getValueForPosition(value);
        } else {
            m_mappedValue = value;
        }
    }

    QString name = objectName();
    QString label;
    if (m_rangeMapper) {
        label = m_rangeMapper->getLabel(value);
    }
    QString text;
    if (label != "") {
        if (name != "") {
            text = tr("%1: %2").arg(name).arg(label);
        } else {
            text = label;
        }
    } else {
        QString unit = "";
        if (m_rangeMapper) {
            unit = m_rangeMapper->getUnit();
        }
        if (name != "") {
            text = tr("%1: %2%3").arg(name).arg(m_mappedValue).arg(unit);
        } else {
            text = tr("%2%3").arg(m_mappedValue).arg(unit);
        }
    }

    m_title = text;

    if (m_showTooltip) {
        setToolTip(text);
    } else {
        setToolTip("");
    }
}

void
AudioDial::setToDefault()
{
    if (m_rangeMapper) {
        setMappedValue(m_defaultMappedValue);
        return;
    }
    int dv = m_defaultValue;
    if (dv < minimum()) dv = minimum();
    if (dv > maximum()) dv = maximum();
    setValue(m_defaultValue);
}

// Alternate mouse behavior event handlers.
void AudioDial::mousePressEvent(QMouseEvent *mouseEvent)
{
    if (m_mouseDial) {
        QDial::mousePressEvent(mouseEvent);
    } else if (mouseEvent->button() == Qt::MidButton ||
               ((mouseEvent->button() == Qt::LeftButton) &&
                (mouseEvent->modifiers() & Qt::ControlModifier))) {
        setToDefault();
    } else if (mouseEvent->button() == Qt::LeftButton) {
        m_mousePressed = true;
        m_posMouse = mouseEvent->pos();
    }
}


void AudioDial::mouseDoubleClickEvent(QMouseEvent *mouseEvent)
{
    //!!! needs a common base class with Thumbwheel

    if (m_mouseDial) {
        QDial::mouseDoubleClickEvent(mouseEvent);
    } else if (mouseEvent->button() != Qt::LeftButton) {
        return;
    }

    edit();
}

void AudioDial::edit()
{
    bool ok = false;

    if (m_rangeMapper) {
        
        double min = m_rangeMapper->getValueForPosition(minimum());
        double max = m_rangeMapper->getValueForPosition(maximum());
        
        if (min > max) { 
            double tmp = min;
            min = max;
            max = tmp;
        }

        QString unit = m_rangeMapper->getUnit();
        
        QString text;
        if (objectName() != "") {
            if (unit != "") {
                text = tr("New value for %1, from %2 to %3 %4:")
                    .arg(objectName()).arg(min).arg(max).arg(unit);
            } else {
                text = tr("New value for %1, from %2 to %3:")
                    .arg(objectName()).arg(min).arg(max);
            }
        } else {
            if (unit != "") {
                text = tr("Enter a new value from %1 to %2 %3:")
                    .arg(min).arg(max).arg(unit);
            } else {
                text = tr("Enter a new value from %1 to %2:")
                    .arg(min).arg(max);
            }
        }
        
        double newValue = QInputDialog::getDouble
            (this,
             tr("Enter new value"),
             text,
             m_mappedValue,
             min,
             max,
             4, 
             &ok);
        
        if (ok) {
            setMappedValue(newValue);
        }
        
    } else {
        
        int newPosition = QInputDialog::getInt
            (this,
             tr("Enter new value"),
             tr("Enter a new value from %1 to %2:")
             .arg(minimum()).arg(maximum()),
             value(), minimum(), maximum(), singleStep(), &ok);
        
        if (ok) {
            setValue(newPosition);
        }
    }
}


void AudioDial::mouseMoveEvent(QMouseEvent *mouseEvent)
{
    if (m_mouseDial) {
        QDial::mouseMoveEvent(mouseEvent);
    } else if (m_mousePressed) {
        const QPoint& posMouse = mouseEvent->pos();
        int v = QDial::value()
            + (posMouse.x() - m_posMouse.x())
            + (m_posMouse.y() - posMouse.y());
        if (v > QDial::maximum())
            v = QDial::maximum();
        else
            if (v < QDial::minimum())
                v = QDial::minimum();
        m_posMouse = posMouse;
        QDial::setValue(v);
    }
}


void AudioDial::mouseReleaseEvent(QMouseEvent *mouseEvent)
{
    if (m_mouseDial) {
        QDial::mouseReleaseEvent(mouseEvent);
    } else if (m_mousePressed) {
        m_mousePressed = false;
    }
}

void
AudioDial::enterEvent(QEvent *e)
{
    QDial::enterEvent(e);
    emit mouseEntered();
}

void
AudioDial::leaveEvent(QEvent *e)
{
    QDial::enterEvent(e);
    emit mouseLeft();
}

