from qgis.core import (QgsField, QgsProject, QgsVectorLayer, QgsWkbTypes,
                       QgsFieldConstraints, QgsEditorWidgetSetup,
                       QgsTextFormat, QgsTextBufferSettings,
                       QgsVectorLayerSimpleLabeling, QgsPalLayerSettings,
                       QgsTextBackgroundSettings, QgsDefaultValue)
from PyQt5.QtGui import QFont, QColor
from PyQt5.QtCore import QVariant

class CamadaPontosManager:
    """
    Cria e gerencia camadas temporárias de pontos, atualizando campos X/Y
    (e DMS, se o CRS for geográfico) e rótulos de ID automaticamente.
    """
    def __init__(self, iface):
        self.iface = iface
        self._conns = {}  # {layer_id: {"added": func, "geom": func}}

        # Desconecta quando a camada for removida do projeto
        QgsProject.instance().layerWillBeRemoved.connect(self._on_layer_will_be_removed)

    def _on_layer_will_be_removed(self, layer_id: str):
        """
        Desconecta com segurança os sinais associados a uma camada que está
        prestes a ser removida do projeto.

        Objetivo:
        - Evitar callbacks dispararem depois que a camada foi destruída,
          prevenindo erros do tipo "wrapped C/C++ object ... has been deleted".
        """
        conns = self._conns.pop(layer_id, None)  # remove e recupera as conexões guardadas
        if not conns:
            return  # nada para desconectar

        lyr = QgsProject.instance().mapLayer(layer_id)  # tenta pegar a camada ainda viva
        if not lyr:
            return  # já não existe no projeto (ou já foi destruída)

        # Desconecta o sinal featureAdded (se ainda estiver conectado)
        try:
            lyr.featureAdded.disconnect(conns["added"])
        except Exception:
            pass

        # Desconecta o sinal geometryChanged (se ainda estiver conectado)
        try:
            lyr.geometryChanged.disconnect(conns["geom"])
        except Exception:
            pass

    def _conectar_sinais(self, camada: QgsVectorLayer, is_geografico: bool):
        """
        Conecta os sinais da camada para manter X/Y (e DMS) atualizados.

        Estratégia:
        - Guarda callbacks nomeados (não usa lambda) para permitir disconnect depois.
        - Usa o layer_id e re-resolve a camada via QgsProject para evitar ponteiros
          inválidos caso a camada seja removida.
        - Evita conexões duplicadas se por acaso este método for chamado duas vezes
          para a mesma camada.
        """
        layer_id = camada.id()  # ID único da camada no projeto

        def on_added(fid, lid=layer_id, geo=is_geografico):
            # Re-resolve a camada pelo ID (evita usar referência morta)
            lyr = QgsProject.instance().mapLayer(lid)
            if not isinstance(lyr, QgsVectorLayer) or not lyr.isValid():
                return
            self._atualizar_valores_ponto(lyr, fid, geo)  # atualiza X/Y/DMS ao adicionar

        def on_geom(fid, _geom, lid=layer_id, geo=is_geografico):
            # Re-resolve a camada pelo ID (evita usar referência morta)
            lyr = QgsProject.instance().mapLayer(lid)
            if not isinstance(lyr, QgsVectorLayer) or not lyr.isValid():
                return
            self._atualizar_valores_ponto(lyr, fid, geo)  # atualiza X/Y/DMS ao mover/editar

        # Evita conexão duplicada se alguém chamar isso duas vezes para a mesma layer
        if layer_id in self._conns:
            self._on_layer_will_be_removed(layer_id)  # desconecta antes de reconectar

        # Conecta sinais principais de edição
        camada.featureAdded.connect(on_added)      # disparado quando cria ponto novo
        camada.geometryChanged.connect(on_geom)    # disparado quando altera geometria

        # Guarda callbacks para permitir desconectar depois
        self._conns[layer_id] = {"added": on_added, "geom": on_geom}

    def criar_camada_pontos(self, nome_camada: str | None = None):
        """
        Cria e adiciona ao projeto uma *camada temporária de pontos* em memória.

        O que esta rotina faz, passo a passo

        1. Obtém o CRS atual do projeto e identifica se ele é geográfico
           (lat/long) ou projetado (metros).
        2. Gera um nome único para a camada caso o usuário não forneça um.
        3. Instancia a camada “memory” do tipo *Point* com o CRS do projeto.
        4. Chama rotinas auxiliares para:
           • `_configura_campos`  – cria campos ID, X, Y (+ X_DMS/Y_DMS se geográfico),
                                   define ID autoincremental e oculta campos numéricos.
           • `_configura_etiquetas` – habilita rótulo azul no campo ID.
           • `_conectar_sinais`   – liga featureAdded / geometryChanged para manter
                                   X, Y e DMS atualizados.
        5. Adiciona a nova camada ao painel de camadas (`QgsProject.addMapLayer`).
        6. Coloca a camada em modo de edição (`startEditing`).
        7. Cria e adiciona ao projeto uma *camada temporária de pontos* em memória
        8. já deixa a ferramenta 'Adicionar Ponto' ativada
        7. Retorna o objeto `QgsVectorLayer` criado.

        :param nome_camada:  Nome desejado ou *None* para gerar automaticamente.
        :return:             Instância da camada criada.
        """
        crs_proj = QgsProject.instance().crs()                        # CRS do projeto
        is_geografico = crs_proj.isGeographic()                       # True se lat/long

        # Nome único se não informado
        nome_camada = nome_camada or self._gera_nome_camada("Ponto Temp")

        # Cria camada em memória: tipo Point + CRS do projeto
        vl = QgsVectorLayer(f"Point?crs={crs_proj.authid()}",         # URI de camada
                            nome_camada, "memory")                    # Nome + provedor

        # Configura metadados, rótulos e sinais
        self._configura_campos(vl, is_geografico)                     # adiciona campos
        self._configura_etiquetas(vl)                                 # rótulo ID
        self._conectar_sinais(vl, is_geografico)                      # sinais X/Y

        # adiciona no TOPO da lista (sem grupos)
        proj = QgsProject.instance()
        proj.addMapLayer(vl, False)          # evita inserir automático
        proj.layerTreeRoot().insertLayer(0, vl)

        # Entra em modo de edição
        vl.startEditing()

        # Ativa a camada no painel e a ferramenta de adicionar ponto
        self.iface.setActiveLayer(vl)                  # torna esta a camada ativa
        action = self.iface.actionAddFeature()         # pega ação "Adicionar feição"
        if action:                                     # se a ação existir
            action.trigger()   

        return vl                                                     # devolve camada

    def _gera_nome_camada(self, nome_base: str) -> str:
        """
        Gera um nome de camada único baseado em um nome base.

        Esta rotina realiza os seguintes passos:
        1. Inicializa um contador em 1.
        2. Constrói o nome da camada concatenando o nome base e o contador.
        3. Enquanto já existir alguma camada no projeto com o mesmo nome:
           a. Incrementa o contador em 1.
           b. Reconstrói o nome usando o novo contador.
        4. Retorna o nome que não conflita com nenhuma camada existente.

        :param nome_base: Texto inicial para a camada (sem número).
        :return:         Nome único, ex.: "Ponto Temp 1", "Ponto Temp 2", etc.
        """
        contador = 1                                           # começa a contagem em 1
        nome = f"{nome_base} {contador}"                       # monta o primeiro nome
        # verifica se já existe camada com este nome
        while QgsProject.instance().mapLayersByName(nome):
            contador += 1                                      # incrementa contador
            nome = f"{nome_base} {contador}"                   # atualiza nome com novo número
        return nome                                             # devolve o nome único

    def _configura_campos(self, camada: QgsVectorLayer, is_geografico: bool):
        """
        Cria e configura os campos da camada de pontos.

        - Adiciona os campos: ID (int), X/Y (double) com precisão dinâmica.
        - Se o CRS for geográfico, adiciona também X_DMS e Y_DMS (texto).
        - Aplica restrições no ID (único e não-nulo) e define autoincremento.
        - Oculta os campos de coordenadas na interface de edição para manter a UI limpa.
        """
        # Precisão: 6 casas em geográfico (lat/long), 3 em projetado (metros)
        prec_xy = 6 if is_geografico else 3

        # Campo inteiro de identificação
        id_field = QgsField("ID", QVariant.Int)

        # Campos numéricos de coordenadas com precisão conforme o CRS
        x_field = QgsField("X", QVariant.Double, "double", 20, prec_xy)  # X com precisão dinâmica
        y_field = QgsField("Y", QVariant.Double, "double", 20, prec_xy)  # Y com precisão dinâmica

        # Restrições do campo ID: único e obrigatório
        cons = QgsFieldConstraints()
        cons.setConstraint(QgsFieldConstraints.ConstraintUnique)   # impede IDs duplicados
        cons.setConstraint(QgsFieldConstraints.ConstraintNotNull)  # impede ID vazio
        id_field.setConstraints(cons)                              # aplica restrições no campo

        # Lista base de campos
        campos = [id_field, x_field, y_field]

        # Se CRS geográfico, adiciona campos DMS (texto)
        if is_geografico:
            campos += [
                QgsField("X_DMS", QVariant.String, len=20),  # longitude em DMS
                QgsField("Y_DMS", QVariant.String, len=20)]   # latitude em DMS

        # Adiciona campos no provider e atualiza metadados da camada
        camada.dataProvider().addAttributes(campos)  # efetiva criação dos campos
        camada.updateFields()                        # atualiza lista de campos na camada

        # Define autoincremento do ID (máximo atual + 1)
        idx_id = camada.fields().indexOf("ID")       # índice do campo ID
        expr = 'coalesce(maximum("ID"),0) + 1'       # expressão de incremento
        camada.setDefaultValueDefinition(idx_id, QgsDefaultValue(expr))  # aplica default

        # Oculta campos de coordenadas na UI (o usuário normalmente não precisa editar manualmente)
        hidden = QgsEditorWidgetSetup("Hidden", {})
        for nome in ("X", "Y", "X_DMS", "Y_DMS"):
            idx = camada.fields().indexOf(nome)      # índice do campo
            if idx != -1:
                camada.setEditorWidgetSetup(idx, hidden)  # esconde o campo no formulário

    def _configura_etiquetas(self, camada: QgsVectorLayer):
        """
        Configura o rótulo de feição para o campo "ID", aplicando:
        1. Ativação do label layer-wide.
        2. Uso do campo "ID" como fonte do texto.
        3. Cor do texto em azul (RGB 0,0,255).
        4. Fonte Arial tamanho 12.
        5. Fundo branco para o texto (para melhorar legibilidade).
        6. Aplica as definições ao provedor de labels da camada.
        
        :param camada: Instância de QgsVectorLayer que receberá as configurações de label.
        """
        pal = QgsPalLayerSettings()                                # cria objeto de configurações de label
        pal.fieldName = "ID"                                       # define o campo que alimenta o texto
        pal.enabled = True                                         # habilita rótulos para esta camada

        fmt = QgsTextFormat()                                      # objeto para formatar o texto
        fmt.setColor(QColor(0, 0, 255))                            # define a cor do texto como azul
        fmt.setFont(QFont("Arial", 12))                            # define a fonte e o tamanho do texto

        bg = QgsTextBackgroundSettings()                           # configurações de fundo do texto
        bg.setEnabled(True)                                        # ativa o fundo
        bg.setFillColor(QColor(255, 255, 255))                     # cor de preenchimento do fundo: branco
        fmt.setBackground(bg)                                      # associa o fundo à formatação de texto

        pal.setFormat(fmt)                                         # aplica o formato ao objeto de settings
        camada.setLabelsEnabled(True)                              # habilita labels na camada (global)
        camada.setLabeling(QgsVectorLayerSimpleLabeling(pal))      # define o esquema de labeling na camada

    def _conectar_sinais(self, camada: QgsVectorLayer, is_geografico: bool):
        """
        Conecta os sinais da camada para manter os atributos de coordenadas
        sempre atualizados ao criar ou modificar pontos.

        Esta rotina realiza:
        1. Conexão do sinal `featureAdded`:
           – sempre que uma nova feição for adicionada,
             chama `_atualizar_valores_ponto` para preencher X/Y (e DMS).
        2. Conexão do sinal `geometryChanged`:
           – sempre que a geometria de uma feição for alterada,
             chama `_atualizar_valores_ponto` para recalcular X/Y (e DMS).
        3. Passagem de parâmetros:
           – `camada` para indicar qual layer atualizar;
           – `fid` para identificar a feição;
           – `is_geografico` para controlar se DMS deve ser calculado.
        
        :param camada:        QgsVectorLayer de pontos sob edição.
        :param is_geografico: True se o CRS do projeto for geográfico.
        """
        # 1) Ao adicionar uma nova feição, atualiza X/Y (e DMS)
        camada.featureAdded.connect(lambda fid: self._atualizar_valores_ponto(camada, fid, is_geografico))
        # 2) Ao alterar a geometria existente, reatualiza X/Y (e DMS)
        camada.geometryChanged.connect(lambda fid, _geom: self._atualizar_valores_ponto(camada, fid, is_geografico))

    def _atualizar_valores_ponto(self, camada: QgsVectorLayer, fid: int, is_geografico: bool):
        """
        Atualiza automaticamente os atributos de coordenadas (X/Y) de uma feição de ponto
        sempre que ela é criada ou tem sua geometria alterada.

        - Grava X e Y arredondados (6 casas em CRS geográfico, 3 em CRS projetado).
        - Se o CRS for geográfico, também atualiza X_DMS e Y_DMS (graus/min/seg).
        - Faz validações para evitar erros quando a camada/feição/geometria não estão prontas.
        """
        # Segurança: camada precisa existir e estar válida
        if not camada or not camada.isValid():
            return

        # Só altera atributos se a camada estiver em modo de edição
        if not camada.isEditable():
            return

        # Busca índices dos campos X/Y (se não existirem, aborta)
        idx_x = camada.fields().indexOf("X")  # índice do campo X
        idx_y = camada.fields().indexOf("Y")  # índice do campo Y
        if idx_x < 0 or idx_y < 0:
            return

        # Recupera a feição pelo fid e valida
        feat = camada.getFeature(fid)  # obtém a feição alvo
        if not (feat and feat.isValid()):
            return

        # Valida geometria
        geom = feat.geometry()  # pega a geometria atual da feição
        if not geom or geom.isEmpty():
            return

        # Pega um ponto "seguro": suporta point e multipoint
        try:
            if geom.isMultipart():  # caso multiponto
                pts = geom.asMultiPoint()
                if not pts:
                    return
                pt = pts[0]  # usa o primeiro ponto
            else:
                pt = geom.asPoint()  # ponto simples
        except Exception:
            return

        # Define precisão: geográfico precisa de mais casas decimais
        casas = 6 if is_geografico else 3
        x_val = round(pt.x(), casas)  # arredonda X
        y_val = round(pt.y(), casas)  # arredonda Y

        # Atualiza atributos X/Y
        camada.changeAttributeValue(fid, idx_x, x_val)  # grava X
        camada.changeAttributeValue(fid, idx_y, y_val)  # grava Y

        # Se CRS for geográfico, também atualiza DMS (se os campos existirem)
        if is_geografico:
            idx_xd = camada.fields().indexOf("X_DMS")  # índice do campo X_DMS
            idx_yd = camada.fields().indexOf("Y_DMS")  # índice do campo Y_DMS

            if idx_xd >= 0:
                camada.changeAttributeValue(fid, idx_xd, self._dec_to_dms(pt.x()))  # grava longitude em DMS
            if idx_yd >= 0:
                camada.changeAttributeValue(fid, idx_yd, self._dec_to_dms(pt.y()))  # grava latitude em DMS

    @staticmethod
    def _dec_to_dms(valor: float) -> str:
        """
        Converte coordenada decimal em string de graus, minutos e segundos (DMS).

        Passos realizados:
        1. Determina o sinal (“–” se negativo, vazio se positivo).
        2. Trabalha com o valor absoluto para cálculo de graus, minutos e segundos.
        3. Extrai a parte inteira como graus.
        4. Converte a parte decimal em minutos.
        5. Converte o restante em segundos.
        6. Formata o resultado como "SINALgraus° minutos' segundos\"".

        :param valor: Valor decimal da coordenada (positivo ou negativo).
        :return:      Representação DMS, ex.: "-23° 33' 45.67\""
        """
        sinal = "-" if valor < 0 else ""                              # 1) define sinal conforme valor
        valor = abs(valor)                                            # 2) usa valor absoluto para cálculos
        graus = int(valor)                                            # 3) parte inteira → graus
        minutos = int((valor - graus) * 60)                           # 4) parte decimal × 60 → minutos
        segundos = (valor - graus - minutos / 60) * 3600               # 5) restante × 3600 → segundos
        return f"{sinal}{graus}° {minutos}' {segundos:.2f}\""          # 6) formata string DMS

