package photoprism

import (
	"bytes"
	"errors"
	"fmt"
	"math"
	"os"
	"os/exec"
	"path/filepath"
	"time"

	"github.com/photoprism/photoprism/internal/event"
	"github.com/photoprism/photoprism/internal/ffmpeg"
	"github.com/photoprism/photoprism/internal/ffmpeg/encode"
	"github.com/photoprism/photoprism/pkg/clean"
	"github.com/photoprism/photoprism/pkg/fs"
)

// ToAvc converts a single video file to MPEG-4 AVC.
func (w *Convert) ToAvc(f *MediaFile, encoder encode.Encoder, noMutex, force bool) (file *MediaFile, err error) {
	// Abort if the source media file is nil.
	if f == nil {
		return nil, fmt.Errorf("convert: no media file provided for processing - you may have found a bug")
	}

	// Sanitized relative filename for use in logs.
	logFileName := clean.Log(f.RootRelName())

	// Abort if the source media file does not exist.
	if !f.Exists() {
		return nil, fmt.Errorf("convert: %s not found", logFileName)
	} else if f.Empty() {
		return nil, fmt.Errorf("convert: %s is empty", logFileName)
	}

	// AVC video filename.
	var avcName string

	// Use .mp4 file extension for animated images and .avi for videos.
	if f.IsAnimatedImage() {
		avcName = fs.VideoMp4.FindFirst(f.FileName(), []string{w.conf.SidecarPath(), fs.PPHiddenPathname}, w.conf.OriginalsPath(), false)
	} else {
		// Convert MPEG-2 Transport Stream (M2TS) files to MPEG4 containers.
		if f.IsM2TS() && w.conf.SidecarWritable() {
			if mp4Name, mp4Err := fs.FileName(f.FileName(), w.conf.SidecarPath(), w.conf.OriginalsPath(), fs.ExtMp4); mp4Err != nil {
				return nil, fmt.Errorf("convert: %s in %s (remux)", mp4Err, clean.Log(f.RootRelName()))
			} else if mp4Err = ffmpeg.RemuxFile(f.FileName(), mp4Name, encode.NewRemuxOptions(conf.FFmpegBin(), fs.VideoMp4, false)); mp4Err != nil {
				return nil, fmt.Errorf("convert: %s in %s (remux)", err, clean.Log(f.RootRelName()))
			} else if mp4File, fileErr := NewMediaFile(mp4Name); mp4File == nil || fileErr != nil {
				log.Warnf("convert: %s could not be converted to mp4", logFileName)
			} else if jsonErr := mp4File.CreateExifToolJson(w); jsonErr != nil {
				log.Warnf("convert: %s in %s (create json)", jsonErr, logFileName)
			} else if jsonErr = mp4File.ReadExifToolJson(); jsonErr != nil {
				log.Warnf("convert: %s in %s (read json)", jsonErr, logFileName)
			} else if mp4File.MetaData().CodecAvc() {
				return mp4File, nil
			}
		}

		avcName = fs.VideoAvc.FindFirst(f.FileName(), []string{w.conf.SidecarPath(), fs.PPHiddenPathname}, w.conf.OriginalsPath(), false)
	}

	mediaFile, err := NewMediaFile(avcName)

	// Return the AVC-encoded video file if it already exists.
	if mediaFile == nil || err != nil {
		// Do nothing.
	} else if mediaFile.IsVideo() {
		// Return existing AVC file.
		log.Debugf("convert: %s has already been transcoded to MPEG-4 AVC", logFileName)
		return mediaFile, nil
	}

	// Check if the sidecar path is writable, otherwise no new AVC file can be created.
	if !w.conf.SidecarWritable() {
		return nil, fmt.Errorf("convert: cannot transcode %s because the sidecar path is not writable", logFileName)
	}

	// Get relative filename for logging.
	relName := f.RelName(w.conf.OriginalsPath())

	// Use .mp4 file extension for animated images and .avi for videos.
	if f.IsAnimatedImage() {
		avcName, _ = fs.FileName(f.FileName(), w.conf.SidecarPath(), w.conf.OriginalsPath(), fs.ExtMp4)
	} else {
		avcName, _ = fs.FileName(f.FileName(), w.conf.SidecarPath(), w.conf.OriginalsPath(), fs.ExtAvc)
	}

	cmd, useMutex, err := w.TranscodeToAvcCmd(f, avcName, encoder)

	// Return if an error occurred.
	if err != nil {
		log.Error(err)
		return nil, err
	}

	// Make sure only one convert command runs at a time.
	if useMutex && !noMutex {
		w.cmdMutex.Lock()
		defer w.cmdMutex.Unlock()
	}

	// Check if target file already exists.
	if fs.FileExists(avcName) {
		avcFile, avcErr := NewMediaFile(avcName)
		if avcErr != nil {
			return avcFile, avcErr
		} else if !force || !avcFile.InSidecar() {
			return avcFile, nil
		} else if err = avcFile.Remove(); err != nil {
			return avcFile, fmt.Errorf("convert: failed removing %s (%s)", clean.Log(avcFile.RootRelName()), err)
		} else {
			log.Infof("convert: replacing %s", clean.Log(avcFile.RootRelName()))
		}
	}

	// Fetch command output.
	var out bytes.Buffer
	var stderr bytes.Buffer
	cmd.Stdout = &out
	cmd.Stderr = &stderr
	cmd.Env = append(cmd.Env, []string{
		fmt.Sprintf("HOME=%s", w.conf.CmdCachePath()),
	}...)

	event.Publish("index.converting", event.Data{
		"fileType": f.FileType(),
		"fileName": relName,
		"baseName": filepath.Base(relName),
		"xmpName":  "",
	})

	log.Infof("%s: transcoding %s to %s", encoder, relName, fs.VideoAvc)

	// Log exact command for debugging in trace mode.
	log.Trace(cmd.String())

	// Transcode source media file to AVC.
	start := time.Now()
	if err = cmd.Run(); err != nil {
		if stderr.String() != "" {
			err = errors.New(stderr.String())
		}

		// Log ffmpeg output for debugging.
		if err.Error() != "" {
			log.Debug(err)
		}

		// Log filename and transcoding time.
		log.Warnf("%s: failed to transcode %s [%s]", encoder, relName, time.Since(start))

		// Remove broken video file.
		if !fs.FileExists(avcName) {
			// Do nothing.
		} else if err = os.Remove(avcName); err != nil {
			return nil, fmt.Errorf("convert: failed to remove %s (%s)", clean.Log(RootRelName(avcName)), err)
		}

		// Try again using software encoder.
		if encoder != encode.SoftwareAvc {
			return w.ToAvc(f, encode.SoftwareAvc, true, false)
		} else {
			return nil, err
		}
	}

	// Log filename and transcoding time.
	log.Infof("%s: created %s [%s]", encoder, filepath.Base(avcName), time.Since(start))

	// Return AVC media file.
	return NewMediaFile(avcName)
}

// TranscodeToAvcCmd returns the command for converting video files to MPEG-4 AVC.
func (w *Convert) TranscodeToAvcCmd(f *MediaFile, avcName string, encoder encode.Encoder) (result *exec.Cmd, useMutex bool, err error) {
	fileExt := f.Extension()
	fileName := f.FileName()

	switch {
	case fileName == "":
		return nil, false, fmt.Errorf("convert: %s video filename is empty - you may have found a bug", f.FileType())
	case !f.IsAnimated():
		return nil, false, fmt.Errorf("convert: file type %s of %s cannot be transcoded", f.FileType(), clean.Log(f.BaseName()))
	}

	// Try to transcode animated WebP images with ImageMagick.
	if w.conf.ImageMagickEnabled() && f.IsWebp() && w.imageMagickExclude.Allow(fileExt) {
		// #nosec G204 -- arguments are built from validated config and file paths.
		return exec.Command(w.conf.ImageMagickBin(), f.FileName(), avcName), false, nil
	}

	// Use FFmpeg to transcode all other media files to AVC.
	var opt encode.Options
	if opt, err = w.conf.FFmpegOptions(encoder, w.AvcBitrate(f)); err != nil {
		return nil, false, fmt.Errorf("convert: failed to transcode %s (%s)", clean.Log(f.BaseName()), err)
	} else {
		return ffmpeg.TranscodeCmd(fileName, avcName, opt)
	}
}

// AvcBitrate returns the ideal AVC encoding bitrate in megabits per second.
func (w *Convert) AvcBitrate(f *MediaFile) string {
	const defaultBitrate = "8M"

	if f == nil {
		return defaultBitrate
	}

	limit := w.conf.FFmpegBitrate()
	quality := 12

	bitrate := int(math.Ceil(float64(f.Width()*f.Height()*quality) / 1000000))

	if bitrate <= 0 {
		return defaultBitrate
	} else if bitrate > limit {
		bitrate = limit
	}

	return fmt.Sprintf("%dM", bitrate)
}
