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

/**
 * Normal NO3 EntityBat steps and animation interpreter adapted from
 * Xeeynamo/sotn-decomp commit 0622fcfd37311fd21baf2938c6bd74cea63005ba:
 * src/st/e_bat.h, animate_entity.h, st_common.h, st_update.h.
 * Binary-decoded EInit, EnemyDef, animation and layout data are registered by
 * the host. No enemy is invented when a room has no registered bat layout.
 *
 * The local camera rectangle is authored by GameEngine. This class repeatedly
 * scans original activation margins instead of reproducing layout cursors and
 * slot replacement by every original entity type. Room changes recreate bats;
 * off-camera destruction permits reactivation only after their layout spawn
 * leaves and re-enters the activation window. Killed bats remain gone until a
 * room change/reset. The original stage RNG recurrence has an isolated stream.
 * Sword/contact overlap and damage response remain authored in GameEngine.
 */
public final class EnemyWorld {
    static final int ONE=1<<16,ENTITY_BAT=72;
    private static final int ROOMS=28,TUPLE=6,MAX_LAYOUT=128;
    private Definition definition;
    private final Layout[][] layouts=new Layout[ROOMS][];
    private final int[] widths=new int[ROOMS],heights=new int[ROOMS];
    private Bat[] bats=new Bat[0];
    private int roomId=-1,randomNext;
    private static final class Definition {
        final int[] init,enemy,fly,drop;
        final int fingerprint;
        Definition(int[] init,int[] enemy,int[] fly,int[] drop){this.init=init.clone();this.enemy=enemy.clone();this.fly=fly.clone();this.drop=drop.clone();int hash=17;for(int[] list:new int[][]{init,enemy,fly,drop})for(int value:list)hash=31*hash+value;fingerprint=hash;}
    }
    private static final class Layout {
        final int index,x,y,entityId,params,slot;
        Layout(int[] data,int offset){index=data[offset];x=data[offset+1];y=data[offset+2];entityId=data[offset+3];params=data[offset+4];slot=data[offset+5];}
    }
    static final class Bat {
        final Layout layout;
        int x,y,vx,vy,accelY,step,pose,poseTimer,frame=31,facingLeft,hp,lastHit=-1;
        boolean active,alive=true,waitingForReentry;
        Bat(Layout layout,int health){this.layout=layout;x=layout.x*ONE;y=layout.y*ONE;hp=health;}
    }
    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]<21||pairs[i+1]>32)return false;
        return pairs[pairs.length-2]==(loop?0:255)&&pairs[pairs.length-1]==0&&(!loop||pairs.length>=22);
    }
    public boolean registerDefinition(int[] init,int[] enemy,int[] fly,int[] drop){
        if(init==null||init.length!=5||enemy==null||enemy.length!=7)return false;
        for(int value:init)if(value<0||value>65535)return false;
        if(init[0]!=0x8005||init[1]>255||init[2]>255||init[3]>2047||init[4]!=0x40)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(fly,true)||!animationValid(drop,false))return false;
        definition=new Definition(init,enemy,fly,drop);enterRoom(roomId);return true;
    }
    public boolean registerLayout(int room,int width,int height,int[] tuples){
        if(room<0||room>=ROOMS||width<256||height<256||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);
            if(entry.index<0||entry.index>65535||entry.x<0||entry.x>=width||entry.y<0||entry.y>=height||entry.entityId<0||entry.entityId>65535||(entry.entityId&0x3FF)!=ENTITY_BAT||(entry.entityId&0xE000)!=0||entry.params<0||entry.params>65535||entry.slot<0||entry.slot>65535||(entry.slot&255)>=192)return false;
            for(int previous=0;previous<i;previous++)if(proposed[previous].index==entry.index||(proposed[previous].slot&255)==(entry.slot&255))return false;
            proposed[i]=entry;
        }
        layouts[room]=proposed;widths[room]=width;heights[room]=height;if(roomId==room)enterRoom(room);return true;
    }
    public void clear(){for(int i=0;i<layouts.length;i++){layouts[i]=null;widths[i]=heights[i]=0;}enterRoom(-1);randomNext=0;}
    public void reset(int room){randomNext=0;enterRoom(room);}
    public void enterRoom(int room){
        roomId=room;
        Layout[] roomLayout=room>=0&&room<ROOMS?layouts[room]:null;
        if(definition==null||roomLayout==null){bats=new Bat[0];return;}
        bats=new Bat[roomLayout.length];for(int i=0;i<bats.length;i++)bats[i]=new Bat(roomLayout[i],definition.enemy[0]);
    }
    private int random(){randomNext=randomNext*0x01010101+1;return(randomNext>>>24)&255;}
    private static int pixel(int fixed){return fixed>>16;}
    private int animate(Bat bat,int[] pairs){
        int offset=bat.pose*2;
        if(offset<0||offset>=pairs.length)return 0;
        if(bat.poseTimer==0){
            int duration=pairs[offset];
            if(duration==255)return 0;
            if(duration==0){bat.pose=1;bat.poseTimer=pairs[0];bat.frame=pairs[1];return 0;}
            bat.poseTimer=duration;bat.frame=pairs[offset+1];bat.pose++;
        }
        bat.poseTimer--;return 1;
    }
    public void update(int playerX,int playerOriginY,int cameraX,int cameraY){
        update(playerX,playerOriginY,cameraX,cameraY,false,0);
    }
    public void update(int playerX,int playerOriginY,int cameraX,int cameraY,boolean timeStopped,int gameTimer){
        if(definition==null)return;
        for(Bat bat:bats){
            boolean inWindow=bat.layout.x>=Math.max(0,cameraX-64)&&bat.layout.x<=cameraX+320&&bat.layout.y>=Math.max(0,cameraY-64)&&bat.layout.y<=cameraY+288;
            if(!inWindow)bat.waitingForReentry=false;
            if(bat.alive&&!bat.active&&inWindow&&!bat.waitingForReentry){bat.active=true;bat.x=bat.layout.x*ONE;bat.y=bat.layout.y*ONE;bat.step=0;bat.pose=0;bat.poseTimer=0;bat.vx=bat.vy=bat.accelY=0;bat.frame=31;bat.facingLeft=0;}
            if(!bat.active||!bat.alive)continue;
            int screenX=pixel(bat.x)-cameraX,screenY=pixel(bat.y)-cameraY;
            if(bat.step>0&&(definition.enemy[6]&0x80000000)!=0){
                boolean barely=(definition.enemy[6]&0x40000000)!=0;
                if(screenX<(barely?-64:-128)||screenX>(barely?320:384)||screenY<(barely?-64:-128)||screenY>(barely?288:352)){bat.active=false;bat.waitingForReentry=true;continue;}
            }
            if(bat.step>0&&(definition.enemy[6]&0x20000000)!=0&&(definition.enemy[6]&0x10000000)==0&&(screenX<-64||screenX>320||screenY<-64||screenY>288))continue;
            if(!StageUpdateGate.shouldRun(bat.step,definition.enemy[6],timeStopped,gameTimer))continue;
            updateBat(bat,playerX,playerOriginY);
        }
    }
    private void updateBat(Bat bat,int playerX,int playerY){
        int px=pixel(bat.x),py=pixel(bat.y);
        switch(bat.step){
        case 0:bat.hp=definition.enemy[0];bat.frame=31;bat.step=1;break;
        case 1:if(Math.abs(px-playerX)<96&&Math.abs(py-playerY)<96&&playerY>=py)bat.step=2;break;
        case 2:
            if(animate(bat,definition.drop)==0){bat.facingLeft=px>playerX?0:1;bat.vy=57344;bat.vx=bat.facingLeft!=0?16384:-16384;bat.pose=(random()&3)*3;bat.poseTimer=0;bat.step=3;}break;
        case 3:
            animate(bat,definition.fly);bat.x+=bat.vx;bat.y+=bat.vy;
            if(Math.abs(pixel(bat.y)-playerY)<32){bat.vx=bat.facingLeft!=0?ONE:-ONE;bat.accelY=2048;bat.step=4;}break;
        case 4:
            animate(bat,definition.fly);bat.x+=bat.vx;bat.y+=bat.vy;
            if(bat.vy<-ONE||bat.vy>ONE)bat.accelY=-bat.accelY;
            bat.vy+=bat.accelY;break;
        default:throw new IllegalStateException("Invalid bat 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(Bat bat:bats)if(bat.active&&bat.alive&&bat.lastHit!=sequence&&Math.abs(pixel(bat.x)-swordX)<definition.enemy[4]+halfWidth&&Math.abs(pixel(bat.y)-swordY)<definition.enemy[5]+halfHeight){bat.lastHit=sequence;bat.hp=Math.max(0,bat.hp-damage);if(bat.hp==0){bat.alive=false;bat.active=false;}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(Bat bat:bats)if(bat.active&&bat.alive&&Math.abs(pixel(bat.x)-playerX)<definition.enemy[4]+7&&pixel(bat.y)+definition.enemy[5]>top&&pixel(bat.y)-definition.enemy[5]<bottom)damage=Math.max(damage,definition.enemy[1]);
        return damage;
    }
    public String getState(){
        StringBuilder result=new StringBuilder("[");
        for(int i=0;i<bats.length;i++){
            if(i!=0)result.append(',');Bat bat=bats[i];
            result.append("{\"kind\":\"bat\",\"roomId\":").append(roomId).append(",\"layoutIndex\":").append(bat.layout.index).append(",\"entityId\":72,\"rawEntityId\":").append(bat.layout.entityId).append(",\"params\":").append(bat.layout.params).append(",\"rawRoomIndex\":").append(bat.layout.slot);
            result.append(",\"x\":").append(pixel(bat.x)).append(",\"y\":").append(pixel(bat.y)).append(",\"xFixed\":").append(bat.x).append(",\"yFixed\":").append(bat.y).append(",\"vxFixed\":").append(bat.vx).append(",\"vyFixed\":").append(bat.vy).append(",\"accelYFixed\":").append(bat.accelY);
            result.append(",\"frame\":").append(bat.frame).append(",\"step\":").append(bat.step).append(",\"pose\":").append(bat.pose).append(",\"poseTimer\":").append(bat.poseTimer).append(",\"facingLeft\":").append(bat.facingLeft).append(",\"face\":").append(bat.facingLeft==0?1:-1).append(",\"hp\":").append(bat.hp).append(",\"alive\":").append(bat.alive).append(",\"active\":").append(bat.active).append('}');
        }
        return result.append(']').toString();
    }
    private int fingerprint(){int hash=definition==null?0:definition.fingerprint;for(int room=0;room<ROOMS;room++){hash=31*hash+room;hash=31*hash+widths[room];hash=31*hash+heights[room];Layout[] list=layouts[room];hash=31*hash+(list==null?-1:list.length);if(list!=null)for(Layout entry:list)for(int field:new int[]{entry.index,entry.x,entry.y,entry.entityId,entry.params,entry.slot})hash=31*hash+field;}return hash;}
    public String save(){
        StringBuilder result=new StringBuilder("JAVA-BATS-1,").append(fingerprint()).append(',').append(roomId).append(',').append(randomNext).append(',').append(bats.length);
        for(Bat bat:bats)result.append(';').append(bat.layout.index).append(',').append(bat.x).append(',').append(bat.y).append(',').append(bat.vx).append(',').append(bat.vy).append(',').append(bat.accelY).append(',').append(bat.step).append(',').append(bat.pose).append(',').append(bat.poseTimer).append(',').append(bat.frame).append(',').append(bat.facingLeft).append(',').append(bat.hp).append(',').append(bat.lastHit).append(',').append(bat.active?1:0).append(',').append(bat.alive?1:0).append(',').append(bat.waitingForReentry?1:0);
        return result.toString();
    }
    public boolean load(String data){
        if(data==null||data.length()>32768)return false;
        try{
            String[] rows=data.split(";",-1),header=rows[0].split(",",-1);
            if(header.length!=5||!header[0].equals("JAVA-BATS-1")||Integer.parseInt(header[1])!=fingerprint()||Integer.parseInt(header[2])!=roomId||Integer.parseInt(header[4])!=bats.length||rows.length!=bats.length+1)return false;
            int proposedRandom=Integer.parseInt(header[3]);Bat[] proposed=new Bat[bats.length];
            for(int i=0;i<bats.length;i++){
                String[] parts=rows[i+1].split(",",-1);if(parts.length!=16)return false;int[] values=new int[16];for(int j=0;j<values.length;j++)values[j]=Integer.parseInt(parts[j]);
                if(values[0]!=bats[i].layout.index||roomId<0||values[1]<-512L*ONE||values[1]>(widths[roomId]+512L)*ONE||values[2]<-512L*ONE||values[2]>(heights[roomId]+512L)*ONE||Math.abs((long)values[3])>ONE||Math.abs((long)values[4])>ONE+2048||values[5]!=0&&Math.abs((long)values[5])!=2048||values[6]<0||values[6]>4||values[7]<0||definition==null||values[7]>((values[6]==2?definition.drop.length:definition.fly.length)/2-1)||values[8]<0||values[8]>254||values[9]<21||values[9]>32||values[10]<0||values[10]>1||values[11]<0||values[11]>definition.enemy[0]||values[12]<-1)return false;
                if(values[6]<2&&(values[7]!=0||values[8]!=0||values[9]!=31||values[3]!=0||values[4]!=0)||values[6]<4&&values[5]!=0||values[6]==4&&Math.abs(values[5])!=2048)return false;
                for(int j=13;j<16;j++)if(values[j]<0||values[j]>1)return false;
                if(values[14]==0&&(values[11]!=0||values[13]!=0)||values[14]==1&&values[11]==0)return false;
                Bat bat=new Bat(bats[i].layout,values[11]);bat.x=values[1];bat.y=values[2];bat.vx=values[3];bat.vy=values[4];bat.accelY=values[5];bat.step=values[6];bat.pose=values[7];bat.poseTimer=values[8];bat.frame=values[9];bat.facingLeft=values[10];bat.lastHit=values[12];bat.active=values[13]==1;bat.alive=values[14]==1;bat.waitingForReentry=values[15]==1;proposed[i]=bat;
            }
            bats=proposed;randomNext=proposedRandom;return true;
        }catch(NumberFormatException ex){return false;}
    }
    // Native regression checks examine fixed state without parsing JSON.
    Bat bat(int index){return bats[index];}int count(){return bats.length;}int randomState(){return randomNext;}
}
