"""Reproduce Session 32's coordinates, contexts, hashes and exact-pixel crops.
Requires Pillow, NumPy and SciPy. Visual rune readings remain human observations.
"""
from pathlib import Path
import hashlib,json,re
import numpy as np
from PIL import Image
from scipy.ndimage import label,find_objects
ROOT=Path(__file__).resolve().parent
R='ᚠᚢᚦᚩᚱᚳᚷᚹᚻᚾᛁᛄᛇᛈᛉᛋᛏᛒᛖᛗᛚᛝᛟᛞᚪᚫᚣᛡᛠ'
LET='F U TH O R K G W H N I J EO P X S T B E M L ING OE D A AE Y IO EA'.split()
raw=(ROOT/'liber-primus__transcription--master.txt').read_text()
lp=raw[raw.index('ᚱ-'):].split('%')[:73][15:]
def runes(s):return ''.join(c for c in s if c in R)
def sha(p):return hashlib.sha256(p.read_bytes()).hexdigest()
specs=[(27,44,28,(830,900,1820,1250),(1681,925,1725,1025),2,19),(55,72,70,(580,1040,1820,1430),(1025,1245,1080,1345),4,76)]
out={'scope':'Retained archive copies, not new authentication of Cicada origin. Coordinates zero-based; crop boxes right/bottom exclusive. Lines one-based. Offsets count runes only.', 'master_sha256':sha(ROOT/'liber-primus__transcription--master.txt'),'anchor_runes':'ᛞᛄᚢᛒᛖᛁ','anchor_labels':['D','J','U','B','E','I'],'anchor_values_mod29':[23,11,1,17,18,10],'occurrences':[]}
for page,n,offset,crop,mark,line,mark_offset in specs:
 text=lp[page];seq=runes(text);im=Image.open(ROOT/f'{n}.jpg').convert('RGB')
 assert seq[offset:offset+6]==out['anchor_runes']
 words=[];count=0
 for w in re.split(r'[-.,&$§;]',text):
  v=runes(w)
  if v:words.append({'start':count,'runes':v,'labels':[LET[R.index(c)] for c in v]});count+=len(v)
 i=next(i for i,w in enumerate(words) if w['start']==offset)
 assert words[i]['runes']=='ᛞᛄᚢ' and words[i+1]['runes']=='ᛒᛖᛁ'
 im.crop(crop).save(ROOT/f'phrase_context_{n}.png')
 a=np.asarray(im.crop(mark));mask=(a[:,:,0]>130)&(a[:,:,1]<100)&(a[:,:,2]<100)
 lab,_=label(mask);components=[]
 for k,s in enumerate(find_objects(lab),1):
  area=int((lab[s]==k).sum())
  if area>=20:
   y,x=s;components.append({'box':[x.start+mark[0],y.start+mark[1],x.stop+mark[0],y.stop+mark[1]],'red_pixels':area})
 assert len(components)==13
 out['occurrences'].append({'lp2_page':page,'image':f'{n}.jpg','image_sha256':sha(ROOT/f'{n}.jpg'),'dimensions':list(im.size),'page_rune_offset':offset,'text_line':line,'page_rune_count':len(seq),'section':6 if page==27 else 8,'section_rune_offset':28 if page==27 else 3310,'context_words':words[max(0,i-3):i+5],'crop_box':list(crop),'crop_file':f'phrase_context_{n}.png','crop_sha256':sha(ROOT/f'phrase_context_{n}.png'),'red_mark_box':list(mark),'red_mark_rune_offset':mark_offset,'red_dot_count':len(components),'red_components':components,'visual_reading':'All six black runes and internal single-dot word separator agree with master transcription. Single black dot before D. '+('Single black dot after I; text continues. Preceding complete word B F.' if page==27 else 'Red 13-dot mark after I; end of text. Preceding complete word K EA I M K X spans lines 3 and 4.')})
a,b=runes(lp[27]),runes(lp[55])
out['preceding_28_aligned_matches']=[{'relative_offset':d,'rune':a[28+d]} for d in range(-28,0) if a[28+d]==b[70+d]]
out['right_extension']='Unavailable within section 8: second anchor ends the section. No comparison to the next solved section is made.'
out['transcription_corrections']=[]
out['image_provenance']='Archive URLs recorded in provenance.json for 44.jpg and 72.jpg; source copies unchanged.'
(ROOT/'phrase_image_results.json').write_text(json.dumps(out,ensure_ascii=False,indent=2)+'\n')
print(json.dumps({'anchors':len(out['occurrences']),'red_dots':[x['red_dot_count'] for x in out['occurrences']],'preceding_matches':len(out['preceding_28_aligned_matches']),'corrections':0}))
