"""say.py - make the computer speak, in English, Thai or Chinese.

No API key. No sign-up. No bill. The voices are free.

    pip3 install edge-tts

    python3 say.py "Good morning, teacher."
    python3 say.py "สวัสดีตอนเช้าค่ะ"
    python3 say.py "老师早上好"
    python3 say.py --lang th --voice th-TH-NiwatNeural "ขอบคุณครับ"
    python3 say.py --voices

The program looks at your LETTERS and picks the right voice by itself.
Thai letters get a Thai voice. Chinese characters get a Chinese voice.
Everything else gets an English voice.
"""
import argparse
import asyncio
import subprocess
import sys
from pathlib import Path

import edge_tts

if hasattr(sys.stdout, "reconfigure"):
    sys.stdout.reconfigure(encoding="utf-8", errors="replace")

# One good voice for each language. Change these to any name from --voices.
VOICES = {
    "en": "en-GB-LibbyNeural",
    "th": "th-TH-PremwadeeNeural",
    "zh": "zh-CN-XiaoxiaoNeural",
}


def detect_language(text):
    """Guess the language from the shape of the letters.

    Every letter has a number. Thai letters sit between 0E00 and 0E7F.
    Chinese characters sit between 4E00 and 9FFF. We just count them.
    """
    thai = sum(1 for c in text if "฀" <= c <= "๿")
    han = sum(1 for c in text if "一" <= c <= "鿿")
    if thai > han and thai > 0:
        return "th"
    if han > 0:
        return "zh"
    return "en"


async def speak(text, voice, out_path, rate, tries=4):
    """Send the words away and get an mp3 back.

    The free voice service fails now and then for no reason. We saw it fail
    once and work perfectly two seconds later. So we try again instead of
    giving up. Wait a little longer after each try.
    """
    for attempt in range(1, tries + 1):
        try:
            talker = edge_tts.Communicate(text, voice, rate=rate)
            await talker.save(str(out_path))
            if out_path.stat().st_size > 0:
                return attempt
        except edge_tts.exceptions.NoAudioReceived:
            pass
        if attempt < tries:
            print("  (no sound came back, trying again)")
            await asyncio.sleep(2 * attempt)
    raise RuntimeError("The voice service gave nothing back after %d tries." % tries)


async def list_voices(langs=("en-GB", "th-", "zh-CN")):
    manager = await edge_tts.VoicesManager.create()
    for prefix in langs:
        found = [v for v in manager.voices if v["Locale"].startswith(prefix)]
        print("\n%s  (%d)" % (prefix, len(found)))
        for v in found:
            print("   %-30s %s" % (v["ShortName"], v["Gender"]))


def play(path):
    """Play the file, using whatever this computer has."""
    if sys.platform == "darwin":
        commands = [["afplay", str(path)]]
    elif sys.platform.startswith("win"):
        commands = [["powershell", "-NoProfile", "-c",
                     "Add-Type -AssemblyName presentationCore; "
                     "$p = New-Object System.Windows.Media.MediaPlayer; "
                     "$p.Open('%s'); $p.Play(); Start-Sleep 6" % Path(path).resolve()]]
    else:
        commands = [["mpg123", "-q", str(path)], ["ffplay", "-nodisp", "-autoexit", str(path)]]

    for cmd in commands:
        try:
            subprocess.run(cmd, check=True,
                           stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
            return True
        except (FileNotFoundError, subprocess.CalledProcessError):
            continue
    return False


def main():
    parser = argparse.ArgumentParser(description="Make the computer speak.")
    parser.add_argument("words", nargs="*", help="what to say")
    parser.add_argument("--lang", choices=sorted(VOICES), default=None,
                        help="force a language instead of guessing")
    parser.add_argument("--voice", default=None, help="an exact voice name")
    parser.add_argument("--out", default=None, help="where to save the mp3")
    parser.add_argument("--rate", default="+0%", help="speed, e.g. -25%% for slow")
    parser.add_argument("--voices", action="store_true", help="list the voices and stop")
    parser.add_argument("--quiet", action="store_true", help="save it, do not play it")
    args = parser.parse_args()

    if args.voices:
        asyncio.run(list_voices())
        return 0

    text = " ".join(args.words).strip()
    if not text:
        parser.print_help()
        return 0

    lang = args.lang or detect_language(text)
    voice = args.voice or VOICES[lang]
    out_path = Path(args.out) if args.out else Path("say_%s.mp3" % lang)

    print("  language : %s  (guessed from the letters)" % lang
          if not args.lang else "  language : %s" % lang)
    print("  voice    : %s" % voice)
    print("  saving   : %s" % out_path)

    try:
        attempts = asyncio.run(speak(text, voice, out_path, args.rate))
        if attempts > 1:
            print("  note     : it took %d tries" % attempts)
    except Exception as problem:
        text_l = str(problem).lower()
        if "connect" in text_l or "resolve" in text_l or "timed out" in text_l:
            print("\n  No internet. The voices are made on a server, so this needs wifi.")
        else:
            print("\n  Could not make the sound: %s" % problem)
        return 1

    size = out_path.stat().st_size
    print("  done     : %d bytes" % size)

    if not args.quiet and not play(out_path):
        print("  (No player found. The file is saved - open it yourself.)")
    return 0


if __name__ == "__main__":
    sys.exit(main())
