#include <stdio.h>
#include <stdlib.h>
#include <linux/fb.h>
#include <sys/ioctl.h>
#include <sys/mman.h>

#include <fcntl.h>
#include <unistd.h>
#include <pthread.h>

#include <SDL/SDL.h>
#include <SDL/SDL_image.h>
#include <SDL/SDL_ttf.h>
#include <errno.h>
#include <stdbool.h>
#include <stdint.h>

#include <msettings.h>

#include "ion.h"
#include "ion-owl.h"
#include "de_atm7059.h"

#include "api.h"
#include "utils.h"
#include "defines.h"

///////////////////////////////

void LOG_note(int level, const char* fmt, ...) {
	char buf[1024] = {0};
	va_list args;
	va_start(args, fmt);
	vsnprintf(buf, sizeof(buf), fmt, args);
	va_end(args);
	switch(level) {
#ifdef DEBUG
	case LOG_DEBUG:
		printf("[DEBUG] %s", buf);
		break;
#endif
	case LOG_INFO:
		printf("[INFO] %s", buf);
		break;
	case LOG_WARN:
		fprintf(stderr, "[WARN] %s", buf);
		break;
	case LOG_ERROR:
		fprintf(stderr, "[ERROR] %s", buf);
		break;
	default:
		break;
	}
	fflush(stdout);
}

///////////////////////////////

typedef struct ion_alloc_info {
	uint32_t			size;
	struct ion_handle	*handle;
	int					fd;
	void*				padd;
	void*				vadd;
} ion_alloc_info_t;
static void ion_alloc(int fd_ion, ion_alloc_info_t* info) {
	struct ion_allocation_data	iad;
	struct ion_fd_data		ifd;
	struct ion_custom_data		icd;
	struct owl_ion_phys_data 	ipd;

	iad.len = info->size;
	iad.align = sysconf(_SC_PAGESIZE);
	iad.heap_id_mask = (1<<ION_HEAP_ID_PMEM);
	iad.flags = 0;
	if (ioctl(fd_ion, ION_IOC_ALLOC, &iad)<0) fprintf(stderr, "ION_ALLOC failed %s\n",strerror(errno));
	icd.cmd = OWL_ION_GET_PHY;
	icd.arg = (uintptr_t)&ipd;
	ipd.handle = iad.handle;
	if (ioctl(fd_ion, ION_IOC_CUSTOM, &icd)<0) printf("ION_GET_PHY failed %s\n",strerror(errno));
	ifd.handle = iad.handle;
	if (ioctl(fd_ion, ION_IOC_MAP, &ifd)<0) fprintf(stderr, "ION_MAP failed %s\n",strerror(errno));

	info->handle = (void*)iad.handle;
	info->fd = ifd.fd;
	info->padd = (void*)ipd.phys_addr;
	info->vadd = mmap(0, info->size, PROT_READ|PROT_WRITE, MAP_SHARED, info->fd, 0);

}
static void ion_free(int fd_ion, ion_alloc_info_t* info) {
	struct ion_handle_data	ihd;
	munmap(info->vadd, info->size);
	close(info->fd);
	ihd.handle = (uintptr_t)info->handle;
	if (ioctl(fd_ion, ION_IOC_FREE, &ihd)<0) fprintf(stderr, "ION_FREE failed %s\n",strerror(errno));
	fflush(stdout);
}

///////////////////////////////

#define	DE		(0xB02E0000)
#define	DE_SIZE	(0x00002000)
static int de_enable_overlay = 0;
enum {
	DE_SCOEF_NONE,
	DE_SCOEF_CRISPY,
	DE_SCOEF_ZOOMIN,
	DE_SCOEF_HALF_ZOOMOUT,
	DE_SCOEF_SMALLER_ZOOMOUT,
	DE_SCOEF_MAX
};
static void DE_setScaleCoef(uint32_t* de_mem, int plane, int scale) {
	switch(scale) {
		case DE_SCOEF_NONE:	// for integer scale	  < L R > (0x40=100%) Applies to the following pixels:
			de_mem[DE_OVL_SCOEF0(plane)/4]= 0x00400000; // L 100%  R 0%
			de_mem[DE_OVL_SCOEF1(plane)/4]= 0x00400000; // L 87.5% R 12.5%
			de_mem[DE_OVL_SCOEF2(plane)/4]= 0x00400000; // L 75%   R 25%
			de_mem[DE_OVL_SCOEF3(plane)/4]= 0x00400000; // L 62.5% R 37.5%
			de_mem[DE_OVL_SCOEF4(plane)/4]= 0x00004000; // L 50%   R 50%
			de_mem[DE_OVL_SCOEF5(plane)/4]= 0x00004000; // L 37.5% R 62.5%
			de_mem[DE_OVL_SCOEF6(plane)/4]= 0x00004000; // L 25%   R 75%
			de_mem[DE_OVL_SCOEF7(plane)/4]= 0x00004000; // L 12.5% R 87.5%
			break;
		case DE_SCOEF_CRISPY:	// crispy setting for upscale
			de_mem[DE_OVL_SCOEF0(plane)/4]= 0x00400000;
			de_mem[DE_OVL_SCOEF1(plane)/4]= 0x00400000;
			de_mem[DE_OVL_SCOEF2(plane)/4]= 0x00400000;
			de_mem[DE_OVL_SCOEF3(plane)/4]= 0x00400000;
			de_mem[DE_OVL_SCOEF4(plane)/4]= 0x00202000;
			de_mem[DE_OVL_SCOEF5(plane)/4]= 0x00004000;
			de_mem[DE_OVL_SCOEF6(plane)/4]= 0x00004000;
			de_mem[DE_OVL_SCOEF7(plane)/4]= 0x00004000;
			break;
		case DE_SCOEF_ZOOMIN:
			de_mem[DE_OVL_SCOEF0(plane)/4]= 0x00400000;
			de_mem[DE_OVL_SCOEF1(plane)/4]= 0xFC3E07FF;
			de_mem[DE_OVL_SCOEF2(plane)/4]= 0xFA3810FE;
			de_mem[DE_OVL_SCOEF3(plane)/4]= 0xF9301BFC;
			de_mem[DE_OVL_SCOEF4(plane)/4]= 0xFA2626FA;
			de_mem[DE_OVL_SCOEF5(plane)/4]= 0xFC1B30F9;
			de_mem[DE_OVL_SCOEF6(plane)/4]= 0xFE1038FA;
			de_mem[DE_OVL_SCOEF7(plane)/4]= 0xFF073EFC;
			break;
		case DE_SCOEF_HALF_ZOOMOUT:
			de_mem[DE_OVL_SCOEF0(plane)/4]= 0x00400000;
			de_mem[DE_OVL_SCOEF1(plane)/4]= 0x00380800;
			de_mem[DE_OVL_SCOEF2(plane)/4]= 0x00301000;
			de_mem[DE_OVL_SCOEF3(plane)/4]= 0x00281800;
			de_mem[DE_OVL_SCOEF4(plane)/4]= 0x00202000;
			de_mem[DE_OVL_SCOEF5(plane)/4]= 0x00182800;
			de_mem[DE_OVL_SCOEF6(plane)/4]= 0x00103000;
			de_mem[DE_OVL_SCOEF7(plane)/4]= 0x00083800;
			break;
		case DE_SCOEF_SMALLER_ZOOMOUT:
			de_mem[DE_OVL_SCOEF0(plane)/4]= 0x10201000;
			de_mem[DE_OVL_SCOEF1(plane)/4]= 0x0E1E1202;
			de_mem[DE_OVL_SCOEF2(plane)/4]= 0x0C1C1404;
			de_mem[DE_OVL_SCOEF3(plane)/4]= 0x0A1A1606;
			de_mem[DE_OVL_SCOEF4(plane)/4]= 0x08181808;
			de_mem[DE_OVL_SCOEF5(plane)/4]= 0x06161A0A;
			de_mem[DE_OVL_SCOEF6(plane)/4]= 0x04141C0C;
			de_mem[DE_OVL_SCOEF7(plane)/4]= 0x02121E0E;
			break;
		default:
			break;
	}
}
static void DE_enableLayer(uint32_t* de_mem) {
	de_mem[DE_PATH_CTL(0)/4] = (de_enable_overlay?0x30300000:0x30100000) | (de_mem[DE_PATH_CTL(0)/4] & 0xCF0FFFFF);
}
static void DE_setRect(uint32_t* de_mem, int x, int y, int w, int h) {
	de_mem[(DE_OVL_OSIZE(0))/4] = ((w-1)&0xFFFF) | ((h-1)<<16);
	de_mem[(DE_OVL_SR(0))/4] = ((0x2000*((de_mem[(DE_OVL_ISIZE(0))/4]&0xFFFF)+1)/w)&0xFFFF) |
						((0x2000*((de_mem[(DE_OVL_ISIZE(0))/4]>>16)+1)/h)<<16);
	de_mem[(DE_OVL_COOR(0,0))/4] = (y<<16) | (x&0xFFFF);
}

///////////////////////////////

#define MAX_PRIVATE_DATA_SIZE 40
struct owlfb_disp_device{
    __u32 mType;
    __u32 mState;
    __u32 mPluginState;
    __u32 mWidth;
    __u32 mHeight;
    __u32 mRefreshRate;
    __u32 mWidthScale;
    __u32 mHeightScale;
    __u32 mCmdMode;
    __u32 mIcType;
    __u32 mPrivateInfo[MAX_PRIVATE_DATA_SIZE];
};

struct display_private_info {
	int LCD_TYPE; // 1
	int	LCD_LIGHTENESS; // 128
	int LCD_SATURATION; // 7
	int LCD_CONSTRAST;  // 5
};

enum CmdMode {
    SET_LIGHTENESS = 0,
    SET_SATURATION = 1,
    SET_CONSTRAST = 2,
	SET_DEFAULT = 3,

};

struct owlfb_sync_info {
	__u8 enabled;
	__u8 disp_id;
	__u16 reserved2;
};
enum owlfb_overlay_type {
	OWLFB_OVERLAY_VIDEO = 1,
	OWLFB_OVERLAY_CURSOR,
};
enum owl_color_mode {
	OWL_DSS_COLOR_RGB16	= 0,
	OWL_DSS_COLOR_BGR16	= 1,
	OWL_DSS_COLOR_ARGB32    = 4,
	OWL_DSS_COLOR_ABGR32    = 5,
	OWL_DSS_COLOR_RGBA32    = 6,
	OWL_DSS_COLOR_BGRA32	= 7,
	OWL_DSS_COLOR_NV21	= 8,
	OWL_DSS_COLOR_NU21	= 9,
	OWL_DSS_COLOR_YU12	= 10,
	OWL_DSS_COLOR_ARGB16	= 12,
	OWL_DSS_COLOR_ABGR16	= 13,
	OWL_DSS_COLOR_RGBA16	= 14,
	OWL_DSS_COLOR_BGRA16	= 15,
	OWL_DSS_COLOR_RGB24U	= 16,
	OWL_DSS_COLOR_RGB24P	= 17,
	OWL_DSS_COLOR_RGBX32	= 18,
	OWL_DSS_COLOR_NV12	= 19,
	OWL_DSS_COLOR_XBGR32	= 20,
	OWL_DSS_COLOR_XRGB32	= 21,
};
struct owlfb_overlay_args {
	__u16 fb_id;
	__u16 overlay_id;
	__u16 overlay_type;
	__u32 overlay_mem_base;
	__u32 overlay_mem_size;
	__u32 uintptr_overly_info;
};
struct owlfb_overlay_info {
	__u32 mem_off;
	__u32 mem_size;
	__u32 screen_width;
	enum owl_color_mode color_mode;
	__u32 img_width;
	__u32 img_height;
	__u32 xoff;
	__u32 yoff;
	__u32 width;
	__u32 height;
	__u8 rotation;
	__u32 pos_x;
	__u32 pos_y;
	__u32 out_width;
	__u32 out_height;
	__u8 lightness;
	__u8 saturation;
	__u8 contrast;
	bool global_alpha_en;
	__u8 global_alpha;
	bool pre_mult_alpha_en;
	__u8 zorder;
};

#define OWL_IOW(num, dtype) _IOW('O', num, dtype)
#define OWL_IOR(num, dtype)	_IOR('O', num, dtype)
#define OWLFB_WAITFORVSYNC            OWL_IOW(57, long long)
#define OWLFB_GET_DISPLAY_INFO		  OWL_IOW(74, struct owlfb_disp_device)
#define OWLFB_SET_DISPLAY_INFO		  OWL_IOW(75, struct owlfb_disp_device)
#define OWLFB_VSYNC_EVENT_EN	      OWL_IOW(67, struct owlfb_sync_info)
#define OWLFB_OVERLAY_REQUEST		  OWL_IOR(41, struct owlfb_overlay_args)
#define OWLFB_OVERLAY_RELEASE		  OWL_IOR(42, struct owlfb_overlay_args)
#define OWLFB_OVERLAY_ENABLE		  OWL_IOR(43, struct owlfb_overlay_args)
#define OWLFB_OVERLAY_DISABLE		  OWL_IOR(45, struct owlfb_overlay_args)
#define OWLFB_OVERLAY_GETINFO		  OWL_IOW(46, struct owlfb_overlay_args)
#define OWLFB_OVERLAY_SETINFO   	  OWL_IOW(47, struct owlfb_overlay_args)

///////////////////////////////

uint32_t RGB_WHITE;
uint32_t RGB_BLACK;
uint32_t RGB_LIGHT_GRAY;
uint32_t RGB_GRAY;
uint32_t RGB_DARK_GRAY;

static struct GFX_Context {
	SDL_Surface* screen;
	SDL_Surface* assets;

	int mode;
	int vsync;
	
	int fd_fb;
	int fd_ion;
	int fd_mem;
	
	uint32_t* de_mem;
	
	struct fb_fix_screeninfo finfo;
	struct fb_var_screeninfo vinfo;
	ion_alloc_info_t fb_info;
	
	int page;
	int width;
	int height;
	int pitch;
	int cleared;
} gfx;

static SDL_Rect asset_rects[] = {
	[ASSET_WHITE_PILL]		= (SDL_Rect){SCALE4( 1, 1,30,30)},
	[ASSET_BLACK_PILL]		= (SDL_Rect){SCALE4(33, 1,30,30)},
	[ASSET_DARK_GRAY_PILL]	= (SDL_Rect){SCALE4(65, 1,30,30)},
	[ASSET_OPTION]			= (SDL_Rect){SCALE4(97, 1,20,20)},
	[ASSET_BUTTON]			= (SDL_Rect){SCALE4( 1,33,20,20)},
	[ASSET_PAGE_BG]			= (SDL_Rect){SCALE4(64,33,15,15)},
	[ASSET_STATE_BG]		= (SDL_Rect){SCALE4(23,54, 8, 8)},
	[ASSET_PAGE]			= (SDL_Rect){SCALE4(39,54, 6, 6)},
	[ASSET_BAR]				= (SDL_Rect){SCALE4(33,58, 4, 4)},
	[ASSET_BAR_BG]			= (SDL_Rect){SCALE4(15,55, 4, 4)},
	[ASSET_BAR_BG_MENU]		= (SDL_Rect){SCALE4(85,56, 4, 4)},
	[ASSET_UNDERLINE]		= (SDL_Rect){SCALE4(85,51, 3, 3)},
	[ASSET_DOT]				= (SDL_Rect){SCALE4(33,54, 2, 2)},
	
	[ASSET_BRIGHTNESS]		= (SDL_Rect){SCALE4(23,33,19,19)},
	[ASSET_VOLUME_MUTE]		= (SDL_Rect){SCALE4(44,33,10,16)},
	[ASSET_VOLUME]			= (SDL_Rect){SCALE4(44,33,18,16)},
	[ASSET_BATTERY]			= (SDL_Rect){SCALE4(47,51,17,10)},
	[ASSET_BATTERY_LOW]		= (SDL_Rect){SCALE4(66,51,17,10)},
	[ASSET_BATTERY_FILL]	= (SDL_Rect){SCALE4(81,33,12, 6)},
	[ASSET_BATTERY_FILL_LOW]= (SDL_Rect){SCALE4( 1,55,12, 6)},
	[ASSET_BATTERY_BOLT]	= (SDL_Rect){SCALE4(81,41,12, 6)},
	
	[ASSET_SCROLL_UP]		= (SDL_Rect){SCALE4(97,23,24, 6)},
	[ASSET_SCROLL_DOWN]		= (SDL_Rect){SCALE4(97,31,24, 6)},
};
static uint32_t asset_rgbs[ASSET_COLORS];
GFX_Fonts font;

///////////////////////////////

static struct POW_Context {
	int can_poweroff;
	int can_autosleep;
	
	pthread_t battery_pt;
	int is_charging;
	int charge;
	int should_warn;

	struct owlfb_overlay_args oargs;
	struct owlfb_overlay_info oinfo;

	SDL_Surface* overlay;
	ion_alloc_info_t ov_info;
} pow;

///////////////////////////////

static int _;

SDL_Surface* GFX_init(int mode) {
	SDL_Init(SDL_INIT_VIDEO);
	SDL_ShowCursor(0);
	SDL_SetVideoMode(0,0,FIXED_DEPTH,0);
	
	////////
	
	gfx.fd_fb = open("/dev/fb0", O_RDWR);
	gfx.fd_ion = open("/dev/ion", O_RDWR);
	gfx.fd_mem = open("/dev/mem", O_RDWR);
	gfx.de_mem = mmap(0, DE_SIZE, PROT_READ|PROT_WRITE, MAP_SHARED, gfx.fd_mem, DE);
	
	ioctl(gfx.fd_fb, FBIOGET_FSCREENINFO, &gfx.finfo);
	ioctl(gfx.fd_fb, FBIOGET_VSCREENINFO, &gfx.vinfo);
	
	gfx.page = 1;
	gfx.width = FIXED_WIDTH;
	gfx.height = FIXED_HEIGHT;
	gfx.pitch = FIXED_PITCH;
	
	gfx.fb_info.size = PAGE_SIZE * PAGE_COUNT;
	ion_alloc(gfx.fd_ion, &gfx.fb_info);

	gfx.screen = SDL_CreateRGBSurfaceFrom(gfx.fb_info.vadd+PAGE_SIZE, gfx.width,gfx.height,FIXED_DEPTH,gfx.pitch, RGBA_MASK_AUTO);
	memset(gfx.screen->pixels, 0, gfx.pitch * gfx.height);
	
	struct owlfb_sync_info sinfo;
	sinfo.enabled = 1;
	sinfo.disp_id = 2;
	if (ioctl(gfx.fd_fb, OWLFB_VSYNC_EVENT_EN, &sinfo)<0) fprintf(stderr, "VSYNC_EVENT_EN failed %s\n",strerror(errno));
	
	int vw = (gfx.de_mem[DE_PATH_SIZE(0)/4]&0xFFFF)+1;
	int vh = (gfx.de_mem[DE_PATH_SIZE(0)/4]>>16)+1;
	
	gfx.de_mem[DE_OVL_ISIZE(0)/4] = gfx.de_mem[DE_OVL_ISIZE(2)/4] = ((gfx.width-1) & 0xFFFF) | ((gfx.height-1) << 16);
	gfx.de_mem[DE_OVL_SR(0)/4] = gfx.de_mem[DE_OVL_SR(2)/4] = ((0x2000*gfx.width/vw)&0xFFFF) | ((0x2000*gfx.height/vh)<<16);
	gfx.de_mem[DE_OVL_STR(0)/4] = gfx.de_mem[DE_OVL_STR(2)/4] = gfx.pitch / 8;
	gfx.de_mem[DE_OVL_BA0(0)/4] = (uintptr_t)(gfx.fb_info.padd + PAGE_SIZE);
	
	GFX_setNearestNeighbor(0);
	
	////////
	
	gfx.vsync = VSYNC_STRICT;
	gfx.mode = mode;
	
	RGB_WHITE		= SDL_MapRGB(gfx.screen->format, TRIAD_WHITE);
	RGB_BLACK		= SDL_MapRGB(gfx.screen->format, TRIAD_BLACK);
	RGB_LIGHT_GRAY	= SDL_MapRGB(gfx.screen->format, TRIAD_LIGHT_GRAY);
	RGB_GRAY		= SDL_MapRGB(gfx.screen->format, TRIAD_GRAY);
	RGB_DARK_GRAY	= SDL_MapRGB(gfx.screen->format, TRIAD_DARK_GRAY);
	
	asset_rgbs[ASSET_WHITE_PILL]	= RGB_WHITE;
	asset_rgbs[ASSET_BLACK_PILL]	= RGB_BLACK;
	asset_rgbs[ASSET_DARK_GRAY_PILL]= RGB_DARK_GRAY;
	asset_rgbs[ASSET_OPTION]		= RGB_DARK_GRAY;
	asset_rgbs[ASSET_BUTTON]		= RGB_WHITE;
	asset_rgbs[ASSET_PAGE_BG]		= RGB_WHITE;
	asset_rgbs[ASSET_STATE_BG]		= RGB_WHITE;
	asset_rgbs[ASSET_PAGE]			= RGB_BLACK;
	asset_rgbs[ASSET_BAR]			= RGB_WHITE;
	asset_rgbs[ASSET_BAR_BG]		= RGB_BLACK;
	asset_rgbs[ASSET_BAR_BG_MENU]	= RGB_DARK_GRAY;
	asset_rgbs[ASSET_UNDERLINE]		= RGB_GRAY;
	asset_rgbs[ASSET_DOT]			= RGB_LIGHT_GRAY;
	
	char asset_path[MAX_PATH];
	sprintf(asset_path, RES_PATH "/assets@%ix.png", SCREEN_SCALE);
	gfx.assets = IMG_Load(asset_path);
	
	TTF_Init();
	font.large 	= TTF_OpenFont(FONT_PATH, SCALE1(FONT_LARGE));
	font.medium = TTF_OpenFont(FONT_PATH, SCALE1(FONT_MEDIUM));
	font.small 	= TTF_OpenFont(FONT_PATH, SCALE1(FONT_SMALL));
	font.tiny 	= TTF_OpenFont(FONT_PATH, SCALE1(FONT_TINY));

	TTF_SetFontStyle(font.large, TTF_STYLE_BOLD);
	TTF_SetFontStyle(font.medium, TTF_STYLE_BOLD);
	TTF_SetFontStyle(font.small, TTF_STYLE_BOLD);
	TTF_SetFontStyle(font.tiny, TTF_STYLE_BOLD);

	return gfx.screen;
}

void GFX_quit(void) {
	TTF_CloseFont(font.large);
	TTF_CloseFont(font.medium);
	TTF_CloseFont(font.small);
	TTF_CloseFont(font.tiny);
	
	SDL_FreeSurface(gfx.assets);
	
	GFX_clearAll();

	ion_free(gfx.fd_ion, &gfx.fb_info);
	munmap(gfx.de_mem, DE_SIZE);
	close(gfx.fd_mem);
	close(gfx.fd_ion);
	close(gfx.fd_fb);
	SDL_FreeSurface(gfx.screen);
	SDL_Quit();
}

void GFX_clear(SDL_Surface* screen) {
	// this buffer is offscreen when cleared
	memset(screen->pixels, 0, PAGE_SIZE); 
}
void GFX_clearAll(void) {
	GFX_clear(gfx.screen); // clear backbuffer
	gfx.cleared = 1; // defer clearing frontbuffer until offscreen
}

void GFX_setMode(int mode) {
	gfx.mode = mode;
}
int GFX_getVsync(void) {
	return gfx.vsync;
}
void GFX_setVsync(int vsync) {
	gfx.vsync = vsync;
}

#define FRAME_BUDGET 17 // 60fps
static uint32_t frame_start = 0;
void GFX_startFrame(void) {
	frame_start = SDL_GetTicks();
}
SDL_Surface* GFX_resize(int w, int h, int pitch) {
	gfx.width = w;
	gfx.height = h;
	gfx.pitch = pitch;

	SDL_FreeSurface(gfx.screen);
	gfx.screen = SDL_CreateRGBSurfaceFrom(gfx.fb_info.vadd + gfx.page*PAGE_SIZE, gfx.width,gfx.height,FIXED_DEPTH,gfx.pitch, RGBA_MASK_AUTO);
	memset(gfx.screen->pixels, 0, gfx.pitch * gfx.height);
	
	int vw = (gfx.de_mem[DE_PATH_SIZE(0)/4]&0xFFFF)+1;
	int vh = (gfx.de_mem[DE_PATH_SIZE(0)/4]>>16)+1;
	
	gfx.de_mem[DE_OVL_ISIZE(0)/4] = gfx.de_mem[DE_OVL_ISIZE(2)/4] = ((gfx.width-1) & 0xFFFF) | ((gfx.height-1) << 16);
	gfx.de_mem[DE_OVL_SR(0)/4] = gfx.de_mem[DE_OVL_SR(2)/4] = ((0x2000*gfx.width/vw)&0xFFFF) | ((0x2000*gfx.height/vh)<<16);
	gfx.de_mem[DE_OVL_STR(0)/4] = gfx.de_mem[DE_OVL_STR(2)/4] = gfx.pitch / 8;
	gfx.de_mem[DE_OVL_BA0(0)/4] = (uintptr_t)(gfx.fb_info.padd + gfx.page * PAGE_SIZE);
	
	return gfx.screen;
}
void GFX_setScaleClip(int x, int y, int width, int height) {
	DE_setRect(gfx.de_mem, x,y,width,height);
}
void GFX_setNearestNeighbor(int enabled) {
	int scale_coef = enabled ? DE_SCOEF_NONE : DE_SCOEF_HALF_ZOOMOUT;
	DE_setScaleCoef(gfx.de_mem, 0, scale_coef);
	DE_setScaleCoef(gfx.de_mem, 1, scale_coef);
	DE_setScaleCoef(gfx.de_mem, 2, scale_coef);
	DE_setScaleCoef(gfx.de_mem, 3, scale_coef);
}
static void POW_flipOverlay(void);
void GFX_flip(SDL_Surface* screen) {
	gfx.de_mem[DE_OVL_BA0(0)/4] = gfx.de_mem[DE_OVL_BA0(2)/4] = (uintptr_t)(gfx.fb_info.padd + gfx.page * PAGE_SIZE);
	DE_enableLayer(gfx.de_mem);

	if (gfx.vsync!=VSYNC_OFF) {
		// this limiting condition helps SuperFX chip games
		if (gfx.vsync==VSYNC_STRICT || frame_start==0 || SDL_GetTicks()-frame_start<FRAME_BUDGET) { // only wait if we're under frame budget
			if (ioctl(gfx.fd_fb, OWLFB_WAITFORVSYNC, &_)) LOG_info("OWLFB_WAITFORVSYNC failed %s\n", strerror(errno));
		}
	}

	// swap backbuffer
	gfx.page ^= 1;
	gfx.screen->pixels = gfx.fb_info.vadd + gfx.page * PAGE_SIZE;
	
	if (gfx.cleared) {
		GFX_clear(gfx.screen);
		gfx.cleared = 0;
	}
}
void GFX_sync(void) {
	if (gfx.vsync!=VSYNC_OFF) {
		// this limiting condition helps SuperFX chip games
		if (gfx.vsync==VSYNC_STRICT || frame_start==0 || SDL_GetTicks()-frame_start<FRAME_BUDGET) { // only wait if we're under frame budget
			if (ioctl(gfx.fd_fb, OWLFB_WAITFORVSYNC, &_)) LOG_info("OWLFB_WAITFORVSYNC failed %s\n", strerror(errno));
		}
	}
	else {
		uint32_t frame_duration = SDL_GetTicks() - frame_start;
		if (frame_duration<FRAME_BUDGET) SDL_Delay(FRAME_BUDGET-frame_duration);
	}
}

SDL_Surface* GFX_getBufferCopy(void) { // must be freed by caller
	int buffer = gfx.page - 1;
	if (buffer<0) buffer += PAGE_COUNT;
	SDL_Surface* copy = SDL_CreateRGBSurface(SDL_SWSURFACE, gfx.screen->w,gfx.screen->h,FIXED_DEPTH,0,0,0,0);
	SDL_BlitSurface(gfx.screen, NULL, copy, NULL); // TODO: this is just copying screen! :facepalm:
	return copy;
}
int GFX_truncateText(TTF_Font* font, const char* in_name, char* out_name, int max_width, int padding) {
	int text_width;
	strcpy(out_name, in_name);
	TTF_SizeUTF8(font, out_name, &text_width, NULL);
	text_width += padding;
	
	while (text_width>max_width) {
		int len = strlen(out_name);
		strcpy(&out_name[len-4], "...\0");
		TTF_SizeUTF8(font, out_name, &text_width, NULL);
		text_width += padding;
	}
	
	return text_width;
}
int GFX_wrapText(TTF_Font* font, char* str, int max_width, int max_lines) {
	if (!str) return 0;
	
	int line_width;
	int max_line_width = 0;
	char* line = str;
	char buffer[MAX_PATH];
	
	TTF_SizeUTF8(font, line, &line_width, NULL);
	if (line_width<=max_width) {
		line_width = GFX_truncateText(font,line,buffer,max_width,0);
		strcpy(line,buffer);
		return line_width;
	}
	
	char* prev = NULL;
	char* tmp = line;
	int lines = 1;
	int i = 0;
	while (!max_lines || lines<max_lines) {
		tmp = strchr(tmp, ' ');
		if (!tmp) {
			if (prev) {
				TTF_SizeUTF8(font, line, &line_width, NULL);
				if (line_width>=max_width) {
					if (line_width>max_line_width) max_line_width = line_width;
					prev[0] = '\n';
					line = prev + 1;
				}
			}
			break;
		}
		tmp[0] = '\0';
		
		TTF_SizeUTF8(font, line, &line_width, NULL);

		if (line_width>=max_width) { // wrap
			if (line_width>max_line_width) max_line_width = line_width;
			tmp[0] = ' ';
			tmp += 1;
			prev[0] = '\n';
			prev += 1;
			line = prev;
			lines += 1;
		}
		else { // continue
			tmp[0] = ' ';
			prev = tmp;
			tmp += 1;
		}
		i += 1;
	}
	
	line_width = GFX_truncateText(font,line,buffer,max_width,0);
	strcpy(line,buffer);
	
	if (line_width>max_line_width) max_line_width = line_width;
	return max_line_width;
}

void GFX_blitAsset(int asset, SDL_Rect* src_rect, SDL_Surface* dst, SDL_Rect* dst_rect) {
	SDL_Rect* rect = &asset_rects[asset];
	SDL_Rect adj_rect = {
		.x = rect->x,
		.y = rect->y,
		.w = rect->w,
		.h = rect->h,
	};
	if (src_rect) {
		adj_rect.x += src_rect->x;
		adj_rect.y += src_rect->y;
		adj_rect.w  = src_rect->w;
		adj_rect.h  = src_rect->h;
	}
	SDL_BlitSurface(gfx.assets, &adj_rect, dst, dst_rect);
}
void GFX_blitPill(int asset, SDL_Surface* dst, SDL_Rect* dst_rect) {
	int x = dst_rect->x;
	int y = dst_rect->y;
	int w = dst_rect->w;
	int h = dst_rect->h;

	if (h==0) h = asset_rects[asset].h;
	
	int r = h / 2;
	if (w < h) w = h;
	w -= h;
	
	GFX_blitAsset(asset, &(SDL_Rect){0,0,r,h}, dst, &(SDL_Rect){x,y});
	x += r;
	if (w>0) {
		SDL_FillRect(dst, &(SDL_Rect){x,y,w,h}, asset_rgbs[asset]);
		x += w;
	}
	GFX_blitAsset(asset, &(SDL_Rect){r,0,r,h}, dst, &(SDL_Rect){x,y});
}
void GFX_blitRect(int asset, SDL_Surface* dst, SDL_Rect* dst_rect) {
	int x = dst_rect->x;
	int y = dst_rect->y;
	int w = dst_rect->w;
	int h = dst_rect->h;
	int c = asset_rgbs[asset];
	
	SDL_Rect* rect = &asset_rects[asset];
	int d = rect->w;
	int r = d / 2;

	GFX_blitAsset(asset, &(SDL_Rect){0,0,r,r}, dst, &(SDL_Rect){x,y});
	SDL_FillRect(dst, &(SDL_Rect){x+r,y,w-d,r}, c);
	GFX_blitAsset(asset, &(SDL_Rect){r,0,r,r}, dst, &(SDL_Rect){x+w-r,y});
	SDL_FillRect(dst, &(SDL_Rect){x,y+r,w,h-d}, c);
	GFX_blitAsset(asset, &(SDL_Rect){0,r,r,r}, dst, &(SDL_Rect){x,y+h-r});
	SDL_FillRect(dst, &(SDL_Rect){x+r,y+h-r,w-d,r}, c);
	GFX_blitAsset(asset, &(SDL_Rect){r,r,r,r}, dst, &(SDL_Rect){x+w-r,y+h-r});
}
void GFX_blitClockAndBattery(SDL_Surface* dst, SDL_Rect* dst_rect) {
	// LOG_info("dst: %p\n", dst);
	
	if (!dst_rect) dst_rect = &(SDL_Rect){0,0,0,0};
	
	SDL_Rect rect = asset_rects[ASSET_BATTERY];
	int x = dst_rect->x;
	int y = dst_rect->y;
	x += (SCALE1(PILL_SIZE) - (rect.w + SCREEN_SCALE)) / 2;
	y += (SCALE1(PILL_SIZE) - rect.h) / 2;
	
	if (pow.is_charging) {
		GFX_blitAsset(ASSET_BATTERY, NULL, dst, &(SDL_Rect){x,y});
		GFX_blitAsset(ASSET_BATTERY_BOLT, NULL, dst, &(SDL_Rect){x+SCALE1(3),y+SCALE1(2)});
	}
	else {
		int percent = pow.charge;
		GFX_blitAsset(percent<=10?ASSET_BATTERY_LOW:ASSET_BATTERY, NULL, dst, &(SDL_Rect){x,y});
		
		rect = asset_rects[ASSET_BATTERY_FILL];
		SDL_Rect clip = rect;
		clip.w *= percent;
		clip.w /= 100;
		if (clip.w<=0) return;
		clip.x = rect.w - clip.w;
		clip.y = 0;
		
		GFX_blitAsset(percent<=20?ASSET_BATTERY_FILL_LOW:ASSET_BATTERY_FILL, &clip, dst, &(SDL_Rect){x+SCALE1(3)+clip.x,y+SCALE1(2)});
	}	
	
	// Get the current time
	time_t currentTime = time(NULL);
	struct tm* timeinfo = localtime(&currentTime);
	int hours = timeinfo->tm_hour;
	int minutes = timeinfo->tm_min;

	// Convert hours and minutes to strings
	char timeStr[6];
	snprintf(timeStr, sizeof(timeStr), "%02d:%02d", hours, minutes);
	SDL_Surface* text = TTF_RenderUTF8_Blended(font.large, timeStr, COLOR_WHITE);
	SDL_BlitSurface(text, NULL, dst, &(SDL_Rect){dst_rect->x - text->w - 20, dst_rect->y + (SCALE1(PILL_SIZE) - text->h) / 2});
	SDL_FreeSurface(text);
}
int GFX_getButtonWidth(char* hint, char* button) {
	int button_width = 0;
	int width;
	
	int special_case = !strcmp(button,BRIGHTNESS_BUTTON_LABEL); // TODO: oof
	
	if (strlen(button)==1) {
		button_width += SCALE1(BUTTON_SIZE);
	}
	else {
		button_width += SCALE1(BUTTON_SIZE) / 2;
		TTF_SizeUTF8(special_case ? font.large : font.tiny, button, &width, NULL);
		button_width += width;
	}
	button_width += SCALE1(BUTTON_MARGIN);
	
	TTF_SizeUTF8(font.small, hint, &width, NULL);
	button_width += width + SCALE1(BUTTON_MARGIN);
	return button_width;
}
void GFX_blitButton(char* hint, char*button, SDL_Surface* dst, SDL_Rect* dst_rect) {
	SDL_Surface* text;
	int ox = 0;
	
	int special_case = !strcmp(button,BRIGHTNESS_BUTTON_LABEL); // TODO: oof
	
	// button
	if (strlen(button)==1) {
		GFX_blitAsset(ASSET_BUTTON, NULL, dst, dst_rect);

		// label
		text = TTF_RenderUTF8_Blended(font.medium, button, COLOR_BUTTON_TEXT);
		SDL_BlitSurface(text, NULL, dst, &(SDL_Rect){dst_rect->x+(SCALE1(BUTTON_SIZE)-text->w)/2,dst_rect->y+(SCALE1(BUTTON_SIZE)-text->h)/2});
		ox += SCALE1(BUTTON_SIZE);
		SDL_FreeSurface(text);
	}
	else {
		text = TTF_RenderUTF8_Blended(special_case ? font.large : font.tiny, button, COLOR_BUTTON_TEXT);
		GFX_blitPill(ASSET_BUTTON, dst, &(SDL_Rect){dst_rect->x,dst_rect->y,SCALE1(BUTTON_SIZE)/2+text->w,SCALE1(BUTTON_SIZE)});
		ox += SCALE1(BUTTON_SIZE)/4;
		
		int oy = special_case ? SCALE1(-2) : 0;
		SDL_BlitSurface(text, NULL, dst, &(SDL_Rect){ox+dst_rect->x,oy+dst_rect->y+(SCALE1(BUTTON_SIZE)-text->h)/2,text->w,text->h});
		ox += text->w;
		ox += SCALE1(BUTTON_SIZE)/4;
		SDL_FreeSurface(text);
	}
	
	ox += SCALE1(BUTTON_MARGIN);

	// hint text
	text = TTF_RenderUTF8_Blended(font.small, hint, COLOR_WHITE);
	SDL_BlitSurface(text, NULL, dst, &(SDL_Rect){ox+dst_rect->x,dst_rect->y+(SCALE1(BUTTON_SIZE)-text->h)/2,text->w,text->h});
	SDL_FreeSurface(text);
}
void GFX_blitMessage(TTF_Font* font, char* msg, SDL_Surface* dst, SDL_Rect* dst_rect) {
	if (dst_rect==NULL) dst_rect = &(SDL_Rect){0,0,dst->w,dst->h};
	
	SDL_Surface* text;
#define TEXT_BOX_MAX_ROWS 16
#define LINE_HEIGHT 24
	char* rows[TEXT_BOX_MAX_ROWS];
	int row_count = 0;

	char* tmp;
	rows[row_count++] = msg;
	while ((tmp=strchr(rows[row_count-1], '\n'))!=NULL) {
		if (row_count+1>=TEXT_BOX_MAX_ROWS) return; // TODO: bail
		rows[row_count++] = tmp+1;
	}
	
	int rendered_height = SCALE1(LINE_HEIGHT) * row_count;
	int y = dst_rect->y;
	y += (dst_rect->h - rendered_height) / 2;
	
	char line[256];
	for (int i=0; i<row_count; i++) {
		int len;
		if (i+1<row_count) {
			len = rows[i+1]-rows[i]-1;
			if (len) strncpy(line, rows[i], len);
			line[len] = '\0';
		}
		else {
			len = strlen(rows[i]);
			strcpy(line, rows[i]);
		}
		
		
		if (len) {
			text = TTF_RenderUTF8_Blended(font, line, COLOR_WHITE);
			int x = dst_rect->x;
			x += (dst_rect->w - text->w) / 2;
			SDL_BlitSurface(text, NULL, dst, &(SDL_Rect){x,y});
			SDL_FreeSurface(text);
		}
		y += SCALE1(LINE_HEIGHT);
	}
}

int GFX_blitHardwareGroup(SDL_Surface* dst, int show_setting) {
	int ox;
	int oy;
	int ow = 0;
	
	int setting_value;
	int setting_min;
	int setting_max;
	
	if (show_setting) {
		ow = SCALE1(PILL_SIZE + SETTINGS_WIDTH + PADDING + 4);
		ox = dst->w - SCALE1(PADDING) - ow;
		oy = SCALE1(PADDING);
		GFX_blitPill(gfx.mode==MODE_MAIN ? ASSET_DARK_GRAY_PILL : ASSET_BLACK_PILL, dst, &(SDL_Rect){
			ox,
			oy,
			ow,
			SCALE1(PILL_SIZE)
		});
		
		if (show_setting==1) {
			setting_value = GetBrightness();
			setting_min = BRIGHTNESS_MIN;
			setting_max = BRIGHTNESS_MAX;
		}
		else {
			setting_value = GetVolume();
			setting_min = VOLUME_MIN;
			setting_max = VOLUME_MAX;
		}
		
		int asset = show_setting==1?ASSET_BRIGHTNESS:(setting_value>0?ASSET_VOLUME:ASSET_VOLUME_MUTE);
		int ax = ox + (show_setting==1 ? SCALE1(6) : SCALE1(8));
		int ay = oy + (show_setting==1 ? SCALE1(5) : SCALE1(7));
		GFX_blitAsset(asset, NULL, dst, &(SDL_Rect){ax,ay});
		
		ox += SCALE1(PILL_SIZE);
		oy += SCALE1((PILL_SIZE - SETTINGS_SIZE) / 2);
		GFX_blitPill(gfx.mode==MODE_MAIN ? ASSET_BAR_BG : ASSET_BAR_BG_MENU, dst, &(SDL_Rect){
			ox,
			oy,
			SCALE1(SETTINGS_WIDTH),
			SCALE1(SETTINGS_SIZE)
		});
		
		float percent = ((float)(setting_value-setting_min) / (setting_max-setting_min));
		if (show_setting==1 || setting_value>0) {
			GFX_blitPill(ASSET_BAR, dst, &(SDL_Rect){
				ox,
				oy,
				SCALE1(SETTINGS_WIDTH) * percent,
				SCALE1(SETTINGS_SIZE)
			});
		}
	}
	else {
		ow = SCALE1(PILL_SIZE);
		ox = dst->w - SCALE1(PADDING) - ow;
		oy = SCALE1(PADDING);
		GFX_blitPill(gfx.mode==MODE_MAIN ? ASSET_DARK_GRAY_PILL : ASSET_BLACK_PILL, dst, &(SDL_Rect){
			ox,
			oy,
			ow,
			SCALE1(PILL_SIZE)
		});
		GFX_blitClockAndBattery(dst, &(SDL_Rect){ox,oy});
	}
	
	return ow;
}
int GFX_blitButtonGroup(char** pairs, SDL_Surface* dst, int align_right) {
	int ox;
	int oy;
	int ow;
	char* hint;
	char* button;

	struct Hint {
		char* hint;
		char* button;
		int ow;
	} hints[2]; 
	int w = 0; // individual button dimension
	int h = 0; // hints index
	ow = 0; // full pill width
	ox = align_right ? dst->w - SCALE1(PADDING) : SCALE1(PADDING);
	oy = dst->h - SCALE1(PADDING + PILL_SIZE);
	
	for (int i=0; i<2; i++) {
		if (!pairs[i*2]) break;
		
		button = pairs[i * 2];
		hint = pairs[i * 2 + 1];
		w = GFX_getButtonWidth(hint, button);
		hints[h].hint = hint;
		hints[h].button = button;
		hints[h].ow = w;
		h += 1;
		ow += SCALE1(BUTTON_MARGIN) + w;
	}
	
	ow += SCALE1(BUTTON_MARGIN);
	if (align_right) ox -= ow;
	GFX_blitPill(gfx.mode==MODE_MAIN ? ASSET_DARK_GRAY_PILL : ASSET_BLACK_PILL, dst, &(SDL_Rect){
		ox,
		oy,
		ow,
		SCALE1(PILL_SIZE)
	});
	
	ox += SCALE1(BUTTON_MARGIN);
	oy += SCALE1(BUTTON_MARGIN);
	for (int i=0; i<h; i++) {
		GFX_blitButton(hints[i].hint, hints[i].button, dst, &(SDL_Rect){ox,oy});
		ox += hints[i].ow + SCALE1(BUTTON_MARGIN);
	}
	return ow;
}

#define MAX_TEXT_LINES 16
void GFX_sizeText(TTF_Font* font, char* str, int leading, int* w, int* h) {
	char* lines[MAX_TEXT_LINES];
	int count = 0;

	char* tmp;
	lines[count++] = str;
	while ((tmp=strchr(lines[count-1], '\n'))!=NULL) {
		if (count+1>MAX_TEXT_LINES) break; // TODO: bail?
		lines[count++] = tmp+1;
	}
	*h = count * leading;
	
	int mw = 0;
	char line[256];
	for (int i=0; i<count; i++) {
		int len;
		if (i+1<count) {
			len = lines[i+1]-lines[i]-1;
			if (len) strncpy(line, lines[i], len);
			line[len] = '\0';
		}
		else {
			len = strlen(lines[i]);
			strcpy(line, lines[i]);
		}
		
		if (len) {
			int lw;
			TTF_SizeUTF8(font, line, &lw, NULL);
			if (lw>mw) mw = lw;
		}
	}
	*w = mw;
}
void GFX_blitText(TTF_Font* font, char* str, int leading, SDL_Color color, SDL_Surface* dst, SDL_Rect* dst_rect) {
	if (dst_rect==NULL) dst_rect = &(SDL_Rect){0,0,dst->w,dst->h};
	
	char* lines[MAX_TEXT_LINES];
	int count = 0;

	char* tmp;
	lines[count++] = str;
	while ((tmp=strchr(lines[count-1], '\n'))!=NULL) {
		if (count+1>MAX_TEXT_LINES) break; // TODO: bail?
		lines[count++] = tmp+1;
	}
	int x = dst_rect->x;
	int y = dst_rect->y;
	
	SDL_Surface* text;
	char line[256];
	for (int i=0; i<count; i++) {
		int len;
		if (i+1<count) {
			len = lines[i+1]-lines[i]-1;
			if (len) strncpy(line, lines[i], len);
			line[len] = '\0';
		}
		else {
			len = strlen(lines[i]);
			strcpy(line, lines[i]);
		}
		
		if (len) {
			text = TTF_RenderUTF8_Blended(font, line, color);
			SDL_BlitSurface(text, NULL, dst, &(SDL_Rect){x+((dst_rect->w-text->w)/2),y+(i*leading)});
			SDL_FreeSurface(text);
		}
	}
}

///////////////////////////////

// based on picoarch's audio 
// implementation, rewritten 
// to (try to) understand it 
// better

#define MAX_SAMPLE_RATE 48000
#define BATCH_SIZE 100

typedef int (*SND_Resampler)(const SND_Frame frame);
static struct SND_Context {
	double frame_rate;
	
	int sample_rate_in;
	int sample_rate_out;
	
	int buffer_seconds;     // current_audio_buffer_size
	SND_Frame* buffer;	// buf
	size_t frame_count; 	// buf_len
	
	int frame_in;     // buf_w
	int frame_out;    // buf_r
	int frame_filled; // max_buf_w
	
	SND_Resampler resample;
} snd;
static void SND_audioCallback(void* userdata, uint8_t* stream, int len) { // plat_sound_callback
	if (snd.frame_count==0) return;
	
	int16_t *out = (int16_t *)stream;
	len /= (sizeof(int16_t) * 2);
	
	while (snd.frame_out!=snd.frame_in && len>0) {
		*out++ = snd.buffer[snd.frame_out].left;
		*out++ = snd.buffer[snd.frame_out].right;
		
		snd.frame_filled = snd.frame_out;
		
		snd.frame_out += 1;
		len -= 1;
		
		if (snd.frame_out>=snd.frame_count) snd.frame_out = 0;
	}
	
	while (len>0) {
		*out++ = 0;
		*out++ = 0;
		len -= 1;
	}
}
static void SND_resizeBuffer(void) { // plat_sound_resize_buffer
	snd.frame_count = snd.buffer_seconds * snd.sample_rate_in / snd.frame_rate;
	if (snd.frame_count==0) return;
	
	SDL_LockAudio();
	
	int buffer_bytes = snd.frame_count * sizeof(SND_Frame);
	snd.buffer = realloc(snd.buffer, buffer_bytes);
	
	memset(snd.buffer, 0, buffer_bytes);
	
	snd.frame_in = 0;
	snd.frame_out = 0;
	snd.frame_filled = snd.frame_count - 1;
	
	SDL_UnlockAudio();
}
static int SND_resampleNone(SND_Frame frame) { // audio_resample_passthrough
	snd.buffer[snd.frame_in++] = frame;
	if (snd.frame_in >= snd.frame_count) snd.frame_in = 0;
	return 1;
}
static int SND_resampleNear(SND_Frame frame) { // audio_resample_nearest
	static int diff = 0;
	int consumed = 0;

	if (diff < snd.sample_rate_out) {
		snd.buffer[snd.frame_in++] = frame;
		if (snd.frame_in >= snd.frame_count) snd.frame_in = 0;
		diff += snd.sample_rate_in;
	}

	if (diff >= snd.sample_rate_out) {
		consumed++;
		diff -= snd.sample_rate_out;
	}

	return consumed;
}
static void SND_selectResampler(void) { // plat_sound_select_resampler
	if (snd.sample_rate_in==snd.sample_rate_out) {
		snd.resample =  SND_resampleNone;
	}
	else {
		snd.resample = SND_resampleNear;
	}
}
size_t SND_batchSamples(const SND_Frame* frames, size_t frame_count) { // plat_sound_write / plat_sound_write_resample
	if (snd.frame_count==0) return 0;
	
	SDL_LockAudio();

	int consumed = 0;
	while (frame_count > 0) {
		int tries = 0;
		int amount = MIN(BATCH_SIZE, frame_count);

		while (tries < 10 && snd.frame_in==snd.frame_filled) {
			tries++;
			SDL_UnlockAudio();
			SDL_Delay(1);
			SDL_LockAudio();
		}

		while (amount && snd.frame_in != snd.frame_filled) {
			consumed = snd.resample(*frames);
			frames += consumed;
			amount -= consumed;
			frame_count -= consumed;
		}
	}
	SDL_UnlockAudio();
	
	return consumed;
}

void SND_init(double sample_rate, double frame_rate) { // plat_sound_init
	LOG_info("SND_init\n");
	
	SDL_InitSubSystem(SDL_INIT_AUDIO);
	
	snd.frame_rate = frame_rate;

	SDL_AudioSpec spec_in;
	SDL_AudioSpec spec_out;
	
	spec_in.freq = MIN(sample_rate, MAX_SAMPLE_RATE); // TODO: always MAX_SAMPLE_RATE on Miyoo Mini? use #ifdef PLATFORM_MIYOOMINI?
	spec_in.format = AUDIO_S16;
	spec_in.channels = 2;
	spec_in.samples = 512;
	spec_in.callback = SND_audioCallback;
	
	SDL_OpenAudio(&spec_in, &spec_out);
	
	snd.buffer_seconds = 5;
	snd.sample_rate_in  = sample_rate;
	snd.sample_rate_out = spec_out.freq;
	
	LOG_info("sample rate: %i (req) %i (rec)\n", snd.sample_rate_in, snd.sample_rate_out);
	
	SND_selectResampler();
	SND_resizeBuffer();
	
	SDL_PauseAudio(0);
}
void SND_quit(void) { // plat_sound_finish
	SDL_PauseAudio(1);
	SDL_CloseAudio();
	
	if (snd.buffer) {
		free(snd.buffer);
		snd.buffer = NULL;
	}
}

///////////////////////////////

static struct PAD_Context {
	int is_pressed;
	int just_pressed;
	int just_released;
	int just_repeated;
	uint32_t repeat_at[BTN_ID_COUNT];
} pad;
#define PAD_REPEAT_DELAY	300
#define PAD_REPEAT_INTERVAL 100
void PAD_reset(void) {
	pad.just_pressed = BTN_NONE;
	pad.is_pressed = BTN_NONE;
	pad.just_released = BTN_NONE;
}
void PAD_poll(void) {
	// reset transient state
	pad.just_pressed = BTN_NONE;
	pad.just_released = BTN_NONE;
	pad.just_repeated = BTN_NONE;

	uint32_t tick = SDL_GetTicks();
	for (int i=0; i<BTN_ID_COUNT; i++) {
		int btn = 1 << i;
		if ((pad.is_pressed & btn) && (tick>=pad.repeat_at[i])) {
			pad.just_repeated |= btn; // set
			pad.repeat_at[i] += PAD_REPEAT_INTERVAL;
		}
	}
	
	// the actual poll
	SDL_Event event;
	while (SDL_PollEvent(&event)) {
		int btn = BTN_NONE;
		int id = -1;
		if (event.type==SDL_KEYDOWN || event.type==SDL_KEYUP) {
			uint8_t code = event.key.keysym.scancode;
				 if (code==CODE_UP) 	{ btn = BTN_UP; 		id = BTN_ID_UP; }
 			else if (code==CODE_DOWN)	{ btn = BTN_DOWN; 		id = BTN_ID_DOWN; }
			else if (code==CODE_LEFT)	{ btn = BTN_LEFT; 		id = BTN_ID_LEFT; }
			else if (code==CODE_RIGHT)	{ btn = BTN_RIGHT; 		id = BTN_ID_RIGHT; }
			else if (code==CODE_A)		{ btn = BTN_A; 			id = BTN_ID_A; }
			else if (code==CODE_B)		{ btn = BTN_B; 			id = BTN_ID_B; }
			else if (code==CODE_X)		{ btn = BTN_X; 			id = BTN_ID_X; }
			else if (code==CODE_Y)		{ btn = BTN_Y; 			id = BTN_ID_Y; }
			else if (code==CODE_START)	{ btn = BTN_START; 		id = BTN_ID_START; }
			else if (code==CODE_SELECT)	{ btn = BTN_SELECT; 	id = BTN_ID_SELECT; }
			else if (code==CODE_MENU)	{ btn = BTN_MENU; 		id = BTN_ID_MENU; }
			else if (code==CODE_L1)		{ btn = BTN_L1; 		id = BTN_ID_L1; }
			else if (code==CODE_L2)		{ btn = BTN_L2; 		id = BTN_ID_L2; }
			else if (code==CODE_R1)		{ btn = BTN_R1; 		id = BTN_ID_R1; }
			else if (code==CODE_R2)		{ btn = BTN_R2; 		id = BTN_ID_R2; }
			else if (code==CODE_PLUS)	{ btn = BTN_PLUS; 		id = BTN_ID_PLUS; }
			else if (code==CODE_MINUS)	{ btn = BTN_MINUS; 		id = BTN_ID_MINUS; }
			else if (code==CODE_POWER)	{ btn = BTN_POWER; 		id = BTN_ID_POWER; }
		}
		
		if (btn==BTN_NONE) continue;
		
		if (event.type==SDL_KEYUP) {
			pad.is_pressed		&= ~btn; // unset
			pad.just_repeated	&= ~btn; // unset
			pad.just_released	|= btn; // set
		}
		else if ((pad.is_pressed & btn)==BTN_NONE) {
			pad.just_pressed	|= btn; // set
			pad.just_repeated	|= btn; // set
			pad.is_pressed		|= btn; // set
			pad.repeat_at[id]	= tick + PAD_REPEAT_DELAY;
		}
	}
}

int PAD_anyPressed(void)		{ return pad.is_pressed!=BTN_NONE; }
int PAD_justPressed(int btn)	{ return pad.just_pressed & btn; }
int PAD_isPressed(int btn)		{ return pad.is_pressed & btn; }
int PAD_justReleased(int btn)	{ return pad.just_released & btn; }
int PAD_justRepeated(int btn)	{ return pad.just_repeated & btn; }

int PAD_tappedMenu(uint32_t now) {
	#define MENU_DELAY 250 // also in POW_update()
	static uint32_t menu_start = 0;
	static int ignore_menu = 0; 
	if (PAD_justPressed(BTN_MENU)) {
		ignore_menu = 0;
		menu_start = now;
	}
	else if (PAD_isPressed(BTN_MENU) && (PAD_justPressed(BTN_PLUS) || PAD_justPressed(BTN_MINUS))) {
		ignore_menu = 1;
	}
	return (!ignore_menu && PAD_justReleased(BTN_MENU) && now-menu_start<MENU_DELAY);
}

///////////////////////////////

static struct VIB_Context {
	pthread_t pt;
	int queued_strength;
	int strength;
} vib;
static void* VIB_thread(void *arg) {
#define DEFER_FRAMES 3
	static int defer = 0;
	while(1) {
		SDL_Delay(17);
		if (vib.queued_strength!=vib.strength) {
			if (defer<DEFER_FRAMES && vib.queued_strength==0) { // minimize vacillation between 0 and some number (which this motor doesn't like)
				defer += 1;
				continue;
			}
			vib.strength = vib.queued_strength;
			defer = 0;

			int val = MAX(0, MIN((100 * vib.strength)>>16, 100));
			// LOG_info("strength: %8i (%3i/100)\n", vib.strength, val);
			int fd = open("/sys/class/power_supply/battery/moto", O_WRONLY);
			if (fd>0) {
				dprintf(fd, "%d", val);
				close(fd);
			}
		}
	}
	return 0;
}
void VIB_init(void) {
	vib.queued_strength = vib.strength = 0;
	pthread_create(&vib.pt, NULL, &VIB_thread, NULL);
}
void VIB_quit(void) {
	VIB_setStrength(0);
	pthread_cancel(vib.pt);
	pthread_join(vib.pt, NULL);
}
void VIB_setStrength(int strength) {
	if (vib.queued_strength==strength) return;
	vib.queued_strength = strength;
}
int VIB_getStrength(void) {
	return vib.strength;
}

///////////////////////////////

#define OVERLAY_WIDTH PILL_SIZE // unscaled
#define OVERLAY_HEIGHT PILL_SIZE // unscaled
#define OVERLAY_BPP 4
#define OVERLAY_DEPTH 32
#define OVERLAY_PITCH (OVERLAY_WIDTH * OVERLAY_BPP) // unscaled
#define OVERLAY_RGBA_MASK 0x00ff0000,0x0000ff00,0x000000ff,0xff000000

#define POW_LOW_CHARGE 10
#define OVERLAY_FB 0
#define OVERLAY_ID 1
static void POW_initOverlay(void) {
	// setup surface
	pow.overlay = SDL_CreateRGBSurfaceFrom(NULL,SCALE2(OVERLAY_WIDTH,OVERLAY_HEIGHT),OVERLAY_DEPTH,SCALE1(OVERLAY_PITCH), OVERLAY_RGBA_MASK);
	uint32_t size = pow.overlay->h * pow.overlay->pitch;
	pow.ov_info.size = size;
	ion_alloc(gfx.fd_ion, &pow.ov_info);
	pow.overlay->pixels = pow.ov_info.vadd;
	memset(pow.overlay->pixels, 0xff, size);

	// draw battery
	SDL_SetAlpha(gfx.assets, 0,0);
	GFX_blitAsset(ASSET_BLACK_PILL, NULL, pow.overlay, NULL);
	SDL_SetAlpha(gfx.assets, SDL_SRCALPHA,0);
	GFX_blitClockAndBattery(pow.overlay, NULL);

	// setup overlay
	memset(&pow.oargs, 0, sizeof(struct owlfb_overlay_args));
	pow.oargs.fb_id = OVERLAY_FB;
	pow.oargs.overlay_id = OVERLAY_ID;
	pow.oargs.overlay_type = OWLFB_OVERLAY_VIDEO;
	pow.oargs.uintptr_overly_info = (uintptr_t)&pow.oinfo;

	int x,y,w,h;
	w = h = pow.overlay->w;
	x = SCREEN_WIDTH - SCALE1(PADDING) - w;
	y = SCALE1(PADDING);

	pow.oinfo.mem_off = (uintptr_t)pow.ov_info.padd - gfx.finfo.smem_start;
	pow.oinfo.mem_size = size;
	pow.oinfo.screen_width = PAGE_WIDTH; // ???
	pow.oinfo.color_mode = OWL_DSS_COLOR_ARGB32;
	pow.oinfo.img_width = w;
	pow.oinfo.img_height = h;
	pow.oinfo.xoff = 0;
	pow.oinfo.yoff = 0;
	pow.oinfo.width = w;
	pow.oinfo.height = h;
	pow.oinfo.rotation = 0;
	pow.oinfo.pos_x = x;	// position
	pow.oinfo.pos_y = y;	//
	pow.oinfo.out_width = w;	// scaled size
	pow.oinfo.out_height = h;	//
	pow.oinfo.global_alpha_en = 0;
	pow.oinfo.global_alpha = 0;
	pow.oinfo.pre_mult_alpha_en = 0;
	pow.oinfo.zorder = 3;
}
static void POW_quitOverlay(void) {
	if (pow.overlay) SDL_FreeSurface(pow.overlay);
	ion_free(gfx.fd_ion, &pow.ov_info);

	memset(&pow.oargs, 0, sizeof(struct owlfb_overlay_args));
	pow.oargs.fb_id = OVERLAY_FB;
	pow.oargs.overlay_id = OVERLAY_ID;
	pow.oargs.overlay_type = OWLFB_OVERLAY_VIDEO;
	pow.oargs.uintptr_overly_info = 0;
	ioctl(gfx.fd_fb, OWLFB_OVERLAY_DISABLE, &pow.oargs);
}

int POW_readBatteryStatus(void) {
	#define READ_VOLTAGE 1

	if (READ_VOLTAGE > 0) {
		int voltage_now = getInt("/sys/class/power_supply/battery/voltage_now");
		return ((voltage_now / 10000) - 310); // 310-410
	}

	// Fallback
	return getInt("/sys/class/power_supply/battery/capacity");
}

static void POW_updateBatteryStatus(void) {
	pow.is_charging = getInt("/sys/class/power_supply/battery/charger_online");

	int i = POW_readBatteryStatus();

	// worry less about battery and more about the game you're playing
	     if (i>80) pow.charge = 100;
	else if (i>60) pow.charge =  80;
	else if (i>40) pow.charge =  60;
	else if (i>20) pow.charge =  40;
	else if (i>10) pow.charge =  20;
	else           pow.charge =  10;
	
	// LOG_info("battery charge: %i (%i) charging: %i\n", pow.charge, i, pow.is_charging);
	// pow.charge = POW_LOW_CHARGE;
	
	de_enable_overlay = pow.should_warn && pow.charge<=POW_LOW_CHARGE;
}

static void* POW_monitorBattery(void *arg) {
	while(1) {
		// TODO: the frequency of checking should depend on whether 
		// we're in game (less frequent) or menu (more frequent)
		sleep(30);
		POW_updateBatteryStatus();
	}
	return NULL;
}

void POW_init(void) {
	pow.can_poweroff = 1;
	pow.can_autosleep = 1;
	pow.should_warn = 0;
	pow.charge = POW_LOW_CHARGE;
	
	POW_initOverlay();

	POW_updateBatteryStatus();
	pthread_create(&pow.battery_pt, NULL, &POW_monitorBattery, NULL);
}
void POW_quit(void) {
	POW_quitOverlay();
	
	// cancel battery thread
	pthread_cancel(pow.battery_pt);
	pthread_join(pow.battery_pt, NULL);
}
void POW_warn(int enable) {
	pow.should_warn = enable;
	de_enable_overlay = pow.should_warn && pow.charge<=POW_LOW_CHARGE;
}

void POW_update(int* _dirty, int* _show_setting, POW_callback_t before_sleep, POW_callback_t after_sleep) {
	int dirty = _dirty ? *_dirty : 0;
	int show_setting = _show_setting ? *_show_setting : 0;
	
	static uint32_t cancel_start = 0;
	static uint32_t power_start = 0;
	
	static uint32_t menu_start = 0;
	static uint32_t setting_start = 0;
	static uint32_t charge_start = 0;
	static int was_charging = -1;
	if (was_charging==-1) was_charging = pow.is_charging;

	uint32_t now = SDL_GetTicks();
	if (cancel_start==0) cancel_start = now;
	if (charge_start==0) charge_start = now;
	
	if (PAD_anyPressed()) cancel_start = now;
	
	#define CHARGE_DELAY 1000
	if (dirty || now-charge_start>=CHARGE_DELAY) {
		int is_charging = pow.is_charging;
		if (was_charging!=is_charging) {
			was_charging = is_charging;
			dirty = 1;
		}
		charge_start = now;
	}
	
	if (power_start && now-power_start>=1000) {
		if (before_sleep) before_sleep();
		POW_powerOff();
	}
	if (PAD_justPressed(BTN_SLEEP)) {
		power_start = now;
	}
	
	#define SLEEP_DELAY 30000
	if (now-cancel_start>=SLEEP_DELAY && POW_preventAutosleep()) cancel_start = now;
	
	if (now-cancel_start>=SLEEP_DELAY || PAD_justReleased(BTN_SLEEP)) {
		if (before_sleep) before_sleep();
		POW_fauxSleep();
		if (after_sleep) after_sleep();
		
		cancel_start = now = SDL_GetTicks();
		power_start = 0;
		dirty = 1;
	}
	
	int was_dirty = dirty; // dirty list (not including settings/battery)
	
	#define SETTING_DELAY 500
	if (show_setting && now-setting_start>=SETTING_DELAY && !PAD_isPressed(BTN_MENU)) {
		show_setting = 0;
		dirty = 1;
	}
	
	if (!show_setting && !PAD_isPressed(BTN_MENU)) {
		menu_start = now; // this is weird, updates until pressed
	}
	
	#define MENU_DELAY 250 // also in PAD_tappedMenu()
	if (PAD_justRepeated(BTN_PLUS) || PAD_justRepeated(BTN_MINUS) || (PAD_isPressed(BTN_MENU) && now-menu_start>=MENU_DELAY)) {
		setting_start = now;
		if (PAD_isPressed(BTN_MENU)) {
			show_setting = 1;
		}
		else {
			show_setting = 2;
		}
	}
	if (show_setting) dirty = 1; // shm is slow or keymon is catching input on the next frame

	if (_dirty) *_dirty = dirty;
	if (_show_setting) *_show_setting = show_setting;
}

void POW_disablePowerOff(void) {
	pow.can_poweroff = 0;
}
void POW_powerOff(void) {
	if (pow.can_poweroff) {
		GFX_resize(FIXED_WIDTH,FIXED_HEIGHT,FIXED_PITCH);

		char* msg = exists(AUTO_RESUME_PATH) ? "Quicksave created,\npowering off" : "Powering off";
		GFX_blitMessage(font.large, msg, gfx.screen, NULL);
		GFX_flip(gfx.screen);

		system("sync");
		system("echo s > /proc/sysrq-trigger");
		system("echo u > /proc/sysrq-trigger");
		system("sync");

		sleep(2);
		
		// actual shutdown
		system("echo o > /proc/sysrq-trigger");
	}
}
void POW_reboot(void) { 
	char* msg = "Rebooting";	
	
	system("sync");
	system("echo b > /proc/sysrq-trigger"); 
	system("sync");
}
#define BACKLIGHT_PATH "/sys/class/backlight/backlight.2/bl_power"

void POW_setCPUSpeed(int speed) {
	char cmd[32];
	sprintf(cmd,"overclock.elf %d\n", speed);
	system(cmd);
}

static void POW_enterSleep(void) {
	SetRawVolume(0);
	putInt(BACKLIGHT_PATH, FB_BLANK_POWERDOWN);
	system("killall -STOP keymon.elf");
	
	sync();
}
static void POW_exitSleep(void) {
	system("killall -CONT keymon.elf");
	
	putInt(BACKLIGHT_PATH, FB_BLANK_UNBLANK);
	SetVolume(GetVolume());
	
	sync();
}
static void POW_waitForWake(void) {
	SDL_Event event;
	int wake = 0;
	uint32_t sleep_ticks = SDL_GetTicks();
	while (!wake) {
		while (SDL_PollEvent(&event)) {
			if (event.type==SDL_KEYUP) {
				uint8_t code = event.key.keysym.scancode;
				if (code==CODE_POWER) {
					wake = 1;
					break;
				}
			}
		}
		SDL_Delay(200);
		if (pow.can_poweroff && SDL_GetTicks()-sleep_ticks>=120000) { // increased to two minutes
			if (pow.is_charging) sleep_ticks += 60000; // check again in a minute
			else POW_powerOff(); // TODO: not working...
		}
	}
	return;
}
void POW_fauxSleep(void) {
	GFX_clear(gfx.screen);
	PAD_reset();
	POW_enterSleep();
	POW_waitForWake();
	POW_exitSleep();
}

void POW_disableAutosleep(void) {
	pow.can_autosleep = 0;
}
void POW_enableAutosleep(void) {
	pow.can_autosleep = 1;
}
int POW_preventAutosleep(void) {
	return pow.is_charging || !pow.can_autosleep;
}

// updated by POW_updateBatteryStatus()
int POW_isCharging(void) {
	return pow.is_charging;
}
int POW_getBattery(void) { // 10-100 in 10-20% fragments
	return pow.charge;
}
