#include <stdio.h>
#include <time.h>
#include <unistd.h>
#include <sys/stat.h>

#include "uxn.h"

#pragma GCC diagnostic push
#pragma clang diagnostic push
#pragma GCC diagnostic ignored "-Wpedantic"
#pragma clang diagnostic ignored "-Wtypedef-redefinition"
#include <SDL.h>
#include "devices/system.h"
#include "devices/console.h"
#include "devices/screen.h"
#include "devices/audio.h"
#include "devices/file.h"
#include "devices/controller.h"
#include "devices/mouse.h"
#include "devices/datetime.h"
#if defined(_WIN32) && defined(_WIN32_WINNT) && _WIN32_WINNT > 0x0602
#include <processthreadsapi.h>
#elif defined(_WIN32)
#include <windows.h>
#include <string.h>
#endif
#ifndef __plan9__
#define USED(x) (void)(x)
#endif
#pragma GCC diagnostic pop
#pragma clang diagnostic pop

/*
Copyright (c) 2021-2023 Devine Lu Linvega, Andrew Alderwick
Copyright (c) 2024 Dario Rodriguez

Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE.
*/

#define INSTANCESSIZE 256

#define PAD 2
#define PAD2 4
#define WIDTH 64 * 8
#define HEIGHT 40 * 8
#define TIMEOUT_MS 334

typedef struct video_t {
        SDL_Window *emu_window;
        SDL_Texture *emu_texture;
        SDL_Renderer *emu_renderer;
        SDL_Rect emu_viewport;
        Uint32 zoom;
        int flag_windowcreated;
        int flag_fullscreen;
        int flag_borderless;
} video_t;

typedef struct instance_t {
        void *varvara;
        video_t *video;
        Uint8 dev[0x100];
        Uxn u;
        Uxn u_audio;
        Uint8 *ram;
        char *rom;
        SDL_AudioDeviceID audio_id;
        SDL_Thread *stdin_thread;
        Uint32 stdin_event;
        Uint32 audio0_event;
        Uint64 exec_deadline;
        Uint64 deadline_interval;
        Uint64 ms_interval;
} instance_t;

typedef struct romdir_t {
        int sizename;
        char *name;
        long offset;
        long csize;
        int method;
        char *data;
} romdir_t;

typedef struct varvara_t {
        video_t video;
        int sizeinstances;
        long sizeromdata;
        char *romdata;
        int sizeromdir;
        romdir_t *romdir;
        instance_t **instances;
} varvara_t;

varvara_t *varvara_init(char *romfilename, int flag_fullscreen, int zoom);
int varvara_romdata2romdir(varvara_t *varvara);
void varvara_free(varvara_t *varvara);

char *varvara_romdata(varvara_t *varvara,char *romdirname);
int varvara_sizeromdata(varvara_t *varvara,char *romdirname);

instance_t *instance_init(varvara_t *varvara);
void instance_free(instance_t *instance);

static char *load_file(char *filename, long *loadedsize);
static int is_extension(char *filename, char *extension);

/* prototypes: devices */

static int clamp(int v, int min, int max);
static void audio_deo(instance_t *instance, int instanceno, Uint8 *d, Uint8 port, Uxn *u);
Uint8 emu_dei(Uxn *u, Uint8 addr);
void emu_deo(Uxn *u, Uint8 addr, Uint8 value);

/* prototypes: Handlers */

void audio_finished_handler(int instance, void *userptr);
static int stdin_handler(void *userptr);
static void set_window_size(video_t *video, SDL_Window *window, int w, int h);
static void set_zoom(video_t *video, Uint8 z, int win);
static void set_flag_fullscreen(video_t *video, int value, int win);
static void set_flag_borderless(video_t *video, int value);
static void set_debugger(Uxn *u, int value);

/* prototypes: emulator primitives */

int emu_resize(Uxn *u, int width, int height);
static void emu_redraw(Uxn *u);
static int emu_init(Uxn *u);
static void emu_restart(Uxn *u, char *rom, int soft);
static void capture_screen(Uxn *u);
static Uint8 get_button(SDL_Event *event);
static Uint8 get_button_joystick(SDL_Event *event);
static Uint8 get_vector_joystick(SDL_Event *event);
static Uint8 get_key(SDL_Event *event);
static int handle_events(Uxn *u);
static int emu_run(Uxn *u, char *rom);
static int emu_end(Uxn *u);

/* overall init/free */

varvara_t *
varvara_init(char *romfilename, int flag_fullscreen, int zoom)
{

        varvara_t *varvara;
        if((varvara=malloc(sizeof(varvara_t)))==NULL
          || memset(varvara,0,sizeof(varvara_t))==NULL
          || (varvara->instances=malloc(sizeof(instance_t *)*INSTANCESSIZE))==NULL
          || memset(varvara->instances,0,sizeof(instance_t *)*INSTANCESSIZE)==NULL
          || (varvara->sizeinstances=INSTANCESSIZE)!=INSTANCESSIZE
        ) {
                varvara_free(varvara),varvara=NULL;
                system_error("Init", "Failed to allocate varvara structs.");
                return(NULL);
        }
        if(is_extension(romfilename,".erom")
           && ((varvara->romdata=load_file(romfilename,&(varvara->sizeromdata)))==NULL
             || varvara_romdata2romdir(varvara)!=0)
        ) {
                varvara_free(varvara),varvara=NULL;
                system_error("Init", "Failed to load erom.");
                return(NULL);
        }
        set_zoom(&(varvara->video),zoom, 0);
        set_flag_fullscreen(&(varvara->video), flag_fullscreen, 0);
        /* Start system. */
        if((varvara->instances[0]=instance_init(varvara))==NULL) {
                varvara_free(varvara),varvara=NULL;
                system_error("Init", "Failed to initialize varvara unx instance.");
                return NULL;
        }
        varvara->instances[0]->u.userptr=(varvara->instances[0]);
        varvara->instances[0]->u_audio.userptr=(varvara->instances[0]);
        varvara->instances[0]->u.dev = (Uint8 *)&(varvara->instances[0]->dev);
        varvara->instances[0]->u_audio.dev = (Uint8 *)&(varvara->instances[0]->dev);
        if((varvara->instances[0]->ram=(Uint8 *)calloc(0x10000 * RAM_PAGES, sizeof(Uint8)))==NULL
          || (varvara->instances[0]->rom=(char *)malloc(strlen(romfilename)+1))==NULL
          || memcpy(varvara->instances[0]->rom,romfilename,strlen(romfilename)+1)==NULL
          || !system_init(&(varvara->instances[0]->u), varvara->instances[0]->ram, varvara->instances[0]->rom, varvara_romdata(varvara,"init.rom"),varvara_sizeromdata(varvara,"init.rom"))
          || !system_init(&(varvara->instances[0]->u_audio), varvara->instances[0]->ram, varvara->instances[0]->rom, varvara_romdata(varvara,"init.rom"),varvara_sizeromdata(varvara,"init.rom"))
        ) {
                varvara_free(varvara),varvara=NULL;
                system_error("Init", "Failed to initialize uxn.");
                return(NULL);
        }
        if(!emu_init(&(varvara->instances[0]->u_audio))) {
                varvara_free(varvara),varvara=NULL;
                system_error("Init", "Failed to initialize varvara.");
                return NULL;
        }
        return(varvara);
}

int
varvara_romdata2romdir(varvara_t *varvara)
{
        int i;
        unsigned char *start,*central,*end;
        int nrecords,sizeregs,offset;
        unsigned char *regsend,*ptr;
        char *strerr;
        romdir_t *romdir;
        unsigned char *header;
        int namelen,extralen;
        if(varvara==NULL || varvara->romdata==NULL)
                return(-1);
        start=((unsigned char *)varvara->romdata);
        end=start+varvara->sizeromdata;
        /* look for end-of-zip directory */
        for(i=varvara->sizeromdata-4;i>=0;i--) {
                if(start[i]==0x50 && start[i+1]==0x4b && start[i+2]==0x05 && start[i+3]==0x06)
                        break;
        }
        /* extract header */
        if((strerr="without end-of-zip directory")==NULL
          || i<0
          || (strerr="with corrupt end-of-zip directory")==NULL
          || (central=start+i+4)>=end
          || (central+15)>=end
          || (nrecords=central[6]|(central[7]<<8))<=0
          || (sizeregs=central[8]|(central[9]<<8)|(central[10]<<16)|(central[11]<<24))<4
          || (offset=central[12]|(central[13]<<8)|(central[14]<<16)|(central[15]<<24))<=0
          || (regsend=start+offset+sizeregs)>end
          || memcmp(start+offset,"\x50\x4b\x01\x02",4)!=0
          || (strerr="couldn't be processed because of insufficient memory")==NULL
          || (varvara->romdir=calloc(nrecords,sizeof(romdir_t)))==NULL
        ) {
                fprintf(stderr,"WARNING: erom %s\n",strerr);
                return(-1); /* corrupt file or insuf. mem. */
        }
        /* parse entries */
        for(i=0,ptr=start+offset+4;i<nrecords && ptr<(start+offset+sizeregs);i++) {
                romdir=varvara->romdir+i;
                if((strerr="is corrupt (too small)")==NULL
                  || (ptr+25)>=regsend
                  || (strerr="is corrupt (sizename is negative)")==NULL
                  || (romdir->sizename=(ptr[24]|(ptr[25]<<8)))<0
                  || (strerr="is corrupt (name too big)")==NULL
                  || (ptr+42+romdir->sizename)>regsend
                  || (strerr="is corrupt (name)")==NULL /* never used */
                  || (romdir->name=((char *)ptr)+42)==NULL
                  || (strerr="is corrupt (offset is negative)")==NULL
                  || (romdir->offset=ptr[38]|(ptr[39]<<8)|(ptr[40]<<16)|(ptr[41]<<24))<0
                  || (strerr="is corrupt (csize is negative)")==NULL
                  || (romdir->csize=ptr[16]|(ptr[17]<<8)|(ptr[18]<<16)|(ptr[19]<<24))<0
                  || (strerr="indicate compressed files but we require zips without compression (i.e. \"zip -0 myfile.erom init.rom\")")==NULL
                  || (romdir->method=ptr[6]|(ptr[7]<<8))!=0
                  || (strerr="is corrupt (data header)")==NULL
                  || (header=start+romdir->offset)<0
                  || (header+30)>central
                  || (namelen=header[26]|(header[27]<<8))<0
                  || (extralen=header[28]|(header[29]<<8))<0
                  || (romdir->data=((char *)start)+romdir->offset+30+namelen+extralen)>((char *)central)
                  || (romdir->data+romdir->csize)>((char *)central)
                ) {
                        fprintf(stderr,"WARNING: erom directory entry %s\n",strerr);
                        free(varvara->romdir),varvara->romdir=NULL;
                        return(-1); /* corrupt file or insuf. mem. */
                }
                /* search next entry */
                for(;ptr<(start+offset+sizeregs-4);ptr++) {
                        if(ptr[0]==0x50 && ptr[1]==0x4b && ptr[2]==0x01 && ptr[3]==0x02) {
                                ptr+=4;
                                break;
                        }
                }
        }
        if(i<nrecords) {
                fprintf(stderr,"WARNING: erom with corrupt end-of-zip directory\n");
                free(varvara->romdir),varvara->romdir=NULL;
                return(-1); /* incomplete file */
        }
        varvara->sizeromdir=i;
        return(0);
}

void
varvara_free(varvara_t *varvara)
{
        int i;
        if(varvara==NULL)
                return;
        if(varvara->instances!=NULL) {
                for(i=0;i<varvara->sizeinstances;i++) {
                        if(varvara->instances[i]==NULL)
                                continue;
                        instance_free(varvara->instances[i]),varvara->instances[i]=NULL;
                }
                free(varvara->instances),varvara->instances=NULL,varvara->sizeinstances=0;
        }
        if(varvara->romdata!=NULL)
                free(varvara->romdata),varvara->romdata=NULL,varvara->sizeromdata=0;
        free(varvara),varvara=NULL;
}

char *
varvara_romdata(varvara_t *varvara,char *romdirname)
{
        int i,l;
        if(varvara==NULL || romdirname==NULL || varvara->romdata==NULL)
                return(NULL);
        l=strlen(romdirname);
        for(i=0;i<varvara->sizeromdir;i++) {
                if(varvara->romdir[i].sizename==l && memcmp(varvara->romdir[i].name,romdirname,l)==0)
                        return(varvara->romdir[i].data);
        }
        return(NULL);
}

int
varvara_sizeromdata(varvara_t *varvara,char *romdirname)
{
        int i,l;
        if(varvara==NULL || romdirname==NULL || varvara->romdata==NULL)
                return(0);
        l=strlen(romdirname);
        for(i=0;i<varvara->sizeromdir;i++) {
                if(varvara->romdir[i].sizename==l && memcmp(varvara->romdir[i].name,romdirname,l)==0)
                        return(varvara->romdir[i].csize);
        }
        return(0);
}


instance_t *
instance_init(varvara_t *varvara)
{
        instance_t *instance;
        if((instance=malloc(sizeof(instance_t)))==NULL)
                return(NULL);
        memset(instance,0,sizeof(instance_t));
        instance->varvara=varvara;
        instance->video=&(varvara->video);
        return(instance);
}

void
instance_free(instance_t *instance)
{
        if(instance==NULL)
                return;
        if(instance->ram!=NULL)
                free(instance->ram),instance->ram=NULL;
        if(instance->rom!=NULL)
                free(instance->rom),instance->rom=NULL;
        free(instance),instance=NULL;
        return;
}

static char *
load_file(char *filename, long *loadedsize)
{
        FILE *fp;
        struct stat st;
        char *buf;
        long filesize;
        /* NOTE: we add a trailing zero byte to be able to return something if the file size is zero */
        if((fp=fopen(filename,"rb"))==NULL
          || fstat(fileno(fp),&st)!=0
          || (filesize=st.st_size)!=st.st_size
          || (buf=malloc(filesize+1))==NULL
          || fread(buf,1,filesize,fp)!=filesize
          || (buf[filesize]=0)!=0
          || fclose(fp)!=0
          || (fp=NULL)!=NULL
        ) {
                if(fp!=NULL)
                        fclose(fp),fp=NULL;
                return(NULL);
        }
        if(loadedsize!=NULL)
                *loadedsize=filesize;
        return(buf);
}

static int
is_extension(char *filename, char *extension)
{
        /* NOTE: extension MUST include the leading dot, i.e. ".txt" */
        int lfilename,lextension;
        if(filename==NULL || extension==NULL)
                return((filename==extension || extension==NULL)?1:0);
        lfilename=strlen(filename);
        lextension=strlen(extension);
        if(lfilename>=lextension)
                return((strcmp(filename+lfilename-lextension,extension)==0)?1:0);
        return(0);
}

/* devices */

static int
clamp(int v, int min, int max)
{
        return (v<min)?min
               :(v>max)?max
               :v;
}

static void
audio_deo(instance_t *instance, int instanceno, Uint8 *d, Uint8 port, Uxn *u)
{
        if(instance==NULL)
                return; /* sanity check failed */
        if(!instance->audio_id) return;
        if(port == 0xf) {
                SDL_LockAudioDevice(instance->audio_id);
                audio_start(instanceno, d, u);
                SDL_UnlockAudioDevice(instance->audio_id);
                SDL_PauseAudioDevice(instance->audio_id, 0);
        }
}

Uint8
emu_dei(Uxn *u, Uint8 addr)
{
        instance_t *instance;
        Uint8 p = addr & 0x0f, d = addr & 0xf0;
        if(u==NULL || (instance=((instance_t *)u->userptr))==NULL)
                return(0); /* sanity check failed */
        switch(d) {
        case 0x00: return system_dei(u, addr);
        case 0x20: return screen_dei(u, addr);
        case 0x30: return audio_dei(0, &u->dev[d], p);
        case 0x40: return audio_dei(1, &u->dev[d], p);
        case 0x50: return audio_dei(2, &u->dev[d], p);
        case 0x60: return audio_dei(3, &u->dev[d], p);
        case 0xc0: return datetime_dei(u, addr);
        }
        return u->dev[addr];
}

void
emu_deo(Uxn *u, Uint8 addr, Uint8 value)
{
        instance_t *instance;
        Uint8 p = addr & 0x0f, d = addr & 0xf0;
        if(u==NULL || (instance=((instance_t *)u->userptr))==NULL)
                return; /* sanity check failed */
        u->dev[addr] = value;
        switch(d) {
        case 0x00:
                system_deo(u, &u->dev[d], p);
                if(p > 0x7 && p < 0xe) screen_palette(&u->dev[0x8]);
                break;
        case 0x10: console_deo(&u->dev[d], p); break;
        case 0x20: screen_deo(u, u->ram, &u->dev[d], p); break;
        case 0x30: audio_deo(instance, 0, &u->dev[d], p, u); break;
        case 0x40: audio_deo(instance, 1, &u->dev[d], p, u); break;
        case 0x50: audio_deo(instance, 2, &u->dev[d], p, u); break;
        case 0x60: audio_deo(instance, 3, &u->dev[d], p, u); break;
        case 0xa0: file_deo(0, u->ram, &u->dev[d], p); break;
        case 0xb0: file_deo(1, u->ram, &u->dev[d], p); break;
        }
}

/* Handlers */

void
audio_finished_handler(int instanceno, void *userptr)
{
        instance_t *instance;
        SDL_Event event;
        if((instance=((instance_t *)userptr))==NULL)
                return; /* sanity check failed */
        event.type = instance->audio0_event + instanceno;
        SDL_PushEvent(&event);
}

static int
stdin_handler(void *userptr)
{
        instance_t *instance;
        SDL_Event event;
        if((instance=((instance_t *)userptr))==NULL)
                return(0); /* sanity check failed */
        USED(userptr);
        event.type = instance->stdin_event;
        while(read(0, &event.cbutton.button, 1) > 0 && SDL_PushEvent(&event) >= 0)
                ;
        return 0;
}

static void
set_window_size(video_t *video, SDL_Window *window, int w, int h)
{
        SDL_Point win_old;
        if(video==NULL || window==NULL)
                return; /* sanity check failed */
        SDL_GetWindowSize(window, &win_old.x, &win_old.y);
        if(w == win_old.x && h == win_old.y) return;
        SDL_RenderClear(video->emu_renderer);
        SDL_SetWindowSize(window, w, h);
}

static void
set_zoom(video_t *video, Uint8 z, int win)
{
        if(video==NULL)
                return; /* sanity check failed */
        if(z < 1) return;
        if(win)
                set_window_size(video, video->emu_window, (uxn_screen.width + PAD2) * z, (uxn_screen.height + PAD2) * z);
        video->zoom = z;
}

static void
set_flag_fullscreen(video_t *video, int value, int win)
{
        Uint32 flags = 0; /* windowed mode; SDL2 has no constant for this */
        if(video==NULL)
                return; /* sanity check failed */
        video->flag_fullscreen = value;
        if(video->flag_fullscreen)
                flags = SDL_WINDOW_FULLSCREEN_DESKTOP;
        if(win)
                SDL_SetWindowFullscreen(video->emu_window, flags);
}

static void
set_flag_borderless(video_t *video, int value)
{
        if(video==NULL)
                return; /* sanity check failed */
        if(video->flag_fullscreen) return;
        video->flag_borderless = value;
        SDL_SetWindowBordered(video->emu_window, !value);
}

static void
set_debugger(Uxn *u, int value)
{
        u->dev[0x0e] = value;
        screen_fill(uxn_screen.fg, 0);
        screen_redraw(u);
}

/* emulator primitives */

int
emu_resize(Uxn *u, int width, int height)
{
        video_t *video;
        if(u==NULL || u->userptr==NULL || (video=((instance_t *)u->userptr)->video)==NULL)
                return(0);
        if(!video->flag_windowcreated)
                return 0;
        if(video->emu_texture != NULL)
                SDL_DestroyTexture(video->emu_texture);
        SDL_RenderSetLogicalSize(video->emu_renderer, width + PAD2, height + PAD2);
        video->emu_texture = SDL_CreateTexture(video->emu_renderer, SDL_PIXELFORMAT_RGB888, SDL_TEXTUREACCESS_STATIC, width, height);
        if(video->emu_texture == NULL || SDL_SetTextureBlendMode(video->emu_texture, SDL_BLENDMODE_NONE))
                return system_error("SDL_SetTextureBlendMode", SDL_GetError());
        if(SDL_UpdateTexture(video->emu_texture, NULL, uxn_screen.pixels, sizeof(Uint32)) != 0)
                return system_error("SDL_UpdateTexture", SDL_GetError());
        video->emu_viewport.x = PAD;
        video->emu_viewport.y = PAD;
        video->emu_viewport.w = uxn_screen.width;
        video->emu_viewport.h = uxn_screen.height;
        set_window_size(video, video->emu_window, (width + PAD2) * video->zoom, (height + PAD2) * video->zoom);
        return 1;
}

static void
emu_redraw(Uxn *u)
{
        video_t *video;
        if(u==NULL || u->userptr==NULL || (video=((instance_t *)u->userptr)->video)==NULL)
                return;
        screen_redraw(u);
        if(SDL_UpdateTexture(video->emu_texture, NULL, uxn_screen.pixels, uxn_screen.width * sizeof(Uint32)) != 0)
                system_error("SDL_UpdateTexture", SDL_GetError());
        SDL_RenderClear(video->emu_renderer);
        SDL_RenderCopy(video->emu_renderer, video->emu_texture, NULL, &(video->emu_viewport));
        SDL_RenderPresent(video->emu_renderer);
}

static int
emu_init(Uxn *u)
{
        instance_t *instance;
        SDL_AudioSpec as;
        if(u==NULL || (instance=((instance_t *)u->userptr))==NULL)
                return system_error("emu_init", "corrupted parameters");
        SDL_zero(as);
        as.freq = SAMPLE_FREQUENCY;
        as.format = AUDIO_S16SYS;
        as.channels = 2;
        as.callback = audio_handler;
        as.samples = AUDIO_BUFSIZE;
        as.userdata = u;
        if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_JOYSTICK) < 0)
                return system_error("sdl", SDL_GetError());
        instance->audio_id = SDL_OpenAudioDevice(NULL, 0, &as, NULL, 0);
        if(!instance->audio_id)
                system_error("sdl_audio", SDL_GetError());
        if(SDL_NumJoysticks() > 0 && SDL_JoystickOpen(0) == NULL)
                system_error("sdl_joystick", SDL_GetError());
        instance->stdin_event = SDL_RegisterEvents(1);
        instance->audio0_event = SDL_RegisterEvents(POLYPHONY);
        SDL_DetachThread(instance->stdin_thread = SDL_CreateThread(stdin_handler, "stdin", (void *)instance));
        SDL_StartTextInput();
        SDL_ShowCursor(SDL_DISABLE);
        SDL_EventState(SDL_DROPFILE, SDL_ENABLE);
        SDL_SetRenderDrawColor(instance->video->emu_renderer, 0x00, 0x00, 0x00, 0xff);
        instance->ms_interval = SDL_GetPerformanceFrequency() / 1000;
        instance->deadline_interval = instance->ms_interval * TIMEOUT_MS;
        instance->exec_deadline = SDL_GetPerformanceCounter() + instance->deadline_interval;
        screen_resize(u,WIDTH, HEIGHT);
        SDL_PauseAudioDevice(instance->audio_id, 1);
        return 1;
}

static void
emu_restart(Uxn *u, char *rom, int soft)
{
        instance_t *instance;
        if(u==NULL || (instance=((instance_t *)u->userptr))==NULL)
                return; /* sanity check error */
        screen_resize(u,WIDTH, HEIGHT);
        screen_fill(uxn_screen.bg, 0);
        screen_fill(uxn_screen.fg, 0);
        system_reboot(u, rom, soft, varvara_romdata((varvara_t *)(instance->varvara),"init.rom"), varvara_sizeromdata((varvara_t *)(instance->varvara),"init.rom"));
        SDL_SetWindowTitle(instance->video->emu_window, boot_rom);
}

static void
capture_screen(Uxn *u)
{
        const Uint32 format = SDL_PIXELFORMAT_RGB24;
#if SDL_BYTEORDER == SDL_BIG_ENDIAN
        /* SDL_PIXELFORMAT_RGB24 */
        Uint32 Rmask = 0x000000FF;
        Uint32 Gmask = 0x0000FF00;
        Uint32 Bmask = 0x00FF0000;
#else
        /* SDL_PIXELFORMAT_BGR24 */
        Uint32 Rmask = 0x00FF0000;
        Uint32 Gmask = 0x0000FF00;
        Uint32 Bmask = 0x000000FF;
#endif
        time_t t = time(NULL);
        char fname[64];
        int w, h;
        SDL_Surface *surface;
        instance_t *instance;
        if(u==NULL || (instance=((instance_t *)u->userptr))==NULL)
                return; /* sanity check error */
        SDL_GetRendererOutputSize(instance->video->emu_renderer, &w, &h);
        if((surface = SDL_CreateRGBSurface(0, w, h, 24, Rmask, Gmask, Bmask, 0)) == NULL)
                return;
        SDL_RenderReadPixels(instance->video->emu_renderer, NULL, format, surface->pixels, surface->pitch);
        strftime(fname, sizeof(fname), "screenshot-%Y%m%d-%H%M%S.bmp", localtime(&t));
        if(SDL_SaveBMP(surface, fname) == 0) {
                fprintf(stderr, "Saved %s\n", fname);
                fflush(stderr);
        }
        SDL_FreeSurface(surface);
}

static Uint8
get_button(SDL_Event *event)
{
        switch(event->key.keysym.sym) {
        case SDLK_LCTRL: return 0x01;
        case SDLK_LALT: return 0x02;
        case SDLK_LSHIFT: return 0x04;
        case SDLK_HOME: return 0x08;
        case SDLK_UP: return 0x10;
        case SDLK_DOWN: return 0x20;
        case SDLK_LEFT: return 0x40;
        case SDLK_RIGHT: return 0x80;
        }
        return 0x00;
}

static Uint8
get_button_joystick(SDL_Event *event)
{
        return 0x01 << (event->jbutton.button & 0x3);
}

static Uint8
get_vector_joystick(SDL_Event *event)
{
        if(event->jaxis.value < -3200)
                return 1;
        if(event->jaxis.value > 3200)
                return 2;
        return 0;
}

static Uint8
get_key(SDL_Event *event)
{
        int sym = event->key.keysym.sym;
        SDL_Keymod mods = SDL_GetModState();
        if(sym < 0x20 || sym == SDLK_DELETE)
                return sym;
        if(mods & KMOD_CTRL) {
                if(sym < SDLK_a)
                        return sym;
                else if(sym <= SDLK_z)
                        return sym - (mods & KMOD_SHIFT) * 0x20;
        }
        return 0x00;
}

static int
handle_events(Uxn *u)
{
        SDL_Event event;
        instance_t *instance;
        if(u==NULL || (instance=((instance_t *)u->userptr))==NULL)
                return 0; /* sanity check error */
        while(SDL_PollEvent(&event)) {
                /* Window */
                if(event.type == SDL_QUIT)
                        return 0;
                else if(event.type == SDL_WINDOWEVENT && event.window.event == SDL_WINDOWEVENT_EXPOSED)
                        emu_redraw(u);
                else if(event.type == SDL_DROPFILE) {
                        emu_restart(u, event.drop.file, 0);
                        SDL_free(event.drop.file);
                }
                /* Mouse */
                else if(event.type == SDL_MOUSEMOTION)
                        mouse_pos(u, &u->dev[0x90], clamp(event.motion.x - PAD, 0, uxn_screen.width - 1), clamp(event.motion.y - PAD, 0, uxn_screen.height - 1));
                else if(event.type == SDL_MOUSEBUTTONUP)
                        mouse_up(u, &u->dev[0x90], SDL_BUTTON(event.button.button));
                else if(event.type == SDL_MOUSEBUTTONDOWN)
                        mouse_down(u, &u->dev[0x90], SDL_BUTTON(event.button.button));
                else if(event.type == SDL_MOUSEWHEEL)
                        mouse_scroll(u, &u->dev[0x90], event.wheel.x, event.wheel.y);
                /* Controller */
                else if(event.type == SDL_TEXTINPUT)
                        controller_key(u, &u->dev[0x80], event.text.text[0]);
                else if(event.type == SDL_KEYDOWN) {
                        int ksym;
                        if(get_key(&event))
                                controller_key(u, &u->dev[0x80], get_key(&event));
                        else if(get_button(&event))
                                controller_down(u, &u->dev[0x80], get_button(&event));
                        else if(event.key.keysym.sym == SDLK_F1)
                                set_zoom(instance->video,(instance->video->zoom==4)?1:(instance->video->zoom+1), 1);
                        else if(event.key.keysym.sym == SDLK_F2)
                                set_debugger(u, !u->dev[0x0e]);
                        else if(event.key.keysym.sym == SDLK_F3)
                                capture_screen(u);
                        else if(event.key.keysym.sym == SDLK_F4)
                                emu_restart(u, boot_rom, 0);
                        else if(event.key.keysym.sym == SDLK_F5)
                                emu_restart(u, boot_rom, 1);
                        else if(event.key.keysym.sym == SDLK_F11)
                                set_flag_fullscreen(instance->video, !instance->video->flag_fullscreen, 1);
                        else if(event.key.keysym.sym == SDLK_F12)
                                set_flag_borderless(instance->video, !instance->video->flag_borderless);
                        ksym = event.key.keysym.sym;
                        if(SDL_PeepEvents(&event, 1, SDL_PEEKEVENT, SDL_KEYUP, SDL_KEYUP) == 1 && ksym == event.key.keysym.sym)
                                return 1;
                } else if(event.type == SDL_KEYUP)
                        controller_up(u, &u->dev[0x80], get_button(&event));
                else if(event.type == SDL_JOYAXISMOTION) {
                        Uint8 vec = get_vector_joystick(&event);
                        if(!vec)
                                controller_up(u, &u->dev[0x80], (3 << (!event.jaxis.axis * 2)) << 4);
                        else
                                controller_down(u, &u->dev[0x80], (1 << ((vec + !event.jaxis.axis * 2) - 1)) << 4);
                } else if(event.type == SDL_JOYBUTTONDOWN)
                        controller_down(u, &u->dev[0x80], get_button_joystick(&event));
                else if(event.type == SDL_JOYBUTTONUP)
                        controller_up(u, &u->dev[0x80], get_button_joystick(&event));
                else if(event.type == SDL_JOYHATMOTION) {
                        /* NOTE: Assuming there is only one joyhat in the controller */
                        switch(event.jhat.value) {
                        case SDL_HAT_UP: controller_down(u, &u->dev[0x80], 0x10); break;
                        case SDL_HAT_DOWN: controller_down(u, &u->dev[0x80], 0x20); break;
                        case SDL_HAT_LEFT: controller_down(u, &u->dev[0x80], 0x40); break;
                        case SDL_HAT_RIGHT: controller_down(u, &u->dev[0x80], 0x80); break;
                        case SDL_HAT_LEFTDOWN: controller_down(u, &u->dev[0x80], 0x40 | 0x20); break;
                        case SDL_HAT_LEFTUP: controller_down(u, &u->dev[0x80], 0x40 | 0x10); break;
                        case SDL_HAT_RIGHTDOWN: controller_down(u, &u->dev[0x80], 0x80 | 0x20); break;
                        case SDL_HAT_RIGHTUP: controller_down(u, &u->dev[0x80], 0x80 | 0x10); break;
                        case SDL_HAT_CENTERED: controller_up(u, &u->dev[0x80], 0x10 | 0x20 | 0x40 | 0x80); break;
                        }
                }
                /* Console */
                else if(event.type == instance->stdin_event)
                        console_input(u, event.cbutton.button, CONSOLE_STD);
        }
        return 1;
}

static int
emu_run(Uxn *u, char *rom)
{
        Uint64 next_refresh = 0;
        Uint64 frame_interval;
        Uint8 *vector_addr;
        Uint32 window_flags = SDL_WINDOW_SHOWN | SDL_WINDOW_ALLOW_HIGHDPI;
        instance_t *instance;
        if(u==NULL || (instance=((instance_t *)u->userptr))==NULL)
                return system_error("Init", "emu_run parameters corrupted");
        instance->video->flag_windowcreated = 1;
        if(u==NULL || rom==NULL || (instance=((instance_t *)u->userptr))==NULL)
                return system_error("Init", "emu_run parameters");
        frame_interval=SDL_GetPerformanceFrequency()/60;
        vector_addr=&u->dev[0x20];
        if(instance->video->flag_fullscreen)
                window_flags = window_flags | SDL_WINDOW_FULLSCREEN_DESKTOP;
        instance->video->emu_window = SDL_CreateWindow(rom,
                SDL_WINDOWPOS_UNDEFINED,
                SDL_WINDOWPOS_UNDEFINED,
                (uxn_screen.width + PAD2) * instance->video->zoom,
                (uxn_screen.height + PAD2) * instance->video->zoom,
                window_flags);
        if(instance->video->emu_window == NULL)
                return system_error("sdl_window", SDL_GetError());
        instance->video->emu_renderer = SDL_CreateRenderer(instance->video->emu_window, -1, SDL_RENDERER_ACCELERATED);
        if(instance->video->emu_renderer == NULL)
                return system_error("sdl_renderer", SDL_GetError());
        emu_resize(u, uxn_screen.width, uxn_screen.height);
        /* game loop */
        for(;;) {
                Uint16 screen_vector;
                Uint64 now = SDL_GetPerformanceCounter();
                /* .System/halt */
                if(u->dev[0x0f])
                        return system_error("Run", "Ended.");
                instance->exec_deadline = now + instance->deadline_interval;
                if(!handle_events(u))
                        return 0;
                screen_vector = PEEK2(vector_addr);
                if(now >= next_refresh) {
                        now = SDL_GetPerformanceCounter();
                        next_refresh = now + frame_interval;
                        uxn_eval(u, screen_vector);
                        if(uxn_screen.x2)
                                emu_redraw(u);
                }
                if(screen_vector || uxn_screen.x2) {
                        Uint64 delay_ms = (next_refresh - now) / instance->ms_interval;
                        if(delay_ms > 0) SDL_Delay(delay_ms);
                } else
                        SDL_WaitEvent(NULL);
        }
}

static int
emu_end(Uxn *u)
{
        instance_t *instance;
        if(u==NULL || (instance=((instance_t *)u->userptr))==NULL)
                return 0;
        SDL_CloseAudioDevice(instance->audio_id);
#ifdef _WIN32
#pragma GCC diagnostic ignored "-Wint-to-pointer-cast"
        TerminateThread((HANDLE)SDL_GetThreadID(instance->stdin_thread), 0);
#elif !defined(__APPLE__)
        close(0); /* make stdin thread exit */
#endif
        SDL_Quit();
        free(u->ram);
        return u->dev[0x0f] & 0x7f;
}

int
main(int argc, char **argv)
{
        int zoom;
        int flag_fullscreen;
        char *rom;
        varvara_t *varvara;
        int i;

        zoom=1;
        flag_fullscreen=0;
        i = 1;
        if(i == argc)
                return system_error("usage", "uxn256emu [-v] | uxnemu [-f | -2x | -3x | --] file.rom [args...]");
        /* Read flag. Right now, there can be only one. */
        if(argv[i][0] == '-') {
                if(argv[i][1] == 'v')
                        return system_version("Uxn256emu - Graphical Varvara256 Emulator", "31 Dec 2023");
                if(argv[i][1] == '-')
                        i++;
                if(strcmp(argv[i], "-2x") == 0 || strcmp(argv[i], "-3x") == 0 || strcmp(argv[i], "-4x") == 0 )
                        zoom=argv[i++][1] - '0';
                if(strcmp(argv[i], "-f") == 0) {
                        i++;
                        flag_fullscreen=1;
                }
        }
        rom = argv[i++];
        if((varvara=varvara_init(rom,flag_fullscreen,zoom))==NULL)
                return system_error("init", "Couln't init varvara256");
        /* Game Loop */
        varvara->instances[0]->u.dev[0x17] = argc - i;
        if(uxn_eval(&(varvara->instances[0]->u), PAGE_PROGRAM)) {
                console_listen(&(varvara->instances[0]->u), i, argc, argv);
                emu_run(&(varvara->instances[0]->u), boot_rom);
        }
        return emu_end(&(varvara->instances[0]->u));
}