"""Registered CC0 scan-derived tile shelf. Runs with Python + numpy/Pillow/requests."""
import json, hashlib
from pathlib import Path
import requests
import numpy as np
from PIL import Image, ImageFilter
ROOT=Path(__file__).resolve().parent
rng=np.random.default_rng(2710)
NAMES=['Render_white','Render_grey','Concrete_boardmarked','Larch_cladding','Larch_cladding_charred','Bronze_aluminium','Glass_clear','Steel_dark','Zinc_coping','Roof_gravel','Paving_concrete','Door_paint']
(ROOT/"textures").mkdir(exist_ok=True)
sources={}
for asset in ['plastered_wall_04','concrete_wall_006','rough_wood','gravelly_sand']:
 doc=requests.get('https://api.polyhaven.com/files/'+asset,timeout=30).json()
 sources[asset]={}
 for kind,key in [('BaseColor','Diffuse'),('Height','Displacement'),('ORM','arm')]:
  choices=doc[key]['1k'];entry=choices.get('jpg') or choices.get('png');path=ROOT/'textures'/f'{asset}_{kind}{Path(entry["url"]).suffix}'
  if not path.exists():
   r=requests.get(entry['url'],timeout=90);r.raise_for_status();path.write_bytes(r.content)
  sources[asset][kind]={'path':str(path.relative_to(ROOT)),'url':entry['url'],'sha256':hashlib.sha256(path.read_bytes()).hexdigest()}

def periodic(a):
 # Exact wrap value with 8 pixel smooth correction, no mirroring.
 a=a.copy()
 for axis in [0,1]:
  t=np.swapaxes(a,0,axis);d=(t[-1]-t[0])*.5
  for i in range(8):
   w=(1-i/8)**2*(1+2*i/8);t[i]+=d*w;t[-1-i]-=d*w
 return a

def scan(asset,kind,n):
 return np.array(Image.open(ROOT/sources[asset][kind]['path']).convert('RGB').resize((n,n),Image.Resampling.LANCZOS)).astype(float)/255

def save(path,a,height=False):
 if height:Image.fromarray(np.uint16(np.clip(a,0,1)*65535)).save(path)
 else:Image.fromarray(np.uint8(np.clip(a,0,1)*255+.5)).save(path)

def normal(h,depth,tile):
 dy,dx=np.gradient(h, tile/(len(h)-1));v=np.stack([-dx*depth,dy*depth,np.ones_like(h)],-1)
 v/=np.linalg.norm(v,axis=2,keepdims=True);return periodic(v*.5+.5)
shelf={}
for i,name in enumerate(NAMES):
 n=1024 if i in [0,1,2,3,4,9,10] else 256
 tile=[2,2,2.048,1.024,1.024,1,1,1,1,2,2,1][i]
 yy,xx=np.mgrid[0:n,0:n]/(n-1)
 height=np.zeros((n,n))+.5;depth=0;metal=0;rough=np.zeros((n,n))+.65
 if i in [0,1,2,9,10]:
  asset='plastered_wall_04' if i<2 else ('gravelly_sand' if i==9 else 'concrete_wall_006')
  col=scan(asset,'BaseColor',n);lum=col.mean(2);h=scan(asset,'Height',n).mean(2);rough=scan(asset,'ORM',n)[:,:,1]
  base=np.array(([.83,.82,.78] if i==0 else [.42,.44,.43]) if i<2 else ([.63,.61,.55] if i==2 else ([.40,.39,.35] if i==9 else [.57,.56,.53])))
  col=base[None,None,:]*np.clip((lum/(lum.mean()+1e-6))[:,:,None],.7,1.25)
  height=h;depth=.0012 if i<2 else (.008 if i==9 else .003)
  rough=np.clip(rough*.3+.56,.55,.94)
  if i==2:
   # 128 mm form boards, 512 mm tie grid. Height and albedo share masks.
   phase=(xx*16)%1;seam=np.exp(-((np.minimum(phase,1-phase))/.024)**2)
   grain=np.sin(xx*1800+np.sin(yy*18)*2)*.014+np.sin(xx*420)*.022
   height=.62+.12*(h-.5)+grain-seam*.15
   shade=.97+.035*np.sin(np.floor(xx*16)*4.71)
   col=base[None,None,:]*(.72+.28*np.clip((lum/lum.mean())[:,:,None],.7,1.25))
   holes=np.zeros_like(h)
   for a in [.125,.375,.625,.875]:
    for b in [.125,.375,.625,.875]:
     rad=np.sqrt((xx-a)**2+(yy-b)**2);holes=np.maximum(holes,np.clip((.008-rad)/.002,0,1))
   height-=holes*.48;depth=.009;col*=shade[:,:,None]*(1-seam[:,:,None]*.06-holes[:,:,None]*.22)
 elif i in [3,4]:
  asset='rough_wood';raw=scan(asset,'BaseColor',n);rawh=scan(asset,'Height',n).mean(2)
  # Sample independent narrow grain strips; explicit 120 mm board + 8 mm gap.
  col=np.zeros_like(raw);height=np.zeros((n,n));width=n/8
  for b in range(8):
   x0=round(b*width);x1=round((b+1)*width);src=(b*97)%750
   strip=Image.fromarray(np.uint8(raw[110:465,src:src+90]*255)).resize((x1-x0,n))
   lum=np.array(strip).mean(2)/255;lum=lum/(lum.mean()+1e-6)
   base=np.array([.46,.39,.31] if i==3 else [.10,.09,.08])*(.94+.10*np.sin(b*17))
   col[:,x0:x1]=np.clip(.55+.45*lum[:,:,None],.88,1.12)*base
   height[:,x0:x1]=.86+np.clip(lum-.9,-.2,.2)*.08
  phase=(xx*8)%1;edge=np.minimum(phase,1-phase);gap=np.clip((.03125-edge)/.007,0,1)
  col*=1-gap[:,:,None]*.87;height-=gap*.72;depth=.009;rough=np.zeros_like(height)+(.73 if i==3 else .85)
 else:
  base={5:[.19,.145,.105],6:[.26,.32,.34],7:[.105,.12,.13],8:[.49,.53,.55],11:[.79,.79,.75]}[i]
  noise=rng.normal(0,.002,(n,n));col=np.ones((n,n,3))*base+noise[:,:,None]
  rough=np.zeros((n,n))+{5:.28,6:.10,7:.37,8:.39,11:.48}[i];metal=1 if i in [5,7,8] else (.40 if i==6 else 0)
 col=periodic(col);height=periodic(height);rough=periodic(rough)
 norm=normal(height,depth,tile);orm=np.stack([np.ones_like(height),rough,np.ones_like(height)*metal],-1)
 folder=ROOT/'materials'/name;folder.mkdir(parents=True,exist_ok=True)
 for k,a in [('BaseColor',col),('Normal',norm),('ORM',orm)]:save(folder/(k+'.png'),a)
 spec={'tile_size_metres':tile,'resolution':n,'maps':{k:f'materials/{name}/{k}.png' for k in ['BaseColor','Normal','ORM']},'normal_convention':'OpenGL tangent +Y','ORM_channels':{'R':'occlusion','G':'roughness','B':'metallic'},'wrap':'REPEAT','color_space':{'BaseColor':'sRGB','Normal':'linear','ORM':'linear'},'base_color_mean':col.mean((0,1)).tolist(),'roughness_mean':float(rough.mean()),'metalness_mean':metal}
 if depth:
  save(folder/'Height.png',height,True);spec.update(height_map=f'materials/{name}/Height.png',height_encoding='16-bit grayscale',displacement_scale_metres=depth,displacement_bias_metres=-depth*.5,height_formula=f'outward_metres = (Height - 0.5) * {depth}')
 if i in [3,4]:spec.update(board_width_metres=.120,shadow_gap_metres=.008,grain_direction='+V',tile_boards=8)
 shelf[name]=spec
(ROOT/'materials.json').write_text(json.dumps({'schema':'forge.material-shelf.v1','materials':shelf,'runtime_binding':'Bind by exact name; UV0 already divided by tile size. RepeatWrapping, flipY=false, BaseColor sRGB, Normal/ORM linear. roughness=metalness=1 with ORM. Multiply vertex COLOR_0 for planter grasses/soil; other vertices are white. Height for generated walls only; normal carries relief on atoms. Glass is deliberately opaque dark reflective glazing for closed buildings, not transmissive interior glass.','provenance':{'license':'CC0-1.0 for Poly Haven scans; authored profile masks and metal/paint swatches generated by this builder','sources':sources,'note':'Scan-derived adaptations, not measured larch species or measured architectural formwork.'}},indent=2))
print('MATERIALS_READY',len(shelf))
