package usbcapture

import (
	"bufio"
	"bytes"
	"context"
	_ "embed"
	"errors"
	"fmt"
	"log"
	"mime/multipart"
	"net/http"
	"net/textproto"
	"os/exec"
	"regexp"
	"strconv"
	"strings"
	"syscall"

	"github.com/vladimirvivien/go4vl/device"
	"github.com/vladimirvivien/go4vl/v4l2"
)

/*
	1920 x 1080 60fps = 55Mbps //Edge not support
	1920 x 1080 30fps = 50Mbps
	1920 x 1080 25fps = 40Mbps
	1920 x 1080 20fps = 30Mbps
	1920 x 1080 10fps = 15Mbps

	1360 x 768 60fps = 28Mbps
	1360 x 768 30fps = 25Mbps
	1360 x 768 25fps = 20Mbps
	1360 x 768 20fps = 18Mbps
	1360 x 768 10fps = 10Mbps
*/

// Struct to store the size and fps info
type FormatInfo struct {
	Format string
	Sizes  []SizeInfo
}

type SizeInfo struct {
	Width  int
	Height int
	FPS    []int
}

//go:embed stream_takeover.jpg
var endOfStreamJPG []byte

// start video capture
func (i *Instance) StartVideoCapture(openWithResolution *CaptureResolution) error {
	if i.Capturing {
		return fmt.Errorf("video capture already started")
	}

	if openWithResolution.FPS == 0 {
		openWithResolution.FPS = 25 //Default to 25 FPS
	}

	devName := i.Config.VideoDeviceName
	if openWithResolution == nil {
		return fmt.Errorf("resolution not provided")
	}
	frameRate := openWithResolution.FPS
	buffSize := 8 //No. of frames to buffer
	//Default to MJPEG
	//Other formats that are commonly supported are YUYV, H264, MJPEG
	format := "mjpeg"

	//Check if the video device is a capture device
	isCaptureDev, err := checkVideoCaptureDevice(devName)
	if err != nil {
		return fmt.Errorf("failed to check video device: %w", err)
	}
	if !isCaptureDev {
		return fmt.Errorf("device %s is not a video capture device", devName)
	}

	//Check if the selected FPS is valid in the provided Resolutions
	resolutionIsSupported, err := deviceSupportResolution(i.Config.VideoDeviceName, openWithResolution)
	if err != nil {
		return err
	}
	if !resolutionIsSupported {
		return errors.New("this device do not support the required resolution settings")
	}

	//Open the video device
	camera, err := device.Open(devName,
		device.WithIOType(v4l2.IOTypeMMAP),
		device.WithPixFormat(v4l2.PixFormat{
			PixelFormat: getFormatType(format),
			Width:       uint32(openWithResolution.Width),
			Height:      uint32(openWithResolution.Height),
			Field:       v4l2.FieldAny,
		}),
		device.WithFPS(uint32(frameRate)),
		device.WithBufferSize(uint32(buffSize)),
	)

	if err != nil {
		return fmt.Errorf("failed to open video device: %w", err)
	}

	i.camera = camera
	caps := camera.Capability()
	log.Printf("device [%s] opened\n", devName)
	log.Printf("device info: %s", caps.String())
	// Should get something like this:
	//2025/03/16 15:45:25 device info: driver: uvcvideo; card: USB Video: USB Video; bus info: usb-0000:00:14.0-2

	// set device format
	currFmt, err := camera.GetPixFormat()
	if err != nil {
		return fmt.Errorf("failed to get current pixel format: %w", err)
	}
	log.Printf("Current format: %s", currFmt)
	//2025/03/16 15:45:25 Current format: Motion-JPEG [1920x1080]; field=any; bytes per line=0; size image=0; colorspace=Default; YCbCr=Default; Quant=Default; XferFunc=Default
	i.pixfmt = currFmt.PixelFormat
	i.width = int(currFmt.Width)
	i.height = int(currFmt.Height)

	i.streamInfo = fmt.Sprintf("%s - %s [%dx%d] %d fps",
		caps.Card,
		v4l2.PixelFormats[currFmt.PixelFormat],
		currFmt.Width, currFmt.Height, frameRate,
	)

	// start capture
	ctx, cancel := context.WithCancel(context.TODO())
	if err := camera.Start(ctx); err != nil {
		log.Fatalf("stream capture: %s", err)
	}
	i.cameraStartContext = cancel

	// video stream
	i.frames_buff = camera.GetOutput()

	log.Printf("device capture started (buffer size set %d)", camera.BufferCount())
	i.Capturing = true
	return nil
}

// start http service
func (i *Instance) ServeVideoStream(w http.ResponseWriter, req *http.Request) {
	//Check if the access count is already 1, if so, kick out the previous access
	if i.accessCount >= 1 {
		log.Println("Another client is already connected, kicking out the previous client...")
		if i.videoTakeoverChan != nil {
			i.videoTakeoverChan <- true
		}
		log.Println("Previous client kicked out, taking over the stream...")
	}
	i.accessCount++
	defer func() { i.accessCount-- }()

	// Set up the multipart response
	mimeWriter := multipart.NewWriter(w)
	w.Header().Set("Content-Type", fmt.Sprintf("multipart/x-mixed-replace; boundary=%s", mimeWriter.Boundary()))
	partHeader := make(textproto.MIMEHeader)
	partHeader.Add("Content-Type", "image/jpeg")

	var frame []byte

	//Chrome MJPEG decoder cannot decode the first frame from MS2109 capture card for unknown reason
	//Thus we are discarding the first frame here
	if i.frames_buff != nil {
		select {
		case <-i.frames_buff:
			// Discard the first frame
		default:
			// No frame to discard
		}
	}

	// Streaming loop
	for frame = range i.frames_buff {
		if len(frame) == 0 {
			log.Print("skipping empty frame")
			continue
		}

		partWriter, err := mimeWriter.CreatePart(partHeader)
		if err != nil {
			log.Printf("failed to create multi-part writer: %s", err)
			return
		}

		if _, err := partWriter.Write(frame); err != nil {
			if errors.Is(err, syscall.EPIPE) {
				//broken pipe, the client browser has exited
				return
			}
			log.Printf("failed to write image: %s", err)
		}

		select {
		case <-req.Context().Done():
			// Client disconnected, exit the loop
			return
		case <-i.videoTakeoverChan:
			// Another client is taking over, exit the loop

			//Send the endofstream.jpg as last frame before exit
			endFrameHeader := make(textproto.MIMEHeader)
			endFrameHeader.Add("Content-Type", "image/jpeg")
			endFrameHeader.Add("Content-Length", fmt.Sprint(len(endOfStreamJPG)))
			partWriter, err := mimeWriter.CreatePart(endFrameHeader)
			if err == nil {
				partWriter.Write(endOfStreamJPG)
			}
			log.Println("Video stream taken over by another client, exiting...")
			return
		default:
			// Continue streaming
		}

	}
}

// StopCapture stops the video capture and closes the camera device
func (i *Instance) StopCapture() error {
	if i.camera != nil {
		i.cameraStartContext()
		i.camera.Close()
		i.camera = nil
	}
	i.Capturing = false
	return nil
}

// CheckVideoCaptureDevice checks if the given video device is a video capture device
func checkVideoCaptureDevice(device string) (bool, error) {
	// Run v4l2-ctl to get device capabilities
	cmd := exec.Command("v4l2-ctl", "--device", device, "--all")
	output, err := cmd.CombinedOutput()
	if err != nil {
		return false, fmt.Errorf("failed to execute v4l2-ctl: %w", err)
	}

	// Convert output to string and check for the "Video Capture" capability
	outputStr := string(output)
	if strings.Contains(outputStr, "Video Capture") {
		return true, nil
	}
	return false, nil
}

// GetDefaultVideoDevice returns the first available video capture device, e.g., /dev/video0
func GetDefaultVideoDevice() (string, error) {
	// List all /dev/video* devices and return the first one that is a video capture device
	for i := 0; i < 10; i++ {
		device := fmt.Sprintf("/dev/video%d", i)
		isCapture, err := checkVideoCaptureDevice(device)
		if err != nil {
			continue
		}
		if isCapture {
			return device, nil
		}
	}
	return "", fmt.Errorf("no video capture device found")
}

// deviceSupportResolution checks if the given video device supports the specified resolution and frame rate
func deviceSupportResolution(devicePath string, resolution *CaptureResolution) (bool, error) {
	formatInfo, err := GetV4L2FormatInfo(devicePath)
	if err != nil {
		return false, err
	}

	// Yes, this is an O(N^3) operation, but a video decices rarely have supported resolution
	// more than 20 combinations. The compute time should be fine
	for _, res := range formatInfo {
		for _, size := range res.Sizes {
			//Check if there is a matching resolution
			if size.Height == resolution.Height && size.Width == resolution.Width {
				//Matching resolution. Check if the required FPS is supported
				for _, fps := range size.FPS {
					if fps == resolution.FPS {
						return true, nil
					}
				}
			}
		}
	}

	return false, nil
}

// PrintV4L2FormatInfo prints the supported formats, resolutions, and frame rates of the given video device
func PrintV4L2FormatInfo(devicePath string) {
	// Check if the device is a video capture device
	isCapture, err := checkVideoCaptureDevice(devicePath)
	if err != nil {
		fmt.Printf("Error checking device: %v\n", err)
		return
	}
	if !isCapture {
		fmt.Printf("Device %s is not a video capture device\n", devicePath)
		return
	}

	// Get format info
	formats, err := GetV4L2FormatInfo(devicePath)
	if err != nil {
		fmt.Printf("Error getting format info: %v\n", err)
		return
	}

	// Print format info
	for _, format := range formats {
		fmt.Printf("Format: %s\n", format.Format)
		for _, size := range format.Sizes {
			fmt.Printf("  Size: %dx%d\n", size.Width, size.Height)
			fmt.Printf("    FPS: %v\n", size.FPS)
		}
	}
}

// Function to run the v4l2-ctl command and parse the output
func GetV4L2FormatInfo(devicePath string) ([]FormatInfo, error) {
	// Run the v4l2-ctl command to list formats
	cmd := exec.Command("v4l2-ctl", "--list-formats-ext", "-d", devicePath)
	var out bytes.Buffer
	cmd.Stdout = &out
	err := cmd.Run()
	if err != nil {
		return nil, err
	}

	// Parse the output
	var formats []FormatInfo
	var currentFormat *FormatInfo
	scanner := bufio.NewScanner(&out)

	formatRegex := regexp.MustCompile(`\[(\d+)\]: '(\S+)'`)
	sizeRegex := regexp.MustCompile(`Size: Discrete (\d+)x(\d+)`)
	intervalRegex := regexp.MustCompile(`Interval: Discrete (\d+\.\d+)s \((\d+\.\d+) fps\)`)

	for scanner.Scan() {
		line := scanner.Text()

		// Match format line
		if matches := formatRegex.FindStringSubmatch(line); matches != nil {
			if currentFormat != nil {
				formats = append(formats, *currentFormat)
			}
			// Start a new format entry
			currentFormat = &FormatInfo{
				Format: matches[2],
			}
		}

		// Match size line
		if matches := sizeRegex.FindStringSubmatch(line); matches != nil {
			width, _ := strconv.Atoi(matches[1])
			height, _ := strconv.Atoi(matches[2])

			// Initialize the size entry
			sizeInfo := SizeInfo{
				Width:  width,
				Height: height,
			}

			// Match FPS intervals for the current size
			for scanner.Scan() {
				line = scanner.Text()

				if fpsMatches := intervalRegex.FindStringSubmatch(line); fpsMatches != nil {
					fps, _ := strconv.ParseFloat(fpsMatches[2], 32)
					sizeInfo.FPS = append(sizeInfo.FPS, int(fps))
				} else {
					// Stop parsing FPS intervals when no more matches are found
					break
				}
			}
			// Add the size information to the current format
			currentFormat.Sizes = append(currentFormat.Sizes, sizeInfo)
		}
	}

	// Append the last format if present
	if currentFormat != nil {
		formats = append(formats, *currentFormat)
	}

	if err := scanner.Err(); err != nil {
		return nil, err
	}

	return formats, nil
}

func getFormatType(fmtStr string) v4l2.FourCCType {
	switch strings.ToLower(fmtStr) {
	case "jpeg":
		return v4l2.PixelFmtJPEG
	case "mpeg":
		return v4l2.PixelFmtMPEG
	case "mjpeg":
		return v4l2.PixelFmtMJPEG
	case "h264", "h.264":
		return v4l2.PixelFmtH264
	case "yuyv":
		return v4l2.PixelFmtYUYV
	case "rgb":
		return v4l2.PixelFmtRGB24
	}
	return v4l2.PixelFmtMPEG
}
