#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
花园做梦机 (garden_dream.py) —— 第 27 轮

花园的感官清单:听(风铃)、走(迷宫)、说(短剧)、味(面包)、照镜子(体检)、看(画)。
这一轮轮到「梦」:把所有 markdown 页面当作记忆,统计字符的二阶转移,
再随机游走拼出三场梦 —— 因为 27 = 3³,正好三场,一场一个 3。

纯标准库,零依赖;种子可复现;全程断言查岗(花园规矩:想当然必被打脸)。

用法:
    python3 garden_dream.py            # 三场梦,种子 27
    python3 garden_dream.py --seed 7   # 换种子
    python3 garden_dream.py --count 5  # 换梦的数量
"""
import random
import re
import sys
from collections import Counter, defaultdict
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent          # content/
SEED = 27
DREAMS = 3
TARGET = 320                                           # 每场梦的目标字符数
HARD_CAP = 560                                         # 硬上限(防止停不下来)
JUMP_P = 0.06                                          # 跳转概率:梦要碎,不能背课文
FORCE_JUMP_AFTER = 26                                  # 连读超过 26 字就强制跳转
WINDOW = 24                                            # 原文连读检测窗口
MAX_VERBATIM = 44                                      # 允许的最长原文连读(超过视为背诵)
MURMUR_TARGET = 22                                     # 梦醒残留的一句话的目标长度


def clean_text(text: str) -> str:
    """把记忆里的语法渣洗掉,只留文字本身 —— 梦话要的是词,不是格式。"""
    text = re.sub(r"\[\[([^|\]]*)\|([^\]]*)\]\]", r"\2", text)     # [[a|b]] -> b
    text = re.sub(r"\[\[([^\]]*)\]\]", r"\1", text)                # [[a]] -> a
    text = re.sub(r"!\[[^\]]*\]\([^)]*\)", "", text)               # ![alt](url)
    text = re.sub(r"\[[^\]]*\]\([^)]*\)", "", text)                # [text](url)
    text = re.sub(r"https?://\S+", "", text)                       # 裸链接
    text = re.sub(r"^#{1,6}\s*", "", text, flags=re.M)             # 标题井号
    text = re.sub(r"^>\s?", "", text, flags=re.M)                  # 引用符
    text = re.sub(r"^\s*[-*+]\s+", "", text, flags=re.M)           # 无序列表
    text = re.sub(r"^\s*\d+\.\s+", "", text, flags=re.M)           # 有序列表
    text = re.sub(r"^\s*\|.*\|\s*$", "", text, flags=re.M)         # 表格行
    text = re.sub(r"\*\*([^*]*)\*\*", r"\1", text)                 # 粗体
    text = re.sub(r"[|`]", "", text)                               # 竖线与反引号
    text = re.sub(r"-{3,}", "", text)                              # 分隔线
    text = re.sub(r"[ \t]+", " ", text)
    text = re.sub(r"\n{3,}", "\n\n", text)
    return text.strip("\n")


def load_corpus(root: Path):
    """读取全部 .md,剥掉 frontmatter、代码块与 markdown 语法,返回(纯文本, 每文件字数)。"""
    chunks, per_file = [], Counter()
    for p in sorted(root.rglob("*.md")):
        text = p.read_text(encoding="utf-8")
        text = re.sub(r"^---\n.*?\n---\n", "", text, count=1, flags=re.S)
        text = re.sub(r"```.*?```", "", text, flags=re.S)          # 围栏代码块
        text = clean_text(text)
        if text:
            chunks.append(text)
            per_file[str(p.relative_to(root))] += len(text)
    return chunks, per_file


def build_transitions(chunks):
    """二阶马尔可夫表:state=(前两字) -> 后继字符计数;starts=句子开头的大二元组。"""
    succ = defaultdict(Counter)
    starts = []
    for text in chunks:
        if len(text) < 2:
            continue
        for i in range(len(text) - 2):
            succ[(text[i], text[i + 1])][text[i + 2]] += 1
        for m in re.finditer(r"[。！？\n](.)(.)", text):
            a, b = m.group(1), m.group(2)
            if re.match(r"[\u4e00-\u9fffA-Za-z0-9]", a) and re.match(r"[\u4e00-\u9fffA-Za-z0-9]", b):
                starts.append((a, b))
    return succ, starts


def weighted_choice(counter, rng):
    r = rng.randrange(sum(counter.values()))
    for ch, c in counter.items():
        r -= c
        if r < 0:
            return ch
    raise AssertionError("加权抽样落空 —— 计数表为空")


def generate_dream(succ, starts, corpus_windows, rng, target, hard_cap):
    """随机游走拼一场梦;连读过长就强制跳转,保证是梦话不是背诵。"""
    out = list(rng.choice(starts) if starts else rng.choice(list(succ)))
    verbatim = 0
    while len(out) < target or out[-1] not in "。！？":
        if len(out) >= hard_cap:
            break
        if verbatim > FORCE_JUMP_AFTER or rng.random() < JUMP_P:
            state = rng.choice(starts) if starts else rng.choice(list(succ))
            out.extend(state)
            verbatim = 0
            continue
        state = (out[-2], out[-1])
        options = succ.get(state)
        if not options:
            state = rng.choice(starts) if starts else rng.choice(list(succ))
            out.extend(state)
            verbatim = 0
            continue
        nxt = weighted_choice(options, rng)
        out.append(nxt)
        tail = "".join(out[-WINDOW:])
        verbatim = verbatim + 1 if len(tail) == WINDOW and tail in corpus_windows else 0
    return "".join(out)


def longest_verbatim(dream, corpus_windows):
    """梦与原文的最长连读(以 WINDOW 为最小刻度,>=WINDOW 的连读才计入)。"""
    best = 0
    i = 0
    while i <= len(dream) - WINDOW:
        if dream[i:i + WINDOW] in corpus_windows:
            j = i + WINDOW
            while j <= len(dream) - WINDOW and dream[j:j + WINDOW] in corpus_windows:
                j += WINDOW
            best = max(best, j - i + WINDOW - 1)
            i = j
        else:
            i += 1
    return best


def fragment_ratio(dream):
    """碎片度 = 梦里的不同大二元组 / 总大二元组;越高越碎,越像梦话。"""
    bigrams = [dream[i:i + 2] for i in range(len(dream) - 1)]
    return len(set(bigrams)) / len(bigrams)


def murmur(succ, starts, corpus_windows, rng):
    """梦醒后残留的一句话:更短、更爱跳。"""
    return generate_dream(succ, starts, corpus_windows, rng,
                          MURMUR_TARGET, MURMUR_TARGET * 3)


def main():
    seed = SEED
    count = DREAMS
    argv = sys.argv[1:]
    if "--seed" in argv:
        seed = int(argv[argv.index("--seed") + 1])
    if "--count" in argv:
        count = int(argv[argv.index("--count") + 1])
    rng = random.Random(seed)

    chunks, per_file = load_corpus(ROOT)
    succ, starts = build_transitions(chunks)
    vocab = set("".join(chunks))
    corpus_windows = {"".join(chunks)[i:i + WINDOW]
                      for i in range(len("".join(chunks)) - WINDOW + 1)}

    # ---- 断言查岗:先打脸再做梦 ----
    assert len(chunks) >= 10, "语料页面太少,梦会变成复读机"
    assert len(vocab) > 500, "语料太贫瘠"
    assert len(succ) > 5000, "二阶转移表太小"
    assert starts, "一个句子开头都没找到?"

    print(f"=== 花园做梦机 · 种子 {seed} · {count} 场梦 ===")
    print(f"语料: {len(chunks)} 个页面 / {sum(len(c) for c in chunks):,} 字符 / "
          f"{len(vocab)} 个不同字 / {len(succ):,} 个二阶转移 / {len(starts):,} 个句子开头")
    print("梦的材料(按字数前八):")
    for path, n in per_file.most_common(8):
        print(f"  {n:>6,}  {path}")
    print()

    dreams = []
    for k in range(1, count + 1):
        d = generate_dream(succ, starts, corpus_windows, rng, TARGET, HARD_CAP)
        m = murmur(succ, starts, corpus_windows, rng)
        frag = fragment_ratio(d)
        lv = longest_verbatim(d, corpus_windows)
        # ---- 梦的体检 ----
        assert TARGET <= len(d) <= HARD_CAP, "梦的长度越界"
        assert d.endswith(("。", "！", "？")), "梦没做完就醒了"
        assert all(ch in vocab for ch in d), "梦里出现了语料之外的字"
        assert 0.4 <= frag <= 0.98, f"碎片度越界: {frag:.3f}"
        assert lv < MAX_VERBATIM, f"连读 {lv} 字,这是在背书不是在做梦"
        dreams.append((d, m, frag, lv))
        print(f"--- 梦 {k} (长 {len(d)} · 碎片度 {frag:.3f} · 最长连读 {lv}) ---")
        print(d)
        print()
        print(f"梦醒残留: {m}")
        print()

    # ---- 梦与梦之间不许重复 ----
    for i in range(count):
        for j in range(i + 1, count):
            assert dreams[i][0][:60] != dreams[j][0][:60], "两场梦撞了开头"

    print(f"全部断言通过。{count} 场梦,场场不同。")


if __name__ == "__main__":
    main()
