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

/*
    Rosegarden
    A MIDI and audio sequencer and musical notation editor.
    Copyright 2000-2026 the Rosegarden development team.

    Other copyrights also apply to some parts of this work.  Please
    see the AUTHORS file and individual file headers for details.

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

#define RG_MODULE_STRING "[ManageMetronomeDialog]"
#define RG_NO_DEBUG_PRINT

#include "ManageMetronomeDialog.h"
#include "misc/Debug.h"
#include "misc/Strings.h"
#include "base/Composition.h"
#include "base/Device.h"
#include "base/Instrument.h"
#include "base/MidiDevice.h"
#include "base/SoftSynthDevice.h"
#include "base/MidiProgram.h"
#include "base/RealTime.h"
#include "base/Studio.h"
#include "document/RosegardenDocument.h"
#include "gui/seqmanager/SequenceManager.h"
#include "gui/studio/StudioControl.h"
#include "gui/widgets/PitchChooser.h"
#include "sound/MappedEvent.h"
#include "sound/PluginIdentifier.h"
#include "sequencer/RosegardenSequencer.h"

#include <QComboBox>
#include <QDialog>
#include <QDialogButtonBox>
#include <QCheckBox>
#include <QFrame>
#include <QGroupBox>
#include <QLabel>
#include <QSpinBox>
#include <QString>
#include <QWidget>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLayout>
#include <QPushButton>


namespace Rosegarden
{


ManageMetronomeDialog::ManageMetronomeDialog(QWidget *parent,
                                             RosegardenDocument *doc) :
    QDialog(parent),
    m_doc(doc),
    m_buttonBox(new QDialogButtonBox(QDialogButtonBox::Ok |
                                     QDialogButtonBox::Close))
{
    setModal(true);
    setWindowTitle(tr("Metronome"));

    QGridLayout *metagrid = new QGridLayout;
    setLayout(metagrid);
    QWidget *hbox = new QWidget(this);
    QHBoxLayout *hboxLayout = new QHBoxLayout;
    hboxLayout->setContentsMargins(0, 0, 0, 0);
    metagrid->addWidget(hbox, 0, 0);

    QWidget *vbox = new QWidget(hbox);
    QVBoxLayout *vboxLayout = new QVBoxLayout;
    vboxLayout->setContentsMargins(0, 0, 0, 0);
    hboxLayout->addWidget(vbox);

    QGroupBox *deviceBox = new QGroupBox( tr("Metronome Instrument"), vbox );
    deviceBox->setContentsMargins(10, 10, 10, 10);
    QGridLayout *deviceBoxLayout = new QGridLayout(deviceBox);
    deviceBoxLayout->setSpacing(5);
    vboxLayout->addWidget(deviceBox);

    deviceBoxLayout->addWidget(new QLabel(tr("Device"), deviceBox), 0, 0);
    m_metronomeDevice = new QComboBox(deviceBox);
    m_metronomeDevice->setToolTip(tr("<qt>Choose the device you want to use to play the metronome</qt>"));
    deviceBoxLayout->addWidget(m_metronomeDevice, 0, 1);

    DeviceVector *devices = doc->getStudio().getDevices();
    DeviceVector::const_iterator it;

    Studio &studio = m_doc->getStudio();
    DeviceId deviceId = studio.getMetronomeDevice();

    for (it = devices->begin(); it != devices->end(); ++it) {

        Device *dev = *it;
        bool hasConnection = false;
        if (!isSuitable(dev, &hasConnection)) continue;

        QString label = QObject::tr(dev->getName().c_str());
        // connections imply some untranslatable external string
        QString connection = RosegardenSequencer::getInstance()->getConnection
            (dev->getId());

        if (hasConnection && connection != "") {
            label = tr("%1 - %2").arg(label).arg(connection);
        } else if (!hasConnection) {
            label = tr("%1 - No connection").arg(label);
        }
        m_metronomeDevice->addItem(label);
        if (dev->getId() == deviceId) {
            m_metronomeDevice->setCurrentIndex(m_metronomeDevice->count() - 1);
        }
    }

    deviceBoxLayout->addWidget(new QLabel(tr("Instrument"), deviceBox), 1, 0);
    m_metronomeInstrument = new QComboBox(deviceBox);
    m_metronomeInstrument->setToolTip(tr("<qt>Choose the instrument you want to use to play the metronome (typically #10)</qt>"));
    connect(m_metronomeInstrument,
                static_cast<void(QComboBox::*)(int)>(&QComboBox::activated),
            this, &ManageMetronomeDialog::slotSetModified);
    deviceBoxLayout->addWidget(m_metronomeInstrument, 1, 1);
    deviceBox->setLayout(deviceBoxLayout);

    QGroupBox *beatBox = new QGroupBox( tr("Beats"), vbox );
    beatBox->setContentsMargins(10, 10, 10, 10);
    QGridLayout *beatBoxLayout = new QGridLayout(beatBox);
    beatBoxLayout->setSpacing(5);
    vboxLayout->addWidget(beatBox);

    beatBoxLayout->addWidget(new QLabel(tr("Resolution"), beatBox), 0, 0);
    m_metronomeResolution = new QComboBox(beatBox);
    m_metronomeResolution->setToolTip(tr("<qt>The metronome can sound bars only, bars and beats, or bars, beats and sub-beats.  The latter mode can be particularly useful for playing in compound time signatures like 12/8.</qt>"));
    m_metronomeResolution->addItem(tr("None"));
    m_metronomeResolution->addItem(tr("Bars only"));
    m_metronomeResolution->addItem(tr("Bars and beats"));
    m_metronomeResolution->addItem(tr("Bars, beats, and sub-beats"));
    connect(m_metronomeResolution,
                static_cast<void(QComboBox::*)(int)>(&QComboBox::activated),
            this, &ManageMetronomeDialog::slotResolutionChanged);
    beatBoxLayout->addWidget(m_metronomeResolution, 0, 1);

    beatBoxLayout->addWidget(new QLabel(tr("Bar velocity"), beatBox), 1, 0);
    m_metronomeBarVely = new QSpinBox(beatBox);
    m_metronomeBarVely->setToolTip(tr("<qt>Controls how forcefully the bar division notes will be struck.  (These are typically the loudest of all.)</qt>"));
    m_metronomeBarVely->setMinimum(0);
    m_metronomeBarVely->setMaximum(127);
    connect(m_metronomeBarVely, (void(QSpinBox::*)(int))&QSpinBox::valueChanged,
            this, &ManageMetronomeDialog::slotSetModified);
    beatBoxLayout->addWidget(m_metronomeBarVely, 1, 1);

    beatBoxLayout->addWidget(new QLabel(tr("Beat velocity"), beatBox), 2, 0);
    m_metronomeBeatVely = new QSpinBox(beatBox);
    m_metronomeBeatVely->setToolTip(tr("<qt>Controls how forcefully the beat division notes will be struck.  (These are typically more quiet than beat division notes.)</qt>"));
    m_metronomeBeatVely->setMinimum(0);
    m_metronomeBeatVely->setMaximum(127);
    connect(m_metronomeBeatVely, (void(QSpinBox::*)(int))&QSpinBox::valueChanged,
            this, &ManageMetronomeDialog::slotSetModified);
    beatBoxLayout->addWidget(m_metronomeBeatVely, 2, 1);

    beatBoxLayout->addWidget(new QLabel(tr("Sub-beat velocity"), beatBox), 3, 0);
    m_metronomeSubBeatVely = new QSpinBox(beatBox);
    m_metronomeSubBeatVely->setToolTip(tr("<qt>Controls how forcefully the sub-beat division notes will be struck.  (These are typically the most quiet of all, and are not heard unless you are working in compound time.)</qt>"));
    m_metronomeSubBeatVely->setMinimum(0);
    m_metronomeSubBeatVely->setMaximum(127);
    connect(m_metronomeSubBeatVely, (void(QSpinBox::*)(int))&QSpinBox::valueChanged,
            this, &ManageMetronomeDialog::slotSetModified);
    beatBoxLayout->addWidget(m_metronomeSubBeatVely, 3, 1);
    beatBox->setLayout(beatBoxLayout);

    vbox->setLayout(vboxLayout);

    vbox = new QWidget(hbox);
    vboxLayout = new QVBoxLayout;
    vboxLayout->setContentsMargins(0, 0, 0, 0);
    hboxLayout->addWidget(vbox);
    hbox->setLayout(hboxLayout);

    m_metronomePitch = new PitchChooser(tr("Pitch"), vbox , 60);
    m_metronomePitch->setToolTip(tr("<qt>It is typical to use a percussion instrument for the metronome, so the pitch normally controls which sort of drum will sound for each tick.  You may configure a different pitch for each of the bar, beat, and sub-beat ticks.</qt>"));
    vboxLayout->addWidget(m_metronomePitch);
    connect(m_metronomePitch, &PitchChooser::pitchChanged,
            this, &ManageMetronomeDialog::slotPitchChanged);
    connect(m_metronomePitch, &PitchChooser::preview,
            this, &ManageMetronomeDialog::slotPreviewPitch);

    m_metronomePitchSelector = new QComboBox();
    m_metronomePitchSelector->addItem(tr("for Bar"));
    m_metronomePitchSelector->addItem(tr("for Beat"));
    m_metronomePitchSelector->addItem(tr("for Sub-beat"));
    m_metronomePitch->addWidgetToLayout(m_metronomePitchSelector);
    connect(m_metronomePitchSelector,
                static_cast<void(QComboBox::*)(int)>(&QComboBox::activated),
            this, &ManageMetronomeDialog::slotPitchSelectorChanged);

    QGroupBox *enableBox = new QGroupBox( tr("Metronome Activated"), vbox );
    QVBoxLayout *enableBoxLayout = new QVBoxLayout;
    vboxLayout->addWidget(enableBox);
    m_playEnabled = new QCheckBox(tr("Playing"), enableBox);
    enableBoxLayout->addWidget(m_playEnabled);
    m_recordEnabled = new QCheckBox(tr("Recording"), enableBox);
    enableBoxLayout->addWidget(m_recordEnabled);
    connect(m_playEnabled, &QAbstractButton::clicked,
            this, &ManageMetronomeDialog::slotSetModified);
    connect(m_recordEnabled, &QAbstractButton::clicked,
            this, &ManageMetronomeDialog::slotSetModified);
    enableBox->setLayout(enableBoxLayout);

    vbox->setLayout(vboxLayout);

    // populate the dialog
    populate(m_metronomeDevice->currentIndex());

    // connect up the device list
    connect(m_metronomeDevice,
                static_cast<void(QComboBox::*)(int)>(&QComboBox::activated),
            this, &ManageMetronomeDialog::populate);
    // connect up the device list
    connect(m_metronomeDevice,
                static_cast<void(QComboBox::*)(int)>(&QComboBox::activated),
            this, &ManageMetronomeDialog::slotSetModified);

    metagrid->addWidget(m_buttonBox, 1, 0);
    metagrid->setRowStretch(0, 10);

    connect(m_buttonBox, &QDialogButtonBox::accepted,
            this, &ManageMetronomeDialog::accept);
    connect(m_buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);

    setModified(false);
}

void
ManageMetronomeDialog::slotResolutionChanged(int depth)
{
    m_metronomeBeatVely->setEnabled(depth > 1);
    m_metronomeSubBeatVely->setEnabled(depth > 2);
    slotSetModified();
}

void
ManageMetronomeDialog::populate(int deviceIndex)
{
    m_metronomeInstrument->clear();

    DeviceVector *devices = m_doc->getStudio().getDevices();
    DeviceVector::const_iterator it;
    int deviceCount = 0;
    Device *dev = nullptr;

    for (it = devices->begin(); it != devices->end(); ++it) {

        dev = *it;
        if (!isSuitable(dev)) continue;

        if (deviceCount == deviceIndex) break;
        deviceCount++;
    }

    // sanity
    if (deviceCount < 0 || dev == nullptr || !isSuitable(dev)) {
        return ;
    }

    // populate instrument list
    InstrumentVector instruments = dev->getPresentationInstruments();
    InstrumentVector::iterator iit;

    const MidiMetronome *metronome = getMetronome(dev);

    // if we've got no metronome against this device then create one
    if (metronome == nullptr) {
        InstrumentId id = SystemInstrumentBase;

        for (iit = instruments.begin(); iit != instruments.end(); ++iit) {
            if ((*iit)->isPercussion()) {
                id = (*iit)->getId();
                break;
            }
        }

        setMetronome(dev, MidiMetronome(id));

        metronome = getMetronome(dev);
    }

    // metronome should now be set but we still check it
    if (metronome) {
        int position = 0;
        int instrumentCount = 0;

        for (iit = instruments.begin(); iit != instruments.end(); ++iit) {

            QString iname(QObject::tr((*iit)->getName().c_str()));
            QString ipname((*iit)->getLocalizedPresentationName());
            QString programName(
                QCoreApplication::translate("INSTRUMENT",
                                            (*iit)->getProgramName().c_str()));

            QString text;

            if ((*iit)->getType() == Instrument::SoftSynth) {

                iname.replace(QCoreApplication::translate("INSTRUMENT",
                                                          "Synth plugin "), "");
                programName = "";

                const AudioPluginInstance *plugin = (*iit)->getPlugin
                    (Instrument::SYNTH_PLUGIN_POSITION);
                if (plugin)
                    programName = strtoqstr(plugin->getDisplayName());

            } else {

                iname = ipname;
            }

            if (programName != "") {
                text = tr("%1 (%2)").arg(iname).arg(programName);
            } else {
                text = iname;
            }

            m_metronomeInstrument->addItem(text);

            if ((*iit)->getId() == metronome->getInstrument()) {
                position = instrumentCount;
            }
            instrumentCount++;
        }
        m_metronomeInstrument->setCurrentIndex(position);

        m_barPitch = metronome->getBarPitch();
        m_beatPitch = metronome->getBeatPitch();
        m_subBeatPitch = metronome->getSubBeatPitch();
        slotPitchSelectorChanged(0);
        m_metronomeResolution->setCurrentIndex(metronome->getDepth());
        m_metronomeBarVely->setValue(metronome->getBarVelocity());
        m_metronomeBeatVely->setValue(metronome->getBeatVelocity());
        m_metronomeSubBeatVely->setValue(metronome->getSubBeatVelocity());
        m_playEnabled->setChecked(m_doc->getComposition().usePlayMetronome());
        m_recordEnabled->setChecked(m_doc->getComposition().useRecordMetronome());
        slotResolutionChanged(metronome->getDepth());
    }
}

void
ManageMetronomeDialog::accept()
{
    slotApply();
    QDialog::accept();
}

void
ManageMetronomeDialog::slotSetModified()
{
    setModified(true);
}

void
ManageMetronomeDialog::setModified(bool value)
{
    if (m_modified == value)
        return ;

    m_modified = value;
}

void
ManageMetronomeDialog::slotApply()
{
    Studio &studio = m_doc->getStudio();

    DeviceVector *devices = m_doc->getStudio().getDevices();
    DeviceVector::const_iterator it;
    int count = 0;
    Device *dev = nullptr;

    for (it = devices->begin(); it != devices->end(); ++it) {

        dev = *it;
        if (!isSuitable(dev)) continue;

        if (count == m_metronomeDevice->currentIndex()) break;
        count++;
    }

    if (!dev || !isSuitable(dev)) {
        RG_WARNING << "Warning: ManageMetronomeDialog::slotApply: no " << m_metronomeDevice->currentIndex() << "th device";
        return ;
    }

    DeviceId deviceId = dev->getId();
    studio.setMetronomeDevice(deviceId);

    if (getMetronome(dev) == nullptr) {
        RG_WARNING << "Warning: ManageMetronomeDialog::slotApply: unable to extract metronome from device " << deviceId;
        return ;
    }
    MidiMetronome metronome(*getMetronome(dev));

    // get instrument
    InstrumentVector list = dev->getPresentationInstruments();

    const Instrument *inst =
        list[m_metronomeInstrument->currentIndex()];

    if (inst) {
        metronome.setInstrument(inst->getId());
    }

    metronome.setBarPitch(m_barPitch);
    metronome.setBeatPitch(m_beatPitch);
    metronome.setSubBeatPitch(m_subBeatPitch);

    metronome.setDepth(
        m_metronomeResolution->currentIndex());

    metronome.setBarVelocity(
        MidiByte(m_metronomeBarVely->value()));

    metronome.setBeatVelocity(
        MidiByte(m_metronomeBeatVely->value()));

    metronome.setSubBeatVelocity(
        MidiByte(m_metronomeSubBeatVely->value()));

    setMetronome(dev, metronome);

    m_doc->getComposition().setPlayMetronome(m_playEnabled->isChecked());
    m_doc->getComposition().setRecordMetronome(m_recordEnabled->isChecked());

    if (inst) {
        m_doc->getSequenceManager()->metronomeChanged(inst->getId(), true);
    }
    m_doc->slotDocumentModified();
    setModified(false);
}

void
ManageMetronomeDialog::slotPreviewPitch(int pitch)
{
    RG_DEBUG << "slotPreviewPitch()";

    // Find the device associated with the m_metronomeDevice combo box's
    // current selection.

    // ??? Pull out a routine that returns a list of only the suitable
    //     devices.  That will simplify all this.  And it looks like it
    //     can be used four times throughout this class.
    //       DeviceVector devices = getSuitableDevices();

    const DeviceVector *devices = m_doc->getStudio().getDevices();
    int count = 0;
    Device *dev = nullptr;

    for (DeviceVector::const_iterator it = devices->begin();
         it != devices->end();
         ++it) {
        dev = *it;
        if (!isSuitable(dev))
            continue;

        // Found it?
        if (count == m_metronomeDevice->currentIndex())
            break;

        ++count;
    }

    // Device not found?  Bail.
    // ??? Not exactly true.  It will likely return the last device if none
    //     is found.  Probably need to simplify this so it does what it seems
    //     to be doing.  First step would be to implement a getSuitablDevices().
    //     See above.
    if (!dev || !isSuitable(dev))
        return;

    // ??? Why are we doing this?  We don't use it.
    const MidiMetronome *metronome = getMetronome(dev);
    if (!metronome)
        return;

    // Get the instrument selected in the m_metronomeInstrument combo box.
    const InstrumentVector instruments = dev->getPresentationInstruments();
    Instrument *inst = instruments[m_metronomeInstrument->currentIndex()];

    // Get the velocity from the m_metronomeBarVely combo box.
    const MidiByte velocity = m_metronomeBarVely->value();

    // Play the preview.
    StudioControl::playPreviewNote(
            inst,  // instrument
            pitch,
            velocity,
            RealTime::fromSeconds(.01));  // duration, .01 seconds
}

void
ManageMetronomeDialog::slotPitchChanged(int pitch)
{
    switch (m_metronomePitchSelector->currentIndex()) {
    case 0:
        m_barPitch = pitch;
        break;
    case 1:
        m_beatPitch = pitch;
        break;
    case 2:
        m_subBeatPitch = pitch;
        break;
    }
    setModified(true);
}

void
ManageMetronomeDialog::slotPitchSelectorChanged(int selection)
{
    switch (selection) {
    case 0:
        m_metronomePitch->slotSetPitch(m_barPitch);
        break;
    case 1:
        m_metronomePitch->slotSetPitch(m_beatPitch);
        break;
    case 2:
        m_metronomePitch->slotSetPitch(m_subBeatPitch);
        break;
    }
}

bool
ManageMetronomeDialog::isSuitable(Device *dev, bool *hasConnectionReturn)
{
    const MidiDevice *md = dynamic_cast<MidiDevice *>(dev);
    if (md && md->getDirection() == MidiDevice::Play) {
        if (hasConnectionReturn) {
            QString conn = RosegardenSequencer::getInstance()->getConnection
                (md->getId());
            if (conn == "") *hasConnectionReturn = false;
            else *hasConnectionReturn = true;
        }
        return true;
    }
    if (dynamic_cast<SoftSynthDevice *>(dev)) {
        if (hasConnectionReturn) *hasConnectionReturn = true;
        return true;
    }
    return false;
}

void
ManageMetronomeDialog::setMetronome(Device *dev, const MidiMetronome &metronome)
{
    MidiDevice *md = dynamic_cast<MidiDevice *>(dev);
    if (md) {
        md->setMetronome(metronome);
        return;
    }
    SoftSynthDevice *ssd = dynamic_cast<SoftSynthDevice *>(dev);
    if (ssd) {
        ssd->setMetronome(metronome);
        return;
    }
}

const MidiMetronome *
ManageMetronomeDialog::getMetronome(Device *dev)
{
    const MidiDevice *md = dynamic_cast<MidiDevice *>(dev);
    if (md) {
        return md->getMetronome();
    }
    const SoftSynthDevice *ssd = dynamic_cast<SoftSynthDevice *>(dev);
    if (ssd) {
        return ssd->getMetronome();
    }
    return nullptr;
}


}
