# -*- coding: utf-8 -*-
"""Registro global de quem esta no mundo, e a retransmissao entre conexoes.

Ate 23/08 o world_server era uma ilha por conexao: `handle_client` numa thread, `entity_sim`
com registro proprio, e nada compartilhado. Dois jogadores logados nao se viam -- eram duas
sessoes solo no mesmo processo. Este modulo e o ponto onde as conexoes passam a se conhecer.

Regra de ouro, a mesma do db.py: NUNCA derrubar o jogo. Se uma retransmissao falhar (conexao
que caiu no meio), o jogador de destino sai do registro e os outros seguem.
"""
from __future__ import annotations

import socket
import threading

import lotacao

_lock = threading.RLock()
_jogadores: dict[int, "Jogador"] = {}


class Jogador:
    """Um jogador dentro do mundo, do ponto de vista dos OUTROS."""

    def __init__(self, entity_id: int, nome: str, conn: socket.socket,
                 send_lock: threading.Lock, aparicao: bytes, heroi=None):
        self.entity_id = int(entity_id)
        # O Heroi vive na thread da conexao dele. Guardamos a referencia aqui porque a EXP
        # passou a ser dividida por dano (25/08): quem mata precisa creditar EXP em quem
        # ajudou, e esses estao em OUTRAS conexoes. Sem isto so daria pra premiar quem
        # desferiu o golpe final, que e justamente o que a divisao veio corrigir.
        self.heroi = heroi
        # Entidades que este jogador JA VE. E a memoria do campo de visao: sem ela nao da
        # pra saber o que acabou de entrar (precisa de 04 02) e o que acabou de sair
        # (precisa de 04 03). Mandar aparicao de quem ja esta na tela desenha em dobro.
        self.visiveis = set()
        self.nome = nome
        self.conn = conn
        self.send_lock = send_lock
        # o frame 04 02 pronto: e o que os outros recebem pra desenhar este personagem
        self.aparicao = aparicao
        # POSICAO: onde ele ESTA, nao para onde disse que ia.
        #
        # O 04 01 do client traz origem e DESTINO. O servidor guardava o destino, portanto a
        # posicao dele saltava para a frente no instante em que a tecla era carregada -- e os
        # monstros, que apontam a esta posicao, arrancavam antes do jogador chegar la. Ele
        # descreveu-o exactamente assim: "parece que eu que controlo o mob, quando aperto W o
        # mob sai primeiro que eu".
        #
        # Isto tambem explicava o resto do que se via na perseguicao: o mob apontava sempre a
        # frente do jogador, portanto passava por ele; atacava de longe porque estava perto do
        # fantasma e nao dele; e as distancias medidas saltavam porque a posicao guardada
        # saltava a cada pacote.
        #
        # Agora guarda-se o segmento (origem, destino, instante, velocidade) e interpola-se em
        # `pos()`. `x`/`y`/`z` ficam a valer a origem, para quem so precisa de uma posicao
        # aproximada e nao chama `pos()`.
        self.x = self.y = self.z = 0.0
        self._mov = None          # (t0, origem, destino, duracao)
        self.rot = 0.0
        # ARMA SACADA (03 02). O client desenha a arma na MAO so quando este estado esta
        # ligado -- com ele desligado a adaga fica na cintura e o rifle nas costas, mesmo
        # durante a animacao de golpe. Guardamos por jogador porque quem entra depois
        # precisa receber o estado de quem ja estava lutando.
        self.arma_sacada = False

    def mover_para(self, origem, destino, agora, velocidade=None):
        """Regista o segmento anunciado pelo client.

        A DURACAO VEM DO RITMO DO PROPRIO CLIENT, nao de uma velocidade adivinhada.
        --------------------------------------------------------------------------
        A primeira versao calculava `distancia / 65`, com 65 tirado da velocidade medida do
        jogador. Isso da a mediana certa -- medido no fio: 65,0 u/s -- mas erra sempre que ele
        nao anda a 65: com um bonus de velocidade, a andar em vez de correr, ou quando muda de
        direccao antes de chegar ao ponto declarado. Nesses casos a interpolacao acabava cedo,
        ficava parada no destino, e saltava quando o pacote seguinte chegava: 35 por cento das
        amostras acima de 90 u/s, com picos de 797.

        O client manda um 04 01 a cada meio segundo mais ou menos. Usar o intervalo REAL entre
        dois pacotes dele como duracao do segmento e auto-calibrante: nao e preciso saber a
        que velocidade ele anda, porque ele proprio diz de quanto em quanto tempo re-anuncia.
        """
        import math as _m
        anterior = getattr(self, "_ult_mov_t", None)
        if velocidade and velocidade > 0.1:
            d = _m.hypot(destino[0] - origem[0], destino[1] - origem[1])
            dur = d / velocidade
        elif anterior is not None and 0.05 < agora - anterior < 2.0:
            dur = agora - anterior
        else:
            dur = 0.5                      # primeiro pacote, ou uma pausa longa
        self._ult_mov_t = agora
        self._mov = (agora, tuple(origem), tuple(destino), dur)
        self.x, self.y, self.z = origem

    def pos(self, agora=None):
        """Onde o jogador esta AGORA, interpolando o segmento anunciado.

        Passado o fim do segmento devolve o destino: o client chegou la e ainda nao anunciou
        o seguinte, portanto e mesmo onde ele esta.
        """
        if self._mov is None:
            return (self.x, self.y, self.z)
        import time as _t
        t0, o, d, dur = self._mov
        if agora is None:
            agora = _t.time()
        if dur <= 0.0:
            return d
        f = (agora - t0) / dur
        if f >= 1.0:
            return d
        if f <= 0.0:
            return o
        return (o[0] + (d[0] - o[0]) * f,
                o[1] + (d[1] - o[1]) * f,
                o[2] + (d[2] - o[2]) * f)

    def enviar(self, frame: bytes) -> bool:
        """True se conseguiu enviar. False (e o jogador sai do registro) se a conexao morreu."""
        try:
            with self.send_lock:
                self.conn.sendall(frame)
            return True
        except OSError:
            return False


# A CONTAGEM VAI PARA FORA DO PROCESSO. O channel_server desenha o estado do canal e o
# login_server recusa quando esta cheio, e nenhum dos dois ve o `_jogadores` daqui.
# Ver server/lotacao.py.
def _publicar_contagem() -> None:
    lotacao.publicar(len(_jogadores))


def entrar(jogador: Jogador) -> list[Jogador]:
    """Poe o jogador no mundo. Devolve quem ja estava la (sem ele)."""
    with _lock:
        _jogadores[jogador.entity_id] = jogador
        _publicar_contagem()
        return [j for j in _jogadores.values() if j.entity_id != jogador.entity_id]


def sair(entity_id: int) -> Jogador | None:
    with _lock:
        saiu = _jogadores.pop(int(entity_id), None)
        _publicar_contagem()
        return saiu


def jogador(entity_id: int) -> Jogador | None:
    with _lock:
        return _jogadores.get(int(entity_id))


def outros(entity_id: int) -> list[Jogador]:
    with _lock:
        return [j for j in _jogadores.values() if j.entity_id != int(entity_id)]


def quantos() -> int:
    with _lock:
        return len(_jogadores)


def nomes() -> list[str]:
    with _lock:
        return [j.nome for j in _jogadores.values()]


def todos() -> list[Jogador]:
    with _lock:
        return list(_jogadores.values())


def transmitir_todos(frame: bytes) -> int:
    """Manda o frame pra TODOS. Usado pela thread do mundo, que nao tem um 'de quem'."""
    mortos, enviados = [], 0
    for j in todos():
        if j.enviar(frame):
            enviados += 1
        else:
            mortos.append(j.entity_id)
    for eid in mortos:
        sair(eid)
    return enviados


def transmitir(de_quem: int, frame: bytes) -> int:
    """Manda o frame pra todos MENOS quem originou. Devolve quantos receberam.

    Quem nao recebe (conexao morta) sai do registro na hora -- senao a lista cresce com
    fantasmas e todo broadcast fica mais lento.
    """
    mortos, enviados = [], 0
    for j in outros(de_quem):
        if j.enviar(frame):
            enviados += 1
        else:
            mortos.append(j.entity_id)
    for eid in mortos:
        sair(eid)
    return enviados
