//=============================================================================
//  MuseScore
//  Linux Music Score Editor
//  $Id: editstaff.cpp 5149 2011-12-29 08:38:43Z wschweer $
//
//  Copyright (C) 2002-2010 Werner Schweer and others
//
//  This program is free software; you can redistribute it and/or modify
//  it under the terms of the GNU General Public License version 2.
//
//  This program 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 this program; if not, write to the Free Software
//  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
//=============================================================================

#include "editstaff.h"

#include "editdrumset.h"
#include "editpitch.h"
#include "editstafftype.h"
#include "editstringdata.h"
#include "icons.h"
#include "libmscore/instrtemplate.h"
#include "libmscore/measure.h"
#include "libmscore/part.h"
#include "libmscore/score.h"
#include "libmscore/staff.h"
#include "libmscore/stringdata.h"
#include "libmscore/text.h"
#include "libmscore/undo.h"
#include "libmscore/utils.h"
#include "musescore.h"
#include "seq.h"
#include "selinstrument.h"

namespace Ms {

//---------------------------------------------------------
//   EditStaff
//---------------------------------------------------------

EditStaff::EditStaff(Staff* s, int /*tick*/, QWidget* parent)
   : QDialog(parent)
      {
      setObjectName("EditStaff");
      setupUi(this);
      setWindowFlags(this->windowFlags() & ~Qt::WindowContextHelpButtonHint);
      setModal(true);

      const QIcon &editIcon = *icons[int(Icons::edit_ICON)];
      minPitchASelect->setIcon(editIcon);
      maxPitchASelect->setIcon(editIcon);
      minPitchPSelect->setIcon(editIcon);
      maxPitchPSelect->setIcon(editIcon);

      staff = nullptr;
      setStaff(s);

      MuseScore::restoreGeometry(this);

      connect(buttonBox,            SIGNAL(clicked(QAbstractButton*)), SLOT(bboxClicked(QAbstractButton*)));
      connect(changeInstrument,     SIGNAL(clicked()),            SLOT(showInstrumentDialog()));
      connect(changeStaffType,      SIGNAL(clicked()),            SLOT(showStaffTypeDialog()));
      connect(minPitchASelect,      SIGNAL(clicked()),            SLOT(minPitchAClicked()));
      connect(maxPitchASelect,      SIGNAL(clicked()),            SLOT(maxPitchAClicked()));
      connect(minPitchPSelect,      SIGNAL(clicked()),            SLOT(minPitchPClicked()));
      connect(maxPitchPSelect,      SIGNAL(clicked()),            SLOT(maxPitchPClicked()));
      connect(editStringData,       SIGNAL(clicked()),            SLOT(editStringDataClicked()));
      connect(lines,                SIGNAL(valueChanged(int)),    SLOT(numOfLinesChanged()));
      connect(lineDistance,         SIGNAL(valueChanged(double)), SLOT(lineDistanceChanged()));
      connect(showClef,             SIGNAL(clicked()),            SLOT(showClefChanged()));
      connect(showTimesig,          SIGNAL(clicked()),            SLOT(showTimeSigChanged()));
      connect(showBarlines,         SIGNAL(clicked()),            SLOT(showBarlinesChanged()));

      connect(nextButton,           SIGNAL(clicked()),            SLOT(gotoNextStaff()));
      connect(previousButton,       SIGNAL(clicked()),            SLOT(gotoPreviousStaff()));

      addAction(getAction("local-help"));  // why is this needed?
      }

//---------------------------------------------------------
//   setStaff
//---------------------------------------------------------

void EditStaff::setStaff(Staff* s)
      {
      if (staff != nullptr)
            delete staff;

      orgStaff = s;
      Part* part        = orgStaff->part();
      instrument        = *part->instrument(/*tick*/);
      Score* score      = part->score();
      staff             = new Staff(score);
      staff->setSmall(orgStaff->small());
      staff->setInvisible(orgStaff->invisible());
      staff->setUserDist(orgStaff->userDist());
      staff->setColor(orgStaff->color());
      staff->setStaffType(orgStaff->staffType());
      staff->setPart(part);
      staff->setNeverHide(orgStaff->neverHide());
      staff->setShowIfEmpty(orgStaff->showIfEmpty());
      staff->setUserMag(orgStaff->userMag());
      staff->setHideSystemBarLine(orgStaff->hideSystemBarLine());

      // get tick range for instrument
      auto i = part->instruments()->upper_bound(0);   // tick
      if (i == part->instruments()->end())
            _tickEnd = -1;
      else
            _tickEnd = i->first;
#if 1
      _tickStart = -1;
#else
      --i;
      if (i == part->instruments()->begin())
            _tickStart = 0;
      else
            _tickStart = i->first;
#endif

      // set dlg controls
      spinExtraDistance->setValue(s->userDist() / score->spatium());
      invisible->setChecked(staff->invisible());
      small->setChecked(staff->small());
      color->setColor(s->color());
      partName->setText(part->partName());
      neverHide->setChecked(staff->neverHide());
      showIfEmpty->setChecked(staff->showIfEmpty());
      hideSystemBarLine->setChecked(staff->hideSystemBarLine());
      mag->setValue(staff->userMag() * 100.0);
      updateStaffType();
      updateInstrument();
      updateNextPreviousButtons();
      }

//---------------------------------------------------------
//   closeEvent
//---------------------------------------------------------

void EditStaff::hideEvent(QHideEvent* ev)
      {
      MuseScore::saveGeometry(this);
      QWidget::hideEvent(ev);
      }

//---------------------------------------------------------
//   updateStaffType
//---------------------------------------------------------

void EditStaff::updateStaffType()
      {
      StaffType* staffType = staff->staffType();
      lines->setValue(staffType->lines());
      lineDistance->setValue(staffType->lineDistance().val());
      showClef->setChecked(staffType->genClef());
      showTimesig->setChecked(staffType->genTimesig());
      showBarlines->setChecked(staffType->showBarlines());
      staffGroupName->setText(qApp->translate("Staff type group name", staffType->groupName()));
      }

//---------------------------------------------------------
//   updateInstrument
//---------------------------------------------------------

void EditStaff::updateInstrument()
      {
      updateInterval(instrument.transpose());

      QList<StaffName>& snl = instrument.shortNames();
      QString df = snl.isEmpty() ? "" : snl[0].name();
      shortName->setPlainText(df);

      QList<StaffName>& lnl = instrument.longNames();
      df = lnl.isEmpty() ? "" : lnl[0].name();

      longName->setPlainText(df);

      if (partName->text() == instrumentName->text())    // Updates part name if no custom name has been set before
            partName->setText(instrument.trackName());

      instrumentName->setText(instrument.trackName());

      _minPitchA = instrument.minPitchA();
      _maxPitchA = instrument.maxPitchA();
      _minPitchP = instrument.minPitchP();
      _maxPitchP = instrument.maxPitchP();
      minPitchA->setText(midiCodeToStr(_minPitchA));
      maxPitchA->setText(midiCodeToStr(_maxPitchA));
      minPitchP->setText(midiCodeToStr(_minPitchP));
      maxPitchP->setText(midiCodeToStr(_maxPitchP));

      // only show string data controls if instrument has strings
      int numStr = instrument.stringData() ? instrument.stringData()->strings() : 0;
      stringDataFrame->setVisible(numStr > 0);
      numOfStrings->setText(QString::number(numStr));
      }

//---------------------------------------------------------
//   updateInterval
//---------------------------------------------------------

void EditStaff::updateInterval(const Interval& iv)
      {
      int diatonic  = iv.diatonic;
      int chromatic = iv.chromatic;

      int oct = chromatic / 12;
      if (oct < 0)
            oct = -oct;

      bool upFlag = true;
      if (chromatic < 0 || diatonic < 0) {
            upFlag    = false;
            chromatic = -chromatic;
            diatonic  = -diatonic;
            }
      chromatic %= 12;
      diatonic  %= 7;

      int interval = searchInterval(diatonic, chromatic);
      if (interval == -1) {
            qDebug("EditStaff: unknown interval %d %d", diatonic, chromatic);
            interval = 0;
            }
      iList->setCurrentIndex(interval);
      up->setChecked(upFlag);
      down->setChecked(!upFlag);
      octave->setValue(oct);
      }

//---------------------------------------------------------
//   updateNextPreviousButtons
//---------------------------------------------------------

void EditStaff::updateNextPreviousButtons()
      {
      int staffIdx = orgStaff->idx();
      nextButton->setEnabled(staffIdx < orgStaff->score()->nstaves() - 1);
      previousButton->setEnabled(staffIdx != 0);
      }

//---------------------------------------------------------
//   gotoNextStaff
//---------------------------------------------------------

void EditStaff::gotoNextStaff()
      {
      Staff* nextStaff = orgStaff->score()->staff(orgStaff->idx() + 1);
      if (nextStaff)
            {
            setStaff(nextStaff);
            }
      }

//---------------------------------------------------------
//   gotoPreviousStaff
//---------------------------------------------------------

void EditStaff::gotoPreviousStaff()
      {
      Staff* prevStaff = orgStaff->score()->staff(orgStaff->idx() - 1);
      if (prevStaff)
            {
            setStaff(prevStaff);
            }
      }

//---------------------------------------------------------
//   bboxClicked
//---------------------------------------------------------

void EditStaff::bboxClicked(QAbstractButton* button)
      {
      QDialogButtonBox::ButtonRole br = buttonBox->buttonRole(button);
      switch(br) {
            case QDialogButtonBox::ApplyRole:
                  apply();
                  break;

            case QDialogButtonBox::AcceptRole:
                  apply();
                  // fall through

            case QDialogButtonBox::RejectRole:
                  close();
                  if (staff != nullptr)
                        delete staff;
                  break;

            default:
                  qDebug("EditStaff: unknown button %d", int(br));
                  break;
            }
      }

//---------------------------------------------------------
//   apply
//---------------------------------------------------------

void EditStaff::apply()
      {
      Score* score  = orgStaff->score();
      Part* part    = orgStaff->part();

      QString sn = shortName->toPlainText();
      QString ln = longName->toPlainText();
      if (!Text::validateText(sn) || !Text::validateText(ln)) {
            QMessageBox msgBox;
            msgBox.setText(tr("The instrument name is invalid."));
            msgBox.exec();
            return;
            }
      shortName->setPlainText(sn);  // show the fixed text
      longName->setPlainText(ln);

      int intervalIdx = iList->currentIndex();
      bool upFlag     = up->isChecked();

      Interval interval  = intervalList[intervalIdx];
      interval.diatonic  += octave->value() * 7;
      interval.chromatic += octave->value() * 12;

      if (!upFlag)
            interval.flip();
      instrument.setTranspose(interval);

      instrument.setMinPitchA(_minPitchA);
      instrument.setMaxPitchA(_maxPitchA);
      instrument.setMinPitchP(_minPitchP);
      instrument.setMaxPitchP(_maxPitchP);

      instrument.setShortName(sn);
      instrument.setLongName(ln);

      bool inv       = invisible->isChecked();
      ClefTypeList clefType = instrument.clefType(orgStaff->rstaff());
      qreal userDist = spinExtraDistance->value();
      bool nhide     = neverHide->isChecked();
      bool ifEmpty   = showIfEmpty->isChecked();
      bool hideSystemBL = hideSystemBarLine->isChecked();

      QString newPartName = partName->text().simplified();
      if (!(instrument == *part->instrument()) || part->partName() != newPartName) {
            // instrument has changed
            Interval v1 = instrument.transpose();
            Interval v2 = part->instrument()->transpose();

            score->undo(new ChangePart(part, new Instrument(instrument), newPartName));
            emit instrumentChanged();

            if (v1 != v2)
                  score->transpositionChanged(part, v2, _tickStart, _tickEnd);
            }
      orgStaff->undoChangeProperty(P_ID::MAG, mag->value() / 100.0);
      orgStaff->undoChangeProperty(P_ID::COLOR, color->color());
      orgStaff->undoChangeProperty(P_ID::SMALL, small->isChecked());

      if (inv != orgStaff->invisible()
         || clefType != orgStaff->defaultClefType()
         || userDist != orgStaff->userDist()
         || nhide != orgStaff->neverHide()
         || ifEmpty != orgStaff->showIfEmpty()
         || hideSystemBL != orgStaff->hideSystemBarLine()
         ) {
            score->undo(new ChangeStaff(orgStaff, inv, clefType, userDist * score->spatium(), nhide, ifEmpty, hideSystemBL));
            }

      if ( !(*orgStaff->staffType() == *staff->staffType()) ) {
            // updateNeeded |= (orgStaff->staffGroup() == StaffGroup::TAB || staff->staffGroup() == StaffGroup::TAB);
            score->undo()->push(new ChangeStaffType(orgStaff, *staff->staffType()));
            }

      score->update();
      score->updateChannel();
      }

//---------------------------------------------------------
//   <Pitch>Clicked
//---------------------------------------------------------

void EditStaff::minPitchAClicked()
      {
      int         newCode;
      EditPitch ep(this, instrument.minPitchA());
      ep.setWindowModality(Qt::WindowModal);
      if ( (newCode = ep.exec()) != -1) {
            minPitchA->setText(midiCodeToStr(newCode));
            _minPitchA = newCode;
            }
      }

void EditStaff::maxPitchAClicked()
      {
      int         newCode;
      EditPitch ep(this, instrument.maxPitchA());
      ep.setWindowModality(Qt::WindowModal);
      if ( (newCode = ep.exec()) != -1) {
            maxPitchA->setText(midiCodeToStr(newCode));
            _maxPitchA = newCode;
            }
      }

void EditStaff::minPitchPClicked()
      {
      int         newCode;
      EditPitch ep(this, instrument.minPitchP());
      ep.setWindowModality(Qt::WindowModal);
      if ( (newCode = ep.exec()) != -1) {
            minPitchP->setText(midiCodeToStr(newCode));
            _minPitchP = newCode;
            }
      }

void EditStaff::maxPitchPClicked()
      {
      int         newCode;
      EditPitch ep(this, instrument.maxPitchP());
      ep.setWindowModality(Qt::WindowModal);
      if ( (newCode = ep.exec()) != -1) {
            maxPitchP->setText(midiCodeToStr(newCode));
            _maxPitchP = newCode;
            }
      }

//---------------------------------------------------------
//   StaffType props slots
//---------------------------------------------------------

void EditStaff::lineDistanceChanged()
      {
      staff->staffType()->setLineDistance(Spatium(lineDistance->value()));
      }

void EditStaff::numOfLinesChanged()
      {
      staff->staffType()->setLines(lines->value());
      }

void EditStaff::showClefChanged()
      {
      staff->staffType()->setGenClef(showClef->checkState() == Qt::Checked);
      }

void EditStaff::showTimeSigChanged()
      {
      staff->staffType()->setGenTimesig(showTimesig->checkState() == Qt::Checked);
      }

void EditStaff::showBarlinesChanged()
      {
      staff->staffType()->setShowBarlines(showBarlines->checkState() == Qt::Checked);
      }

//---------------------------------------------------------
//   showInstrumentDialog
//---------------------------------------------------------

void EditStaff::showInstrumentDialog()
      {
      SelectInstrument si(&instrument, this);
      si.setWindowModality(Qt::WindowModal);
      if (si.exec()) {
            instrument = Instrument::fromTemplate(si.instrTemplate());
            updateInstrument();
            }
      }

//---------------------------------------------------------
//   editStringDataClicked
//---------------------------------------------------------

void EditStaff::editStringDataClicked()
      {
      int                     frets = instrument.stringData()->frets();
      QList<instrString>      stringList = instrument.stringData()->stringList();

      EditStringData* esd = new EditStringData(this, &stringList, &frets);
      esd->setWindowModality(Qt::WindowModal);
      if (esd->exec()) {
            StringData stringData(frets, stringList);

            // update instrument pitch ranges as necessary
            if (stringList.size() > 0) {
                  // get new string range bottom and top
                  // as we have to choose an int size, INT16 are surely beyond midi pitch limits
                  int oldHighestStringPitch     = INT16_MIN;
                  int highestStringPitch        = INT16_MIN;
                  int lowestStringPitch         = INT16_MAX;
                  for (const instrString& str : stringList) {
                        if (str.pitch > highestStringPitch) highestStringPitch = str.pitch;
                        if (str.pitch < lowestStringPitch)  lowestStringPitch  = str.pitch;
                        }
                  // get old string range bottom
                  for (const instrString& str : instrument.stringData()->stringList())
                        if (str.pitch > oldHighestStringPitch) oldHighestStringPitch = str.pitch;
                  // if there were no string, arbitrarely set old top to maxPitchA
                  if (oldHighestStringPitch == INT16_MIN)
                        oldHighestStringPitch = instrument.maxPitchA();

                  // range bottom is surely the pitch of the lowest string
                  instrument.setMinPitchA(lowestStringPitch);
                  instrument.setMinPitchP(lowestStringPitch);
                  // range top should keep the same interval with the highest string it has now
                  instrument.setMaxPitchA(instrument.maxPitchA() + highestStringPitch - oldHighestStringPitch);
                  instrument.setMaxPitchP(instrument.maxPitchP() + highestStringPitch - oldHighestStringPitch);
                  // update dlg controls
                  minPitchA->setText(midiCodeToStr(instrument.minPitchA()));
                  maxPitchA->setText(midiCodeToStr(instrument.maxPitchA()));
                  minPitchP->setText(midiCodeToStr(instrument.minPitchP()));
                  maxPitchP->setText(midiCodeToStr(instrument.maxPitchP()));
                  // if no longer there is any string, leave everything as it is now
                  }

            // update instrument data and dlg controls
            instrument.setStringData(stringData);
            numOfStrings->setText(QString::number(stringData.strings()));
            }
      }

//---------------------------------------------------------
//   midiCodeToStr
//    Converts a MIDI numeric pitch code to human-readable note name
//---------------------------------------------------------

static const char* g_cNoteName[] = {
      QT_TRANSLATE_NOOP("editstaff", "C"),
      QT_TRANSLATE_NOOP("editstaff", "C#"),
      QT_TRANSLATE_NOOP("editstaff", "D"),
      QT_TRANSLATE_NOOP("editstaff", "Eb"),
      QT_TRANSLATE_NOOP("editstaff", "E"),
      QT_TRANSLATE_NOOP("editstaff", "F"),
      QT_TRANSLATE_NOOP("editstaff", "F#"),
      QT_TRANSLATE_NOOP("editstaff", "G"),
      QT_TRANSLATE_NOOP("editstaff", "Ab"),
      QT_TRANSLATE_NOOP("editstaff", "A"),
      QT_TRANSLATE_NOOP("editstaff", "Bb"),
      QT_TRANSLATE_NOOP("editstaff", "B")
      };

QString EditStaff::midiCodeToStr(int midiCode)
      {
      return QString("%1 %2").arg(qApp->translate("editstaff", g_cNoteName[midiCode % 12])).arg(midiCode / 12 - 1);
      }

//---------------------------------------------------------
//   showStaffTypeDialog
//---------------------------------------------------------

void EditStaff::showStaffTypeDialog()
      {
      EditStaffType editor(this, staff);
      editor.setWindowModality(Qt::WindowModal);
      if (editor.exec()) {
            staff->setStaffType(editor.getStaffType());
            updateStaffType();
            }
      }
}

