// Package ffmpeg provides a wrapper around the ffmpeg and ffprobe executables.
package ffmpeg

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"os/exec"
	"regexp"
	"strconv"
	"strings"
	"sync"

	stashExec "github.com/stashapp/stash/pkg/exec"
	"github.com/stashapp/stash/pkg/fsutil"
	"github.com/stashapp/stash/pkg/logger"
)

func ffmpegHelp(ffmpegPath string) (string, error) {
	cmd := stashExec.Command(ffmpegPath, "-h")
	bytes, err := cmd.CombinedOutput()
	output := string(bytes)
	if err != nil {
		var exitErr *exec.ExitError
		if errors.As(err, &exitErr) {
			return "", fmt.Errorf("error running ffmpeg: %v", output)
		}

		return "", fmt.Errorf("error running ffmpeg: %v", err)
	}

	return output, nil
}

func ValidateFFMpeg(ffmpegPath string) error {
	_, err := ffmpegHelp(ffmpegPath)
	return err
}

func ValidateFFMpegCodecSupport(ffmpegPath string) error {
	output, err := ffmpegHelp(ffmpegPath)
	if err != nil {
		return err
	}

	var missingSupport []string

	if !strings.Contains(output, "--enable-libopus") {
		missingSupport = append(missingSupport, "libopus")
	}
	if !strings.Contains(output, "--enable-libvpx") {
		missingSupport = append(missingSupport, "libvpx")
	}
	if !strings.Contains(output, "--enable-libx264") {
		missingSupport = append(missingSupport, "libx264")
	}
	if !strings.Contains(output, "--enable-libx265") {
		missingSupport = append(missingSupport, "libx265")
	}
	if !strings.Contains(output, "--enable-libwebp") {
		missingSupport = append(missingSupport, "libwebp")
	}

	if len(missingSupport) > 0 {
		return fmt.Errorf("ffmpeg missing codec support: %v", missingSupport)
	}

	return nil
}

func LookPathFFMpeg() string {
	ret, _ := exec.LookPath(getFFMpegFilename())

	if ret != "" {
		// ensure ffmpeg has the correct flags
		if err := ValidateFFMpeg(ret); err != nil {
			logger.Warnf("ffmpeg found (%s), could not be executed: %v", ret, err)
			ret = ""
		}
	}

	return ret
}

func FindFFMpeg(path string) string {
	ret := fsutil.FindInPaths([]string{path}, getFFMpegFilename())

	if ret != "" {
		// ensure ffmpeg has the correct flags
		if err := ValidateFFMpeg(ret); err != nil {
			logger.Warnf("ffmpeg found (%s), could not be executed: %v", ret, err)
			ret = ""
		}
	}

	return ret
}

// ResolveFFMpeg attempts to resolve the path to the ffmpeg executable.
// It first looks in the provided path, then resolves from the environment, and finally looks in the fallback path.
// It will prefer an ffmpeg binary that has the required codec support.
// Returns an empty string if a valid ffmpeg cannot be found.
func ResolveFFMpeg(path string, fallbackPath string) string {
	var ret string
	// look in the provided path first
	pathFound := FindFFMpeg(path)
	if pathFound != "" {
		err := ValidateFFMpegCodecSupport(pathFound)
		if err == nil {
			return pathFound
		}

		logger.Warnf("ffmpeg found (%s), but it is missing required flags: %v", pathFound, err)
		ret = pathFound
	}

	// then resolve from the environment
	envFound := LookPathFFMpeg()
	if envFound != "" {
		err := ValidateFFMpegCodecSupport(envFound)
		if err == nil {
			return envFound
		}

		logger.Warnf("ffmpeg found (%s), but it is missing required flags: %v", envFound, err)
		if ret == "" {
			ret = envFound
		}
	}

	// finally, look in the fallback path
	fallbackFound := FindFFMpeg(fallbackPath)
	if fallbackFound != "" {
		err := ValidateFFMpegCodecSupport(fallbackFound)
		if err == nil {
			return fallbackFound
		}

		logger.Warnf("ffmpeg found (%s), but it is missing required flags: %v", fallbackFound, err)
		if ret == "" {
			ret = fallbackFound
		}
	}

	return ret
}

var version_re = regexp.MustCompile(`ffmpeg version n?((\d+)\.(\d+)(?:\.(\d+))?)`)

func (f *FFMpeg) getVersion() error {
	var args Args
	args = append(args, "-version")
	cmd := f.Command(context.Background(), args)

	var stdout bytes.Buffer
	cmd.Stdout = &stdout

	var err error
	if err = cmd.Run(); err != nil {
		return err
	}

	stdoutStr := stdout.String()
	match := version_re.FindStringSubmatchIndex(stdoutStr)
	if match == nil {
		return errors.New("version string malformed")
	}

	majorS := stdoutStr[match[4]:match[5]]
	minorS := stdoutStr[match[6]:match[7]]

	// patch is optional
	var patchS string
	if match[8] != -1 && match[9] != -1 {
		patchS = stdoutStr[match[8]:match[9]]
	}

	if i, err := strconv.Atoi(majorS); err == nil {
		f.version.major = i
	}
	if i, err := strconv.Atoi(minorS); err == nil {
		f.version.minor = i
	}
	if i, err := strconv.Atoi(patchS); err == nil {
		f.version.patch = i
	}
	logger.Debugf("FFMpeg version %s detected", f.version.String())

	return nil
}

// FFMpeg version params
type Version struct {
	major int
	minor int
	patch int
}

// Gteq returns true if the version is greater than or equal to the other version.
func (v Version) Gteq(other Version) bool {
	if v.major > other.major {
		return true
	}
	if v.major == other.major && v.minor > other.minor {
		return true
	}
	if v.major == other.major && v.minor == other.minor && v.patch >= other.patch {
		return true
	}
	return false
}

func (v Version) String() string {
	return fmt.Sprintf("%d.%d.%d", v.major, v.minor, v.patch)
}

// FFMpeg provides an interface to ffmpeg.
type FFMpeg struct {
	ffmpeg              string
	version             Version
	hwCodecSupport      []VideoCodec
	hwCodecSupportMutex sync.RWMutex
}

// Creates a new FFMpeg encoder
func NewEncoder(ffmpegPath string) *FFMpeg {
	ret := &FFMpeg{
		ffmpeg: ffmpegPath,
	}
	if err := ret.getVersion(); err != nil {
		logger.Warnf("FFMpeg version not detected %v", err)
	}

	return ret
}

// Returns an exec.Cmd that can be used to run ffmpeg using args.
func (f *FFMpeg) Command(ctx context.Context, args []string) *exec.Cmd {
	return stashExec.CommandContext(ctx, string(f.ffmpeg), args...)
}

func (f *FFMpeg) Path() string {
	return f.ffmpeg
}

func (f *FFMpeg) getHWCodecSupport() []VideoCodec {
	f.hwCodecSupportMutex.RLock()
	defer f.hwCodecSupportMutex.RUnlock()
	return f.hwCodecSupport
}
