#!/usr/bin/env python3
"""dia2formas: que forma de Dia lleva cada rama de un circuito de Simusol.

    dia2formas tanque.dia             escribe tanque.formas
    dia2formas -r DIR...              todos los .dia de esos directorios

El argumento es el diagrama de Simusol (junta-dias lo deja al lado de cada
circuito como NOMBRE.dia). En el diagrama cada forma lleva un texto, y el
nombre de la rama es la letra de la forma mas ese texto: la forma
'Termico - acumulador' con el texto A es la rama CA. Si al lado esta el
circuito (NOMBRE.d o NOMBRE.ds) se usan sus ramas para los nombres (una
rama que en el diagrama ya tiene el nombre entero) y para no anotar formas
de ramas que el circuito no tiene. El resultado es un archivo aparte,
NOMBRE.formas, con lo que el .d no dice y hace falta para dibujarlo:

    # nombre  forma                 orientacion         x      y      ancho  alto
    CA        Termico - acumulador  fija                2.439  6.842  1.723  1.516
    J1        Termico - flujoi      hacia la izquierda  6.425  3.675  1.55   1.55
    R5        Termico - hconductiva horizontal          9.13   1.565  1.44   1.171
    masa      Termico - 0gradosc    fija                8.05   7.3    1.9    1.9
    nodo 1    Termico - nodo        fija                5.175  1.425  1.55   1.55

con los campos separados por un tabulador: un renglon por rama, uno por
cada simbolo de masa y uno por cada nodo, con el nombre que tiene en el
diagrama. La orientacion sale del nombre de la forma (h horizontal, v
vertical; en los flujos d, i, D y U la direccion) y de si en Dia esta
volteada (flip_horizontal, flip_vertical): "horizontal, volteada
horizontal"; las que se dibujan de una sola manera (el tanque, la fuente,
la masa) dicen "fija". x e y son la esquina de la forma en el diagrama, y
ancho y alto su tamano, en las unidades de Dia (cm, y crece hacia abajo).

dibuja lo lee si esta junto al circuito y dibuja cada rama con su forma;
las ramas que no esten, con el conjunto de formas que predomina.

Copyright (C) Diego Saravia <dsa@unsa.edu.ar>
GPL version 3 o posterior.
"""
import argparse
import gzip
import json
import os
import re
import shutil
import subprocess
import sys

AQUI = os.path.dirname(os.path.realpath(__file__))
SIRU = os.path.normpath(os.path.join(AQUI, ".."))

# la letra de la rama segun la forma (por la palabra del nombre de la forma)
LETRAS = [("resist", "R"), ("conductiva", "R"), ("convectiva", "R"), ("radiativa", "R"),
          ("capacitor", "C"), ("acumulador", "C"), ("induct", "L"),
          ("fem", "E"), ("fuente", "E"), ("corriente", "J"), ("flujo", "J"),
          ("llave", "S"), ("transformador", "T")]
MASAS = ("0gradosc", "tierra", "ground", "Generico - 0")


def letra(forma):
    f = forma.lower()
    for palabra, l in LETRAS:
        if palabra in f:
            return l
    return None


def es_masa(forma):
    return any(m.lower() == forma.lower() or forma.lower().endswith(m.lower()) for m in MASAS)


def orientacion(forma, volteada_h, volteada_v):
    """la orientacion de la forma: la del nombre (h, v; la direccion de un
    flujo) y si esta volteada"""
    nombre = forma.split(" - ", 1)[-1]
    direcciones = {"flujod": "hacia la derecha", "flujoi": "hacia la izquierda",
                   "flujoD": "hacia abajo", "flujoU": "hacia arriba", "flujoab": "hacia abajo"}
    if nombre in direcciones:
        o = direcciones[nombre]
    elif nombre[:1] == "h" and len(nombre) > 1:
        o = "horizontal"
    elif nombre[:1] == "v" and len(nombre) > 1:
        o = "vertical"
    else:
        o = "fija"
    if volteada_h:
        o += ", volteada horizontal"
    if volteada_v:
        o += ", volteada vertical"
    return o


def real(cuerpo, atributo, tipo="real"):
    m = re.search(r'name="%s">\s*<dia:%s val="([^"]+)"' % (atributo, tipo), cuerpo)
    return m.group(1) if m else ""


def lugar(cuerpo):
    """x, y, ancho, alto de la forma en el diagrama (cm de Dia)"""
    x, _, y = real(cuerpo, "elem_corner", "point").partition(",")
    return [("%.4g" % float(v)) if v else "" for v in (x, y, real(cuerpo, "elem_width"), real(cuerpo, "elem_height"))]


def lee_dia(archivo):
    """las formas del diagrama: [(forma, texto, orientacion, lugar)]"""
    b = open(archivo, "rb").read()
    if b[:2] == b"\x1f\x8b":
        b = gzip.decompress(b)
    t = b.decode("utf-8", "replace")
    res = []
    for m in re.finditer(r'<dia:object type="([^"]+)"[^>]*>(.*?)</dia:object>', t, re.S):
        forma, cuerpo = m.group(1), m.group(2)
        if " - " not in forma or forma.startswith(("Standard -", "UML -")):
            continue
        textos = [s.strip() for s in re.findall(r"<dia:string>#(.*?)#</dia:string>", cuerpo, re.S) if s.strip()]
        vh = bool(re.search(r'name="flip_horizontal">\s*<dia:boolean val="true"', cuerpo))
        vv = bool(re.search(r'name="flip_vertical">\s*<dia:boolean val="true"', cuerpo))
        res.append((forma, textos[0] if textos else "", orientacion(forma, vh, vv), lugar(cuerpo)))
    return res


def ramas_del_circuito(circuito):
    """los nombres de las ramas, con d2datdinc; si no puede, de ELEMENTS"""
    arbol = os.path.join(SIRU, "slex", "d2datdinc")
    prog = os.path.join(arbol, "d2datdinc")
    cmd = ([prog, "-c", os.path.join(arbol, "d2datdinc.slex"), "-b", os.path.join(arbol, "biblioteca")]
           if os.access(prog, os.X_OK) else [shutil.which("d2datdinc") or "d2datdinc"])
    try:
        r = subprocess.run(cmd + [os.path.basename(circuito), "-o", "-"], cwd=os.path.dirname(circuito) or ".",
                           capture_output=True, text=True, timeout=60)
        if r.returncode == 0:
            return set((json.loads(r.stdout).get("GLOBAL") or {}).get("ramas") or {})
    except (OSError, subprocess.TimeoutExpired, ValueError):
        pass
    nombres, en = set(), False
    for l in open(circuito, encoding="utf-8", errors="replace"):
        if re.match(r"^\s*ELEMENTS\s*$", l, re.I):
            en = True
        elif re.match(r"^\s*[A-Z][A-Z ]+\s*$", l):
            en = False
        elif en:
            m = re.match(r"^\s*(?:[A-Za-z0-9_]+-)?([A-Za-z0-9_]+)\s*,", l)
            if m:
                nombres.add(m.group(1))
    return nombres


def nombres_de(formas, ramas):
    """(forma, texto) -> nombre de la rama: la letra mas el texto, o el
    texto si ya es el nombre de una rama"""
    res = []
    for forma, texto, orient, sitio in formas:
        l = letra(forma)
        if not l or not texto:
            continue
        cand = l + texto
        if cand not in ramas and texto in ramas:
            cand = texto
        res.append((cand, forma, orient, tuple(sitio)))
    return res


def formas_de(dia):
    """escribe NOMBRE.formas junto a NOMBRE.dia; devuelve (archivo, ramas
    con forma, ramas del circuito o None si no esta) o el motivo"""
    base = os.path.splitext(dia)[0]
    circuito = next((base + e for e in (".d", ".ds") if os.path.exists(base + e)), None)
    ramas = ramas_del_circuito(circuito) if circuito else None
    formas = lee_dia(dia)
    if ramas:
        pares = sorted({x for x in nombres_de(formas, ramas) if x[0] in ramas})
    else:
        pares = sorted(set(nombres_de(formas, set())))
    if not pares:
        return "el .dia no tiene formas de ramas"
    masas = [(f, o, s) for f, t, o, s in formas if es_masa(f)]
    nodos = sorted(((t, f, o, s) for f, t, o, s in formas if f.split(" - ", 1)[-1] in ("nodo", "nodod") and t),
                   key=lambda x: x[0])
    salida = base + ".formas"
    with open(salida, "w", encoding="utf-8") as o:
        o.write("# formas de Dia de %s, de las ramas de %s (dia2formas)\n"
                % (os.path.basename(dia), os.path.basename(circuito) if circuito else "su circuito"))
        o.write("# nombre\tforma\torientacion\tx\ty\tancho\talto  (lugar en el diagrama, cm de Dia)\n")
        for r, f, orient, sitio in pares:
            o.write("\t".join([r, f, orient] + list(sitio)) + "\n")
        for f, orient, sitio in masas:
            o.write("\t".join(["masa", f, orient] + list(sitio)) + "\n")
        for t, f, orient, sitio in nodos:
            o.write("\t".join(["nodo " + t, f, orient] + list(sitio)) + "\n")
    return salida, len(pares), (len(ramas) if ramas else None)


def main():
    a = argparse.ArgumentParser(description="Que forma de Dia lleva cada rama de un circuito de Simusol (NOMBRE.formas, de NOMBRE.dia).")
    a.add_argument("dias", nargs="+", help="los .dia (con -r, directorios)")
    a.add_argument("-r", "--recorre", action="store_true", help="los argumentos son directorios: todos sus .dia")
    o = a.parse_args()
    lista = []
    if o.recorre:
        for d in o.dias:
            for raiz, dirs, archivos in os.walk(d):
                dirs[:] = sorted(x for x in dirs if x != ".svn")
                lista += [os.path.join(raiz, x) for x in sorted(archivos) if x.endswith(".dia")]
    else:
        lista = o.dias
    cuenta = {}
    for dia in lista:
        r = formas_de(dia)
        if isinstance(r, tuple):
            cuenta["con formas"] = cuenta.get("con formas", 0) + 1
            if not o.recorre:
                print("%s: %d ramas%s" % (r[0], r[1], " de %d" % r[2] if r[2] is not None else ""))
        else:
            cuenta[r] = cuenta.get(r, 0) + 1
            if not o.recorre:
                print("%s: %s" % (dia, r))
    if o.recorre:
        print("%d .dia: %s" % (len(lista), ", ".join("%d %s" % (n, e) for e, n in sorted(cuenta.items()))))


if __name__ == "__main__":
    main()
