#!/usr/local/bin/python3.14

# Copyright (C) 2006--2022 Brailcom, o.p.s.
#
# Author: Milan Zamazal <pdm@brailcom.org>
#
# This file is part of LilyPond, the GNU music typesetter.
#
# LilyPond is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# LilyPond is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with LilyPond.  If not, see <http://www.gnu.org/licenses/>.

import optparse
import os
import sys

"""

# relocate-preamble.py.in
#
# This file is part of LilyPond, the GNU music typesetter.
#
# Copyright (C) 2007--2022  Han-Wen Nienhuys <hanwen@xs4all.nl>
#
# LilyPond is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# LilyPond is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with LilyPond.  If not, see <http://www.gnu.org/licenses/>.
#

This is generic code, used for all python scripts.

The quotes are to ensure that the source .py file can still be
run as a python script, but does not include any sys.path handling.
Otherwise, the lilypond-book calls inside the build
might modify installed .pyc files.

"""

# This is needed for installations with a non-default layout, ie where share/
# is not next to bin/.
sys.path.insert (0, os.path.join ('/usr/local/share/lilypond/2.24.4', 'python'))

# Dynamic relocation, for installations with a default layout including GUB,
# but also for execution from the build directory.
bindir = os.path.abspath (os.path.dirname (sys.argv[0]))
topdir = os.path.dirname (bindir)
if bindir.endswith (r'/scripts/out'):
    topdir = os.path.join (os.path.dirname (topdir), 'out')
datadir = os.path.abspath (os.path.join (topdir, 'share', 'lilypond'))
for v in [ 'current', '2.24.4' ]:
    sys.path.insert (0, os.path.join (datadir, v, 'python'))

"""
"""


def process_options(args):
    parser = optparse.OptionParser(version="2.24.4")
    parser.add_option('', '--filter-tracks', metavar='REGEXP', action='store', type='string', dest='regexp',
                      help="display only tracks numbers, of those track names matching REGEXP")
    parser.add_option('', '--prefix-tracks', metavar='PREFIX', action='store', type='string', dest='prefix',
                      help="prefix filtered track numbers with PREFIX")
    parser.add_option('', '--dump', action='store_true', dest='dump',
                      help="just dump parsed contents of the MIDI file")
    parser.add_option('', '--pretty', action='store_true', dest='pretty',
                      help="dump parsed contents of the MIDI file in human-readable form (implies --dump)")
    parser.usage = parser.usage + " FILE"
    options, args = parser.parse_args(args)
    if len(args) != 1:
        parser.print_help()
        sys.exit(2)
    return options, args


def read_midi(file):
    import midi
    return midi.parse(open(file, 'rb').read())


def track_info(data):
    tracks = data[1]

    def track_name(track):
        name = ''
        for time, event in track:
            if time > 0:
                break
            if event[0] == 255 and event[1] == 3:
                name = event[2]
                break
        return name
    track_info = []
    for i in range(len(tracks)):
        track_info.append((i, track_name(tracks[i])))
    return track_info


class formatter:
    def __init__(self, txt=""):
        self.text = txt

    def format_vals(self, val1, val2=""):
        return str(val1) + str(val2)

    def format(self, val1, val2=""):
        return self.text + self.format_vals(val1, val2)


class none_formatter (formatter):
    def format_vals(self, val1, val2=""):
        return ''


class meta_formatter (formatter):
    def format_vals(self, val1, val2):
        return str(val2)


class tempo_formatter (formatter):
    def format_vals(self, val1, val2):
        return str(ord(val2[0])*65536 + ord(val2[1])*256 + ord(val2[2])) \
            + " msec/quarter"


class time_signature_formatter (formatter):
    def format_vals(self, val1, val2=""):
        from fractions import Fraction
        # if there are more notated 32nd notes per midi quarter than 8,
        # we display a fraction smaller than 1 as scale factor.
        r = Fraction(8, ord(val2[3]))
        if r == 1:
            ratio = ""
        else:
            ratio = " *" + str(r)
        return str(ord(val2[0])) + "/" + str(1 << ord(val2[1])) + ratio \
            + ", metronome " + str(Fraction(ord(val2[2]), 96))


class key_signature_formatter (formatter):
    def format_vals(self, val1, val2):
        key_names = ['F', 'C', 'G', 'D', 'A', 'E', 'B']
        key = (((ord(val2[0])+128) % 256)-128) + ord(val2[1])*3 + 1
        return (key_names[key % 7] + (key//7) * "is" + (-(key//7)) * "es"
                + " " + ['major', 'minor'][ord(val2[1])])


class channel_formatter (formatter):
    def __init__(self, txt, ch):
        formatter.__init__(self, txt)
        self.channel = ch

    def format(self, val1, val2=""):
        return self.text + "Channel " + str(self.channel) + ", " + \
            self.format_vals(val1, val2)


class control_mode_formatter (formatter):
    def __init__(self, txt, ch):
        formatter.__init__(self, txt)
        self.mode = ch

    def format(self, val1, val2=""):
        return self.text + str(self.mode) + ", " + \
            self.format_vals(val1, val2)


class note_formatter (channel_formatter):
    def pitch(self, val):
        pitch_names = ['C', 'Cis', 'D', 'Dis', 'E',
                       'F', 'Fis', 'G', 'Gis', 'A', 'Ais', 'B']
        p = val % 12
        oct = val // 12 - 1
        return pitch_names[p] + str(oct) + "(" + str(val) + ")"

    def velocity(self, val):
        return str(val)

    def format_vals(self, val1, val2):
        if val2 > 0:
            return self.pitch(val1) + '@' + self.velocity(val2)
        return self.pitch(val1)


meta_dict = {0x00: meta_formatter("Seq.Nr.:    "),
             0x01: meta_formatter("Text:       "),
             0x02: meta_formatter("Copyright:  "),
             0x03: meta_formatter("Track name: "),
             0x04: meta_formatter("Instrument: "),
             0x05: meta_formatter("Lyric:      "),
             0x06: meta_formatter("Marker:     "),
             0x07: meta_formatter("Cue point:  "),
             0x2F: none_formatter("End of Track"),
             0x51: tempo_formatter("Tempo:      "),
             0x54: meta_formatter("SMPTE Offs.:"),
             0x58: time_signature_formatter("Time signature: "),
             0x59: key_signature_formatter("Key signature: ")
             }


def dump_event(ev, time, padding):
    ch = ev[0] & 0x0F
    func = ev[0] & 0xF0
    f = None
    if ev[0] == 0xFF:
        f = meta_dict.get(ev[1], formatter())
    if func == 0x80:
        f = note_formatter("Note off: ", ch)
    elif func == 0x90:
        if ev[2] == 0:
            desc = "Note off: "
        else:
            desc = "Note on: "
        f = note_formatter(desc, ch)
    elif func == 0xA0:
        f = note_formatter("Polyphonic aftertouch: ",
                           ch, "Aftertouch pressure: ")
    elif func == 0xB0:
        f = control_mode_formatter("Control mode change: ", ch)
    elif func == 0xC0:
        f = channel_formatter("Program Change: ", ch)
    elif func == 0xD0:
        f = channel_formatter("Channel aftertouch: ", ch)
    elif ev[0] in [0xF0, 0xF7]:
        f = meta_formatter("System-exclusive event: ")

    if f:
        if len(ev) > 2:
            print(padding + f.format(ev[1], ev[2]))
        elif len(ev) > 1:
            print(padding + f.format(ev[1]))
        else:
            print(padding + f.format())
    else:
        print(padding + "Unrecognized MIDI event: " + str(ev))


def dump_midi(data, midi_file, options):
    if not options.pretty:
        print(data)
        return
    # First, dump general info, #tracks, etc.
    print("Filename:     " + midi_file)
    i = data[0]
    m_formats = {0: 'single multi-channel track',
                 1: "one or more simultaneous tracks",
                 2: "one or more sequentially independent single-track patterns"}
    print("MIDI format:  " + str(i[0]) + " (" + m_formats.get(i[0], "") + ")")
    print("Divisions:    " + str(i[1]) + " per whole note")
    print("#Tracks:      " + str(len(data[1])))
    n = 0
    for tr in data[1]:
        time = 0
        n += 1
        print()
        print("Track " + str(n) + ":")
        print("    Time 0:")
        for ev in tr:
            if ev[0] > time:
                time = ev[0]
                print("    Time " + str(time) + ": ")
            dump_event(ev[1], time, "        ")


def go():
    options, args = process_options(sys.argv[1:])
    midi_file = args[0]
    midi_data = read_midi(midi_file)
    info = track_info(midi_data)
    if (options.dump or options.pretty):
        dump_midi(midi_data, midi_file, options)
    elif options.regexp:
        import re
        regexp = re.compile(options.regexp)
        numbers = [str(n+1) for n, name in info if regexp.search(name)]
        if numbers:
            if options.prefix:
                sys.stdout.write('%s ' % (options.prefix,))
            sys.stdout.write(','.join(numbers))
            sys.stdout.write('\n')
    else:
        for n, name in info:
            sys.stdout.write('%d %s\n' % (n+1, name,))


if __name__ == '__main__':
    go()
