//=============================================================================
//  MuseScore
//  Linux Music Score Editor
//
//  Copyright (C) 2015 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 "libmscore/box.h"
#include "libmscore/instrtemplate.h"
#include "libmscore/measure.h"
#include "libmscore/page.h"
#include "libmscore/part.h"
#include "libmscore/staff.h"
#include "libmscore/stringdata.h"
#include "libmscore/sym.h"
#include "libmscore/symbol.h"
#include "libmscore/timesig.h"
#include "libmscore/style.h"
#include "libmscore/spanner.h"
#include "libmscore/bracketItem.h"
 
#include "importmxmlpass1.h"
#include "importmxmlpass2.h"
#include "preferences.h"
 
namespace Ms {
 
//---------------------------------------------------------
//   noteTypeToFraction
//---------------------------------------------------------
 
/**
 Convert MusicXML note type to fraction.
 */
 
static Fraction noteTypeToFraction(const QString& type)
      {
      if (type == "1024th")
            return Fraction(1, 1024);
      else if (type == "512th")
            return Fraction(1, 512);
      else if (type == "256th")
            return Fraction(1, 256);
      else if (type == "128th")
            return Fraction(1, 128);
      else if (type == "64th")
            return Fraction(1, 64);
      else if (type == "32nd")
            return Fraction(1, 32);
      else if (type == "16th")
            return Fraction(1, 16);
      else if (type == "eighth")
            return Fraction(1, 8);
      else if (type == "quarter")
            return Fraction(1, 4);
      else if (type == "half")
            return Fraction(1, 2);
      else if (type == "whole")
            return Fraction(1, 1);
      else if (type == "breve")
            return Fraction(2, 1);
      else if (type == "long")
            return Fraction(4, 1);
      else if (type == "maxima")
            return Fraction(8, 1);
      else
            return Fraction(0, 0);
      }
 
//---------------------------------------------------------
//   calculateFraction
//---------------------------------------------------------
 
/**
 Convert note type, number of dots and actual and normal notes into a duration
 */
 
static Fraction calculateFraction(const QString& type, const int dots, const Fraction timeMod)
      {
      // type
      Fraction f = noteTypeToFraction(type);
      if (f.isValid()) {
            // dot(s)
            Fraction f_no_dots = f;
            for (int i = 0; i < dots; ++i)
                  f += (f_no_dots / (2 << i));
            // tuplet
            if (timeMod.isValid())
                  f *= timeMod;
            // clean up (just in case)
            f.reduce();
            }
      return f;
      }
 
//---------------------------------------------------------
//   allocateStaves
//---------------------------------------------------------
 
/**
 Allocate MuseScore staff to MusicXML voices.
 For each staff, allocate at most VOICES voices to the staff.
 */
 
// for regular (non-overlapping) voices:
// 1) assign voice to a staff (allocateStaves)
// 2) assign voice numbers (allocateVoices)
// due to cross-staving, it is not a priori clear to which staff
// a voice has to be assigned
// allocate ordered by number of chordrests in the MusicXML voice
//
// for overlapping voices:
// 1) assign voice to staves it is found in (allocateStaves)
// 2) assign voice numbers (allocateVoices)
 
static void allocateStaves(VoiceList& vcLst)
      {
      // initialize
      int voicesAllocated[MAX_STAVES]; // number of voices allocated on each staff
      for (int i = 0; i < MAX_STAVES; ++i)
            voicesAllocated[i] = 0;
 
      // handle regular (non-overlapping) voices
      // note: outer loop executed vcLst.size() times, as each inner loop handles exactly one item
      for (int i = 0; i < vcLst.size(); ++i) {
            // find the regular voice containing the highest number of chords and rests that has not been handled yet
            int max = 0;
            QString key;
            for (VoiceList::const_iterator j = vcLst.constBegin(); j != vcLst.constEnd(); ++j) {
                  if (!j.value().overlaps() && j.value().numberChordRests() > max && j.value().staff() == -1) {
                        max = j.value().numberChordRests();
                        key = j.key();
                        }
                  }
            if (key != "") {
                  int prefSt = vcLst.value(key).preferredStaff();
                  if (voicesAllocated[prefSt] < VOICES) {
                        vcLst[key].setStaff(prefSt);
                        voicesAllocated[prefSt]++;
                        }
                  else
                        // out of voices: mark as used but not allocated
                        vcLst[key].setStaff(-2);
                  }
            }
 
      // handle overlapping voices
      // for every staff allocate remaining voices (if space allows)
      // the ones with the highest number of chords and rests get allocated first
      for (int h = 0; h < MAX_STAVES; ++h) {
            // note: middle loop executed vcLst.size() times, as each inner loop handles exactly one item
            for (int i = 0; i < vcLst.size(); ++i) {
                  // find the overlapping voice containing the highest number of chords and rests that has not been handled yet
                  int max = 0;
                  QString key;
                  for (VoiceList::const_iterator j = vcLst.constBegin(); j != vcLst.constEnd(); ++j) {
                        if (j.value().overlaps() && j.value().numberChordRests(h) > max && j.value().staffAlloc(h) == -1) {
                              max = j.value().numberChordRests(h);
                              key = j.key();
                              }
                        }
                  if (key != "") {
                        int prefSt = h;
                        if (voicesAllocated[prefSt] < VOICES) {
                              vcLst[key].setStaffAlloc(prefSt, 1);
                              voicesAllocated[prefSt]++;
                              }
                        else
                              // out of voices: mark as used but not allocated
                              vcLst[key].setStaffAlloc(prefSt, -2);
                        }
                  }
            }
      }
 
//---------------------------------------------------------
//   allocateVoices
//---------------------------------------------------------
 
/**
 Allocate MuseScore voice to MusicXML voices.
 For each staff, the voices are number 1, 2, 3, 4
 in the same order they are numbered in the MusicXML file.
 */
 
static void allocateVoices(VoiceList& vcLst)
      {
      int nextVoice[MAX_STAVES]; // number of voices allocated on each staff
      for (int i = 0; i < MAX_STAVES; ++i)
            nextVoice[i] = 0;
      // handle regular (non-overlapping) voices
      // a voice is allocated on one specific staff
      for (VoiceList::const_iterator i = vcLst.constBegin(); i != vcLst.constEnd(); ++i) {
            int staff = i.value().staff();
            QString key   = i.key();
            if (staff >= 0) {
                  vcLst[key].setVoice(nextVoice[staff]);
                  nextVoice[staff]++;
                  }
            }
      // handle overlapping voices
      // each voice may be in every staff
      for (VoiceList::const_iterator i = vcLst.constBegin(); i != vcLst.constEnd(); ++i) {
            for (int j = 0; j < MAX_STAVES; ++j) {
                  int staffAlloc = i.value().staffAlloc(j);
                  QString key   = i.key();
                  if (staffAlloc >= 0) {
                        vcLst[key].setVoice(j, nextVoice[j]);
                        nextVoice[j]++;
                        }
                  }
            }
      }
 
 
//---------------------------------------------------------
//   copyOverlapData
//---------------------------------------------------------
 
/**
 Copy the overlap data from the overlap detector to the voice list.
 */
 
static void copyOverlapData(VoiceOverlapDetector& vod, VoiceList& vcLst)
      {
      for (VoiceList::const_iterator i = vcLst.constBegin(); i != vcLst.constEnd(); ++i) {
            QString key = i.key();
            if (vod.stavesOverlap(key))
                  vcLst[key].setOverlap(true);
            }
      }
 
//---------------------------------------------------------
//   MusicXMLParserPass1
//---------------------------------------------------------
 
MusicXMLParserPass1::MusicXMLParserPass1(Score* score)
      : _divs(0), _score(score)
      {
      // nothing
      }
 
//---------------------------------------------------------
//   initPartState
//---------------------------------------------------------
 
/**
 Initialize members as required for reading the MusicXML part element.
 TODO: factor out part reading into a separate class
 TODO: preferably use automatically initialized variables
 Note that Qt automatically initializes new elements in QVector (tuplets).
 */
 
void MusicXMLParserPass1::initPartState(const QString& /* partId */)
      {
      _timeSigDura = Fraction(0, 0);       // invalid
      _octaveShifts.clear();
      _firstInstrSTime = Fraction(0, 1);
      _firstInstrId = "";
      }
 
//---------------------------------------------------------
//   determineMeasureLength
//---------------------------------------------------------
 
/**
 Determine the length in ticks of each measure in all parts.
 Return false on error.
 */
 
bool MusicXMLParserPass1::determineMeasureLength(QVector<Fraction>& ml) const
      {
      ml.clear();
 
      // determine number of measures: max number of measures in any part
      int nMeasures = 0;
      foreach (const MusicXmlPart &part, _parts) {
            if (part.nMeasures() > nMeasures)
                  nMeasures = part.nMeasures();
            }
 
      // determine max length of a specific measure in all parts
      for (int i = 0; i < nMeasures; ++i) {
            Fraction maxMeasDur;
            foreach (const MusicXmlPart &part, _parts) {
                  if (i < part.nMeasures()) {
                        Fraction measDurPartJ = part.measureDuration(i);
                        if (measDurPartJ > maxMeasDur)
                              maxMeasDur = measDurPartJ;
                        }
                  }
            //qDebug("determineMeasureLength() measure %d %s (%d)", i, qPrintable(maxMeasDur.print()), maxMeasDur.ticks());
            ml.append(maxMeasDur);
            }
      return true;
      }
 
//---------------------------------------------------------
//   getVoiceList
//---------------------------------------------------------
 
/**
 Get the VoiceList for part \a id.
 Return an empty VoiceList on error.
 */
 
VoiceList MusicXMLParserPass1::getVoiceList(const QString id) const
      {
      if (_parts.contains(id))
            return _parts.value(id).voicelist;
      return VoiceList();
      }
 
//---------------------------------------------------------
//   getInstrList
//---------------------------------------------------------
 
/**
 Get the MusicXmlInstrList for part \a id.
 Return an empty MusicXmlInstrList on error.
 */
 
MusicXmlInstrList MusicXMLParserPass1::getInstrList(const QString id) const
      {
      if (_parts.contains(id))
            return _parts.value(id)._instrList;
      return MusicXmlInstrList();
      }
 
//---------------------------------------------------------
//   determineMeasureLength
//---------------------------------------------------------
 
/**
 Set default notehead, line and stem direction
 for instrument \a instrId in part \a id.
 */
 
void MusicXMLParserPass1::setDrumsetDefault(const QString& id,
                                            const QString& instrId,
                                            const NoteHead::Group hg,
                                            const int line,
                                            const Direction sd)
      {
      if (_drumsets.contains(id)
          && _drumsets[id].contains(instrId)) {
            _drumsets[id][instrId].notehead = hg;
            _drumsets[id][instrId].line = line;
            _drumsets[id][instrId].stemDirection = sd;
            }
      }
 
 
//---------------------------------------------------------
//   determineStaffMoveVoice
//---------------------------------------------------------
 
/**
 For part \a id, determine MuseScore (ms) staffmove, track and voice from MusicXML (mx) staff and voice
 MusicXML staff is 0 for the first staff, 1 for the second.
 Note: track is the first track of the ms staff in the score, add ms voice for elements in a voice
 Return true if OK, false on error
 TODO: finalize
 */
 
bool MusicXMLParserPass1::determineStaffMoveVoice(const QString& id, const int mxStaff, const QString& mxVoice,
                                                  int& msMove, int& msTrack, int& msVoice) const
      {
      VoiceList voicelist = getVoiceList(id);
      msMove = 0; // TODO
      msTrack = 0; // TODO
      msVoice = 0; // TODO
 
 
      // Musicxml voices are counted for all staffs of an
      // instrument. They are not limited. In mscore voices are associated
      // with a staff. Every staff can have at most VOICES voices.
 
      // The following lines map musicXml voices to mscore voices.
      // If a voice crosses two staffs, this is expressed with the
      // "move" parameter in mscore.
 
      // Musicxml voices are unique within a part, but not across parts.
 
      //qDebug("voice mapper before: voice='%s' staff=%d", qPrintable(mxVoice), mxStaff);
      int s; // staff mapped by voice mapper
      int v; // voice mapped by voice mapper
      if (voicelist.value(mxVoice).overlaps()) {
            // for overlapping voices, the staff does not change
            // and the voice is mapped and staff-dependent
            s = mxStaff;
            v = voicelist.value(mxVoice).voice(s);
            }
      else {
            // for non-overlapping voices, both staff and voice are
            // set by the voice mapper
            s = voicelist.value(mxVoice).staff();
            v = voicelist.value(mxVoice).voice();
            }
 
      //qDebug("voice mapper mapped: s=%d v=%d", s, v);
      if (s < 0 || v < 0) {
            qDebug("too many voices (staff=%d voice='%s' -> s=%d v=%d)",
                   mxStaff + 1, qPrintable(mxVoice), s, v);
            return false;
            }
 
      msMove  = mxStaff - s;
      msVoice = v;
 
      // make score-relative instead on part-relative
      Part* part = _partMap.value(id);
      Q_ASSERT(part);
      int scoreRelStaff = _score->staffIdx(part); // zero-based number of parts first staff in the score
      msTrack = (scoreRelStaff + s) * VOICES;
 
      //qDebug("voice mapper after: scoreRelStaff=%d partRelStaff=%d msMove=%d msTrack=%d msVoice=%d",
      //       scoreRelStaff, s, msMove, msTrack, msVoice);
      // note: relStaff is the staff number relative to the parts first staff
      //       voice is the voice number in the staff
 
      return true;
      }
 
//---------------------------------------------------------
//   hasPart
//---------------------------------------------------------
 
/**
 Check if part \a id is found.
 */
 
bool MusicXMLParserPass1::hasPart(const QString& id) const
      {
      return _parts.contains(id);
      }
 
//---------------------------------------------------------
//   trackForPart
//---------------------------------------------------------
 
/**
 Return the (score relative) track number for the first staff of part \a id.
 */
 
int MusicXMLParserPass1::trackForPart(const QString& id) const
      {
      Part* part = _partMap.value(id);
      Q_ASSERT(part);
      int scoreRelStaff = _score->staffIdx(part); // zero-based number of parts first staff in the score
      return scoreRelStaff * VOICES;
      }
 
//---------------------------------------------------------
//   getMeasureStart
//---------------------------------------------------------
 
/**
 Return the measure start time for measure \a i.
 */
 
Fraction MusicXMLParserPass1::getMeasureStart(const int i) const
      {
      if (0 <= i && i < _measureStart.size())
            return _measureStart.at(i);
      else
            return Fraction(0, 0);       // invalid
      }
 
//---------------------------------------------------------
//   octaveShift
//---------------------------------------------------------
 
/**
 Return the octave shift for part \a id in \a staff at \a f.
 */
 
int MusicXMLParserPass1::octaveShift(const QString& id, const int staff, const Fraction f) const
      {
      if (_parts.contains(id))
            return _parts.value(id).octaveShift(staff, f);
 
      return 0;
      }
 
//---------------------------------------------------------
//   logDebugTrace
//---------------------------------------------------------
 
/**
 Log debug (function) trace.
 */
 
void MusicXMLParserPass1::logDebugTrace(const QString& /*info*/)
      {
      //qDebug("Trace %s", qPrintable(info));
      }
 
//---------------------------------------------------------
//   logDebugInfo
//---------------------------------------------------------
 
/**
 Log debug \a info (non-fatal events relevant for debugging).
 */
 
void MusicXMLParserPass1::logDebugInfo(const QString& /*info*/)
      {
      //qDebug("Info at line %lld col %lld: %s",
      //       _e.lineNumber(), _e.columnNumber(), qPrintable(info));
      }
 
//---------------------------------------------------------
//   logError
//---------------------------------------------------------
 
/**
 Log \a error (possibly non-fatal but to be reported to the user anyway).
 */
 
void MusicXMLParserPass1::logError(const QString& error)
      {
      QString err;
      err = QString("Error at line %1 col %2: %3").arg(_e.lineNumber()).arg(_e.columnNumber()).arg(error);
      qDebug("%s", qPrintable(err));
      _parseStatus += err;
      }
 
 
//---------------------------------------------------------
//   skipLogCurrElem
//---------------------------------------------------------
 
/**
 Skip the current element, log debug as info.
 */
 
void MusicXMLParserPass1::skipLogCurrElem()
      {
      logDebugInfo(QString("skipping '%1'").arg(_e.name().toString()));
      _e.skipCurrentElement();
      }
 
//---------------------------------------------------------
//   createMeasures
//---------------------------------------------------------
 
/**
 Create required measures with correct number, start tick and length for Score \a score.
 */
 
static void createMeasures(Score* score, const QVector<Fraction>& ml, const QVector<Fraction>& ms)
      {
      for (int i = 0; i < ml.size(); ++i) {
            Measure* measure  = new Measure(score);
            measure->setTick(ms.at(i).ticks());
            measure->setLen(ml.at(i));
            measure->setNo(i);
            score->measures()->add(measure);
            }
      }
 
//---------------------------------------------------------
//   determineMeasureStart
//---------------------------------------------------------
 
/**
 Determine the start ticks of each measure
 i.e. the sum of all previous measures length
 or start tick measure equals start tick previous measure plus length previous measure
 */
 
static void determineMeasureStart(const QVector<Fraction>& ml, QVector<Fraction>& ms)
      {
      ms.resize(ml.size());
      if (!(ms.size() > 0))
            return;  // no parts read
 
      // first measure starts at t = 0
      ms[0] = Fraction(0, 1);
      // all others start at start time previous measure plus length previous measure
      for (int i = 1; i < ml.size(); i++)
            ms[i] = ms.at(i - 1) + ml.at(i - 1);
      //for (int i = 0; i < ms.size(); i++)
      //      qDebug("measurestart ms[%d] %s", i + 1, qPrintable(ms.at(i).print()));
      }
 
//---------------------------------------------------------
//   addText
//---------------------------------------------------------
 
/**
 Add text \a strTxt to VBox \a vbx using SubStyle \a stl.
 */
 
static void addText(VBox* vbx, Score* s, QString strTxt, SubStyle stl)
      {
      if (!strTxt.isEmpty()) {
            Text* text = new Text(stl, s);
            text->setXmlText(strTxt);
            vbx->add(text);
            }
      }
 
//---------------------------------------------------------
//   addText
//---------------------------------------------------------
 
/**
 Add text \a strTxt to VBox \a vbx using SubStyle \a stl.
 Also sets Align and Yoff.
 */
 
static void addText2(VBox* vbx, Score* s, QString strTxt, SubStyle stl, Align v, double yoffs)
      {
      if (!strTxt.isEmpty()) {
            Text* text = new Text(stl, s);
            text->setXmlText(strTxt);
            text->setAlign(v);
            text->setOffset(QPointF(0.0, yoffs));
            vbx->add(text);
            }
      }
 
//---------------------------------------------------------
//   doCredits
//---------------------------------------------------------
 
/**
 Create Text elements for the credits read from MusicXML credit-words elements.
 Apply simple heuristics using only default x and y to recognize the meaning of credit words
 If no credits are found, create credits from meta data.
 */
 
static void doCredits(Score* score, const CreditWordsList& credits, const int pageWidth, const int pageHeight)
      {
      /*
      qDebug("MusicXml::doCredits()");
      qDebug("page format set (inch) w=%g h=%g tm=%g spatium=%g DPMM=%g DPI=%g",
             pf->width(), pf->height(), pf->oddTopMargin(), score->spatium(), DPMM, DPI);
      */
      // page width, height and odd top margin in tenths
      const double ph  = score->styleD(StyleIdx::pageHeight) * 10 * DPI / score->spatium();
      const int pw1 = pageWidth / 3;
      const int pw2 = pageWidth * 2 / 3;
      const int ph2 = pageHeight / 2;
      /*
      const double pw  = pf->width() * 10 * DPI / score->spatium();
      const double tm  = pf->oddTopMargin() * 10 * DPI / score->spatium();
      const double tov = ph - tm;
      qDebug("page format set (tenths) w=%g h=%g tm=%g tov=%g", pw, ph, tm, tov);
      qDebug("page format (xml, tenths) w=%d h=%d", pageWidth, pageHeight);
      qDebug("page format pw1=%d pw2=%d ph2=%d", pw1, pw2, ph2);
      */
      // dump the credits
      /*
      for (ciCreditWords ci = credits.begin(); ci != credits.end(); ++ci) {
            CreditWords* w = *ci;
            qDebug("credit-words defx=%g defy=%g just=%s hal=%s val=%s words='%s'",
                   w->defaultX,
                   w->defaultY,
                   qPrintable(w->justify),
                   qPrintable(w->hAlign),
                   qPrintable(w->vAlign),
                   qPrintable(w->words));
            }
      */
 
      int nWordsHeader = 0;               // number of credit-words in the header
      int nWordsFooter = 0;               // number of credit-words in the footer
      for (ciCreditWords ci = credits.begin(); ci != credits.end(); ++ci) {
            CreditWords* w = *ci;
            double defy = w->defaultY;
            // and count #words in header and footer
            if (defy > ph2)
                  nWordsHeader++;
            else
                  nWordsFooter++;
            } // end for (ciCreditWords ...
 
      // if there are any credit words in the header, use these
      // else use the credit words in the footer (if any)
      bool useHeader = nWordsHeader > 0;
      bool useFooter = nWordsHeader == 0 && nWordsFooter > 0;
      //qDebug("header %d footer %d useHeader %d useFooter %d",
      //       nWordsHeader, nWordsFooter, useHeader, useFooter);
 
      // determine credits height and create vbox to contain them
      qreal vboxHeight = 10;            // default height in spatium
      double miny = pageHeight;
      double maxy = 0;
      if (pageWidth > 1 && pageHeight > 1) {
            for (ciCreditWords ci = credits.begin(); ci != credits.end(); ++ci) {
                  CreditWords* w = *ci;
                  double defy = w->defaultY;
                  if ((useHeader && defy > ph2) || (useFooter && defy < ph2)) {
                        if (defy > maxy) maxy = defy;
                        if (defy < miny) miny = defy;
                        }
                  }
            //qDebug("miny=%g maxy=%g", miny, maxy);
            if (miny < (ph - 1) && maxy > 1) {  // if both miny and maxy set
                  double diff = maxy - miny;    // calculate height in tenths
                  if (diff > 1 && diff < ph2) { // and size is reasonable
                        vboxHeight = diff;
                        vboxHeight /= 10;       // height in spatium
                        vboxHeight += 2.5;      // guesstimated correction for last line
                        }
                  }
            }
      //qDebug("vbox height %g sp", vboxHeight);
      VBox* vbox = new VBox(score);
      vbox->setBoxHeight(Spatium(vboxHeight));
 
      QMap<int, CreditWords*> creditMap;  // store credit-words sorted on y pos
      bool creditWordsUsed = false;
 
      for (ciCreditWords ci = credits.begin(); ci != credits.end(); ++ci) {
            CreditWords* w = *ci;
            double defx = w->defaultX;
            double defy = w->defaultY;
            // handle all credit words in the box
            if ((useHeader && defy > ph2) || (useFooter && defy < ph2)) {
                  creditWordsUsed = true;
                  // composer is in the right column
                  if (pw2 < defx) {
                        // found composer
                        addText2(vbox, score, w->words,
                                 SubStyle::COMPOSER, Align::RIGHT | Align::BOTTOM,
                                 (miny - w->defaultY) * score->spatium() / (10 * DPI));
                        }
                  // poet is in the left column
                  else if (defx < pw1) {
                        // found poet/lyricist
                        addText2(vbox, score, w->words,
                                 SubStyle::POET, Align::LEFT | Align::BOTTOM,
                                 (miny - w->defaultY) * score->spatium() / (10 * DPI));
                        }
                  // save others (in the middle column) to be handled later
                  else {
                        creditMap.insert(defy, w);
                        }
                  }
            // keep remaining footer text for possible use as copyright
            else if (useHeader && defy < ph2) {
                  // found credit words in both header and footer
                  // header was used to create a vbox at the top of the first page
                  // footer is ignored as it conflicts with the default MuseScore footer style
                  //qDebug("add to copyright: '%s'", qPrintable(w->words));
                  }
            } // end for (ciCreditWords ...
 
      /*
       QMap<int, CreditWords*>::const_iterator ci = creditMap.constBegin();
       while (ci != creditMap.constEnd()) {
       CreditWords* w = ci.value();
       qDebug("creditMap %d credit-words defx=%g defy=%g just=%s hal=%s val=%s words=%s",
       ci.key(),
       w->defaultX,
       w->defaultY,
       qPrintable(w->justify),
       qPrintable(w->hAlign),
       qPrintable(w->vAlign),
       qPrintable(w->words));
       ++ci;
       }
       */
 
      // assign title, subtitle and copyright
      QList<int> keys = creditMap.uniqueKeys(); // note: ignoring credit-words at the same y pos
 
      // if any credit-words present, the highest is the title
      // note that the keys are sorted in ascending order
      // -> use the last key
      if (keys.size() >= 1) {
            CreditWords* w = creditMap.value(keys.at(keys.size() - 1));
            //qDebug("title='%s'", qPrintable(w->words));
            addText2(vbox, score, w->words,
                     SubStyle::TITLE, Align::HCENTER | Align::TOP,
                     (maxy - w->defaultY) * score->spatium() / (10 * DPI));
            }
 
      // add remaining credit-words as subtitles
      for (int i = 0; i < (keys.size() - 1); i++) {
            CreditWords* w = creditMap.value(keys.at(i));
            //qDebug("subtitle='%s'", qPrintable(w->words));
            addText2(vbox, score, w->words,
                     SubStyle::SUBTITLE, Align::HCENTER | Align::TOP,
                     (maxy - w->defaultY) * score->spatium() / (10 * DPI));
            }
 
      // use metadata if no workable credit-words found
      if (!creditWordsUsed) {
 
            QString strTitle;
            QString strSubTitle;
            QString strComposer;
            QString strPoet;
            QString strTranslator;
 
            if (!(score->metaTag("movementTitle").isEmpty() && score->metaTag("workTitle").isEmpty())) {
                  strTitle = score->metaTag("movementTitle");
                  if (strTitle.isEmpty())
                        strTitle = score->metaTag("workTitle");
                  }
            if (!(score->metaTag("movementNumber").isEmpty() && score->metaTag("workNumber").isEmpty())) {
                  strSubTitle = score->metaTag("movementNumber");
                  if (strSubTitle.isEmpty())
                        strSubTitle = score->metaTag("workNumber");
                  }
            QString metaComposer = score->metaTag("composer");
            QString metaPoet = score->metaTag("poet");
            QString metaTranslator = score->metaTag("translator");
            if (!metaComposer.isEmpty()) strComposer = metaComposer;
            if (metaPoet.isEmpty()) metaPoet = score->metaTag("lyricist");
            if (!metaPoet.isEmpty()) strPoet = metaPoet;
            if (!metaTranslator.isEmpty()) strTranslator = metaTranslator;
 
            addText(vbox, score, strTitle.toHtmlEscaped(),      SubStyle::TITLE);
            addText(vbox, score, strSubTitle.toHtmlEscaped(),   SubStyle::SUBTITLE);
            addText(vbox, score, strComposer.toHtmlEscaped(),   SubStyle::COMPOSER);
            addText(vbox, score, strPoet.toHtmlEscaped(),       SubStyle::POET);
            addText(vbox, score, strTranslator.toHtmlEscaped(), SubStyle::TRANSLATOR);
            }
 
      if (vbox) {
            vbox->setTick(0);
            score->measures()->add(vbox);
            }
      }
 
//---------------------------------------------------------
//   parse
//---------------------------------------------------------
 
/**
 Parse MusicXML in \a device and extract pass 1 data.
 */
 
Score::FileError MusicXMLParserPass1::parse(QIODevice* device)
      {
      logDebugTrace("MusicXMLParserPass1::parse device");
      _parts.clear();
      _e.setDevice(device);
      Score::FileError res = parse();
      if (res != Score::FileError::FILE_NO_ERROR)
            return res;
 
      // Determine the start tick of each measure in the part
      determineMeasureLength(_measureLength);
      determineMeasureStart(_measureLength, _measureStart);
      createMeasures(_score, _measureLength, _measureStart);
      return res;
      }
 
//---------------------------------------------------------
//   parse
//---------------------------------------------------------
 
/**
 Start the parsing process, after verifying the top-level node is score-partwise
 */
 
Score::FileError MusicXMLParserPass1::parse()
      {
      logDebugTrace("MusicXMLParserPass1::parse");
 
      bool found = false;
      while (_e.readNextStartElement()) {
            if (_e.name() == "score-partwise") {
                  found = true;
                  scorePartwise();
                  }
            else {
                  logError(QString("this is not a MusicXML score-partwise file (top-level node '%1')")
                           .arg(_e.name().toString()));
                  _e.skipCurrentElement();
                  return Score::FileError::FILE_BAD_FORMAT;
                  }
            }
 
      if (!found) {
            logError("this is not a MusicXML score-partwise file, node <score-partwise> not found");
            return Score::FileError::FILE_BAD_FORMAT;
            }
 
      return Score::FileError::FILE_NO_ERROR;
      }
 
//---------------------------------------------------------
//   allStaffGroupsIdentical
//---------------------------------------------------------
 
/**
 Return true if all staves in Part \a p have the same staff group
 */
 
static bool allStaffGroupsIdentical(Part const* const p)
      {
      for (int i = 1; i < p->nstaves(); ++i) {
            if (p->staff(0)->staffType(0)->group() != p->staff(i)->staffType(0)->group())
                  return false;
            }
      return true;
      }
 
//---------------------------------------------------------
//   scorePartwise
//---------------------------------------------------------
 
/**
 Parse the MusicXML top-level (XPath /score-partwise) node.
 */
 
void MusicXMLParserPass1::scorePartwise()
      {
      Q_ASSERT(_e.isStartElement() && _e.name() == "score-partwise");
      logDebugTrace("MusicXMLParserPass1::scorePartwise");
 
      MusicXmlPartGroupList partGroupList;
      CreditWordsList credits;
      int pageWidth;                             ///< Page width read from defaults
      int pageHeight;                            ///< Page height read from defaults
 
      while (_e.readNextStartElement()) {
            if (_e.name() == "part")
                  part();
            else if (_e.name() == "part-list") {
                  // if any credits are present, they have been read now
                  // add the credits to the score before adding any measure
                  // note that a part-list element must always be present
                  doCredits(_score, credits, pageWidth, pageHeight);
                  // and read the part list
                  partList(partGroupList);
                  }
            else if (_e.name() == "work") {
                  while (_e.readNextStartElement()) {
                        if (_e.name() == "work-number")
                              _score->setMetaTag("workNumber", _e.readElementText());
                        else if (_e.name() == "work-title")
                              _score->setMetaTag("workTitle", _e.readElementText());
                        else
                              skipLogCurrElem();
                        }
                  }
            else if (_e.name() == "identification")
                  identification();
            else if (_e.name() == "defaults")
                  defaults(pageWidth, pageHeight);
            else if (_e.name() == "movement-number")
                  _score->setMetaTag("movementNumber", _e.readElementText());
            else if (_e.name() == "movement-title")
                  _score->setMetaTag("movementTitle", _e.readElementText());
            else if (_e.name() == "credit")
                  credit(credits);
            else
                  skipLogCurrElem();
            }
 
      // add brackets where required
 
      /*
       qDebug("partGroupList");
       for (int i = 0; i < (int) partGroupList.size(); i++) {
       MusicXmlPartGroup* pg = partGroupList[i];
       qDebug("part-group span %d start %d type %hhd barlinespan %d",
       pg->span, pg->start, pg->type, pg->barlineSpan);
       }
       */
 
      // set of (typically multi-staff) parts containing one or more explicit brackets
      // spanning only that part: these won't get an implicit brace later
      // e.g. a two-staff piano part with an explicit brace
      QSet<Part const* const> partSet;
 
      // handle the explicit brackets
      const QList<Part*>& il = _score->parts();
      for (int i = 0; i < (int) partGroupList.size(); i++) {
            MusicXmlPartGroup* pg = partGroupList[i];
            // add part to set
            if (pg->span == 1)
                  partSet << il.at(pg->start);
            // determine span in staves
            int stavesSpan = 0;
            for (int j = 0; j < pg->span; j++)
                  stavesSpan += il.at(pg->start + j)->nstaves();
            // add bracket and set the span
            // TODO: use group-symbol default-x to determine horizontal order of brackets
            Staff* staff = il.at(pg->start)->staff(0);
            if (pg->type == BracketType::NO_BRACKET)
                  staff->setBracketType(0, BracketType::NO_BRACKET);
            else {
                  staff->addBracket(new BracketItem(staff->score(), pg->type, stavesSpan));
                  }
            if (pg->barlineSpan)
                  staff->setBarLineSpan(pg->span);
            }
 
      // handle the implicit brackets:
      // multi-staff parts w/o explicit brackets get a brace
      foreach(Part const* const p, il) {
            if (p->nstaves() > 1 && !partSet.contains(p)) {
                  p->staff(0)->addBracket(new BracketItem(p->score(), BracketType::BRACE, p->nstaves()));
                  if (allStaffGroupsIdentical(p)) {
                        // span only if the same types
                        p->staff(0)->setBarLineSpan(p->nstaves());
                        }
                  }
            }
      }
 
//---------------------------------------------------------
//   identification
//---------------------------------------------------------
 
/**
 Parse the /score-partwise/identification node:
 read the metadata.
 */
 
void MusicXMLParserPass1::identification()
      {
      Q_ASSERT(_e.isStartElement() && _e.name() == "identification");
      logDebugTrace("MusicXMLParserPass1::identification");
 
      while (_e.readNextStartElement()) {
            if (_e.name() == "creator") {
                  // type is an arbitrary label
                  QString strType = _e.attributes().value("type").toString();
                  _score->setMetaTag(strType, _e.readElementText());
                  }
            else if (_e.name() == "rights")
                  _score->setMetaTag("copyright", _e.readElementText());
            else if (_e.name() == "encoding") {
                  // TODO
                  _e.skipCurrentElement(); // skip but don't log
                  // _score->setMetaTag("encoding", _e.readElementText()); works with DOM but not with pull parser
                  // temporarily fake the encoding tag (compliant with DOM parser) to help the autotester
                  if (MScore::debugMode)
                        _score->setMetaTag("encoding", "MuseScore 0.7.02007-09-10");
                  }
            else if (_e.name() == "source")
                  _score->setMetaTag("source", _e.readElementText());
            else if (_e.name() == "miscellaneous")
                  // TODO
                  _e.skipCurrentElement();  // skip but don't log
            else
                  skipLogCurrElem();
            }
      }
 
//---------------------------------------------------------
//   text2syms
//---------------------------------------------------------
 
/**
 Convert SMuFL code points to MuseScore <sym>...</sym>
 */
 
static QString text2syms(const QString& t)
      {
      //QTime time;
      //time.start();
 
      // first create a map from symbol (Unicode) text to symId
      // note that this takes about 1 msec on a Core i5,
      // caching does not gain much
 
      ScoreFont* sf = ScoreFont::fallbackFont();
      QMap<QString, SymId> map;
      int maxStringSize = 0;        // maximum string size found
 
      for (int i = int(SymId::noSym); i < int(SymId::lastSym); ++i) {
            SymId id((SymId(i)));
            QString string(sf->toString(id));
            // insert all syms except space to prevent matching all regular spaces
            if (id != SymId::space)
                  map.insert(string, id);
            if (string.size() > maxStringSize)
                  maxStringSize = string.size();
            }
      //qDebug("text2syms map count %d maxsz %d filling time elapsed: %d ms",
      //       map.size(), maxStringSize, time.elapsed());
 
      // then look for matches
      QString in = t;
      QString res;
 
      while (in != "") {
            // try to find the largest match possible
            int maxMatch = qMin(in.size(), maxStringSize);
            QString sym;
            while (maxMatch > 0) {
                  QString toBeMatched = in.left(maxMatch);
                  if (map.contains(toBeMatched)) {
                        sym = Sym::id2name(map.value(toBeMatched));
                        break;
                        }
                  maxMatch--;
                  }
            if (maxMatch > 0) {
                  // found a match, add sym to res and remove match from string in
                  res += "<sym>";
                  res += sym;
                  res += "</sym>";
                  in.remove(0, maxMatch);
                  }
            else {
                  // not found, move one char from res to in
                  res += in.left(1);
                  in.remove(0, 1);
                  }
            }
 
      //qDebug("text2syms total time elapsed: %d ms, res '%s'", time.elapsed(), qPrintable(res));
      return res;
      }
 
//---------------------------------------------------------
//   decodeEntities
//---------------------------------------------------------
 
/**
 Decode &#...; in string \a src into UNICODE (utf8) character.
 */
 
static QString decodeEntities( const QString& src )
      {
      QString ret(src);
      QRegExp re("&#([0-9]+);");
      re.setMinimal(true);
 
      int pos = 0;
      while ( (pos = re.indexIn(src, pos)) != -1 ) {
            ret = ret.replace(re.cap(0), QChar(re.cap(1).toInt(0,10)));
            pos += re.matchedLength();
            }
      return ret;
      }
 
//---------------------------------------------------------
//   nextPartOfFormattedString
//---------------------------------------------------------
 
// TODO: probably should be shared between pass 1 and 2
 
/**
 Read the next part of a MusicXML formatted string and convert to MuseScore internal encoding.
 */
 
static QString nextPartOfFormattedString(QXmlStreamReader& e)
      {
      //QString lang       = e.attribute(QString("xml:lang"), "it");
      QString fontWeight = e.attributes().value("font-weight").toString();
      QString fontSize   = e.attributes().value("font-size").toString();
      QString fontStyle  = e.attributes().value("font-style").toString();
      QString underline  = e.attributes().value("underline").toString();
      QString fontFamily = e.attributes().value("font-family").toString();
      // TODO: color, enclosure, yoffset in only part of the text, ...
 
      QString txt        = e.readElementText();
      // replace HTML entities
      txt = decodeEntities(txt);
      QString syms       = text2syms(txt);
 
      QString importedtext;
 
      if (!fontSize.isEmpty()) {
            bool ok = true;
            float size = fontSize.toFloat(&ok);
            if (ok)
                  importedtext += QString("<font size=\"%1\"/>").arg(size);
            }
      if (!fontFamily.isEmpty() && txt == syms) {
            // add font family only if no <sym> replacement made
            importedtext += QString("<font face=\"%1\"/>").arg(fontFamily);
            }
      if (fontWeight == "bold")
            importedtext += "<b>";
      if (fontStyle == "italic")
            importedtext += "<i>";
      if (!underline.isEmpty()) {
            bool ok = true;
            int lines = underline.toInt(&ok);
            if (ok && (lines > 0))  // 1,2, or 3 underlines are imported as single underline
                  importedtext += "<u>";
            else
                  underline = "";
            }
      if (txt == syms) {
            txt.replace(QString("\r"), QString("")); // convert Windows line break \r\n -> \n
            importedtext += txt.toHtmlEscaped();
            }
      else {
            // <sym> replacement made, should be no need for line break or other conversions
            importedtext += syms;
            }
      if (underline != "")
            importedtext += "</u>";
      if (fontStyle == "italic")
            importedtext += "</i>";
      if (fontWeight == "bold")
            importedtext += "</b>";
      //qDebug("importedtext '%s'", qPrintable(importedtext));
      return importedtext;
      }
 
//---------------------------------------------------------
//   credit
//---------------------------------------------------------
 
/**
 Parse the /score-partwise/credit node:
 read the credits for later handling by doCredits().
 */
 
void MusicXMLParserPass1::credit(CreditWordsList& credits)
      {
      Q_ASSERT(_e.isStartElement() && _e.name() == "credit");
      logDebugTrace("MusicXMLParserPass1::credit");
 
      QString page = _e.attributes().value("page").toString();
      // handle only page 1 credits (to extract title etc.)
      // assume no page attribute means page 1
      if (page == "" || page == "1") {
            // multiple credit-words elements may be present,
            // which are appended
            // use the position info from the first one
            // font information is ignored, credits will be styled
            bool creditWordsRead = false;
            double defaultx = 0;
            double defaulty = 0;
            QString justify;
            QString halign;
            QString valign;
            QString crwords;
            while (_e.readNextStartElement()) {
                  if (_e.name() == "credit-words") {
                        // IMPORT_LAYOUT
                        if (!creditWordsRead) {
                              defaultx = _e.attributes().value("default-x").toString().toDouble();
                              defaulty = _e.attributes().value("default-y").toString().toDouble();
                              justify  = _e.attributes().value("justify").toString();
                              halign   = _e.attributes().value("halign").toString();
                              valign   = _e.attributes().value("valign").toString();
                              creditWordsRead = true;
                              }
                        crwords += nextPartOfFormattedString(_e);
                        }
                  else if (_e.name() == "credit-type")
                        skipLogCurrElem();
                  else
                        skipLogCurrElem();
                  }
            if (crwords != "") {
                  CreditWords* cw = new CreditWords(defaultx, defaulty, justify, halign, valign, crwords);
                  credits.append(cw);
                  }
            }
      else
            skipLogCurrElem();
 
      Q_ASSERT(_e.isEndElement() && _e.name() == "credit");
      }
 
//---------------------------------------------------------
//   mustSetSize
//---------------------------------------------------------
 
/**
 Determine if i is a style type for which the default size must be set
 */
 
// The MusicXML specification does not specify to which kinds of text
// the word-font setting applies. Setting all sizes to the size specified
// gives bad results, e.g. for measure numbers, so a selection is made.
// Some tweaking may still be required.
#if 0
static bool mustSetSize(const int i)
      {
      return
            i == int(SubStyle::TITLE)
            || i == int(SubStyle::SUBTITLE)
            || i == int(SubStyle::COMPOSER)
            || i == int(SubStyle::POET)
            || i == int(SubStyle::INSTRUMENT_LONG)
            || i == int(SubStyle::INSTRUMENT_SHORT)
            || i == int(SubStyle::INSTRUMENT_EXCERPT)
            || i == int(SubStyle::TEMPO)
            || i == int(SubStyle::METRONOME)
            || i == int(SubStyle::TRANSLATOR)
            || i == int(SubStyle::SYSTEM)
            || i == int(SubStyle::STAFF)
            || i == int(SubStyle::REPEAT_LEFT)
            || i == int(SubStyle::REPEAT_RIGHT)
            || i == int(SubStyle::TEXTLINE)
            || i == int(SubStyle::GLISSANDO)
            || i == int(SubStyle::INSTRUMENT_CHANGE);
      }
#endif
 
//---------------------------------------------------------
//   updateStyles
//---------------------------------------------------------
 
/**
 Update the style definitions to match the MusicXML word-font and lyric-font.
 */
 
static void updateStyles(Score* score,
                         const QString& /*wordFamily*/, const QString& /*wordSize*/,
                         const QString& lyricFamily, const QString& lyricSize)
      {
//TODO:ws       const float fWordSize = wordSize.toFloat();   // note conversion error results in value 0.0
      const float fLyricSize = lyricSize.toFloat(); // but avoid comparing float with exact value later
 
      // loop over all text styles (except the empty, always hidden, first one)
      // set all text styles to the MusicXML defaults
#if 0 // TODO:ws
      for (int i = int(SubStyle::DEFAULT) + 1; i < int(SubStyle::TEXT_STYLES); ++i) {
            TextStyle ts = score->style().textStyle(TextStyleType(i));
            if (i == int(SubStyle::LYRIC1) || i == int(SubStyle::LYRIC2)) {
                  if (lyricFamily != "")
                        ts.setFamily(lyricFamily);
                  if (fLyricSize > 0.001)
                        ts.setSize(fLyricSize);
                  }
            else {
                  if (wordFamily != "")
                        ts.setFamily(wordFamily);
                  if (fWordSize > 0.001 && mustSetSize(i))
                        ts.setSize(fWordSize);
                  }
            score->style().setTextStyle(ts);
            }
#endif
      if (lyricFamily != "") {
            score->style().set(StyleIdx::lyricsOddFontFace, lyricFamily);
            score->style().set(StyleIdx::lyricsEvenFontFace, lyricFamily);
            }
      if (fLyricSize > 0.001) {
            score->style().set(StyleIdx::lyricsOddFontSize, fLyricSize);
            score->style().set(StyleIdx::lyricsEvenFontSize, fLyricSize);
            }
      }
 
//---------------------------------------------------------
//   defaults
//---------------------------------------------------------
 
/**
 Parse the /score-partwise/defaults node:
 read the general score layout settings.
 */
 
void MusicXMLParserPass1::defaults(int& pageWidth, int& pageHeight)
      {
      Q_ASSERT(_e.isStartElement() && _e.name() == "defaults");
      logDebugTrace("MusicXMLParserPass1::defaults");
 
      double millimeter = _score->spatium()/10.0;
      double tenths = 1.0;
      QString lyricFontFamily;
      QString lyricFontSize;
      QString wordFontFamily;
      QString wordFontSize;
 
      while (_e.readNextStartElement()) {
            if (_e.name() == "scaling") {
                  while (_e.readNextStartElement()) {
                        if (_e.name() == "millimeters")
                              millimeter = _e.readElementText().toDouble();
                        else if (_e.name() == "tenths")
                              tenths = _e.readElementText().toDouble();
                        else
                              skipLogCurrElem();
                        }
                  double _spatium = DPMM * (millimeter * 10.0 / tenths);
                  if (preferences.musicxmlImportLayout)
                        _score->setSpatium(_spatium);
                  }
            else if (_e.name() == "page-layout") {
                  PageFormat pf;
                  pageLayout(pf, millimeter / (tenths * INCH), pageWidth, pageHeight);
//TODO:ws                  if (preferences.musicxmlImportLayout)
//                        _score->setPageFormat(pf);
                  }
            else if (_e.name() == "system-layout") {
                  while (_e.readNextStartElement()) {
                        if (_e.name() == "system-margins")
                              skipLogCurrElem();
                        else if (_e.name() == "system-distance") {
                              Spatium val(_e.readElementText().toDouble() / 10.0);
                              if (preferences.musicxmlImportLayout) {
                                    _score->style().set(StyleIdx::minSystemDistance, val);
                                    qDebug("system distance %f", val.val());
                                    }
                              }
                        else if (_e.name() == "top-system-distance")
                              skipLogCurrElem();
                        else
                              skipLogCurrElem();
                        }
                  }
            else if (_e.name() == "staff-layout") {
                  while (_e.readNextStartElement()) {
                        if (_e.name() == "staff-distance") {
                              Spatium val(_e.readElementText().toDouble() / 10.0);
                              if (preferences.musicxmlImportLayout)
                                    _score->style().set(StyleIdx::staffDistance, val);
                              }
                        else
                              skipLogCurrElem();
                        }
                  }
            else if (_e.name() == "music-font")
                  skipLogCurrElem();
            else if (_e.name() == "word-font") {
                  wordFontFamily = _e.attributes().value("font-family").toString();
                  wordFontSize = _e.attributes().value("font-size").toString();
                  _e.skipCurrentElement();
                  }
            else if (_e.name() == "lyric-font") {
                  lyricFontFamily = _e.attributes().value("font-family").toString();
                  lyricFontSize = _e.attributes().value("font-size").toString();
                  _e.skipCurrentElement();
                  }
            else if (_e.name() == "appearance")
                  skipLogCurrElem();
            else if (_e.name() == "lyric-language")
                  skipLogCurrElem();
            else
                  skipLogCurrElem();
            }
 
      /*
       qDebug("word font family '%s' size '%s' lyric font family '%s' size '%s'",
       qPrintable(wordFontFamily), qPrintable(wordFontSize),
       qPrintable(lyricFontFamily), qPrintable(lyricFontSize));
       */
      updateStyles(_score, wordFontFamily, wordFontSize, lyricFontFamily, lyricFontSize);
 
      _score->setDefaultsRead(true); // TODO only if actually succeeded ?
      }
 
//---------------------------------------------------------
//   pageLayout
//---------------------------------------------------------
 
/**
 Parse the /score-partwise/defaults/page-layout node:
 read the page layout.
 */
 
void MusicXMLParserPass1::pageLayout(PageFormat& pf, const qreal conversion,
                                     int& pageWidth, int& pageHeight)
      {
      Q_ASSERT(_e.isStartElement() && _e.name() == "page-layout");
      logDebugTrace("MusicXMLParserPass1::pageLayout");
 
      qreal _oddRightMargin  = 0.0;
      qreal _evenRightMargin = 0.0;
      QSizeF size;
 
      while (_e.readNextStartElement()) {
            if (_e.name() == "page-margins") {
                  QString type = _e.attributes().value("type").toString();
                  if (type == "")
                        type = "both";
                  qreal lm = 0.0, rm = 0.0, tm = 0.0, bm = 0.0;
                  while (_e.readNextStartElement()) {
                        if (_e.name() == "left-margin")
                              lm = _e.readElementText().toDouble() * conversion;
                        else if (_e.name() == "right-margin")
                              rm = _e.readElementText().toDouble() * conversion;
                        else if (_e.name() == "top-margin")
                              tm = _e.readElementText().toDouble() * conversion;
                        else if (_e.name() == "bottom-margin")
                              bm = _e.readElementText().toDouble() * conversion;
                        else
                              skipLogCurrElem();
                        }
                  pf.twosided = type == "odd" || type == "even";
                  if (type == "odd" || type == "both") {
                        pf.oddLeftMargin = lm;
                        _oddRightMargin = rm;
                        pf.oddTopMargin = tm;
                        pf.oddBottomMargin = bm;
                        }
                  if (type == "even" || type == "both") {
                        pf.evenLeftMargin = lm;
                        _evenRightMargin = rm;
                        pf.evenTopMargin = tm;
                        pf.evenBottomMargin = bm;
                        }
                  }
            else if (_e.name() == "page-height") {
                  double val = _e.readElementText().toDouble();
                  size.rheight() = val * conversion;
                  // set pageHeight and pageWidth for use by doCredits()
                  pageHeight = static_cast<int>(val + 0.5);
                  }
            else if (_e.name() == "page-width") {
                  double val = _e.readElementText().toDouble();
                  size.rwidth() = val * conversion;
                  // set pageHeight and pageWidth for use by doCredits()
                  pageWidth = static_cast<int>(val + 0.5);
                  }
            else
                  skipLogCurrElem();
            }
      pf.size = size;
      qreal w1 = size.width() - pf.oddLeftMargin - _oddRightMargin;
      qreal w2 = size.width() - pf.evenLeftMargin - _evenRightMargin;
      pf.printableWidth = qMax(w1, w2);   // silently adjust right margins
      }
 
//---------------------------------------------------------
//   partList
//---------------------------------------------------------
 
/**
 Parse the /score-partwise/part-list:
 create the parts and for each part set id and name.
 Also handle the part-groups.
 */
 
void MusicXMLParserPass1::partList(MusicXmlPartGroupList& partGroupList)
      {
      Q_ASSERT(_e.isStartElement() && _e.name() == "part-list");
      logDebugTrace("MusicXMLParserPass1::partList");
 
      int scoreParts = 0; // number of score-parts read sofar
      MusicXmlPartGroupMap partGroups;
 
      while (_e.readNextStartElement()) {
            if (_e.name() == "part-group")
                  partGroup(scoreParts, partGroupList, partGroups);
            else if (_e.name() == "score-part") {
                  scorePart();
                  scoreParts++;
                  }
            else
                  skipLogCurrElem();
            }
      }
 
//---------------------------------------------------------
//   createPart
//---------------------------------------------------------
 
/**
 Create the part, set its \a id and insert it in PartMap \a pm.
 Part name (if any) will be set later.
 */
 
static void createPart(Score* score, const QString& id, PartMap& pm)
      {
      Part* part = new Part(score);
      pm.insert(id, part);
      part->setId(id);
      score->appendPart(part);
      Staff* staff = new Staff(score);
      staff->setPart(part);
      part->staves()->push_back(staff);
      score->staves().push_back(staff);
      // TODO TBD tuplets.resize(VOICES); // part now contains one staff, thus VOICES voices
      }
 
//---------------------------------------------------------
//   partGroupStart
//---------------------------------------------------------
 
typedef std::map<int,MusicXmlPartGroup*> MusicXmlPartGroupMap;
 
/**
 Store part-group start with number \a n, first part \a p and symbol / \a s in the partGroups
 map \a pgs for later reference, as at this time insufficient information is available to be able
 to generate the brackets.
 */
 
static void partGroupStart(MusicXmlPartGroupMap& pgs, int n, int p, QString s, bool barlineSpan)
      {
      //qDebug("partGroupStart number=%d part=%d symbol=%s", n, p, qPrintable(s));
 
      if (pgs.count(n) > 0) {
            qDebug("part-group number=%d already active", n);
            return;
            }
 
      BracketType bracketType = BracketType::NO_BRACKET;
      if (s == "")
            ;        // ignore (handle as NO_BRACKET)
      else if (s == "none")
            ;        // already set to NO_BRACKET
      else if (s == "brace")
            bracketType = BracketType::BRACE;
      else if (s == "bracket")
            bracketType = BracketType::NORMAL;
      else if (s == "line")
            bracketType = BracketType::LINE;
      else if (s == "square")
            bracketType = BracketType::SQUARE;
      else {
            qDebug("part-group symbol=%s not supported", qPrintable(s));
            return;
            }
 
      MusicXmlPartGroup* pg = new MusicXmlPartGroup;
      pg->span = 0;
      pg->start = p;
      pg->barlineSpan = barlineSpan,
      pg->type = bracketType;
      pgs[n] = pg;
      }
 
//---------------------------------------------------------
//   partGroupStop
//---------------------------------------------------------
 
/**
 Handle part-group stop with number \a n and part \a p.
 
 For part group n, the start part, span (in parts) and type are now known.
 To generate brackets, the span in staves must also be known.
 */
 
static void partGroupStop(MusicXmlPartGroupMap& pgs, int n, int p,
                          MusicXmlPartGroupList& pgl)
      {
      if (pgs.count(n) == 0) {
            qDebug("part-group number=%d not active", n);
            return;
            }
 
      pgs[n]->span = p - pgs[n]->start;
      //qDebug("partgroupstop number=%d start=%d span=%d type=%hhd",
      //       n, pgs[n]->start, pgs[n]->span, pgs[n]->type);
      pgl.push_back(pgs[n]);
      pgs.erase(n);
      }
 
//---------------------------------------------------------
//   partGroup
//---------------------------------------------------------
 
/**
 Parse the /score-partwise/part-list/part-group node.
 */
 
void MusicXMLParserPass1::partGroup(const int scoreParts,
                                    MusicXmlPartGroupList& partGroupList,
                                    MusicXmlPartGroupMap& partGroups)
      {
      Q_ASSERT(_e.isStartElement() && _e.name() == "part-group");
      logDebugTrace("MusicXMLParserPass1::partGroup");
      bool barlineSpan = true;
      int number = _e.attributes().value("number").toInt();
      if (number > 0) number--;
      QString symbol = "";
      QString type = _e.attributes().value("type").toString();
 
      while (_e.readNextStartElement()) {
            if (_e.name() == "group-symbol")
                  symbol = _e.readElementText();
            else if (_e.name() == "group-barline") {
                  if (_e.readElementText() == "no")
                        barlineSpan = false;
                  }
            else
                  skipLogCurrElem();
            }
 
      if (type == "start")
            partGroupStart(partGroups, number, scoreParts, symbol, barlineSpan);
      else if (type == "stop")
            partGroupStop(partGroups, number, scoreParts, partGroupList);
      else
            qDebug("MusicXMLParserPass1::partGroup: part-group type '%s' not supported",
                   qPrintable(type));  // TODO
      }
 
//---------------------------------------------------------
//   findInstrument
//---------------------------------------------------------
 
/**
 Find the first InstrumentTemplate with musicXMLid instrSound
 and a non-empty set of channels.
 */
 
static const InstrumentTemplate* findInstrument(const QString& instrSound)
      {
      const InstrumentTemplate* instr = nullptr;
 
      for (const InstrumentGroup* group : instrumentGroups) {
            for (const InstrumentTemplate* templ : group->instrumentTemplates) {
                  if (templ->musicXMLid == instrSound && !templ->channel.isEmpty()) {
                        return templ;
                        }
                  }
            }
      return instr;
      }
 
//---------------------------------------------------------
//   fixupMidiProgram
//---------------------------------------------------------
 
static void fixupMidiProgram(MusicXMLDrumset& drumset)
      {
      for (auto& instr : drumset) {
            if (instr.midiProgram < 0 && instr.sound != "") {
                  const InstrumentTemplate* templ = findInstrument(instr.sound);
                  if (templ) {
                        const int prog = templ->channel.at(0).program;
                        instr.midiProgram = prog;
                        }
                  }
            }
      }
 
//---------------------------------------------------------
//   scorePart
//---------------------------------------------------------
 
/**
 Parse the /score-partwise/part-list/score-part node:
 create the part and sets id and name.
 Note that a part is created even if no part-name is present
 which is invalid MusicXML but is (sometimes ?) generated by NWC2MusicXML.
 */
 
void MusicXMLParserPass1::scorePart()
      {
      Q_ASSERT(_e.isStartElement() && _e.name() == "score-part");
      logDebugTrace("MusicXMLParserPass1::scorePart");
      QString id = _e.attributes().value("id").toString();
 
      if (_parts.contains(id)) {
            logError(QString("duplicate part id '%1'").arg(id));
            skipLogCurrElem();
            return;
            }
      else {
            _parts.insert(id, MusicXmlPart(id));
            _drumsets.insert(id, MusicXMLDrumset());
            createPart(_score, id, _partMap);
            }
 
      while (_e.readNextStartElement()) {
            if (_e.name() == "part-name") {
                  // Element part-name contains the displayed (full) part name
                  // It is displayed by default, but can be suppressed (print-object=”no”)
                  // As of MusicXML 3.0, formatting is deprecated, with part-name in plain text
                  // and the formatted version in the part-name-display element
                  bool doLong = !(_e.attributes().value("print-object") == "no");
                  QString name = _e.readElementText();
                  _partMap[id]->setPartName(name);
                  if (doLong)
                        _partMap[id]->setLongName(name);
                  }
            else if (_e.name() == "part-name-display") {
                  // TODO
                  _e.skipCurrentElement(); // skip but don't log
                  }
            else if (_e.name() == "part-abbreviation") {
                  // Element part-name contains the displayed (abbreviated) part name
                  // It is displayed by default, but can be suppressed (print-object=”no”)
                  // As of MusicXML 3.0, formatting is deprecated, with part-name in plain text
                  // and the formatted version in the part-abbreviation-display element
                  QString name = _e.readElementText();
                  if (!(_e.attributes().value("print-object") == "no"))
                        _partMap[id]->setPlainShortName(name);
                  }
            else if (_e.name() == "part-abbreviation-display")
                  _e.skipCurrentElement();  // skip but don't log
            else if (_e.name() == "score-instrument")
                  scoreInstrument(id);
            else if (_e.name() == "midi-device") {
                  if (!_e.attributes().hasAttribute("port")) {
                        _e.readElementText(); // empty string
                        continue;
                        }
                  QString instrId = _e.attributes().value("id").toString();
                  QString port = _e.attributes().value("port").toString();
                  // If instrId is missing, the device assignment affects all
                  // score-instrument elements in the score-part
                  if (instrId.isEmpty()) {
                        for (auto it = _drumsets[id].cbegin(); it != _drumsets[id].cend(); ++it)
                              _drumsets[id][it.key()].midiPort = port.toInt() - 1;
                        }
                  else if (_drumsets[id].contains(instrId))
                        _drumsets[id][instrId].midiPort = port.toInt() - 1;
 
                  _e.readElementText(); // empty string
                  }
            else if (_e.name() == "midi-instrument")
                  midiInstrument(id);
            else
                  skipLogCurrElem();
            }
 
      fixupMidiProgram(_drumsets[id]);
 
      Q_ASSERT(_e.isEndElement() && _e.name() == "score-part");
      }
 
//---------------------------------------------------------
//   scoreInstrument
//---------------------------------------------------------
 
/**
 Parse the /score-partwise/part-list/score-part/score-instrument node.
 */
 
void MusicXMLParserPass1::scoreInstrument(const QString& partId)
      {
      Q_ASSERT(_e.isStartElement() && _e.name() == "score-instrument");
      logDebugTrace("MusicXMLParserPass1::scoreInstrument");
      QString instrId = _e.attributes().value("id").toString();
 
      while (_e.readNextStartElement()) {
            if (_e.name() == "ensemble")
                  skipLogCurrElem();
            else if (_e.name() == "instrument-name") {
                  QString instrName = _e.readElementText();
                  /*
                  qDebug("partId '%s' instrId '%s' instrName '%s'",
                         qPrintable(partId),
                         qPrintable(instrId),
                         qPrintable(instrName)
                         );
                   */
                  _drumsets[partId].insert(instrId, MusicXMLDrumInstrument(instrName));
                  // Element instrument-name is typically not displayed in the score,
                  // but used only internally
                  if (_drumsets[partId].contains(instrId))
                        _drumsets[partId][instrId].name = instrName;
                  // try to prevent an empty track name
                  if (_partMap[partId]->partName() == "")
                        _partMap[partId]->setPartName(instrName);
                  }
            else if (_e.name() == "instrument-sound") {
                  QString instrSound = _e.readElementText();
                  if (_drumsets[partId].contains(instrId))
                        _drumsets[partId][instrId].sound = instrSound;
                  }
            else if (_e.name() == "virtual-instrument") {
                  while (_e.readNextStartElement()) {
                        if (_e.name() == "virtual-library") {
                              QString virtualLibrary = _e.readElementText();
                              if (_drumsets[partId].contains(instrId))
                                    _drumsets[partId][instrId].virtLib = virtualLibrary;
                              }
                        else if (_e.name() == "virtual-name") {
                              QString virtualName = _e.readElementText();
                              if (_drumsets[partId].contains(instrId))
                                    _drumsets[partId][instrId].virtName = virtualName;
                              }
                        else
                              skipLogCurrElem();
                        }
                  }
            else
                  skipLogCurrElem();
            }
      Q_ASSERT(_e.isEndElement() && _e.name() == "score-instrument");
      }
 
//---------------------------------------------------------
//   midiInstrument
//---------------------------------------------------------
 
/**
 Parse the /score-partwise/part-list/score-part/midi-instrument node.
 */
 
void MusicXMLParserPass1::midiInstrument(const QString& partId)
      {
      Q_ASSERT(_e.isStartElement() && _e.name() == "midi-instrument");
      logDebugTrace("MusicXMLParserPass1::midiInstrument");
      QString instrId = _e.attributes().value("id").toString();
 
      while (_e.readNextStartElement()) {
            if (_e.name() == "midi-bank")
                  skipLogCurrElem();
            else if (_e.name() == "midi-channel") {
                  int channel = _e.readElementText().toInt();
                  if (channel < 1) {
                        logError(QString("MusicXml::xmlScorePart: incorrect midi-channel: %1").arg(channel));
                        channel = 1;
                        }
                  else if (channel > 16) {
                        logError(QString("MusicXml::xmlScorePart: incorrect midi-channel: %1").arg(channel));
                        channel = 16;
                        }
                  if (_drumsets[partId].contains(instrId))
                        _drumsets[partId][instrId].midiChannel = channel - 1;
                  }
            else if (_e.name() == "midi-program") {
                  int program = _e.readElementText().toInt();
                  // Bug fix for Cubase 6.5.5 which generates <midi-program>0</midi-program>
                  // Check program number range
                  if (program < 1) {
                        logError(QString("MusicXml::xmlScorePart: incorrect midi-program: %1").arg(program));
                        program = 1;
                        }
                  else if (program > 128) {
                        logError(QString("MusicXml::xmlScorePart: incorrect midi-program: %1").arg(program));
                        program = 128;
                        }
                  if (_drumsets[partId].contains(instrId))
                        _drumsets[partId][instrId].midiProgram = program - 1;
                  }
            else if (_e.name() == "midi-unpitched") {
                  if (_drumsets[partId].contains(instrId))
                        _drumsets[partId][instrId].pitch = _e.readElementText().toInt() - 1;
                  }
            else if (_e.name() == "volume") {
                  double vol = _e.readElementText().toDouble();
                  if (vol >= 0 && vol <= 100) {
                        if (_drumsets[partId].contains(instrId))
                              _drumsets[partId][instrId].midiVolume = static_cast<int>((vol / 100) * 127);
                        }
                  else
                        logError(QString("MusicXml::xmlScorePart: incorrect midi-volume: %1").arg(vol));
                  }
            else if (_e.name() == "pan") {
                  double pan = _e.readElementText().toDouble();
                  if (pan >= -90 && pan <= 90) {
                        if (_drumsets[partId].contains(instrId))
                              _drumsets[partId][instrId].midiPan = static_cast<int>(((pan + 90) / 180) * 127);
                        }
                  else
                        logError(QString("MusicXml::xmlScorePart: incorrect midi-volume: %g1").arg(pan));
                  }
            else
                  skipLogCurrElem();
            }
      Q_ASSERT(_e.isEndElement() && _e.name() == "midi-instrument");
      }
 
//---------------------------------------------------------
//   part
//---------------------------------------------------------
 
/**
 Parse the /score-partwise/part node:
 read the parts data to determine measure timing and octave shifts.
 Assign voices and staves.
 */
 
void MusicXMLParserPass1::part()
      {
      Q_ASSERT(_e.isStartElement() && _e.name() == "part");
      logDebugTrace("MusicXMLParserPass1::part");
      const QString id = _e.attributes().value("id").toString();
 
      if (!_parts.contains(id)) {
            logError(QString("MusicXMLParserPass1::part cannot find part '%1'").arg(id));
            skipLogCurrElem();
            }
 
      initPartState(id);
 
      VoiceOverlapDetector vod;
      Fraction time;  // current time within part
      Fraction mdur;  // measure duration
 
      while (_e.readNextStartElement()) {
            if (_e.name() == "measure") {
                  measure(id, time, mdur, vod);
                  time += mdur;
                  }
            else
                  skipLogCurrElem();
            }
 
      // allocate MuseScore staff to MusicXML voices
      allocateStaves(_parts[id].voicelist);
      // allocate MuseScore voice to MusicXML voices
      allocateVoices(_parts[id].voicelist);
      // calculate the octave shifts
      _parts[id].calcOctaveShifts();
      // set first instrument for multi-instrument part starting with rest
      if (_firstInstrId != "" && _firstInstrSTime > Fraction(0, 1))
            _parts[id]._instrList.setInstrument(_firstInstrId, Fraction(0, 1));
 
      // debug: print results
      /*
      qDebug("voiceMapperStats: new staff");
      VoiceList& vl = _parts[id].voicelist;
      for (auto i = vl.constBegin(); i != vl.constEnd(); ++i) {
            qDebug("voiceMapperStats: voice %s staff data %s",
                   qPrintable(i.key()), qPrintable(i.value().toString()));
            }
      */
      }
 
//---------------------------------------------------------
//   measureDurationAsFraction
//---------------------------------------------------------
 
/**
 Determine a suitable measure duration value given the time signature
 by setting the duration denominator to be greater than or equal
 to the time signature denominator
 */
 
static Fraction measureDurationAsFraction(const Fraction length, const int tsigtype)
      {
      if (tsigtype <= 0)
            // invalid tsigtype
            return length;
 
      Fraction res = length;
      while (res.denominator() < tsigtype) {
            res.setNumerator(res.numerator() * 2);
            res.setDenominator(res.denominator() * 2);
            }
      return res;
      }
 
//---------------------------------------------------------
//   measure
//---------------------------------------------------------
 
/**
 Parse the /score-partwise/part/measure node:
 read the measures data as required to determine measure timing, octave shifts
 and assign voices and staves.
 */
 
void MusicXMLParserPass1::measure(const QString& partId,
                                  const Fraction time,
                                  Fraction& mdur,
                                  VoiceOverlapDetector& vod)
      {
      Q_ASSERT(_e.isStartElement() && _e.name() == "measure");
      logDebugTrace("MusicXMLParserPass1::measure");
      QString number = _e.attributes().value("number").toString();
 
      Fraction mTime; // current time stamp within measure
      Fraction mDura; // current total measure duration
      vod.newMeasure();
 
      while (_e.readNextStartElement()) {
            if (_e.name() == "attributes")
                  attributes(partId);
            else if (_e.name() == "note") {
                  Fraction dura;
                  // note: chord and grace note handling done in note()
                  note(partId, time + mTime, dura, vod);
                  if (dura.isValid()) {
                        mTime += dura;
                        if (mTime > mDura)
                              mDura = mTime;
                        }
                  }
            else if (_e.name() == "forward") {
                  Fraction dura;
                  forward(dura);
                  if (dura.isValid()) {
                        mTime += dura;
                        if (mTime > mDura)
                              mDura = mTime;
                        }
                  }
            else if (_e.name() == "backup") {
                  Fraction dura;
                  backup(dura);
                  if (dura.isValid()) {
                        if (dura <= mTime)
                              mTime -= dura;
                        else {
                              logError("backup beyond measure start");
                              mTime.set(0, 1);
                              }
                        }
                  }
            else if (_e.name() == "direction")
                  direction(partId, time + mTime);
            else
                  skipLogCurrElem();
 
            /*
             qDebug("mTime %s (%s) mDura %s (%s)",
             qPrintable(mTime.print()),
             qPrintable(mTime.reduced().print()),
             qPrintable(mDura.print()),
             qPrintable(mDura.reduced().print()));
             */
            }
 
      // debug vod
      // vod.dump();
      // copy overlap data from vod to voicelist
      copyOverlapData(vod, _parts[partId].voicelist);
 
      // measure duration fixups
      mDura.reduce();
 
      // fix for PDFtoMusic Pro v1.3.0d Build BF4E (which sometimes generates empty measures)
      // if no valid length found and length according to time signature is known,
      // use length according to time signature
      if (mDura.isZero() && _timeSigDura.isValid() && _timeSigDura > Fraction(0, 1))
            mDura = _timeSigDura;
 
      // if necessary, round up to an integral number of 1/64s,
      // to comply with MuseScores actual measure length constraints
      // TODO: calculate in fraction
      int length = mDura.ticks();
      int correctedLength = length;
      if ((length % (MScore::division/16)) != 0) {
            correctedLength = ((length / (MScore::division/16)) + 1) * (MScore::division/16);
            mDura = Fraction::fromTicks(correctedLength);
            }
 
      // set measure duration to a suitable value given the time signature
      if (_timeSigDura.isValid() && _timeSigDura > Fraction(0, 1)) {
            int btp = _timeSigDura.denominator();
            if (btp > 0)
                  mDura = measureDurationAsFraction(mDura, btp);
            }
 
      // set return value(s)
      mdur = mDura;
 
      // set measure number and duration
      /*
      qDebug("part %s measure %s dura %s (%d)",
             qPrintable(partId), qPrintable(number), qPrintable(mdur.print()), mdur.ticks());
       */
      _parts[partId].addMeasureNumberAndDuration(number, mdur);
      }
 
//---------------------------------------------------------
//   attributes
//---------------------------------------------------------
 
/**
 Parse the /score-partwise/part/measure/attributes node.
 */
 
void MusicXMLParserPass1::attributes(const QString& partId)
      {
      Q_ASSERT(_e.isStartElement() && _e.name() == "attributes");
      logDebugTrace("MusicXMLParserPass1::attributes");
 
      while (_e.readNextStartElement()) {
            if (_e.name() == "clef")
                  clef(partId);
            else if (_e.name() == "divisions")
                  divisions();
            else if (_e.name() == "staff-details")
                  staffDetails(partId);
            else if (_e.name() == "staves")
                  staves(partId);
            else if (_e.name() == "time")
                  time();
            else
                  skipLogCurrElem();
            }
      }
 
//---------------------------------------------------------
//   clef
//---------------------------------------------------------
 
/**
 Parse the /score-partwise/part/measure/attributes/clef node.
 Set the staff type based on clef type
 TODO: check if staff type setting could be simplified
 */
 
void MusicXMLParserPass1::clef(const QString& partId)
      {
      Q_ASSERT(_e.isStartElement() && _e.name() == "clef");
      logDebugTrace("MusicXMLParserPass1::clef");
 
      QString number = _e.attributes().value("number").toString();
      int n = 0;
      if (number != "") {
            n = number.toInt();
            if (n <= 0) {
                  logError(QString("invalid number %1").arg(number));
                  n = 0;
                  }
            else
                  n--;              // make zero-based
            }
 
      StaffTypes staffType = StaffTypes::STANDARD;
 
      while (_e.readNextStartElement()) {
            if (_e.name() == "sign") {
                  QString sign = _e.readElementText();
                  if (sign == "TAB")
                        staffType = StaffTypes::TAB_DEFAULT;
                  else if (sign == "percussion")
                        staffType = StaffTypes::PERC_DEFAULT;
                  }
            else
                  skipLogCurrElem();
            }
 
      Part* part = getPart(partId);
      Q_ASSERT(part);
      int staves = part->nstaves();
      int staffIdx = _score->staffIdx(part);
 
      // TODO: changed for #55501, but now staff type init is shared between pass 1 and 2
      // old code: if (0 <= n && n < staves && staffType != StaffTypes::STANDARD)
      if (0 <= n && n < staves && staffType == StaffTypes::TAB_DEFAULT)
            _score->staff(staffIdx + n)->setStaffType(0, StaffType::preset(staffType));
      }
 
//---------------------------------------------------------
//   determineTimeSig
//---------------------------------------------------------
 
/**
 Determine the time signature based on \a beats, \a beatType and \a timeSymbol.
 Sets return parameters \a st, \a bts, \a btp.
 Return true if OK, false on error.
 */
 
// TODO: share between pass 1 and pass 2
 
static bool determineTimeSig(const QString beats, const QString beatType, const QString timeSymbol,
                             TimeSigType& st, int& bts, int& btp)
      {
      // initialize
      st  = TimeSigType::NORMAL;
      bts = 0;             // the beats (max 4 separated by "+") as integer
      btp = 0;             // beat-type as integer
      // determine if timesig is valid
      if (beats == "2" && beatType == "2" && timeSymbol == "cut") {
            st = TimeSigType::ALLA_BREVE;
            bts = 2;
            btp = 2;
            return true;
            }
      else if (beats == "4" && beatType == "4" && timeSymbol == "common") {
            st = TimeSigType::FOUR_FOUR;
            bts = 4;
            btp = 4;
            return true;
            }
      else {
            if (!timeSymbol.isEmpty() && timeSymbol != "normal") {
                  qDebug("determineTimeSig: time symbol <%s> not recognized with beats=%s and beat-type=%s",
                         qPrintable(timeSymbol), qPrintable(beats), qPrintable(beatType));       // TODO
                  return false;
                  }
 
            btp = beatType.toInt();
            QStringList list = beats.split("+");
            for (int i = 0; i < list.size(); i++)
                  bts += list.at(i).toInt();
            }
 
      // determine if bts and btp are valid
      if (bts <= 0 || btp <=0) {
            qDebug("determineTimeSig: beats=%s and/or beat-type=%s not recognized",
                   qPrintable(beats), qPrintable(beatType));         // TODO
            return false;
            }
 
      return true;
      }
 
//---------------------------------------------------------
//   time
//---------------------------------------------------------
 
/**
 Parse the /score-partwise/part/measure/attributes/time node.
 */
 
void MusicXMLParserPass1::time()
      {
      Q_ASSERT(_e.isStartElement() && _e.name() == "time");
 
      QString beats;
      QString beatType;
      QString timeSymbol = _e.attributes().value("symbol").toString();
 
      while (_e.readNextStartElement()) {
            if (_e.name() == "beats")
                  beats = _e.readElementText();
            else if (_e.name() == "beat-type")
                  beatType = _e.readElementText();
            else
                  skipLogCurrElem();
            }
 
      if (beats != "" && beatType != "") {
            // determine if timesig is valid
            TimeSigType st  = TimeSigType::NORMAL;
            int bts = 0;       // total beats as integer (beats may contain multiple numbers, separated by "+")
            int btp = 0;       // beat-type as integer
            if (determineTimeSig(beats, beatType, timeSymbol, st, bts, btp)) {
                  _timeSigDura = Fraction(bts, btp);
                  }
            }
      }
 
//---------------------------------------------------------
//   divisions
//---------------------------------------------------------
 
/**
 Parse the /score-partwise/part/measure/attributes/divisions node.
 */
 
void MusicXMLParserPass1::divisions()
      {
      Q_ASSERT(_e.isStartElement() && _e.name() == "divisions");
 
      _divs = _e.readElementText().toInt();
      if (!(_divs > 0))
            logError("illegal divisions");
      }
 
//---------------------------------------------------------
//   setStaffLines
//---------------------------------------------------------
 
/**
 Set stafflines and barline span for a single staff
 */
 
static void setStaffLines(Score* score, int staffIdx, int stafflines)
      {
      score->staff(staffIdx)->setLines(0, stafflines);
      score->staff(staffIdx)->setBarLineTo(0);  // default
      }
 
//---------------------------------------------------------
//   staffDetails
//---------------------------------------------------------
 
/**
 Parse the /score-partwise/part/measure/attributes/staff-details node.
 */
 
void MusicXMLParserPass1::staffDetails(const QString& partId)
      {
      Q_ASSERT(_e.isStartElement() && _e.name() == "staff-details");
      logDebugTrace("MusicXMLParserPass1::staffDetails");
 
      Part* part = getPart(partId);
      Q_ASSERT(part);
      int staves = part->nstaves();
 
      QString number = _e.attributes().value("number").toString();
      int n = 1; // default
      if (number != "") {
            n = number.toInt();
            if (n <= 0 || n > staves) {
                  logError(QString("invalid staff-details number %1").arg(number));
                  n = 1;
                  }
            }
      n--;        // make zero-based
 
      int staffIdx = _score->staffIdx(part) + n;
 
      StringData* t = 0;
      if (_score->staff(staffIdx)->isTabStaff(0)) {
            t = new StringData;
            t->setFrets(25);       // sensible default
            }
 
      int staffLines = 0;
      while (_e.readNextStartElement()) {
            if (_e.name() == "staff-lines") {
                  // save staff lines for later
                  staffLines = _e.readElementText().toInt();
                  // for a TAB staff also resize the string table and init with zeroes
                  if (t) {
                        if (0 < staffLines)
                              t->stringList() = QVector<instrString>(staffLines).toList();
                        else
                              logError(QString("illegal staff-lines %1").arg(staffLines));
                        }
                  }
            else if (_e.name() == "staff-tuning")
                  staffTuning(t);
            else
                  skipLogCurrElem();
            }
 
      if (staffLines > 0) {
            setStaffLines(_score, staffIdx, staffLines);
            }
 
      if (t) {
            Instrument* i = part->instrument();
            i->setStringData(*t);
            }
      }
 
//---------------------------------------------------------
//   MusicXMLStepAltOct2Pitch
//---------------------------------------------------------
 
/**
 Convert MusicXML \a step (0=C, 1=D, etc.) / \a alter / \a octave to midi pitch.
 Note: same code is in pass 1 and in pass 2.
 TODO: combine
 */
 
static int MusicXMLStepAltOct2Pitch(int step, int alter, int octave)
      {
      //                       c  d  e  f  g  a   b
      static int table[7]  = { 0, 2, 4, 5, 7, 9, 11 };
      if (step < 0 || step > 6) {
            qDebug("MusicXMLStepAltOct2Pitch: illegal step %d", step);
            return -1;
            }
      int pitch = table[step] + alter + (octave+1) * 12;
 
      if (pitch < 0)
            pitch = -1;
      if (pitch > 127)
            pitch = -1;
 
      return pitch;
      }
 
//---------------------------------------------------------
//   staffTuning
//---------------------------------------------------------
 
/**
 Parse the /score-partwise/part/measure/attributes/staff-details/staff-tuning node.
 */
 
void MusicXMLParserPass1::staffTuning(StringData* t)
      {
      Q_ASSERT(_e.isStartElement() && _e.name() == "staff-tuning");
      logDebugTrace("MusicXMLParserPass1::staffTuning");
 
      // ignore <staff-tuning> if not a TAB staff
      if (!t) {
            logError("<staff-tuning> on non-TAB staff");
            skipLogCurrElem();
            return;
            }
 
      int line   = _e.attributes().value("line").toInt();
      int step   = 0;
      int alter  = 0;
      int octave = 0;
      while (_e.readNextStartElement()) {
            if (_e.name() == "tuning-alter")
                  alter = _e.readElementText().toInt();
            else if (_e.name() == "tuning-octave")
                  octave = _e.readElementText().toInt();
            else if (_e.name() == "tuning-step") {
                  QString strStep = _e.readElementText();
                  int pos = QString("CDEFGAB").indexOf(strStep);
                  if (strStep.size() == 1 && pos >=0 && pos < 7)
                        step = pos;
                  else
                        logError(QString("invalid step '%1'").arg(strStep));
                  }
            else
                  skipLogCurrElem();
            }
 
      if (0 < line && line <= t->stringList().size()) {
            int pitch = MusicXMLStepAltOct2Pitch(step, alter, octave);
            if (pitch >= 0)
                  t->stringList()[line - 1].pitch = pitch;
            else
                  logError(QString("invalid string %1 tuning step/alter/oct %2/%3/%4")
                           .arg(line).arg(step).arg(alter).arg(octave));
            }
 
      }
 
//---------------------------------------------------------
//   staves
//---------------------------------------------------------
 
/**
 Set number of staves for part \a partId to the max value of the current value
 and the value in the <staves> element.
 */
 
void MusicXMLParserPass1::staves(const QString& partId)
      {
      Q_ASSERT(_e.isStartElement() && _e.name() == "staves");
      logDebugTrace("MusicXMLParserPass1::staves");
 
      int staves = _e.readElementText().toInt();
      if (!(staves > 0 && staves <= MAX_STAVES)) {
            logError("illegal staves");
            return;
            }
 
      Part* part = _partMap.value(partId);
      Q_ASSERT(part);
      if (staves > part->nstaves())
            part->setStaves(staves);
      }
 
//---------------------------------------------------------
//   direction
//---------------------------------------------------------
 
/**
 Parse the /score-partwise/part/measure/direction node
 to be able to handle octave-shifts, as these must be interpreted
 in musical order instead of in MusicXML file order.
 */
 
void MusicXMLParserPass1::direction(const QString& partId, const Fraction cTime)
      {
      Q_ASSERT(_e.isStartElement() && _e.name() == "direction");
 
      // note: file order is direction-type first, then staff
      // this means staff is still unknown when direction-type is handled
 
      QList<MxmlOctaveShiftDesc> starts;
      QList<MxmlOctaveShiftDesc> stops;
      int staff = 0;
 
      while (_e.readNextStartElement()) {
            if (_e.name() == "direction-type")
                  directionType(cTime, starts, stops);
            else if (_e.name() == "staff") {
                  int nstaves = getPart(partId)->nstaves();
                  QString strStaff = _e.readElementText();
                  staff = strStaff.toInt() - 1;
                  if (0 <= staff && staff < nstaves)
                        ;  //qDebug("direction staff %d", staff + 1);
                  else {
                        logError(QString("invalid staff %1").arg(strStaff));
                        staff = 0;
                        }
                  }
            else
                  _e.skipCurrentElement();
            }
 
      // handle the stops first
      foreach (auto desc, stops) {
            if (_octaveShifts.contains(desc.num)) {
                  MxmlOctaveShiftDesc prevDesc = _octaveShifts.value(desc.num);
                  if (prevDesc.tp == MxmlOctaveShiftDesc::Type::UP
                      || prevDesc.tp == MxmlOctaveShiftDesc::Type::DOWN) {
                        // a complete pair
                        _parts[partId].addOctaveShift(staff, prevDesc.size, prevDesc.time);
                        _parts[partId].addOctaveShift(staff, -prevDesc.size, desc.time);
                        }
                  else
                        logError("double octave-shift stop");
                  _octaveShifts.remove(desc.num);
                  }
            else
                  _octaveShifts.insert(desc.num, desc);
            }
 
      // then handle the starts
      foreach (auto desc, starts) {
            if (_octaveShifts.contains(desc.num)) {
                  MxmlOctaveShiftDesc prevDesc = _octaveShifts.value(desc.num);
                  if (prevDesc.tp == MxmlOctaveShiftDesc::Type::STOP) {
                        // a complete pair
                        _parts[partId].addOctaveShift(staff, desc.size, desc.time);
                        _parts[partId].addOctaveShift(staff, -desc.size, prevDesc.time);
                        }
                  else
                        logError("double octave-shift start");
                  _octaveShifts.remove(desc.num);
                  }
            else
                  _octaveShifts.insert(desc.num, desc);
            }
      }
 
//---------------------------------------------------------
//   directionType
//---------------------------------------------------------
 
/**
 Parse the /score-partwise/part/measure/direction/direction-type node.
 */
 
void MusicXMLParserPass1::directionType(const Fraction cTime,
                                        QList<MxmlOctaveShiftDesc>& starts,
                                        QList<MxmlOctaveShiftDesc>& stops)
      {
      Q_ASSERT(_e.isStartElement() && _e.name() == "direction-type");
 
      while (_e.readNextStartElement()) {
            if (_e.name() == "octave-shift") {
                  QString number = _e.attributes().value("number").toString();
                  int n = 0;
                  if (number != "") {
                        n = number.toInt();
                        if (n <= 0)
                              logError(QString("invalid number %1").arg(number));
                        else
                              n--;  // make zero-based
                        }
 
                  if (0 <= n && n < MAX_NUMBER_LEVEL) {
                        short size = _e.attributes().value("size").toShort();
                        QString type = _e.attributes().value("type").toString();
                        //qDebug("octave-shift type '%s' size %d number %d", qPrintable(type), size, n);
                        MxmlOctaveShiftDesc osDesc;
                        handleOctaveShift(cTime, type, size, osDesc);
                        osDesc.num = n;
                        if (osDesc.tp == MxmlOctaveShiftDesc::Type::UP
                            || osDesc.tp == MxmlOctaveShiftDesc::Type::DOWN)
                              starts.append(osDesc);
                        else if (osDesc.tp == MxmlOctaveShiftDesc::Type::STOP)
                              stops.append(osDesc);
                        }
                  else {
                        logError(QString("invalid octave-shift number %1").arg(number));
                        }
                  _e.skipCurrentElement();
                  }
            else
                  _e.skipCurrentElement();
            }
 
      Q_ASSERT(_e.isEndElement() && _e.name() == "direction-type");
      }
 
//---------------------------------------------------------
//   handleOctaveShift
//---------------------------------------------------------
 
void MusicXMLParserPass1::handleOctaveShift(const Fraction cTime,
                                            const QString& type, short size,
                                            MxmlOctaveShiftDesc& desc)
      {
      MxmlOctaveShiftDesc::Type tp = MxmlOctaveShiftDesc::Type::NONE;
      short sz = 0;
 
      switch (size) {
            case   8: sz =  1; break;
            case  15: sz =  2; break;
            default:
                  logError(QString("invalid octave-shift size %1").arg(size));
                  return;
            }
 
      if (!cTime.isValid() || cTime < Fraction(0, 1))
            logError("invalid current time");
 
      if (type == "up")
            tp = MxmlOctaveShiftDesc::Type::UP;
      else if (type == "down") {
            tp = MxmlOctaveShiftDesc::Type::DOWN;
            sz *= -1;
            }
      else if (type == "stop")
            tp = MxmlOctaveShiftDesc::Type::STOP;
      else {
            logError(QString("invalid octave-shift type '%1'").arg(type));
            return;
            }
 
      desc = MxmlOctaveShiftDesc(tp, sz, cTime);
      }
 
//---------------------------------------------------------
//   setFirstInstr
//---------------------------------------------------------
 
void MusicXMLParserPass1::setFirstInstr(const QString& id, const Fraction stime)
      {
      // check for valid arguments
      if (id == "" || !stime.isValid() || stime < Fraction(0, 1))
            return;
 
      // check for no instrument found yet or new earliest start time
      // note: compare using <= to catch instrument at t=0
      if (_firstInstrId == "" || stime <= _firstInstrSTime) {
            _firstInstrId = id;
            _firstInstrSTime = stime;
            }
      }
 
//---------------------------------------------------------
//   note
//---------------------------------------------------------
 
/**
 Parse the /score-partwise/part/measure/note node.
 */
 
void MusicXMLParserPass1::note(const QString& partId,
                               const Fraction sTime,
                               Fraction& dura,
                               VoiceOverlapDetector& vod)
      {
      Q_ASSERT(_e.isStartElement() && _e.name() == "note");
      //logDebugTrace("MusicXMLParserPass1::note");
 
      if (_e.attributes().value("print-spacing") == "no") {
            notePrintSpacingNo(dura);
            return;
            }
 
      //float alter = 0;
      bool chord = false;
      int dots = 0;
      bool grace = false;
      //int octave = -1;
      bool bRest = false;
      int staff = 1;
      //int step = 0;
      Fraction timeMod(1, 1);
      QString type;
      QString voice = "1";
      QString instrId;
 
      while (_e.readNextStartElement()) {
            if (_e.name() == "chord") {
                  chord = true;
                  _e.readNext();
                  }
            else if (_e.name() == "dot") {
                  dots++;
                  _e.readNext();
                  }
            else if (_e.name() == "duration")
                  duration(dura);
            else if (_e.name() == "grace") {
                  grace = true;
                  _e.readNext();
                  }
            else if (_e.name() == "instrument") {
                  instrId = _e.attributes().value("id").toString();
                  _e.readNext();
                  }
            else if (_e.name() == "pitch")
                  _e.skipCurrentElement();  // skip but don't log
            else if (_e.name() == "rest") {
                  bRest = true;
                  rest();
                  }
            else if (_e.name() == "staff") {
                  QString strStaff = _e.readElementText();
                  staff = strStaff.toInt();
                  // Bug fix for Cubase 6.5.5 which generates <staff>2</staff> in a single staff part
                  // Same fix is required in pass 1 and pass 2
                  Part* part = _partMap.value(partId);
                  Q_ASSERT(part);
                  if (staff <= 0 || staff > part->nstaves()) {
                        logError(QString("illegal staff '%1'").arg(strStaff));
                        staff = 1;
                        }
                  }
            else if (_e.name() == "time-modification")
                  timeModification(timeMod);
            else if (_e.name() == "type")
                  type = _e.readElementText();
            else if (_e.name() == "voice")
                  voice = _e.readElementText();
            else
                  skipLogCurrElem();
            }
 
      // convert staff to zero-based
      staff--;
 
      // multi-instrument handling
      setFirstInstr(instrId, sTime);
      QString prevInstrId = _parts[partId]._instrList.instrument(sTime);
      bool mustInsert = instrId != prevInstrId;
      /*
      qDebug("tick %s (%d) staff %d voice '%s' previnst='%s' instrument '%s' mustInsert %d",
             qPrintable(sTime.print()),
             sTime.ticks(),
             staff + 1,
             qPrintable(voice),
             qPrintable(prevInstrId),
             qPrintable(instrId),
             mustInsert
             );
      */
      if (mustInsert)
            _parts[partId]._instrList.setInstrument(instrId, sTime);
 
      // normalize duration
      if (dura.isValid())
            dura.reduce();
 
      // timing error check(s)
      QString errorStr;
      Fraction calcDura = calculateFraction(type, dots, timeMod);
      if (dura.isValid() && calcDura.isValid()) {
            if (dura != calcDura) {
                  errorStr = QString("calculated duration (%1) not equal to specified duration (%2)")
                        .arg(calcDura.print()).arg(dura.print());
 
                  if (bRest && type == "whole" && dura.isValid()) {
                        // Sibelius whole measure rest (not an error)
                        errorStr = "";
                        }
                  else if (grace && dura == Fraction(0, 1)) {
                        // grace note (not an error)
                        errorStr = "";
                        }
                  else {
                        const int maxDiff = 3; // maximum difference considered a rounding error
                        if (qAbs(calcDura.ticks() - dura.ticks()) <= maxDiff) {
                              errorStr += " -> assuming rounding error";
                              dura = calcDura;
                              }
                        }
 
                  // Special case:
                  // Encore generates rests in tuplets w/o <tuplet> or <time-modification>.
                  // Detect this by comparing the actual duration with the expected duration
                  // based on note type. If actual is 2/3 of expected, the rest is part
                  // of a tuplet.
                  if (bRest && !timeMod.isValid()) {
                        if (2 * calcDura.ticks() == 3 * dura.ticks()) {
                              timeMod = Fraction(2, 3);
                              errorStr += " -> assuming triplet";
                              }
                        }
                  }
            }
      else if (dura.isValid()) {
            // do not report an error for typeless (whole measure) rests
            if (!(bRest && type == ""))
                  errorStr = "calculated duration invalid, using specified duration";
            }
      else if (calcDura.isValid()) {
            if (!grace) {
                  errorStr = "specified duration invalid, using calculated duration";
                  dura = calcDura; // overrule dura
                  }
            }
      else {
            errorStr = "calculated and specified duration invalid, using 4/4";
            dura = Fraction(4, 4);
            }
 
      if (errorStr != "")
            logError(errorStr);
 
      // don't count chord or grace note duration
      // note that this does not check the MusicXML requirement that notes in a chord
      // cannot have a duration longer than the first note in the chord
      if (chord || grace)
            dura.set(0, 1);
 
      // store result
      if (dura.isValid() && dura > Fraction(0, 1)) {
            // count the chords
            if (!_parts.value(partId).voicelist.contains(voice)) {
                  VoiceDesc vs;
                  _parts[partId].voicelist.insert(voice, vs);
                  }
            _parts[partId].voicelist[voice].incrChordRests(staff);
            // determine note length for voice overlap detection
            // TODO
            vod.addNote(sTime.ticks(), (sTime + dura).ticks(), voice, staff);
            }
      }
 
//---------------------------------------------------------
//   notePrintSpacingNo
//---------------------------------------------------------
 
/**
 Parse the /score-partwise/part/measure/note node for a note with print-spacing="no".
 These are handled like a forward: only moving the time forward.
 */
 
void MusicXMLParserPass1::notePrintSpacingNo(Fraction& dura)
      {
      Q_ASSERT(_e.isStartElement() && _e.name() == "note");
      //logDebugTrace("MusicXMLParserPass1::notePrintSpacingNo");
 
      bool chord = false;
      bool grace = false;
 
      while (_e.readNextStartElement()) {
            if (_e.name() == "chord") {
                  chord = true;
                  _e.readNext();
                  }
            else if (_e.name() == "duration")
                  duration(dura);
            else if (_e.name() == "grace") {
                  grace = true;
                  _e.readNext();
                  }
            else
                  _e.skipCurrentElement();        // skip but don't log
            }
 
      // don't count chord or grace note duration
      // note that this does not check the MusicXML requirement that notes in a chord
      // cannot have a duration longer than the first note in the chord
      if (chord || grace)
            dura.set(0, 1);
 
      Q_ASSERT(_e.isEndElement() && _e.name() == "note");
      }
 
//---------------------------------------------------------
//   duration
//---------------------------------------------------------
 
/**
 Parse the /score-partwise/part/measure/note/duration node.
 */
 
void MusicXMLParserPass1::duration(Fraction& dura)
      {
      Q_ASSERT(_e.isStartElement() && _e.name() == "duration");
      //logDebugTrace("MusicXMLParserPass1::duration");
 
      dura.set(0, 0);  // invalid unless set correctly
      int intDura = _e.readElementText().toInt();
      if (intDura > 0) {
            if (_divs > 0)
                  dura.set(intDura, 4 * _divs);
            else
                  logError("illegal or uninitialized divisions");
            }
      else
            logError("illegal duration");
      //qDebug("duration %s valid %d", qPrintable(dura.print()), dura.isValid());
      }
 
//---------------------------------------------------------
//   forward
//---------------------------------------------------------
 
/**
 Parse the /score-partwise/part/measure/note/forward node.
 */
 
void MusicXMLParserPass1::forward(Fraction& dura)
      {
      Q_ASSERT(_e.isStartElement() && _e.name() == "forward");
      //logDebugTrace("MusicXMLParserPass1::forward");
 
      while (_e.readNextStartElement()) {
            if (_e.name() == "duration")
                  duration(dura);
            else if (_e.name() == "staff")
                  _e.skipCurrentElement();  // skip but don't log
            else if (_e.name() == "voice")
                  _e.skipCurrentElement();  // skip but don't log
            else
                  skipLogCurrElem();
            }
      }
 
//---------------------------------------------------------
//   backup
//---------------------------------------------------------
 
/**
 Parse the /score-partwise/part/measure/note/backup node.
 */
 
void MusicXMLParserPass1::backup(Fraction& dura)
      {
      Q_ASSERT(_e.isStartElement() && _e.name() == "backup");
      //logDebugTrace("MusicXMLParserPass1::backup");
 
      while (_e.readNextStartElement()) {
            if (_e.name() == "duration")
                  duration(dura);
            else
                  skipLogCurrElem();
            }
      }
 
//---------------------------------------------------------
//   timeModification
//---------------------------------------------------------
 
/**
 Parse the /score-partwise/part/measure/note/time-modification node.
 */
 
void MusicXMLParserPass1::timeModification(Fraction& timeMod)
      {
      Q_ASSERT(_e.isStartElement() && _e.name() == "time-modification");
      //logDebugTrace("MusicXMLParserPass1::timeModification");
 
      int intActual = 0;
      int intNormal = 0;
      QString strActual;
      QString strNormal;
 
      while (_e.readNextStartElement()) {
            if (_e.name() == "actual-notes")
                  strActual = _e.readElementText();
            else if (_e.name() == "normal-notes")
                  strNormal = _e.readElementText();
            else
                  skipLogCurrElem();
            }
 
      intActual = strActual.toInt();
      intNormal = strNormal.toInt();
      if (intActual > 0 && intNormal > 0)
            timeMod.set(intNormal, intActual);
      else {
            timeMod.set(1, 1);
            logError(QString("illegal time-modification: actual-notes %1 normal-notes %2")
                     .arg(strActual).arg(strNormal));
            }
      }
 
//---------------------------------------------------------
//   rest
//---------------------------------------------------------
 
/**
 Parse the /score-partwise/part/measure/note/rest node.
 */
 
void MusicXMLParserPass1::rest()
      {
      Q_ASSERT(_e.isStartElement() && _e.name() == "rest");
      //logDebugTrace("MusicXMLParserPass1::rest");
 
      while (_e.readNextStartElement()) {
            skipLogCurrElem();
            }
      }
 
} // namespace Ms

V668 There is no sense in testing the 'vbox' pointer against null, as the memory was allocated using the 'new' operator. The exception will be generated in the case of memory allocation error.

V614 Uninitialized variable 'pageWidth' used. Consider checking the third actual argument of the 'doCredits' function.

V614 Uninitialized variable 'pageHeight' used. Consider checking the fourth actual argument of the 'doCredits' function.

V560 A part of conditional expression is always true: 0 <= n.