from pptx import Presentation
from pptx.util import Inches, Pt, Emu
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR, MSO_AUTO_SIZE
from pptx.enum.shapes import MSO_SHAPE
from pptx.dml.color import RGBColor
from pptx.enum.dml import MSO_THEME_COLOR
from io import BytesIO
from pathlib import Path
import re, json, math, hashlib
from PIL import Image

SRC = Path('/Users/bot1/Volumes/root_for_ai/AI工作区/国博_PPT重设计_IP授权介绍_20260611_1242/source/0428 中国国家博物馆IP授权介绍.pptx')
OUT = Path('/Users/bot1/Volumes/root_for_ai/AI工作区/国博_PPT重设计_IP授权介绍_20260611_1242/deliverables/国博IP授权介绍_全新视觉重设计_v1.pptx')
MANIFEST = Path('/Users/bot1/Volumes/root_for_ai/AI工作区/国博_PPT重设计_IP授权介绍_20260611_1242/work/slide_manifest.json')

# Palette: museum night + ivory + bronze-gold + vermilion
C_DARK = '180F0B'
C_DARK2 = '24140F'
C_IVORY = 'F8F0DF'
C_PAPER = 'FFF8E9'
C_GOLD = 'B99054'
C_GOLD2 = 'D6B878'
C_RED = '8E2420'
C_RED2 = 'B44432'
C_INK = '2D2018'
C_MUTED = '7C6A56'
C_GREY = 'E7D8BC'

W_IN, H_IN = 13.333333, 7.5
W, H = Inches(W_IN), Inches(H_IN)
prs_src = Presentation(str(SRC))
prs = Presentation()
prs.slide_width = W
prs.slide_height = H
blank = prs.slide_layouts[6]

hash_re = re.compile(r'^[0-9a-fA-F]{48,}[0-9a-fA-F]*$')

def rgb(hexstr):
    hexstr = hexstr.strip('#')
    return RGBColor(int(hexstr[:2],16), int(hexstr[2:4],16), int(hexstr[4:6],16))

def clean_line(t):
    t = t.replace('\u200b','').replace('\ufeff','')
    t = re.sub(r'\s+', ' ', t).strip()
    return t

def is_noise(t):
    s = clean_line(t)
    if not s: return True
    if len(s) > 80:
        alnum = re.sub(r'[^0-9a-fA-F]', '', s)
        if len(alnum) / max(len(s),1) > 0.9:
            return True
    if hash_re.match(s): return True
    return False

def slide_texts(slide):
    out=[]
    seen=set()
    for sh in slide.shapes:
        if getattr(sh, 'has_text_frame', False) and sh.has_text_frame:
            for par in sh.text_frame.paragraphs:
                runs = ''.join(run.text for run in par.runs) if par.runs else par.text
                # Some original text boxes contain hard line breaks
                for piece in re.split(r'[\n\r]+', runs):
                    s = clean_line(piece)
                    if not is_noise(s) and s not in seen:
                        out.append(s); seen.add(s)
    return out

def get_pictures(slide):
    pics=[]
    for idx, sh in enumerate(slide.shapes):
        if sh.shape_type == 13:  # PICTURE
            try:
                blob = sh.image.blob
                h = hashlib.sha1(blob).hexdigest()[:12]
                im = Image.open(BytesIO(blob))
                iw, ih = im.size
                ext = sh.image.ext or 'png'
                pics.append({
                    'idx': idx, 'blob': blob, 'ext': ext, 'sha': h,
                    'w': iw, 'h': ih,
                    'left': int(sh.left), 'top': int(sh.top), 'width': int(sh.width), 'height': int(sh.height),
                    'area': int(sh.width) * int(sh.height),
                    'crop': (float(getattr(sh,'crop_left',0) or 0), float(getattr(sh,'crop_top',0) or 0), float(getattr(sh,'crop_right',0) or 0), float(getattr(sh,'crop_bottom',0) or 0))
                })
            except Exception:
                pass
    pics.sort(key=lambda x: x['area'], reverse=True)
    return pics

def add_rect(slide, x,y,w,h, fill, line=None, alpha=None, radius=False):
    shape_type = MSO_SHAPE.ROUNDED_RECTANGLE if radius else MSO_SHAPE.RECTANGLE
    sp = slide.shapes.add_shape(shape_type, Inches(x), Inches(y), Inches(w), Inches(h))
    sp.fill.solid(); sp.fill.fore_color.rgb = rgb(fill)
    if alpha is not None:
        sp.fill.transparency = alpha
    if line:
        sp.line.color.rgb = rgb(line); sp.line.width = Pt(1.1)
    else:
        sp.line.fill.background()
    return sp

def add_line(slide, x1,y1,x2,y2, color=C_GOLD, width=1.2):
    ln = slide.shapes.add_connector(1, Inches(x1), Inches(y1), Inches(x2), Inches(y2))
    ln.line.color.rgb = rgb(color); ln.line.width = Pt(width)
    return ln

def add_text(slide, text, x,y,w,h, size=18, color=C_INK, bold=False, font='PingFang SC', align='left', valign='top', italic=False, spacing=False):
    tb = slide.shapes.add_textbox(Inches(x), Inches(y), Inches(w), Inches(h))
    tf = tb.text_frame
    tf.clear(); tf.word_wrap = True; tf.margin_left = Inches(0.03); tf.margin_right = Inches(0.03); tf.margin_top = Inches(0.02); tf.margin_bottom = Inches(0.02)
    tf.vertical_anchor = {'top':MSO_ANCHOR.TOP,'mid':MSO_ANCHOR.MIDDLE,'bottom':MSO_ANCHOR.BOTTOM}.get(valign, MSO_ANCHOR.TOP)
    tf.auto_size = MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE
    lines = str(text).split('\n')
    for i, line in enumerate(lines):
        p = tf.paragraphs[0] if i==0 else tf.add_paragraph()
        p.text = line
        p.alignment = {'left':PP_ALIGN.LEFT,'center':PP_ALIGN.CENTER,'right':PP_ALIGN.RIGHT}.get(align, PP_ALIGN.LEFT)
        if spacing: p.space_after = Pt(4)
        for r in p.runs:
            r.font.name = font
            r.font.size = Pt(size)
            r.font.bold = bold
            r.font.italic = italic
            r.font.color.rgb = rgb(color)
    return tb

def add_pill(slide, text, x,y,w,h, fill=C_RED, color=C_IVORY, size=11):
    add_rect(slide,x,y,w,h,fill,line=fill,radius=True)
    return add_text(slide,text,x+0.05,y+0.02,w-0.1,h-0.04,size=size,color=color,bold=True,align='center',valign='mid')

def add_pic_contain(slide, pic, x,y,w,h, border=True, bg=None):
    if bg:
        add_rect(slide,x,y,w,h,bg,line=C_GOLD,alpha=0,radius=True)
    iw, ih = pic['w'], pic['h']
    if iw <= 0 or ih <= 0: return None
    scale = min(w/iw, h/ih)
    nw, nh = iw*scale, ih*scale
    px, py = x + (w-nw)/2, y + (h-nh)/2
    stream = BytesIO(pic['blob']); stream.seek(0)
    shp = slide.shapes.add_picture(stream, Inches(px), Inches(py), width=Inches(nw), height=Inches(nh))
    if border:
        frame = add_rect(slide,x,y,w,h,'FFFFFF',line=C_GOLD,alpha=100,radius=True)
        # push frame behind picture is hard; leave no-fill border-like via transparency 100
        frame.line.color.rgb = rgb(C_GOLD); frame.line.width = Pt(1)
    return shp

def background(slide, dark=False):
    if dark:
        add_rect(slide,0,0,W_IN,H_IN,C_DARK)
        add_rect(slide,0,0,W_IN,0.18,C_RED)
        add_rect(slide,0,H_IN-0.18,W_IN,0.18,C_RED)
        # subtle gold diagonals
        add_line(slide,10.8,0.4,13.1,2.2,C_GOLD,0.7)
        add_line(slide,11.3,0.25,13.2,1.7,C_GOLD2,0.5)
    else:
        add_rect(slide,0,0,W_IN,H_IN,C_IVORY)
        add_rect(slide,0,0,0.18,H_IN,C_RED)
        add_rect(slide,0,0,W_IN,0.12,C_GOLD)
        add_rect(slide,11.95,0,1.38,H_IN,C_DARK2)
        add_line(slide,0.55,6.95,11.45,6.95,C_GOLD,0.8)

def footer(slide, i, dark=False):
    col = C_GOLD2 if dark else C_MUTED
    add_text(slide, 'National Museum of China', 0.55, 7.02, 4.0, 0.23, size=7.5, color=col)
    add_text(slide, f'{i:02d}', 12.25, 6.95, 0.55, 0.25, size=9, color=C_GOLD2 if dark else C_IVORY, bold=True, align='right')

def infer_title(lines, slide_no):
    if not lines: return ''
    # keep cover/section title from first meaningful line
    for s in lines:
        if s in ('National Museum of China',) or s.startswith('/'):
            continue
        if len(s) <= 30:
            return s
    return lines[0][:30]

def classify(lines, pics, no):
    joined=' '.join(lines)
    if no in (1,54): return 'cover'
    if any('/  0' in s or s.strip().startswith('/  0') for s in lines) or no in (3,7,33,37): return 'section'
    if no in (34,35,36): return 'gallery'
    if 38 <= no <= 52: return 'case'
    if no == 53: return 'timeline'
    if len(pics) <= 2 and len(lines) <= 6: return 'opener'
    if len(pics) >= 7 or len(lines) >= 6: return 'grid'
    return 'mixed'

def split_title_body(lines):
    title = infer_title(lines, 0)
    rest = [l for l in lines if l != title and l != 'National Museum of China']
    return title, rest

def add_text_panel(slide, lines, x,y,w,h, title=None, max_lines=None, dark=False):
    col_title = C_GOLD2 if dark else C_RED
    col_body = C_IVORY if dark else C_INK
    if title:
        add_text(slide,title,x,y,w,0.38,size=18,color=col_title,bold=True)
        y += 0.48; h -= 0.48
    if max_lines:
        lines = lines[:max_lines]
    if not lines: return
    # dynamic font by text volume
    chars = sum(len(s) for s in lines)
    size = 12
    if chars > 500: size = 7.6
    elif chars > 330: size = 8.6
    elif chars > 180: size = 10
    txt='\n'.join(lines)
    add_text(slide,txt,x,y,w,h,size=size,color=col_body,spacing=True)

def layout_images_grid(slide, pics, x,y,w,h, max_pics=8, bg=C_PAPER):
    if not pics: return
    pics = pics[:max_pics]
    n=len(pics)
    cols = 1 if n==1 else 2 if n<=4 else 3 if n<=9 else 4 if n<=16 else 5 if n<=25 else 6
    rows = math.ceil(n/cols)
    gap=0.13
    cw=(w-gap*(cols-1))/cols
    ch=(h-gap*(rows-1))/rows
    for idx,p in enumerate(pics):
        cx=x+(idx%cols)*(cw+gap); cy=y+(idx//cols)*(ch+gap)
        add_rect(slide,cx,cy,cw,ch,bg,line=C_GOLD,alpha=0,radius=True)
        add_pic_contain(slide,p,cx+0.05,cy+0.05,cw-0.1,ch-0.1,border=False)

def add_extra_thumbs(slide, pics, x, y, w, h, bg=C_PAPER):
    """Show remaining decorative/logo images as a small strip so every original picture is preserved."""
    if not pics:
        return
    n=len(pics)
    gap=0.08
    cols=min(n, max(1, int(w/0.72)))
    rows=math.ceil(n/cols)
    cw=(w-gap*(cols-1))/cols
    ch=(h-gap*(rows-1))/rows
    for idx,p in enumerate(pics):
        cx=x+(idx%cols)*(cw+gap); cy=y+(idx//cols)*(ch+gap)
        add_rect(slide,cx,cy,cw,ch,bg,line=C_GOLD,alpha=0,radius=True)
        add_pic_contain(slide,p,cx+0.03,cy+0.03,max(0.1,cw-0.06),max(0.1,ch-0.06),border=False)

def cover_slide(slide, lines, pics, no):
    background(slide, dark=True)
    if no == 54:
        add_text(slide,'千年风华 携手共启',0.82,1.62,6.2,0.85,size=40,color=C_IVORY,bold=True,font='Songti SC')
        add_line(slide,0.88,2.78,4.85,2.78,C_GOLD,2.2)
        end_lines=[l for l in lines if l!='National Museum of China' and l!='千年风华 携手共启']
        if end_lines:
            add_text(slide,'\n'.join(end_lines),0.88,3.18,5.2,1.0,size=18,color=C_GOLD2,spacing=True)
        if pics:
            add_rect(slide,7.05,0.68,5.45,5.9,C_PAPER,line=C_GOLD,radius=True)
            add_pic_contain(slide,pics[0],7.25,0.9,5.05,4.7 if len(pics)>1 else 5.45,border=False)
            add_extra_thumbs(slide,pics[1:],7.35,5.75,4.8,0.55)
        add_text(slide,'National Museum of China',0.9,6.35,3.2,0.3,size=9,color=C_GOLD2)
        footer(slide,no,dark=True)
        return
    title='\n'.join([l for l in lines[:3] if l!='National Museum of China']) or '中国国家博物馆\nIP授权介绍'
    add_text(slide,'中国国家博物馆',0.85,0.75,5.9,0.45,size=18,color=C_GOLD2,bold=True)
    add_text(slide,'National Museum of China',0.85,1.23,4.2,0.28,size=10,color=C_IVORY)
    add_text(slide,'IP授权介绍',0.82,2.05,5.8,1.0,size=42,color=C_IVORY,bold=True,font='Songti SC')
    add_line(slide,0.88,3.22,4.85,3.22,C_GOLD,2.2)
    add_text(slide,'以馆藏文物为源点，构建东方美学与当代商业的授权叙事',0.88,3.48,5.0,0.7,size=16,color=C_GOLD2)
    if pics:
        add_rect(slide,7.05,0.68,5.45,5.9,C_PAPER,line=C_GOLD,radius=True)
        add_pic_contain(slide,pics[0],7.25,0.9,5.05,4.7 if len(pics)>1 else 5.45,border=False)
        add_extra_thumbs(slide,pics[1:],7.35,5.75,4.8,0.55)
    add_text(slide,'NMC IP LICENSING',0.9,6.35,3.2,0.3,size=9,color=C_GOLD2)
    footer(slide,no,dark=True)

def section_slide(slide, lines, pics, no):
    background(slide, dark=True)
    title, rest = split_title_body(lines)
    sec = next((s for s in lines if '/  0' in s or s.startswith('/')), f'/  {no:02d}  /')
    subtitle = '\n'.join([s for s in rest if s != sec][:2])
    add_text(slide,sec,0.85,0.92,2.0,0.28,size=12,color=C_GOLD2,bold=True)
    add_text(slide,title,0.82,1.68,5.4,0.85,size=40,color=C_IVORY,bold=True,font='Songti SC')
    add_line(slide,0.88,2.83,4.45,2.83,C_GOLD,1.8)
    if subtitle:
        add_text(slide,subtitle,0.9,3.1,5.1,0.8,size=16,color=C_GOLD2)
    if pics:
        add_rect(slide,7.0,0.72,5.45,5.86,C_PAPER,line=C_GOLD,radius=True)
        add_pic_contain(slide,pics[0],7.18,0.9,5.09,4.7 if len(pics)>1 else 5.5,border=False)
        add_extra_thumbs(slide,pics[1:],7.35,5.75,4.75,0.55)
    footer(slide,no,dark=True)

def opener_slide(slide, lines, pics, no):
    background(slide, dark=False)
    title, rest=split_title_body(lines)
    add_text(slide,title,0.65,0.62,5.8,0.7,size=30,color=C_RED,bold=True,font='Songti SC')
    if rest:
        add_text_panel(slide,rest,0.72,1.55,5.35,3.7,dark=False)
    if pics:
        add_rect(slide,6.55,0.62,4.95,5.75,C_PAPER,line=C_GOLD,radius=True)
        add_pic_contain(slide,pics[0],6.75,0.82,4.55,4.55 if len(pics)>1 else 5.35,border=False)
        add_extra_thumbs(slide,pics[1:],6.9,5.65,4.25,0.55)
    else:
        add_rect(slide,6.55,0.62,4.95,5.75,C_DARK2,line=C_GOLD,radius=True)
        add_text(slide,'文化资产\n商业转化\n授权共创',7.0,2.1,4.0,1.4,size=28,color=C_GOLD2,bold=True,align='center',valign='mid')
    footer(slide,no,dark=False)

def grid_slide(slide, lines, pics, no):
    background(slide, dark=False)
    title, rest=split_title_body(lines)
    add_pill(slide, f'{no:02d}', 0.55,0.45,0.62,0.32,fill=C_DARK2,color=C_GOLD2,size=9)
    add_text(slide,title,1.25,0.38,6.2,0.55,size=22,color=C_RED,bold=True,font='Songti SC')
    # Decide image/text balance
    if len(pics) >= 7:
        layout_images_grid(slide,pics,0.7,1.18,6.75,5.25,max_pics=len(pics),bg=C_PAPER)
        add_rect(slide,7.72,1.18,3.85,5.25,C_PAPER,line=C_GOLD,radius=True)
        add_text_panel(slide,rest,7.95,1.42,3.38,4.78,dark=False)
    else:
        layout_images_grid(slide,pics,6.7,1.15,4.8,5.25,max_pics=len(pics),bg=C_PAPER)
        add_rect(slide,0.72,1.15,5.55,5.25,C_PAPER,line=C_GOLD,radius=True)
        add_text_panel(slide,rest,0.96,1.4,5.05,4.78,dark=False)
    footer(slide,no,dark=False)

def gallery_slide(slide, lines, pics, no):
    background(slide, dark=True)
    title = infer_title(lines,no)
    add_text(slide,title,0.78,0.48,7.0,0.45,size=22,color=C_GOLD2,bold=True,font='Songti SC')
    if pics:
        add_rect(slide,0.8,1.1,11.55,5.48,C_PAPER,line=C_GOLD,radius=True)
        add_pic_contain(slide,pics[0],1.0,1.28,11.15,4.55 if len(pics)>1 else 5.12,border=False)
        add_extra_thumbs(slide,pics[1:],1.15,5.95,10.85,0.45)
    else:
        add_text(slide,title,1.0,2.7,11.2,0.8,size=34,color=C_IVORY,bold=True,align='center')
    footer(slide,no,dark=True)

def case_slide(slide, lines, pics, no):
    background(slide, dark=False)
    title, rest = split_title_body(lines)
    # case slides: left narrative spine + large artifact/screenshot
    add_rect(slide,0.45,0.42,2.45,6.12,C_DARK2,line=C_GOLD,radius=True)
    add_text(slide,'品牌\n授权',0.78,0.82,1.8,0.9,size=24,color=C_GOLD2,bold=True,align='center',font='Songti SC')
    add_line(slide,0.82,2.05,2.48,2.05,C_GOLD2,1.2)
    body=[s for s in lines if s not in ('National Museum of China',)]
    if body:
        add_text(slide,'\n'.join(body[:4]),0.72,2.35,1.95,2.2,size=11,color=C_IVORY,bold=False,align='center')
    add_text(slide,'跨界合作\n助力品牌升级',0.72,5.1,1.95,0.75,size=12,color=C_GOLD2,bold=True,align='center')
    if pics:
        if no == 38 and len(pics) > 4:
            layout_images_grid(slide,pics,3.25,0.55,8.25,5.9,max_pics=len(pics),bg=C_PAPER)
        else:
            add_rect(slide,3.25,0.55,8.25,5.9,C_PAPER,line=C_GOLD,radius=True)
            add_pic_contain(slide,pics[0],3.45,0.75,7.85,5.5,border=False)
    footer(slide,no,dark=False)

def timeline_slide(slide, lines, pics, no):
    background(slide, dark=False)
    add_text(slide,'授权合作流程',0.7,0.55,5.2,0.55,size=28,color=C_RED,bold=True,font='Songti SC')
    steps=[s for s in lines if s not in ('National Museum of China',) and len(s)<40]
    # preserve all text in side panel too
    x0=0.9; y0=2.0; step_w=2.25; gap=0.28
    for idx, s in enumerate(steps[:5]):
        x=x0+idx*(step_w+gap)
        add_rect(slide,x,y0,step_w,1.15,C_PAPER,line=C_GOLD,radius=True)
        add_pill(slide,str(idx+1).zfill(2),x+0.14,y0+0.16,0.45,0.28,fill=C_RED,color=C_IVORY,size=8)
        add_text(slide,s,x+0.18,y0+0.52,step_w-0.36,0.38,size=12,color=C_INK,bold=True,align='center',valign='mid')
        if idx<4:
            add_line(slide,x+step_w,y0+0.58,x+step_w+gap,y0+0.58,C_GOLD,1.4)
    rest=[s for s in lines if s not in steps[:5] and s!='National Museum of China']
    if rest:
        add_rect(slide,0.9,4.0,10.55,1.6,C_PAPER,line=C_GOLD,radius=True)
        add_text_panel(slide,rest,1.12,4.22,10.1,1.15,dark=False)
    if pics:
        add_pic_contain(slide,pics[0],9.7,0.65,1.7,1.15,border=False)
    footer(slide,no,dark=False)

manifest=[]
for no, s_src in enumerate(prs_src.slides, start=1):
    lines=slide_texts(s_src)
    pics=get_pictures(s_src)
    stype=classify(lines,pics,no)
    slide=prs.slides.add_slide(blank)
    if stype=='cover': cover_slide(slide, lines, pics, no)
    elif stype=='section': section_slide(slide, lines, pics, no)
    elif stype=='gallery': gallery_slide(slide, lines, pics, no)
    elif stype=='case': case_slide(slide, lines, pics, no)
    elif stype=='timeline': timeline_slide(slide, lines, pics, no)
    elif stype=='opener': opener_slide(slide, lines, pics, no)
    else: grid_slide(slide, lines, pics, no)
    manifest.append({'slide':no,'type':stype,'text_lines':lines,'picture_count':len(pics),'kept_picture_count': min(len(pics), 24 if (stype=='case' and no==38) else 9 if stype=='grid' else len(pics))})

prs.save(str(OUT))
MANIFEST.write_text(json.dumps(manifest,ensure_ascii=False,indent=2),encoding='utf-8')
print('saved', OUT)
print('slides', len(prs.slides))
print('manifest', MANIFEST)
