"""お手本の規則を、スライドごとに検査する（2〜8枚目の7枚。規則9個）。
usage: rules.py deck.pptx -> JSON {rule: 合格枚数}"""
import sys, json, re, zipfile
from pptx import Presentation
from lxml import etree
A = "{http://schemas.openxmlformats.org/drawingml/2006/main}"
P = "{http://schemas.openxmlformats.org/presentationml/2006/main}"
C = "{http://schemas.openxmlformats.org/drawingml/2006/chart}"
IN = 914400
ACC, GRAY, BG = "C2410C", "8A8F94", "FAF7F2"

def shapes(sl):
    for s in sl.shapes:
        if s.shape_type == 6:
            for t in s.shapes: yield t
        else: yield s

def runs(shp):
    """(size, color, bold, text) of each run"""
    out = []
    if not shp.has_text_frame: return out
    for p in shp.text_frame.paragraphs:
        for r in p.runs:
            col = None
            rp = r._r.find(A + "rPr")
            if rp is not None:
                c = rp.find(f"{A}solidFill/{A}srgbClr")
                if c is not None: col = c.get("val").upper()
            out.append((r.font.size.pt if r.font.size else None, col, bool(r.font.bold), r.text))
    return out

def fillc(s):
    try:
        if s.shape_type == 1 and s.fill.type == 1: return str(s.fill.fore_color.rgb)
    except Exception: pass
    return None

def theme_shadow(root):
    """p:style の effectRef が0以外で、図形側に effectLst の上書きがない図形があるか（テーマ由来の影）"""
    for sp in root.iter(P + "sp", P + "cxnSp"):
        er = sp.find(f"{P}style/{A}effectRef")
        if er is not None and er.get("idx") not in (None, "0") and sp.find(f"{P}spPr/{A}effectLst") is None:
            return True
    return False

def near(a, b, tol=0.12): return abs(a - b) <= tol

def check(path):
    prs = Presentation(path); z = zipfile.ZipFile(path)
    res = {k: 0 for k in ["bg", "kicker", "title", "accent_bar", "footer", "source", "no_deco", "chart_clean", "body_size"]}
    n = 0; chart_n = 0
    for si, sl in enumerate(prs.slides, 1):
        if si == 1: continue
        n += 1
        xml = z.read(f"ppt/slides/slide{si}.xml")
        root = etree.fromstring(xml)
        # bg
        bg = root.find(f"{P}cSld/{P}bg")
        bgc = bg.find(f".//{A}srgbClr").get("val").upper() if bg is not None and bg.find(f".//{A}srgbClr") is not None else None
        # 背景が塗りの全面矩形でも可
        full = any(fillc(s) == BG and s.left <= 5000 and s.top <= 5000 and s.width >= prs.slide_width - 20000 and s.height >= prs.slide_height - 20000 for s in shapes(sl) if fillc(s))
        res["bg"] += int(bgc == BG or full)
        tb = [s for s in shapes(sl) if s.has_text_frame and s.text_frame.text.strip() and s.top is not None]
        # kicker
        res["kicker"] += int(any(near(s.left/IN, .7, .15) and near(s.top/IN, .35, .15) and any(r[0] == 12 and r[1] == ACC for r in runs(s)) for s in tb))
        res["title"] += int(any(near(s.left/IN, .7, .1) and near(s.top/IN, .65, .1) and any(r[0] == 28 and r[2] for r in runs(s)) for s in tb))
        res["accent_bar"] += int(any(near(s.left/IN, .7, .1) and near(s.top/IN, 1.45, .12) and 0.7 <= s.width/IN <= 1.1 and s.height/IN <= 0.12
                                     and fillc(s) == ACC for s in shapes(sl)))
        foot = [s for s in tb if s.top/IN >= 6.9 and any(r[0] == 10 for r in runs(s))]
        res["footer"] += int(len(foot) >= 2)
        res["source"] += int(any(6.2 <= s.top/IN <= 6.7 and any(r[0] == 12 and r[1] == GRAY for r in runs(s)) for s in tb))
        x = xml.decode("utf8")
        res["no_deco"] += int("outerShdw" not in x and "gradFill" not in x and "roundRect" not in x and not theme_shadow(root))
        # 本文サイズ: 帯（kicker/source/footer）以外の文字で18pt未満がない（表のセルも対象）
        small = 0
        for s in shapes(sl):
            if s.has_text_frame:
                y = s.top/IN if s.top is not None else 0
                if y < 0.6 or 6.2 <= y <= 6.7 or y >= 6.9: continue
                small += sum(1 for r in runs(s) if r[0] and r[0] < 18 and r[3].strip())
            if getattr(s, "has_table", False) and s.has_table:
                for row in s.table.rows:
                    for c in row.cells:
                        for p in c.text_frame.paragraphs:
                            small += sum(1 for r in p.runs if r.font.size and r.font.size.pt < 18 and r.text.strip())
        res["body_size"] += int(small == 0)
        # chart
        for s in shapes(sl):
            if getattr(s, "has_chart", False) and s.has_chart:
                chart_n += 1; cx = s.chart._chartSpace
                ok = cx.find(f".//{C}majorGridlines") is None and cx.find(f".//{C}legend") is None
                atd = cx.find(f"{C}chart/{C}autoTitleDeleted")
                has_title = cx.find(f"{C}chart/{C}title") is not None
                ok = ok and not has_title and cx.find(f".//{C}dLbls") is not None
                res["chart_clean"] += int(ok)
    res["_slides"] = n; res["_charts"] = chart_n
    return res
if __name__ == "__main__": print(json.dumps(check(sys.argv[1]), ensure_ascii=False))
