package main

//
// Example: Audio/Video Player using the LT API client
//
// This example demonstrates how to:
//      1. Connect to an LT310 device and retrieve audio/video data from a given source URL.
//      2. Start two independent workers:
//          - One for fetching and displaying video frames using OpenGL.
//          - One for fetching and playing audio frames using the oto audio library.
//      3. Use channels to pass packets from the workers to the player loops.
//      4. Handle packet drops when the playback loop cannot keep up with the input rate.
//      5. Gracefully stop background goroutines when the window is closed.
//
// Usage:
//      av_player.exe -source <sourceURL>
//
// Arguments:
//      -source, -s   URL of a valid LT device input, for example:
//                        lt310:/0/hdmi-in/0
//                        lt310:/0/sdi-in/1
//
// Key points:
//      - The source URL must be provided by the user; this example does not scan or select inputs automatically.
//      - Video rendering must remain in the main thread because OpenGL contexts are thread-specific.
//      - Audio and video playback are handled in separate goroutines to avoid blocking one stream with the other.
//      - The LT client API is used to create "data workers" that provide raw frames according to the selected media type.
//      - Packet metadata is provided as JSON and can be unmarshalled into the corresponding lt.VideoMetadata or lt.AudioMetadata structs.
//
// Note:
//      This code is intended for demonstration purposes. For production use,
//      ensure proper error handling and resource cleanup.
//
// © 2025 Enciris Technologies. All rights reserved.
//

import (
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"log"
	"math"
	"os"
	"runtime"
	"sync"
	"time"

	"github.com/go-gl/gl/v4.1-core/gl"
	"github.com/go-gl/glfw/v3.3/glfw"
	"github.com/hajimehoshi/oto"

	lt "lt/client/go"
	"lt/client/go/example/av_player/surface"
)

// Stats structure
type statsData struct {
	VideoFPS     float64
	VideoMissed  int
	VideoDropped int
	//
	AudioRate    float64 // kb/s
	AudioMissed  int
	AudioDropped int
}

var (
	statsMu sync.Mutex
	stats   statsData
)

var argUsage = `
Arguments:
    -source <string>   URL of a valid LT device input (e.g. lt310:/0/hdmi-in/0)

This example connects to the given source, retrieves audio and video frames
from the LT API, and plays them in real time.
`

func main() {
	// Command-line flags
	flag.Usage = func() {
		fmt.Printf("Usage:\n    %s -source <sourceURL>\n", os.Args[0])
		fmt.Print(argUsage)
		os.Exit(1)
	}

	var sourceURL string
	flag.StringVar(&sourceURL, "source", "", "Source URL (e.g. lt310:/0/hdmi-in/0)")
	flag.Parse()

	if sourceURL == "" {
		flag.Usage()
	}

	// -------------------------------------------------------------------------
	// Check if input selected is active
	// -------------------------------------------------------------------------
	var input lt.Input
	{
		if err := lt.Get(sourceURL, &input); err != nil {
			log.Fatal(err)
		}
		if input.Video.Signal != "locked" {
			log.Fatal("input is not locked")
		}
		fmt.Println("input source:", sourceURL)
	}

	// Video parameters
	width, height, interlaced, fps := input.Video.Size[0], input.Video.Size[1], input.Video.Interlaced, input.Video.Framerate
	fmt.Printf("Video: %dx%d %s %.2f Hz\n", width, height, map[bool]string{true: "i", false: "p"}[interlaced], fps)

	// Audio parameters
	channels, samplerate, depth := input.Audio.Channels, input.Audio.Samplerate, input.Audio.Depth
	fmt.Printf("Audio: %d KHz %d channels %d-bit pcm\n", samplerate, channels, depth)

	// Select media type
	videoMedia := "video/nv12" // "video/yuyv"
	audioMedia := "audio/pcm"  // "audio/pcm"

	// -------------------------------------------------------------------------
	// Start fetch video frames thread
	// -------------------------------------------------------------------------
	playVideo := make(chan *lt.Packet, 1)
	closeVideo := make(chan struct{})
	go fetchVideoFrame(sourceURL, videoMedia, playVideo, closeVideo)

	time.Sleep(100 * time.Millisecond) // Wait for video thread to start

	// -------------------------------------------------------------------------
	// Start fetch audio frames thread
	// -------------------------------------------------------------------------
	playAudio := make(chan *lt.Packet, 1)
	closeAudio := make(chan struct{})
	go fetchAudioFrame(sourceURL, audioMedia, playAudio, closeAudio)

	// -------------------------------------------------------------------------
	// Start stats printer
	// -------------------------------------------------------------------------
	go printStatsLoop()

	// -------------------------------------------------------------------------
	// Audio player
	// -------------------------------------------------------------------------
	// Initialize audio player
	size := channels * samplerate * depth / (8 * 20)
	ctx, err := oto.NewContext(samplerate, channels, depth/8, size)
	if err != nil {
		log.Fatal(err)
	}
	audioPlayer := ctx.NewPlayer()

	// Start audio player thread
	go func() {
		defer audioPlayer.Close()

		for {
			select {
			case <-closeAudio:
				return
			case packet := <-playAudio:
				audioPlayer.Write(packet.Data)
				packet.Close()
			}
		}
	}()

	// -------------------------------------------------------------------------
	// Video player
	// -------------------------------------------------------------------------

	// Initialize the GL library
	runtime.LockOSThread()
	defer runtime.UnlockOSThread()

	if err := glfw.Init(); err != nil {
		log.Fatal(err)
	}
	defer glfw.Terminate()

	// Create a windowed mode window and its OpenGL context
	title := fmt.Sprintf("%s (%dx%d%s%02.02f Hz)", sourceURL, width, height, map[bool]string{true: "i", false: "p"}[interlaced], fps)
	window, err := glfw.CreateWindow(640, 360, title, nil, nil)
	if err != nil {
		log.Fatal(err)
	}
	window.SetFramebufferSizeCallback(surface.SizeCallback)
	window.SetKeyCallback(surface.KeyCallback)

	// Make the window's context current
	window.MakeContextCurrent()

	// OpenGL
	if err := gl.Init(); err != nil {
		log.Fatal(err)
	}
	fmt.Println("OpenGL version", gl.GoStr(gl.GetString(gl.VERSION)))

	// OpenGL surface to render to
	if err := surface.Init(); err != nil {
		log.Fatal(err)
	}

	// Render loop
	for !window.ShouldClose() {
		select {
		case packet := <-playVideo:
			// Packet metadata
			var meta lt.VideoMetadata
			if err := json.Unmarshal(packet.Meta, &meta); err != nil {
				log.Fatal("worker packet metadata:", err)
			}

			// OpenGL draw buffer
			surface.Draw(videoMedia, meta.Size[0], meta.Size[1], packet.Data)
			// Release packet reference
			packet.Close()

			// Update window size
			w, h := window.GetFramebufferSize()
			gl.Viewport(0, 0, int32(w), int32(h))

			// Update window title
			if width != meta.Size[0] || height != meta.Size[1] || fps != meta.Framerate || interlaced != meta.Interlaced {
				window.SetTitle(fmt.Sprintf("%s (%dx%d%s%02.02f)", sourceURL, meta.Size[0], meta.Size[1], map[bool]string{true: "i", false: "p"}[interlaced], meta.Framerate))
				width, height, fps, interlaced = meta.Size[0], meta.Size[1], meta.Framerate, meta.Interlaced
			}

			// Swap front and back buffers
			window.SwapBuffers()
		case <-time.After(50 * time.Millisecond):
		}

		// Poll for window events
		glfw.PollEvents()
	}

	// Close channels to stop goroutines
	close(closeVideo)
	close(closeAudio)
}

func fetchVideoFrame(sourceURL, media string, playChan chan<- *lt.Packet, closeChan <-chan struct{}) {
	var client lt.Client
	defer client.Close()

	// Create a worker to capture video frames
	err := client.Post(sourceURL+"/data", lt.VideoDataWorker{Media: media}, nil)
	if !errors.Is(err, lt.ErrRedirect) {
		log.Fatal("worker creation failed:", err)
	}
	workerURL := lt.RedirectLocation(err)

	// Loop
	var prevTS int64
	var missed, dropped int
	start := time.Now()

	for cnt := 0; ; cnt++ {
		// Fetch worker response
		var worker lt.Worker
		if err := client.Get(workerURL, &worker); err != nil {
			log.Fatal(err)
		}

		// Packet data
		if len(worker.Packets) == 0 {
			log.Fatal("worker packet: not found")
		}
		packet := worker.Packets[0]

		// Packet metadata
		var meta lt.VideoMetadata
		if err := json.Unmarshal(packet.Meta, &meta); err != nil {
			log.Fatal("worker packet metadata:", err)
		}

		// Count missed packet
		ts := packet.Timestamp
		if prevTS != 0 {
			inc := int(math.Round(float64(ts-prevTS) * meta.Framerate / 1_000_000))
			if inc > 1 {
				missed++
			}
		}
		prevTS = ts

		// Update stats
		duration := time.Since(start).Seconds()
		if duration > 1 {
			statsMu.Lock()
			stats.VideoFPS = float64(cnt) / duration
			stats.VideoMissed = missed
			stats.VideoDropped = dropped
			statsMu.Unlock()
			//
			start = time.Now()
			cnt = 0
		}

		// Send packet to player thread or drop if full (don't forget to release packet reference if dropped)
		select {
		case <-closeChan:
			packet.Close()
			return
		case playChan <- &packet:
		default:
			dropped++
			packet.Close()
		}
	}
}

func fetchAudioFrame(sourceURL, media string, playChan chan<- *lt.Packet, closeChan <-chan struct{}) {
	var client lt.Client
	defer client.Close()

	// Create a worker to capture audio frames
	err := client.Post(sourceURL+"/data", lt.AudioDataWorker{Media: media}, nil)
	if !errors.Is(err, lt.ErrRedirect) {
		log.Fatal("worker creation failed:", err)
	}
	workerURL := lt.RedirectLocation(err)

	// Loop
	var prevTS int64
	var missed, dropped int
	var totalBytes int
	start := time.Now()

	for cnt := 0; ; cnt++ {
		// Fetch worker response
		var worker lt.Worker
		if err := client.Get(workerURL, &worker); err != nil {
			log.Fatal(err)
		}

		if len(worker.Packets) == 0 {
			log.Fatal("worker packet: not found")
		}
		packet := worker.Packets[0]

		// Metadata
		var meta lt.AudioMetadata
		if err := json.Unmarshal(packet.Meta, &meta); err != nil {
			log.Fatal("worker packet metadata:", err)
		}

		// Count missed packet (approximation: 1 audio packet ≈ 1 video frame)
		ts := packet.Timestamp
		if prevTS != 0 {
			frameDuration := float64(len(packet.Data)) / float64(meta.Samplerate*meta.Channels*(meta.Depth/8))
			approxFPS := 1 / frameDuration
			inc := int(math.Round(float64(ts-prevTS) * approxFPS / 1_000_000))
			missed += inc - 1
		}
		prevTS = ts

		// Update stats
		totalBytes += len(packet.Data)
		duration := time.Since(start).Seconds()
		if duration > 1 {
			statsMu.Lock()
			stats.AudioRate = float64(totalBytes/1024) / duration // kB/s
			stats.AudioMissed = missed
			stats.AudioDropped = dropped
			statsMu.Unlock()
			//
			totalBytes = 0
			start = time.Now()
			cnt = 0
		}

		// Send packet to player thread or drop if full (don't forget to release packet reference if dropped)
		select {
		case <-closeChan:
			packet.Close()
			return
		case playChan <- &packet:
		default:
			dropped++
			packet.Close()
		}
	}
}

func printStatsLoop() {
	ticker := time.NewTicker(time.Second)
	defer ticker.Stop()

	// Print stats every second
	for range ticker.C {
		statsMu.Lock()
		fmt.Printf(
			"Video: %.2f FPS - missed %d - dropped %d | Audio: %.1f kB/s - missed %d - dropped %d\n",
			stats.VideoFPS, stats.VideoMissed, stats.VideoDropped,
			stats.AudioRate, stats.AudioMissed, stats.AudioDropped,
		)
		statsMu.Unlock()
	}
}
