# -*- coding: utf-8 -*-
# Read — Appendix D II.b.2 (letter stability across the eight transmitted readings).
# Input: the KFGQPC eight-reading data, github.com/thetruetruth/quran-data-kfgqpc (commit 281dbbe8, Feb 2022).
# Usage: python3 letter_stability.py /path/to/quran-data-kfgqpc
import sys
KF = sys.argv[1] if len(sys.argv) > 1 else "quran-data-kfgqpc"
"""21 Oct 2026 — Letter stability across the whole Qur'an, four independent measures per letter.
Data: KFGQPC Unicode texts of eight riwāyāt (github.com/thetruetruth/quran-data-kfgqpc): Ḥafṣ v18 (with its
imlāʾī field), Shuʿba, Warsh, Qālūn, al-Dūrī, al-Sūsī, al-Bazzī, Qunbul.
Base-letter normalisation (the 'written letter' view, close to T8): ٱ أ إ آ→ا ; ؤ→و ; ئ ى ے ٮ→ي ; ة kept apart and
also reported merged into ه ; free hamza ء reported on its own (it is not part of the rasm).
  A. Reading/edition spread: per sūra (2-114) |count in riwāya − count in Ḥafṣ|, summed, worst riwāya, as % of Ḥafṣ count.
  B. Spelling layer: Ḥafṣ Uthmani vs Ḥafṣ imlāʾī (modern spelling), per verse |diff| summed, % of Uthmani count.
  C. Convention exposure (Ḥafṣ Uthmani): share of a letter's occurrences that some counting convention treats
     differently — shadda (counted twice under T1/T3), hamza seat, waṣla, alif maqṣūra, tāʾ marbūṭa — plus the
     small/dagger letters (ٰ ۥ ۦ ۧ ۨ) a convention may add.
  D. Dot dependence: share of occurrences whose undotted skeleton is shared with another letter in that position.
     ف/ق treated as one skeleton in every position (as Appendix D does); ن ي share the tooth only when joined forward.
"""
import json, collections, glob, re, csv, sys

FILES = {'Ḥafṣ':'hafs/data/hafsData_v18.json','Shuʿba':'shouba/data/ShoubaData08.json','Warsh':'warsh/data/warshData_v10.json',
         'Qālūn':'qaloon/data/QaloonData_v10.json','al-Dūrī':'doori/data/DooriData_v09.json','al-Sūsī':'soosi/data/SoosiData09.json',
         'al-Bazzī':'bazzi/data/BazziData_v07.json','Qunbul':'qumbul/data/QumbulData_v07.json'}
NORM = {'ٱ':'ا','أ':'ا','إ':'ا','آ':'ا','ؤ':'و','ئ':'ي','ى':'ي','ے':'ي','ٮ':'ي'}
LETTERS = list('ابتثجحخدذرزسشصضطظعغفقكلمنهويء') + ['ة']
MUQ = set('الرسصطعقكمنهيح')
def base(t):
    return [NORM.get(c, c) for c in t if c in NORM or c in LETTERS]
def sura_key(a): return a.get('sura_no', a.get('sora'))
data = {r: json.load(open(f'{KF}/{p}', encoding='utf8')) for r, p in FILES.items()}
# A. per-sura counts
def per_sura(d):
    out = collections.defaultdict(collections.Counter)
    for a in d: out[int(sura_key(a))].update(base(a['aya_text']))
    return out
PS = {r: per_sura(d) for r, d in data.items()}
H = PS['Ḥafṣ']; tot = collections.Counter()
for s in range(1, 115): tot.update(H[s])
A = {}
for L in LETTERS:
    worst = 0; who = ''
    for r in FILES:
        if r == 'Ḥafṣ': continue
        dev = sum(abs(PS[r][s][L] - H[s][L]) for s in range(2, 115))
        if dev > worst: worst, who = dev, r
    A[L] = (worst, who)
# B. Uthmani vs imlaei, per verse
B = collections.Counter()
for a in data['Ḥafṣ']:
    u = collections.Counter(base(a['aya_text'])); m = collections.Counter(base(a['aya_text_emlaey']))
    for L in LETTERS: B[L] += abs(u[L] - m[L])
# C. convention exposure, D. dot dependence (Ḥafṣ Uthmani)
C = collections.Counter(); CS = collections.Counter(); D = collections.Counter()
NONJOIN = set('ادذرزوءة') | {'ٱ','أ','إ','آ','ؤ','ى','ے'}
PAIRS = {'ب':'all','ت':'all','ث':'all','ن':'join','ي':'join','ج':'all','ح':'all','خ':'all','د':'all','ذ':'all','ر':'all','ز':'all',
         'س':'all','ش':'all','ص':'all','ض':'all','ط':'all','ظ':'all','ع':'all','غ':'all','ف':'all','ق':'all'}
SMALL = {'ٰ':'ا','ۥ':'و','ۦ':'ي','ۧ':'ي','ۨ':'ن'}
SEAT = {'أ','إ','آ','ؤ','ئ'}
for a in data['Ḥafṣ']:
    t = a['aya_text']
    for w in t.replace('\xa0', ' ').split(' '):
        idx = [i for i, c in enumerate(w) if c in NORM or c in LETTERS]
        for k, i in enumerate(idx):
            c = w[i]; L = NORM.get(c, c)
            nxt = w[i+1:idx[k+1]] if k + 1 < len(idx) else w[i+1:]
            if 'ّ' in nxt: CS[L] += 1
            if c in SEAT or c in 'ٱىة': C[L] += 1
            if L in PAIRS:
                joined = k + 1 < len(idx) and c not in NONJOIN
                if PAIRS[L] == 'all' or joined: D[L] += 1
        for c in w:
            if c in SMALL: C[SMALL[c]] += 1
rows = []
for L in LETTERS:
    n = tot[L]
    if not n: continue
    rows.append(dict(letter=L, muq='★' if L in MUQ else '', count=n,
        A_pct=round(100*A[L][0]/n, 2), A_worst=A[L][1], B_pct=round(100*B[L]/n, 2),
        C_pct=round(100*C[L]/n, 2), Cshadda_pct=round(100*CS[L]/n, 2), D_pct=round(100*D[L]/n, 1)))
w = csv.DictWriter(open('letter_stability.csv', 'w', encoding='utf8', newline=''), fieldnames=list(rows[0]))
w.writeheader(); w.writerows(rows)
for r in sorted(rows, key=lambda r: r['A_pct'] + r['B_pct'] + r['C_pct']):
    print(f"{r['letter']} {r['muq']:1} {r['count']:6}  A {r['A_pct']:6.2f}% ({r['A_worst']:8})  B {r['B_pct']:6.2f}%  C {r['C_pct']:6.2f}%  Csh {r['Cshadda_pct']:6.2f}%  D {r['D_pct']:5.1f}%")
