package meta

import (
	"fmt"
	"path/filepath"
	"runtime/debug"
	"strings"

	"github.com/dsoprea/go-exif/v3"
	heicexif "github.com/dsoprea/go-heic-exif-extractor/v2"
	jpegstructure "github.com/dsoprea/go-jpeg-image-structure/v2"
	pngstructure "github.com/dsoprea/go-png-image-structure/v2"
	tiffstructure "github.com/dsoprea/go-tiff-image-structure/v2"

	"github.com/photoprism/photoprism/pkg/clean"
	"github.com/photoprism/photoprism/pkg/fs"
)

// RawExif extracts the raw EXIF block from the given media file, optionally falling back to a
// brute-force search when native parsers fail.
func RawExif(fileName string, fileFormat fs.Type, bruteForce bool) (rawExif []byte, err error) {
	defer func() {
		if e := recover(); e != nil {
			err = fmt.Errorf("%s in %s (raw exif panic)\nstack: %s", e, clean.Log(filepath.Base(fileName)), debug.Stack())
		}
	}()

	// Extract raw Exif block.
	var parsed bool

	// Sanitized and shortened file name for logs.
	logName := clean.Log(filepath.Base(fileName))

	// Try Exif parser for specific media file format first.
	switch fileFormat {
	case fs.ImageJpeg:
		jpegMp := jpegstructure.NewJpegMediaParser()

		sl, err := jpegMp.ParseFile(fileName)

		if err != nil {
			log.Infof("metadata: %s while parsing jpeg file %s", err, logName)
		} else {
			_, rawExif, err = sl.Exif()

			if err != nil {
				switch {
				case !bruteForce || strings.HasPrefix(err.Error(), "no exif header"):
					return rawExif, fmt.Errorf("found no exif header")
				case strings.HasPrefix(err.Error(), "no exif data"):
					log.Debugf("metadata: failed parsing %s, starting brute-force search (parse jpeg)", logName)
				default:
					log.Infof("metadata: %s in %s, starting brute-force search (parse jpeg)", err, logName)
				}
			} else {
				parsed = true
			}
		}
	case fs.ImagePng:
		pngMp := pngstructure.NewPngMediaParser()

		cs, err := pngMp.ParseFile(fileName)

		if err != nil {
			return rawExif, fmt.Errorf("%s while parsing png file", err)
		} else {
			_, rawExif, err = cs.Exif()

			if err != nil {
				if err.Error() == "file does not have EXIF" || strings.HasPrefix(err.Error(), "no exif data") {
					return rawExif, fmt.Errorf("found no exif header")
				} else {
					log.Infof("metadata: %s in %s (parse png)", err, logName)
				}
			} else {
				parsed = true
			}
		}
	case fs.ImageHeif, fs.ImageHeic, fs.ImageHeicS, fs.ImageAvif, fs.ImageAvifS:
		heicMp := heicexif.NewHeicExifMediaParser()

		cs, err := heicMp.ParseFile(fileName)

		if err != nil {
			return rawExif, fmt.Errorf("%s while parsing heic file", err)
		} else {
			_, rawExif, err = cs.Exif()

			if err != nil {
				if err.Error() == "file does not have EXIF" || strings.HasPrefix(err.Error(), "no exif data") {
					return rawExif, fmt.Errorf("found no exif header")
				} else {
					log.Infof("metadata: %s in %s (parse heic)", err, logName)
				}
			} else {
				parsed = true
			}
		}
	case fs.ImageTiff:
		tiffMp := tiffstructure.NewTiffMediaParser()

		cs, err := tiffMp.ParseFile(fileName)

		if err != nil {
			return rawExif, fmt.Errorf("%s while parsing tiff file", err)
		} else {
			_, rawExif, err = cs.Exif()

			if err != nil {
				if err.Error() == "file does not have EXIF" || strings.HasPrefix(err.Error(), "no exif data") {
					return rawExif, fmt.Errorf("found no exif header")
				} else {
					log.Infof("metadata: %s in %s (parse tiff)", err, logName)
				}
			} else {
				parsed = true
			}
		}
	default:
		log.Debugf("metadata: no native file format support for %s, performing brute-force exif search", logName)
		bruteForce = true
	}

	// Start brute-force search for Exif data?
	if !parsed && bruteForce {
		rawExif, err = exif.SearchFileAndExtractExif(fileName)

		if err != nil {
			return rawExif, fmt.Errorf("found no exif data")
		}
	}

	return rawExif, nil
}
