"""
toy_language.py —— 第 6 讲的"玩具语言"：一个能完整列举、能精确判断语法对错的小语言
=====================================================================================

【为什么需要它】
    真实的语言模型要在几十亿个 token 上训练，我们在笔记本 CPU 上做不到，也没法知道"真实分布"是什么。
    这里用代码定义一个很小的语言，好处和前几讲的二维高斯混合一样：**真相完全已知**。
        - 所有合法句子都能列举出来（4608 个），每个句子的概率都能精确算出；
        - 任何一串 token 是否合法，都能用规则精确判断，而且能说出错在哪；
        - 分布的熵（"最好的语言模型能做到的平均负对数似然"）可以精确算出：约 8.15 nats / 句。

【语法】每个句子 = 主语名词短语 + 动词 + 宾语名词短语 + "."，补 "_"（PAD）到 8 个 token：
        冠词  [形容词]  名词  动词  冠词  [形容词]  名词  .   ( _ _ )
        the   big       cats  chase a     red       dog   .
    规则（每一条都是 token 之间的"约束"，后面检查并行解码时会看到它们怎样被破坏）：
        R1 冠词与名词的数一致："a" 只接单数，"two" 只接复数，"the" 都可以；
        R2 主谓一致：单数主语配 sees / likes / chases，复数主语配 see / like / chase；
        R3 宾语和主语不能是同一种动物（"the cat sees a cat" 不合法）——一条**长距离**约束；
        R4 结构：形容词可有可无，所以名词、动词、句号的位置会变（6–8 个 token），其余位置是 PAD。
    生成过程：每个选择独立、均匀（形容词：一半概率没有，一半概率从 3 个里均匀选），
    宾语动物从"另外两种"里均匀选。

【用法】
    from toy_language import ToyLanguage
    lang = ToyLanguage()
    x = lang.sample(5, torch.Generator().manual_seed(0))      # (5, 8) 的 token 编号
    print(lang.decode(x[0]), lang.check(x[0]))
"""

import itertools
import math

import torch

PAD = "_"
DETS = {"sg": ["the", "a"], "pl": ["the", "two"]}
ADJS = ["red", "big", "small"]
ANIMALS = ["cat", "dog", "bird"]
NOUNS = {"sg": ["cat", "dog", "bird"], "pl": ["cats", "dogs", "birds"]}
VERBS = {"sg": ["sees", "likes", "chases"], "pl": ["see", "like", "chase"]}

# 词表：20 个"真"token。顺序固定（编号就是在这个列表里的位置）
VOCAB = [PAD, "the", "a", "two", "red", "big", "small",
         "cat", "dog", "bird", "cats", "dogs", "birds",
         "sees", "likes", "chases", "see", "like", "chase", "."]
V = len(VOCAB)                 # 20
SEQ_LEN = 8
MASK_ID = V                    # 掩码扩散用的 [MASK]，编号 20（不是"真"token，模型从不输出它）
BOS_ID = V + 1                 # 自回归模型用的"句首"标记，编号 21
TOKEN_ID = {w: i for i, w in enumerate(VOCAB)}


def _noun_info(word):
    """名词 → (动物种类, 数)；不是名词返回 None。"""
    for num in ("sg", "pl"):
        if word in NOUNS[num]:
            return ANIMALS[NOUNS[num].index(word)], num
    return None


def _np_choices():
    """
    列举所有名词短语：[(token 列表, 动物, 数, 概率), ...]，概率按"生成过程"计算（不含动物的选择概率）。

    【思路】数（1/2）× 冠词（1/2）× 形容词（无：1/2；每个：1/6）。动物的概率由调用者另算，
        因为宾语的动物要排除主语的动物。
    """
    out = []
    for num in ("sg", "pl"):
        for det in DETS[num]:
            for adj in [None] + ADJS:
                p_adj = 0.5 if adj is None else 0.5 / len(ADJS)
                for k, animal in enumerate(ANIMALS):
                    words = [det] + ([adj] if adj else []) + [NOUNS[num][k]]
                    out.append((words, animal, num, 0.5 * 0.5 * p_adj))
    return out


class ToyLanguage:
    """
    玩具语言：列举全部合法句子及其概率；抽样；把句子编号 ↔ 文字；检查合法性。

    【属性】
        sentences : (M, 8) long 张量，全部 M = 4608 个合法句子（token 编号，PAD 补齐）
        probs     : (M,) float64，每个句子的真实概率，和为 1
        entropy   : 真实分布的熵（nats / 句）——任何语言模型的平均负对数似然都不可能低于它
    """

    def __init__(self):
        rows, probs = [], []
        nps = _np_choices()
        for (subj, s_animal, s_num, p_s), verb_k, (obj, o_animal, _, p_o) in itertools.product(
                nps, range(len(VERBS["sg"])), nps):
            if o_animal == s_animal:                                    # R3：宾语不能是同一种动物
                continue
            words = subj + [VERBS[s_num][verb_k]] + obj + ["."]
            words += [PAD] * (SEQ_LEN - len(words))
            rows.append([TOKEN_ID[w] for w in words])
            # 主语动物 1/3，动词 1/3，宾语动物 1/2（从另外两种里选）
            probs.append(p_s / 3 * (1 / 3) * p_o / 2)
        self.sentences = torch.tensor(rows, dtype=torch.long)
        self.probs = torch.tensor(probs, dtype=torch.float64)
        assert abs(self.probs.sum().item() - 1) < 1e-12
        self.entropy = -(self.probs * self.probs.log()).sum().item()
        # 句子 → 编号 的查找表（判断一个生成的句子是第几个合法句子，用于算真实概率）
        self._index = {tuple(r): i for i, r in enumerate(rows)}

    # ------------------------------------------------------------------
    def sample(self, n, generator=None):
        """
        从真实分布抽 n 个句子。

        【返回】(n, 8) long
        【Python 小知识】torch.multinomial(p, n, replacement=True) 按概率 p 有放回地抽 n 个下标。
        """
        idx = torch.multinomial(self.probs, n, replacement=True, generator=generator)
        return self.sentences[idx]

    def log_prob(self, x):
        """
        每个句子在真实分布下的对数概率；不合法的句子返回 -inf。

        【参数】x：(n, 8) long
        【返回】(n,) float64
        """
        out = torch.full((x.shape[0],), -math.inf, dtype=torch.float64)
        for i, row in enumerate(x.tolist()):
            j = self._index.get(tuple(row))
            if j is not None:
                out[i] = self.probs[j].log()
        return out

    # ------------------------------------------------------------------
    @staticmethod
    def decode(x):
        """token 编号 → 文字（PAD 去掉；[MASK] 显示成 "■"）。x：一维的 8 个编号。"""
        words = []
        for i in x.tolist():
            if i == MASK_ID:
                words.append("■")
            elif i != TOKEN_ID[PAD]:
                words.append(VOCAB[i] if i < V else "?")
        return " ".join(words)

    @staticmethod
    def check(x):
        """
        判断一串 token 是否合法；不合法时说出第一条违反的规则。

        【思路】按语法从左往右"解析"：冠词 → 可选形容词 → 名词 → 动词 → 冠词 → 可选形容词 → 名词 → "." → 全是 PAD。
            任何一步对不上就是"结构"错误；结构对了再检查 R1–R3。
        【参数】x：一维的 8 个 token 编号（可以含 [MASK]，那一定不合法）
        【返回】(ok: bool, reason: str)。reason 取值：
            "ok" | "结构" | "冠词–名词的数（R1）" | "主谓一致（R2）" | "宾语与主语同种动物（R3）"
        【边界】只看第一处错误；R1 在主语和宾语里都会检查。
        """
        w = [VOCAB[i] if i < V else "■" for i in x.tolist()]
        pos = 0

        def parse_np():
            nonlocal pos
            if pos >= len(w) or w[pos] not in ("the", "a", "two"):
                return None
            det = w[pos]
            pos += 1
            if pos < len(w) and w[pos] in ADJS:
                pos += 1
            info = _noun_info(w[pos]) if pos < len(w) else None
            if info is None:
                return None
            pos += 1
            return det, info

        subj = parse_np()
        if subj is None or pos >= len(w):
            return False, "结构"
        verb = w[pos]
        pos += 1
        obj = parse_np()
        if obj is None or pos >= len(w) or w[pos] != ".":
            return False, "结构"
        if any(t != PAD for t in w[pos + 1:]):
            return False, "结构"
        if verb not in VERBS["sg"] + VERBS["pl"]:
            return False, "结构"
        for det, (_, num) in (subj, obj):
            if (det == "a" and num != "sg") or (det == "two" and num != "pl"):
                return False, "冠词–名词的数（R1）"
        if (verb in VERBS["sg"]) != (subj[1][1] == "sg"):
            return False, "主谓一致（R2）"
        if subj[1][0] == obj[1][0]:
            return False, "宾语与主语同种动物（R3）"
        return True, "ok"

    def validity(self, x):
        """
        一批句子的合法率，以及各类错误的比例。

        【返回】(rate: float, reasons: dict{原因: 比例})，比例相对于全部句子
        """
        counts = {}
        for row in x:
            _, reason = self.check(row)
            counts[reason] = counts.get(reason, 0) + 1
        n = x.shape[0]
        return counts.get("ok", 0) / n, {k: c / n for k, c in counts.items()}


if __name__ == "__main__":
    lang = ToyLanguage()
    print(f"合法句子 {len(lang.sentences)} 个，熵 {lang.entropy:.4f} nats / 句")
    x = lang.sample(5, torch.Generator().manual_seed(0))
    for row in x:
        print(" ", lang.decode(row), lang.check(row))
    bad = torch.tensor([[TOKEN_ID[t] for t in "a cats sees the dog . _ _".split()]])
    print(" ", lang.decode(bad[0]), lang.check(bad[0]))
