# -*- coding: utf-8 -*-
"""备课台数据构建脚本
把「课件页 + 讲稿 + 教案」合成为 data.js，供 index.html 使用。
用法：python3 _build_prep.py
"""
import os, re, sys, json, html as H
from datetime import datetime

HERE = os.path.dirname(os.path.abspath(__file__))
PKG = os.path.dirname(HERE)                       # 22-软件项目管理教学包
CW = os.path.join(PKG, '课件成品-HTML', '软件项目管理-16周课件')
SCRIPT_DIR = os.path.join(PKG, '讲稿')
JIAOAN_DIR = os.path.join(PKG, '教案')

# ---------- 极简 Markdown → HTML ----------
def esc(s):
    return H.escape(s, quote=False)

def inline(s):
    s = esc(s)
    s = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', s)
    s = re.sub(r'(?<!\*)\*([^*\n]+?)\*(?!\*)', r'<em>\1</em>', s)
    s = re.sub(r'`([^`]+?)`', r'<code>\1</code>', s)
    return s

def md2html(md):
    lines = md.split('\n')
    out, i = [], 0
    while i < len(lines):
        ln = lines[i]
        s = ln.strip()
        if not s:
            i += 1; continue
        start_i = i
        # 表格
        if s.startswith('|'):
            rows = []
            while i < len(lines) and lines[i].strip().startswith('|'):
                rows.append([c.strip() for c in lines[i].strip().strip('|').split('|')])
                i += 1
            if len(rows) >= 2 and re.match(r'^[\s:\-]+$', rows[1][0] or '-'):
                head, body = rows[0], rows[2:]
            else:
                head, body = rows[0], rows[1:]
            t = ['<table><thead><tr>'] + [f'<th>{inline(c)}</th>' for c in head] + ['</tr></thead><tbody>']
            for r in body:
                t.append('<tr>' + ''.join(f'<td>{inline(c)}</td>' for c in r) + '</tr>')
            t.append('</tbody></table>')
            out.append(''.join(t))
        # 引用块（讲稿口播）
        elif s.startswith('>'):
            buf = []
            while i < len(lines) and lines[i].strip().startswith('>'):
                buf.append(lines[i].strip()[1:].strip()); i += 1
            out.append('<blockquote>' + '<br>'.join(inline(b) for b in buf if b) + '</blockquote>')
        # 分隔线
        elif re.match(r'^-{3,}$', s):
            out.append('<hr>'); i += 1
        # 标题（支持 # ~ ####）
        elif re.match(r'^#{1,4}\s+', s):
            lv = len(re.match(r'^(#{1,4})\s+', s).group(1))
            out.append(f'<h{lv}>{inline(re.sub(r"^#{1,4}\s+", "", s))}</h{lv}>'); i += 1
        # 无序列表
        elif re.match(r'^[-*]\s+', s):
            items = []
            while i < len(lines) and re.match(r'^[-*]\s+', lines[i].strip()):
                items.append(inline(re.sub(r'^[-*]\s+', '', lines[i].strip()))); i += 1
            out.append('<ul>' + ''.join(f'<li>{x}</li>' for x in items) + '</ul>')
        # 有序列表
        elif re.match(r'^\d+[.、]\s*', s):
            items = []
            while i < len(lines) and re.match(r'^\d+[.、]\s*', lines[i].strip()):
                items.append(inline(re.sub(r'^\d+[.、]\s*', '', lines[i].strip()))); i += 1
            out.append('<ol>' + ''.join(f'<li>{x}</li>' for x in items) + '</ol>')
        # 普通段落
        else:
            buf = []
            while i < len(lines) and lines[i].strip() and not re.match(r'^([|>#]|[-*]\s|\d+[.、]\s|-{3,}$)', lines[i].strip()):
                buf.append(lines[i].strip()); i += 1
            if i == start_i:      # 兜底：任何分支都没消费输入时强制前进，杜绝死循环
                out.append('<p>' + inline(s) + '</p>'); i += 1
            else:
                out.append('<p>' + '<br>'.join(inline(b) for b in buf) + '</p>')
    return '\n'.join(out)

# ---------- 解析讲稿：按 【N.M】 锚点切片 ----------
def parse_script(path):
    """返回 {slide_no: HTML}，slide_no 形如 '1.1'；区间锚点（1.25–1.26）复制到两页。"""
    if not path or not os.path.isfile(path):
        return {}, None
    raw = open(path, encoding='utf-8').read()
    # 去 YAML 风格头部引用块顶部说明？保留正文即可
    parts = re.split(r'^###\s+【([0-9]+\.[0-9]+)(?:\s*[–\-—]\s*([0-9]+\.[0-9]+))?】\s*(.*)$',
                     raw, flags=re.M)
    result = {}
    # parts: [前言, no1, no2(end), title1, body1, no, end, title, body, ...]
    idx = 1
    while idx + 3 <= len(parts):
        n1, n2, title = parts[idx].strip(), (parts[idx + 1] or '').strip(), parts[idx + 2].strip()
        body = parts[idx + 3] if idx + 3 < len(parts) else ''
        html_body = md2html(body)
        head = f'<div class="sc-head"><span class="sc-anchor">【{n1}{("–" + n2) if n2 else ""}】</span><span class="sc-title">{esc(title)}</span></div>'
        html_full = head + html_body
        result[n1] = html_full
        if n2:
            result[n2] = f'<div class="sc-note">本页讲稿与 {n1} 合并讲解（覆盖 {n1}–{n2}）</div>' + html_full
        idx += 4
    return result, raw

# ---------- 读取课件幻灯片标题 ----------
def load_lecture_slides():
    sys.path.insert(0, CW)
    titles = {}
    try:
        import importlib.util
        def load(name, path):
            spec = importlib.util.spec_from_file_location(name, path)
            m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m); return m
        c1 = load('_c1', os.path.join(CW, '_content1.py'))
        c2 = load('_c2', os.path.join(CW, '_content2.py'))
        titles[1] = [t for (_pt, t, _b) in (c1.SLIDES + c2.SLIDES)]
        b2 = load('_b2', os.path.join(CW, '_build2.py'))
        titles[2] = [t for (_pt, t, _b) in b2.SLIDES]
    except Exception as e:
        print('⚠ 课件标题解析失败:', e)
    return titles

# ---------- 全书 16 次课元数据（取自 _build.py LECTURES 口径） ----------
LECTURES = [
 (1, '第1周', '第1章 软件项目管理概述——华为鸿蒙OS开发项目管理实践案例', '项目/软件项目 → 软件项目管理（成败·环境·认证）→ 价值驱动的知识体系；以鸿蒙案例贯穿全书。', '01'),
 (2, '第2周', '第2章 软件项目启动', '立项（建议书·可行性研究·评估与决策）、项目经理与组织结构、识别干系人、制定项目章程、项目启动会议。', '02'),
 (3, '第3周', '第3章 软件项目采购管理（一）', '采购概述与敏捷应用、规划采购管理（自制-外购分析、采购管理计划/SOW、供方选择标准）。', '03'),
 (4, '第4周', '第3章 软件项目采购管理（二）', '实施采购（招标/投标/评标）、控制采购、合同类型与合同管理过程。', '03'),
 (5, '第5周', '第4章 软件项目范围管理（一）', '范围概念与敏捷应用、规划范围/需求管理计划、收集需求（访谈/问卷/原型等）。', '04'),
 (6, '第6周', '第4章 软件项目范围管理（二）', '定义范围、创建 WBS、确认范围（验收）、控制范围（蔓延/镀金）、交付绩效域。', '04'),
 (7, '第7周', '第5章 软件项目进度管理（一）', '进度概念与敏捷应用、规划进度管理、定义活动、排列活动顺序（PDM 网络图）。', '05'),
 (8, '第8周', '第5章 软件项目进度管理（二）', '估算活动持续时间（三点估算）、制订进度计划：CPM 正逆推、时差、关键路径、进度压缩。', '05'),
 (9, '第9周', '第5章 软件项目进度管理（三）', '控制进度、开发方法和生命周期绩效域、进度综合案例＋ProjectLibre 演示。', '05'),
 (10, '第10周', '第6章 软件项目成本管理（一）', '成本概念与敏捷应用、规划成本管理、估算成本（类比/参数/自下而上、储备分析）。', '06'),
 (11, '第11周', '第6章 软件项目成本管理（二）', '制订成本预算（基准）、控制成本：挣值 EVM 全套公式与预测、度量绩效域。', '06'),
 (12, '第12周', '第7章 软件项目质量管理', '规划/管理/控制质量（七工具等）、配置管理、度量与交付绩效域。', '07'),
 (13, '第13周', '第8章 软件项目资源管理', '规划资源/估算活动资源/获取资源、团队建设（塔克曼）与团队管理（冲突五策略）、控制资源、团队绩效域。', '08'),
 (14, '第14周', '第9章 软件项目干系人管理与沟通管理', '干系人识别/参与规划-管理-监督；沟通规划/管理/监督、渠道数；干系人绩效域。', '09'),
 (15, '第15周', '第10章 软件项目风险管理', '规划风险、识别、定性与定量分析（概率-影响矩阵/EMV）、威胁/机会应对策略、实施与监督、不确定性绩效域。', '10'),
 (16, '第16周', '案例分析：综合案例讲解＋第11章整合管理串讲＋全书复习答疑', '综合案例串联 1–10 章；整合管理；期末复习与题型说明。', '11'),
]

JIAOAN_FILE = {}
for f in os.listdir(JIAOAN_DIR) if os.path.isdir(JIAOAN_DIR) else []:
    m = re.match(r'03-教案-第(\d+)章-(.+)\.md$', f)
    if m:
        JIAOAN_FILE[int(m.group(1))] = f

SCRIPT_FILE = {1: '第1章-软件项目管理概述-讲稿.md', 2: '第2章-软件项目启动-讲稿.md'}

def build():
    titles = load_lecture_slides()
    lectures = []
    for (no, week, title, desc, chap) in LECTURES:
        lec_dir = os.path.join(CW, f'lecture{no}')
        # 真实幻灯片文件（排除 index.html）
        slides = []
        if os.path.isdir(lec_dir):
            files = [f for f in os.listdir(lec_dir) if re.match(rf'^{no}\.\d+\.html$', f)]
            files.sort(key=lambda x: int(re.findall(r'\d+', x)[1]))
            for f in files:
                n = f[:-5]
                i = int(n.split('.')[1])
                t = titles.get(no, [])[i - 1] if i - 1 < len(titles.get(no, [])) else f'第 {i} 页'
                slides.append({'n': n, 'idx': i, 'title': t,
                               'href': f'../课件成品-HTML/软件项目管理-16周课件/lecture{no}/{f}'})
        # 讲稿
        script_map, _raw = parse_script(os.path.join(SCRIPT_DIR, SCRIPT_FILE.get(no, '')))
        for s in slides:
            s['script'] = script_map.get(s['n'], '')
            # 口播字数（仅统计 <blockquote> 内的汉字）与预计用时，供备课控制节奏
            spoken = 0
            for bq in re.findall(r'<blockquote>(.*?)</blockquote>', s['script'], re.S):
                spoken += len(re.sub(r'[^\u4e00-\u9fff]', '', bq))
            s['spoken'] = spoken
            s['minutes'] = round(spoken / 240, 1) if spoken else 0   # 按 240 字/分钟估算
        # 教案（整章）
        jf = JIAOAN_FILE.get(int(chap))
        jiaohan = ''
        if jf:
            md = open(os.path.join(JIAOAN_DIR, jf), encoding='utf-8').read()
            jiaohan = md2html(md)
        lectures.append({
            'no': no, 'week': week, 'title': title, 'desc': desc, 'chapter': chap,
            'slides': slides, 'slideCount': len(slides),
            'hasScript': bool(script_map), 'hasCourseware': bool(slides),
            'jiaohanFile': jf or '', 'jiaohan': jiaohan,
        })
    data = {
        'generated': datetime.now().strftime('%Y-%m-%d %H:%M'),
        'course': {'title': '软件项目管理', 'code': '154431006', 'teacher': '周宇文',
                   'phone': '13580390715', 'hours': '32 学时 / 2 学分',
                   'assess': '平时 30%（作业20＋课堂讨论和练习10）＋ 期末 70%'},
        'lectures': lectures,
    }
    out = os.path.join(HERE, 'data.js')
    with open(out, 'w', encoding='utf-8') as f:
        f.write('window.PREP_DATA = ')
        json.dump(data, f, ensure_ascii=False, indent=1)
        f.write(';\n')
    tot = sum(l['slideCount'] for l in lectures)
    sc = sum(1 for l in lectures if l['hasScript'])
    print(f'✓ data.js 生成：{len(lectures)} 次课 / {tot} 页课件 / {sc} 次课含讲稿')
    for l in lectures:
        flag = '讲稿✓' if l['hasScript'] else '讲稿待写'
        cw = f"{l['slideCount']}页" if l['hasCourseware'] else '课件待制作'
        print(f"   第{l['no']:>2}次课  {cw:<10} {flag}  {l['title'][:34]}")

if __name__ == '__main__':
    build()
