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

/** Selected US bottle, glass and green flame from pinned sotn-decomp
 * 0622fcfd37311fd21baf2938c6bd74cea63005ba, src/dra/84B88.c.
 * Decoded arrays supply definitions, trigonometry, shard shape/UV and factories.
 * Source integer collision corrections, motion, schedules and primitives are
 * retained. Next-tick factories, isolated BIOS-rand stream, bounded shared GPU
 * budget and local once-per-target combat replace whole-game integration.
 * Selected BP4/7 green TILE particles share that RNG/resource boundary.
 * Audio, other particle profiles and full global scheduling remain pending.
 */
public final class HolyWaterWorld {
    static final int ONE=65536,BOTTLE=22,GLASS=40,FLAME=23,PARTICLES=5,POOL=32;
    interface Environment {
        EnemyTerrain.Collision query(int x,int y);
        int hit(Actor actor,int x,int y,int halfWidth,int halfHeight,int sequence,int damage);
    }
    static final class Primitive {
        final int[] quad=new int[8],uv=new int[8],rgb=new int[3];int drawMode;
    }
    static final class Mover {int x,y,vx,vy;}
    static final class Tile {
        final int[] rgb=new int[3];int x,y,vx,vy,delay,width,height,priority,drawMode,primitiveType;
        boolean control;
    }
    static final class Actor {
        final int entityId,slot,serial,sequence,params;final Object[] targets=new Object[256];
        int x,y,vx,vy,step,remaining,face,facingLeft,frame,animSet,hitboxState,hitboxWidth,hitboxHeight,hitboxOffY,hitCount,targetCount,angle,heightBase,flameHeight,unsupportedQueries,damageRoll;
        boolean active=true,allocated;String destroyedReason="";Primitive[] primitives;Mover[] movers;Tile[] tiles;Actor parent;
        Actor(int id,int slot,int serial,int sequence,int params,int x,int y,int face){entityId=id;this.slot=slot;this.serial=serial;this.sequence=sequence;this.params=params;this.x=x;this.y=y;this.face=face;facingLeft=face<0?1:0;}
        boolean hasHit(Object target){for(int i=0;i<targetCount;i++)if(targets[i]==target)return true;return false;}
        boolean rememberHit(Object target){if(target==null||hasHit(target)||targetCount==targets.length)return false;targets[targetCount++]=target;return true;}
    }
    static final class Pending {
        final int entityId,x,y,face,sequence,params,created;final Actor parent;
        Pending(int id,Actor parent,int params,int time){entityId=id;x=parent.x;y=id==PARTICLES?parent.y-10*ONE:parent.y;face=parent.face;sequence=parent.sequence;this.params=params;created=time;this.parent=parent;}
    }
    private final DaggerWorld.SharedPool pool;private final Actor[] actors=new Actor[POOL];private final Pending[] factories=new Pending[8];
    private int[] bottleFields,flameFields,quarter,shardOffsets,uvOrigins,blueprints,particleFields,particleBlueprint;
    private int serial,frameCounter,flameCounter,randomState=1,randomCalls,allocationFailures,factoryFailures,peakPrimitiveUsage;
    public HolyWaterWorld(){this(new DaggerWorld.SharedPool());}
    HolyWaterWorld(DaggerWorld.SharedPool shared){if(shared==null)throw new IllegalArgumentException("Missing shared pool");pool=shared;}
    private static boolean fieldsValid(int[] fields){if(fields==null||fields.length!=13)return false;for(int i=0;i<13;i++)if(fields[i]<0||fields[i]>(i<3?65535:255))return false;return fields[0]>0&&fields[0]<10000&&fields[8]==2;}
    public boolean register(int[] bottle,int[] flame,int[] sineQuarter,int[] offsets,int[] origins,int[] factories){
        if(!fieldsValid(bottle)||!fieldsValid(flame)||bottle[1]!=3||bottle[3]!=3||bottle[6]!=188||bottle[7]!=27||bottle[10]!=11||sineQuarter==null||sineQuarter.length!=1025||offsets==null||offsets.length!=24||origins==null||origins.length!=16||factories==null||factories.length!=18)return false;
        if(sineQuarter[0]!=0||sineQuarter[1024]!=4096)return false;for(int i=0;i<1025;i++)if(sineQuarter[i]<0||sineQuarter[i]>4096||i>0&&sineQuarter[i]<sineQuarter[i-1])return false;
        for(int offset:offsets)if(offset<-16||offset>16)return false;for(int origin:origins)if(origin<0||origin>127)return false;
        for(int value:factories)if(value<0||value>255)return false;
        for(int i=0;i<3;i++){int at=i*6;if(factories[at]!=(i==0?BOTTLE:i==1?FLAME:GLASS)||factories[at+1]!=1||factories[at+2]!=193||factories[at+3]!=0||factories[at+4]!=(i==0?34:i==1?2:0)||factories[at+5]!=0)return false;}
        bottleFields=bottle.clone();flameFields=flame.clone();quarter=sineQuarter.clone();shardOffsets=offsets.clone();uvOrigins=origins.clone();blueprints=factories.clone();reset();return true;
    }
    /** Explicit opt-in preserves all earlier unregistered-particle fixtures. */
    public boolean registerParticles(int[] fields,int[] factory){
        if(fields==null||fields.length!=10||factory==null||factory.length!=6)return false;
        if(fields[0]!=8||fields[4]!=1||fields[5]!=1||fields[6]!=4||fields[7]!=115||fields[8]!=3||fields[9]!=0x08800000)return false;
        for(int i=1;i<=3;i++)if(fields[i]<0||fields[i]>255)return false;
        int[] expected={5,1,193,0,0,0};for(int i=0;i<6;i++)if(factory[i]!=expected[i])return false;
        particleFields=fields.clone();particleBlueprint=factory.clone();reset();return true;
    }
    public void reset(){for(int i=0;i<POOL;i++){if(actors[i]!=null)pool.release(actors[i]);actors[i]=null;}for(int i=0;i<factories.length;i++)factories[i]=null;serial=frameCounter=flameCounter=randomCalls=allocationFailures=factoryFailures=peakPrimitiveUsage=0;randomState=1;}
    private boolean allocate(Actor actor,int count){if(!pool.allocate(actor,count))return false;peakPrimitiveUsage=Math.max(peakPrimitiveUsage,pool.usedPrimitives());return true;}
    public boolean registered(){return bottleFields!=null;}public int heartCost(){return bottleFields==null?0:bottleFields[1];}public int attack(){return bottleFields==null?0:bottleFields[0];}
    public int activeCount(){int count=0;for(int i=0;i<16;i++){Actor a=actors[i];if(a!=null&&a.active&&a.entityId==BOTTLE)count++;}return count;}
    public int refusal(){return bottleFields==null?1:activeCount()>=bottleFields[3]||pool.freeSlot()<0?2:0;}
    public boolean spawn(int x,int originY,int face,int sequence){if(refusal()!=0||face!=1&&face!=-1||sequence<1||x<Integer.MIN_VALUE+64*ONE||x>Integer.MAX_VALUE-64*ONE||originY<Integer.MIN_VALUE+64*ONE||originY>Integer.MAX_VALUE-64*ONE)return false;create(BOTTLE,pool.freeSlot(),x,originY,face,sequence,0);return true;}
    private Actor create(int id,int index,int x,int y,int face,int sequence,int params){Actor actor=new Actor(id,32+index,++serial,sequence,params,x,y,face);pool.reserve(index,actor);actors[index]=actor;return actor;}
    private void queue(int id,Actor parent,int params){for(int i=0;i<factories.length;i++)if(factories[i]==null){factories[i]=new Pending(id,parent,params,frameCounter);return;}factoryFailures++;}
    void beginFrame(){frameCounter++;for(int i=0;i<factories.length;i++){Pending p=factories[i];if(p==null||p.created>=frameCounter)continue;factories[i]=null;int slot=p.entityId==GLASS||p.entityId==PARTICLES?pool.freeReverse():pool.freeSlot();if(slot<0){factoryFailures++;continue;}Actor child=create(p.entityId,slot,p.x,p.y,p.face,p.sequence,p.params);child.parent=p.parent;}}
    private int random(){randomState=randomState*1103515245+12345;randomCalls++;return(randomState>>>16)&32767;}
    // Selected Cross shares the existing isolated BIOS-rand stream. This is
    // still separate from unported original global callers and initial phase.
    int nextSelectedRandom(){return random();}
    private int sin(int angle){int a=angle&4095;return a<=1024?quarter[a]:a<=2048?quarter[2048-a]:a<=3072?-quarter[a-2048]:-quarter[4096-a];}
    private void destroy(Actor actor,String reason){actor.active=actor.allocated=false;actor.hitboxState=0;actor.destroyedReason=reason;if(actor.primitives!=null)for(Primitive p:actor.primitives)p.drawMode|=8;if(actor.tiles!=null)for(Tile tile:actor.tiles)tile.drawMode|=8;pool.release(actor);}
    private EnemyTerrain.Collision probe(Environment environment,Actor actor,int x,int y){EnemyTerrain.Collision c=environment.query(x,y);if(c.unsupported)actor.unsupportedQueries++;return c;}
    /** Original two-result floor helper preserves fractional Y on corrections. */
    int floor(Environment environment,Actor actor,int baseY,int baseX){
        int x=(short)((actor.x>>16)+baseX),y=(short)((actor.y>>16)+baseY);EnemyTerrain.Collision first=probe(environment,actor,x,y);
        if(first.unsupported)return 1;int effects=(short)(first.effects&0xF801);EnemyTerrain.Collision second=probe(environment,actor,x,(short)(y-1+first.up));int newY=(short)(baseY+(actor.y>>16)+first.up);
        if(second.unsupported)return 1;
        if((effects&0x8801)==1||(effects&0x8801)==0x801){effects=(short)(second.effects&0xF001);if((effects&1)==0){actor.y=(newY<<16)|(actor.y&65535);return 1;}if((second.effects&0x8001)==0x8001){actor.y=((short)(newY-1+second.up)<<16)|(actor.y&65535);return effects;}return 0;}
        if((effects&0x8001)==0x8001){actor.y=(newY<<16)|(actor.y&65535);return effects&0xF001;}return 0;
    }
    /** Source SIDE correction clears fractional X, unlike prize field helper. */
    int side(Environment environment,Actor actor,int y,int x){if(actor.vx==0)return 0;EnemyTerrain.Collision c=probe(environment,actor,(short)((actor.x>>16)+x),(short)((actor.y>>16)+y));if((c.effects&2)!=0){actor.x=((short)((actor.x>>16)+(actor.vx>0?c.left:c.right)))<<16;return 2;}return 0;}
    public void update(Environment environment,int cameraX,int cameraY,int gameTimer){beginFrame();for(int i=0;i<POOL;i++)updateSlot(i,environment,cameraX,cameraY,gameTimer);}
    void updateSlot(int index,Environment environment,int cameraX,int cameraY,int gameTimer){
        if(bottleFields==null)return;Actor a=actors[index];if(a==null||!a.active)return;
        if(a.hitboxState!=0&&a.hitboxWidth>0&&a.hitboxHeight>0){int hits=environment.hit(a,a.x>>16,(a.y>>16)+a.hitboxOffY,a.hitboxWidth,a.hitboxHeight,a.sequence,a.entityId==FLAME?flameFields[0]:bottleFields[0]);if(hits>0)a.hitCount++;}
        if(a.entityId==BOTTLE)updateBottle(a,environment);else if(a.entityId==GLASS)updateGlass(a);else if(a.entityId==PARTICLES)updateParticles(a);else updateFlame(a,gameTimer);
        if(a.active&&((a.x>>16)-cameraX>288||(a.x>>16)-cameraX<-32||(a.y>>16)-cameraY>256||(a.y>>16)-cameraY<-16))destroy(a,"outside-source-camera");
    }
    private void updateBottle(Actor a,Environment environment){switch(a.step){
        case 0:a.animSet=9;a.frame=29;a.y+=8*ONE;a.vx=a.face*81920;a.vy=-204800;a.damageRoll=random()%12;a.hitboxWidth=a.hitboxHeight=4;a.hitboxState=bottleFields[8];a.step=1;break;
        case 1:
            a.y+=a.vy;if(a.vy<4*ONE)a.vy+=14336;int collision=floor(environment,a,0,0);a.x+=a.vx;collision|=side(environment,a,-7,a.vx<0?-4:4);if((collision&2)!=0)collision=1;
            if((collision&1)!=0){queue(GLASS,a,0);a.animSet=0;a.remaining=16;a.step=2;}break;
        case 2:if((a.remaining&3)==0)queue(FLAME,a,(flameCounter++&255)<<8);if(--a.remaining==0){a.remaining=4;a.step=3;}break;
        case 3:if(--a.remaining==0)destroy(a,"break-schedule-complete");break;
        default:throw new IllegalStateException("Invalid bottle step");
    }}
    private void shardQuad(Primitive p,Mover mover,int shape){int x=mover.x>>16,y=mover.y>>16,at=shape*6;for(int i=0;i<3;i++){p.quad[i*2]=x+shardOffsets[at+i*2];p.quad[i*2+1]=y+shardOffsets[at+i*2+1];}p.quad[6]=p.quad[4];p.quad[7]=p.quad[5];}
    private void updateGlass(Actor a){if(a.step==0){
        if(!allocate(a,16)){allocationFailures++;destroy(a,"primitive-allocation-failed");return;}a.allocated=true;a.primitives=new Primitive[8];a.movers=new Mover[8];
        for(int i=0;i<8;i++){Mover m=new Mover();m.x=(a.x>>16)<<16;m.y=(a.y>>16)<<16;m.vx=(random()&16383)+16384;if((i&1)!=0)m.vx=-m.vx;m.vy=-((random()*2)+163840);a.movers[i]=m;}
        for(int i=0;i<8;i++){Primitive p=new Primitive();p.rgb[0]=(random()&15)+48;p.rgb[2]=(random()&127)+128;p.rgb[1]=(random()&31)+48;p.drawMode=(random()&1)!=0?55:6;shardQuad(p,a.movers[i],(i+8)&3);a.primitives[i]=p;}a.remaining=20;a.step=1;
    }else{if(--a.remaining==0){destroy(a,"glass-timer-complete");return;}for(int i=0;i<8;i++){Mover m=a.movers[i];m.x+=m.vx;m.y+=m.vy;m.vy+=18432;shardQuad(a.primitives[i],m,(i+8)&3);}}}
    private void updateFlame(Actor a,int gameTimer){
        int texture=(gameTimer&7)*2,texX=uvOrigins[texture],texY=uvOrigins[texture+1];
        if(a.step==0){int r=(random()&31)+64,g=(random()&31)+128,b=(random()&31)+96;if(!allocate(a,4)){allocationFailures++;destroy(a,"primitive-allocation-failed");return;}a.allocated=true;a.primitives=new Primitive[4];for(int i=0;i<4;i++){Primitive p=new Primitive();p.rgb[0]=r;p.rgb[1]=g;p.rgb[2]=b;p.drawMode=63;a.primitives[i]=p;}a.damageRoll=random()%12;a.hitboxState=flameFields[8];a.hitboxWidth=4;if(particleFields!=null)queue(PARTICLES,a,7<<8);a.remaining=80;a.heightBase=(random()&15)+18;a.step=1;return;}
        int[] xs=new int[5],ys=new int[5];for(int i=0;i<4;i++)xs[i]=(short)((a.x>>16)+(sin(a.angle+(i<<10))>>10));xs[0]=xs[4]=(short)(a.x>>16);a.angle+=384;
        a.flameHeight=(short)((sin((a.remaining-16)*64+3072)>>8)+a.heightBase);ys[0]=(short)((a.y>>16)-a.flameHeight);ys[4]=(short)(a.y>>16);ys[2]=(short)((ys[0]+ys[4])/2);ys[1]=(short)((ys[0]+ys[2])/2);ys[3]=(short)((ys[2]+ys[4])/2);
        a.hitboxState=(a.remaining&3)!=0?0:2;if(--a.remaining<17){destroy(a,"flame-timer-complete");return;}
        for(int i=0;i<4;i++){Primitive p=a.primitives[i];if(a.remaining<41)for(int channel=0;channel<3;channel++)if(p.rgb[channel]>16)p.rgb[channel]-=5;
            p.quad[0]=xs[i]-8;p.quad[1]=ys[i];p.quad[2]=xs[i]+8;p.quad[3]=ys[i];p.quad[4]=xs[i+1]-8;p.quad[5]=ys[i+1];p.quad[6]=xs[i+1]+8;p.quad[7]=ys[i+1];
            p.uv[0]=p.uv[2]=texX+128-i*7;p.uv[4]=p.uv[6]=texX+128-(i+1)*7;p.uv[1]=p.uv[5]=texY+128;p.uv[3]=p.uv[7]=texY+144;p.drawMode&=~8;if(ys[4]-ys[0]<7)p.drawMode|=8;
        }a.hitboxHeight=a.flameHeight>>1;a.hitboxOffY=(-a.flameHeight)>>1;
    }
    private void updateParticles(Actor a){
        if(a.step==0){
            if(!allocate(a,particleFields[0]+1)){allocationFailures++;destroy(a,"primitive-allocation-failed");return;}
            a.allocated=true;a.remaining=31;a.tiles=new Tile[9];
            for(int i=0;i<9;i++){
                Tile tile=new Tile();tile.drawMode=particleFields[7];tile.priority=particleFields[6];a.tiles[i]=tile;
                if(i==8){tile.control=true;tile.primitiveType=1;tile.drawMode&=~8;break;}
                tile.primitiveType=17;tile.x=((short)((a.x>>16)+(random()&15)-7))<<16;tile.y=((short)((a.y>>16)-(random()&31)))<<16;tile.vy=-49152-(random()&32767);tile.vx=a.parent==null?0:a.parent.vx;tile.delay=15+2*i;tile.width=particleFields[4];tile.height=particleFields[5];for(int c=0;c<3;c++)tile.rgb[c]=particleFields[c+1];
            }a.step=1;return;
        }
        // Source case7 falls through case5/6: BOTH pre-decrements are real.
        if(--a.remaining==0||--a.remaining==0){destroy(a,"particle-timer-complete");return;}
        for(int i=0;i<8;i++){Tile tile=a.tiles[i];tile.y+=tile.vy;if(--tile.delay<0)tile.drawMode|=8;}
        // Source terminal is visible but has width0; it sets ABR3 for LIFO tiles.
        Tile terminal=a.tiles[8];terminal.drawMode&=~8;terminal.x=terminal.y=terminal.width=0;
    }
    public String getState(){StringBuilder out=new StringBuilder("[");boolean comma=false;for(Actor a:actors)if(a!=null){if(comma)out.append(',');comma=true;String kind=a.entityId==BOTTLE?"holyWaterBottle":a.entityId==GLASS?"holyWaterGlass":a.entityId==PARTICLES?"holyWaterParticles":"holyWaterFlame";
        out.append("{\"kind\":\"").append(kind).append("\",\"entityId\":").append(a.entityId).append(",\"inventoryId\":").append(a.entityId==BOTTLE?3:a.entityId==FLAME?11:0).append(",\"slot\":").append(a.slot).append(",\"serial\":").append(a.serial).append(",\"sequence\":").append(a.sequence).append(",\"params\":").append(a.params).append(",\"x\":").append(a.x>>16).append(",\"y\":").append(a.y>>16).append(",\"xFixed\":").append(a.x).append(",\"yFixed\":").append(a.y).append(",\"vxFixed\":").append(a.vx).append(",\"vyFixed\":").append(a.vy).append(",\"face\":").append(a.face).append(",\"facingLeft\":").append(a.facingLeft).append(",\"step\":").append(a.step).append(",\"frame\":").append(a.frame).append(",\"animSet\":").append(a.animSet).append(",\"damageRoll\":").append(a.damageRoll).append(",\"remaining\":").append(a.remaining).append(",\"angle\":").append(a.angle).append(",\"flameHeight\":").append(a.flameHeight).append(",\"heightBase\":").append(a.heightBase).append(",\"hitboxState\":").append(a.hitboxState).append(",\"hitboxWidth\":").append(a.hitboxWidth).append(",\"hitboxHeight\":").append(a.hitboxHeight).append(",\"hitboxOffY\":").append(a.hitboxOffY).append(",\"hitCount\":").append(a.hitCount).append(",\"hitTargetCount\":").append(a.targetCount).append(",\"unsupportedQueries\":").append(a.unsupportedQueries).append(",\"visible\":").append(a.active&&a.entityId==BOTTLE&&a.animSet!=0).append(",\"active\":").append(a.active).append(",\"alive\":").append(a.active).append(",\"destroyedReason\":\"").append(a.destroyedReason).append("\",\"primitives\":[");
        if(a.primitives!=null)for(int i=0;i<a.primitives.length;i++){if(i!=0)out.append(',');Primitive p=a.primitives[i];out.append("{\"index\":").append(a.entityId==GLASS?i+8:i).append(",\"quad\":[");for(int j=0;j<8;j++){if(j!=0)out.append(',');out.append(p.quad[j]);}out.append("],\"uv\":[");for(int j=0;j<8;j++){if(j!=0)out.append(',');out.append(p.uv[j]);}out.append("],\"rgb\":[").append(p.rgb[0]).append(',').append(p.rgb[1]).append(',').append(p.rgb[2]).append("],\"palette\":").append(a.entityId==FLAME?434:0).append(",\"drawMode\":").append(p.drawMode).append(",\"translucent\":").append((p.drawMode&1)!=0).append(",\"visible\":").append(a.active&&(p.drawMode&8)==0).append('}');}
        out.append("],\"movers\":[");if(a.movers!=null)for(int i=0;i<a.movers.length;i++){if(i!=0)out.append(',');Mover m=a.movers[i];out.append("{\"index\":").append(i).append(",\"xFixed\":").append(m.x).append(",\"yFixed\":").append(m.y).append(",\"vxFixed\":").append(m.vx).append(",\"vyFixed\":").append(m.vy).append(",\"visible\":false}");}
        out.append("],\"tiles\":[");if(a.tiles!=null)for(int i=0;i<a.tiles.length;i++){if(i!=0)out.append(',');Tile t=a.tiles[i];out.append("{\"index\":").append(i).append(",\"primitiveType\":").append(t.primitiveType).append(",\"behavior\":3,\"control\":").append(t.control).append(",\"x\":").append(t.x>>16).append(",\"y\":").append(t.y>>16).append(",\"xFixed\":").append(t.x).append(",\"yFixed\":").append(t.y).append(",\"vxFixed\":").append(t.vx).append(",\"vyFixed\":").append(t.vy).append(",\"delay\":").append(t.delay).append(",\"width\":").append(t.width).append(",\"height\":").append(t.height).append(",\"rgb\":[").append(t.rgb[0]).append(',').append(t.rgb[1]).append(',').append(t.rgb[2]).append("],\"drawMode\":").append(t.drawMode).append(",\"priorityRelativeToPlayer\":").append(t.priority).append(",\"visible\":").append(a.active&&(t.drawMode&8)==0&&t.width>0&&t.height>0).append('}');}out.append("]}");}return out.append(']').toString();}
    Actor actor(int index){return actors[index];}int allocationFailures(){return allocationFailures;}int factoryFailures(){return factoryFailures;}int randomState(){return randomState;}int randomCalls(){return randomCalls;}void seedRandom(int seed){randomState=seed;randomCalls=0;}int sine(int angle){return sin(angle);}void setPrimitiveCapacity(int capacity){pool.setPrimitiveCapacity(capacity);}int primitiveUsage(){return pool.usedPrimitives();}int peakPrimitiveUsage(){return peakPrimitiveUsage;}
}
