package io.sqsmap.sdk;

import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.os.Handler;
import android.os.Looper;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;

/** API24 map display only. Applications supply their own tools, layers and attribution UI. */
public final class SqsMapView extends View {
    public interface Listener {
        /** Called on the main thread for each fresh host. Reinitialize application-owned map data here. */
        default void onReady(SqsMapView view,NativeMapHost host){}
        default void onEvents(SqsMapView view,String json){}
        default void onScriptState(SqsMapView view,String json){}
        default void onError(SqsMapView view,Throwable error){}
    }
    /** Each generation needs its own transport, which is closed with its host. Null uses the default. */
    public interface TransportFactory {NativeMapHost.Transport create();}
    private final Handler ui=new Handler(Looper.getMainLooper());
    private final ViewFrameGate gate=new ViewFrameGate();
    private final Paint paint=new Paint(Paint.FILTER_BITMAP_FLAG);
    private final Rect destination=new Rect();
    private final AtomicLong faultedEpoch=new AtomicLong(-1);
    private Listener listener=new Listener(){};private TransportFactory transportFactory;
    private NativeMapHost host;private AndroidTouchInput touch;private AtomicLong sinkEpoch;
    private boolean attached,paused,destroyed,opening,closing,failed,resizing,readyDelivered;
    private int logicalWidth,logicalHeight;private double density;
    private Bitmap bitmap;private int[] pixels;
    private final CompletableFuture<Void> destroyedFuture=new CompletableFuture<>();
    private final Runnable drainRunnable=this::drainUi;

    public SqsMapView(Context context){this(context,null);}
    public SqsMapView(Context context,AttributeSet attributes){this(context,attributes,0);}
    public SqsMapView(Context context,AttributeSet attributes,int style){super(context,attributes,style);setWillNotDraw(false);}
    private static void mainThread(){if(Looper.myLooper()!=Looper.getMainLooper())throw new IllegalStateException("SqsMapView operations require the Android main thread");}
    public void setListener(Listener value){mainThread();listener=value==null?new Listener(){}:value;}
    public void setTransportFactory(TransportFactory value){mainThread();if(host!=null||opening||closing)throw new IllegalStateException("Set transport factory before attachment or after pause finishes");transportFactory=value;}
    public boolean isReady(){mainThread();return host!=null&&desired()&&!failed&&!resizing;}
    public NativeMapHost getHost(){mainThread();return isReady()?host:null;}
    public boolean isCurrent(NativeMapHost value){mainThread();return value!=null&&value==host&&isReady();}
    /** Stops and releases the current host, resources and scripts. State is not retained across pause. */
    public void pause(){mainThread();if(destroyed)return;paused=true;stopCurrent();}
    /** Recreates after layout/attachment and prior release; onReady runs again for the fresh host. */
    public void resume(){mainThread();if(destroyed)throw new IllegalStateException("SqsMapView destroyed");paused=false;failed=false;reconcile();}
    public CompletableFuture<Void> destroyAsync(){mainThread();if(!destroyed){destroyed=true;stopCurrent();finishDestroy();}return destroyedFuture;}
    public void destroy(){destroyAsync();}
    /** Convenience initialization. Outside onReady, the host may be unavailable; never block the UI on this Future. */
    public CompletableFuture<String> runScript(String module){mainThread();if(!isReady()){CompletableFuture<String> result=new CompletableFuture<>();result.completeExceptionally(new IllegalStateException("CANCELLED: map View is not ready"));return result;}return host.runScript(module);}
    @Override protected void onAttachedToWindow(){super.onAttachedToWindow();attached=true;reconcile();}
    @Override protected void onDetachedFromWindow(){attached=false;stopCurrent();super.onDetachedFromWindow();}
    @Override protected void onSizeChanged(int width,int height,int oldWidth,int oldHeight){super.onSizeChanged(width,height,oldWidth,oldHeight);reconcile();}
    @Override protected void onConfigurationChanged(Configuration configuration){super.onConfigurationChanged(configuration);reconcile();}
    private boolean desired(){return attached&&!paused&&!destroyed&&getWidth()>0&&getHeight()>0;}
    private boolean accepts(long epoch){return gate.current(epoch)&&faultedEpoch.get()!=epoch;}
    private void reconcile(){
        mainThread();if(!desired()||failed){if(host!=null)stopCurrent();return;}if(opening||closing)return;
        double nextDensity=getResources().getDisplayMetrics().density;
        int nextWidth,nextHeight;
        try{ViewFrameGate.validateViewport(getWidth(),getHeight(),nextDensity);nextWidth=ViewFrameGate.logicalSize(getWidth(),nextDensity);nextHeight=ViewFrameGate.logicalSize(getHeight(),nextDensity);}catch(Throwable error){failed=true;stopCurrent();report(error);return;}
        if(host!=null){
            if(resizing||nextWidth==logicalWidth&&nextHeight==logicalHeight&&nextDensity==density)return;
            // Do not accept an old-sized framebuffer while a resize is in flight.
            resizing=true;long resizeEpoch=gate.stop();clearBitmap();final NativeMapHost current=host;final AtomicLong currentSink=sinkEpoch;
            current.resize(nextWidth,nextHeight,nextDensity).whenComplete((ignored,error)->{
                long updated=error==null?gate.beginIfSame(resizeEpoch):-1;if(updated>=0)currentSink.set(updated);
                ui.post(()->{
                if(host!=current||!desired()){resizing=false;return;}
                resizing=false;if(error!=null){failed=true;stopCurrent();report(error);return;}
                logicalWidth=nextWidth;logicalHeight=nextHeight;density=nextDensity;
                reconcile();deliverReady();
            });});return;
        }
        logicalWidth=nextWidth;logicalHeight=nextHeight;density=nextDensity;
        final long epoch=gate.begin();final AtomicLong ownedEpoch=new AtomicLong(epoch);sinkEpoch=ownedEpoch;faultedEpoch.set(-1);opening=true;readyDelivered=false;
        final AtomicReference<NativeMapHost> owned=new AtomicReference<>();
        NativeMapHost.Listener sink=new NativeMapHost.Listener(){
            private void notice(ViewFrameGate.Kind kind,String text,Throwable error){long value=ownedEpoch.get();if(!accepts(value))return;try{if(gate.offerNotice(value,kind,text,error))ui.post(drainRunnable);}catch(Throwable failure){failFromOwner(value,failure,owned);}}
            public void onFrame(int width,int height,byte[] rgba){onFrame(width,height,rgba,NativeMapHost.FrameFormat.SRGB_PREMULTIPLIED_RGBA);}
            public void onFrame(int width,int height,byte[] rgba,NativeMapHost.FrameFormat format){long value=ownedEpoch.get();if(!accepts(value))return;try{if(gate.offerFrame(value,width,height,rgba,format==NativeMapHost.FrameFormat.SRGB_PREMULTIPLIED_RGBA))ui.post(drainRunnable);}catch(Throwable error){failFromOwner(value,error,owned);}}
            public void onEvents(String json){notice(ViewFrameGate.Kind.EVENTS,json,null);}
            public void onScriptState(String json){notice(ViewFrameGate.Kind.SCRIPT,json,null);}
            public void onError(Throwable error){notice(ViewFrameGate.Kind.ERROR,null,error);}
        };
        NativeMapHost.Transport transport=null;
        try{if(transportFactory!=null)transport=transportFactory.create();
            NativeMapHost.open(nextWidth,nextHeight,nextDensity,sink,transport).whenComplete((created,error)->{
                if(created!=null){owned.set(created);if(!gate.sameEpoch(epoch))created.closeAsync();}
                ui.post(()->{
                    opening=false;
                    if(error!=null){if(gate.sameEpoch(epoch)&&desired()){failed=true;gate.stop();report(error);}finishDestroy();reconcile();return;}
                    if(!gate.sameEpoch(epoch)||!desired()||failed||faultedEpoch.get()==epoch){closeHost(created);return;}
                    host=created;touch=new AndroidTouchInput(created);setOnTouchListener(touch);
                    reconcile();deliverReady();
                });
            });
        }catch(Throwable error){opening=false;failed=true;gate.stop();if(transport!=null)try{transport.close();}catch(Throwable ignored){}report(error);finishDestroy();}
    }
    private void deliverReady(){if(!readyDelivered&&isReady()){readyDelivered=true;NativeMapHost value=host;safe(()->listener.onReady(this,value));}}
    private void failFromOwner(long epoch,Throwable error,AtomicReference<NativeMapHost> owned){
        if(!gate.current(epoch)||!faultedEpoch.compareAndSet(-1,epoch))return;
        NativeMapHost value=owned.get();if(value!=null)value.closeAsync();
        ui.post(()->{if(!gate.sameEpoch(epoch)||destroyed)return;failed=true;stopCurrent();report(error);});
    }
    private void stopCurrent(){
        mainThread();gate.stop();faultedEpoch.set(-1);resizing=false;readyDelivered=false;
        if(touch!=null){setOnTouchListener(null);touch.close();touch=null;}
        clearBitmap();NativeMapHost previous=host;host=null;if(previous!=null)closeHost(previous);else finishDestroy();
    }
    private void closeHost(NativeMapHost previous){
        closing=true;previous.closeAsync().whenComplete((ignored,error)->ui.post(()->{
            closing=false;if(error!=null&&!destroyed)report(error);if(error!=null&&destroyed&&!destroyedFuture.isDone())destroyedFuture.completeExceptionally(error);
            finishDestroy();reconcile();
        }));
    }
    private void finishDestroy(){if(destroyed&&!opening&&!closing){if(!destroyedFuture.isDone())destroyedFuture.complete(null);}}
    private void drainUi(){
        mainThread();ViewFrameGate.Batch batch=gate.drain();if(!accepts(batch.epoch)||!desired()||failed)return;
        for(ViewFrameGate.Notice notice:batch.notices){if(!accepts(notice.epoch)||!desired())return;
            if(notice.kind==ViewFrameGate.Kind.EVENTS)safe(()->listener.onEvents(this,notice.text));
            else if(notice.kind==ViewFrameGate.Kind.SCRIPT)safe(()->listener.onScriptState(this,notice.text));
            else report(notice.error);
        }
        if(batch.frame==null||!accepts(batch.frame.epoch)||!desired())return;
        try{ViewFrameGate.Frame frame=batch.frame;pixels=ViewFrameGate.toArgb(frame,pixels);
            if(bitmap==null||bitmap.getWidth()!=frame.width||bitmap.getHeight()!=frame.height){if(bitmap!=null)bitmap.recycle();bitmap=Bitmap.createBitmap(frame.width,frame.height,Bitmap.Config.ARGB_8888);}
            bitmap.setPixels(pixels,0,frame.width,0,0,frame.width,frame.height);invalidate();
        }catch(Throwable error){failed=true;stopCurrent();report(error);}
    }
    @Override protected void onDraw(Canvas canvas){super.onDraw(canvas);if(bitmap!=null&&!bitmap.isRecycled()){destination.set(0,0,getWidth(),getHeight());canvas.drawBitmap(bitmap,null,destination,paint);}}
    private void clearBitmap(){if(bitmap!=null){bitmap.recycle();bitmap=null;}pixels=null;invalidate();}
    private void report(Throwable error){safe(()->listener.onError(this,error));}
    private static void safe(Runnable callback){try{callback.run();}catch(Throwable error){Log.e("SqsMapView","Application listener failed",error);}}
}
