#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""教学幻灯片 PPT 生成器（4 天讲授 + Day5 综合案例）—— 手工维护，非 wiki 生成产物。

- 数据：同目录 day1.py … day5.py（每页一个 dict）
- 产物：docs/teaching/ppt/day{1..5}.pptx，页面内容与 docs/teaching/slides/*.html 对应
- 讲解说明文字（含建议时长/讲法/演示/互动/过渡）只写入**演讲者备注**，不进主页面

用法（python-pptx 不在系统 Python 里，用任一装了它的解释器）：
    python3 -m venv ~/.venv-pptx && ~/.venv-pptx/bin/pip install python-pptx
    ~/.venv-pptx/bin/python tools/teaching_ppt/gen.py
"""
import importlib
import math
import os
import sys

from pptx import Presentation
from pptx.dml.color import RGBColor
from pptx.enum.text import MSO_ANCHOR
from pptx.oxml.ns import qn
from pptx.util import Inches, Pt

ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
OUT_DIR = os.path.join(ROOT, 'docs', 'teaching', 'ppt')

FONT = 'Microsoft YaHei'
MONO = 'Consolas'

# (填充, 边框, 标题色)
BOX_STYLE = {
    'concept': (RGBColor(0xE8, 0xF5, 0xE9), RGBColor(0x2F, 0x85, 0x5A), RGBColor(0x22, 0x54, 0x3D)),
    'pitfall': (RGBColor(0xFD, 0xEC, 0xEA), RGBColor(0xC5, 0x30, 0x30), RGBColor(0x82, 0x27, 0x27)),
    'info':    (RGBColor(0xEB, 0xF8, 0xFF), RGBColor(0x2B, 0x6C, 0xB0), RGBColor(0x2A, 0x43, 0x65)),
    'task':    (RGBColor(0xFF, 0xFA, 0xEB), RGBColor(0xB7, 0x79, 0x1F), RGBColor(0x74, 0x4D, 0x0B)),
}
BOX_DEFAULT_HEADING = {'concept': '核心概念', 'pitfall': '常见的坑', 'info': '说明', 'task': '任务卡'}

TEXT_DARK = RGBColor(0x1A, 0x20, 0x2C)
TEXT_GRAY = RGBColor(0x71, 0x80, 0x96)
CODE_BG = RGBColor(0x1E, 0x1E, 0x1E)
CODE_FG = RGBColor(0xD4, 0xD4, 0xD4)

ML = 0.45          # 左右边距 (inch)
W = 13.333 - 2 * ML


def set_font(run, name=FONT, size=None, bold=None, color=None, italic=None):
    f = run.font
    f.name = name
    if size is not None:
        f.size = Pt(size)
    if bold is not None:
        f.bold = bold
    if italic is not None:
        f.italic = italic
    if color is not None:
        f.color.rgb = color
    # 东亚字体要单独设置（python-pptx 的 .name 只设 latin）
    rpr = run._r.get_or_add_rPr()
    ea = rpr.find(qn('a:ea'))
    if ea is None:
        ea = rpr.makeelement(qn('a:ea'), {})
        rpr.append(ea)
    ea.set('typeface', name)


def add_textbox(slide, left, top, width, height):
    tb = slide.shapes.add_textbox(Inches(left), Inches(top), Inches(width), Inches(height))
    tf = tb.text_frame
    tf.word_wrap = True
    tf.margin_left = tf.margin_right = Pt(4)
    tf.margin_top = tf.margin_bottom = Pt(2)
    return tb, tf


def wrap_lines(text, per_line):
    return max(1, math.ceil(len(text) / per_line))


def add_box(slide, kind, heading, lines, top):
    """彩色概念/坑/说明卡，返回其高度(inch)。"""
    fill, border, head_color = BOX_STYLE[kind]
    body_h = sum(wrap_lines(t, 62) for t in lines) * 0.28
    h = 0.42 + body_h + 0.10
    shape = slide.shapes.add_shape(1, Inches(ML), Inches(top), Inches(W), Inches(h))  # 1 = 矩形
    shape.fill.solid()
    shape.fill.fore_color.rgb = fill
    shape.line.color.rgb = border
    shape.line.width = Pt(1.2)
    shape.shadow.inherit = False
    tf = shape.text_frame
    tf.word_wrap = True
    tf.margin_left = Pt(10)
    tf.margin_right = Pt(10)
    tf.margin_top = Pt(5)
    tf.margin_bottom = Pt(4)
    tf.vertical_anchor = MSO_ANCHOR.TOP
    p = tf.paragraphs[0]
    r = p.add_run()
    r.text = heading or BOX_DEFAULT_HEADING[kind]
    set_font(r, size=14, bold=True, color=head_color)
    for line in lines:
        p = tf.add_paragraph()
        p.space_before = Pt(3)
        r = p.add_run()
        r.text = '• ' + line
        set_font(r, size=13, color=TEXT_DARK)
    return h


def add_code(slide, code, top):
    lines = code.rstrip('\n').split('\n')
    h = 0.26 + len(lines) * 0.245
    shape = slide.shapes.add_shape(1, Inches(ML), Inches(top), Inches(W), Inches(h))
    shape.fill.solid()
    shape.fill.fore_color.rgb = CODE_BG
    shape.line.fill.background()
    shape.shadow.inherit = False
    tf = shape.text_frame
    tf.word_wrap = False
    tf.margin_left = Pt(12)
    tf.margin_top = Pt(6)
    first = True
    for line in lines:
        p = tf.paragraphs[0] if first else tf.add_paragraph()
        first = False
        r = p.add_run()
        r.text = line if line else ' '
        set_font(r, name=MONO, size=12, color=CODE_FG)
    return h


def set_notes(slide, spec):
    tf = slide.notes_slide.notes_text_frame
    tf.clear()
    minutes = spec.get('minutes')
    segment = spec.get('segment', '')
    header = f"⏱ 建议 {minutes} 分钟 · {segment}" if minutes else segment
    paras = ([header] if header.strip(' ·') else []) + spec.get('notes', '').strip().split('\n')
    first = True
    for para in paras:
        p = tf.paragraphs[0] if first else tf.add_paragraph()
        first = False
        r = p.add_run()
        r.text = para
        set_font(r, size=12, bold=(first and bool(minutes)))


def build_title_slide(prs, spec):
    slide = prs.slides.add_slide(prs.slide_layouts[6])
    _, tf = add_textbox(slide, ML, 0.35, W, 0.35)
    r = tf.paragraphs[0].add_run()
    r.text = spec.get('badge', '')
    set_font(r, size=12, color=TEXT_GRAY)
    _, tf = add_textbox(slide, ML, 1.5, W, 1.0)
    r = tf.paragraphs[0].add_run()
    r.text = spec['title']
    set_font(r, size=40, bold=True, color=TEXT_DARK)
    _, tf = add_textbox(slide, ML, 2.6, W, 0.6)
    r = tf.paragraphs[0].add_run()
    r.text = spec.get('subtitle', '')
    set_font(r, size=20, color=RGBColor(0x2B, 0x6C, 0xB0))
    top = 3.5
    _, tf = add_textbox(slide, ML, top, W, 2.6)
    first = True
    for b in spec.get('bullets', []):
        p = tf.paragraphs[0] if first else tf.add_paragraph()
        first = False
        p.space_before = Pt(6)
        r = p.add_run()
        r.text = '· ' + b
        set_font(r, size=16, color=TEXT_DARK)
    if spec.get('source'):
        _, tf = add_textbox(slide, ML, 6.95, W, 0.4)
        r = tf.paragraphs[0].add_run()
        r.text = spec['source']
        set_font(r, size=11, color=TEXT_GRAY)
    set_notes(slide, spec)


def build_content_slide(prs, spec, deck, idx):
    slide = prs.slides.add_slide(prs.slide_layouts[6])
    _, tf = add_textbox(slide, ML, 0.18, W, 0.32)
    r = tf.paragraphs[0].add_run()
    r.text = spec.get('badge', '')
    set_font(r, size=11, color=TEXT_GRAY)
    _, tf = add_textbox(slide, ML, 0.5, W, 0.75)
    r = tf.paragraphs[0].add_run()
    r.text = spec['title']
    set_font(r, size=27, bold=True, color=TEXT_DARK)
    top = 1.35
    for b in spec.get('bullets', []):
        _, tf = add_textbox(slide, ML, top, W, 0.4)
        r = tf.paragraphs[0].add_run()
        r.text = '· ' + b
        set_font(r, size=15, color=TEXT_DARK)
        top += wrap_lines(b, 56) * 0.34 + 0.04
    if spec.get('code'):
        top += 0.06
        top += add_code(slide, spec['code'], top) + 0.14
    for box in spec.get('boxes', []):
        kind, heading, lines = box
        top += add_box(slide, kind, heading, lines, top) + 0.14
    if spec.get('source'):
        _, tf = add_textbox(slide, ML, 7.02, W, 0.35)
        r = tf.paragraphs[0].add_run()
        r.text = spec['source']
        set_font(r, size=10.5, color=TEXT_GRAY)
    if top > 7.05:
        print(f'  [overflow] {deck} 第{idx}页「{spec["title"]}」内容高度 {top:.2f}in > 7.05in，请精简')
    set_notes(slide, spec)


def build_deck(mod_name):
    mod = importlib.import_module(mod_name)
    prs = Presentation()
    prs.slide_width = Inches(13.333)
    prs.slide_height = Inches(7.5)
    for i, spec in enumerate(mod.SLIDES, 1):
        if spec.get('kind') == 'title':
            build_title_slide(prs, spec)
        else:
            build_content_slide(prs, spec, mod_name, i)
    out = os.path.join(OUT_DIR, f'{mod_name}.pptx')
    prs.save(out)
    total = sum(float(s.get('minutes', 0) or 0) for s in mod.SLIDES)
    print(f'{out}: {len(mod.SLIDES)} 页, 备注时长合计 ≈ {total:.0f} 分钟')


def main():
    os.makedirs(OUT_DIR, exist_ok=True)
    sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
    for n in range(1, 6):
        build_deck(f'day{n}')


if __name__ == '__main__':
    main()
