package eu.darken.sdmse.squeezer.core.processor

import androidx.exifinterface.media.ExifInterface
import dagger.Reusable
import eu.darken.sdmse.common.debug.logging.Logging.Priority.VERBOSE
import eu.darken.sdmse.common.debug.logging.Logging.Priority.WARN
import eu.darken.sdmse.common.debug.logging.log
import eu.darken.sdmse.common.debug.logging.logTag
import eu.darken.sdmse.squeezer.core.MetadataPreservationException
import java.io.File
import javax.inject.Inject

@Reusable
class ExifPreserver @Inject constructor() {

    data class GpsCoords(
        val lat: Double,
        val lon: Double,
    )

    data class ExifData(
        val attributes: Map<String, String?>,
        val gpsCoords: GpsCoords? = null,
        val altitude: Double? = null,
    )

    /**
     * Returns `null` only when the file genuinely carries no EXIF worth preserving. A failed
     * extraction throws [MetadataPreservationException] instead — `null` would be
     * indistinguishable from "nothing to preserve" and the photo would get compressed with its
     * date/location/camera tags silently stripped (the HEIC path fails closed the same way).
     */
    fun extractExif(file: File): ExifData? {
        return try {
            val exif = ExifInterface(file)
            val attributes = PRESERVED_TAGS.associateWith { tag ->
                exif.getAttribute(tag)
            }.filterValues { it != null }

            // GPS coordinates must use getLatLong()/setLatLong() — setAttribute() for
            // individual GPS tags does not create the GPS sub-IFD on a fresh file.
            val gpsCoords = exif.latLong?.let { GpsCoords(it[0], it[1]) }
            val altitude = if (exif.getAttribute(ExifInterface.TAG_GPS_ALTITUDE) != null) {
                exif.getAltitude(Double.NaN).takeIf { !it.isNaN() }
            } else {
                null
            }

            if (attributes.isEmpty() && gpsCoords == null) {
                log(TAG, VERBOSE) { "No EXIF data found" }
                null
            } else {
                log(TAG, VERBOSE) { "Extracted ${attributes.size} EXIF attributes, gps=${gpsCoords != null}" }
                ExifData(attributes, gpsCoords, altitude)
            }
        } catch (e: Exception) {
            log(TAG, WARN) { "Failed to extract EXIF: ${e.message}" }
            throw MetadataPreservationException(
                "EXIF metadata of ${file.path} could not be read (${e.message}); " +
                    "aborting to avoid silently stripping date/location/camera tags",
            )
        }
    }

    fun applyExif(outputPath: String, exifData: ExifData?) {
        if (exifData == null) return

        val exif = ExifInterface(outputPath)

        exifData.attributes.forEach { (tag, value) ->
            if (value != null) {
                exif.setAttribute(tag, value)
            }
        }

        // Use setLatLong()/setAltitude() to properly create the GPS sub-IFD.
        // setAttribute() for individual GPS tags doesn't create the IFD on a fresh file.
        exifData.gpsCoords?.let { exif.setLatLong(it.lat, it.lon) }
        exifData.altitude?.let { exif.setAltitude(it) }

        exif.saveAttributes()
        log(TAG, VERBOSE) { "Applied ${exifData.attributes.size} EXIF attributes, gps=${exifData.gpsCoords != null}" }
    }

    fun hasCompressionMarker(file: File): Boolean {
        return try {
            val exif = ExifInterface(file)
            val userComment = exif.getAttribute(ExifInterface.TAG_USER_COMMENT)
            userComment?.contains(MARKER_FULL) == true
        } catch (e: Exception) {
            log(TAG, WARN) { "Failed to read EXIF marker: ${e.message}" }
            false
        }
    }

    fun writeCompressionMarker(filePath: String) {
        try {
            val exif = ExifInterface(filePath)
            val existingComment = exif.getAttribute(ExifInterface.TAG_USER_COMMENT)
            val newComment = if (existingComment.isNullOrBlank()) {
                MARKER_FULL
            } else {
                "$existingComment $MARKER_FULL"
            }
            exif.setAttribute(ExifInterface.TAG_USER_COMMENT, newComment)
            exif.saveAttributes()
            log(TAG, VERBOSE) { "Written compression marker to $filePath" }
        } catch (e: Exception) {
            log(TAG, WARN) { "Failed to write EXIF marker: ${e.message}" }
        }
    }

    companion object {
        private const val MARKER_PREFIX = "SDMSE"
        private const val MARKER_VERSION = 1
        private const val MARKER_FULL = "$MARKER_PREFIX:v$MARKER_VERSION"
        private val TAG = logTag("Squeezer", "ExifPreserver")

        /**
         * All known EXIF tags to preserve during image compression.
         * We preserve everything to avoid losing any user data.
         */
        private val PRESERVED_TAGS = listOf(
            ExifInterface.TAG_F_NUMBER,
            ExifInterface.TAG_APERTURE_VALUE,
            ExifInterface.TAG_ARTIST,
            ExifInterface.TAG_BITS_PER_SAMPLE,
            ExifInterface.TAG_BRIGHTNESS_VALUE,
            ExifInterface.TAG_CFA_PATTERN,
            ExifInterface.TAG_COLOR_SPACE,
            ExifInterface.TAG_COMPONENTS_CONFIGURATION,
            ExifInterface.TAG_COMPRESSED_BITS_PER_PIXEL,
            ExifInterface.TAG_COMPRESSION,
            ExifInterface.TAG_CONTRAST,
            ExifInterface.TAG_COPYRIGHT,
            ExifInterface.TAG_CUSTOM_RENDERED,
            ExifInterface.TAG_DATETIME,
            ExifInterface.TAG_DATETIME_DIGITIZED,
            ExifInterface.TAG_DATETIME_ORIGINAL,
            ExifInterface.TAG_DEFAULT_CROP_SIZE,
            ExifInterface.TAG_DEVICE_SETTING_DESCRIPTION,
            ExifInterface.TAG_DIGITAL_ZOOM_RATIO,
            ExifInterface.TAG_DNG_VERSION,
            ExifInterface.TAG_EXIF_VERSION,
            ExifInterface.TAG_EXPOSURE_BIAS_VALUE,
            ExifInterface.TAG_EXPOSURE_INDEX,
            ExifInterface.TAG_EXPOSURE_MODE,
            ExifInterface.TAG_EXPOSURE_PROGRAM,
            ExifInterface.TAG_EXPOSURE_TIME,
            ExifInterface.TAG_FILE_SOURCE,
            ExifInterface.TAG_FLASH,
            ExifInterface.TAG_FLASHPIX_VERSION,
            ExifInterface.TAG_FLASH_ENERGY,
            ExifInterface.TAG_FOCAL_LENGTH,
            ExifInterface.TAG_FOCAL_LENGTH_IN_35MM_FILM,
            ExifInterface.TAG_FOCAL_PLANE_RESOLUTION_UNIT,
            ExifInterface.TAG_FOCAL_PLANE_X_RESOLUTION,
            ExifInterface.TAG_FOCAL_PLANE_Y_RESOLUTION,
            ExifInterface.TAG_GAIN_CONTROL,
            ExifInterface.TAG_GPS_ALTITUDE,
            ExifInterface.TAG_GPS_ALTITUDE_REF,
            ExifInterface.TAG_GPS_AREA_INFORMATION,
            ExifInterface.TAG_GPS_DATESTAMP,
            ExifInterface.TAG_GPS_DEST_BEARING,
            ExifInterface.TAG_GPS_DEST_BEARING_REF,
            ExifInterface.TAG_GPS_DEST_DISTANCE,
            ExifInterface.TAG_GPS_DEST_DISTANCE_REF,
            ExifInterface.TAG_GPS_DEST_LATITUDE,
            ExifInterface.TAG_GPS_DEST_LATITUDE_REF,
            ExifInterface.TAG_GPS_DEST_LONGITUDE,
            ExifInterface.TAG_GPS_DEST_LONGITUDE_REF,
            ExifInterface.TAG_GPS_DIFFERENTIAL,
            ExifInterface.TAG_GPS_DOP,
            ExifInterface.TAG_GPS_IMG_DIRECTION,
            ExifInterface.TAG_GPS_IMG_DIRECTION_REF,
            ExifInterface.TAG_GPS_LATITUDE,
            ExifInterface.TAG_GPS_LATITUDE_REF,
            ExifInterface.TAG_GPS_LONGITUDE,
            ExifInterface.TAG_GPS_LONGITUDE_REF,
            ExifInterface.TAG_GPS_MAP_DATUM,
            ExifInterface.TAG_GPS_MEASURE_MODE,
            ExifInterface.TAG_GPS_PROCESSING_METHOD,
            ExifInterface.TAG_GPS_SATELLITES,
            ExifInterface.TAG_GPS_SPEED,
            ExifInterface.TAG_GPS_SPEED_REF,
            ExifInterface.TAG_GPS_STATUS,
            ExifInterface.TAG_GPS_TIMESTAMP,
            ExifInterface.TAG_GPS_TRACK,
            ExifInterface.TAG_GPS_TRACK_REF,
            ExifInterface.TAG_GPS_VERSION_ID,
            ExifInterface.TAG_IMAGE_DESCRIPTION,
            ExifInterface.TAG_IMAGE_LENGTH,
            ExifInterface.TAG_IMAGE_UNIQUE_ID,
            ExifInterface.TAG_IMAGE_WIDTH,
            ExifInterface.TAG_INTEROPERABILITY_INDEX,
            ExifInterface.TAG_ISO_SPEED,
            ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT,
            ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH,
            ExifInterface.TAG_LIGHT_SOURCE,
            ExifInterface.TAG_MAKE,
            ExifInterface.TAG_MAKER_NOTE,
            ExifInterface.TAG_MAX_APERTURE_VALUE,
            ExifInterface.TAG_METERING_MODE,
            ExifInterface.TAG_MODEL,
            ExifInterface.TAG_NEW_SUBFILE_TYPE,
            ExifInterface.TAG_OECF,
            ExifInterface.TAG_ORF_ASPECT_FRAME,
            ExifInterface.TAG_ORF_PREVIEW_IMAGE_LENGTH,
            ExifInterface.TAG_ORF_PREVIEW_IMAGE_START,
            ExifInterface.TAG_ORF_THUMBNAIL_IMAGE,
            ExifInterface.TAG_ORIENTATION,
            ExifInterface.TAG_PHOTOMETRIC_INTERPRETATION,
            ExifInterface.TAG_PIXEL_X_DIMENSION,
            ExifInterface.TAG_PIXEL_Y_DIMENSION,
            ExifInterface.TAG_PLANAR_CONFIGURATION,
            ExifInterface.TAG_PRIMARY_CHROMATICITIES,
            ExifInterface.TAG_REFERENCE_BLACK_WHITE,
            ExifInterface.TAG_RELATED_SOUND_FILE,
            ExifInterface.TAG_RESOLUTION_UNIT,
            ExifInterface.TAG_ROWS_PER_STRIP,
            ExifInterface.TAG_RW2_ISO,
            ExifInterface.TAG_RW2_JPG_FROM_RAW,
            ExifInterface.TAG_RW2_SENSOR_BOTTOM_BORDER,
            ExifInterface.TAG_RW2_SENSOR_LEFT_BORDER,
            ExifInterface.TAG_RW2_SENSOR_RIGHT_BORDER,
            ExifInterface.TAG_RW2_SENSOR_TOP_BORDER,
            ExifInterface.TAG_SAMPLES_PER_PIXEL,
            ExifInterface.TAG_SATURATION,
            ExifInterface.TAG_SCENE_CAPTURE_TYPE,
            ExifInterface.TAG_SCENE_TYPE,
            ExifInterface.TAG_SENSING_METHOD,
            ExifInterface.TAG_SHARPNESS,
            ExifInterface.TAG_SHUTTER_SPEED_VALUE,
            ExifInterface.TAG_SOFTWARE,
            ExifInterface.TAG_SPATIAL_FREQUENCY_RESPONSE,
            ExifInterface.TAG_SPECTRAL_SENSITIVITY,
            ExifInterface.TAG_STRIP_BYTE_COUNTS,
            ExifInterface.TAG_STRIP_OFFSETS,
            ExifInterface.TAG_SUBFILE_TYPE,
            ExifInterface.TAG_SUBJECT_AREA,
            ExifInterface.TAG_SUBJECT_DISTANCE,
            ExifInterface.TAG_SUBJECT_DISTANCE_RANGE,
            ExifInterface.TAG_SUBJECT_LOCATION,
            ExifInterface.TAG_SUBSEC_TIME,
            ExifInterface.TAG_SUBSEC_TIME_DIGITIZED,
            ExifInterface.TAG_SUBSEC_TIME_ORIGINAL,
            ExifInterface.TAG_THUMBNAIL_IMAGE_LENGTH,
            ExifInterface.TAG_THUMBNAIL_IMAGE_WIDTH,
            ExifInterface.TAG_TRANSFER_FUNCTION,
            ExifInterface.TAG_USER_COMMENT,
            ExifInterface.TAG_WHITE_BALANCE,
            ExifInterface.TAG_WHITE_POINT,
            ExifInterface.TAG_X_RESOLUTION,
            ExifInterface.TAG_Y_CB_CR_COEFFICIENTS,
            ExifInterface.TAG_Y_CB_CR_POSITIONING,
            ExifInterface.TAG_Y_CB_CR_SUB_SAMPLING,
            ExifInterface.TAG_Y_RESOLUTION,
        )
    }
}
