package vidio

import (
	"bytes"
	"errors"
	"fmt"
	"io"
	"os"
	"os/exec"
	"regexp"
	"runtime"
	"strconv"
	"strings"
)

// Returns true if file exists, false otherwise.
// https://stackoverflow.com/questions/12518876/how-to-check-if-a-file-exists-in-go.
func exists(filename string) bool {
	_, err := os.Stat(filename)
	if err == nil {
		return true
	}
	if errors.Is(err, os.ErrNotExist) {
		return false
	}
	return false
}

// Checks if the given program is installed.
func installed(program string) error {
	cmd := exec.Command(program, "-version")

	if err := cmd.Run(); err != nil {
		return fmt.Errorf("vidio: %s is not installed", program)
	}

	return nil
}

// Runs ffprobe on the given file and returns a map of the metadata.
func ffprobe(filename, stype string) ([]map[string]string, error) {
	// "stype" is stream stype. "v" for video, "a" for audio.
	// Extract video information with ffprobe.
	cmd := exec.Command(
		"ffprobe",
		"-show_streams",
		"-select_streams", stype,
		"-print_format", "compact",
		"-loglevel", "quiet",
		filename,
	)

	pipe, err := cmd.StdoutPipe()
	if err != nil {
		return nil, err
	}

	if err := cmd.Start(); err != nil {
		return nil, err
	}

	// Read ffprobe output from Stdout.
	builder := bytes.Buffer{}
	buffer := make([]byte, 1024)
	for {
		n, err := pipe.Read(buffer)
		builder.Write(buffer[:n])
		if err == io.EOF {
			break
		}
	}

	// Wait for ffprobe command to complete.
	if err := cmd.Wait(); err != nil {
		return nil, err
	}

	// Parse ffprobe output to fill in video data.
	datalist := make([]map[string]string, 0)
	metadata := builder.String()
	for _, stream := range strings.Split(metadata, "\n") {
		if len(strings.TrimSpace(stream)) > 0 {
			data := make(map[string]string)
			for _, line := range strings.Split(stream, "|") {
				if strings.Contains(line, "=") {
					keyValue := strings.Split(line, "=")
					if _, ok := data[keyValue[0]]; !ok {
						data[keyValue[0]] = keyValue[1]
					}
				}
			}
			datalist = append(datalist, data)
		}
	}

	return datalist, nil
}

// Parses the given data into a float64.
func parse(data string) float64 {
	n, err := strconv.ParseFloat(data, 64)
	if err != nil {
		return 0
	}
	return n
}

// Returns the webcam name used for the -f option with ffmpeg.
func webcam() (string, error) {
	switch runtime.GOOS {
	case "linux":
		return "v4l2", nil
	case "darwin":
		return "avfoundation", nil // qtkit
	case "windows":
		return "dshow", nil // vfwcap
	default:
		return "", fmt.Errorf("vidio: unsupported OS: %s", runtime.GOOS)
	}
}

// For webcam streaming on windows, ffmpeg requires a device name.
// All device names are parsed and returned by this function.
func parseDevices(buffer string) []string {
	index := strings.Index(strings.ToLower(buffer), "directshow video device")
	if index != -1 {
		buffer = buffer[index:]
	}

	index = strings.Index(strings.ToLower(buffer), "directshow audio device")
	if index != -1 {
		buffer = buffer[:index]
	}

	type Pair struct {
		name string
		alt  string
	}
	// Parses ffmpeg output to get device names. Windows only.
	// Uses parsing approach from https://github.com/imageio/imageio/blob/master/imageio/plugins/ffmpeg.py#L681.

	pairs := []Pair{}
	// Find all device names surrounded by quotes. E.g "Windows Camera Front"
	regex := regexp.MustCompile("\"[^\"]+\"")
	for _, line := range strings.Split(strings.ReplaceAll(buffer, "\r\n", "\n"), "\n") {
		if strings.Contains(strings.ToLower(line), "alternative name") {
			match := regex.FindString(line)
			if len(match) > 0 {
				pairs[len(pairs)-1].alt = match[1 : len(match)-1]
			}
		} else {
			match := regex.FindString(line)
			if len(match) > 0 {
				pairs = append(pairs, Pair{name: match[1 : len(match)-1]})
			}
		}
	}

	devices := []string{}
	// If two devices have the same name, use the alternate name of the later device as its name.
	for _, pair := range pairs {
		if contains(devices, pair.name) {
			devices = append(devices, pair.alt)
		} else {
			devices = append(devices, pair.name)
		}
	}
	return devices
}

func contains(list []string, item string) bool {
	for _, i := range list {
		if i == item {
			return true
		}
	}
	return false
}

// Returns the webcam device name.
// On windows, ffmpeg output from the -list_devices command is parsed to find the device name.
func getDevicesWindows() ([]string, error) {
	// Run command to get list of devices.
	cmd := exec.Command(
		"ffmpeg",
		"-hide_banner",
		"-list_devices", "true",
		"-f", "dshow",
		"-i", "dummy",
	)

	pipe, err := cmd.StderrPipe()
	if err != nil {
		return nil, err
	}

	if err := cmd.Start(); err != nil {
		return nil, err
	}

	// Read list devices from Stdout.
	builder := bytes.Buffer{}
	buffer := make([]byte, 1024)
	for {
		n, err := pipe.Read(buffer)
		builder.Write(buffer[:n])
		if err == io.EOF {
			break
		}
	}

	cmd.Wait()

	devices := parseDevices(builder.String())
	return devices, nil
}

// Error representing a strings.Builder failure in the buildSelectExpression func.
var errExpressionBuilder = fmt.Errorf("vidio: failed to write tokens to the frame select expresion")

// Helper function used to generate a "-vf select" expression that specifies which video frames should be exported.
func buildSelectExpression(n ...int) (string, error) {
	sb := strings.Builder{}
	if _, err := sb.WriteString("select='"); err != nil {
		return "", errExpressionBuilder
	}

	for index, frame := range n {
		if index != 0 {
			if _, err := sb.WriteRune('+'); err != nil {
				return "", errExpressionBuilder
			}
		}

		if _, err := sb.WriteString("eq(n\\,"); err != nil {

			return "", errExpressionBuilder
		}

		if _, err := sb.WriteString(strconv.Itoa(frame)); err != nil {

			return "", errExpressionBuilder
		}

		if _, err := sb.WriteRune(')'); err != nil {

			return "", errExpressionBuilder
		}
	}

	if _, err := sb.WriteRune('\''); err != nil {

		return "", errExpressionBuilder
	}

	return sb.String(), nil
}
