package io.sqsmap.sdk;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;

/** Package-private Android-independent delivery gate used by SqsMapView. */
final class ViewFrameGate {
    static final int MAX_FRAME_BYTES=16*1024*1024;
    static final int MAX_NOTICES=32,MAX_NOTICE_BYTES=256*1024,MAX_TOTAL_NOTICE_BYTES=4*1024*1024;
    enum Kind {EVENTS,SCRIPT,ERROR}
    static final class Frame {
        final long epoch;final int width,height;final byte[] rgba;final boolean srgb;
        Frame(long epoch,int width,int height,byte[] rgba){this.epoch=epoch;this.width=width;this.height=height;this.rgba=rgba;this.srgb=false;}
        Frame(long epoch,int width,int height,byte[] rgba,boolean srgb){this.epoch=epoch;this.width=width;this.height=height;this.rgba=rgba;this.srgb=srgb;}
    }
    static final class Notice {
        final long epoch;final Kind kind;final String text;final Throwable error;final int bytes;
        Notice(long epoch,Kind kind,String text,Throwable error){this.epoch=epoch;this.kind=kind;this.text=text;this.error=error;this.bytes=text==null?4096:text.length()*2;}
    }
    static final class Batch {
        final long epoch;final Frame frame;final List<Notice> notices;
        Batch(long epoch,Frame frame,List<Notice> notices){this.epoch=epoch;this.frame=frame;this.notices=notices;}
    }
    private long epoch;private boolean active,posted;private Frame latest;
    private final ArrayDeque<Notice> notices=new ArrayDeque<>();private int bytes;
    synchronized long begin(){epoch++;active=true;latest=null;notices.clear();bytes=0;return epoch;}
    synchronized long stop(){epoch++;active=false;latest=null;notices.clear();bytes=0;return epoch;}
    synchronized long beginIfSame(long value){return epoch==value?begin():-1;}
    synchronized boolean current(long value){return active&&epoch==value;}
    synchronized boolean sameEpoch(long value){return epoch==value;}
    synchronized boolean offerFrame(long value,int width,int height,byte[] rgba){
        return offerFrame(value,width,height,rgba,false);
    }
    synchronized boolean offerFrame(long value,int width,int height,byte[] rgba,boolean srgb){
        if(!current(value))return false;validateFrame(width,height,rgba);
        // JNI hands the listener an owned byte array; no other SDK callback receives it.
        latest=new Frame(value,width,height,rgba,srgb);return claimPost();
    }
    synchronized boolean offerNotice(long value,Kind kind,String text,Throwable error){
        if(!current(value))return false;Notice notice=new Notice(value,kind,text,error);
        if(notice.bytes>MAX_NOTICE_BYTES||notices.size()>=MAX_NOTICES||bytes+notice.bytes>MAX_TOTAL_NOTICE_BYTES)throw new IllegalStateException("RESOURCE_LIMIT: Android View notification queue exceeded");
        notices.add(notice);bytes+=notice.bytes;return claimPost();
    }
    private boolean claimPost(){if(posted)return false;posted=true;return true;}
    synchronized Batch drain(){posted=false;Frame frame=latest;latest=null;List<Notice> values=new ArrayList<>(notices);notices.clear();bytes=0;return new Batch(epoch,frame,values);}
    static void validateFrame(int width,int height,byte[] rgba){
        long length=(long)width*height*4;
        if(width<=0||height<=0||length>MAX_FRAME_BYTES||rgba==null||rgba.length!=length)throw new IllegalArgumentException("RESOURCE_LIMIT: invalid or oversized Android View RGBA frame");
    }
    static int logicalSize(int physical,double density){
        if(physical<=0||!Double.isFinite(density)||density<0.25||density>8)throw new IllegalArgumentException("INVALID_ARGUMENT: positive View size and density 0.25..8 are required");
        long logical=Math.max(1,Math.round(physical/density));if(logical>8192)throw new IllegalArgumentException("RESOURCE_LIMIT: View logical dimension exceeds 8192");return (int)logical;
    }
    static void validateViewport(int width,int height,double density){
        int logicalWidth=logicalSize(width,density),logicalHeight=logicalSize(height,density);
        long physicalWidth=Math.round(logicalWidth*density),physicalHeight=Math.round(logicalHeight*density);
        if(physicalWidth<=0||physicalHeight<=0||physicalWidth>4096||physicalHeight>4096||physicalWidth*physicalHeight*4>MAX_FRAME_BYTES)throw new IllegalArgumentException("RESOURCE_LIMIT: Android View framebuffer exceeds 4096 edge or 16 MiB");
    }
    // 64 KiB lookup avoids per-pixel pow on the UI thread; construction is finite and once-only.
    private static final byte[] SRGB_STRAIGHT=createSrgbLookup();
    private static byte[] createSrgbLookup(){
        byte[] table=new byte[256*256];double[] decoded=new double[256];
        for(int c=0;c<256;c++){double v=c/255.0;decoded[c]=v<=0.04045?v/12.92:Math.pow((v+0.055)/1.055,2.4);}
        for(int a=1;a<256;a++)for(int c=0;c<256;c++){
            double linear=Math.min(1,decoded[c]*255/a);
            table[(a<<8)|c]=(byte)Math.round(255*(linear<=0.0031308?12.92*linear:1.055*Math.pow(linear,1/2.4)-0.055));
        }return table;
    }
    private static int straight(int component,int alpha,boolean srgb){
        return srgb?SRGB_STRAIGHT[(alpha<<8)|component]&255:Math.min(255,(component*255+alpha/2)/alpha);
    }
    /** Bitmap.setPixels consumes straight ARGB; native transparent render targets are premultiplied RGBA. */
    static int[] toArgb(Frame frame,int[] reuse){
        int pixels=frame.width*frame.height;int[] result=reuse!=null&&reuse.length==pixels?reuse:new int[pixels];
        for(int i=0,j=0;i<pixels;i++,j+=4){int r=frame.rgba[j]&255,g=frame.rgba[j+1]&255,b=frame.rgba[j+2]&255,a=frame.rgba[j+3]&255;
            if(a==0){r=0;g=0;b=0;}else if(a<255){r=straight(r,a,frame.srgb);g=straight(g,a,frame.srgb);b=straight(b,a,frame.srgb);}
            result[i]=(a<<24)|(r<<16)|(g<<8)|b;
        }
        return result;
    }
}
