package speech

import (
	"bufio"
	"bytes"
	"errors"
	"os/exec"
	"runtime"
	"strings"
	"sync"
)

var (
	mu         sync.Mutex
	currentCmd *exec.Cmd

	voiceOnce   sync.Once
	cachedVoice string
)

func parseMacVoice(data []byte) (string, bool) {
	var firstVoice string
	scanner := bufio.NewScanner(bytes.NewReader(data))
	for scanner.Scan() {
		line := scanner.Text()
		idx := strings.Index(line, "ja_JP")
		if idx == -1 {
			continue
		}
		prefix := strings.TrimSpace(line[:idx])
		voiceName := prefix
		if paren := strings.Index(voiceName, "("); paren != -1 {
			voiceName = strings.TrimSpace(voiceName[:paren])
		}
		if voiceName == "Kyoko" {
			return "Kyoko", true
		}
		if firstVoice == "" && voiceName != "" {
			firstVoice = voiceName
		}
	}
	return firstVoice, firstVoice != ""
}

func macVoice() string {
	voiceOnce.Do(func() {
		out, err := exec.Command("say", "-v", "?").Output()
		if err != nil {
			return
		}
		v, ok := parseMacVoice(out)
		if ok {
			cachedVoice = v
		}
	})
	return cachedVoice
}

func speechCmd(text string) (string, []string, error) {
	if runtime.GOOS == "darwin" {
		if _, err := exec.LookPath("say"); err != nil {
			return "", nil, errors.New("speech: say command not found")
		}
		voice := macVoice()
		if voice != "" {
			return "say", []string{"-v", voice, text}, nil
		}
		return "say", []string{text}, nil
	}

	if runtime.GOOS == "linux" {
		if path, err := exec.LookPath("spd-say"); err == nil {
			return path, []string{"-l", "ja", "-w", text}, nil
		}
		if path, err := exec.LookPath("espeak-ng"); err == nil {
			return path, []string{"-v", "ja", text}, nil
		}
		return "", nil, errors.New("speech: neither spd-say nor espeak-ng found")
	}

	return "", nil, errors.New("speech: unsupported platform")
}

// Speak speaks text non-blocking. If an utterance is already in progress,
// Speak kills the previous utterance process before starting the new one.
func Speak(text string) error {
	prog, args, err := speechCmd(text)
	if err != nil {
		return err
	}

	mu.Lock()
	if currentCmd != nil && currentCmd.Process != nil {
		_ = currentCmd.Process.Kill()
	}

	cmd := exec.Command(prog, args...)
	if err := cmd.Start(); err != nil {
		mu.Unlock()
		return err
	}
	currentCmd = cmd
	mu.Unlock()

	go func() {
		_ = cmd.Wait()
		mu.Lock()
		if currentCmd == cmd {
			currentCmd = nil
		}
		mu.Unlock()
	}()

	return nil
}
