//=============================================================================
//  MuseScore
//  Music Composition & Notation
//
//  Copyright (C) 2011 Werner Schweer
//
//  This program is free software; you can redistribute it and/or modify
//  it under the terms of the GNU General Public License version 2
//  as published by the Free Software Foundation and appearing in
//  the file LICENCE.GPL
//=============================================================================
 
#include "config.h"
#include "chordlist.h"
#include "score.h"
#include "xml.h"
#include "pitchspelling.h"
#include "mscore.h"
 
namespace Ms {
 
//---------------------------------------------------------
//   HChord
//---------------------------------------------------------
 
HChord::HChord(const QString& str)
      {
      static const char* const scaleNames[2][12] = {
            { "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B" },
            { "C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab", "A", "Bb", "B" }
            };
      keys = 0;
      QStringList sl = str.split(" ", QString::SkipEmptyParts);
      foreach(const QString& s, sl) {
            for (int i = 0; i < 12; ++i) {
                  if (s == scaleNames[0][i] || s == scaleNames[1][i]) {
                        operator+=(i);
                        break;
                        }
                  }
            }
      }
 
//---------------------------------------------------------
//   HChord
//---------------------------------------------------------
 
HChord::HChord(int a, int b, int c, int d, int e, int f, int g, int h, int i, int k, int l)
      {
      keys = 0;
      if (a >= 0)
            operator+=(a);
      if (b >= 0)
            operator+=(b);
      if (c >= 0)
            operator+=(c);
      if (d >= 0)
            operator+=(d);
      if (e >= 0)
            operator+=(e);
      if (f >= 0)
            operator+=(f);
      if (g >= 0)
            operator+=(g);
      if (h >= 0)
            operator+=(h);
      if (i >= 0)
            operator+=(i);
      if (k >= 0)
            operator+=(k);
      if (l >= 0)
            operator+=(l);
      }
 
//---------------------------------------------------------
//   rotate
//    rotate 12 Bits
//---------------------------------------------------------
 
void HChord::rotate(int semiTones)
      {
      while (semiTones > 0) {
            if (keys & 0x800)
                  keys = ((keys & ~0x800) << 1) + 1;
            else
                  keys <<= 1;
            --semiTones;
            }
      while (semiTones < 0) {
            if (keys & 1)
                  keys = (keys >> 1) | 0x800;
            else
                  keys >>= 1;
            ++semiTones;
            }
      }
 
//---------------------------------------------------------
//   name
//---------------------------------------------------------
 
QString HChord::name(int tpc) const
      {
      static const HChord C0(0,3,6,9);
      static const HChord C1(0,3);
 
      QString buf = tpc2name(tpc, NoteSpellingType::STANDARD, NoteCaseType::AUTO, false);
      HChord c(*this);
 
      int key = tpc2pitch(tpc);
 
      c.rotate(-key);        // transpose to C
 
      // special cases
      if (c == C0) {
            buf += "dim";
            return buf;
            }
      if (c == C1) {
            buf += "no5";
            return buf;
            }
 
      bool seven   = false;
      bool sharp9  = false;
      bool nat11   = false;
      bool sharp11 = false;
      bool nat13   = false;
      bool flat13  = false;
 
      // minor?
      if (c.contains(3)) {
            if (!c.contains(4))
                  buf += "m";
            else
                  sharp9 = true;
            }
 
      // 7
      if (c.contains(11)) {
            buf += "Maj7";
            seven = true;
            }
      else if (c.contains(10)) {
            buf += "7";
            seven = true;
            }
 
      // 4
      if (c.contains(5)) {
            if (!c.contains(4)) {
                  buf += "sus4";
                  }
            else
                  nat11 = true;
            }
 
      // 5
      if (c.contains(7)) {
            if (c.contains(6))
                  sharp11 = true;
            if (c.contains(8))
                  flat13 = true;
            }
      else {
            if (c.contains(6))
                  buf += "b5";
            if (c.contains(8))
                  buf += "#5";
            }
 
      // 6
      if (c.contains(9)) {
            if (!seven)
                  buf += "6";
            else
                  nat13 = true;
            }
 
      // 9
      if (c.contains(1))
            buf += "b9";
      if (c.contains(2))
            buf += "9";
      if (sharp9)
            buf += "#9";
 
      // 11
      if (nat11)
            buf += "11 ";
      if (sharp11)
            buf += "#11";
 
      // 13
      if (flat13)
            buf += "b13";
      if (nat13) {
            if (c.contains(1) || c.contains(2) || sharp9 || nat11 || sharp11)
                  buf += "13";
            else
                  buf += "add13";
            }
      return buf;
      }
 
//---------------------------------------------------------
//   voicing
//---------------------------------------------------------
 
QString HChord::voicing() const
      {
      QString s = "C";
      const char* names[] = { "C", " Db", " D", " Eb", " E", " F", " Gb", " G", " Ab", " A", " Bb", " B" };
 
      for (int i = 1; i < 12; i++) {
            if (contains(i))
                  s += names[i];
            }
      return s;
      }
 
 
//---------------------------------------------------------
//   print
//---------------------------------------------------------
 
void HChord::print() const
      {
      const char* names[] = { "C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab", "A", "Bb", "B" };
 
      for (int i = 0; i < 12; i++) {
            if (contains(i))
                  qDebug(" %s", names[i]);
            }
      }
 
//---------------------------------------------------------
//   add
//---------------------------------------------------------
 
void HChord::add(const QList<HDegree>& degreeList)
      {
// qDebug("HChord::add   ");print();
      // convert degrees to semitones
      static const int degreeTable[] = {
            // 1  2  3  4  5  6   7
            // C  D  E  F  G  A   B
               0, 2, 4, 5, 7, 9, 11
            };
      // factor in the degrees
      foreach(const HDegree& d, degreeList) {
            int dv  = degreeTable[(d.value() - 1) % 7] + d.alter();
            int dv1 = degreeTable[(d.value() - 1) % 7];
 
            if (d.value() == 7 && d.alter() == 0) {
                  // DEBUG: seventh degree is Bb, not B
                  //        except Maj   (TODO)
                  dv -= 1;
                  }
 
            if (d.type() == HDegreeType::ADD)
                  *this += dv;
            else if (d.type() == HDegreeType::ALTER) {
                  if (contains(dv1)) {
                        *this -= dv1;
                        *this += dv;
                        }
                  else {
//                        qDebug("HDegreeType::ALTER: chord does not contain degree %d(%d):",
//                           d.value(), d.alter());
//                        print();
                        *this += dv;      // DEBUG: default to add
                        }
                  }
            else if (d.type() == HDegreeType::SUBTRACT) {
                  if (contains(dv1))
                        *this -= dv1;
                  else {
                        qDebug("SUB: chord does not contain degree %d(%d):",
                           d.value(), d.alter());
                        }
                  }
            else
                  qDebug("degree type %d not supported", static_cast<int>(d.type()));
 
// qDebug("  HCHord::added  "); print();
            }
      }
 
//---------------------------------------------------------
//   readRenderList
//---------------------------------------------------------
 
static void readRenderList(QString val, QList<RenderAction>& renderList)
      {
      renderList.clear();
      QStringList sl = val.split(" ", QString::SkipEmptyParts);
      foreach(const QString& s, sl) {
            if (s.startsWith("m:")) {
                  QStringList ssl = s.split(":", QString::SkipEmptyParts);
                  if (ssl.size() == 3) {
                        RenderAction a;
                        a.type = RenderAction::RenderActionType::MOVE;
                        a.movex = ssl[1].toDouble();
                        a.movey = ssl[2].toDouble();
                        renderList.append(a);
                        }
                  }
            else if (s == ":push")
                  renderList.append(RenderAction(RenderAction::RenderActionType::PUSH));
            else if (s == ":pop")
                  renderList.append(RenderAction(RenderAction::RenderActionType::POP));
            else if (s == ":n")
                  renderList.append(RenderAction(RenderAction::RenderActionType::NOTE));
            else if (s == ":a")
                  renderList.append(RenderAction(RenderAction::RenderActionType::ACCIDENTAL));
            else {
                  RenderAction a(RenderAction::RenderActionType::SET);
                  a.text = s;
                  renderList.append(a);
                  }
            }
      }
 
//---------------------------------------------------------
//   writeRenderList
//---------------------------------------------------------
 
static void writeRenderList(XmlWriter& xml, const QList<RenderAction>* al, const QString& name)
      {
      QString s;
 
      int n = al->size();
      for (int i = 0; i < n; ++i) {
            if (!s.isEmpty())
                  s += " ";
            const RenderAction& a = (*al)[i];
            switch(a.type) {
                  case RenderAction::RenderActionType::SET:
                        s += a.text;
                        break;
                  case RenderAction::RenderActionType::MOVE:
                        if (a.movex != 0.0 || a.movey != 0.0)
                              s += QString("m:%1:%2").arg(a.movex).arg(a.movey);
                        break;
                  case RenderAction::RenderActionType::PUSH:
                        s += ":push";
                        break;
                  case RenderAction::RenderActionType::POP:
                        s += ":pop";
                        break;
                  case RenderAction::RenderActionType::NOTE:
                        s += ":n";
                        break;
                  case RenderAction::RenderActionType::ACCIDENTAL:
                        s += ":a";
                        break;
                  }
            }
      xml.tag(name, s);
      }
 
//---------------------------------------------------------
//  read
//---------------------------------------------------------
 
void ChordToken::read(XmlReader& e)
      {
      QString c = e.attribute("class");
      if (c == "quality")
            tokenClass = ChordTokenClass::QUALITY;
      else if (c == "extension")
            tokenClass = ChordTokenClass::EXTENSION;
      else if (c == "modifier")
            tokenClass = ChordTokenClass::MODIFIER;
      else
            tokenClass = ChordTokenClass::ALL;
      while (e.readNextStartElement()) {
            const QStringRef& tag(e.name());
            if (tag == "name")
                  names += e.readElementText();
            else if (tag == "render")
                  readRenderList(e.readElementText(), renderList);
            }
      }
 
//---------------------------------------------------------
//  write
//---------------------------------------------------------
 
void ChordToken::write(XmlWriter& xml) const
      {
      QString t = "token";
      switch (tokenClass) {
            case ChordTokenClass::QUALITY:
                  t += " class=\"quality\"";
                  break;
            case ChordTokenClass::EXTENSION:
                  t += " class=\"extension\"";
                  break;
            case ChordTokenClass::MODIFIER:
                  t += " class=\"modifier\"";
                  break;
            default:
                  break;
      }
      xml.stag(t);
      foreach(const QString& s, names)
            xml.tag("name", s);
      writeRenderList(xml, &renderList, "render");
      xml.etag();
      }
 
//---------------------------------------------------------
//  ParsedChord
//---------------------------------------------------------
 
ParsedChord::ParsedChord()
      {
      _parseable = false;
      _understandable = false;
      }
 
//---------------------------------------------------------
//  configure
//---------------------------------------------------------
 
void ParsedChord::configure(const ChordList* cl)
      {
      if (!cl)
            return;
      // TODO: allow this to be parameterized via chord list
      major << "ma" << "maj" << "major" << "t" << "^";
      minor << "mi" << "min" << "minor" << "-" << "=";
      diminished << "dim" << "o";
      augmented << "aug" << "+";
      lower << "b" << "-" << "dim";
      raise << "#" << "+" << "aug";
      mod1 << "sus" << "alt";
      mod2 << "sus" << "add" << "no" << "omit" << "^";
      symbols << "t" << "^" << "-" << "+" << "o" << "0";
      }
 
//---------------------------------------------------------
//  correctXmlText
//    remove digits from _xmlText, optionally replace with s
//    needed for m(Maj9) et al
//---------------------------------------------------------
 
void ParsedChord::correctXmlText(const QString& s)
      {
      _xmlText.remove(QRegExp("[0-9]"));
      if (s != "") {
            int pos = _xmlText.lastIndexOf(')');
            if (pos == -1)
                  pos = _xmlText.size();
            _xmlText.insert(pos,s);
            }
      }
 
//---------------------------------------------------------
//  parse
//    returns true if chord was parseable
//---------------------------------------------------------
 
bool ParsedChord::parse(const QString& s, const ChordList* cl, bool syntaxOnly, bool preferMinor)
      {
      QString tok1, tok1L, tok2, tok2L;
      QString extensionDigits = "123456789";
      QString special = "()[],/\\ ";
      QString leading = "([ ";
      QString trailing = ")],/\\ ";
      QString initial;
      bool take6 = false, take7 = false, take9 = false, take11 = false, take13 = false;
#if 0
// enable this to allow quality or extension to be parenthesized
      int firstLeadingToken;
#endif
      int lastLeadingToken;
      int len = s.size();
      int i, j;
      int thirdKey = 0, seventhKey = 0;
      bool susChord = false;
      QList<HDegree> hdl;
      int key[] = { 0, 0, 2, 4, 5, 7, 9, 11, 0, 2, 4, 5, 7, 9, 11 };
 
      configure(cl);
      _name = s;
      _parseable = true;
      _understandable = true;
      i = 0;
 
#if 0
// enable this code to allow quality to be parenthesized
// otherwise, parentheses will automatically cause contents to be interpreted as extension or modifier
      // eat leading parens
      firstLeadingToken = _tokenList.size();
      while (i < len && leading.contains(s[i]))
           addToken(QString(s[i++]),ChordTokenClass::QUALITY);
#endif
      lastLeadingToken = _tokenList.size();
      // get quality
      for (j = 0, tok1 = "", tok1L = "", initial = ""; i < len; ++i, ++j) {
            // up to first (non-zero) digit, paren, or comma
            if (extensionDigits.contains(s[i]) || special.contains(s[i]))
                  break;
            tok1[j] = s[i];
            tok1L[j] = s[i].toLower();
            if (tok1L == "m" || major.contains(tok1L) || minor.contains(tok1L) || diminished.contains(tok1L) || augmented.contains(tok1L))
                  initial = tok1;
            }
      // special case for "madd", which needs to parse as m,add rather than ma,dd
      if (tok1L.startsWith("madd"))
            initial = tok1[0];
      // quality and first modifier ran together with no separation - eg, mima7, augadd
      // keep quality portion, reset index to read modifier portion later
      if (initial != "" && initial != tok1 && tok1L != "tristan" && tok1L != "omit") {
            i -= (tok1.length() - initial.length());
            tok1 = initial;
            tok1L = initial.toLower();
            }
      // determine quality
      if (tok1 == "M" || major.contains(tok1L)) {
            _quality = "major";
            take6 = true; take7 = true; take9 = true; take11 = true; take13 = true;
            if (!syntaxOnly)
                  chord = HChord("C E G");
            }
      else if (tok1 == "m" || minor.contains(tok1L)) {
            _quality = "minor";
            take6 = true; take7 = true; take9 = true; take11 = true; take13 = true;
            if (!syntaxOnly)
                  chord = HChord("C Eb G");
            }
      else if (diminished.contains(tok1L)) {
            _quality = "diminished";
            take7 = true;
            if (!syntaxOnly)
                  chord = HChord("C Eb Gb");
            }
      else if (augmented.contains(tok1L)) {
            _quality = "augmented";
            take7 = true;
            if (!syntaxOnly)
                  chord = HChord("C E G#");
            }
      else if (tok1L == "0") {
            _quality = "half-diminished";
            if (!syntaxOnly)
                  chord = HChord("C Eb Gb Bb");
            }
      else if (tok1L == "") {
            // empty quality - this will turn out to be major or dominant (or minor if preferMinor)
            _quality = "";
            if (!syntaxOnly)
                  chord = preferMinor ? HChord("C Eb G") : HChord("C E G");
            if (preferMinor)
                  _name = "=" + _name;
            }
      else {
            // anything else is not a quality after all, but a modifier
            // reset to read again as modifier
            _quality = "";
            tok1 = "";
            tok1L = "";
            i = lastLeadingToken;
            if (!syntaxOnly)
                  chord = HChord("C E G");
            }
      if (tok1L == "=") {
            tok1 = "";
            tok1L = "";
            }
      if (tok1 != "")
            addToken(tok1,ChordTokenClass::QUALITY);
#if 0
// enable this code to allow quality to be parenthesized
// otherwise, parentheses will automatically cause contents to be interpreted as extension or modifier
      else {
            // leading tokens were not really ChordTokenClass::QUALITY
            for (int t = firstLeadingToken; t < lastLeadingToken; ++t)
                  _tokenList[t].tokenClass = ChordTokenClass::EXTENSION;
            }
#endif
      if (!syntaxOnly) {
            _xmlKind = _quality;
            _xmlParens = "no";
            if (symbols.contains(tok1)) {
                  _xmlSymbols = "yes";
                  _xmlText = "";
                  }
            else {
                  _xmlSymbols = "no";
                  _xmlText = tok1;
                  }
            }
      // eat trailing parens and commas
      while (i < len && trailing.contains(s[i]))
           addToken(QString(s[i++]),ChordTokenClass::QUALITY);
 
#if 0
// enable this code to allow extensions to be parenthesized
// otherwise, parentheses will automatically cause contents to be interpreted as modifier
      // eat leading parens
      firstLeadingToken = _tokenList.size();
      while (i < len && leading.contains(s[i]))
            addToken(QString(s[i++]),ChordTokenClass::EXTENSION);
#endif
      lastLeadingToken = _tokenList.size();
      // get extension - up to first non-digit
      for (j = 0, tok1 = ""; i < len; ++i, ++j) {
            if (!s[i].isDigit())
                  break;
            tok1[j] = s[i];
            }
      _extension = tok1;
      if (_quality == "") {
            if (_extension == "7" || _extension == "9" || _extension == "11" || _extension == "13") {
                  _quality = preferMinor ? "minor" : "dominant";
                  if (!syntaxOnly)
                        _xmlKind = preferMinor ? "minor" : "dominant";
                  take7 = true; take9 = true; take11 = true; take13 = true;
                  }
            else {
                  _quality = preferMinor ? "minor" : "major";
                  if (!syntaxOnly)
                        _xmlKind = preferMinor ? "minor" : "major";
                  take6 = true; take7 = true; take9 = true; take11 = true; take13 = true;
                  }
            }
      if (tok1 != "")
            addToken(tok1,ChordTokenClass::EXTENSION);
#if 0
// enable this code to allow extensions to be parenthesized
// otherwise, parentheses will automatically cause contents to be interpreted as modifier
      else {
            // leading tokens were not really ChordTokenClass::EXTENSION
            for (int t = firstLeadingToken; t < lastLeadingToken; ++t) {
                  _tokenList[t].tokenClass = ChordTokenClass::MODIFIER;
                  if (!syntaxOnly)
                        _xmlParens = "yes";
                  }
            }
#endif
      if (!syntaxOnly) {
            if (_quality == "minor")
                  thirdKey = 3;
            else
                  thirdKey = 4;
            if (_quality == "major")
                  seventhKey = 11;
            else if (_quality == "diminished")
                  seventhKey = 9;
            else
                  seventhKey = 10;
            _xmlText += _extension;
            QStringList extl;
            if (tok1 == "2") {
                  QString d = "add" + tok1;
                  _xmlDegrees += d;
                  _xmlText.remove(tok1);
                  chord += 2;
                  }
            else if (tok1 == "4") {
                  QString d = "add" + tok1;
                  _xmlDegrees += d;
                  _xmlText.remove(tok1);
                  chord += 5;
                  }
            else if (tok1 == "5") {
                  _xmlKind = "power";
                  chord -= thirdKey;
                  }
            else if (tok1 == "6") {
                  if (take6)
                        _xmlKind += "-sixth";
                  else
                        extl << "6";
                  chord += 9;
                  }
            else if (tok1 == "7") {
                  if (take7)
                        _xmlKind += "-seventh";
                  else if (_xmlKind != "half-diminished")
                        extl << "7";
                  chord += seventhKey;
                  }
            else if (tok1 == "9") {
                  if (take9)
                        _xmlKind += "-ninth";
                  else if (take7) {
                              _xmlKind += "-seventh";
                              extl << "9";
                              correctXmlText("7");
                              }
                  else if (_xmlKind == "half-diminished") {
                        extl << "9";
                        correctXmlText();
                        }
                  else {
                        extl << "7" << "9";
                        correctXmlText();
                        }
                  chord += seventhKey;
                  chord += 2;
                  }
            else if (tok1 == "11") {
                  if (take11)
                        _xmlKind += "-11th";
                  else if (take7) {
                        _xmlKind += "-seventh";
                        extl << "9" << "11";
                        correctXmlText("7");
                        }
                  else if (_xmlKind == "half-diminished") {
                        extl << "9" << "11";
                        correctXmlText();
                        }
                  else {
                        extl << "7" << "9" << "11";
                        correctXmlText();
                        }
                  chord += seventhKey;
                  chord += 2;
                  chord += 5;
                  }
            else if (tok1 == "13") {
                  if (take13)
                        _xmlKind += "-13th";
                  else if (take7) {
                        _xmlKind += "-seventh";
                        extl << "9" << "11" << "13";
                        correctXmlText("7");
                        }
                  else if (_xmlKind == "half-diminished") {
                        extl << "9" << "11" << "13";
                        correctXmlText();
                        }
                  else {
                        extl << "7" << "9" << "11" << "13";
                        correctXmlText();
                        }
                  chord += seventhKey;
                  chord += 2;
                  chord += 5;
                  chord += 9;
                  }
            else if (tok1 == "69") {
                  if (take6) {
                        _xmlKind += "-sixth";
                        extl << "9";
                        correctXmlText("6");
                        }
                  else {
                        extl << "6" << "9";
                        correctXmlText();
                        }
                  chord += 9;
                  chord += 2;
                  }
            foreach (QString e, extl) {
                  QString d = "add" + e;
                  _xmlDegrees += d;
                  }
            if (_xmlKind == "dominant-seventh")
                  _xmlKind = "dominant";
            }
      // eat trailing parens and commas
      while (i < len && trailing.contains(s[i]))
           addToken(QString(s[i++]),ChordTokenClass::EXTENSION);
 
      // get modifiers
      bool addPending = false;
      _modifierList.clear();
      while (i < len) {
            // eat leading parens
            while (i < len && leading.contains(s[i])) {
                  addToken(QString(s[i++]),ChordTokenClass::MODIFIER);
                  _xmlParens = "yes";
                  }
            // get first token - up to first digit, paren, or comma
            for (j = 0, tok1 = "", tok1L = "", initial = ""; i < len; ++i, ++j) {
                  if (s[i].isDigit() || special.contains(s[i]))
                        break;
                  tok1[j] = s[i];
                  tok1L[j] = s[i].toLower();
                  if (mod2.contains(tok1L))
                        initial = tok1;
                  }
            // if we reached the end of the string and never got a token,
            // then nothing to do, and no sense in looking for a second token
            if (i == len && tok1 == "")
                  break;
            if (initial != "" && initial != tok1) {
                  // two modifiers ran together with no separation - eg, susb9
                  // keep first, reset index to read second later
                  i -= (tok1.length() - initial.length());
                  tok1 = initial;
                  tok1L = initial.toLower();
                  }
            // for "add", just add the token and then read argument as a separate modifier
            // this allows the argument to itself be a two-part string
            // thus allowing addb9 -> add;b,9
            if (tok1L == "add") {
                  addToken(tok1,ChordTokenClass::MODIFIER);
                  addPending = true;
                  continue;
                  }
            // eat spaces
            while (i < len && s[i] == ' ')
                  ++i;
            // get second token - a number <= 13
            for (j = 0, tok2 = ""; i < len; ++i) {
                  if (!s[i].isDigit())
                        break;
                  if (j == 1 && (tok2[0] != '1' || s[i] > '3'))
                        break;
                  tok2[j++] = s[i];
                  }
            tok2L = tok2.toLower();
            // re-attach "add"
            if (addPending) {
                  if (raise.contains(tok1L))
                        tok1L = "#";
                  else if (lower.contains(tok1L))
                        tok1L = "b";
                  else if (tok1 == "M" || major.contains(tok1L))
                        tok1L = "major";
                  tok2L = tok1L + tok2L;
                  tok1L = "add";
                  }
            // standardize spelling
            if (tok1 == "M" || major.contains(tok1L))
                  tok1L = "major";
            else if (tok1L == "omit")
                  tok1L = "no";
            else if (tok1L == "sus" && tok2L == "")
                  tok2L = "4";
            else if (augmented.contains(tok1L) && tok2L == "") {
                  if (_quality == "dominant" && _extension == "7") {
                        _quality = "augmented";
                        if (!syntaxOnly) {
                              _xmlKind = "augmented-seventh";
                              _xmlText = _extension + tok1;
                              chord -= 7;
                              chord += 8;
                              }
                        tok1L = "";
                        }
                  else {
                        tok1L = "#";
                        tok2L = "5";
                        }
                  }
            else if ((lower.contains(tok1L) || raise.contains(tok1L)) && tok2L == "") {
                  // trailing alteration - treat as applying to extension (and convert to modifier)
                  // this handles C5b, C9#, etc
                  tok2L = _extension;
                  if (!syntaxOnly) {
                        _xmlKind = (_quality == "dominant") ? "major" : _quality;
                        _xmlText.remove(_extension);
                        if (_extension == "5")
                              chord += thirdKey;
                        else
                              chord -= seventhKey;
                        }
                  if (_quality == "dominant")
                        _quality = "major";
                  _extension = "";
                  if (lower.contains(tok1L))
                        tok1L = "b";
                  else
                        tok1L = "#";
                  }
            else if (lower.contains(tok1L))
                  tok1L = "b";
            else if (raise.contains(tok1L))
                  tok1L = "#";
            QString m = tok1L + tok2L;
            if (m != "")
                  _modifierList += m;
            if (tok1 != "")
                  addToken(tok1,ChordTokenClass::MODIFIER);
            if (tok2 != "")
                  addToken(tok2,ChordTokenClass::MODIFIER);
            if (!syntaxOnly) {
                  int d;
                  if (tok2L == "")
                        d = 0;
                  else
                        d = tok2L.toInt();
                  if (d > 13)
                        d = 13;
                  QString degree;
                  bool alter = false;
                  if (tok1L == "add") {
                        if (d)
                              hdl += HDegree(d, 0, HDegreeType::ADD);
                        else if (tok2L != "") {
                              // this was result of addPending
                              // alteration; tok1 = alter, tok2 = value
                              d = tok2.toInt();
                              if (raise.contains(tok1) || tok1 == "M" || major.contains(tok1.toLower())) {
                                    if (d == 7) {
                                          chord += 11;
                                          tok2L = "#7";
                                          }
                                    else if (raise.contains(tok1))
                                          hdl += HDegree(d, 1, HDegreeType::ADD);
                                    }
                              else if (lower.contains(tok1)) {
                                    if (d == 7)
                                          chord += 10;
                                    else
                                          hdl += HDegree(d, -1, HDegreeType::ADD);
                                    }
                              else if (d)
                                    hdl += HDegree(d, 0, HDegreeType::ADD);
                              }
                        degree = "add" + tok2L;
                        }
                  else if (tok1L == "no") {
                        degree = "sub" + tok2L;
                        if (d)
                              hdl += HDegree(d, 0, HDegreeType::SUBTRACT);
                        }
                  else if (tok1L == "sus") {
#if 0
// enable this code to export 7sus4 as dominant,sub3,add4 rather than suspended-fourth,add7 (similar for 9sus4, 13sus4)
// should basically work, but not fully tested
// probably needs corresponding special casing at end of loop
                        if (_extension == "") {
                              if (tok2L == "4")
                                    _xmlKind = "suspended-fourth";
                              else if (tok2L == "2")
                                    _xmlKind = "suspended-second";
                              _xmlText = tok1 + tok2;
                              }
                        else {
                              _xmlDegrees += "sub3";
                              tok1L = "add";
                              }
#else
// enable this code to export 7sus4 as suspended-fourth,add7 rather than dominant,sub3,add4 (similar for 9sus4, 13sus4)
                        // convert chords with sus into suspended "kind"
                        // extension then becomes a series of degree adds
                        if (tok2L == "4")
                              _xmlKind = "suspended-fourth";
                        else if (tok2L == "2")
                              _xmlKind = "suspended-second";
                        _xmlText = tok1 + tok2;
                        if (_extension == "7" || _extension == "9" || _extension == "11" || _extension == "13") {
                              _xmlDegrees += (_quality == "major") ? "add#7" : "add7";
                              // hack for programs that cannot assemble names well
                              // even though the kind is suspended, set text to also include the extension
                              // in export, we will set the degree text to null
                              _xmlText = _extension + _xmlText;
                              degree = "";
                              }
                        else if (_extension != "")
                              degree = "add" + _extension;
                        if (_extension == "13") {
                              _xmlDegrees += "add9";
                              _xmlDegrees += "add11";
                              _xmlDegrees += "add13";
                        }
                        else if (_extension == "11") {
                              _xmlDegrees += "add9";
                              _xmlDegrees += "add11";
                              }
                        else if (_extension == "9")
                              _xmlDegrees += "add9";
#endif
                        susChord = true;
                        chord -= thirdKey;
                        if (d)
                              chord += key[d];
                        }
                  else if (tok1L == "major") {
                        if (_xmlKind.startsWith("minor")) {
                              _xmlKind = "major-minor";
                              if (_extension == "9" || tok2L == "9")
                                    _xmlDegrees += "add9";
                              if (_extension == "11" || tok2L == "11") {
                                    _xmlDegrees += "add9";
                                    _xmlDegrees += "add11";
                                    }
                              if (_extension == "13" || tok2L == "13") {
                                    _xmlDegrees += "add9";
                                    _xmlDegrees += "add11";
                                    _xmlDegrees += "add13";
                                    }
                              _xmlText += tok1 + tok2;
                              correctXmlText("7");
                              }
                        else
                              tok1L = "add";
                        chord -= 10;
                        chord += 11;
                        if (d && d != 7)
                              hdl += HDegree(d, 0, HDegreeType::ADD);
                        }
                  else if (tok1L == "alt") {
                        _xmlDegrees += "altb5";
                        _xmlDegrees += "add#5";
                        _xmlDegrees += "addb9";
                        _xmlDegrees += "add#9";
                        chord -= 7;
                        chord += 6;
                        chord += 8;
                        chord += 1;
                        chord += 3;
                        }
                  else if (tok1L == "blues") {
                        // this isn't really well-defined, but it might as well mean something
                        if (_extension == "11" || _extension == "13")
                              _xmlDegrees += "alt#9";
                        else
                              _xmlDegrees += "add#9";
                        chord += 3;
                        }
                  else if (tok1L == "lyd") {
                        if (_extension == "13")
                              _xmlDegrees += "alt#11";
                        else
                              _xmlDegrees += "add#11";
                        chord += 6;
                        }
                  else if (tok1L == "phryg") {
                        if (!_xmlKind.startsWith("minor"))
                              _xmlKind = "minor-seventh";
                        if (_extension == "11" || _extension == "13")
                              _xmlDegrees += "altb9";
                        else
                              _xmlDegrees += "addb9";
                        _xmlText += tok1;
                        chord = HChord("C Db Eb G Bb");
                        }
                  else if (tok1L == "tristan") {
                        _xmlKind = "Tristan";
                        _xmlText = tok1;
                        chord = HChord("C F# A# D#");
                        }
                  else if (addPending) {
                        degree = "add" + tok1L + tok2L;
                        if (raise.contains(tok1L))
                              hdl += HDegree(d, 1, HDegreeType::ADD);
                        else if (lower.contains(tok1L))
                              hdl += HDegree(d, -1, HDegreeType::ADD);
                        else
                              hdl += HDegree(d, 0, HDegreeType::ADD);
                        }
                  else if (tok1L == "" && tok2L != "") {
                        degree = "add" + tok2L;
                        hdl += HDegree(d, 0, HDegreeType::ADD);
                        }
                  else if (lower.contains(tok1L)) {
                        tok1L = "b";
                        alter = true;
                        }
                  else if (raise.contains(tok1L)) {
                        tok1L = "#";
                        alter = true;
                        }
                  else {
                        _understandable = false;
                        if (s.startsWith(tok1)) {
                              // unrecognized token right from very beginning
                              _xmlKind = "other";
                              _xmlText = tok1;
                              }
                        }
                  if (alter) {
                        if (tok2L == "4" && _xmlKind == "suspended-fourth")
                              degree = "alt";
                        else if (tok2L == "5")
                              degree = "alt";
                        else if (tok2L == "9" && (_extension == "11" || _extension == "13"))
                              degree = "alt";
                        else if (tok2L == "11" && _extension == "13")
                              degree = "alt";
                        else
                              degree = "add";
                        degree += tok1L + tok2L;
                        if (chord.contains(key[d]) && !(susChord && (d == 11)))
                              hdl += HDegree(d, 0, HDegreeType::SUBTRACT);
                        if (tok1L == "#")
                              hdl += HDegree(d, 1, HDegreeType::ADD);
                        else if (tok1L == "b")
                              hdl += HDegree(d, -1, HDegreeType::ADD);
                        }
                  if (degree != "")
                        _xmlDegrees += degree;
                  }
            // eat trailing parens and commas
            while (i < len && trailing.contains(s[i]))
                  addToken(QString(s[i++]),ChordTokenClass::MODIFIER);
            addPending = false;
            }
      if (!syntaxOnly) {
            chord.add(hdl);
            // fix "add" / "alt" conflicts
            // so add9,altb9 -> addb9
            QStringList altList = _xmlDegrees.filter("alt");
            foreach (const QString& d, altList) {
                  QString unalt(d);
                  unalt.replace(QRegExp("alt[b#]"),"add");
                  if (_xmlDegrees.removeAll(unalt) > 0) {
                        QString alt(d);
                        alt.replace("alt","add");
                        int i = _xmlDegrees.indexOf(d);
                        _xmlDegrees.replace(i,alt);
                        }
                  }
            }
 
      // construct handle
      if (!_modifierList.empty()) {
            _modifierList.sort();
            _modifiers = "<" + _modifierList.join("><") + ">";
            }
      _handle = "<" + _quality + "><" + _extension + ">" + _modifiers;
 
      // force <minor><7><b5> to export as half-diminished
      if (!syntaxOnly && _handle == "<minor><7><b5>") {
                  _xmlKind = "half-diminished";
                  _xmlText = s;
                  _xmlDegrees.clear();
            }
      if (MScore::debugMode) {
            qDebug("parse: source = <%s>, handle = %s", qPrintable(s), qPrintable(_handle));
            if (!syntaxOnly) {
                  qDebug("parse: HChord = <%s> (%d)", qPrintable(chord.voicing()), chord.getKeys());
                  qDebug("parse: xmlKind = <%s>, text = <%s>", qPrintable(_xmlKind), qPrintable(_xmlText));
                  qDebug("parse: xmlSymbols = %s, xmlParens = %s", qPrintable(_xmlSymbols), qPrintable(_xmlParens));
                  qDebug("parse: xmlDegrees = <%s>", qPrintable(_xmlDegrees.join(",")));
                  }
            }
      return _parseable;
      }
 
//---------------------------------------------------------
//   fromXml
//---------------------------------------------------------
 
QString ParsedChord::fromXml(const QString& rawKind, const QString& rawKindText, const QString& useSymbols, const QString& useParens, const QList<HDegree>& dl, const ChordList* cl)
      {
      QString kind = rawKind;
      QString kindText = rawKindText;
      bool symbols = (useSymbols == "yes");
      bool parens = (useParens == "yes");
      bool implied = false;
      bool extend = false;
      int extension = 0;
      _parseable = true;
      _understandable = true;
 
      // get quality info from kind
      if (kind == "major-minor") {
            _quality = "minor";
            _modifierList += "major7";
            extend = true;
            }
      else if (kind.contains("major")) {
            _quality = "major";
            if (kind == "major" || kind == "major-sixth")
                  implied = true;
            }
      else if (kind.contains("minor"))
            _quality = "minor";
      else if (kind.contains("dominant")) {
            _quality = "dominant";
            implied = true;
            extension = 7;
            }
      else if (kind == "augmented-seventh") {
            _quality = "augmented";
            extension = 7;
            extend = true;
            }
      else if (kind == "augmented")
            _quality = "augmented";
      else if (kind == "half-diminished") {
            _quality = "half-diminished";
            if (symbols) {
                  extension = 7;
                  extend = true;
                  }
            }
      else if (kind == "diminished-seventh") {
            _quality = "diminished";
            extension = 7;
            }
      else if (kind == "diminished")
            _quality = "diminished";
      else if (kind == "suspended-fourth") {
            _quality = "major";
            implied = true;
            _modifierList += "sus4";
            }
      else if (kind == "suspended-second") {
            _quality = "major";
            implied = true;
            _modifierList += "sus2";
            }
      else if (kind == "power") {
            _quality = "major";
            implied = true;
            extension = 5;
            }
      else
            _quality = kind;
 
      // get extension info from kind
      if (kind.contains("seventh"))
            extension = 7;
      else if (kind.contains("ninth"))
            extension = 9;
      else if (kind.contains("11th"))
            extension = 11;
      else if (kind.contains("13th"))
            extension = 13;
      else if (kind.contains("sixth"))
            extension = 6;
 
      // get modifier info from degree list
      foreach (const HDegree& d, dl) {
            QString mod;
            int v = d.value();
            switch (d.type()) {
                  case HDegreeType::ADD:
                  case HDegreeType::ALTER:
                        switch (d.alter()) {
                              case -1:    mod = "b"; break;
                              case 1:     mod = "#"; break;
                              case 0:     mod = "add"; break;
                              }
                        break;
                  case HDegreeType::SUBTRACT:
                        mod = "no";
                        break;
                  case HDegreeType::UNDEF:
                  default:
                        break;
                  }
            mod += QString("%1").arg(v);
            if (mod == "add7" && kind.contains("suspended")) {
                  _quality = "dominant";
                  implied = true;
                  extension = 7;
                  extend = true;
                  mod = "";
                  }
            else if (mod == "add#7" && kind.contains("suspended")) {
                  _quality = "major";
                  implied = false;
                  extension = 7;
                  extend = true;
                  mod = "";
                  }
            else if (mod == "add9" && extend) {
                  if (extension < 9)
                        extension = 9;
                  mod = "";
                  }
            else if (mod == "add11" && extend) {
                  if (extension < 11)
                        extension = 11;
                  mod = "";
                  }
            else if (mod == "add13" && extend) {
                  extension = 13;
                  mod = "";
                  }
            else if (mod == "add9" && kind.contains("sixth")) {
                  extension = 69;
                  mod = "";
                  }
            if (mod != "")
                  _modifierList += mod;
            }
      // convert no3,add[42] into sus[42]
      int no3 = _modifierList.indexOf("no3");
      if (no3 >= 0) {
            int addn = _modifierList.indexOf("add4");
            if (addn == -1)
                  addn = _modifierList.indexOf("add2");
            if (addn != -1) {
                  QString& s = _modifierList[addn];
                  s.replace("add","sus");
                  _modifierList.removeAt(no3);
                  }
            }
      // convert kind=minor-seventh, degree=altb5 to kind=half-diminished (suppression of degree=altb comes later)
      if (kind == "minor-seventh" && _modifierList.size() == 1 && _modifierList.front() == "b5")
            kind = "half-diminished";
      // force parens where necessary)
      if (!parens && extension == 0 && !_modifierList.empty()) {
            QString firstMod = _modifierList.front();
            if (firstMod != "" && (firstMod.startsWith('#') || firstMod.startsWith('b')))
                  parens = true;
            }
 
      // record extension
      if (extension)
            _extension = QString("%1").arg(extension);
 
      // validate kindText
      if (kindText != "" && kind != "none" && kind != "other") {
            ParsedChord validate;
            validate.parse(kindText, cl, false);
            // kindText should parse to produce same kind, no degrees
            if (validate._xmlKind != kind || !validate._xmlDegrees.empty())
                  kindText = "";
            }
 
      // construct name & handle
      _name = "";
      if (kindText != "") {
            if (_extension != "" && kind.contains("suspended"))
                  _name += _extension;
            _name += kindText;
            if (extension == 69)
                  _name += "9";
            }
      else if (implied)
            _name = _extension;
      else {
            if (_quality == "major")
                  _name = symbols ? "^" : "maj";
            else if (_quality == "minor")
                  _name = symbols ? "-" : "m";
            else if (_quality == "augmented")
                  _name = symbols ? "+" : "aug";
            else if (_quality == "diminished")
                  _name = symbols ? "o" : "dim";
            else if (_quality == "half-diminished")
                  _name = symbols ? "0" : "m7b5";
            else
                  _name = _quality;
            _name += _extension;
            }
      if (parens)
            _name += "(";
      foreach (QString mod, _modifierList) {
            mod.replace("major","maj");
            if (kindText != "" && kind.contains("suspended") && mod.startsWith("sus"))
                  continue;
            else if (kindText != "" && kind == "major-minor" && mod.startsWith("maj"))
                  continue;
            _name += mod;
            }
      if (parens)
            _name += ")";
 
      // parse name to construct handle & tokenList
      parse(_name, cl, true);
 
      // record original MusicXML
      _xmlKind = kind;
      _xmlText = kindText;
      _xmlSymbols = useSymbols;
      _xmlParens = useParens;
      foreach (const HDegree& d, dl) {
            if (kind == "half-diminished" && d.type() == HDegreeType::ALTER && d.alter() == -1 && d.value() == 5)
                  continue;
            _xmlDegrees += d.text();
            }
 
      return _name;
      }
 
//---------------------------------------------------------
//   renderList
//---------------------------------------------------------
 
const QList<RenderAction>& ParsedChord::renderList(const ChordList* cl)
      {
      // generate anew on each call,
      // in case chord list has changed since last time
      if (!_renderList.empty())
            _renderList.clear();
      foreach (ChordToken tok, _tokenList) {
            QString n = tok.names.first();
            QList<RenderAction> rl;
            QList<ChordToken> definedTokens;
            bool found = false;
            // potential definitions for token
            if (cl) {
                  foreach (ChordToken ct, cl->chordTokenList) {
                        foreach (QString ctn, ct.names) {
                              if (ctn == n)
                                    definedTokens += ct;
                              }
                        }
                  }
            // find matching class, fallback on ChordTokenClass::ALL
            foreach (ChordToken matchingTok, definedTokens) {
                  if (tok.tokenClass == matchingTok.tokenClass) {
                        rl = matchingTok.renderList;
                        found = true;
                        break;
                        }
                  else if (matchingTok.tokenClass == ChordTokenClass::ALL) {
                        rl = matchingTok.renderList;
                        found = true;
                        }
                  }
            if (found)
                  _renderList.append(rl);
            else {
                  // no definition for token, so render as literal
                  RenderAction a(RenderAction::RenderActionType::SET);
                  a.text = tok.names.first();
                  _renderList.append(a);
                  }
            }
      return _renderList;
      }
 
//---------------------------------------------------------
//   addToken
//---------------------------------------------------------
 
void ParsedChord::addToken(QString s, ChordTokenClass tc)
      {
      if (s == "")
            return;
      ChordToken tok;
      tok.names += s;
      tok.tokenClass = tc;
      _tokenList += tok;
      }
 
//---------------------------------------------------------
//   ChordDescription
//    this form is used when reading from file
//    a private id is assigned for id = 0
//---------------------------------------------------------
 
ChordDescription::ChordDescription(int i)
      {
      if (!i)
//            i = --(cl->privateID);
            i = -- ChordList::privateID;
      id = i;
      generated = false;
      renderListGenerated = false;
      exportOk = true;
      }
 
//---------------------------------------------------------
//   ChordDescription
//    this form is used when generating from name
//    a private id is always assigned
//---------------------------------------------------------
 
ChordDescription::ChordDescription(const QString& name)
      {
      id = -- ChordList::privateID;
      generated = true;
      names.append(name);
      renderListGenerated = false;
      exportOk = false;
      }
 
//---------------------------------------------------------
//   complete
//    generate missing renderList and semantic (Xml) info
//---------------------------------------------------------
 
void ChordDescription::complete(ParsedChord* pc, const ChordList* cl)
      {
      ParsedChord tempPc;
      if (!pc) {
            // generate parsed chord for its rendering & semantic (xml) info
            pc = &tempPc;
            QString n;
            if (!names.empty())
                  n = names.front();
            pc->parse(n, cl);
            }
      parsedChords.append(*pc);
      if (renderList.empty() || renderListGenerated) {
            renderList = pc->renderList(cl);
            renderListGenerated = true;
            }
      if (xmlKind == "") {
            xmlKind = pc->xmlKind();
            xmlDegrees = pc->xmlDegrees();
            }
      // these fields are not read from chord description files
      // so get them from the parsed representation in all cases
      xmlText = pc->xmlText();
      xmlSymbols = pc->xmlSymbols();
      xmlParens = pc->xmlParens();
      if (chord.getKeys() == 0) {
            chord = HChord(pc->keys());
            }
      _quality = pc->quality();
      }
 
//---------------------------------------------------------
//   read
//---------------------------------------------------------
 
void ChordDescription::read(XmlReader& e)
      {
      int ni = 0;
      id = e.attribute("id").toInt();
      while (e.readNextStartElement()) {
            const QStringRef& tag(e.name());
            if (tag == "name") {
                  QString n = e.readElementText();
                  // stack names for this file on top of the list
                  names.insert(ni++,n);
                  }
            else if (tag == "xml")
                  xmlKind = e.readElementText();
            else if (tag == "degree")
                  xmlDegrees.append(e.readElementText());
            else if (tag == "voicing")
                  chord = HChord(e.readElementText());
            else if (tag == "render") {
                  readRenderList(e.readElementText(), renderList);
                  renderListGenerated = false;
                  }
            else
                  e.unknown();
            }
      }
 
//---------------------------------------------------------
//   write
//---------------------------------------------------------
 
void ChordDescription::write(XmlWriter& xml) const
      {
      if (generated && !exportOk)
            return;
      if (id > 0)
            xml.stag(QString("chord id=\"%1\"").arg(id));
      else
            xml.stag(QString("chord"));
      foreach(const QString& s, names)
            xml.tag("name", s);
      xml.tag("xml", xmlKind);
      xml.tag("voicing", chord.voicing());
      foreach(const QString& s, xmlDegrees)
            xml.tag("degree", s);
      writeRenderList(xml, &renderList, "render");
      xml.etag();
      }
 
 
//---------------------------------------------------------
//   ChordList
//---------------------------------------------------------
 
int ChordList::privateID = -1000;
 
//---------------------------------------------------------
//   read
//---------------------------------------------------------
 
void ChordList::read(XmlReader& e)
      {
      int fontIdx = 0;
      while (e.readNextStartElement()) {
            const QStringRef& tag(e.name());
            if (tag == "font") {
                  ChordFont f;
                  f.family = e.attribute("family", "default");
                  f.mag    = 1.0;
                  while (e.readNextStartElement()) {
                        if (e.name() == "sym") {
                              ChordSymbol cs;
                              QString code;
                              QString symClass;
                              cs.fontIdx = fontIdx;
                              cs.name    = e.attribute("name");
                              cs.value   = e.attribute("value");
                              code       = e.attribute("code");
                              symClass   = e.attribute("class");
                              if (code != "") {
                                    bool ok = true;
                                    int val = code.toInt(&ok, 0);
                                    if (!ok) {
                                          cs.code = 0;
                                          cs.value = code;
                                          }
                                    else if (val & 0xffff0000) {
                                          cs.code = 0;
                                          QChar high = QChar(QChar::highSurrogate(val));
                                          QChar low = QChar(QChar::lowSurrogate(val));
                                          cs.value = QString("%1%2").arg(high).arg(low);
                                          }
                                    else {
                                          cs.code = val;
                                          cs.value = QString(cs.code);
                                          }
                                    }
                              else
                                    cs.code = 0;
                              if (cs.value == "")
                                    cs.value = cs.name;
                              cs.name = symClass + cs.name;
                              symbols.insert(cs.name, cs);
                              e.readNext();
                              }
                        else if (e.name() == "mag")
                              f.mag = e.readDouble();
                        else
                              e.unknown();
                        }
                  fonts.append(f);
                  ++fontIdx;
                  }
            else if (tag == "token") {
                  ChordToken t;
                  t.read(e);
                  chordTokenList.append(t);
                  }
            else if (tag == "chord") {
                  int id = e.intAttribute("id");
                  // if no id attribute (id == 0), then assign it a private id
                  // user chords that match these ChordDescriptions will be treated as normal recognized chords
                  // except that the id will not be written to the score file
                  ChordDescription cd = (id && contains(id)) ? take(id) : ChordDescription(id);
 
                  // record updated id
                  id = cd.id;
                  // read rest of description
                  cd.read(e);
                  // restore updated id
                  cd.id = id;
                  // throw away previously parsed chords
                  cd.parsedChords.clear();
                  // generate any missing info (including new parsed chords)
                  cd.complete(0,this);
                  // add to list
                  insert(id, cd);
                  }
            else if (tag == "renderRoot")
                  readRenderList(e.readElementText(), renderListRoot);
            else if (tag == "renderBase")
                  readRenderList(e.readElementText(), renderListBase);
            else
                  e.unknown();
            }
      }
 
//---------------------------------------------------------
//   write
//---------------------------------------------------------
 
void ChordList::write(XmlWriter& xml) const
      {
      int fontIdx = 0;
      foreach (ChordFont f, fonts) {
            xml.stag(QString("font id=\"%1\" family=\"%2\"").arg(fontIdx).arg(f.family));
            xml.tag("mag", f.mag);
            foreach(ChordSymbol s, symbols) {
                  if (s.fontIdx == fontIdx) {
                        if (s.code.isNull())
                              xml.tagE(QString("sym name=\"%1\" value=\"%2\"").arg(s.name).arg(s.value));
                        else
                              xml.tagE(QString("sym name=\"%1\" code=\"0x%2\"").arg(s.name).arg(s.code.unicode(),0,16));
                        }
                  }
            xml.etag();
            ++fontIdx;
            }
      foreach (ChordToken t, chordTokenList)
            t.write(xml);
      if (!renderListRoot.empty())
            writeRenderList(xml, &renderListRoot, "renderRoot");
      if (!renderListBase.empty())
            writeRenderList(xml, &renderListBase, "renderBase");
      foreach(const ChordDescription& d, *this)
            d.write(xml);
      }
 
//---------------------------------------------------------
//   read
//    read Chord List, return false on error
//---------------------------------------------------------
 
bool ChordList::read(const QString& name)
      {
//      qDebug("ChordList::read <%s>", qPrintable(name));
      QString path;
      QFileInfo ftest(name);
      if (ftest.isAbsolute())
            path = name;
      else {
#if defined(Q_OS_IOS)
            path = QString("%1/%2").arg(MScore::globalShare()).arg(name);
#elif defined(Q_OS_ANDROID)
            path = QString(":/styles/%1").arg(name);
#else
            path = QString("%1styles/%2").arg(MScore::globalShare()).arg(name);
#endif
            }
      // default to chords_std.xml
      QFileInfo fi(path);
      if (!fi.exists())
#if defined(Q_OS_IOS)
            path = QString("%1/%2").arg(MScore::globalShare()).arg("chords_std.xml");
#elif defined(Q_OS_ANDROID)
            path = QString(":/styles/chords_std.xml");
#else
            path = QString("%1styles/%2").arg(MScore::globalShare()).arg("chords_std.xml");
#endif
 
      if (name.isEmpty())
            return false;
      QFile f(path);
      if (!f.open(QIODevice::ReadOnly)) {
            MScore::lastError = QObject::tr("Cannot open chord description:\n%1\n%2").arg(f.fileName()).arg(f.errorString());
            qDebug("ChordList::read failed: <%s>", qPrintable(path));
            return false;
            }
      XmlReader e(0, &f);
      docName = f.fileName();
 
      while (e.readNextStartElement()) {
            if (e.name() == "museScore") {
                  // QString version = e.attribute(QString("version"));
                  // QStringList sl = version.split('.');
                  // int _mscVersion = sl[0].toInt() * 100 + sl[1].toInt();
                  read(e);
                  return true;
                  }
            }
      return false;
      }
 
//---------------------------------------------------------
//   writeChordList
//---------------------------------------------------------
 
bool ChordList::write(const QString& name) const
      {
      QFileInfo info(name);
 
      if (info.suffix().isEmpty()) {
            QString path = info.filePath();
            path += QString(".xml");
            info.setFile(path);
            }
 
      QFile f(info.filePath());
 
      if (!f.open(QIODevice::WriteOnly)) {
            MScore::lastError = QObject::tr("Open Chord Description\n%1\nfailed: %2").arg(f.fileName()).arg(f.errorString());
            return false;
            }
 
      XmlWriter xml(0, &f);
      xml.header();
      xml.stag("museScore version=\"" MSC_VERSION "\"");
 
      write(xml);
      xml.etag();
      if (f.error() != QFile::NoError) {
            MScore::lastError = QObject::tr("Write Chord Description failed: %1").arg(f.errorString());
            }
      return true;
      }
 
//---------------------------------------------------------
//   loaded
//---------------------------------------------------------
 
bool ChordList::loaded() const
      {
      // track whether a description file has been loaded
      // since chords.xml really doesn't load enough to stand alone,
      // we need a way to track when a "real" chord list has been loaded
      // for lack of anything better, key off renderListRoot
      return !renderListRoot.empty();
      }
 
//---------------------------------------------------------
//   unload
//---------------------------------------------------------
 
void ChordList::unload()
      {
      clear();
      symbols.clear();
      fonts.clear();
      renderListRoot.clear();
      renderListBase.clear();
      chordTokenList.clear();
      }
 
 
}

V688 The 'symbols' local variable possesses the same name as one of the class members, which can result in a confusion.