"""Рендерит представления из .archimate в один самодостаточный HTML со встроенным SVG.
Нужен, чтобы схемы можно было посмотреть без установки Archi."""
import sys, io, pathlib, html, re
import xml.etree.ElementTree as ET

sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
XSI = "{http://www.w3.org/2001/XMLSchema-instance}type"
src, dst = pathlib.Path(sys.argv[1]), pathlib.Path(sys.argv[2])
root = ET.parse(src).getroot()

LAYER = {  # цвета Archi по слоям
    "ApplicationComponent": "#B5FFFF", "ApplicationService": "#B5FFFF",
    "Node": "#C9E7B7", "SystemSoftware": "#C9E7B7",
    "TechnologyService": "#C9E7B7", "Artifact": "#C9E7B7",
    "Grouping": "#F4F4F4",
}
STROKE = {"Application": "#2a7f8f", "Technology": "#4a7f3a", "Grouping": "#888"}

concepts, rels = {}, {}
for folder in root.findall("folder"):
    for e in folder.findall("element"):
        et = (e.get(XSI) or "").split(":")[-1]
        if et.endswith("Relationship"):
            rels[e.get("id")] = (et[:-len("Relationship")], e.get("source"), e.get("target"))
        elif et != "ArchimateDiagramModel":
            concepts[e.get("id")] = (et, e.get("name") or "",
                                     (e.findtext("documentation") or "").strip())

def collect(node, ox, oy, nodes, conns, notes):
    b = node.find("bounds")
    x, y = ox + int(b.get("x", 0)), oy + int(b.get("y", 0))
    w, h = int(b.get("width", 120)), int(b.get("height", 55))
    if (node.get(XSI) or "") == "archimate:Note":
        notes.append((node.findtext("content") or "", x, y, w, h))
        return
    nodes[node.get("id")] = (node.get("archimateElement"), x, y, w, h)
    for c in node.findall("sourceConnection"):
        conns.append((c.get("relationship"), c.get("source"), c.get("target")))
    for ch in node.findall("child"):
        collect(ch, x, y, nodes, conns, notes)

def tooltip(doc, limit=260):
    """Первый абзац описания, обрезанный по границе предложения: подсказка должна
    давать понять, что это за элемент, а не пересказывать весь текст."""
    s = doc.split("\n\n")[0].replace("\n", " ").replace("`", "").strip()
    if len(s) <= limit:
        return s
    cut = s[:limit]
    end = max(cut.rfind(". "), cut.rfind("; "))
    return (cut[:end + 1] if end > limit // 2 else cut.rstrip() + "…")

def paragraphs(doc):
    return [p.replace("\n", " ").strip() for p in doc.split("\n\n") if p.strip()]

def rich(p):
    """Описания пришли из markdown-файлов: обратные кавычки там означают код."""
    return re.sub(r"`([^`]+)`", r"<code>\1</code>", html.escape(p))

def wrap(s, width, limit=4):
    out, line = [], ""
    for word in s.split():
        if len(line) + len(word) + 1 > width and line:
            out.append(line); line = word
        else:
            line = f"{line} {word}".strip()
    if line:
        out.append(line)
    return out[:limit]

def edge_point(x, y, w, h, tx, ty):
    """Точка пересечения луча к (tx,ty) с границей прямоугольника."""
    cx, cy = x + w / 2, y + h / 2
    dx, dy = tx - cx, ty - cy
    if dx == 0 and dy == 0:
        return cx, cy
    sx = (w / 2) / abs(dx) if dx else float("inf")
    sy = (h / 2) / abs(dy) if dy else float("inf")
    s = min(sx, sy)
    return cx + dx * s, cy + dy * s

MARKERS = """
<defs>
  <marker id="open" markerWidth="12" markerHeight="12" refX="10" refY="4" orient="auto">
    <path d="M0,0 L10,4 L0,8" fill="none" stroke="#555" stroke-width="1.4"/></marker>
  <marker id="hollow" markerWidth="14" markerHeight="14" refX="11" refY="5" orient="auto">
    <path d="M0,0 L11,5 L0,10 Z" fill="#fff" stroke="#555" stroke-width="1.2"/></marker>
  <marker id="filled" markerWidth="12" markerHeight="12" refX="10" refY="4" orient="auto">
    <path d="M0,0 L10,4 L0,8 Z" fill="#555"/></marker>
  <marker id="diamond" markerWidth="16" markerHeight="12" refX="1" refY="5" orient="auto">
    <path d="M0,5 L6,1 L13,5 L6,9 Z" fill="#555"/></marker>
  <marker id="ball" markerWidth="10" markerHeight="10" refX="2" refY="5" orient="auto">
    <circle cx="4" cy="5" r="3.2" fill="#555"/></marker>
  <marker id="odiamond" markerWidth="16" markerHeight="12" refX="1" refY="5" orient="auto">
    <path d="M0,5 L6,1 L13,5 L6,9 Z" fill="#fff" stroke="#555" stroke-width="1.2"/></marker>
</defs>"""

STYLE = {
    "Serving":     ('stroke-dasharray="none"', 'marker-end="url(#open)"', ""),
    "Realization": ('stroke-dasharray="6,4"', 'marker-end="url(#hollow)"', ""),
    "Composition": ('stroke-dasharray="none"', "", 'marker-start="url(#diamond)"'),
    "Assignment":  ('stroke-dasharray="none"', 'marker-end="url(#filled)"', 'marker-start="url(#ball)"'),
    "Aggregation": ('stroke-dasharray="none"', "", 'marker-start="url(#odiamond)"'),
}

parts = []
views = [e for f in root.findall("folder") for e in f.findall("element")
         if (e.get(XSI) or "").endswith("ArchimateDiagramModel")]

for v in views:
    nodes, conns, notes = {}, [], []
    for ch in v.findall("child"):
        collect(ch, 0, 0, nodes, conns, notes)
    if not nodes:
        continue
    boxes = [(x, y, w, h) for _, x, y, w, h in nodes.values()] + \
            [(x, y, w, h) for _, x, y, w, h in notes]
    maxx = max(x + w for x, _, w, _ in boxes) + 40
    maxy = max(y + h for _, y, _, h in boxes) + 40

    svg = [f'<svg viewBox="0 0 {maxx} {maxy}" xmlns="http://www.w3.org/2000/svg" '
           f'font-family="system-ui, sans-serif">', MARKERS]

    # связи под блоками
    for rid, so, to in conns:
        if so not in nodes or to not in nodes or rid not in rels:
            continue
        rtype = rels[rid][0]
        _, sx, sy, sw, sh = nodes[so]
        _, tx, ty, tw, th = nodes[to]
        x1, y1 = edge_point(sx, sy, sw, sh, tx + tw / 2, ty + th / 2)
        x2, y2 = edge_point(tx, ty, tw, th, sx + sw / 2, sy + sh / 2)
        dash, mend, mstart = STYLE.get(rtype, ('stroke-dasharray="none"', 'marker-end="url(#open)"', ""))
        svg.append(f'<line x1="{x1:.0f}" y1="{y1:.0f}" x2="{x2:.0f}" y2="{y2:.0f}" '
                   f'stroke="#555" stroke-width="1.3" opacity="0.75" {dash} {mend} {mstart}/>')

    # блоки: контейнеры раньше содержимого — сортируем по площади убыв.
    for nid, (eref, x, y, w, h) in sorted(nodes.items(), key=lambda kv: -kv[1][3] * kv[1][4]):
        etype, name, doc = concepts.get(eref, ("", eref or "?", ""))
        fill = LAYER.get(etype, "#EEEEEE")
        stroke = (STROKE["Grouping"] if etype == "Grouping" else
                  STROKE["Application"] if etype.startswith("Application") else STROKE["Technology"])
        big = w * h > 60000
        # описание элемента — всплывающей подсказкой: на схеме ему места нет,
        # а в модели оно есть, иначе его видно только в Archi
        svg.append("<g>" + (f'<title>{html.escape(tooltip(doc))}</title>' if doc else ""))
        svg.append(f'<rect x="{x}" y="{y}" width="{w}" height="{h}" rx="4" fill="{fill}" '
                   f'fill-opacity="{0.35 if big else 1}" stroke="{stroke}" stroke-width="1.2"/>')
        lines = wrap(name, max(12, w // 7))
        if big:  # подпись контейнера — сверху слева
            svg.append(f'<text x="{x+10}" y="{y+20}" font-size="13" font-weight="600" '
                       f'fill="#1a3a1a">{html.escape(name)}</text>')
        else:
            fs = 11 if len(lines) > 2 else 12
            y0 = y + h / 2 - (len(lines) - 1) * fs * 0.62
            for i, ln in enumerate(lines):
                svg.append(f'<text x="{x + w/2}" y="{y0 + i*fs*1.25:.0f}" font-size="{fs}" '
                           f'text-anchor="middle" dominant-baseline="middle" fill="#111">'
                           f'{html.escape(ln)}</text>')
        svg.append("</g>")

    # надписи — поверх всего, без заливки слоя: это не элементы модели
    for content, x, y, w, h in notes:
        svg.append(f'<rect x="{x}" y="{y}" width="{w}" height="{h}" rx="3" fill="#fffbe8" '
                   f'stroke="#d8cfa8" stroke-width="1"/>')
        for i, ln in enumerate(wrap(content, max(16, int(w / 6.2)), limit=int(h // 15))):
            svg.append(f'<text x="{x+9}" y="{y + 17 + i*14}" font-size="11" fill="#4a4632">'
                       f'{html.escape(ln)}</text>')
    svg.append("</svg>")

    doc = v.find("documentation")
    parts.append(f'<section id="{v.get("id")}"><h2>{html.escape(v.get("name") or "")}</h2>'
                 + (f'<p class="doc">{html.escape(doc.text)}</p>' if doc is not None and doc.text else "")
                 + "".join(svg) + "</section>")

toc = "".join(f'<a href="#{v.get("id")}">{html.escape(v.get("name") or "")}</a>' for v in views)
purpose = (root.findtext("purpose") or "").strip()

# ---------------------------------------------------------------- прикладной слой
# Описания компонентов лежат в модели, но на схемах их не видно: там помещается
# только имя. Поэтому — отдельным справочником, в том же порядке, что и на карте A0.
APP = "ApplicationComponent"
groups, seen = [], set()
for gid, (etype, gname, gdoc) in concepts.items():
    if etype != "Grouping":
        continue
    members = [t for rtype, s, t in rels.values()
               if rtype == "Aggregation" and s == gid and concepts.get(t, ("",))[0] == APP]
    if members:
        seen.update(members)
        groups.append((gname, gdoc, sorted(members, key=lambda i: concepts[i][1])))
rest = sorted((i for i, c in concepts.items() if c[0] == APP and i not in seen),
              key=lambda i: concepts[i][1])
if rest:
    groups.append(("Вне групп", "", rest))

app_html = ""
if groups:
    total = sum(len(m) for _, _, m in groups)
    app_html = ('<section id="app-layer"><h2>Прикладной слой: описания компонентов</h2>'
                f'<p class="doc">{total} компонентов в том же порядке, что и на карте A0. '
                'Описания выведены не из манифестов контура, а из исходного кода компонентных '
                'репозиториев, и потому проверяются чтением кода, а не сверкой с конфигурацией. '
                'То же описание всплывает подсказкой, если задержать курсор на блоке любой схемы.</p>')
    for gname, gdoc, members in groups:
        app_html += f'<h3>{html.escape(gname)}</h3>'
        for p in paragraphs(gdoc):
            app_html += f'<p class="doc">{rich(p)}</p>'
        app_html += '<dl class="apps">'
        for i in members:
            _, name, doc = concepts[i]
            # запасное значение сборщика — путь к манифестам, а не описание
            body = ("" if doc.startswith("apps/") else
                    "".join(f"<p>{rich(p)}</p>" for p in paragraphs(doc))) or \
                   f'<p class="none">описание не заполнено — см. {html.escape(doc or "манифесты")}</p>'
            app_html += f'<dt>{html.escape(name)}</dt><dd>{body}</dd>'
        app_html += "</dl>"
    app_html += "</section>"

page = f"""<!doctype html>
<html lang="ru"><meta charset="utf-8">
<title>{html.escape(root.get('name') or 'Модель')}</title>
<style>
 body {{ font-family: system-ui, sans-serif; margin: 0 auto; padding: 24px; max-width: 1760px;
        background:#fafafa; color:#111; }}
 h1 {{ font-size: 22px; }} h2 {{ font-size: 16px; margin: 0 0 4px; }}
 h3 {{ font-size: 14px; margin: 22px 0 6px; }}
 .doc {{ color:#555; font-size:13px; margin:0 0 10px; max-width:900px; }}
 section {{ background:#fff; border:1px solid #ddd; border-radius:6px; padding:14px; margin-bottom:18px;
            overflow-x:auto; scroll-margin-top:12px; }}
 svg {{ width:100%; height:auto; min-width:700px; }}
 .legend {{ font-size:13px; color:#444; max-width:900px; }}
 .legend code {{ background:#eee; padding:1px 5px; border-radius:3px; }}
 nav {{ display:flex; flex-wrap:wrap; gap:6px; margin:14px 0 22px; }}
 nav a {{ font-size:12px; color:#245; text-decoration:none; background:#fff; border:1px solid #ddd;
          border-radius:4px; padding:3px 8px; }}
 nav a:hover {{ background:#eef4f8; }}
 dl.apps {{ margin:0; }}
 dl.apps dt {{ font-weight:600; font-size:13px; margin-top:14px; }}
 dl.apps dd {{ margin:2px 0 0; padding-left:0; color:#333; font-size:13px; max-width:900px; }}
 dl.apps dd p {{ margin:0 0 6px; }}
 dl.apps .none {{ color:#999; font-style:italic; }}
 code {{ background:#f0f0f0; padding:1px 4px; border-radius:3px; font-size:12px; }}
</style>
<h1>{html.escape(root.get('name') or '')}</h1>
<p class="legend">Автогенерация из <code>{html.escape(src.name)}</code>. Зелёный — технологический слой,
голубой — прикладной. Пунктир с полым треугольником — realization, стрелка — serving,
ромб — composition, линия с шариком — assignment, полый ромб — aggregation. Подробнее — в представлении
«L1. Как читать схемы». Описание элемента всплывает подсказкой при наведении на блок.</p>
{''.join(f'<p class="legend">{html.escape(p)}</p>' for p in paragraphs(purpose))}
<nav>{toc}{'<a href="#app-layer">Прикладной слой: описания</a>' if app_html else ''}</nav>
{''.join(parts)}
{app_html}
</html>"""

dst.write_text(page, encoding="utf-8", newline="\n")
print(f"записано: {dst}  ({len(parts)} представлений)")
