// SPDX-License-Identifier: AGPL-3.0-or-later
package preservation;

/**
 * NO3 EntityZombie/EntityZombieSpawner port from Xeeynamo/sotn-decomp commit
 * 0622fcfd37311fd21baf2938c6bd74cea63005ba, src/st/e_zombie.h,
 * animate_entity.h, st_common.h, st_update.h. Registered data comes from the
 * user's disc. No direct zombie layout is invented: original entity77 creates
 * entity76 in slots160..167. Both hall records use shared global slot208.
 *
 * Original step order, integer/fixed-point operations, animation interpreter,
 * RNG recurrence, palette choice, spawn delays/edges and selected collision
 * helpers are retained. Layout activation repeatedly scans the authored camera
 * instead of the full original cursor/entity loader. The persistent spawner
 * is not replaced by the other record while shared208 is occupied. Room entry,
 * reset and player checkpoint load recreate this world and reset its isolated
 * RNG; CASTLE_TURNED_ON is an explicit local test gate, not the original event.
 * Original death effects, sounds, defense/resistances and drops are unfinished.
 * Bat RNG/snapshots are independent and unchanged. Zombie progress is not saved.
 */
public final class ZombieWorld {
    static final int ONE=1<<16,ENTITY_ZOMBIE=76,ENTITY_SPAWNER=77,SLOT_FIRST=160,SLOT_COUNT=8;
    private static final int ROOMS=28,TUPLE=6,MAX_LAYOUT=128;
    private Definition definition;
    private final Layout[][] layouts=new Layout[ROOMS][];
    private StageMap.Room room;
    private Layout activeLayout;
    private final Zombie[] zombies=new Zombie[SLOT_COUNT];
    private boolean castleTurnedOn;
    private int randomNext,spawnDelay,spawnSide,spawnerX,attempts,rejectedEdges,capacityMisses,spawnSerial;
    private static final class Definition {
        final int[] init,enemy,walk,emerge,sink,fallSensors,walkSensors;
        Definition(int[] init,int[] enemy,int[] walk,int[] emerge,int[] sink,int[] fall,int[] sensors){this.init=init.clone();this.enemy=enemy.clone();this.walk=walk.clone();this.emerge=emerge.clone();this.sink=sink.clone();fallSensors=fall.clone();walkSensors=sensors.clone();}
    }
    private static final class Layout {
        final int index,x,y,rawId,params,rawRoomIndex;
        Layout(int[] values,int offset){index=values[offset];x=values[offset+1];y=values[offset+2];rawId=values[offset+3];params=values[offset+4];rawRoomIndex=values[offset+5];}
    }
    static final class Zombie extends EnemyTerrain.Body {
        final int slot,serial,sourceLayout;
        int step,pose,poseTimer,frame,palette,facingLeft,hp,hitboxWidth,hitboxHeight,hitboxOffY,hitboxState,lastHit=-1;
        boolean active=true,alive=true;
        Zombie(int slot,int serial,int source,int px,int py){this.slot=slot;this.serial=serial;sourceLayout=source;x=px*ONE;y=py*ONE;}
    }
    private static boolean animationValid(int[] pairs,boolean loop){
        if(pairs==null||pairs.length<4||pairs.length>512||pairs.length%2!=0)return false;
        for(int i=0;i<pairs.length-2;i+=2)if(pairs[i]<1||pairs[i]>254||pairs[i+1]<1||pairs[i+1]>10)return false;
        return pairs[pairs.length-2]==(loop?0:255)&&pairs[pairs.length-1]==0;
    }
    public boolean registerDefinition(int[] init,int[] enemy,int[] walk,int[] emerge,int[] sink,int[] fall,int[] sensors){
        if(init==null||init.length!=5||enemy==null||enemy.length!=7||fall==null||fall.length!=8||sensors==null||sensors.length!=4)return false;
        for(int value:init)if(value<0||value>65535)return false;
        if(init[0]!=0x8007||init[1]!=0||init[2]!=0x4D||init[3]<0||init[3]>2045||init[4]!=0x6E)return false;
        if(enemy[0]<1||enemy[0]>32767||enemy[1]<0||enemy[1]>32767||enemy[2]<0||enemy[2]>65535||enemy[3]<0||enemy[3]>65535||enemy[4]<1||enemy[4]>64||enemy[5]<1||enemy[5]>64)return false;
        if(!animationValid(walk,true)||!animationValid(emerge,false)||!animationValid(sink,false))return false;
        for(int value:fall)if(value<-64||value>64)return false;for(int value:sensors)if(value<-64||value>64)return false;
        definition=new Definition(init,enemy,walk,emerge,sink,fall,sensors);reset(room);return true;
    }
    public boolean registerLayouts(StageMap.Room target,int[] tuples){
        if(target==null||target.id<0||target.id>=ROOMS||target.redirectRoom>=0||tuples==null||tuples.length%TUPLE!=0||tuples.length>MAX_LAYOUT*TUPLE)return false;
        Layout[] proposed=new Layout[tuples.length/TUPLE];
        for(int i=0;i<proposed.length;i++){
            Layout entry=new Layout(tuples,i*TUPLE);
            // This narrow port reproduces the shared original208 slot only.
            if(entry.index<0||entry.index>65535||entry.x<0||entry.x>=target.width||entry.y<48||entry.y>=target.height||entry.rawId!=ENTITY_SPAWNER||entry.params!=0||entry.rawRoomIndex!=144)return false;
            for(int j=0;j<i;j++)if(proposed[j].index==entry.index)return false;
            proposed[i]=entry;
        }
        layouts[target.id]=proposed;if(room==target)reset(target);return true;
    }
    public void setCastleTurnedOn(boolean enabled){castleTurnedOn=enabled;}
    public boolean castleTurnedOn(){return castleTurnedOn;}
    public void clear(){for(int i=0;i<layouts.length;i++)layouts[i]=null;reset(null);}
    public void reset(StageMap.Room target){room=target;activeLayout=null;for(int i=0;i<zombies.length;i++)zombies[i]=null;randomNext=0;spawnDelay=spawnSide=spawnerX=attempts=rejectedEdges=capacityMisses=spawnSerial=0;}
    private int random(){randomNext=randomNext*0x01010101+1;return randomNext>>>24;}
    private static int animate(Zombie zombie,int[] pairs){
        if(zombie.poseTimer==0){int duration=pairs[zombie.pose*2];if(duration==255)return 0;if(duration==0){zombie.pose=1;zombie.poseTimer=pairs[0];zombie.frame=pairs[1];return 0;}zombie.poseTimer=duration;zombie.frame=pairs[zombie.pose*2+1];zombie.pose++;}
        zombie.poseTimer--;return 1;
    }
    private static void setStep(Zombie zombie,int step){zombie.step=step;zombie.pose=zombie.poseTimer=0;}
    private static void destroy(Zombie zombie){zombie.active=zombie.alive=false;zombie.hitboxState=0;}
    public void update(int playerX,int playerOriginY,int cameraX,int cameraY,int timer){
        update(playerX,playerOriginY,cameraX,cameraY,timer,false);
    }
    public void update(int playerX,int playerOriginY,int cameraX,int cameraY,int timer,boolean timeStopped){
        if(definition==null||room==null)return;
        if(activeLayout==null&&layouts[room.id]!=null)for(Layout entry:layouts[room.id])if(entry.x>=Math.max(0,cameraX-64)&&entry.x<=cameraX+320&&entry.y>=Math.max(0,cameraY-64)&&entry.y<=cameraY+288){activeLayout=entry;spawnDelay=1;spawnerX=entry.x;break;}
        // Original global160..167 run before208; a new zombie waits one tick.
        for(Zombie zombie:zombies)if(zombie!=null&&zombie.active){
            int screenX=(zombie.x>>16)-cameraX,screenY=(zombie.y>>16)-cameraY;
            if(zombie.step>0&&(zombie.flags&0x80000000)!=0&&(screenX<-128||screenX>384||screenY<-128||screenY>352)){destroy(zombie);continue;}
            if(!StageUpdateGate.shouldRun(zombie.step,zombie.flags,timeStopped,timer))continue;
            updateZombie(zombie,playerX,timer);
        }
        // Original initialized spawner flags2000 remain immune to stopwatch.
        if(activeLayout!=null&&castleTurnedOn){spawnerX=cameraX+128;if(--spawnDelay==0)spawn();}
    }
    private void spawn(){
        attempts++;int slot=-1;for(int i=0;i<zombies.length;i++)if(zombies[i]==null||!zombies[i].active){slot=i;break;}
        if(slot>=0){
            int distance=(random()&63)+96,px=spawnerX+(spawnSide!=0?distance:-distance);spawnSide^=1;
            if(px<128||room.width-128<px)rejectedEdges++;else zombies[slot]=new Zombie(slot+SLOT_FIRST,++spawnSerial,activeLayout.index,px,activeLayout.y-48);
        }else capacityMisses++;
        spawnDelay=(random()&63)+32;
    }
    private void updateZombie(Zombie zombie,int playerX,int timer){
        switch(zombie.step){
        case 0:
            zombie.hp=definition.enemy[0];zombie.flags=definition.enemy[6];zombie.hitboxState=definition.enemy[3];zombie.hitboxWidth=8;zombie.hitboxHeight=0;zombie.hitboxOffY=16;
            zombie.palette=definition.init[3]+(timer&1)+(random()&1);zombie.frame=0;zombie.step=1;break;
        case 1:if((EnemyTerrain.land(room,zombie,definition.fallSensors)&1)!=0){zombie.facingLeft=(zombie.x>>16)>playerX?0:1;zombie.step=2;}break;
        case 2:
            if(animate(zombie,definition.emerge)==0)setStep(zombie,3);
            if(zombie.poseTimer==0){zombie.hitboxOffY-=2;zombie.hitboxHeight+=2;}break;
        case 3:
            animate(zombie,definition.walk);int result=EnemyTerrain.walk(room,zombie,definition.walkSensors);zombie.vx=zombie.facingLeft!=0?ONE/2:-ONE/2;
            if((result&0xC0)!=0){zombie.hitboxState=0;setStep(zombie,4);}break;
        case 4:if(animate(zombie,definition.sink)==0)destroy(zombie);break;
        default:throw new IllegalStateException("Invalid zombie step");
        }
    }
    public int hitSword(int swordX,int swordY,int halfWidth,int halfHeight,int sequence,int damage){
        if(definition==null||damage<1)return 0;int count=0;
        for(Zombie zombie:zombies)if(zombie!=null&&zombie.active&&zombie.hitboxState!=0&&zombie.hitboxHeight>0&&zombie.lastHit!=sequence&&Math.abs((zombie.x>>16)-swordX)<zombie.hitboxWidth+halfWidth&&Math.abs((zombie.y>>16)+zombie.hitboxOffY-swordY)<zombie.hitboxHeight+halfHeight){zombie.lastHit=sequence;zombie.hp=Math.max(0,zombie.hp-damage);if(zombie.hp==0)destroy(zombie);count++;}
        return count;
    }
    public int contactDamage(int playerX,int feetY,int bodyHeight){
        if(definition==null)return 0;int top=feetY-bodyHeight+1,bottom=feetY-1,damage=0;
        for(Zombie zombie:zombies)if(zombie!=null&&zombie.active&&zombie.hitboxState!=0&&zombie.hitboxHeight>0&&Math.abs((zombie.x>>16)-playerX)<zombie.hitboxWidth+7&&(zombie.y>>16)+zombie.hitboxOffY+zombie.hitboxHeight>top&&(zombie.y>>16)+zombie.hitboxOffY-zombie.hitboxHeight<bottom)damage=Math.max(damage,definition.enemy[1]);
        return damage;
    }
    public String getState(){
        StringBuilder out=new StringBuilder("[");boolean comma=false;
        for(Zombie zombie:zombies)if(zombie!=null){if(comma)out.append(',');comma=true;
            out.append("{\"kind\":\"zombie\",\"roomId\":").append(room.id).append(",\"entityId\":76,\"slot\":").append(zombie.slot).append(",\"serial\":").append(zombie.serial).append(",\"sourceLayoutIndex\":").append(zombie.sourceLayout);
            out.append(",\"x\":").append(zombie.x>>16).append(",\"y\":").append(zombie.y>>16).append(",\"xFixed\":").append(zombie.x).append(",\"yFixed\":").append(zombie.y).append(",\"vxFixed\":").append(zombie.vx).append(",\"vyFixed\":").append(zombie.vy);
            out.append(",\"frame\":").append(zombie.frame).append(",\"palette\":").append(zombie.palette).append(",\"step\":").append(zombie.step).append(",\"pose\":").append(zombie.pose).append(",\"poseTimer\":").append(zombie.poseTimer).append(",\"facingLeft\":").append(zombie.facingLeft).append(",\"face\":").append(zombie.facingLeft==0?1:-1);
            out.append(",\"hp\":").append(zombie.hp).append(",\"hitboxState\":").append(zombie.hitboxState).append(",\"hitboxWidth\":").append(zombie.hitboxWidth).append(",\"hitboxHeight\":").append(zombie.hitboxHeight).append(",\"hitboxOffY\":").append(zombie.hitboxOffY).append(",\"unsupportedTerrainProbes\":").append(zombie.unsupportedQueries).append(",\"alive\":").append(zombie.alive).append(",\"active\":").append(zombie.active).append('}');
        }
        return out.append(']').toString();
    }
    public String getSpawners(){
        StringBuilder out=new StringBuilder("[");Layout[] list=room==null?null:layouts[room.id];
        if(list!=null)for(int i=0;i<list.length;i++){if(i!=0)out.append(',');Layout entry=list[i];boolean active=entry==activeLayout;
            out.append("{\"kind\":\"zombieSpawner\",\"roomId\":").append(room.id).append(",\"layoutIndex\":").append(entry.index).append(",\"entityId\":77,\"rawEntityId\":").append(entry.rawId).append(",\"rawRoomIndex\":").append(entry.rawRoomIndex).append(",\"slot\":208,\"params\":").append(entry.params).append(",\"x\":").append(active?spawnerX:entry.x).append(",\"y\":").append(entry.y).append(",\"layoutX\":").append(entry.x).append(",\"active\":").append(active).append(",\"spawnDelay\":").append(active?spawnDelay:0).append(",\"spawnSide\":").append(active?spawnSide:0).append(",\"spawnAttempts\":").append(active?attempts:0).append(",\"edgeRejected\":").append(active?rejectedEdges:0).append(",\"capacityMisses\":").append(active?capacityMisses:0).append('}');
        }
        return out.append(']').toString();
    }
    // Native regression/audit access to fixed states, not a public game API.
    Zombie zombie(int slot){return zombies[slot];}int randomState(){return randomNext;}int delay(){return spawnDelay;}int side(){return spawnSide;}int attempts(){return attempts;}int rejected(){return rejectedEdges;}int capacityMisses(){return capacityMisses;}
}
