#!/usr/bin/env python3
"""Extract full structure of template1.pptx: slides, layouts, masters, theme."""
import re, json, glob, os
from xml.etree import ElementTree as ET

NS = {
    'a': 'http://schemas.openxmlformats.org/drawingml/2006/main',
    'p': 'http://schemas.openxmlformats.org/presentationml/2006/main',
    'r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
}
EMU_IN = 914400

def qn(prefix, tag):
    return '{%s}%s' % (NS[prefix], tag)

def emu2in(v):
    try: return round(int(v) / EMU_IN, 2)
    except: return None

def get_fill(sp_el):
    """Get solid fill color from shape properties."""
    colors = []
    for sf in sp_el.iter(qn('a', 'solidFill')):
        srgb = sf.find(qn('a', 'srgbClr'))
        sch = sf.find(qn('a', 'schemeClr'))
        if srgb is not None:
            colors.append('#' + srgb.get('val'))
        elif sch is not None:
            colors.append('scheme:' + sch.get('val'))
    return colors

def get_text_runs(txbody):
    """Extract paragraphs with runs: text, size, bold, color, font."""
    paras = []
    if txbody is None: return paras
    for p in txbody.findall(qn('a', 'p')):
        runs = []
        for r in p.findall(qn('a', 'r')):
            t = r.find(qn('a', 't'))
            rpr = r.find(qn('a', 'rPr'))
            run = {'text': t.text if t is not None and t.text else ''}
            if rpr is not None:
                if rpr.get('sz'): run['size'] = int(rpr.get('sz')) / 100
                if rpr.get('b') == '1': run['bold'] = True
                sf = rpr.find(qn('a', 'solidFill'))
                if sf is not None:
                    srgb = sf.find(qn('a', 'srgbClr'))
                    sch = sf.find(qn('a', 'schemeClr'))
                    if srgb is not None: run['color'] = '#' + srgb.get('val')
                    elif sch is not None: run['color'] = 'scheme:' + sch.get('val')
                lat = rpr.find(qn('a', 'latin'))
                if lat is not None and lat.get('typeface') and not lat.get('typeface').startswith('+'):
                    run['font'] = lat.get('typeface')
            runs.append(run)
        # paragraph alignment
        ppr = p.find(qn('a', 'pPr'))
        para = {'runs': runs}
        if ppr is not None and ppr.get('algn'): para['align'] = ppr.get('algn')
        if runs: paras.append(para)
    return paras

def parse_shape(sp_el, rels):
    """Parse a single shape (sp, pic, graphicFrame, grpSp)."""
    out = {}
    cNvPr = sp_el.find('.//' + qn('p', 'cNvPr'))
    if cNvPr is None:
        cNvPr = sp_el.find('.//' + qn('a', 'cNvPr'))
    if cNvPr is not None:
        out['name'] = cNvPr.get('name')

    xfrm = sp_el.find('.//' + qn('a', 'xfrm'))
    if xfrm is not None:
        off = xfrm.find(qn('a', 'off'))
        ext = xfrm.find(qn('a', 'ext'))
        if off is not None:
            out['x'] = emu2in(off.get('x'))
            out['y'] = emu2in(off.get('y'))
        if ext is not None:
            out['w'] = emu2in(ext.get('cx'))
            out['h'] = emu2in(ext.get('cy'))

    # geometry
    geom = sp_el.find('.//' + qn('a', 'prstGeom'))
    if geom is not None:
        out['geometry'] = geom.get('prst')

    fill = get_fill(sp_el)
    if fill: out['fill'] = fill

    # line
    ln = sp_el.find(qn('a', 'ln'))
    if ln is not None:
        lfill = get_fill(ln)
        if lfill: out['line'] = lfill

    # text
    txbody = sp_el.find(qn('p', 'txBody'))
    if txbody is None:
        txbody = sp_el.find(qn('a', 'txBody'))
    if txbody is not None:
        paras = get_text_runs(txbody)
        if paras: out['text'] = paras

    # picture rel
    blip = sp_el.find('.//' + qn('a', 'blip'))
    if blip is not None and rels:
        rid = blip.get(qn('r', 'embed'))
        if rid and rid in rels:
            out['image'] = rels[rid]

    # chart
    chart = sp_el.find('.//' + qn('a', 'chart'))
    if chart is not None and rels:
        rid = chart.get(qn('r', 'id'))
        if rid and rid in rels:
            out['chart'] = rels[rid]

    return out

def parse_slide(path, rels_path):
    rels = {}
    if os.path.exists(rels_path):
        tree = ET.parse(rels_path)
        for rel in tree.getroot():
            rels[rel.get('Id')] = rel.get('Target').replace('../', '')
    tree = ET.parse(path)
    root = tree.getroot()
    shapes = []
    def walk(el, depth=0):
        tag = el.tag.split('}')[-1]
        if tag in ('sp', 'pic', 'graphicFrame'):
            s = parse_shape(el, rels)
            s['tag'] = tag
            if depth > 0: s['depth'] = depth
            shapes.append(s)
        elif tag == 'grpSp':
            shapes.append({'tag': 'grpSp', **parse_shape(el, rels)})
            for child in el:
                walk(child, depth + 1)
        else:
            for child in el:
                walk(child, depth)
    spTree = root.find('.//' + qn('p', 'spTree'))
    if spTree is not None:
        for child in spTree:
            walk(child)
    return shapes

def parse_all():
    base = '_extract'
    result = {'slides': []}
    for i in range(1, 17):
        path = f'{base}/ppt/slides/slide{i}.xml'
        rels = f'{base}/ppt/slides/_rels/slide{i}.xml.rels'
        result['slides'].append({'slide': i, 'shapes': parse_slide(path, rels)})
    # layouts
    result['layouts'] = []
    for lf in sorted(glob.glob(f'{base}/ppt/slideLayouts/slideLayout*.xml')):
        num = re.search(r'slideLayout(\d+)', lf).group(1)
        rels = f'{base}/ppt/slideLayouts/_rels/slideLayout{num}.xml.rels'
        r = {}
        if os.path.exists(rels):
            tree = ET.parse(rels)
            for rel in tree.getroot():
                r[rel.get('Id')] = rel.get('Target').replace('../', '')
        # layout name
        tree = ET.parse(lf)
        name = None
        for cNvPr in tree.getroot().findall('.//' + qn('p', 'cNvPr')):
            name = cNvPr.get('name'); break
        shapes = parse_slide(lf, rels)
        result['layouts'].append({'file': num, 'name': name, 'shapes': shapes})
    # masters
    result['masters'] = []
    for mf in sorted(glob.glob(f'{base}/ppt/slideMasters/slideMaster*.xml')):
        num = re.search(r'slideMaster(\d+)', mf).group(1)
        rels = f'{base}/ppt/slideMasters/_rels/slideMaster{num}.xml.rels'
        r = {}
        if os.path.exists(rels):
            tree = ET.parse(rels)
            for rel in tree.getroot():
                r[rel.get('Id')] = rel.get('Target').replace('../', '')
        shapes = parse_slide(mf, rels)
        result['masters'].append({'file': num, 'shapes': shapes})
    return result

if __name__ == '__main__':
    data = parse_all()
    with open('_extract/deck_structure.json', 'w', encoding='utf-8') as f:
        json.dump(data, f, ensure_ascii=False, indent=1)
    # summary
    for s in data['slides']:
        texts = []
        for sh in s['shapes']:
            if 'text' in sh:
                for p in sh['text']:
                    texts.append(''.join(r['text'] for r in p['runs']))
        print(f"--- Slide {s['slide']}: {len(s['shapes'])} shapes")
        for t in texts[:12]:
            if t.strip(): print(f"   {t[:80]}")
