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

/** Selected US Bible book/trail from pinned sotn-decomp
 * 0622fcfd37311fd21baf2938c6bd74cea63005ba, src/dra/8A0A4.c.
 * The US routine reads inherited uninitialized $s4 before defining it. This
 * reconstruction explicitly uses fresh0 each orbit callback, like FIX_UB;
 * that is an authored deterministic correction, NOT recovered US ABI state.
 * Remaining selected integer phases/motion/UV/factory/trail behavior is ported.
 * Screen coordinates, current-camera battle overlap, next-tick factories,
 * shared256 primitive proxy/base8 damage and selected isolated RNG replace
 * whole-game dispatch/battle/GPU/player/audio integration.
 */
public final class BibleWorld {
    static final int ONE=65536,BOOK=43,TRAIL=44,PLAYER_PRIORITY=148;
    interface RandomSource {int next();}
    interface Environment {int hit(Actor actor,int x,int y,int width,int height,int sequence,int damage);}
    static final class Primitive {final int[] quad=new int[8],uv=new int[8],rgb=new int[3];int drawMode,priority,palette,tpage;}
    static final class Actor {
        final int entityId,slot,serial,sequence;final Object[] targets=new Object[256];
        int x,y,vx,vy,face,step,counter,radius,angle,tilt,tiltSpeed,motionMode,gray,priority=PLAYER_PRIORITY,hitboxState,hitboxWidth,hitboxHeight,hitCount,targetCount,damageRoll;
        boolean active=true,allocated,keepAlive;String destroyedReason="";Primitive primitive;
        Actor(int id,int slot,int serial,int sequence,int x,int y,int face){entityId=id;this.slot=slot;this.serial=serial;this.sequence=sequence;this.x=x;this.y=y;this.face=face;}
        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 x,y,face,sequence,priority,created;Pending(Actor a,int tick){x=a.x;y=a.y;face=a.face;sequence=a.sequence;priority=a.priority;created=tick;}}
    private final DaggerWorld.SharedPool pool;private final RandomSource randomSource;
    private final Actor[] actors=new Actor[32];private final Pending[] pending=new Pending[8];
    private int[] fields,quarter,blueprints;private int serial,frameCounter,randomState=1,randomCalls,allocationFailures,factoryFailures,trailBirths,soundEvents,playerTimerRequests,peakPrimitiveUsage;
    public BibleWorld(){this(new DaggerWorld.SharedPool(),null);}
    BibleWorld(DaggerWorld.SharedPool shared,RandomSource random){if(shared==null)throw new IllegalArgumentException("Missing shared pool");pool=shared;randomSource=random;}
    public boolean register(int[] definition,int[] sineQuarter,int[] factoryBlueprints){
        if(definition==null||definition.length!=13||sineQuarter==null||sineQuarter.length!=1025||factoryBlueprints==null||factoryBlueprints.length!=12)return false;
        for(int i=0;i<13;i++)if(definition[i]<0||definition[i]>(i<3?65535:255))return false;
        if(definition[0]<1||definition[0]>9999||definition[1]!=5||definition[3]!=1||definition[6]!=93||definition[7]!=77||definition[8]!=2)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;
        int[] expected={43,1,193,0,2,0,44,1,193,0,0,0};for(int i=0;i<12;i++)if(factoryBlueprints[i]!=expected[i])return false;
        fields=definition.clone();quarter=sineQuarter.clone();blueprints=factoryBlueprints.clone();reset();return true;
    }
    public boolean registered(){return fields!=null;}public int heartCost(){return fields==null?0:fields[1];}
    public int activeCount(){int count=0;for(int i=0;i<16;i++){Actor a=actors[i];if(a!=null&&a.active&&a.entityId==BOOK)count++;}return count;}
    public int refusal(){return !registered()?1:activeCount()>=fields[3]||pool.freeSlot()<0?2:0;}
    public boolean spawn(int screenXFixed,int originYFixed,int face,int sequence){if(refusal()!=0||face!=1&&face!=-1||sequence<1||screenXFixed<Integer.MIN_VALUE+64*ONE||screenXFixed>Integer.MAX_VALUE-64*ONE||originYFixed<Integer.MIN_VALUE+64*ONE||originYFixed>Integer.MAX_VALUE-64*ONE)return false;create(BOOK,pool.freeSlot(),screenXFixed,originYFixed,face,sequence);return true;}
    private Actor create(int id,int index,int x,int y,int face,int sequence){Actor a=new Actor(id,index+32,++serial,sequence,x,y,face);pool.reserve(index,a);actors[index]=a;if(id==TRAIL)trailBirths++;return a;}
    public void reset(){for(int i=0;i<32;i++){if(actors[i]!=null)pool.release(actors[i]);actors[i]=null;}for(int i=0;i<pending.length;i++)pending[i]=null;serial=frameCounter=randomCalls=allocationFailures=factoryFailures=trailBirths=soundEvents=playerTimerRequests=peakPrimitiveUsage=0;randomState=1;}
    private int random(){randomCalls++;if(randomSource!=null)return randomSource.next();randomState=randomState*1103515245+12345;return(randomState>>>16)&32767;}
    private boolean allocate(Actor a){if(!pool.allocate(a,1)){allocationFailures++;destroy(a,"primitive-allocation-failed");return false;}a.allocated=true;a.keepAlive=true;a.primitive=new Primitive();peakPrimitiveUsage=Math.max(peakPrimitiveUsage,pool.usedPrimitives());return true;}
    private void destroy(Actor a,String reason){a.active=a.allocated=false;a.hitboxState=0;a.destroyedReason=reason;if(a.primitive!=null)a.primitive.drawMode|=8;pool.release(a);}
    private void queue(Actor parent){for(int i=0;i<pending.length;i++)if(pending[i]==null){pending[i]=new Pending(parent,frameCounter);return;}factoryFailures++;}
    void beginFrame(){frameCounter++;for(int i=0;i<pending.length;i++){Pending p=pending[i];if(p==null||p.created>=frameCounter)continue;pending[i]=null;int slot=pool.freeReverse();if(slot<0){factoryFailures++;continue;}Actor a=create(TRAIL,slot,p.x,p.y,p.face,p.sequence);a.priority=p.priority;}}
    public void update(Environment environment,int playerScreenXFixed,int playerScreenYFixed,int cameraX,int cameraY,int timer){beginFrame();for(int i=0;i<32;i++)updateSlot(i,environment,playerScreenXFixed,playerScreenYFixed,cameraX,cameraY,timer);}
    void updateSlot(int index,Environment e,int playerX,int playerY,int cameraX,int cameraY,int timer){if(!registered())return;Actor a=actors[index];if(a==null||!a.active)return;
        if(a.hitboxState!=0&&a.hitboxWidth>0&&a.hitboxHeight>0&&e.hit(a,(a.x>>16)+cameraX,(a.y>>16)+cameraY,a.hitboxWidth,a.hitboxHeight,a.sequence,fields[0])>0)a.hitCount++;
        if(a.entityId==BOOK)book(a,playerX,playerY,timer);else trail(a);
        if(a.active&&!a.keepAlive&&((a.x>>16)>288||(a.x>>16)<-32||(a.y>>16)>256||(a.y>>16)<-16))destroy(a,"outside-source-camera");
    }
    /** Source C signed32 intermediate operations; seed is explicit test input.
     * Normal callbacks choose0; this does NOT reconstruct inherited US $s4. */
    static int[] orbit(int[] q,int angle,int tilt,int radius,boolean left,int seed){int sine=CrossMath.sine(q,angle),cosine=CrossMath.sine(q,angle+1024);int s5=(sine*radius)>>12,s3=(cosine*radius)>>12,s7=cosine*s5+sine*s3,sp48=cosine*s3-sine*s5;s5=s7>>12;s3=sp48>>12;sine=CrossMath.sine(q,tilt);cosine=CrossMath.sine(q,tilt+1024);s7=(cosine*s5+sine*seed)>>12;int s4=(cosine*seed-sine*s5)>>12;int s6=(cosine*s4+(left?sine*s3:-sine*s3))>>12;return new int[]{s7,s6,s3};}
    private void book(Actor a,int playerX,int playerY,int timer){switch(a.step){
        case 0:if(!allocate(a))return;Primitive p=a.primitive;p.tpage=30;p.palette=383;p.drawMode=264;p.priority=PLAYER_PRIORITY+1;p.uv[0]=p.uv[4]=152;p.uv[2]=p.uv[6]=168;p.uv[1]=p.uv[3]=216;p.uv[5]=p.uv[7]=240;a.tiltSpeed=a.face<0?32:-32;a.damageRoll=random()%12;a.hitboxState=fields[8];a.hitboxWidth=a.hitboxHeight=6;a.step=1;break;
        case 1:a.primitive.drawMode&=~8;a.motionMode++;playerTimerRequests++;a.step=2;
        case 2:a.counter=(short)(a.counter+1);a.radius=(short)(a.radius+1);if(a.radius>=48)a.step=3;break;
        case 3:a.counter=(short)(a.counter+1);if(a.counter>=300){a.keepAlive=false;a.vx=a.face*12*ONE;a.vy=-12*ONE;soundEvents++;a.motionMode++;a.step=4;}break;
        default:break;}
        if(a.motionMode==1){int[] point=orbit(quarter,a.angle,a.tilt,a.radius,a.face<0,0);a.angle=(short)(a.angle+(a.face<0?128:-128));a.angle&=4095;a.tilt=(short)(a.tilt+a.tiltSpeed);if(Math.abs(a.tilt)>=512)a.tiltSpeed=(short)-a.tiltSpeed;a.x=((short)((playerX>>16)+point[0])<<16)|(a.x&65535);a.y=((short)((playerY>>16)+point[1])<<16)|(a.y&65535);a.priority=PLAYER_PRIORITY+(point[2]<0?2:-2);
        }else if(a.motionMode==2){a.x+=a.vx;a.y+=a.vy;a.vy-=2*ONE;}
        if(a.motionMode!=0){rect(a.primitive,a.x>>16,a.y>>16,8,12);a.primitive.priority=a.priority;queue(a);if(timer%10==0)soundEvents++;}
    }
    private void trail(Actor a){switch(a.step){case 0:if(!allocate(a))return;Primitive p=a.primitive;p.tpage=28;p.palette=413;p.uv[0]=p.uv[4]=32;p.uv[2]=p.uv[6]=48;p.uv[1]=p.uv[3]=0;p.uv[5]=p.uv[7]=16;p.priority=a.priority;p.drawMode=277;rect(p,a.x>>16,a.y>>16,8,8);a.gray=96;a.step=1;break;
        case 1:a.counter=(short)(a.counter+1);if(a.counter>5)a.step=2;a.gray=(short)(a.gray-8);break;
        case 2:destroy(a,"trail-fade-complete");return;default:throw new IllegalStateException("Invalid Bible trail step");}
        for(int i=0;i<3;i++)a.primitive.rgb[i]=a.gray;
    }
    private static void rect(Primitive p,int x,int y,int halfWidth,int halfHeight){p.quad[0]=p.quad[4]=(short)(x-halfWidth);p.quad[2]=p.quad[6]=(short)(x+halfWidth);p.quad[1]=p.quad[3]=(short)(y-halfHeight);p.quad[5]=p.quad[7]=(short)(y+halfHeight);}
    public String getState(){StringBuilder out=new StringBuilder("[");boolean comma=false;for(Actor a:actors)if(a!=null){if(comma)out.append(',');comma=true;out.append("{\"kind\":\"").append(a.entityId==BOOK?"bibleBook":"bibleTrail").append("\",\"screenSpace\":true,\"entityId\":").append(a.entityId).append(",\"inventoryId\":").append(a.entityId==BOOK?5: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(",\"vxFixed\":").append(a.vx).append(",\"vyFixed\":").append(a.vy).append(",\"face\":").append(a.face).append(",\"facingLeft\":").append(a.face<0?1:0).append(",\"step\":").append(a.step).append(",\"counter\":").append(a.counter).append(",\"radius\":").append(a.radius).append(",\"angle\":").append(a.angle).append(",\"tilt\":").append(a.tilt).append(",\"tiltSpeed\":").append(a.tiltSpeed).append(",\"motionMode\":").append(a.motionMode).append(",\"gray\":").append(a.gray).append(",\"priority\":").append(a.priority).append(",\"keepAliveOffCamera\":").append(a.keepAlive).append(",\"hitboxState\":").append(a.hitboxState).append(",\"hitboxWidth\":").append(a.hitboxWidth).append(",\"hitboxHeight\":").append(a.hitboxHeight).append(",\"damageRoll\":").append(a.damageRoll).append(",\"hitCount\":").append(a.hitCount).append(",\"hitTargetCount\":").append(a.targetCount).append(",\"active\":").append(a.active).append(",\"alive\":").append(a.active).append(",\"orbitSeed\":0,\"orbitSeedPolicy\":\"authored fresh-zero FIX_UB-style stabilization\",\"destroyedReason\":\"").append(a.destroyedReason).append("\",\"primitives\":[");if(a.primitive!=null){Primitive p=a.primitive;out.append("{\"index\":0,\"quad\":");array(out,p.quad);out.append(",\"uv\":");array(out,p.uv);out.append(",\"rgb\":");array(out,p.rgb);out.append(",\"drawMode\":").append(p.drawMode).append(",\"palette\":").append(p.palette).append(",\"tpage\":").append(p.tpage).append(",\"priority\":").append(p.priority).append(",\"priorityRelativeToPlayer\":").append(p.priority-PLAYER_PRIORITY).append(",\"visible\":").append(a.active&&(p.drawMode&8)==0).append('}');}out.append("]}");}return out.append(']').toString();}
    private static void array(StringBuilder out,int[] a){out.append('[');for(int i=0;i<a.length;i++){if(i>0)out.append(',');out.append(a[i]);}out.append(']');}
    public String getStatistics(){int jobs=0;for(Pending p:pending)if(p!=null)jobs++;return "{\"randomCalls\":"+randomCalls+",\"trailBirths\":"+trailBirths+",\"pendingFactories\":"+jobs+",\"allocationFailures\":"+allocationFailures+",\"factoryFailures\":"+factoryFailures+",\"primitiveUsage\":"+pool.usedPrimitives()+",\"primitiveCapacity\":"+pool.primitiveCapacity+",\"peakPrimitiveUsage\":"+peakPrimitiveUsage+",\"soundEvents\":"+soundEvents+",\"playerTimerRequests\":"+playerTimerRequests+",\"orbitSeed\":0,\"orbitSeedPolicy\":\"authored fresh-zero FIX_UB-style stabilization\"}";}
    Actor actor(int i){return actors[i];}int allocationFailures(){return allocationFailures;}int randomCalls(){return randomCalls;}int randomState(){return randomState;}void seedRandom(int seed){randomState=seed;randomCalls=0;}void setPrimitiveCapacity(int capacity){pool.setPrimitiveCapacity(capacity);}int peakPrimitiveUsage(){return peakPrimitiveUsage;}int trailBirths(){return trailBirths;}
}
