//go:build linux

package player

import (
	"bufio"
	"bytes"
	"context"
	"fmt"
	"log"
	"os"
	"os/exec"
	"strings"
	"sync"
	"time"
)

const playerWindowTitle = "VideoToolsPlayer"

func newController() Controller {
	return &ffplayController{}
}

type ffplayController struct {
	mu     sync.Mutex
	cmd    *exec.Cmd
	stdin  *bufio.Writer
	ctx    context.Context
	cancel context.CancelFunc
	path   string
	paused bool
	seekT  *time.Timer
	seekAt float64
	volume int // 0-100
	winX   int
	winY   int
	winW   int
	winH   int
}

// pickLastID runs a command and returns the last whitespace-delimited token from stdout.
func pickLastID(cmd *exec.Cmd) string {
	out, err := cmd.Output()
	if err != nil {
		return ""
	}
	parts := strings.Fields(string(out))
	if len(parts) == 0 {
		return ""
	}
	return parts[len(parts)-1]
}

var (
	keyFullscreen = []byte{'f'}
	keyPause      = []byte{'p'}
	keyQuit       = []byte{'q'}
	keyVolDown    = []byte{'9'}
	keyVolUp      = []byte{'0'}
)

func (c *ffplayController) Load(path string, offset float64) error {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.path = path
	if c.volume == 0 {
		c.volume = 100
	}
	c.paused = true
	return c.startLocked(offset)
}

func (c *ffplayController) SetWindow(x, y, w, h int) {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.winX, c.winY, c.winW, c.winH = x, y, w, h
}

func (c *ffplayController) Play() error {
	c.mu.Lock()
	defer c.mu.Unlock()
	// Only toggle if we believe we are paused.
	if c.paused {
		if err := c.sendLocked(keyPause); err != nil {
			return err
		}
	}
	c.paused = false
	return nil
}

func (c *ffplayController) Pause() error {
	c.mu.Lock()
	defer c.mu.Unlock()
	if !c.paused {
		if err := c.sendLocked(keyPause); err != nil {
			return err
		}
	}
	c.paused = true
	return nil
}

func (c *ffplayController) Seek(offset float64) error {
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.path == "" {
		return fmt.Errorf("no source loaded")
	}
	if offset < 0 {
		offset = 0
	}
	c.seekAt = offset
	if c.seekT != nil {
		c.seekT.Stop()
	}
	c.seekT = time.AfterFunc(90*time.Millisecond, func() {
		c.mu.Lock()
		defer c.mu.Unlock()
		// Timer may fire after stop; guard.
		if c.path == "" {
			return
		}
		_ = c.startLocked(c.seekAt)
	})
	return nil
}

func (c *ffplayController) FullScreen() error { return c.send(keyFullscreen) }
func (c *ffplayController) Stop() error       { return c.send(keyQuit) }
func (c *ffplayController) SetVolume(level float64) error {
	c.mu.Lock()
	defer c.mu.Unlock()
	target := int(level + 0.5)
	if target < 0 {
		target = 0
	}
	if target > 100 {
		target = 100
	}
	if target == c.volume {
		return nil
	}
	diff := target - c.volume
	c.volume = target

	if !c.runningLocked() {
		return nil
	}

	key := keyVolUp
	steps := diff
	if diff < 0 {
		key = keyVolDown
		steps = -diff
	}
	// Limit burst size to avoid overwhelming stdin.
	for i := 0; i < steps; i++ {
		if err := c.sendLocked(key); err != nil {
			return err
		}
		// Tiny delay to let ffplay process the keys.
		if steps > 8 {
			time.Sleep(8 * time.Millisecond)
		}
	}
	return nil
}

func (c *ffplayController) Close() {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.stopLocked()
}

func (c *ffplayController) send(seq []byte) error {
	c.mu.Lock()
	defer c.mu.Unlock()
	return c.sendLocked(seq)
}

func (c *ffplayController) sendLocked(seq []byte) error {
	if !c.runningLocked() {
		return fmt.Errorf("ffplay not running")
	}
	if _, err := c.stdin.Write(seq); err != nil {
		return err
	}
	return c.stdin.Flush()
}

func (c *ffplayController) stopLocked() {
	if c.stdin != nil {
		c.stdin.Write(keyQuit)
		c.stdin.Flush()
	}
	if c.cancel != nil {
		c.cancel()
	}
	c.cmd = nil
	c.stdin = nil
	c.cancel = nil
	c.path = ""
	c.paused = false
	if c.seekT != nil {
		c.seekT.Stop()
		c.seekT = nil
	}
}

func (c *ffplayController) waitForExit(cmd *exec.Cmd, cancel context.CancelFunc, stderr *bytes.Buffer) {
	err := cmd.Wait()
	exit := ""
	if cmd.ProcessState != nil {
		exit = cmd.ProcessState.String()
	}
	if err != nil {
		msg := strings.TrimSpace(stderr.String())
		if msg != "" {
			log.Printf("[ffplay] exit error: %v (%s) stderr=%s", err, exit, msg)
		} else {
			log.Printf("[ffplay] exit error: %v (%s)", err, exit)
		}
	} else {
		msg := strings.TrimSpace(stderr.String())
		if msg != "" {
			log.Printf("[ffplay] exit: %s stderr=%s", exit, msg)
		} else {
			log.Printf("[ffplay] exit: %s", exit)
		}
	}
	cancel()
	c.mu.Lock()
	defer c.mu.Unlock()
	c.cmd = nil
	c.stdin = nil
	c.ctx = nil
	c.cancel = nil
	c.path = ""
	c.paused = false
	if c.seekT != nil {
		c.seekT.Stop()
		c.seekT = nil
	}
}

func (c *ffplayController) runningLocked() bool {
	if c.cmd == nil || c.stdin == nil {
		return false
	}
	if c.cmd.ProcessState != nil && c.cmd.ProcessState.Exited() {
		return false
	}
	return true
}

func (c *ffplayController) startLocked(offset float64) error {
	if _, err := exec.LookPath("ffplay"); err != nil {
		return fmt.Errorf("ffplay not found in PATH: %w", err)
	}

	if strings.TrimSpace(c.path) == "" {
		return fmt.Errorf("no input path set")
	}
	input := c.path

	c.stopLocked()
	c.path = input

	ctx, cancel := context.WithCancel(context.Background())
	args := []string{
		"-hide_banner",
		"-loglevel", "error",
		"-autoexit",
		"-window_title", playerWindowTitle,
		"-noborder",
	}
	if c.winW > 0 {
		args = append(args, "-x", fmt.Sprintf("%d", c.winW))
	}
	if c.winH > 0 {
		args = append(args, "-y", fmt.Sprintf("%d", c.winH))
	}
	if c.volume <= 0 {
		args = append(args, "-volume", "0")
	} else {
		args = append(args, "-volume", fmt.Sprintf("%d", c.volume))
	}
	if offset > 0 {
		args = append(args, "-ss", fmt.Sprintf("%.3f", offset))
	}
	args = append(args, input)

	cmd := exec.CommandContext(ctx, "ffplay", args...)
	env := os.Environ()
	if c.winX != 0 || c.winY != 0 {
		// SDL honors SDL_VIDEO_WINDOW_POS for initial window placement.
		pos := fmt.Sprintf("%d,%d", c.winX, c.winY)
		env = append(env, fmt.Sprintf("SDL_VIDEO_WINDOW_POS=%s", pos))
	}
	if os.Getenv("SDL_VIDEODRIVER") == "" {
		env = append(env, "SDL_VIDEODRIVER=x11")
	}
	if os.Getenv("XDG_RUNTIME_DIR") == "" {
		run := fmt.Sprintf("/run/user/%d", os.Getuid())
		if fi, err := os.Stat(run); err == nil && fi.IsDir() {
			env = append(env, fmt.Sprintf("XDG_RUNTIME_DIR=%s", run))
		}
	}
	cmd.Env = env
	var stderr bytes.Buffer
	cmd.Stderr = &stderr
	stdin, err := cmd.StdinPipe()
	if err != nil {
		cancel()
		return err
	}
	if err := cmd.Start(); err != nil {
		msg := strings.TrimSpace(stderr.String())
		if msg != "" {
			return fmt.Errorf("ffplay start failed: %w (%s)", err, msg)
		}
		cancel()
		return err
	}
	log.Printf("[ffplay] start pid=%d args=%v pos=(%d,%d) size=%dx%d offset=%.3f vol=%d env(SDL_VIDEODRIVER=%s XDG_RUNTIME_DIR=%s DISPLAY=%s)", cmd.Process.Pid, args, c.winX, c.winY, c.winW, c.winH, offset, c.volume, os.Getenv("SDL_VIDEODRIVER"), os.Getenv("XDG_RUNTIME_DIR"), os.Getenv("DISPLAY"))

	c.cmd = cmd
	c.stdin = bufio.NewWriter(stdin)
	c.ctx = ctx
	c.cancel = cancel

	// Best-effort window placement via xdotool in case WM ignores SDL hints.
	if c.winW > 0 && c.winH > 0 {
		go func(title string, x, y, w, h int) {
			time.Sleep(120 * time.Millisecond)
			ffID := pickLastID(exec.Command("xdotool", "search", "--name", title))
			mainID := pickLastID(exec.Command("xdotool", "search", "--name", "VideoTools"))
			if ffID == "" {
				return
			}
			// Reparent into main window if found, then move/size.
			if mainID != "" {
				_ = exec.Command("xdotool", "windowreparent", ffID, mainID).Run()
			}
			_ = exec.Command("xdotool", "windowmove", ffID, fmt.Sprintf("%d", x), fmt.Sprintf("%d", y)).Run()
			_ = exec.Command("xdotool", "windowsize", ffID, fmt.Sprintf("%d", w), fmt.Sprintf("%d", h)).Run()
			_ = exec.Command("xdotool", "windowraise", ffID).Run()
		}(playerWindowTitle, c.winX, c.winY, c.winW, c.winH)
	}

	go c.waitForExit(cmd, cancel, &stderr)

	// Reapply paused state if needed (ffplay starts unpaused).
	if c.paused {
		time.Sleep(20 * time.Millisecond)
		_ = c.sendLocked(keyPause)
	}
	return nil
}
