//=============================================================================
//  MuseScore
//  Music Composition & Notation
//
//  Note Names Plugin
//
//  Copyright (C) 2012 Werner Schweer
//  Copyright (C) 2013 - 2019 Joachim Schmitz
//  Copyright (C) 2014 Jörn Eichler
//  Copyright (C) 2020 MuseScore BVBA
//
//  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
//=============================================================================

import QtQuick 2.2
import QtQuick.Controls 2.0
import MuseScore 3.0

MuseScore {
    version: "3.4"
    description: qsTr("This plugin names notes as per your language setting")
    menuPath: "Plugins.Notes." + qsTr("Note Names (Interactive)")
    pluginType: "dock"

    implicitHeight: controls.implicitHeight * 1.5
    implicitWidth: controls.implicitWidth

    // Small note name size is fraction of the full font size.
    property var defaultFontSize
    property var fontSizeMini: 0.7;

    property int nstaves: 0 // for validators in staff number inputs

    property bool inCmd: false

    function ensureCmdStarted() {
        if (!inCmd) {
            curScore.startCmd();
            inCmd = true;
        }
    }

    function ensureCmdEnded() {
        if (inCmd) {
            curScore.endCmd();
            inCmd = false;
        }
    }

    function findSegment(el) {
        while (el && el.type != Element.SEGMENT)
            el = el.parent;
        return el;
    }

    function getChordName(chord) {
        var text = "";
        var notes = chord.notes;
        for (var i = 0; i < notes.length; i++) {
            var sep = "\n";   // change to "," if you want them horizontally (anybody?)
            if ( i > 0 )
                text = sep + text; // any but top note
            if (typeof notes[i].tpc === "undefined") // like for grace notes ?!?
                return;
            switch (notes[i].tpc) {
                case -1: text = qsTranslate("InspectorAmbitus", "Fâ™­â™­") + text; break;
                case  0: text = qsTranslate("InspectorAmbitus", "Câ™­â™­") + text; break;
                case  1: text = qsTranslate("InspectorAmbitus", "Gâ™­â™­") + text; break;
                case  2: text = qsTranslate("InspectorAmbitus", "Dâ™­â™­") + text; break;
                case  3: text = qsTranslate("InspectorAmbitus", "Aâ™­â™­") + text; break;
                case  4: text = qsTranslate("InspectorAmbitus", "Eâ™­â™­") + text; break;
                case  5: text = qsTranslate("InspectorAmbitus", "Bâ™­â™­") + text; break;
                case  6: text = qsTranslate("InspectorAmbitus", "Fâ™­")  + text; break;
                case  7: text = qsTranslate("InspectorAmbitus", "Câ™­")  + text; break;

                case  8: text = qsTranslate("InspectorAmbitus", "Gâ™­")  + text; break;
                case  9: text = qsTranslate("InspectorAmbitus", "Dâ™­")  + text; break;
                case 10: text = qsTranslate("InspectorAmbitus", "Aâ™­")  + text; break;
                case 11: text = qsTranslate("InspectorAmbitus", "Eâ™­")  + text; break;
                case 12: text = qsTranslate("InspectorAmbitus", "Bâ™­")  + text; break;
                case 13: text = qsTranslate("InspectorAmbitus", "F")   + text; break;
                case 14: text = qsTranslate("InspectorAmbitus", "C")   + text; break;
                case 15: text = qsTranslate("InspectorAmbitus", "G")   + text; break;
                case 16: text = qsTranslate("InspectorAmbitus", "D")   + text; break;
                case 17: text = qsTranslate("InspectorAmbitus", "A")   + text; break;
                case 18: text = qsTranslate("InspectorAmbitus", "E")   + text; break;
                case 19: text = qsTranslate("InspectorAmbitus", "B")   + text; break;

                case 20: text = qsTranslate("InspectorAmbitus", "F♯")  + text; break;
                case 21: text = qsTranslate("InspectorAmbitus", "C♯")  + text; break;
                case 22: text = qsTranslate("InspectorAmbitus", "G♯")  + text; break;
                case 23: text = qsTranslate("InspectorAmbitus", "D♯")  + text; break;
                case 24: text = qsTranslate("InspectorAmbitus", "A♯")  + text; break;
                case 25: text = qsTranslate("InspectorAmbitus", "E♯")  + text; break;
                case 26: text = qsTranslate("InspectorAmbitus", "B♯")  + text; break;
                case 27: text = qsTranslate("InspectorAmbitus", "F♯♯") + text; break;
                case 28: text = qsTranslate("InspectorAmbitus", "C♯♯") + text; break;
                case 29: text = qsTranslate("InspectorAmbitus", "G♯♯") + text; break;
                case 30: text = qsTranslate("InspectorAmbitus", "D♯♯") + text; break;
                case 31: text = qsTranslate("InspectorAmbitus", "A♯♯") + text; break;
                case 32: text = qsTranslate("InspectorAmbitus", "E♯♯") + text; break;
                case 33: text = qsTranslate("InspectorAmbitus", "B♯♯") + text; break;
                default: text = qsTr("?")   + text; break;
            } // end switch tpc

            // octave, middle C being C4
            //text += (Math.floor(notes[i].pitch / 12) - 1)
            // or
            //text += (Math.floor(notes[i].ppitch / 12) - 1)

            // change below false to true for courtesy- and microtonal accidentals
            // you might need to come up with suitable translations
            // only #, b, natural and possibly also ## seem to be available in UNICODE
            if (false) {
                switch (notes[i].userAccidental) {
                    case  0: break;
                    case  1: text = qsTranslate("accidental", "Sharp") + text; break;
                    case  2: text = qsTranslate("accidental", "Flat") + text; break;
                    case  3: text = qsTranslate("accidental", "Double sharp") + text; break;
                    case  4: text = qsTranslate("accidental", "Double flat") + text; break;
                    case  5: text = qsTranslate("accidental", "Natural") + text; break;
                    case  6: text = qsTranslate("accidental", "Flat-slash") + text; break;
                    case  7: text = qsTranslate("accidental", "Flat-slash2") + text; break;
                    case  8: text = qsTranslate("accidental", "Mirrored-flat2") + text; break;
                    case  9: text = qsTranslate("accidental", "Mirrored-flat") + text; break;
                    case 10: text = qsTranslate("accidental", "Mirrored-flat-slash") + text; break;
                    case 11: text = qsTranslate("accidental", "Flat-flat-slash") + text; break;
                    case 12: text = qsTranslate("accidental", "Sharp-slash") + text; break;
                    case 13: text = qsTranslate("accidental", "Sharp-slash2") + text; break;
                    case 14: text = qsTranslate("accidental", "Sharp-slash3") + text; break;
                    case 15: text = qsTranslate("accidental", "Sharp-slash4") + text; break;
                    case 16: text = qsTranslate("accidental", "Sharp arrow up") + text; break;
                    case 17: text = qsTranslate("accidental", "Sharp arrow down") + text; break;
                    case 18: text = qsTranslate("accidental", "Sharp arrow both") + text; break;
                    case 19: text = qsTranslate("accidental", "Flat arrow up") + text; break;
                    case 20: text = qsTranslate("accidental", "Flat arrow down") + text; break;
                    case 21: text = qsTranslate("accidental", "Flat arrow both") + text; break;
                    case 22: text = qsTranslate("accidental", "Natural arrow down") + text; break;
                    case 23: text = qsTranslate("accidental", "Natural arrow up") + text; break;
                    case 24: text = qsTranslate("accidental", "Natural arrow both") + text; break;
                    case 25: text = qsTranslate("accidental", "Sori") + text; break;
                    case 26: text = qsTranslate("accidental", "Koron") + text; break;
                    default: text = qsTr("?") + text; break;
                }  // end switch userAccidental
            }  // end if courtesy- and microtonal accidentals
        }  // end for note

        return text;
    }

    function getGraceNoteNames(graceChordsList) {
        var names = [];
        // iterate through all grace chords
        for (var chordNum = 0; chordNum < graceChordsList.length; chordNum++) {
            var chord = graceChordsList[chordNum];
            var chordName = getChordName(chord);
            // append the name to the list of names
            names.push(chordName);

        }
        return names;
    }

    function getAllChords(el) {
        // List chords in the following order:
        // 1) Leading grace notes;
        // 2) Chord itself;
        // 3) Trailing grace notes.

        if (!el || el.type != Element.CHORD)
            return [];

        var chord = el;
        var allChords = [ chord ];

        // Search for grace notes
        var graceChords = chord.graceNotes;
        for (var chordNum = 0; chordNum < graceChords.length; chordNum++) {
            var graceChord = graceChords[chordNum];
            var noteType = graceChord.noteType;

            switch (noteType) {
                case NoteType.GRACE8_AFTER:
                case NoteType.GRACE16_AFTER:
                case NoteType.GRACE32_AFTER:
                    leadingLifo.push(graceChord); // append trailing grace chord to list
                    break;
                default:
                    allChords.unshift(graceChord); // prepend leading grace chord to list
                    break;
            }
        }

        return allChords;
    }

    function isNoteName(el) {
        return el.type == Element.STAFF_TEXT; // TODO: how to distinguish note names from all staff texts?
    }

    function getExistingNoteNames(segment, track) {
        var annotations = segment.annotations;
        var noteNames = [];

        for (var i = 0; i < annotations.length; ++i) {
            var a = annotations[i];
            if (a.track != track)
                continue;
            if (isNoteName(a))
                noteNames.push(a);
        }

        return noteNames;
    }

    function handleChordAtCursor(cursor) {
        var allNoteNames = getExistingNoteNames(cursor.segment, cursor.track);
        var allChords = getAllChords(cursor.element);
        var chordIdx = 0;

        for (; chordIdx < allChords.length; ++chordIdx) {
            var chord = allChords[chordIdx];
            var noteName = allNoteNames[chordIdx];

            var chordProperties = {
                "offsetX": chord.posX,
                "fontSize" : chord.noteType == NoteType.NORMAL ? defaultFontSize : (defaultFontSize * fontSizeMini),
                "placement": (chord.voice & 1) ? Placement.BELOW : Placement.ABOVE, // place below for voice 1 and voice 3 (numbered as 2 and 4 in user interface)
                "text": getChordName(chord)
            }

            if (!noteName) {
                // Note name does not exist, add a new one
                ensureCmdStarted();
                var nameText = newElement(Element.STAFF_TEXT);
                for (var prop in chordProperties) {
                    if (nameText[prop] != chordProperties[prop])
                        nameText[prop] = chordProperties[prop];
                }
                cursor.add(nameText);
            } else {
                // Note name exists, ensure it is up to date
                for (var prop in chordProperties) {
                    if (noteName[prop] != chordProperties[prop]) {
                        ensureCmdStarted();
                        noteName[prop] = chordProperties[prop];
                    }
                }
            } // end if/else noteName
        } // end for allChords

        // Remove the remaining redundant note names, if any
        for (; chordIdx < allNoteNames.length; ++chordIdx) {
            ensureCmdStarted();
            var noteName = allNoteNames[chordIdx];
            removeElement(noteName);
        }
    } // end handleChordAtCursor()

    function processRange(startTick, endTick, firstStaff, lastStaff) {
        if (startTick < 0)
            startTick = 0;
        if (endTick < 0)
            endTick = Infinity; // process the entire score

        var cursor = curScore.newCursor();

        for (var staff = firstStaff; staff <= lastStaff; staff++) {
            for (var voice = 0; voice < 4; voice++) {
                cursor.voice    = voice;
                cursor.staffIdx = staff;
                cursor.rewindToTick(startTick);

                while (cursor.segment && cursor.tick <= endTick) {
                    handleChordAtCursor(cursor);
                    cursor.next();
                } // end while segment

            } // end for voice
        } // end for staff

        ensureCmdEnded();
    }

    function getStavesRange() {
        if (allStavesCheckBox.checked)
            return [0, curScore.nstaves];

        var firstStaff = firstStaffInput.acceptableInput ? +firstStaffInput.text : curScore.nstaves;
        var lastStaff = lastStaffInput.acceptableInput ? +lastStaffInput.text : -1;

        return [firstStaff, lastStaff];
    }

    onScoreStateChanged: {
        if (inCmd) // prevent recursion from own changes
            return;
        if (state.undoRedo) // try not to interfere with undo/redo commands
            return;

        nstaves = curScore.nstaves; // needed for validators in staff number inputs

        if (!noteNamesEnabledCheckBox.checked)
            return;
        if (!curScore || state.startLayoutTick < 0) // nothing to process?
            return;

        var stavesRange = getStavesRange();

        processRange(state.startLayoutTick, state.endLayoutTick, stavesRange[0], stavesRange[1]);
    }

    onRun: {
        defaultFontSize = newElement(Element.STAFF_TEXT).fontSize;
    }

    Column {
        id: controls

        CheckBox {
            id: noteNamesEnabledCheckBox
            text: "Enable notes naming"
        }
        CheckBox {
            id: allStavesCheckBox
            checked: true
            text: "All staves"
        }

        Grid {
            id: staffRangeControls
            columns: 2
            spacing: 4
            enabled: !allStavesCheckBox.checked

            Text {
                height: firstStaffInput.height
                verticalAlignment: Text.AlignVCenter
                text: "first staff:"
            }
            TextField {
                id: firstStaffInput
                text: "0"
                validator: IntValidator { bottom: 0; top: nstaves - 1 }
                onTextChanged: {
                    if (+lastStaffInput.text < +text)
                        lastStaffInput.text = text
                }
            }

            Text {
                height: lastStaffInput.height
                verticalAlignment: Text.AlignVCenter
                text: "last staff:"
            }
            TextField {
                id: lastStaffInput
                text: "0"
                validator: IntValidator { bottom: 0; top: nstaves - 1 }
                onTextChanged: {
                    if (text !== "" && (+firstStaffInput.text > +text))
                        firstStaffInput.text = text
                }
            }
        }
    }
}