# -*- coding: utf-8 -*-
"""
LVT4U — Flycam Photos: EXIF & XMP Reader.

Extracts GPS coordinates, altitude, azimuth, camera info, and timestamps
from geotagged photos using exifread (preferred) or Pillow (fallback).
Also parses DJI XMP metadata for FlightYawDegree.

Author: Lộc Vũ Trung (LVT) / Slow Forest
License: GPL-3.0

Based on ImportPhotos plugin (GPL-2.0+) by KIOS Research Center.
"""

import os
import sys
import datetime

# Add local libs folder to sys.path so bundled packages (like exifread) work
_plugin_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
_libs_dir = os.path.join(_plugin_dir, 'libs')
if _libs_dir not in sys.path:
    sys.path.insert(0, _libs_dir)

try:
    import defusedxml.ElementTree as ET
except ImportError:
    # Fallback: XMP data is read from local photo files only (trusted)
    import xml.etree.ElementTree as ET  # nosec B405
from typing import Optional  # noqa: E402


# ---------------------------------------------------------------------------
# DJI XMP Flight Yaw
# ---------------------------------------------------------------------------

def get_flight_yaw(photo_path: str) -> Optional[float]:
    """Extract FlightYawDegree from XMP block inside the image file.

    Returns yaw in degrees normalized to [0, 360), or None if not found.
    """
    try:
        with open(photo_path, 'rb') as f:
            content = f.read()

        start = content.find(b"<x:xmpmeta")
        end = content.find(b"</x:xmpmeta>")
        if start == -1 or end == -1:
            return None

        xmp_data = content[start:end + len(b"</x:xmpmeta>")]
        xml_str = xmp_data.decode('utf-8', errors='ignore')
        root = ET.fromstring(xml_str)  # nosec B314

        ns = {
            'drone-dji': 'http://www.dji.com/drone-dji/1.0/',
            'rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'
        }

        for desc in root.findall('.//rdf:Description', ns):
            yaw = desc.attrib.get(
                '{http://www.dji.com/drone-dji/1.0/}FlightYawDegree')
            if yaw is not None:
                try:
                    value = float(str(yaw).replace(',', '.'))
                    return value % 360
                except Exception:
                    return None
        return None
    except Exception:
        return None


# ---------------------------------------------------------------------------
# EXIF GPS Helpers
# ---------------------------------------------------------------------------

def _dms_to_decimal(dms_values, ref):
    """Convert EXIF DMS (degrees, minutes, seconds) to decimal degrees.

    Args:
        dms_values: Sequence of 3 Ratio/float values [degrees, minutes, seconds].
        ref: Reference direction ('N', 'S', 'E', 'W').

    Returns:
        float: Decimal degrees (negative for S and W).
    """
    try:
        d = float(dms_values[0])
        m = float(dms_values[1])
        s = float(dms_values[2])
        decimal = d + m / 60.0 + s / 3600.0
        if ref in ('S', 'W'):
            decimal = -decimal
        return decimal
    except Exception:
        return None


# ---------------------------------------------------------------------------
# EXIF Reading — exifread backend
# ---------------------------------------------------------------------------

def _parse_with_exifread(photo_path):
    """Read EXIF data using the exifread library.

    Returns:
        dict with keys: lat, lon, altitude, azimuth, date, time,
                       timestamp, cam_maker, cam_model, title, comment
        or None if GPS data not found.
    """
    try:
        import exifread
    except ImportError:
        return None

    try:
        with open(photo_path, 'rb') as f:
            tags = exifread.process_file(f, details=False)

        if not tags:
            return None

        # GPS coordinates
        lat_tag = tags.get('GPS GPSLatitude')
        lat_ref_tag = tags.get('GPS GPSLatitudeRef')
        lon_tag = tags.get('GPS GPSLongitude')
        lon_ref_tag = tags.get('GPS GPSLongitudeRef')

        if not all([lat_tag, lat_ref_tag, lon_tag, lon_ref_tag]):
            return None

        lat_values = lat_tag.values
        lon_values = lon_tag.values
        lat_ref = str(lat_ref_tag)
        lon_ref = str(lon_ref_tag)

        lat = _dms_to_decimal(lat_values, lat_ref)
        lon = _dms_to_decimal(lon_values, lon_ref)

        if lat is None or lon is None:
            return None

        # Altitude
        alt_tag = tags.get('GPS GPSAltitude')
        alt_ref_tag = tags.get('GPS GPSAltitudeRef')
        altitude = 0.0
        if alt_tag:
            try:
                altitude = float(alt_tag.values[0])
                if alt_ref_tag and str(alt_ref_tag) == '1':
                    altitude = -altitude
            except Exception:
                altitude = 0.0

        # Azimuth (GPS direction)
        azimuth_tag = tags.get('GPS GPSImgDirection')
        azimuth = 0.0
        if azimuth_tag:
            try:
                azimuth = float(azimuth_tag.values[0])
            except Exception:
                azimuth = 0.0

        # Try XMP yaw (DJI drones)
        xmp_yaw = get_flight_yaw(photo_path)
        if xmp_yaw is not None:
            azimuth = xmp_yaw

        # Date and Time
        date_tag = tags.get('EXIF DateTimeOriginal') or tags.get('Image DateTime')
        photo_date = ''
        photo_time = ''
        timestamp = ''
        if date_tag:
            dt_str = str(date_tag)
            try:
                dt = datetime.datetime.strptime(dt_str, '%Y:%m:%d %H:%M:%S')
                photo_date = dt.strftime('%Y-%m-%d')
                photo_time = dt.strftime('%H:%M:%S')
                timestamp = dt_str
            except Exception:
                timestamp = dt_str

        # Camera info
        cam_maker = str(tags.get('Image Make', '')).strip()
        cam_model = str(tags.get('Image Model', '')).strip()

        # User comment / title
        title = str(tags.get('Image ImageDescription', '')).strip()
        comment = str(tags.get('EXIF UserComment', '')).strip()

        return {
            'lat': lat,
            'lon': lon,
            'altitude': altitude,
            'azimuth': azimuth,
            'date': photo_date,
            'time': photo_time,
            'timestamp': timestamp,
            'cam_maker': cam_maker,
            'cam_model': cam_model,
            'title': title,
            'comment': comment,
        }
    except Exception:
        return None


# ---------------------------------------------------------------------------
# EXIF Reading — Pillow (PIL) backend
# ---------------------------------------------------------------------------

def _parse_with_pil(photo_path):
    """Read EXIF data using the Pillow (PIL) library.

    Returns:
        dict with keys: lat, lon, altitude, azimuth, date, time,
                       timestamp, cam_maker, cam_model, title, comment
        or None if GPS data not found.
    """
    try:
        from PIL import Image
        from PIL.ExifTags import TAGS, GPSTAGS
    except ImportError:
        return None

    try:
        img = Image.open(photo_path)
        exif_data = img._getexif()
        if not exif_data:
            return None

        # Build tag name dictionary
        decoded = {}
        for tag_id, value in exif_data.items():
            tag_name = TAGS.get(tag_id, tag_id)
            decoded[tag_name] = value

        gps_info = decoded.get('GPSInfo')
        if not gps_info:
            return None

        # Decode GPS tags
        gps = {}
        for key, val in gps_info.items():
            gps_tag = GPSTAGS.get(key, key)
            gps[gps_tag] = val

        if 'GPSLatitude' not in gps or 'GPSLongitude' not in gps:
            return None

        lat = _dms_to_decimal(gps['GPSLatitude'],
                              gps.get('GPSLatitudeRef', 'N'))
        lon = _dms_to_decimal(gps['GPSLongitude'],
                              gps.get('GPSLongitudeRef', 'E'))

        if lat is None or lon is None:
            return None

        # Altitude
        altitude = 0.0
        if 'GPSAltitude' in gps:
            try:
                altitude = float(gps['GPSAltitude'])
                if gps.get('GPSAltitudeRef', 0) == 1:
                    altitude = -altitude
            except Exception:
                altitude = 0.0

        # Azimuth
        azimuth = 0.0
        if 'GPSImgDirection' in gps:
            try:
                azimuth = float(gps['GPSImgDirection'])
            except Exception:
                azimuth = 0.0

        # Try XMP yaw
        xmp_yaw = get_flight_yaw(photo_path)
        if xmp_yaw is not None:
            azimuth = xmp_yaw

        # Date and Time
        dt_str = decoded.get('DateTimeOriginal', decoded.get('DateTime', ''))
        photo_date = ''
        photo_time = ''
        timestamp = ''
        if dt_str:
            try:
                dt = datetime.datetime.strptime(str(dt_str),
                                                '%Y:%m:%d %H:%M:%S')
                photo_date = dt.strftime('%Y-%m-%d')
                photo_time = dt.strftime('%H:%M:%S')
                timestamp = str(dt_str)
            except Exception:
                timestamp = str(dt_str)

        # Camera info
        cam_maker = str(decoded.get('Make', '')).strip()
        cam_model = str(decoded.get('Model', '')).strip()

        # Title / Comment
        title = str(decoded.get('ImageDescription', '')).strip()
        comment = str(decoded.get('UserComment', '')).strip()

        return {
            'lat': lat,
            'lon': lon,
            'altitude': altitude,
            'azimuth': azimuth,
            'date': photo_date,
            'time': photo_time,
            'timestamp': timestamp,
            'cam_maker': cam_maker,
            'cam_model': cam_model,
            'title': title,
            'comment': comment,
        }
    except Exception:
        return None


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------

def read_photo_exif(photo_path):
    """Read EXIF/XMP metadata from a geotagged photo.

    Tries exifread first, then falls back to Pillow.

    Args:
        photo_path: Absolute path to the JPEG/JPG file.

    Returns:
        dict with keys: lat, lon, altitude, azimuth, date, time,
                       timestamp, cam_maker, cam_model, title, comment
        or None if no GPS data found.
    """
    result = _parse_with_exifread(photo_path)
    if result is not None:
        return result
    return _parse_with_pil(photo_path)


def scan_photos_in_folder(folder_path, extensions=('.jpg', '.jpeg', '.dng', '.tif', '.tiff', '.png')):
    """Scan a folder recursively for photo files.

    Args:
        folder_path: Root directory to scan.
        extensions: Tuple of file extensions to include.

    Returns:
        list of absolute file paths.
    """
    photos = []
    for root, _dirs, files in os.walk(folder_path):
        for fname in sorted(files):
            if fname.lower().endswith(extensions):
                photos.append(os.path.join(root, fname))
    return photos
