//=============================================================================
//  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);
    for (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
    for (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);
    for (const QString& s : sl) {
        if (s.startsWith("m:")) {
            QStringList ssl = s.split(":", QString::SkipEmptyParts);
            if (ssl.size() == 3) {
                // m:x:y
                RenderAction a;
                a.type = RenderAction::RenderActionType::MOVE;
                a.movex = ssl[1].toDouble();
                a.movey = ssl[2].toDouble();
                renderList.append(a);
            }
#if 0
            else if (ssl.size() == 2) {
                // m:keyword
                RenderAction a;
                a.type = RenderAction::RenderActionType::MOVE;
                // TODO: derive offset from keyword
            }
#endif
        } 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);
    for (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 other than comma or slash
    for (j = 0, tok1 = ""; i < len; ++i, ++j) {
        if (!s[i].isDigit() && s[i] != ',' && s[i] != '/') {
            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" || tok1 == "6,9" || tok1 == "6/9") {
            if (take6) {
                _xmlKind += "-sixth";
                extl << "9";
                correctXmlText("6");
            } else {
                extl << "6" << "9";
                correctXmlText();
            }
            _extension = "69";
            chord += 9;
            chord += 2;
        }
        for (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");
        for (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 i1 = _xmlDegrees.indexOf(d);
                _xmlDegrees.replace(i1, 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 syms = (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 (syms) {
            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
    for (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 = syms ? "^" : "maj";
        } else if (_quality == "minor") {
            _name = syms ? "-" : "m";
        } else if (_quality == "augmented") {
            _name = syms ? "+" : "aug";
        } else if (_quality == "diminished") {
            _name = syms ? "o" : "dim";
        } else if (_quality == "half-diminished") {
            _name = syms ? "0" : "m7b5";
        } else {
            _name = _quality;
        }
        _name += _extension;
    }
    if (parens) {
        _name += "(";
    }
    for (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;
    for (const HDegree& d : dl) {
        if (kind == "half-diminished" && d.type() == HDegreeType::ALTER && d.alter() == -1 && d.value() == 5) {
            continue;
        }
        _xmlDegrees += d.text();
    }

    return _name;
}

//---------------------------------------------------------
//   position
//---------------------------------------------------------

qreal ChordList::position(const QStringList& names, ChordTokenClass ctc) const
{
    QString name = names.empty() ? "" : names.first();
    switch (ctc) {
    case ChordTokenClass::EXTENSION:
        return _eadjust;
    case ChordTokenClass::MODIFIER: {
        QChar c = name.isEmpty() ? name.at(0) : '0';
        if (c.isDigit() || c.isPunct()) {
            return _madjust;
        } else {
            return 0.0;
        }
    }
    default:
        if (name == "o" || name == "0") {
            return _eadjust;
        } else {
            return 0.0;
        }
    }
}

//---------------------------------------------------------
//   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();
    }
    bool adjust = cl ? cl->autoAdjust() : false;
    for (ChordToken tok : _tokenList) {
        QString n = tok.names.first();
        QList<RenderAction> rl;
        QList<ChordToken> definedTokens;
        bool found = false;
        // potential definitions for token
        if (cl) {
            for (ChordToken ct : cl->chordTokenList) {
                for (QString ctn : ct.names) {
                    if (ctn == n) {
                        definedTokens += ct;
                    }
                }
            }
        }
        // find matching class, fallback on ChordTokenClass::ALL
        ChordTokenClass ctc = ChordTokenClass::ALL;
        for (ChordToken matchingTok : definedTokens) {
            if (tok.tokenClass == matchingTok.tokenClass) {
                rl = matchingTok.renderList;
                ctc = tok.tokenClass;
                found = true;
                break;
            } else if (matchingTok.tokenClass == ChordTokenClass::ALL) {
                rl = matchingTok.renderList;
                found = true;
            }
        }
        // check for adjustments
        // stop adjusting when first non-adjusted modifier found
        qreal p = adjust ? cl->position(tok.names, ctc) : 0.0;
        if (tok.tokenClass == ChordTokenClass::MODIFIER && p == 0.0) {
            adjust = false;
        }
        // build render list
        if (p != 0.0) {
            RenderAction m1 = RenderAction(RenderAction::RenderActionType::MOVE);
            m1.movex = 0.0;
            m1.movey = p;
            _renderList.append(m1);
        }
        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);
        }
        if (p != 0.0) {
            RenderAction m2 = RenderAction(RenderAction::RenderActionType::MOVE);
            m2.movex = 0.0;
            m2.movey = -p;
            _renderList.append(m2);
        }
    }
    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"));
    }
    for (const QString& s : names) {
        xml.tag("name", s);
    }
    xml.tag("xml", xmlKind);
    xml.tag("voicing", chord.voicing());
    for (const QString& s : xmlDegrees) {
        xml.tag("degree", s);
    }
    writeRenderList(xml, &renderList, "render");
    xml.etag();
}

//---------------------------------------------------------
//   ChordList
//---------------------------------------------------------

int ChordList::privateID = -1000;

//---------------------------------------------------------
//   configureAutoAdjust
//---------------------------------------------------------

void ChordList::configureAutoAdjust(qreal emag, qreal eadjust, qreal mmag, qreal madjust)
{
    _emag = emag;
    _eadjust = eadjust;
    _mmag = mmag;
    _madjust = madjust;
#if 0
    // TODO: regenerate all chord descriptions
    // currently we always reload the entire chordlist
    if (_autoAdjust) {
        for (ChordFont cf : fonts) {
            if (cf.fontClass == "extension") {
                cf.mag = _emag;
            } else if (cf.fontClass == "modifier") {
                cf.mag = _mmag;
            }
        }
    }
#endif
}

//---------------------------------------------------------
//   read
//---------------------------------------------------------

void ChordList::read(XmlReader& e)
{
    int fontIdx = 0;
    _autoAdjust = false;
    while (e.readNextStartElement()) {
        const QStringRef& tag(e.name());
        if (tag == "font") {
            ChordFont f;
            f.family = e.attribute("family", "default");
            if (f.family == "MuseJazz") {
                f.family = "MuseJazz Text";
            }
            f.mag    = 1.0;
            f.fontClass = e.attribute("class");
            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();
                }
            }
            if (_autoAdjust) {
                if (f.fontClass == "extension") {
                    f.mag *= _emag;
                } else if (f.fontClass == "modifier") {
                    f.mag *= _mmag;
                }
            }
            fonts.append(f);
            ++fontIdx;
        } else if (tag == "autoAdjust") {
            QString nmag = e.attribute("mag");
            _nmag = nmag.toDouble();
            QString nadjust = e.attribute("adjust");
            _nadjust = nadjust.toDouble();
            _autoAdjust = e.readBool();
        } 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 == "renderFunction") {
            readRenderList(e.readElementText(), renderListFunction);
        } else if (tag == "renderBase") {
            readRenderList(e.readElementText(), renderListBase);
        } else {
            e.unknown();
        }
    }
}

//---------------------------------------------------------
//   write
//---------------------------------------------------------

void ChordList::write(XmlWriter& xml) const
{
    int fontIdx = 0;
    for (ChordFont f : fonts) {
        xml.stag(QString("font id=\"%1\" family=\"%2\"").arg(fontIdx).arg(f.family));
        xml.tag("mag", f.mag);
        for (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;
    }
    if (_autoAdjust) {
        xml.tagE(QString("autoAdjust mag=\"%1\" adjust=\"%2\"").arg(_nmag).arg(_nadjust));
    }
    for (ChordToken t : chordTokenList) {
        t.write(xml);
    }
    if (!renderListRoot.empty()) {
        writeRenderList(xml, &renderListRoot, "renderRoot");
    }
    if (!renderListFunction.empty()) {
        writeRenderList(xml, &renderListRoot, "renderFunction");
    }
    if (!renderListBase.empty()) {
        writeRenderList(xml, &renderListBase, "renderBase");
    }
    for (const ChordDescription& cd : *this) {
        cd.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(&f);

    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();
    _autoAdjust = false;
}

//---------------------------------------------------------
//   print
//    only for debugging
//---------------------------------------------------------

void RenderAction::print() const
{
    static const char* names[] = {
        "SET", "MOVE", "PUSH", "POP",
        "NOTE", "ACCIDENTAL"
    };
    qDebug("%10s <%s> %f %f", names[int(type)], qPrintable(text), movex, movey);
}
}
