"""
=============================================================================
第 5 讲 · 工具模块：一门带"长距离主谓一致"的玩具语言
=============================================================================

对应讲义：lectures/lecture05_language_models_rnn.md 第 2、8 节
被本目录（以及第 6 讲）的代码使用。

-----------------------------------------------------------------------------
【为什么要造一门玩具语言？】
-----------------------------------------------------------------------------
语言模型要学的东西五花八门，很难单独测量"它记不记得住远处的信息"。
英语里有一个干净的测试：**主谓一致**（subject–verb agreement）。
    the dog runs .         单数主语 → 动词加 s
    the dogs run .         复数主语 → 动词不加 s
如果主语和动词之间隔了很多词，模型要选对动词形式，就必须"记住"很早以前出现的主语是单数还是复数：
    the dog near the old trees that the girls like runs .
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    中间插入了 9 个词，其中 trees、girls 都是复数（叫"干扰名词"，attractor），
    但动词仍然要和最前面的单数主语 dog 一致。
Linzen、Dupoux 和 Goldberg（2016）就用这个任务来测试 RNN 能否学到长距离的语法依赖。

-----------------------------------------------------------------------------
【语法】
-----------------------------------------------------------------------------
    句子   → 主语短语 修饰语* 主动词短语 .
    主语短语 → the (形容词) 名词                  名词是单数或复数
    修饰语 → 介词短语："near the (形容词) 名词"      （名词的单复数随机 → 可能成为干扰名词）
           | 宾语关系从句："that the 名词 动词"     （从句里的动词和从句主语一致！）
           | 主语关系从句："that 动词 the 名词"     （从句动词和主句主语一致）
    主动词短语 → 不及物动词 (副词) | is/are 形容词     **和主句主语一致**

    可以控制修饰语的个数，从而控制主语和主动词之间的距离。

【用法】
    from toy_language import generate_corpus, agreement_test_set, Vocab
    train = generate_corpus(20000, seed=0)          # list[list[str]]
    vocab = Vocab(train)
    tests = agreement_test_set(n_modifiers=2, n=500)  # 主谓一致测试题
"""

import random

NOUNS = [("dog", "dogs"), ("cat", "cats"), ("boy", "boys"), ("girl", "girls"), ("teacher", "teachers"),
         ("farmer", "farmers"), ("bird", "birds"), ("king", "kings"), ("doctor", "doctors"), ("student", "students")]
PLACES = [("house", "houses"), ("tree", "trees"), ("garden", "gardens"), ("river", "rivers"), ("table", "tables"),
          ("road", "roads")]
ADJECTIVES = ["old", "small", "happy", "tall", "red", "quiet", "young", "angry"]
# 不及物动词：(单数形式, 复数形式)
INTRANSITIVE = [("runs", "run"), ("sleeps", "sleep"), ("sings", "sing"), ("laughs", "laugh"), ("waits", "wait"),
                ("smiles", "smile")]
# 及物动词：用于关系从句
TRANSITIVE = [("likes", "like"), ("sees", "see"), ("chases", "chase"), ("helps", "help"), ("knows", "know")]
ADVERBS = ["quickly", "loudly", "often", "again"]
COPULA = ("is", "are")
PRED_ADJ = ["happy", "tired", "ready", "hungry"]

SING, PLUR = 0, 1


# =============================================================================
# 第 1 部分：生成
# =============================================================================
def noun_phrase(rng, pairs, number, adj_prob=0.3):
    """
    生成 "the (形容词) 名词"。

    【参数】
        rng      : random.Random
        pairs    : 名词对列表，每个元素是 (单数, 复数)
        number   : SING 或 PLUR
        adj_prob : 加一个形容词的概率

    【返回】list[str]
    """
    words = ["the"]
    if rng.random() < adj_prob:
        words.append(rng.choice(ADJECTIVES))
    words.append(rng.choice(pairs)[number])
    return words


def modifier(rng, head_number):
    """
    生成一个插在主语和主动词之间的修饰语，并返回其中名词的单复数（用于统计干扰名词）。

    【参数】
        rng         : random.Random
        head_number : 主句主语的单复数（主语关系从句的动词要和它一致）

    【返回】(words, nouns_numbers)：词列表，以及修饰语中每个名词的单复数列表
    """
    kind = rng.random()
    n = rng.choice([SING, PLUR])                       # 修饰语里那个名词的单复数，随机
    if kind < 0.4:                                     # 介词短语
        return ["near"] + noun_phrase(rng, PLACES, n), [n]
    if kind < 0.7:                                     # 宾语关系从句：动词与从句主语一致
        return ["that"] + noun_phrase(rng, NOUNS, n, adj_prob=0.0) + [rng.choice(TRANSITIVE)[n]], [n]
    return ["that", rng.choice(TRANSITIVE)[head_number]] + noun_phrase(rng, NOUNS, n, adj_prob=0.0), [n]


def main_vp(rng, number):
    """生成与主语一致的主动词短语，返回 (词列表, 主动词在列表中的位置 0)。"""
    if rng.random() < 0.7:
        words = [rng.choice(INTRANSITIVE)[number]]
        if rng.random() < 0.3:
            words.append(rng.choice(ADVERBS))
    else:
        words = [COPULA[number], rng.choice(PRED_ADJ)]
    return words


def make_sentence(rng, n_modifiers):
    """
    生成一个句子，返回 (词列表, 信息字典)。

    【信息字典】
        verb_pos    : 主动词的位置（下标）
        number      : 主语的单复数
        subj_pos    : 主语名词的位置
        attractors  : 修饰语中与主语单复数**不同**的名词个数（干扰名词）
    """
    number = rng.choice([SING, PLUR])
    words = noun_phrase(rng, NOUNS, number)
    subj_pos = len(words) - 1
    attractors = 0
    for _ in range(n_modifiers):
        w, nums = modifier(rng, number)
        words += w
        attractors += sum(1 for x in nums if x != number)
    verb_pos = len(words)
    words += main_vp(rng, number) + ["."]
    return words, {"verb_pos": verb_pos, "number": number, "subj_pos": subj_pos, "attractors": attractors}


def generate_corpus(n_sentences=20000, seed=0, modifier_probs=(0.4, 0.3, 0.2, 0.1)):
    """
    生成训练语料。

    【参数】
        n_sentences    : 句子数
        seed           : 随机种子（讲义里的数字依赖默认值）
        modifier_probs : 修饰语个数为 0、1、2、3 的概率。训练语料里**最多 3 个修饰语**，
                         测试时可以用 4 个来检验模型能否推广到更长的距离。

    【返回】list[list[str]]
    """
    rng = random.Random(seed)
    out = []
    for _ in range(n_sentences):
        k = rng.choices(range(len(modifier_probs)), weights=modifier_probs)[0]
        out.append(make_sentence(rng, k)[0])
    return out


def agreement_test_set(n_modifiers, n=400, seed=123):
    """
    生成主谓一致测试题：给模型看主动词之前的部分，比较正确形式和错误形式哪个概率更高。

    【参数】
        n_modifiers : 主语和主动词之间插入几个修饰语
        n           : 题目数
        seed        : 随机种子（和训练语料不同）

    【返回】list[dict]，每个元素：
        prefix      : list[str]，主动词之前的所有词
        correct     : str，正确的动词形式
        wrong       : str，错误的动词形式（单复数相反）
        distance    : int，主语名词和主动词之间隔了几个词
        attractors  : int，干扰名词的个数
    """
    rng = random.Random(seed + 1000 * n_modifiers)
    tests = []
    for _ in range(n):
        words, info = make_sentence(rng, n_modifiers)
        v = info["verb_pos"]
        verb = words[v]
        pair = next(p for p in INTRANSITIVE + [COPULA] if verb in p)
        tests.append({"prefix": words[:v], "correct": verb, "wrong": pair[1 - info["number"]],
                      "distance": v - info["subj_pos"] - 1, "attractors": info["attractors"]})
    return tests


# =============================================================================
# 第 2 部分：词表
# =============================================================================
class Vocab:
    """
    词 ↔ 编号。编号 0 是 <pad>（批处理时的填充），1 是 <s>（句子开始），2 是 </s>（句子结束），3 是 <unk>。

    【用法】
        vocab = Vocab(train_sentences)
        ids = vocab.encode(["the", "dog", "runs", "."])     # 自动在前后加 <s>、</s>
        vocab.decode(ids)
    """
    PAD, BOS, EOS, UNK = "<pad>", "<s>", "</s>", "<unk>"

    def __init__(self, sentences):
        self.itos = [self.PAD, self.BOS, self.EOS, self.UNK] + sorted(set(w for s in sentences for w in s))
        self.stoi = {w: i for i, w in enumerate(self.itos)}

    def __len__(self):
        return len(self.itos)

    def encode(self, words, add_bos=True, add_eos=True):
        """词列表 → 编号列表。"""
        ids = [self.stoi.get(w, self.stoi[self.UNK]) for w in words]
        return ([self.stoi[self.BOS]] if add_bos else []) + ids + ([self.stoi[self.EOS]] if add_eos else [])

    def decode(self, ids):
        """编号列表 → 词列表（去掉特殊符号）。"""
        return [self.itos[i] for i in ids if self.itos[i] not in (self.PAD, self.BOS, self.EOS)]


if __name__ == "__main__":
    corpus = generate_corpus()
    vocab = Vocab(corpus)
    lengths = [len(s) for s in corpus]
    print(f"{len(corpus)} 句，{sum(lengths)} 个词，词表 {len(vocab)} 个（含 4 个特殊符号），平均句长 {sum(lengths) / len(lengths):.1f}")
    for s in corpus[:6]:
        print("   ", " ".join(s))
    for k in [0, 2, 4]:
        t = agreement_test_set(k, n=1)[0]
        print(f"\n修饰语 {k} 个：{' '.join(t['prefix'])} ___  （正确 {t['correct']}，错误 {t['wrong']}，"
              f"距离 {t['distance']}，干扰名词 {t['attractors']} 个）")
