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

/** Selected US EntitySubwpnAgunea and EntityAguneaHitEnemy from pinned
 * src/dra/84B88.c. Source row9, quarter/atan tables and BP22/23/61 are data.
 * Parent line, held target phases/costs and all40 ribbon strips are retained,
 * including allocator-zero y1=-16 and unused left-facing +0x800 source bugs.
 * Stable target identity/current overlap/base10 once-per-pulse damage, selected
 * next-tick jobs, shared256 primitive budget and world-camera conversion are
 * explicit local bridges. BP61/2 outline requests can connect to a separately
 * registered selected PlayerOutlineWorld; BP44/52 can connect to the selected
 * normal-Alucard PlayerBlinkWorld. Broader blink profiles remain unfinished.
 * Global player/factory/battle/GPU/RNG phase and
 * complete inventory/status/audio are unfinished. All actors are volatile.
 */
public final class AguneaWorld {
    static final int ONE=65536,BOLT=17,RIBBON=18,PRIORITY=150;
    interface RandomSource {int next();}
    interface OutlineSink {boolean registered();void request(int sequence);}
    interface BlinkSink {boolean registered();void request(int sequence,int x,int y,int face);}
    static final class Target {
        final Object identity;int entityId,x,y,hp,flags;
        Target(Object id,int entityId,int x,int y,int hp,int flags){identity=id;this.entityId=entityId;this.x=x;this.y=y;this.hp=hp;this.flags=flags;}
    }
    interface Environment {
        Target capture(int x,int y,int halfWidth,int halfHeight,int damage,int sequence);
        Target resolve(Object identity);
        void pulse(Object identity,int damage,int sequence);
        int hearts();boolean spend(int amount);boolean cancelled();
    }
    static final class Primitive {
        final int[] quad=new int[8],uv=new int[8];
        int gray,palette=404,drawMode=8;
    }
    static final class Actor {
        final int slot,serial,sequence,kind;int x,y,vx,face,step,substep,counter,pulses,anchorX,anchorY,red,green,blue,drawMode,flags,hitboxState,hitCount,damageRoll,angle,turn,turnRemaining,lastStrip,params;
        boolean active=true,allocated;String destroyedReason="";Object target;
        Actor parent;Primitive[] primitives=new Primitive[0];
        Actor(int slot,int serial,int sequence,int kind,int x,int y,int face){this.slot=slot;this.serial=serial;this.sequence=sequence;this.kind=kind;this.x=x;this.y=y;this.face=face;}
    }
    private static final class Job {final Actor parent;Job(Actor parent){this.parent=parent;}}
    private final DaggerWorld.SharedPool pool;private final RandomSource randomSource;
    private final Actor[] actors=new Actor[32];private final Job[] jobs=new Job[128];private int jobCount;
    private int[] fields,quarter,blueprints,atan;
    private OutlineSink outlineSink;
    private BlinkSink blinkSink;
    private int serial,randomState=1,randomCalls,allocationFailures,factoryFailures,ribbonBirths,pulseCount,repeatHeartsSpent,blinkRequests,outlineFactoryRequests,outlineChildRequests,soundEvents,playerTimerRequests,peakPrimitiveUsage,heartBroaches;
    public AguneaWorld(){this(new DaggerWorld.SharedPool(),null);}
    AguneaWorld(DaggerWorld.SharedPool pool,RandomSource random){if(pool==null)throw new IllegalArgumentException("Missing shared pool");this.pool=pool;randomSource=random;}
    public boolean register(int[] f,int[] q,int[] bp,int[] table){
        if(f==null||f.length!=13||q==null||q.length!=1025||bp==null||bp.length!=18||!AguneaMath.valid(table))return false;
        int[] exact={10,5,20480,1,10,4,93,22,2,1,0,0,0},factories={17,1,193,0,34,0,18,1,193,0,32,0,31,3,193,5,32,0};
        for(int i=0;i<13;i++)if(f[i]!=exact[i])return false;for(int i=0;i<18;i++)if(bp[i]!=factories[i])return false;
        if(q[0]!=0||q[1024]!=4096)return false;for(int i=0;i<1025;i++)if(q[i]<0||q[i]>4096||i>0&&q[i]<q[i-1])return false;
        fields=f.clone();quarter=q.clone();blueprints=bp.clone();atan=table.clone();reset();return true;
    }
    public boolean registered(){return fields!=null;}
    void setOutlineSink(OutlineSink sink){outlineSink=sink;}
    void setBlinkSink(BlinkSink sink){blinkSink=sink;}
    public int heartCost(){int cost=registered()?fields[1]:5;if(heartBroaches==1)cost/=2;else if(heartBroaches==2)cost/=3;return Math.max(1,cost);}
    public boolean setHeartBroaches(int count){if(count<0||count>2)return false;heartBroaches=count;return true;}
    public int activeCount(){int n=0;for(Actor a:actors)if(a!=null&&a.active&&a.kind==BOLT)n++;return n;}
    public int refusal(){return !registered()?1:activeCount()>=fields[3]||pool.freeSlot()<0?2:0;}
    public boolean spawn(int x,int y,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||y<Integer.MIN_VALUE+64*ONE||y>Integer.MAX_VALUE-64*ONE)return false;int slot=pool.freeSlot();Actor a=new Actor(slot+32,++serial,sequence,BOLT,x,y,face);pool.reserve(slot,a);actors[slot]=a;return true;}
    public void reset(){for(int i=0;i<actors.length;i++){if(actors[i]!=null)pool.release(actors[i]);actors[i]=null;}for(int i=0;i<jobCount;i++)jobs[i]=null;jobCount=0;serial=randomCalls=allocationFailures=factoryFailures=ribbonBirths=pulseCount=repeatHeartsSpent=blinkRequests=outlineFactoryRequests=outlineChildRequests=soundEvents=playerTimerRequests=peakPrimitiveUsage=heartBroaches=0;randomState=1;}
    private int random(){randomCalls++;if(randomSource!=null)return randomSource.next();randomState=randomState*1103515245+12345;return(randomState>>>16)&32767;}
    private int sin(int angle){return CrossMath.sine(quarter,angle);}private int cos(int angle){return sin(angle+1024);}
    private static int high(int fixed,int value){return((short)value<<16)|(fixed&65535);}
    private static int vertex(int value){return(short)value;}
    private void destroy(Actor a,String reason){a.active=a.allocated=false;a.hitboxState=0;a.destroyedReason=reason;a.drawMode|=8;for(Primitive p:a.primitives)p.drawMode|=8;pool.release(a);}
    private boolean allocate(Actor a,int amount){if(!pool.allocate(a,amount)){allocationFailures++;destroy(a,"primitive-allocation-failed");return false;}a.allocated=true;peakPrimitiveUsage=Math.max(peakPrimitiveUsage,pool.usedPrimitives());return true;}
    private void queueRibbon(Actor a){if(jobCount==jobs.length){factoryFailures++;return;}jobs[jobCount++]=new Job(a);}
    /** Selected jobs are created before the next ascending source-slot pass. */
    public void beginFrame(int playerX,int playerY,int face){int count=jobCount;jobCount=0;for(int i=0;i<count;i++){Job job=jobs[i];jobs[i]=null;int slot=pool.freeReverse();if(slot<0){factoryFailures++;continue;}Actor parent=job.parent,a=new Actor(slot+32,++serial,parent.sequence,RIBBON,playerX,playerY,face);a.parent=parent;pool.reserve(slot,a);actors[slot]=a;ribbonBirths++;}}
    public void update(Environment e,int playerX,int playerY,int face,int playerOffY,boolean held,int cameraX,int cameraY){beginFrame(playerX,playerY,face);for(int i=0;i<32;i++)updateSlot(i,e,playerX,playerY,face,playerOffY,held,cameraX,cameraY);}
    void updateSlot(int index,Environment e,int playerX,int playerY,int face,int playerOffY,boolean held,int cameraX,int cameraY){if(!registered())return;Actor a=actors[index];if(a==null||!a.active)return;if(a.kind==BOLT)updateBolt(a,e,playerX,playerY,face,playerOffY,held,cameraX,cameraY);else updateRibbon(a,playerX,playerY,face,playerOffY);}
    private void updateBolt(Actor a,Environment e,int playerX,int playerY,int face,int playerOffY,boolean held,int cameraX,int cameraY){
        if(e.cancelled()){destroy(a,"source-status-cancelled");return;}
        switch(a.step){
        case 0:
            if(!allocate(a,1))return;a.flags=0x0C800000;a.damageRoll=random()%12;a.hitboxState=fields[8];
            a.face=face;a.x=high(a.x,playerX>>16);a.y=high(a.y,(playerY>>16)+playerOffY-8);a.anchorX=a.x>>16;a.anchorY=a.y>>16;
            a.red=96;a.green=0;a.blue=128;a.drawMode=0x331;a.vx=a.face*6*ONE;blinkRequests++;if(blinkSink!=null&&blinkSink.registered())blinkSink.request(a.sequence,a.x,a.y,a.face);soundEvents++;playerTimerRequests++;a.step=1;break;
        case 1:{
            Target target=e.capture((a.x>>16)+a.face*4,a.y>>16,4,4,fields[0],a.sequence);
            a.x+=a.vx;if((a.x>>16)-cameraX<-64||(a.x>>16)-cameraX>320||(a.y>>16)-cameraY<-32||(a.y>>16)-cameraY>288)a.step=2;
            if(target!=null&&target.identity!=null){a.target=target.identity;a.hitCount++;a.step=3;}
            break;}
        case 4:{
            Target t=e.resolve(a.target);
            // Safe identity bridge replaces dereferencing a source entity slot.
            if(t!=null){a.x=high(a.x,t.x>>16);a.y=high(a.y,t.y>>16);}
            a.counter=(short)(a.counter+1);if(a.counter<16)break;
            if(a.red<5){destroy(a,"released-line-faded");return;}break;}
        case 2:if(a.red<5){destroy(a,"line-faded");return;}break;
        case 3:{
            if(!held)a.step=4;
            Target t=e.resolve(a.target);if(t==null||t.entityId==0){a.step=2;return;}
            if(a.counter!=0&&(t.hp>0x7000||t.hp==0||(t.flags&0x100)!=0)){a.step=2;return;}
            a.x=high(a.x,t.x>>16);a.y=high(a.y,t.y>>16);
            if(a.counter%12==0){
                // Source consumes jitter even when a later paid pulse fails.
                int xJitter=(random()&15)-8,yJitter=(random()&15)-8;
                // BP23/BP61 are origin2, so their later factory captures PLAYER.
                // Jitter is still recorded by RNG; source restores target high halves.
                if(a.pulses==0||e.spend(heartCost())){if(a.pulses>0)repeatHeartsSpent+=heartCost();a.pulses++;pulseCount++;queueRibbon(a);outlineFactoryRequests++;outlineChildRequests+=3;if(outlineSink!=null&&outlineSink.registered())outlineSink.request(a.sequence);soundEvents++;e.pulse(a.target,fields[0],a.sequence);}
                else a.step=4;
            }
            a.counter=(short)(a.counter+1);break;}
        default:throw new IllegalStateException("Invalid Agunea parent step");}
        if(a.red>=4)a.red-=4;if(a.green>=4)a.green-=4;if(a.blue>=4)a.blue-=4;if(a.blue<5)a.drawMode|=8;
    }
    private void updateRibbon(Actor a,int playerX,int playerY,int face,int playerOffY){
        Actor parent=a.parent;a.x=high(a.x,playerX>>16);a.y=high(a.y,(playerY>>16)+playerOffY-8);
        if(parent==null||!parent.active||parent.kind!=BOLT){if(a.step==0){destroy(a,"source-parent-missing-before-init");return;}if(a.step==1||a.step==2||a.step==4)a.step=3;}
        switch(a.step){
        case 0:
            if(!allocate(a,40))return;a.flags=0x04800000;a.face=face;a.angle=((random()&1023)-512)&4095; // Original left +0x800 expression is unused.
            a.primitives=new Primitive[40];for(int i=0;i<40;i++){Primitive p=a.primitives[i]=new Primitive();int u=(144+(i%6)*16)&255;p.uv[0]=p.uv[2]=u;p.uv[4]=p.uv[6]=(u+16)&255;p.uv[1]=p.uv[5]=208;p.uv[3]=p.uv[7]=192;p.quad[0]=a.x>>16;p.quad[1]=a.y>>16;p.quad[4]=a.x>>16;p.quad[5]=a.x>>16; // Original y2=X typo on hidden records.
                p.gray=128;p.palette=404;p.drawMode=8;}
            Primitive head=a.primitives[0];head.quad[0]=head.quad[2]=head.quad[4]=head.quad[6]=a.x>>16;head.quad[1]=head.quad[5]=a.y>>16;head.quad[3]=-16;head.quad[7]=vertex((a.y>>16)-16);a.step=1;break;
        case 1:
            for(int segment=0;segment<2;segment++){
                Primitive p=a.primitives[a.lastStrip];int dx=(short)((parent.x>>16)-p.quad[4]),dy=(short)((parent.y>>16)-p.quad[5]);
                if(Math.abs(dx)<8&&Math.abs(dy)<8){a.step=2;return;}
                boolean near=Math.abs(dx)<64&&Math.abs(dy)<64;
                if(a.turnRemaining==0){a.turnRemaining=near?2:4;int diff=(short)(AguneaMath.atan2(-dy,dx,atan)-a.angle);if(diff>2048)diff=4096-diff;if(diff<-2048)diff=4096+diff;a.turn=(short)(diff/(near?2:4));}
                int angle=(short)(a.angle+a.turn);if(!near)angle=(short)(angle+384-((random()&3)<<8));angle&=4095;
                int next=a.lastStrip+1;if(next==40){a.step=2;return;}
                Primitive q=a.primitives[next];q.quad[0]=p.quad[4];q.quad[1]=p.quad[5];q.quad[2]=p.quad[6];q.quad[3]=p.quad[7];a.angle=angle;a.lastStrip=next;
                q.quad[4]=vertex(q.quad[0]+((cos(angle)*12)>>12));q.quad[5]=vertex(q.quad[1]-((sin(angle)*12)>>12));int width=(16-a.params*4)&255;
                q.quad[6]=vertex(q.quad[4]+((width*cos(angle-1024))>>12));q.quad[7]=vertex(q.quad[5]-((width*sin(angle-1024))>>12));q.drawMode=6;a.turnRemaining--;
            }break;
        case 2:
            if(a.substep==0){for(Primitive p:a.primitives)p.palette=351;a.substep=1;return;}
            for(Primitive p:a.primitives){p.palette=404;p.gray=96;}a.substep=0;a.step=3;break;
        case 3:
            boolean done=true;for(Primitive p:a.primitives){int value=(p.gray-4)&255;if(value<16)value=16;else done=false;p.gray=value;}
            if(done)destroy(a,"ribbon-brightness-ended");break;
        default:throw new IllegalStateException("Invalid Agunea ribbon step");}
    }
    public String getState(){StringBuilder s=new StringBuilder("[");boolean comma=false;for(Actor a:actors)if(a!=null){if(comma)s.append(',');comma=true;s.append("{\"kind\":\"").append(a.kind==BOLT?"aguneaBolt":"aguneaRibbon").append("\",\"screenSpace\":false,\"entityId\":").append(a.kind).append(",\"inventoryId\":").append(a.kind==BOLT?9:0).append(",\"slot\":").append(a.slot).append(",\"serial\":").append(a.serial).append(",\"sequence\":").append(a.sequence).append(",\"x\":").append(a.x>>16).append(",\"y\":").append(a.y>>16).append(",\"xFixed\":").append(a.x).append(",\"yFixed\":").append(a.y).append(",\"face\":").append(a.face).append(",\"step\":").append(a.step).append(",\"substep\":").append(a.substep).append(",\"counter\":").append(a.counter).append(",\"pulses\":").append(a.pulses).append(",\"angle\":").append(a.angle).append(",\"turn\":").append(a.turn).append(",\"turnRemaining\":").append(a.turnRemaining).append(",\"lastStrip\":").append(a.lastStrip).append(",\"flags\":").append(a.flags).append(",\"hitboxState\":").append(a.hitboxState).append(",\"hitCount\":").append(a.hitCount).append(",\"damageRoll\":").append(a.damageRoll).append(",\"active\":").append(a.active).append(",\"alive\":").append(a.active).append(",\"destroyedReason\":\"").append(a.destroyedReason).append("\",\"lines\":[");
            if(a.kind==BOLT&&a.allocated)s.append("{\"index\":0,\"x0\":").append(a.anchorX).append(",\"y0\":").append(a.anchorY).append(",\"x1\":").append(a.x>>16).append(",\"y1\":").append(a.y>>16).append(",\"rgb\":[0,0,0,").append(a.red).append(',').append(a.green).append(',').append(a.blue).append("],\"drawMode\":").append(a.drawMode).append(",\"priority\":150,\"visible\":").append(a.active&&(a.drawMode&8)==0).append('}');s.append("],\"primitives\":[");
            for(int i=0;i<a.primitives.length;i++){if(i>0)s.append(',');Primitive p=a.primitives[i];s.append("{\"index\":").append(i).append(",\"quad\":[");appendArray(s,p.quad);s.append("],\"uv\":[");appendArray(s,p.uv);s.append("],\"rgb\":[").append(p.gray).append(',').append(p.gray).append(',').append(p.gray).append("],\"drawMode\":").append(p.drawMode).append(",\"palette\":").append(p.palette).append(",\"priority\":150,\"visible\":").append(a.active&&(p.drawMode&8)==0).append('}');}s.append("]}");}return s.append(']').toString();}
    private static void appendArray(StringBuilder s,int[] values){for(int i=0;i<values.length;i++){if(i>0)s.append(',');s.append(values[i]);}}
    public String getStatistics(){return "{\"randomCalls\":"+randomCalls+",\"allocationFailures\":"+allocationFailures+",\"factoryFailures\":"+factoryFailures+",\"ribbonBirths\":"+ribbonBirths+",\"pulses\":"+pulseCount+",\"repeatHeartsSpent\":"+repeatHeartsSpent+",\"heartBroaches\":"+heartBroaches+",\"heartCost\":"+heartCost()+",\"pendingJobs\":"+jobCount+",\"blinkRequests\":"+blinkRequests+",\"outlineFactoryRequests\":"+outlineFactoryRequests+",\"outlineChildRequests\":"+outlineChildRequests+",\"outlinesImplemented\":"+(outlineSink!=null&&outlineSink.registered())+",\"blinkImplemented\":"+(blinkSink!=null&&blinkSink.registered())+",\"blinkPending\":"+(blinkSink==null||!blinkSink.registered())+",\"pendingCosmeticScope\":"+(blinkSink==null||!blinkSink.registered()||outlineSink==null||!outlineSink.registered())+",\"soundEvents\":"+soundEvents+",\"playerTimerRequests\":"+playerTimerRequests+",\"primitiveUsage\":"+pool.usedPrimitives()+",\"primitiveCapacity\":"+pool.primitiveCapacity+",\"peakPrimitiveUsage\":"+peakPrimitiveUsage+"}";}
    Actor actor(int index){return actors[index];}int randomCalls(){return randomCalls;}int allocationFailures(){return allocationFailures;}void seedRandom(int seed){randomState=seed;randomCalls=0;}
}
