# -*- coding: utf-8 -*-
"""Recount the seventeen muqaṭṭaʿāt cases under all eight counting conventions.

Text: the Uthmani text shipped with quran-json (Tanzil lineage).

Scope rules, fixed across conventions and chosen because they reproduce the
chapter's four convergent counts exactly:
  * the basmala heads every sura in scope (it is the sura's first line in the
    Uthmani text) — without it cases 3 and 4 fall 1 and 35 short;
  * ة is a tāʾ, not a hāʾ;  ى is a written yāʾ unless a convention says
    otherwise; hamza on the line (ء) is not an alif.

Two documented variant readings, applied in every convention because they are
textual, not orthographic:
  * Ṣād cluster — the Tashkent codex reads bas(s)ṭatan at 7:69 with sīn: −1 ṣād.
  * Nūn (68) — the Ibn ʿAbbās reading spells the opening نون: +1 nūn.
"""
import json, re

Q = json.load(open('package/dist/quran.json'))
SURA = {c['id']: ''.join(v['text'] for v in c['verses']) for c in Q}
BASMALA = 'بِسۡمِ ٱللَّهِ ٱلرَّحۡمَٰنِ ٱلرَّحِيمِ'

WASLA, DAGGER, SHADDA, HAMZA_YA, MAQSURA = 'ٱ', 'ٰ', 'ّ', 'ئ', 'ى'
ALIF_BASE = 'اأإآ'
MARKS = set(range(0x064B, 0x0660)) | {0x0670, 0x0653, 0x0654, 0x0655, 0x0656,
                                      0x0657, 0x065E, 0x0640} | set(range(0x06D6, 0x06EE))
SIMPLE = {'l': 'ل', 'm': 'م', 'r': 'ر', 'sad': 'ص', 'n': 'ن', 'q': 'ق',
          'ayn': 'ع', 'sin': 'س', 'h': 'ه', 'k': 'ك', 'ha': 'ح',
          'ta': 'ط'}   # ta' is needed only for the Ta-Sin-Mim / Ta-Ha control cases

# Rule 2 (Appendix D, II): ta' marbuta (ة) is counted as ha'. In the undotted
# rasm ة and ه are one glyph, and at pause ة is uttered as ha'. Earlier runs of
# this script omitted the rule; only cases that count ha' are affected
# (K-H-Y-A-S in sura 19, and the Ta-Ha control). RULE2 = False reproduces the
# omission for comparison.
RULE2 = True
MARBUTA = 'ة'

# wasla, dagger, shadda, hamza-on-ya
CONV = {'T1': (1, 1, 1, 1), 'T2': (1, 1, 0, 0), 'T3': (1, 0, 1, 1),
        'T4': (1, 1, 0, 1), 'T5': (0, 1, 0, 0), 'T6': (0, 0, 0, 0),
        'T7': (1, 0, 0, 0), 'T8': (1, 0, 0, 1),
        'K':  (1, 0, 0, 1),   # ya rule matches T8; alif rule extended below
        # P is exploratory: the alif as a sound rather than a glyph. Every
        # written ā counts (base alif, madda, dagger alif); the waṣla alif is
        # elided in continuous recitation and the otiose alif of the wāw group
        # is silent, so neither counts. Non-alif letters are unaffected.
        'P': (0, 1, 0, 0)}
PHONETIC = 'P'
# K is Khalifa's published family rule (submission.org, "How to count the
# Quranic initials"): alif glyphs + wasla + madda + the free-standing hamza,
# counted as alif; hamza on a ya chair counts as ya, on a waw chair as waw;
# no dagger alif. Verified against the Submitters' 2002 published tables:
# lam/mim/ra match exactly in all eleven contested suras; the alif/hamza
# column lands within 2-23 per sura (Tanzil vs King Fahd hamza-seat encoding).
FAMILY = 'K'
OTIOSE = re.compile('\u0648[\u064B-\u0652\u06D6-\u06ED]*\u0627(?![\u0621-\u064A])')


def count(text, letter, conv):
    wasla, dagger, shadda, hamzaya = CONV[conv]
    if letter == 'alif':
        hits = set(ALIF_BASE) | ({WASLA} if wasla else set())
    elif letter == 'ya':
        hits = {'ي', MAQSURA} | ({HAMZA_YA} if hamzaya else set())
    elif letter == 'h' and RULE2:
        hits = {'ه', MARBUTA}
    else:
        hits = {SIMPLE[letter]}
    n = 0
    for i, ch in enumerate(text):
        if ch not in hits:
            continue
        n += 1
        if shadda:                       # a shadda'd letter is two letters
            j = i + 1
            while j < len(text) and ord(text[j]) in MARKS:
                if text[j] == SHADDA:
                    n += 1
                    break
                j += 1
    if letter == 'alif' and dagger:
        n += text.count(DAGGER)
    if letter == 'alif' and conv == FAMILY:
        n += text.count('\u0621')            # free-standing hamza counts as alif
        n += text.count('\u0654') + text.count('\u0655')   # seat-mark hamzas
    if letter == 'alif' and conv == PHONETIC:
        n -= len(OTIOSE.findall(text))      # the silent alif of ـوا
        n -= text.count('أ') + text.count('إ')   # hamza carriers, not a long ā
    return n


CASES = [
    (1,  [50], ['q']),                        (2,  [42], ['q']),
    (3,  [42], ['ayn', 'sin', 'q']),          (4,  list(range(40, 47)), ['ha', 'm']),
    (5,  [7, 19, 38], ['sad']),               (6,  [68], ['n']),
    (7,  [36], ['ya', 'sin']),                (8,  [19], ['k', 'h', 'ya', 'ayn', 'sad']),
    (9,  [2],  ['alif', 'l', 'm']),           (10, [3],  ['alif', 'l', 'm']),
    (11, [29], ['alif', 'l', 'm']),           (12, [30], ['alif', 'l', 'm']),
    (13, [31], ['alif', 'l', 'm']),           (14, [32], ['alif', 'l', 'm']),
    (15, [7],  ['alif', 'l', 'm', 'sad']),    (16, [13], ['alif', 'l', 'm', 'r']),
    (17, [10, 11, 12, 14, 15], ['alif', 'l', 'r']),
]

# Control cases (Appendix D III.b): sign-framed openings that contain NO alif.
# These are not among the seventeen the Code 19 literature claimed; they are the
# only place where 'opening type predicts convergence' and 'alif-freedom predicts
# convergence' make different predictions.
CONTROLS = [
    ('Ta-Sin-Mim 26+28', [26, 28],     ['ta', 'sin', 'm']),
    ('Ta-Sin 27',        [27],         ['ta', 'sin']),
    ('TSM+TS 26,27,28',  [26, 27, 28], ['ta', 'sin', 'm']),
    ('Ta-Ha 20',         [20],         ['ta', 'h']),
]
# Khalifa's overlapping claim (Appendix 1, Table 6): ha' over 19+20, ta' over
# 20,26,27,28, sin over 26,27,28, mim over 26,28 -> published 1,767 = 19 x 93.
# Not one of the seventeen; a constructed set, reported for transparency.
OVERLAP = [([19, 20], ['h']), ([20, 26, 27, 28], ['ta']), ([26, 27, 28], ['sin']), ([26, 28], ['m'])]
VARIANT = {5: -1, 6: +1}
PUBLISHED = {1: 57, 2: 57, 3: 209, 4: 2147, 5: 152, 6: 133, 7: 285, 8: 798,
             9: 9901, 10: 5652, 11: 1669, 12: 1253, 13: 823, 14: 577,
             15: 5313, 16: 1487, 17: 9483}

table = {}
DETAIL = {}
print(f"{'#':>2} " + ''.join(f'{k:>8s}' for k in CONV) + f"{'published':>11s}")
for n, suras, letters in CASES:
    text = ''.join(BASMALA + SURA[s] for s in suras)
    row = {c: sum(count(text, L, c) for L in letters) + VARIANT.get(n, 0) for c in CONV}
    detail = {c: {L: count(text, L, c) for L in letters} for c in CONV}
    for c in CONV:
        if VARIANT.get(n):
            k = 'sad' if n == 5 else 'n'
            detail[c][k] += VARIANT[n]
    table[n] = row
    DETAIL[n] = detail
    flag = '' if row['T8'] == PUBLISHED[n] else '  <-- differs'
    print(f'{n:>2} ' + ''.join(f'{row[c]:>8d}' for c in CONV) + f'{PUBLISHED[n]:>11d}{flag}')

json.dump({str(k): v for k, v in table.items()}, open('counts19.json', 'w'))
json.dump({str(k): v for k, v in DETAIL.items()}, open('counts19-detail.json', 'w'))
print('\ndivisible by 19:')
for c in CONV:
    hits = [n for n in table if table[n][c] % 19 == 0]
    print(f'  {c}: {len(hits):2d}/17  cases {hits}')

# ---- control cases: do sign-framed, alif-free openings converge? ----
print()
print('CONTROLS (Appendix D III.b) - sign-framed openings with no alif')
print(f"{'case':20s}" + ''.join(f'{k:>7s}' for k in CONV))
for nm, suras, letters in CONTROLS:
    text = ''.join(BASMALA + SURA[s] for s in suras)
    row = {c: sum(count(text, L, c) for L in letters) for c in CONV}
    print(f'{nm:20s}' + ''.join(f'{row[c]:>7d}' for c in CONV))
    print(f"{'   mod 19':20s}" + ''.join(f'{row[c] % 19:>7d}' for c in CONV))

# ---- Khalifa's overlapping set, and the effect of Rule 2 ----
print()
print("KHALIFA'S OVERLAPPING SET (H 19+20, T 20/26/27/28, S 26-28, M 26+28); published 1,767")
row = {c: sum(count(''.join(BASMALA + SURA[x] for x in suras), L, c) for suras, letters in OVERLAP for L in letters) for c in CONV}
print(f"{'overlap set':20s}" + ''.join(f'{row[c]:>7d}' for c in CONV))
print(f"{'   mod 19':20s}" + ''.join(f'{row[c] % 19:>7d}' for c in CONV))
RULE2 = False
print()
print('WITH RULE 2 SWITCHED OFF (ta marbuta not counted as ha) - for comparison only')
for nm, suras, letters in [('K-H-Y-A-S 19', [19], ['k', 'h', 'ya', 'ayn', 'sad']), ('Ta-Ha 20', [20], ['ta', 'h'])]:
    text = ''.join(BASMALA + SURA[x] for x in suras)
    r2 = {c: sum(count(text, L, c) for L in letters) for c in CONV}
    print(f'{nm:20s}' + ''.join(f'{r2[c]:>7d}' for c in CONV))
    print(f"{'   mod 19':20s}" + ''.join(f'{r2[c] % 19:>7d}' for c in CONV))
r2 = {c: sum(count(''.join(BASMALA + SURA[x] for x in suras), L, c) for suras, letters in OVERLAP for L in letters) for c in CONV}
print(f"{'overlap set':20s}" + ''.join(f'{r2[c]:>7d}' for c in CONV))
print(f"{'   mod 19':20s}" + ''.join(f'{r2[c] % 19:>7d}' for c in CONV))
RULE2 = True
