#!/usr/bin/env python3

# Copyright (c) 2006--2020 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--2020  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.

"""

for d in ['/usr/share/lilypond/2.21.3',
	  '/usr/lib/lilypond/2.21.3']:
    sys.path.insert (0, os.path.join (d, 'python'))

# dynamic relocation, for GUB binaries.
bindir = os.path.abspath (os.path.dirname (sys.argv[0]))
for p in ['share', 'lib']:
    datadir = os.path.abspath (bindir + '/../%s/lilypond/current/python/' % p)
    sys.path.insert (0, datadir)

# Python scripts executed during 'make test' and 'make test-baseline'
# must use their own versions of the scripts and the files loaded by
# those scripts. Assume to be in such a situation if the path to the
# script ends with 'scripts/out'.
if bindir.endswith (r'/scripts/out'):
    # only works for in-tree builds
    sys.path.insert (0, bindir + r'/../../python')
    # build in separate dir, but incorrect.
    sys.path.insert (0, bindir + r'/../../python/out')
"""
"""


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.21.3")
    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)
        output = open (xml_temp_file, 'w')
        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:
                    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:
            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 ()
