"""Measure DESIGN.md files against Google's DESIGN.md spec (alpha): tokens, sections, recommended names, contrast.

Input: a folder of DESIGN.md files (one per brand, e.g. from https://github.com/VoltAgent/awesome-design-md).
Spec rules come from https://github.com/google-labs-code/design.md (packages/cli/src/linter/spec-config.yaml).
Standard library plus PyYAML.
"""
import glob
import json
import os
import re
import statistics
import sys

import yaml

SECTIONS = [("overview", ["overview", "brand & style"]), ("colors", ["colors"]), ("typography", ["typography"]),
            ("layout", ["layout", "layout & spacing"]), ("elevation & depth", ["elevation & depth", "elevation"]),
            ("shapes", ["shapes"]), ("components", ["components"]), ("do's and don'ts", ["do's and don'ts"])]
# WCAG 2 exempts inactive (disabled) controls, and non-text parts (dividers, rings, icons, dots, tracks) fall under
# the 3:1 non-text rule, so they are kept in the data but left out of the 4.5:1 text count.
NOT_TEXT = re.compile(r"disabled|inactive|divider|ring|icon|dot|track|skeleton|rule|border", re.I)
RECOMMENDED_COLORS = ["primary", "secondary", "tertiary", "neutral", "surface", "on-surface", "error"]


def tokens(text):
    """Merge YAML front matter and ```yaml blocks into one token tree, as the spec does."""
    tree, blocks = {}, []
    m = re.match(r"---\n(.*?)\n---", text, re.S)
    if m:
        blocks.append(m.group(1))
    blocks += re.findall(r"```ya?ml\n(.*?)```", text, re.S)
    for b in blocks:
        try:
            d = yaml.safe_load(b)
        except yaml.YAMLError:
            continue
        if isinstance(d, dict):
            tree.update(d)
    return tree


def hexrgb(v):
    v = v.strip().lower()
    if re.fullmatch(r"#[0-9a-f]{3}", v):
        v = "#" + "".join(c * 2 for c in v[1:])
    if re.fullmatch(r"#[0-9a-f]{6}([0-9a-f]{2})?", v):
        return tuple(int(v[i:i + 2], 16) for i in (1, 3, 5))
    return None


def luminance(rgb):
    def ch(c):
        c /= 255
        return c / 12.92 if c <= 0.03928 else ((c + 0.055) / 1.055) ** 2.4
    r, g, b = (ch(c) for c in rgb)
    return 0.2126 * r + 0.7152 * g + 0.0722 * b


def contrast(a, b):
    la, lb = sorted([luminance(a), luminance(b)], reverse=True)
    return (la + 0.05) / (lb + 0.05)


def resolve(value, tree, depth=0):
    """Follow {colors.name} references to a literal value."""
    if not isinstance(value, str) or depth > 10:
        return value
    m = re.fullmatch(r"\{([\w.-]+)\}", value.strip())
    if not m:
        return value
    node = tree
    for part in m.group(1).split("."):
        node = node.get(part) if isinstance(node, dict) else None
    return resolve(node, tree, depth + 1)


def measure(path):
    text = open(path, encoding="utf-8", errors="ignore").read()
    heads = [re.sub(r"^\d+\.\s*", "", h.strip()).lower() for h in re.findall(r"^##\s+(.+)$", text, re.M)]
    found = []
    for canon, names in SECTIONS:
        idx = next((i for i, h in enumerate(heads) if h in names), None)
        found.append((canon, idx))
    present = [c for c, i in found if i is not None]
    order = [i for _, i in found if i is not None]
    tree = tokens(text)
    colors = tree.get("colors") if isinstance(tree.get("colors"), dict) else {}
    pairs = []
    for name, comp in (tree.get("components") or {}).items() if isinstance(tree.get("components"), dict) else []:
        if not isinstance(comp, dict):
            continue
        bg, fg = (hexrgb(str(resolve(comp.get(k), tree) or "")) for k in ("backgroundColor", "textColor"))
        if bg and fg:
            pairs.append({"component": name, "ratio": round(contrast(bg, fg), 2), "text": not NOT_TEXT.search(name)})
    return {
        "brand": os.path.basename(os.path.dirname(path)) if os.path.basename(path) == "DESIGN.md" else os.path.basename(path)[:-3],
        "chars": len(text), "words": len(text.split()), "approxTokens": round(len(text) / 4),
        "hasYamlTokens": bool(colors), "version": tree.get("version"),
        "specSectionsPresent": len(present), "specSectionsInOrder": order == sorted(order),
        "extraSections": [h for h in heads if not any(h in n for _, n in SECTIONS)],
        "colorTokens": len(colors), "recommendedColorNames": sum(1 for c in RECOMMENDED_COLORS if c in colors),
        "typographyTokens": len(tree.get("typography") or {}) if isinstance(tree.get("typography"), dict) else 0,
        "componentTokens": len(tree.get("components") or {}) if isinstance(tree.get("components"), dict) else 0,
        "hexValuesInText": len(set(x.lower() for x in re.findall(r"#[0-9a-fA-F]{6}\b", text))),
        "textOnBackgroundPairs": pairs,
    }


def main():
    rows = [measure(p) for p in sorted(glob.glob(os.path.join(sys.argv[1], "*.md")))]
    pairs = [p for r in rows for p in r["textOnBackgroundPairs"]]
    yaml_rows = [r for r in rows if r["hasYamlTokens"]]
    med = lambda rs, k: statistics.median(r[k] for r in rs) if rs else None
    summary = {
        "files": len(rows), "withYamlTokens": len(yaml_rows),
        "medianChars": med(rows, "chars"), "medianApproxTokens": med(rows, "approxTokens"),
        "allEightSpecSections": sum(1 for r in rows if r["specSectionsPresent"] == 8),
        "specSectionsInOrder": sum(1 for r in rows if r["specSectionsInOrder"] and r["specSectionsPresent"] == 8),
        "medianColorTokens": med(yaml_rows, "colorTokens"), "medianTypographyTokens": med(yaml_rows, "typographyTokens"),
        "medianComponentTokens": med(yaml_rows, "componentTokens"),
        "medianRecommendedColorNames": med(yaml_rows, "recommendedColorNames"),
        "filesUsingAllSevenRecommendedColorNames": sum(1 for r in yaml_rows if r["recommendedColorNames"] == 7),
        "textOnBackgroundPairs": len(pairs),
        "textPairs": sum(1 for p in pairs if p["text"]),
        "textPairsBelowAA4_5": sum(1 for p in pairs if p["text"] and p["ratio"] < 4.5),
        "textPairsBelow3": sum(1 for p in pairs if p["text"] and p["ratio"] < 3),
        "filesWithATextPairBelowAA": sum(1 for r in rows if any(p["text"] and p["ratio"] < 4.5 for p in r["textOnBackgroundPairs"])),
        "filesWithPairs": sum(1 for r in rows if r["textOnBackgroundPairs"]),
    }
    json.dump({"summary": summary, "rows": rows}, sys.stdout, indent=1)


if __name__ == "__main__":
    main()
