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

    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):
        """
        Configura os campos da camada de pontos:
        1. Adiciona campos para ID, X e Y.
        2. Se o CRS for geográfico, inclui também X_DMS e Y_DMS.
        3. Define restrições de unicidade e não-nulo para o campo ID.
        4. Aplica valor padrão autoincremental a ID.
        5. Oculta os campos numéricos (X, Y, X_DMS e Y_DMS) na interface de edição.
        
        :param camada:         Instância de QgsVectorLayer a ser configurada.
        :param is_geografico:  True se o CRS do projeto for geográfico (lat/long).
        """
        # Cria campo inteiro para ID
        id_field = QgsField("ID", QVariant.Int)
        # Cria campos numéricos para coordenadas
        x_field  = QgsField("X",  QVariant.Double, "double", 20, 3)
        y_field  = QgsField("Y",  QVariant.Double, "double", 20, 3)

        # Define restrições de unicidade e obrigatoriedade no campo ID
        cons = QgsFieldConstraints()                             # objeto de restrições
        cons.setConstraint(QgsFieldConstraints.ConstraintUnique) # ID único
        cons.setConstraint(QgsFieldConstraints.ConstraintNotNull)# ID não-nulo
        id_field.setConstraints(cons)                             # aplica restrições

        # Monta a lista de campos básicos
        campos = [id_field, x_field, y_field]
        # Se o CRS for geográfico, adiciona campos de string para DMS
        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 os campos à camada e atualiza o provedor
        camada.dataProvider().addAttributes(campos)               # insere novos campos
        camada.updateFields()                                     # atualiza metadados

        # Configura valor padrão para ID como (máximo ID + 1)
        idx_id = camada.fields().indexOf("ID")                    # índice do campo ID
        expr   = 'coalesce(maximum("ID"),0) + 1'                  # expressão para autoincremento
        camada.setDefaultValueDefinition(idx_id, QgsDefaultValue(expr))

        # Prepara widget que oculta colunas na UI de edição
        hidden = QgsEditorWidgetSetup("Hidden", {})

        # Itera sobre todos os nomes de campos numéricos e oculta-os se existirem
        for nome in ("X", "Y", "X_DMS", "Y_DMS"):
            idx = camada.fields().indexOf(nome)                    # busca índice do campo
            if idx != -1:
                camada.setEditorWidgetSetup(idx, hidden)          # aplica widget oculto

    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 os atributos de coordenadas de um ponto sempre que ele
        é criado ou sua geometria é modificada.

        Passos executados:
        1. Localiza os índices dos campos X e Y na camada.
        2. Recupera a feição pelo seu ID (fid).
        3. Verifica se a feição e sua geometria são válidas e não vazias.
           – Caso contrário, aborta sem alterar nada.
        4. Obtém a geometria em formato de ponto (QgsPointXY).
        5. Calcula as coordenadas X e Y com casas decimais adequadas:
           • 6 casas se CRS for geográfico (lat/long).
           • 3 casas caso contrário (projetado).
        6. Grava os valores de X e Y nos atributos correspondentes.
        7. Se o CRS for geográfico, também:
           a. Converte X e Y para DMS.
           b. Atualiza os campos X_DMS e Y_DMS.
        """
        # 1) Índices dos campos numéricos
        idx_x = camada.fields().indexOf("X")                        # busca índice do campo X
        idx_y = camada.fields().indexOf("Y")                        # busca índice do campo Y

        # 2) Recupera a feição pelo ID
        feat = camada.getFeature(fid)                               # obtém objeto QgsFeature
        # 3) Valida feição e geometria
        if not (feat and feat.isValid() and feat.geometry()         # confere existência e validade
                and not feat.geometry().isEmpty()):                # e não-vazia
            return                                                 # sai se inválida

        # 4) Extrai o ponto (QgsPointXY)
        pt = feat.geometry().asPoint()                             # coordenadas do vértice

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

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

        # 7) Se geográfico, calcula e atualiza DMS
        if is_geografico:
            idx_xd = camada.fields().indexOf("X_DMS")               # índice X_DMS
            idx_yd = camada.fields().indexOf("Y_DMS")               # índice Y_DMS
            # converte valores decimais para DMS e grava
            camada.changeAttributeValue(fid, idx_xd, self._dec_to_dms(pt.x()))
            camada.changeAttributeValue(fid, idx_yd, self._dec_to_dms(pt.y()))

    @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

