"""Session 18: source-pixel measurements and diagnostic plots, not restoration.
Requires Pillow, NumPy, SciPy and Matplotlib. Original inputs are never modified.
"""
import hashlib, json
from pathlib import Path
import numpy as np
from PIL import Image
from scipy import ndimage
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from lp_core import lp, R, vals

ROOT=Path(__file__).resolve().parent
TREE_BOX=(640,2020,1770,3200)
TEXT_BOX=(585,650,1810,1750)
THRESHOLDS=[128,160,192,224]
# Visual alignment to transcribed rune counts; not OCR-inferred boundaries.
ALIGN={32:[121],33:[10,91,99],34:[],35:[],36:[99],37:[],38:[186],39:[119,122,240]}

def groups(page, dominance=65):
    a=np.asarray(Image.open(ROOT/f'{page+17}.jpg').convert('RGB')).astype(int)
    mask=(a[:,:,0]>100)&(a[:,:,0]-a[:,:,1]>dominance)&(a[:,:,0]-a[:,:,2]>dominance)
    labels,n=ndimage.label(mask); dots=[]
    for k,bb in enumerate(ndimage.find_objects(labels),1):
        if bb is None:continue
        sy,sx=bb;area=int(np.count_nonzero(labels[bb]==k))
        if 15<=area<=200 and sx.stop-sx.start<=18 and sy.stop-sy.start<=18:
            dots.append([sx.start,sy.start,sx.stop,sy.stop,area])
    joined=[]
    for d in sorted(dots,key=lambda d:d[1]):
        near=[g for g in joined if any(abs(d[0]-e[0])<28 and abs(d[1]-e[1])<28 for e in g)]
        if not near:joined.append([d])
        else:
            g=near[0];g.append(d)
            for other in near[1:]:g.extend(other);joined.remove(other)
    out=[]
    for g in joined:
        if len(g)<3:continue # Excludes red single word separators.
        out.append(dict(lp2_page=page,archive_file=f'{page+17}.jpg',dots=len(g),
            bbox=[min(d[0] for d in g),min(d[1] for d in g),max(d[2] for d in g),max(d[3] for d in g)],
            dot_boxes=g))
    return sorted(out,key=lambda g:g['bbox'][1])

def punctuation():
    records=[];threshold_checks=[]
    for p in range(32,40):
        base=groups(p)
        assert len(base)==len(ALIGN[p]),(p,len(base))
        for dom in [40,65,90]:
            counts=[g['dots'] for g in groups(p,dom)]
            assert counts==[g['dots'] for g in base],(p,dom,counts)
            threshold_checks.append(dict(page=p,red_dominance=dom,counts=counts))
        for g,offset in zip(base,ALIGN[p]):
            g['page_rune_offset']=offset
            n=0;punct='';found=False
            for ch in lp[p]:
                if ch in R:
                    if n==offset:break
                    n+=1
                elif n==offset:punct+=ch
            assert '.' in punct,(p,offset,punct)
            g['transcribed_delimiters']=''.join(c for c in punct if c in '.&$§')
            if p>=33:
                g['section_offset']=sum(len(vals(lp[j])) for j in range(33,p))+offset-91
            records.append(g)
    fig,axes=plt.subplots(1,len(records),figsize=(15,3.5),layout='constrained')
    for ax,g in zip(axes,records):
        x0,y0,x1,y1=g['bbox']
        # Plot native decoded pixels in each measured bounding rectangle.
        a=np.asarray(Image.open(ROOT/g['archive_file']).convert('RGB'))
        ax.imshow(a[y0-6:y1+6,x0-6:x1+6],interpolation='nearest')
        ax.set_title(f"LP2 {g['lp2_page']}\n{g['dots']} dots",fontsize=11)
        ax.set_xlabel(f"rune {g['page_rune_offset']}",fontsize=10);ax.set_xticks([]);ax.set_yticks([])
    fig.suptitle('Red multi-dot punctuation: original pixel samples; rune offsets are page-local',fontsize=14)
    fig.savefig(ROOT/'punctuation_samples.png',dpi=160);plt.close(fig)
    return records,threshold_checks

def topology(gray):
    results=[]
    for t in THRESHOLDS:
        ink=gray<=t
        lab,n=ndimage.label(ink,structure=np.ones((3,3)))
        sizes=np.bincount(lab.ravel())[1:]
        back,bn=ndimage.label(~ink) # Four-connected background, eight-connected ink.
        border=np.unique(np.r_[back[0,:],back[-1,:],back[:,0],back[:,-1]])
        counts=np.bincount(back.ravel());interior=[int(counts[k]) for k in range(1,bn+1) if k not in border]
        results.append(dict(threshold=t,ink_components=int(n),
            ink_components_at_least_25_pixels=int((sizes>=25).sum()),
            largest_ink_component_pixels=int(sizes.max()) if len(sizes) else 0,
            enclosed_white_components=len(interior),
            enclosed_white_components_at_least_25_pixels=sum(s>=25 for s in interior),
            largest_enclosed_white_pixels=max(interior,default=0)))
    return results

def tree_plots():
    rgb=np.asarray(Image.open(ROOT/'49.jpg').convert('RGB'))
    # Integer luminance approximation, also used by the website viewer.
    # Promote all channels first; unsigned-byte arithmetic must not overflow.
    a=rgb.astype(np.int32);gray=((299*a[:,:,0]+587*a[:,:,1]+114*a[:,:,2]+500)//1000).astype(np.uint8)
    x0,y0,x1,y1=TREE_BOX;g=gray[y0:y1,x0:x1]
    panels=[('Original luminance',g,'gray'),('Brightness inverted',255-g,'gray'),
        ('Rotated 180 degrees',np.rot90(g,2),'gray'),('Mirrored left/right',g[:,::-1],'gray'),
        ('Ink threshold 160',np.where(g<=160,0,255),'gray'),
        ('White-space threshold 224',np.where(g>224,0,255),'gray')]
    fig,axes=plt.subplots(2,3,figsize=(12,8.8),layout='constrained')
    for ax,(title,z,cmap) in zip(axes.flat,panels):
        ax.imshow(z,cmap=cmap,vmin=0,vmax=255,interpolation='nearest');ax.set_title(title,fontsize=13);ax.axis('off')
    fig.suptitle('Archive 49.jpg / LP2 32: fixed source-pixel views of the tree',fontsize=15)
    fig.savefig(ROOT/'tree_pixel_views.png',dpi=150);plt.close(fig)
    x0,y0,x1,y1=TEXT_BOX;text=gray[y0:y1,x0:x1]
    fig,axes=plt.subplots(1,2,figsize=(12,6),layout='constrained')
    for ax,z,title in zip(axes,[text,255-text],['Text and inter-line spaces','Same pixels, brightness inverted']):
        ax.imshow(z,cmap='gray',vmin=0,vmax=255,interpolation='nearest');ax.set_title(title);ax.axis('off')
    fig.suptitle('LP2 32: whitespace inspection; no glyphs traced or reconstructed',fontsize=15)
    fig.savefig(ROOT/'text_whitespace_views.png',dpi=150);plt.close(fig)
    return topology(g)

def main():
    records,checks=punctuation();measurements=tree_plots()
    source_names=[f'{p}.jpg' for p in range(49,57)]+['tree_user_upload.png','liber-primus__transcription--master.txt']
    report=dict(source_sha256={n:hashlib.sha256((ROOT/n).read_bytes()).hexdigest() for n in source_names},
        tree_archive='49.jpg',tree_lp2_page=32,tree_bbox=TREE_BOX,text_bbox=TEXT_BOX,
        red_punctuation=records,red_threshold_checks=checks,tree_thresholds=measurements,
        scope='LP2 32-39, red multi-dot punctuation only; red headings and Arabic numerals excluded.',
        limits=['No deblurring, inpainting, learned restoration, glyph classifier, or hidden-message claim.',
            'Thresholds inspect topology, not all possible open negative-space shapes.',
            'Source alignment and semantic boundary roles are manually interpreted.'])
    (ROOT/'visual_audit_results.json').write_text(json.dumps(report,indent=2,ensure_ascii=False))
    print(json.dumps({'red_marks':[(x['lp2_page'],x['dots'],x['page_rune_offset'],x['transcribed_delimiters']) for x in records],
        'tree_thresholds':measurements},ensure_ascii=False,indent=2))

if __name__=='__main__':main()
