# -*- coding: utf-8 -*-

"""
Reamb_ImportPhotos.py
***************************************************************************
*                                                                         *
*   This program is free software; you can redistribute it and/or modify  *
*   it under the terms of the GNU General Public License as published by  *
*   the Free Software Foundation; either version 2 of the License, or     *
*   (at your option) any later version.                                   *
*                                                                         *
***************************************************************************
"""
__author__ = 'Leandro França'
__date__ = '2021-02-16'
__copyright__ = '(C) 2021, Leandro França'

from qgis.PyQt.QtCore import QMetaType
from qgis.core import (QgsApplication,
                       QgsGeometry,
                       QgsPoint,
                       QgsWkbTypes,
                       QgsFeature,
                       QgsField,
                       QgsFields,
                       QgsAction,
                       QgsCoordinateReferenceSystem,
                       QgsProcessingParameterFile,
                       QgsProcessingParameterBoolean,
                       QgsProcessingParameterEnum,
                       QgsFeatureSink,
                       QgsProcessingUtils,
                       QgsProcessingException,
                       QgsProcessingAlgorithm,
                       QgsProcessingParameterFeatureSink)

import datetime
import shutil
from lftools.geocapt.imgs import Imgs
from lftools.geocapt.topogeo import azimute as CalAZ
from lftools.geocapt.cartography import simbologiaPontos3D
from lftools.translations.translate import translate
import os, re
import processing
from math import pi
from qgis.PyQt.QtGui import QIcon


class ImportPhotos(QgsProcessingAlgorithm):

    LOC = QgsApplication.locale()[:2]

    def tr(self, *string):
        return translate(string, self.LOC)

    def createInstance(self):
        return ImportPhotos()

    def name(self):
        return 'importphotos'

    def displayName(self):
        return self.tr('Photos with geotag', 'Fotos com geotag')

    def group(self):
        return self.tr('Reambulation', 'Reambulação')

    def groupId(self):
        return 'reambulation'

    def tags(self):
        return 'GeoOne,import,photo,reambulation,geotag,geophoto,reambulação,fotografia,photography,drone,DJI'.split(',')

    def icon(self):
        return QIcon(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'images/reamb_camera.png'))

    txt_en = 'Imports photos with geotag to a Point Layer.'
    txt_pt = 'Importa fotos com geotag para uma camada de pontos.'
    figure = 'images/tutorial/reamb_geotag.jpg'

    def shortHelpString(self):
        social_BW = Imgs().social_BW
        footer = '''<div align="center">
                      <img src="'''+ os.path.join(os.path.dirname(os.path.dirname(__file__)), self.figure) +'''">
                      </div>
                      <div align="right">
                      <p align="right">
                      <b>'''+self.tr('Author: Leandro Franca', 'Autor: Leandro França')+'''</b>
                      </p>'''+ social_BW + '''</div>
                    </div>'''
        return self.tr(self.txt_en, self.txt_pt) + footer

    FOLDER = 'FOLDER'
    NONGEO = 'NONGEO'
    OUTPUT = 'OUTPUT'
    SUBFOLDER = 'SUBFOLDER'
    AZIMUTH = 'AZIMUTH'
    YPR = 'YPR'
    STYLE = 'STYLE'

    def initAlgorithm(self, config=None):

        self.addParameter(
            QgsProcessingParameterFile(
                self.FOLDER,
                self.tr('Folder with geotagged photos', 'Pasta de fotos com Geotag'),
                behavior=QgsProcessingParameterFile.Folder,
                defaultValue=None
            )
        )

        self.addParameter(
            QgsProcessingParameterBoolean(
                self.SUBFOLDER,
                self.tr('Check subfolders', 'Verificar sub-pastas'),
                defaultValue = False
            )
        )

        STYLES = [self.tr('Simple Camera', 'Camera simples'),
                  self.tr('Drone'),
                  self.tr('VR Photo 360°', 'RV Foto 360°') ]

        self.addParameter(
            QgsProcessingParameterEnum(
                self.STYLE,
                self.tr('Layer Style', 'Estilo da camada'),
				options = STYLES,
                defaultValue= 0
            )
        )

        self.addParameter(
            QgsProcessingParameterBoolean(
                self.AZIMUTH,
                self.tr('Estimate azimuth', 'Estimar azimute'),
                defaultValue = False
            )
        )

        self.addParameter(
            QgsProcessingParameterBoolean(
                self.YPR,
                self.tr('Yaw, pitch, roll'),
                defaultValue = False
            )
        )

        self.addParameter(
            QgsProcessingParameterFile(
                self.NONGEO,
                self.tr('Folder to copy the photos without geotag', 'Pasta para copiar as fotos sem geotag'),
                behavior=QgsProcessingParameterFile.Folder,
                defaultValue=None,
                optional = True
            )
        )


        self.addParameter(
            QgsProcessingParameterFeatureSink(
                self.OUTPUT,
                self.tr('Geolocated photos', 'Fotos Geolocalizadas')
            )
        )


    def processAlgorithm(self, parameters, context, feedback):

        from lftools.dependencies import ensure_pillow
        Image = ensure_pillow(feedback)
        if Image is None:
            raise QgsProcessingException(
                "The Pillow library (PIL) is required for this tool and could not be loaded automatically."
            )

        from PIL import TiffTags, ExifTags
        from PIL.TiffTags import TAGS
        from PIL.TiffImagePlugin import ImageFileDirectory_v2
        ImageFileDirectory_v2._load_dispatch[13] = ImageFileDirectory_v2._load_dispatch[TiffTags.LONG]

        pasta = self.parameterAsFile(
            parameters,
            self.FOLDER,
            context
        )
        if not pasta:
            raise QgsProcessingException(self.invalidSourceError(parameters, self.FOLDER))

        subpasta = self.parameterAsBool(
            parameters,
            self.SUBFOLDER,
            context
        )

        CalcAz = self.parameterAsBool(
            parameters,
            self.AZIMUTH,
            context
        )
        if CalcAz:
            Atributos = []

        YPR = self.parameterAsBool(
            parameters,
            self.YPR,
            context
        )

        fotos_nao_geo = self.parameterAsFile(
            parameters,
            self.NONGEO,
            context
        )

        feedback.pushInfo(self.tr('Checking files in the folder...', 'Checando arquivos na pasta...'))
        lista = []
        if subpasta:
            for root, dirs, files in os.walk(pasta, topdown=True):
                for name in files:
                    if (name).lower().endswith(('.jpg', '.jpeg', '.tif', '.tiff', '.dng')):
                        lista += [os.path.join(root, name)]
        else:
            for item in os.listdir(pasta):
                if (item).lower().endswith(('.jpg', '.jpeg', '.tif', '.tiff', '.dng')):
                    lista += [os.path.join(pasta, item)]

        tam = len(lista)
        copy_ngeo = False
        if os.path.isdir(fotos_nao_geo):
            copy_ngeo = True

        # Funcao para transformar os dados do EXIF em coordenadas em graus decimais
        def coordenadas(exif):
            try:
                ref_lat = exif['GPSInfo'][1][0]
                ref_lon = exif['GPSInfo'][3][0]
                sinal_lat, sinal_lon = 0, 0
                if ref_lat == 'S':
                    sinal_lat = -1
                elif ref_lat == 'N':
                    sinal_lat = 1
                if ref_lon == 'W':
                    sinal_lon = -1
                elif ref_lon == 'E':
                    sinal_lon = 1

                try:
                    grausLat,grausLon = exif['GPSInfo'][2][0][0], exif['GPSInfo'][4][0][0]
                    minLat, minLon = exif['GPSInfo'][2][1][0], exif['GPSInfo'][4][1][0]
                    segLat = exif['GPSInfo'][2][2][0]/float(exif['GPSInfo'][2][2][1])
                    segLon = exif['GPSInfo'][4][2][0]/float(exif['GPSInfo'][4][2][1])
                except:
                    grausLat,grausLon = exif['GPSInfo'][2][0], exif['GPSInfo'][4][0]
                    minLat, minLon = exif['GPSInfo'][2][1], exif['GPSInfo'][4][1]
                    segLat = exif['GPSInfo'][2][2]
                    segLon = exif['GPSInfo'][4][2]
                if sinal_lat!=0 and sinal_lon!=0:
                    lat = sinal_lat*(float(grausLat)+minLat/60.0+segLat/3600.0)
                    lon = sinal_lon*(float(grausLon)+minLon/60.0+segLon/3600.0)
                return lat, lon
            except:
                return 0,0

        # Funcao para pegar Azimute
        def azimute(exif):
            Az = exif['GPSInfo'][17]
            if isinstance(Az, tuple):
                Az = Az[0]/float(Az[1])
                return Az
            else:
                return Az

        # Funcao para gerar o padrao data-hora
        def data_hora(texto):
            data_hora = texto.replace(' ',':').replace('-',':')
            data_hora = data_hora.split(':')
            ano = int(data_hora[0])
            mes = int(data_hora[1])
            dia = int(data_hora[2])
            hora = int(data_hora[3])
            minuto = int(data_hora[4])
            segundo = int(data_hora[5])
            data_hora = unicode(datetime.datetime(ano, mes, dia, hora, minuto, segundo))
            return data_hora
        
        # Mensagem de erro
        def erro_msg(arquivo):
            return self.tr('The file "{}" has no geotag!'.format(arquivo), 'A imagem "{}" não possui geotag!'.format(arquivo)) 

        # Criando Output
        crs = QgsCoordinateReferenceSystem('EPSG:4326')
        fields = QgsFields()
        fields.append(QgsField(self.tr('name'), QMetaType.Type.QString))
        fields.append(QgsField(self.tr('longitude'), QMetaType.Type.Double))
        fields.append(QgsField(self.tr('latitude'), QMetaType.Type.Double))
        fields.append(QgsField(self.tr('altitude'), QMetaType.Type.Double))
        fields.append(QgsField(self.tr('azimuth'), QMetaType.Type.Int))
        fields.append(QgsField(self.tr('date_time'), QMetaType.Type.QString))
        fields.append(QgsField(self.tr('path'), QMetaType.Type.QString))
        fields.append(QgsField(self.tr('make','fabricante'), QMetaType.Type.QString))
        fields.append(QgsField(self.tr('model','modelo'), QMetaType.Type.QString))
        if YPR:
            fields.append(QgsField(self.tr('FlightYaw'), QMetaType.Type.Double))
            fields.append(QgsField(self.tr('FlightPitch'), QMetaType.Type.Double))
            fields.append(QgsField(self.tr('FlightRoll'), QMetaType.Type.Double))
            fields.append(QgsField(self.tr('GimbalYaw'), QMetaType.Type.Double))
            fields.append(QgsField(self.tr('GimbalPitch'), QMetaType.Type.Double))
            fields.append(QgsField(self.tr('GimbalRoll'), QMetaType.Type.Double))

        (sink, dest_id) = self.parameterAsSink(
            parameters,
            self.OUTPUT,
            context,
            fields,
            QgsWkbTypes.PointZ,
            crs
        )
        if sink is None:
            raise QgsProcessingException(self.invalidSinkError(parameters, self.OUTPUT))

        Percent = 100.0/tam if tam!=0 else 0

        for index, filepath in enumerate(lista):
            
            if (filepath).lower().endswith(('.jpg', '.jpeg')):
                caminho, arquivo = os.path.split(filepath)
                
                try:
                    img = Image.open(os.path.join(caminho,arquivo))
                    if img._getexif():
                        exif = {
                            ExifTags.TAGS[k]: v
                            for k, v in img._getexif().items()
                            if k in ExifTags.TAGS
                        }
                    else:
                        exif = {}
                    lon, lat = 0, 0
                    Az = None
                    date_time = None
                    altitude = None
                    img.close()
                except:
                    lon = 0
                    exif = {}
                if 'GPSInfo' in exif:
                    lat, lon = coordenadas(exif)
                    if lat != 0:
                        if 17 in exif['GPSInfo']:
                            Az = float(azimute(exif))
                        if 6 in exif['GPSInfo']:
                            try:
                                altitude = float(exif['GPSInfo'][6][0])/exif['GPSInfo'][6][1]
                            except:
                                altitude = float(exif['GPSInfo'][6])
                if 'DateTimeOriginal' in exif:
                    date_time = data_hora(exif['DateTimeOriginal'])
                elif 'DateTime' in exif:
                    date_time = data_hora(exif['DateTime'])

                if 'Make' in exif:
                    fabricante = str(exif['Make'].replace('\x00', ''))
                else:
                    fabricante = ''

                if 'Model' in exif:
                    modelo = str(exif['Model'].replace('\x00', ''))
                else:
                    modelo = ''

                if lon != 0:
                    if YPR:
                        FlightYaw, FlightPitch, FlightRoll, GimbalYaw, GimbalPitch, GimbalRoll = self.extract_dji_orientation(filepath)
                    if not CalcAz:
                        feature = QgsFeature(fields)
                        feature.setGeometry(QgsGeometry(QgsPoint(lon, lat, altitude if altitude != None else 0)))
                        att = [arquivo, lon, lat, altitude, Az, date_time, filepath, fabricante, modelo]
                        if YPR:
                            att[4] = FlightYaw
                            att += [FlightYaw, FlightPitch, FlightRoll, GimbalYaw, GimbalPitch, GimbalRoll]
                        feature.setAttributes(att)
                        sink.addFeature(feature, QgsFeatureSink.FastInsert)
                    else:
                        if YPR:
                            Atributos += [[arquivo, lon, lat, altitude, Az, date_time, filepath, fabricante, modelo, FlightYaw, FlightPitch, FlightRoll, GimbalYaw, GimbalPitch, GimbalRoll]]
                        else:
                            Atributos += [[arquivo, lon, lat, altitude, Az, date_time, filepath, fabricante, modelo]]
                else:
                    feedback.reportError(erro_msg(arquivo))
                    if copy_ngeo:
                        shutil.copy2(os.path.join(pasta, arquivo), os.path.join(fotos_nao_geo, arquivo))

            elif (filepath).lower().endswith(('.tif', '.tiff', '.dng')):
                caminho, arquivo = os.path.split(filepath)
                img = Image.open(os.path.join(caminho,arquivo))

                try:
                    meta_dict = {TAGS[key] : img.tag[key] for key in img.tag_v2}
                except:
                    exif = img.getexif()
                    meta_dict = {}
                    for tag_id, value in exif.items():
                        tag_name = TAGS.get(tag_id)
                        meta_dict[tag_name] = value

                gps_offset = img.tag_v2.get(0x8825)
                tags = {}
                if gps_offset:
                    ifh = b"II\x2A\x00\x08\x00\x00\x00" if img.tag_v2._endian == "<" else "MM\x00\x2A\x00\x00\x00\x08"
                    info = ImageFileDirectory_v2(ifh)
                    img.fp.seek(gps_offset)
                    info.load(img.fp)
                    gps_keys = ['GPSVersionID','GPSLatitudeRef','GPSLatitude','GPSLongitudeRef','GPSLongitude','GPSAltitudeRef','GPSAltitude','GPSTimeStamp','GPSSatellites','GPSStatus','GPSMeasureMode','GPSDOP','GPSSpeedRef','GPSSpeed','GPSTrackRef','GPSTrack','GPSImgDirectionRef','GPSImgDirection','GPSMapDatum','GPSDestLatitudeRef','GPSDestLatitude','GPSDestLongitudeRef','GPSDestLongitude','GPSDestBearingRef','GPSDestBearing','GPSDestDistanceRef','GPSDestDistance','GPSProcessingMethod','GPSAreaInformation','GPSDateStamp','GPSDifferential']
                    for k, v in info.items():
                        tags[gps_keys[k]] = str(v)
                

                lon, lat = 0, 0
                Az = None
                date_time = None
                altitude = None

                if 'GPSLatitudeRef' in tags:
                    lat_ref = str(tags['GPSLatitudeRef'])
                    lat = eval(str(tags['GPSLatitude']))
                    lat = (-1 if lat_ref.upper() == 'S' else 1)*(lat[0] + lat[1]/60 + lat[2]/3600)
                    lon_ref = str(tags['GPSLongitudeRef'])
                    lon = eval(str(tags['GPSLongitude']))
                    lon = (-1 if lon_ref.upper() == 'W' else 1)*(lon[0] + lon[1]/60 + lon[2]/3600)
                    alt_ref = str(tags['GPSAltitudeRef'])
                    altitude = eval(str(tags['GPSAltitude']))
                    try:
                        date_time = data_hora(meta_dict['DateTime'][0])
                    except:
                        date_time = data_hora(meta_dict['DateTime'])
                    if 'Make' in meta_dict:
                        if isinstance(meta_dict['Make'], tuple):
                            fabricante = str(meta_dict['Make'][0])
                        else:
                            fabricante = str(meta_dict['Make'])
                    else:
                        fabricante = ''

                    if 'Model' in meta_dict:
                        if isinstance(meta_dict['Model'], tuple):
                            modelo = str(meta_dict['Model'][0])
                        else:
                            modelo = str(meta_dict['Model'])
                    else:
                        modelo = ''

                    if lon != 0:
                        if YPR:
                            FlightYaw, FlightPitch, FlightRoll, GimbalYaw, GimbalPitch, GimbalRoll = self.extract_dji_orientation(filepath)
                        if not CalcAz:
                            feature = QgsFeature(fields)
                            feature.setGeometry(QgsGeometry(QgsPoint(lon, lat, altitude if altitude != None else 0)))
                            att = [arquivo, lon, lat, altitude, Az, date_time, filepath, fabricante, modelo]
                            if YPR:
                                att += [FlightYaw, FlightPitch, FlightRoll, GimbalYaw, GimbalPitch, GimbalRoll]
                            feature.setAttributes(att)
                            sink.addFeature(feature, QgsFeatureSink.FastInsert)
                        else:
                            if YPR:
                                Atributos += [[arquivo, lon, lat, altitude, Az, date_time, filepath, fabricante, modelo, FlightYaw, FlightPitch, FlightRoll, GimbalYaw, GimbalPitch, GimbalRoll]]
                            else:
                                Atributos += [[arquivo, lon, lat, altitude, Az, date_time, filepath, fabricante, modelo]]
                    else:
                        feedback.reportError(erro_msg(arquivo))
                else:
                    feedback.reportError(erro_msg(arquivo))
                    if copy_ngeo:
                        shutil.copy2(os.path.join(pasta, arquivo), os.path.join(fotos_nao_geo, arquivo))
                img.close()
            if feedback.isCanceled():
                break
            feedback.setProgress(int((index+1) * Percent))


        if CalcAz and len(Atributos) > 0:
            # Calcular azimutes
            for k in range(len(Atributos)-1):
                pntA = QgsPoint( float(Atributos[k][1]),  float(Atributos[k][2]))
                pntB = QgsPoint( float(Atributos[k+1][1]),float(Atributos[k+1][2]))
                Az = int(180*CalAZ(pntA, pntB)[0]/pi)
                Atributos[k][4] = Az
            Atributos[-1][4] = Az

            # Criar feições
            for att in Atributos:
                feature = QgsFeature(fields)
                lon, lat = att[1], att[2]
                h = att[3] if att[3] != None else 0
                feature.setGeometry(QgsGeometry(QgsPoint(float(lon), float(lat), float(h))))
                feature.setAttributes(att)
                sink.addFeature(feature, QgsFeatureSink.FastInsert)

        feedback.pushInfo(self.tr('Operation completed successfully!', 'Operação finalizada com sucesso!'))
        feedback.pushInfo(self.tr('Leandro Franca - Cartographic Engineer', 'Leandro França - Eng Cart'))

        estilo = self.parameterAsEnum(
            parameters,
            self.STYLE,
            context
        )
        self.ESTILO = [4,1,2][estilo]  # 0:camera simples (4), 1:drone (1), 2:camera 360 (2)
        self.SAIDA = dest_id
        self.pasta = pasta
        return {self.OUTPUT: dest_id}
    
    
    def extract_dji_orientation(self, image_path):
        """
        Extrai yaw, pitch e roll de imagens DJI (JPEG, DNG, etc.) analisando o bloco XMP embutido.
        """
        try:
            # Lê o conteúdo binário e tenta localizar o bloco XMP
            with open(image_path, "rb") as fb:
                data = fb.read()
            m = re.search(br"<x:xmpmeta[\s\S]*?</x:xmpmeta>", data, re.IGNORECASE)
            if not m:
                m = re.search(br"<xmpmeta[\s\S]*?</xmpmeta>", data, re.IGNORECASE)
            if not m:
                return None, None, None, None, None, None
            xmp = m.group(0).decode("utf-8", errors="ignore")
            # print(xmp)
            def find_attr_or_elem(names):
                """Busca valor numérico para qualquer nome de tag (atributo ou elemento)."""
                for name in names:
                    # Tenta achar como atributo
                    rx_attr = re.compile(rf'{re.escape(name)}\s*=\s*"([\-+]?\d+(?:\.\d+)?)"', re.IGNORECASE)
                    m = rx_attr.search(xmp)
                    if m:
                        return float(m.group(1))
                    # Tenta achar como elemento
                    rx_elem = re.compile(
                        rf'<[^:>]*:{re.escape(name)}\s*>([\-+]?\d+(?:\.\d+)?)\s*</[^:>]*:{re.escape(name)}\s*>',
                        re.IGNORECASE
                    )
                    m = rx_elem.search(xmp)
                    if m:
                        return float(m.group(1))
                return None

            FlightYawDegree = find_attr_or_elem(["FlightYawDegree"])
            FlightPitchDegree = find_attr_or_elem(["FlightPitchDegree"])
            FlightRollDegree = find_attr_or_elem(["FlightRollDegree"])
            
            GimbalYawDegree = find_attr_or_elem(["GimbalYawDegree"]) # "PoseYawDegrees"
            GimbalPitchDegree = find_attr_or_elem(["GimbalPitchDegree"]) # "PosePitchDegrees", 
            GimbalRollDegree = find_attr_or_elem(["GimbalRollDegree"]) # "PoseRollDegrees"

            return FlightYawDegree, FlightPitchDegree, FlightRollDegree, GimbalYawDegree, GimbalPitchDegree, GimbalRollDegree

        except Exception as e:
            return None, None, None, None, None, None
        

    def postProcessAlgorithm(self, context, feedback):
        layer = QgsProcessingUtils.mapLayerFromString(self.SAIDA, context)
        
        params = { 'LAYER' : layer, 'STYLE_POINT' : self.ESTILO, 'STYLE_LINE' : 0, 'STYLE_POLYGON' : 0 }
        processing.run("lftools:magicstyles", params)

        if self.pasta[0:2] in (r'\\', r'//'):
            layer.setMapTipTemplate(r'''[%''' + self.tr("name") + '''%]<br><img src="file://[%''' + self.tr('path') + '''%]" width="450">''')
        else:
            layer.setMapTipTemplate(r'''[%''' + self.tr("name") + '''%]<br><img src="file:///[%''' + self.tr('path') + '''%]" width="450">''')

        renderer3d = simbologiaPontos3D()
        layer.setRenderer3D(renderer3d)
        layer.trigger3DUpdate()
        layer.triggerRepaint() 

        return {self.OUTPUT: self.SAIDA}
