"""File-derived terrain sampling and client WorldToCam projection.

Runtime uses the exported projected marker positions. These helpers are pure;
they never query a game process, infer unlocks, or create server state.
"""
import math
import struct


class HeightMap:
    def __init__(self,data):
        cx,cz,sx,sz,rotation,nx,nz,low,high,count=struct.unpack_from('<5f2i2fi',data)
        if rotation!=0 or min(nx,nz)<=0 or count!=nx*nz or len(data)!=40+count*4:
            raise ValueError('Unsupported height-map layout/rotation')
        self.origin=(cx-sx/2,cz-sz/2);self.size=(sx,sz);self.resolution=(nx,nz)
        self.heights=struct.unpack_from('<'+str(count)+'f',data,40)
        if not all(math.isfinite(x) for x in self.heights):raise ValueError('Nonfinite terrain height')

    def sample(self,position):
        x,z=position;nx,nz=self.resolution
        fx=(x-self.origin[0])/self.size[0]*nx-.5
        fz=(z-self.origin[1])/self.size[1]*nz-.5
        # Client WorldToLocal uses cvttss2si; NativeHeightMapData clamps samples.
        ix,iz=int(fx),int(fz);tx,tz=fx-ix,fz-iz
        def at(x,z):return self.heights[max(0,min(nz-1,z))*nx+max(0,min(nx-1,x))]
        def lerp(a,b,t):return a+(b-a)*max(0,min(1,t))
        return lerp(lerp(at(ix,iz),at(ix,iz+1),tz),lerp(at(ix+1,iz),at(ix+1,iz+1),tz),tx)


def world_to_map(position,height,config):
    x,z=position;m=config['WorldToCam']
    # Client transposes serialization into its matrix storage, then multiplies
    # the 3D world point by rows of this serialized matrix. Map is X/Y camera.
    padding=config.get('TilemapWorldRangePadding',{}) if config.get('SourceZoomLevel',0)>0 else {}
    return [m[f'M{row}1']*x+m[f'M{row}2']*height+m[f'M{row}3']*z+m[f'M{row}4']+padding.get(axis,0)
            for row,axis in ((1,'X'),(2,'Y'))]


def map_to_uv(position,size):return [position[0]/size+.5,.5-position[1]/size]
