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

/** Selected US Stopwatch from pinned src/dra/8A0A4.c and BP4/14 from
 * 7E4BC.c. Decoded definition, trigonometry and factory/profile arrays are
 * registered, never executed. Callback phase/motion/geometry and flag gate
 * are ported; next-tick factories, shared256 primitive budget, world-coordinate
 * player/camera following and sound counters are explicit local bridges.
 * In particular original post-callback player displacement and compositor
 * camera/shake primitive rewrites are not simulated by these world coordinates.
 * Stopwatch advances before local stage callbacks so start/end freeze applies
 * in that same call. Other existing projectile family ordering is unchanged.
 */
public final class StopwatchWorld {
    static final int ONE=65536,CLOCK=41,RING=42,PARTICLES=5,PLAYER_PRIORITY=148;
    interface RandomSource {int next();}
    static final class Primitive {final int[] quad=new int[8],uv=new int[8];final int[] rgb;int drawMode,palette,tpage,priority;Primitive(int count){rgb=new int[count];}}
    static final class Tile {int xFixed,yFixed,vy,delay,width=1,height=1,primitiveType=17,drawMode=51;boolean visible=true;}
    static final class Actor {
        final int entityId,slot,serial,sequence;int x,y,face,step,countdown,phaseTime,angle,scale,height,bounceVy,kick,twoDigits,size,timer,remaining,flags;
        boolean active=true,allocated;String destroyedReason="";Primitive[] primitives=new Primitive[0];Tile[] tiles=new Tile[0];
        Actor(int id,int index,int serial,int sequence,int x,int y,int face){entityId=id;slot=index+32;this.serial=serial;this.sequence=sequence;this.x=x;this.y=y;this.face=face;}
    }
    static final class Pending {final int id,x,y,face,sequence,created;Pending(int id,Actor a,int tick){this.id=id;x=a.x;y=a.y;face=a.face;sequence=a.sequence;created=tick;}}
    private final DaggerWorld.SharedPool pool;private final RandomSource randomSource;private final Actor[] actors=new Actor[32];private final Pending[] pending=new Pending[4];
    private int[] fields,quarter,blueprints,particleFields;private int serial,frameCounter,randomState=1,randomCalls,allocationFailures,factoryFailures,ringBirths,particleBirths,alertSounds,tickSounds,shrinkSounds,peakPrimitiveUsage;
    private boolean timeStopped,cutscenePause;
    public StopwatchWorld(){this(new DaggerWorld.SharedPool(),null);}
    StopwatchWorld(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[] factories,int[] particleProfile){
        if(definition==null||definition.length!=13||sineQuarter==null||sineQuarter.length!=1025||factories==null||factories.length!=18||particleProfile==null||particleProfile.length!=10)return false;
        int[] row={0,20,4096,1,50,4,184,71,2,1,0,0,0},bp={41,1,193,0,2,0,42,1,193,0,0,0,5,1,193,0,0,0},profile={20,127,127,192,1,1,-2,51,12,209715200};
        for(int i=0;i<13;i++)if(definition[i]!=row[i])return false;for(int i=0;i<18;i++)if(factories[i]!=bp[i])return false;for(int i=0;i<10;i++)if(particleProfile[i]!=profile[i])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;
        fields=definition.clone();quarter=sineQuarter.clone();blueprints=factories.clone();particleFields=particleProfile.clone();reset();return true;
    }
    public boolean registered(){return fields!=null;}public int heartCost(){return fields==null?0:fields[1];}public boolean timeStopped(){return timeStopped;}public boolean cutscenePause(){return cutscenePause;}public void setCutscenePause(boolean pause){cutscenePause=pause;}
    public int activeCount(){int count=0;for(int i=0;i<16;i++)if(actors[i]!=null&&actors[i].active&&actors[i].entityId==CLOCK)count++;return count;}
    public int refusal(){return !registered()?1:cutscenePause?4:activeCount()>=fields[3]||pool.freeSlot()<0?2:0;}
    public boolean spawn(int xFixed,int originYFixed,int face,int sequence){if(refusal()!=0||face!=1&&face!=-1||sequence<1||xFixed<Integer.MIN_VALUE+64*ONE||xFixed>Integer.MAX_VALUE-64*ONE||originYFixed<Integer.MIN_VALUE+64*ONE||originYFixed>Integer.MAX_VALUE-64*ONE)return false;create(CLOCK,pool.freeSlot(),xFixed,originYFixed,face,sequence);return true;}
    private Actor create(int id,int slot,int x,int y,int face,int sequence){Actor a=new Actor(id,slot,++serial,sequence,x,y,face);pool.reserve(slot,a);actors[slot]=a;if(id==RING)ringBirths++;if(id==PARTICLES)particleBirths++;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=ringBirths=particleBirths=alertSounds=tickSounds=shrinkSounds=peakPrimitiveUsage=0;randomState=1;timeStopped=cutscenePause=false;}
    private int random(){randomCalls++;if(randomSource!=null)return randomSource.next();randomState=randomState*1103515245+12345;return(randomState>>>16)&32767;}
    private boolean allocate(Actor a,int cost){if(!pool.allocate(a,cost)){allocationFailures++;destroy(a,"primitive-allocation-failed");return false;}a.allocated=true;peakPrimitiveUsage=Math.max(peakPrimitiveUsage,pool.usedPrimitives());return true;}
    private void destroy(Actor a,String reason){a.active=a.allocated=false;a.destroyedReason=reason;for(Primitive p:a.primitives)p.drawMode|=8;for(Tile t:a.tiles)t.visible=false;pool.release(a);}
    private void queue(int id,Actor a){for(int i=0;i<pending.length;i++)if(pending[i]==null){pending[i]=new Pending(id,a,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;}create(p.id,slot,p.x,p.y,p.face,p.sequence);}}
    public void update(int playerX,int playerOriginY,int playerFace){beginFrame();for(int i=0;i<32;i++)updateSlot(i,playerX,playerOriginY,playerFace);}
    void updateSlot(int index,int playerX,int playerY,int playerFace){if(!registered())return;Actor a=actors[index];if(a==null||!a.active)return;if(a.entityId==CLOCK)clock(a,playerX,playerY,playerFace);else if(a.entityId==RING)ring(a);else particles(a);}
    private void clock(Actor a,int playerX,int playerY,int playerFace){
        if(cutscenePause){timeStopped=false;if(a.step>0&&a.step<4)a.step=4;}
        switch(a.step){
        case 0:if(cutscenePause){destroy(a,"cutscene-inhibited");return;}if(!allocate(a,3))return;a.flags=0x0C860000;a.primitives=new Primitive[]{new Primitive(3),new Primitive(3),new Primitive(3)};for(int i=0;i<3;i++){Primitive p=a.primitives[i];p.tpage=i==0?30:26;p.palette=i==0?383:390;p.priority=PLAYER_PRIORITY+(i==0?1:3);p.drawMode=778;}Primitive body=a.primitives[0];body.uv[1]=body.uv[3]=192;body.uv[5]=body.uv[7]=216;queue(RING,a);alertSounds++;timeStopped=true;a.step=1;break;
        case 1:a.primitives[0].drawMode&=~8;a.height+=3*ONE/2;if(a.height>=26*ONE)a.step=2;break;
        case 2:a.height-=ONE;if(a.height<=16*ONE){a.countdown=5;tickSounds++;a.step=3;}break;
        case 3:a.phaseTime=(short)(a.phaseTime+1);if(a.phaseTime>80){tickSounds++;a.phaseTime=0;a.kick=1;a.countdown=(short)(a.countdown-1);if(a.countdown<0){a.step=4;break;}}if(a.countdown<5){Primitive p=a.primitives[1];if(a.countdown>=10){a.twoDigits=1;digitUv(p,a.countdown/10);p=a.primitives[2];}else a.twoDigits=0;digitUv(p,a.countdown%10);}break;
        case 4:a.flags&=~0x40000;a.primitives[0].priority=194;a.primitives[0].drawMode&=~512;a.primitives[1].drawMode|=8;a.primitives[2].drawMode|=8;a.x=writeHigh(a.x,(a.x>>16)+(a.face<0?6:-6));a.y=writeHigh(a.y,(a.y>>16)-(a.height>>16));a.countdown=0;a.step=5;break;
        case 5:a.countdown=(short)(a.countdown+1);if(a.countdown>=4){Primitive p=a.primitives[0];p.palette=351;p.drawMode|=4;p.rgb[0]=p.rgb[1]=64;p.rgb[2]=96;shrinkSounds++;a.step=6;}break;
        case 6:a.countdown=(short)(a.countdown+1);if(a.countdown>14){queue(PARTICLES,a);a.step=7;}break;
        case 7:timeStopped=false;destroy(a,"stopwatch-complete");return;
        default:throw new IllegalStateException("Invalid Stopwatch phase");}
        Primitive p=a.primitives[0];if(a.step<5){int targetX=playerX+(playerFace<0?8*ONE:-8*ONE),targetY=playerY-16*ONE;a.x+=(targetX-a.x)/12;a.y+=(targetY-a.y)/4;if(playerFace!=a.face&&(Math.abs(targetX-a.x)<ONE||a.face>0&&a.x>targetX||a.face<0&&a.x<targetX))a.face=playerFace;p.uv[0]=p.uv[4]=a.face<0?152:168;p.uv[2]=p.uv[6]=a.face<0?168:152;}
        int cx=(a.x>>16)+(a.face<0?6:-6),cy=(a.y>>16)-(a.height>>16);
        if(a.step<3){if(a.scale<100)a.scale=(short)(a.scale+4);if(a.angle<4096)a.angle=(short)(a.angle+128);rotated(p,cx,cy,a.scale*8/100,a.scale*12/100,a.angle);}
        else if(a.step<5){if(a.height<=16*ONE){a.bounceVy=((random()%(a.kick!=0?64:128))+(a.kick!=0?512:256))<<8;a.kick=0;a.angle=(short)(a.angle>=0?-((random()%64)+64):(random()%64)+64);}a.height+=a.bounceVy;a.bounceVy-=ONE/4;cy=(a.y>>16)-(a.height>>16);rotated(p,cx,cy,8,12,a.angle);}
        else rect(p,a.x>>16,a.y>>16,Math.max(1,8-a.countdown),Math.min(128,a.countdown*32+12));
        if(a.step<4)digits(a);
    }
    private static int writeHigh(int fixed,int high){return((short)high<<16)|(fixed&65535);}
    private static void digitUv(Primitive p,int digit){int x=digit*8;if(x==0)x=80;p.uv[0]=p.uv[4]=x+24;p.uv[2]=p.uv[6]=x+30;p.uv[1]=p.uv[3]=64;p.uv[5]=p.uv[7]=73;p.drawMode&=~8;}
    private void digits(Actor a){int y=(a.y>>16)-14,t=a.phaseTime;if(a.twoDigits!=0){int x=(a.x>>16)-(a.face<0?10:-4),w=4,h=0;if(t<8){w=t/2;h=7-t;}else if(t>=68){w=Math.max(0,(80-(t+4))/2);h=Math.min(8,t-68);}rect(a.primitives[1],x,y,w,8+h);x=(a.x>>16)-(a.face<0?4:-10);w=4;h=0;if(t<12){w=Math.max(0,(t-4)/2);h=Math.max(0,7-(t-4));}else if(t>=72){w=(80-t)/2;h=t-72;}rect(a.primitives[2],x,y,w,8+h);return;}int x=(a.x>>16)-(a.face<0?4:-4),w=5,h=0;if(t<8){w=t/2;h=7-t;}else if(t>=72){w=(80-t)/2;h=t-72;}rect(a.primitives[1],x,y,w,8+h);}
    private int sin(int angle){return CrossMath.sine(quarter,angle);}private int cos(int angle){return sin(angle+1024);}
    private void rotated(Primitive p,int cx,int cy,int w,int h,int angle){int s=sin(angle),c=cos(angle);for(int i=0;i<4;i++){int dx=(i&1)==0?-w:w,dy=i<2?-h:h;p.quad[i*2]=(short)(cx+((c*dx-s*dy)>>12));p.quad[i*2+1]=(short)(cy+((s*dx+c*dy)>>12));}}
    private static void rect(Primitive p,int x,int y,int w,int h){p.quad[0]=p.quad[4]=(short)(x-w);p.quad[2]=p.quad[6]=(short)(x+w);p.quad[1]=p.quad[3]=(short)(y-h);p.quad[5]=p.quad[7]=(short)(y+h);}
    private void ring(Actor a){if(a.step==0){if(!allocate(a,16))return;a.flags=0x0C820000;a.primitives=new Primitive[16];for(int i=0;i<16;i++){Primitive p=a.primitives[i]=new Primitive(12);p.drawMode=1077;p.palette=351;p.tpage=26;p.priority=194;for(int v=0;v<4;v++){p.rgb[v*3]=p.rgb[v*3+1]=v<2?64:0;p.rgb[v*3+2]=v<2?64:32;}p.uv[0]=((sin(i*256)<<5)>>12)+32;p.uv[1]=-((cos(i*256)<<5)>>12)+223;p.uv[2]=((sin((i+1)*256)<<5)>>12)+32;p.uv[3]=-((cos((i+1)*256)<<5)>>12)+223;p.uv[4]=p.uv[6]=32;p.uv[5]=p.uv[7]=224;}a.size=32;a.step=1;}else{a.size=(short)(a.size+24);a.timer=(short)(a.timer+1);if(a.timer>30){destroy(a,"ring-expanded");return;}}for(int i=0;i<16;i++){Primitive p=a.primitives[i];for(int v=0;v<4;v++){int angle=(i+(v&1))*256,r=v<2?a.size:a.size-32;p.quad[v*2]=(short)((a.x>>16)+((sin(angle)*r)>>12));p.quad[v*2+1]=(short)((a.y>>16)-((cos(angle)*r)>>12));}}}
    private void particles(Actor a){if(a.step==0){if(!allocate(a,21))return;a.flags=particleFields[9];a.tiles=new Tile[21];for(int i=0;i<21;i++){Tile t=a.tiles[i]=new Tile();t.xFixed=(short)(a.x>>16)*ONE;t.yFixed=(short)(a.y>>16)*ONE;t.delay=63;if(i==20){t.primitiveType=1;t.width=0;t.vy=0;}else{int rank=i%10;t.vy=(rank*rank*0x1800+8192)*(i<10?-1:1);}}a.remaining=63;a.step=1;return;}if(--a.remaining==0){destroy(a,"particle-lifetime-ended");return;}--a.remaining;for(int i=0;i<20;i++){Tile t=a.tiles[i];t.yFixed=writeHigh(t.yFixed,t.yFixed>>16);t.yFixed+=t.vy;if(--t.delay<0)t.visible=false;}}
    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==CLOCK?"stopwatchClock":a.entityId==RING?"stopwatchRing":"stopwatchParticles").append("\",\"screenSpace\":false,\"entityId\":").append(a.entityId).append(",\"inventoryId\":").append(a.entityId==CLOCK?6:0).append(",\"params\":").append(a.entityId==PARTICLES?3584: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(",\"facingLeft\":").append(a.face<0?1:0).append(",\"step\":").append(a.step).append(",\"countdown\":").append(a.countdown).append(",\"phaseTime\":").append(a.phaseTime).append(",\"heightFixed\":").append(a.height).append(",\"bounceVyFixed\":").append(a.bounceVy).append(",\"angle\":").append(a.angle).append(",\"scale\":").append(a.scale).append(",\"kick\":").append(a.kick).append(",\"twoDigits\":").append(a.twoDigits).append(",\"size\":").append(a.size).append(",\"timer\":").append(a.timer).append(",\"remaining\":").append(a.remaining).append(",\"flags\":").append(a.flags).append(",\"active\":").append(a.active).append(",\"alive\":").append(a.active).append(",\"destroyedReason\":\"").append(a.destroyedReason).append("\",\"primitives\":[");for(int i=0;i<a.primitives.length;i++){if(i>0)out.append(',');Primitive p=a.primitives[i];out.append("{\"index\":").append(i).append(",\"primitiveType\":4,\"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("],\"tiles\":[");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(",\"x\":").append(t.xFixed>>16).append(",\"y\":").append(t.yFixed>>16).append(",\"xFixed\":").append(t.xFixed).append(",\"yFixed\":").append(t.yFixed).append(",\"vyFixed\":").append(t.vy).append(",\"delay\":").append(t.delay).append(",\"width\":").append(t.width).append(",\"height\":").append(t.height).append(",\"primitiveType\":").append(t.primitiveType).append(",\"drawMode\":").append(t.drawMode).append(",\"priority\":146,\"rgb\":[127,127,192],\"visible\":").append(a.active&&t.visible).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 "{\"timeStopped\":"+timeStopped+",\"cutscenePause\":"+cutscenePause+",\"randomCalls\":"+randomCalls+",\"ringBirths\":"+ringBirths+",\"particleBirths\":"+particleBirths+",\"pendingFactories\":"+jobs+",\"allocationFailures\":"+allocationFailures+",\"factoryFailures\":"+factoryFailures+",\"primitiveUsage\":"+pool.usedPrimitives()+",\"primitiveCapacity\":"+pool.primitiveCapacity+",\"peakPrimitiveUsage\":"+peakPrimitiveUsage+",\"alertSounds\":"+alertSounds+",\"tickSounds\":"+tickSounds+",\"shrinkSounds\":"+shrinkSounds+",\"coordinatePolicy\":\"selected world-coordinate follow; full post-callback player/camera primitive rewrite pending\"}";}
    Actor actor(int index){return actors[index];}int randomCalls(){return randomCalls;}int allocationFailures(){return allocationFailures;}int peakPrimitiveUsage(){return peakPrimitiveUsage;}void seedRandom(int seed){randomState=seed;randomCalls=0;}
}
