/* ============================================================
   Wildlife Kingdom — js/world.js
   Tile grid · terrain/paths/fences/objects · buildings ·
   habitat region detection · pathfinding (visitors/staff/animals)
   ============================================================ */
(function(){
'use strict';
const WK = window.WK;
const D = WK.Data;

WK.GIDS = Object.keys(D.GROUND);
WK.PIDS = Object.keys(D.PATH);
WK.NIDS = Object.keys(D.NATURE);

class World{
  constructor(cols,rows){
    this.cols=cols; this.rows=rows;
    const n=cols*rows;
    this.ground=new Uint8Array(n);
    this.path=new Uint8Array(n);
    this.obj=new Uint8Array(n);
    this.objVar=new Uint8Array(n);
    this.fenceS=new Uint8Array(n); // edge between (x,y)-(x,y+1)
    this.fenceE=new Uint8Array(n); // edge between (x,y)-(x+1,y)
    this.occ=new Int16Array(n);    // building index or -1
    this.region=new Int16Array(n); // habitat region id or -1
    this.occ.fill(-1);
    this.region.fill(-1);
    this.buildings=[];
    this.regions=[];               // [{id,tiles,area,openEdges,hasPath}]
    this.regionsDirty=true;
    this.terrainDirty=true;
    this.viewCache=null;
    this.nextBid=1;
  }
  idx(x,y){ return y*this.cols+x; }
  inBounds(x,y){ return x>=0&&y>=0&&x<this.cols&&y<this.rows; }
  gid(x,y){ return WK.GIDS[this.ground[this.idx(x,y)]]; }
  pid(x,y){ const p=this.path[this.idx(x,y)]; return p?WK.PIDS[p-1]:null; }
  oid(x,y){ const o=this.obj[this.idx(x,y)]; return o?WK.NIDS[o-1]:null; }

  /* ---------- raw setters (cost/validation in Game) ---------- */
  setGround(x,y,gid){
    if(!this.inBounds(x,y)) return false;
    this.ground[this.idx(x,y)]=WK.GIDS.indexOf(gid);
    this.path[this.idx(x,y)]=0; // path destroyed by terrain change
    this.terrainDirty=true; this.touch();
    return true;
  }
  setPath(x,y,pid){
    if(!this.inBounds(x,y)) return false;
    const i=this.idx(x,y);
    if(D.GROUND[this.gid(x,y)].water||this.occ[i]>=0) return false;
    this.path[i]=pid?(WK.PIDS.indexOf(pid)+1):0;
    if(!pid&&this.obj[i]){} // keep objects
    this.terrainDirty=true; this.touch();
    return true;
  }
  setObj(x,y,oid,varr){
    if(!this.inBounds(x,y)) return false;
    const i=this.idx(x,y);
    if(this.occ[i]>=0) return false;
    this.obj[i]=oid?(WK.NIDS.indexOf(oid)+1):0;
    this.objVar[i]=varr||0;
    this.terrainDirty=true; this.touch();
    return true;
  }
  setFence(x,y,edge,kind){
    if(!this.inBounds(x,y)) return false;
    const i=this.idx(x,y);
    if(edge==='S') this.fenceS[i]=kind; else this.fenceE[i]=kind;
    this.terrainDirty=true; this.touch();
    return true;
  }
  touch(){ this.ver=(this.ver||0)+1; this.regionsDirty=true; this.viewCache=null; }

  fenceBetween(ax,ay,bx,by){
    if(bx===ax&&by===ay+1) return this.fenceS[this.idx(ax,ay)];
    if(bx===ax&&by===ay-1) return this.fenceS[this.idx(bx,by)];
    if(by===ay&&bx===ax+1) return this.fenceE[this.idx(ax,ay)];
    if(by===ay&&bx===ax-1) return this.fenceE[this.idx(bx,by)];
    return 0;
  }

  /* ---------- buildings ---------- */
  canPlaceBuilding(bx,by,w,h){
    for(let j=0;j<h;j++)for(let i=0;i<w;i++){
      const x=bx+i,y=by+j;
      if(!this.inBounds(x,y)) return false;
      const k=this.idx(x,y);
      if(this.occ[k]>=0) return false;
      if(D.GROUND[this.gid(x,y)].water) return false;
      if(this.obj[k]){ const o=D.NATURE[WK.NIDS[this.obj[k]-1]]; if(o.block) return false; }
      if(this.path[k]) return false;
    }
    return true;
  }
  placeBuilding(type,x,y){
    const def=D.BUILDING[type];
    if(!this.canPlaceBuilding(x,y,def.w,def.h)) return null;
    const b={id:this.nextBid++, type, x, y, w:def.w, h:def.h};
    this.buildings.push(b);
    for(let j=0;j<def.h;j++)for(let i=0;i<def.w;i++) this.occ[this.idx(x+i,y+j)]=b.id;
    this.terrainDirty=true; this.touch();
    return b;
  }
  removeBuildingById(id){
    const bi=this.buildings.findIndex(b=>b.id===id);
    if(bi<0) return null;
    const b=this.buildings[bi];
    for(let j=0;j<b.h;j++)for(let i=0;i<b.w;i++){
      const k=this.idx(b.x+i,b.y+j);
      if(this.occ[k]===b.id) this.occ[k]=-1;
    }
    this.buildings.splice(bi,1);
    this.terrainDirty=true; this.touch();
    return b;
  }
  buildingAt(x,y){
    const id=this.occ[this.idx(x,y)];
    return id>=0 ? this.buildings.find(b=>b.id===id) : null;
  }
  /* best guest-facing door tile: prefer front-south perimeter on a path */
  doorTile(b){
    const cands=[];
    for(let i=0;i<b.w;i++){
      cands.push([b.x+i,b.y+b.h]);           // south side
    }
    for(let j=b.h-1;j>=0;j--) cands.push([b.x+b.w,b.y+j]); // east side
    for(const [x,y] of cands){
      if(this.inBounds(x,y)&&this.path[this.idx(x,y)]) return [x,y];
    }
    for(const [x,y] of cands) if(this.inBounds(x,y)&&!D.GROUND[this.gid(x,y)].water&&!this.occ[this.idx(x,y)]) return [x,y];
    return [b.x,b.y+b.h];
  }

  /* ---------- habitat regions ---------- */
  computeRegions(){
    const {cols,rows}=this;
    this.region.fill(-1);
    this.regions=[];
    let id=0;
    for(let y=0;y<rows;y++)for(let x=0;x<cols;x++){
      const start=this.idx(x,y);
      if(this.region[start]!==-1) continue;
      if(this.occ[start]>=0) continue;
      id++;
      const tiles=[];
      let openEdges=0, hasPath=false;
      const stack=[start];
      this.region[start]=id;
      while(stack.length){
        const k=stack.pop();
        tiles.push(k);
        const cx=k%cols, cy=(k/cols)|0;
        if(this.path[k]) hasPath=true;
        const nb=[[cx+1,cy],[cx-1,cy],[cx,cy+1],[cx,cy-1]];
        for(let d=0;d<4;d++){
          const nx=nb[d][0], ny=nb[d][1];
          if(!this.inBounds(nx,ny)){ continue; } // map border = wall
          const ni=this.idx(nx,ny);
          if(this.occ[ni]>=0){ continue; }       // building = wall
          if(this.fenceBetween(cx,cy,nx,ny)){ continue; } // fence = wall
          if(this.region[ni]===-1){ this.region[ni]=id; stack.push(ni); }
        }
        // open-edge audit (no fence to the outside world)
        for(let d=0;d<4;d++){
          const nx=nb[d][0], ny=nb[d][1];
          if(!this.inBounds(nx,ny)) continue;
          const ni=this.idx(nx,ny);
          if(this.region[ni]===id) continue;
          if(this.occ[ni]>=0) continue;
          if(this.fenceBetween(cx,cy,nx,ny)) continue;
          openEdges++;
        }
      }
      this.regions.push({id, tiles, area:tiles.length, openEdges, hasPath});
    }
    this.regionsDirty=false;
    this.viewCache=null;
    return this.regions;
  }
  ensureRegions(){ if(this.regionsDirty) this.computeRegions(); }
  regionOf(x,y){ return this.region[this.idx(x,y)]; }
  validHabitats(){
    this.ensureRegions();
    return this.regions.filter(r=>r.openEdges===0&&!r.hasPath);
  }

  /* viewpoints: path tiles adjacent to a habitat region */
  viewpoints(regionId){
    this.ensureRegions();
    if(!this.viewCache) this.viewCache=new Map();
    let v=this.viewCache.get(regionId);
    if(v) return v;
    v=[];
    const seen=new Set();
    const reg=this.regions.find(r=>r.id===regionId);
    if(reg){
      for(const k of reg.tiles){
        const cx=k%this.cols, cy=(k/this.cols)|0;
        for(const [nx,ny] of [[cx+1,cy],[cx-1,cy],[cx,cy+1],[cx,cy-1]]){
          if(!this.inBounds(nx,ny)) continue;
          const ni=this.idx(nx,ny);
          if(this.region[ni]!==regionId && this.path[ni] && !seen.has(ni)){
            seen.add(ni); v.push([nx,ny]);
          }
        }
      }
    }
    this.viewCache.set(regionId,v);
    return v;
  }

  /* ---------- pathfinding ---------- */
  static NB4=[[1,0],[-1,0],[0,1],[0,-1]];
  findPathVisitor(sx,sy,tx,ty){
    return this._bfs(sx,sy,tx,ty,(x,y)=>{
      if(!this.inBounds(x,y)||!this.path[this.idx(x,y)]) return false;
      return true;
    },(ax,ay,bx,by)=> this.fenceBetween(ax,ay,bx,by)!==0 ); // visitors blocked by ANY fence incl gates
  }
  findPathKeeper(sx,sy,tx,ty){
    // Dijkstra-lite: fences climbable at high cost, gates cheap
    const cols=this.cols, n=cols*this.rows;
    const dist=new Float32Array(n).fill(Infinity);
    const prev=new Int32Array(n).fill(-1);
    const si=this.idx(sx,sy), ti=this.idx(tx,ty);
    dist[si]=0;
    const open=[[0,si]];
    const passable=(x,y)=>{
      if(!this.inBounds(x,y)) return false;
      const k=this.idx(x,y);
      if(this.occ[k]>=0) return false;
      const g=D.GROUND[this.gid(x,y)];
      if(g.water===2) return false;
      if(this.obj[k]){ const o=D.NATURE[WK.NIDS[this.obj[k]-1]]; if(o.block) return false; }
      return true;
    };
    if(!passable(tx,ty)||!passable(sx,sy)) return null;
    while(open.length){
      let bi=0; for(let i=1;i<open.length;i++) if(open[i][0]<open[bi][0]) bi=i;
      const [d,k]=open.splice(bi,1)[0];
      if(k===ti) break;
      if(d>dist[k]) continue;
      const cx=k%cols, cy=(k/cols)|0;
      for(const [dx,dy] of World.NB4){
        const nx=cx+dx, ny=cy+dy;
        if(!passable(nx,ny)) continue;
        const ni=this.idx(nx,ny);
        let c=1;
        const f=this.fenceBetween(cx,cy,nx,ny);
        if(f===1||f===2) c+=9;
        if(f===3) c+=0.5;
        if(D.GROUND[this.gid(nx,ny)].water===1) c+=2;
        const nd=d+c;
        if(nd<dist[ni]-1e-6){ dist[ni]=nd; prev[ni]=k; open.push([nd,ni]); }
      }
    }
    if(prev[ti]===-1&&ti!==si) return null;
    const out=[];
    let cur=ti;
    while(cur!==-1&&cur!==si){ out.push([cur%cols,(cur/cols)|0]); cur=prev[cur]; }
    out.reverse();
    return out;
  }
  _bfs(sx,sy,tx,ty,passable,blockedEdge){
    if(!passable(sx,sy)||!passable(tx,ty)) return null;
    const cols=this.cols, n=cols*this.rows;
    const prev=new Int32Array(n).fill(-2);
    const si=this.idx(sx,sy), ti=this.idx(tx,ty);
    prev[si]=-1;
    let q=[si];
    while(q.length){
      const nq=[];
      for(const k of q){
        if(k===ti){
          const out=[]; let cur=ti;
          while(cur!==-1){ out.push([cur%cols,(cur/cols)|0]); cur=prev[cur]; }
          out.reverse(); out.shift();
          return out.length?out:[[tx,ty]];
        }
        const cx=k%cols, cy=(k/cols)|0;
        for(const [dx,dy] of World.NB4){
          const nx=cx+dx, ny=cy+dy;
          if(!passable(nx,ny)) continue;
          const ni=this.idx(nx,ny);
          if(prev[ni]!==-2) continue;
          if(blockedEdge&&blockedEdge(cx,cy,nx,ny)) continue;
          prev[ni]=k; nq.push(ni);
        }
      }
      q=nq;
    }
    return null;
  }
  /* path for an animal confined to its region */
  findPathAnimal(species,x0,y0,x1,y1,regionId){
    const sp=species;
    const passable=(x,y)=>{
      if(!this.inBounds(x,y)) return false;
      if(this.regionOf(x,y)!==regionId) return false;
      const g=D.GROUND[this.gid(x,y)];
      if(g.water){
        if(!sp.swim) return false;
      }
      const o=this.oid(x,y);
      if(o&&D.NATURE[o].block) return false;
      return true;
    };
    return this._bfs(Math.round(x0),Math.round(y0),Math.round(x1),Math.round(y1),
      (x,y)=>passable(x,y)||(x===Math.round(x0)&&y===Math.round(y0)),
      (ax,ay,bx,by)=> this.fenceBetween(ax,ay,bx,by)!==0 );
  }
  /* random passable tile inside region for a species */
  randomRegionTile(regionId,sp,rnd){
    const reg=this.regions.find(r=>r.id===regionId);
    if(!reg) return null;
    for(let tries=0;tries<24;tries++){
      const k=reg.tiles[(rnd()*reg.tiles.length)|0];
      const x=k%this.cols, y=(k/this.cols)|0;
      const g=D.GROUND[this.gid(x,y)];
      if(g.water&&!sp.swim) continue;
      const o=this.oid(x,y);
      if(o&&D.NATURE[o].block) continue;
      return [x,y];
    }
    return null;
  }

  /* ---------- persistence ---------- */
  serialize(){
    return {
      cols:this.cols, rows:this.rows,
      ground:Array.from(this.ground), path:Array.from(this.path),
      obj:Array.from(this.obj), objVar:Array.from(this.objVar),
      fenceS:Array.from(this.fenceS), fenceE:Array.from(this.fenceE),
      buildings:this.buildings.map(b=>({...b})), nextBid:this.nextBid,
    };
  }
  deserialize(s){
    this.ground.set(s.ground); this.path.set(s.path);
    this.obj.set(s.obj); this.objVar.set(s.objVar);
    this.fenceS.set(s.fenceS); this.fenceE.set(s.fenceE);
    this.buildings=s.buildings.map(b=>({...b}));
    this.nextBid=s.nextBid||this.buildings.length+1;
    this.occ.fill(-1);
    for(const b of this.buildings)
      for(let j=0;j<b.h;j++)for(let i=0;i<b.w;i++)
        this.occ[this.idx(b.x+i,b.y+j)]=b.id;
    this.terrainDirty=true; this.touch();
  }
}
WK.World = World;
})();
