# -*- coding: utf-8 -*-
r"""Constroi um mapa de alturas do terreno a partir das capturas do OFICIAL.

    python tools/extrair_alturas.py [pasta_ou_ficheiros...]  ->  Data/Maps/Altura.ini

PORQUE EXISTE. Em 28/08 comparou-se um pacote 04 01 nosso com um do oficial, campo a campo,
e a diferenca estava no Z:

    OFICIAL   z=546,04/545,54  ->  546,04/545,93  ->  545,93/545,32  ->  544,33/543,17
    NOSSO     z=531,53/531,53  ->  531,53/531,53  ->  531,53/531,53

Medido em seguida: 18 das 21 entidades do oficial variam de altura (amplitude mediana 8,38,
maxima 35,44) e 20 das 20 nossas tem o Z ABSOLUTAMENTE FIXO. O nosso monstro guarda a altura
do sitio onde nasceu para o resto da vida e desliza a essa altura por cima de encostas.

Isto escapou a um dia inteiro de medicoes porque todas elas -- passo, intervalo, rumo,
rectidao, buracos -- eram sobre x e y. Nunca ninguem olhou para o terceiro float.

O CLIENT nao tem aqui um mapa de alturas que saibamos ler, e a trilha gravada so tem x e y
(formato ms:x:y:dx:dy:vel:estado). Mas as capturas trazem a altura de graca: cada 04 01 tem
a posicao completa da entidade, e 12,8 horas de gravacao dao 300 mil pontos. E terreno
medido, nao inventado.

GRELHA DE 25 UNIDADES. Medida a dispersao do z dentro da celula: mediana 3,15 a 25 u, 6,21 a
50 u, 11,02 a 100 u. A 25 fica fino que chegue e ainda da 18 mil celulas ocupadas. Guarda-se
a MEDIANA da celula, nao a media: um ponto num telhado ou numa ponte nao deve arrastar o
chao todo.
"""
import collections
import io
import os
import re
import statistics
import struct
import sys

CAB = re.compile(r"^\[(\d{1,2}):(\d\d):(\d\d)\.(\d+)\] (SEND|RECV)")
HEX = re.compile(r"^[0-9A-Fa-f ]+$")
LADO = 25.0
MIN_AMOSTRAS = 2


def pontos_de(caminho, limite=None):
    saida = []
    atual, buf, lidos = None, [], 0

    def fechar():
        if atual is None:
            return
        try:
            b = bytes.fromhex("".join(buf))
        except ValueError:
            return
        i = 0
        while i + 4 <= len(b):
            k = int.from_bytes(b[i:i + 4], "big")
            if k <= 0 or k > 65535 or i + 4 + k > len(b):
                break
            p = b[i + 4:i + 4 + k]
            # SO O 04 01. O 04 02 (aparicao) leva a altura somada de ALTURA_APARICAO e
            # estragaria o chao em 5 unidades.
            if len(p) == 42 and p[0] == 4 and p[1] == 1:
                for off in (12, 24):
                    x, y, z = struct.unpack_from("<3f", p, off)
                    if 0 < x < 100000 and 0 < y < 100000 and 0 < z < 5000:
                        saida.append((x, y, z))
            i += 4 + k

    with io.open(caminho, encoding="latin-1", errors="ignore") as f:
        for ln in f:
            lidos += len(ln)
            if limite and lidos > limite:
                break
            m = CAB.match(ln)
            if m:
                fechar()
                atual, buf = m.group(5), []
            elif atual and HEX.match(ln.strip()):
                buf.append(ln.strip().replace(" ", ""))
    fechar()
    return saida


def main() -> int:
    alvos = []
    for a in (sys.argv[1:] or [os.path.join(os.environ.get("USERPROFILE", ""),
                                            "Desktop", "CapturaKaros")]):
        if os.path.isdir(a):
            alvos += [os.path.join(a, f) for f in sorted(os.listdir(a)) if f.endswith(".log")]
        elif os.path.isfile(a):
            alvos.append(a)
    if not alvos:
        print(__doc__)
        return 2

    cel = collections.defaultdict(list)
    total = 0
    for cam in alvos:
        mb = os.path.getsize(cam) / 1e6
        pts = pontos_de(cam, 45_000_000 if mb > 60 else None)
        total += len(pts)
        for x, y, z in pts:
            cel[(int(x // LADO), int(y // LADO))].append(z)
        print("   %-32s %7d pontos" % (os.path.basename(cam), len(pts)))

    linhas = []
    for (cx, cy), zs in sorted(cel.items()):
        if len(zs) < MIN_AMOSTRAS:
            continue
        linhas.append((cx, cy, statistics.median(zs), len(zs)))
    print()
    print("%d pontos -> %d celulas de %.0f unidades" % (total, len(linhas), LADO))

    destino = os.path.join(os.path.dirname(os.path.abspath(__file__)),
                           "..", "server", "Data", "Maps", "Altura.ini")
    nl = chr(10)
    with io.open(destino, "w", encoding="utf-8", newline="") as f:
        f.write("; Altura -- mapa de alturas do terreno, MEDIDO nas capturas do oficial." + nl)
        f.write("; colunas: cx, cy, z, amostras     (celula de %.0f unidades)" % LADO + nl)
        f.write(";" + nl)
        f.write("; Gerado por tools/extrair_alturas.py. Cada celula guarda a MEDIANA do z" + nl)
        f.write("; observado la dentro. Ver o cabecalho dessa ferramenta para o porque." + nl)
        f.write(";" + nl)
        # O CABECALHO TEM DE COMECAR POR barra-barra. As linhas ';' sao so comentario, e
        # sem essa linha o data_files nao sabe nomear as colunas e ignora o ficheiro
        # INTEIRO em silencio -- devolve zero linhas, sem aviso nenhum.
        f.write("//cx\tcy\tz\tamostras" + nl)
        for cx, cy, z, n in linhas:
            f.write("%d\t%d\t%.2f\t%d%s" % (cx, cy, z, n, nl))
    print("escrito em %s" % os.path.normpath(destino))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
