#!/opt/local/bin/python3.11

# 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 codecs
import optparse
import os
import subprocess
import sys
import tempfile

"""

# 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 ('/opt/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'))

"""
"""


FESTIVAL_COMMAND = ['festival', '--pipe']
VOICE_CODINGS = {'voice_czech_ph': 'iso-8859-2'}

_USAGE = """lilysong [-p PLAY-PROGRAM] FILE.xml [LANGUAGE-CODE-OR-VOICE [SPEEDUP]]
       lilysong FILE.ly [LANGUAGE-CODE-OR-VOICE]
       lilysong --list-voices
       lilysong --list-languages
"""


def usage():
    print('Usage:', _USAGE)
    sys.exit(2)


def process_options(args):
    parser = optparse.OptionParser(usage=_USAGE, version="2.24.4")
    parser.add_option('', '--list-voices', action='store_true', dest='list_voices',
                      help="list available Festival voices")
    parser.add_option('', '--list-languages', action='store_true', dest='list_languages',
                      help="list available Festival languages")
    parser.add_option('-p', '--play-program', metavar='PROGRAM',
                      action='store', type='string', dest='play_program',
                      help="use PROGRAM to play song immediately")
    options, args = parser.parse_args(args)
    return options, args


def call_festival(scheme_code):
    p = subprocess.Popen(FESTIVAL_COMMAND, stdin=subprocess.PIPE,
                         stdout=subprocess.PIPE, close_fds=True)
    p.stdin.write(scheme_code)
    p.stdin.close()
    answer = ''
    while True:
        process_output = p.stdout.read()
        if not process_output:
            break
        answer = answer + process_output
    return answer


def select_voice(language_or_voice):
    if language_or_voice[:6] == 'voice_':
        voice = language_or_voice
    else:
        voice = call_festival('''
(let ((candidates '()))
  (mapcar (lambda (v)
            (if (eq (cadr (assoc 'language (cadr (voice.description v)))) '%s)
                (set! candidates (cons v candidates))))
          (append (voice.list) (mapcar car Voice_descriptions)))
  (if candidates
      (format t "voice_%%s" (car candidates))
      (format t "nil")))
''' % (language_or_voice,))
        if voice == 'nil':
            voice = None
    return voice


def list_voices():
    print(call_festival('''
(let ((voices (voice.list))
      (print-voice (lambda (v) (format t "voice_%s\n" v))))
  (mapcar print-voice voices)
  (mapcar (lambda (v) (if (not (member v voices)) (print-voice v)))
          (mapcar car Voice_descriptions)))
'''))


def list_languages():
    print(call_festival('''
(let ((languages '()))
  (let ((voices (voice.list))
        (print-language (lambda (v)
                          (let ((language (cadr (assoc 'language (cadr (voice.description v))))))
                            (if (and language (not (member language languages)))
                                (begin
                                  (set! languages (cons language languages))
                                  (print language)))))))
    (mapcar print-language voices)
    (mapcar (lambda (v) (if (not (member v voices)) (print-language v)))
            (mapcar car Voice_descriptions))))
'''))


def process_xml_file(file_name, voice, speedup, play_program):
    if speedup == 1:
        speedup = None
    coding = (VOICE_CODINGS.get(voice) or 'iso-8859-1')
    _, xml_temp_file = tempfile.mkstemp('.xml')
    try:
        # recode the XML file
        recodep = (coding != 'utf-8')
        if recodep:
            decode = codecs.getdecoder('utf-8')
            encode = codecs.getencoder(coding)
        input = open(file_name, encoding='utf-8')
        output = open(xml_temp_file, 'w', encoding='utf-8')
        while True:
            data = input.read()
            if not data:
                break
            if recodep:
                data = encode(decode(data)[0])[0]
            output.write(data)
        output.close()
        # synthesize
        wav_file = file_name[:-3] + 'wav'
        if speedup:
            _, wav_temp_file = tempfile.mkstemp('.wav')
        else:
            wav_temp_file = wav_file
        try:
            print("text2wave -eval '(%s)' -mode singing '%s' -o '%s'" %
                  (voice, xml_temp_file, wav_temp_file,))
            result = os.system("text2wave -eval '(%s)' -mode singing '%s' -o '%s'" %
                               (voice, xml_temp_file, wav_temp_file,))
            if result:
                sys.stdout.write("Festival processing failed.\n")
                return
            if speedup:
                result = os.system("sox '%s' '%s' speed '%f'" %
                                   (wav_temp_file, wav_file, speedup,))
                if result:
                    sys.stdout.write("Festival processing failed.\n")
                    return
        finally:
            if speedup:
                try:
                    os.delete(wav_temp_file)
                except OSError:
                    pass
        sys.stdout.write("%s created.\n" % (wav_file,))
        # play
        if play_program:
            os.system("%s '%s' >/dev/null" % (play_program, wav_file,))
    finally:
        try:
            os.delete(xml_temp_file)
        except OSError:
            pass


def process_ly_file(file_name, voice):
    result = os.system("lilypond '%s'" % (file_name,))
    if result:
        return
    xml_file = None
    for f in os.listdir(os.path.dirname(file_name) or '.'):
        if (f[-4:] == '.xml' and
                (not xml_file or os.stat.st_mtime(f) > os.stat.st_mtime(xml_file))):
            xml_file = f
    if xml_file:
        process_xml_file(xml_file, voice, None, None)
    else:
        sys.stderr.write("No XML file found\n")


def go():
    options, args = process_options(sys.argv[1:])
    if options.list_voices:
        list_voices()
    elif options.list_languages:
        list_languages()
    else:
        arglen = len(args)
        if arglen < 1:
            usage()
        file_name = args[0]
        if arglen > 1:
            language_or_voice = args[1]
            voice = select_voice(language_or_voice)
        else:
            voice = None
        if file_name[-3:] == '.ly':
            if arglen > 2:
                usage()
            process_ly_file(file_name, voice)
        else:
            if arglen > 3:
                usage()
            elif arglen == 3:
                try:
                    speedup = float(args[2])
                except ValueError:
                    usage()
            else:
                speedup = None
            process_xml_file(file_name, voice, speedup, options.play_program)


if __name__ == '__main__':
    go()
