#include <stdint.h>
#include <stddef.h>

#define DL_REASON_ATTACH	0
#define DL_REASON_DETACH	1

#define BSS_START	0x80B89D38
#define BSS_END		0x80CCFB58

#define N			4096
#define F			18
#define THRESHOLD	2

typedef void*		(*MALLOC_FUNC)(size_t);
typedef void		(*FREE_FUNC)(void *);
typedef void		(*LCD_SET_FRAME_FUNC)(void);
typedef uint16_t*	(*LCD_GET_FRAME_FUNC)(void);

static void 		initialize(void);
static void*		memset(void *s, int c, size_t n);
static void*		memcpy(void *__restrict s1, const void *__restrict s2, size_t n);
static int			lzss_unpack(uint8_t *src, int s, uint8_t *dst);
static uint16_t**	get_framebuffer_pointer(void);
static void			new_lcd_set_frame_subroutine(void);

static MALLOC_FUNC const			malloc = (MALLOC_FUNC)0x80A00250;
static FREE_FUNC const				free = (FREE_FUNC)0x80A00260;
static LCD_SET_FRAME_FUNC const		_lcd_set_frame = (LCD_SET_FRAME_FUNC)0x80A002B8;
static LCD_GET_FRAME_FUNC const		_lcd_get_frame = (LCD_GET_FRAME_FUNC)0x80A002C0;

static uint32_t		initialized = 0x30323341;
static uint8_t		text_buf[N + F - 1];

static uint16_t**	framebuffer_ptr;
static uint16_t*	internal_framebuffer;
static uint16_t*	system_framebuffer;

static inline void write_jump_to(uint32_t *address, uint32_t jump_to)
{
	jump_to = ((jump_to & 0x0FFFFFFF) / 4) | 0x08000000;
	
	*address++ = jump_to;
	*address = 0;
}

uint32_t dl_main(void *handle, uint32_t reason)
{
	if (initialized == 0x30323341)
		initialize();

	if (reason == DL_REASON_ATTACH)
	{
		memset((void*)BSS_START, 0, BSS_END - BSS_START);

		internal_framebuffer = (uint16_t*)malloc(0x25800);

		if (!internal_framebuffer)
			return 0;
		
		memset((void *)internal_framebuffer, 0, 0x25800);
	}
	else if (reason == DL_REASON_DETACH)
	{
		if (*framebuffer_ptr != system_framebuffer)
		{
			*framebuffer_ptr = system_framebuffer;
			_lcd_set_frame();
		}
		
		free(internal_framebuffer);
	}
	else
		return 0;
	
	return 1;
}

static void initialize(void)
{
	lzss_unpack((uint8_t*)0xA0A0688C, 65536, (uint8_t*)0xA1FE0000);
	
	memcpy((void*)0xA0A02000, (const void *)0xA1FE0000, 65536);
		
	write_jump_to((uint32_t*)0xA0A000A0, 0x81FD0000);
	
	write_jump_to((uint32_t*)0xA0A0001C, (uint32_t)new_lcd_set_frame_subroutine);
	
	framebuffer_ptr = get_framebuffer_pointer();
	
	system_framebuffer = *framebuffer_ptr;
	
	initialized = 0xDEADBEEF;
}

static void *memset(void *s, int c, size_t n)
{
	register unsigned char *p = (unsigned char *) s;

	while (n) {
		*p++ = (unsigned char) c;
		--n;
	}

	return s;
}

static void *memcpy(void *__restrict s1, const void *__restrict s2, size_t n)
{
	register char *r1 = s1;
	register const char *r2 = s2;

	while (n) {
		*r1++ = *r2++;
		--n;
	}

	return s1;
}

static int lzss_unpack(uint8_t *src, int s, uint8_t *dst)
{
	int		i, j, k, c, r;
	int		size = 0;
	int		flags;

	i = j = k = c = flags = 0;
	r = N-F;

	memset(text_buf, 0, sizeof(text_buf));

	while(1)
	{
		if (((flags >>= 1) & 256) == 0)
		{
			c = *(src++);
			flags = c | 0xFF00;
		}

		if (flags & 1)
		{
			c = *(src++);
			text_buf[r++] = c;
			r &= (N - 1);
			*(dst++) = c;
			if (++size >= s)
				goto getout;
		}
		else
		{
			i = *(src++);
			j = *(src++);

			i |= ((j & 0xF0) << 4);
			j = (j & 0x0F) + THRESHOLD;

			for (k=0; k <= j; k++)
			{
				c = text_buf[(i + k) & (N - 1)];
				text_buf[r++] = c;
				r &= (N - 1);
				*(dst++) = c;
				if (++size >= s)
					goto getout;
			}
		}
	}

getout:
	return size;
}

/* Routine to get a pointer to the framebuffer pointer, by zaxxon. */
static uint16_t** get_framebuffer_pointer(void)
{
	uint32_t	*LP, LV;
	int16_t		*SP;

	LP = (uint32_t *)_lcd_get_frame;
	LV = *LP;
	LV = ((LV & 0x03FFFFFF) << 2) | 0x80000000;
	LP = (uint32_t *)LV;
	LV = LP[2];
	LV = ((LV & 0x03FFFFFF) << 2) | 0x80000000;
	LP = (uint32_t *)LV;
	LV = LP[10];
	LV = ((LV & 0x03FFFFFF) << 2) | 0x80000000;
	LP = (uint32_t *)LV;
	SP = (int16_t *)LV;
	LV = (LP[0] << 16) + ((int32_t)(SP[4]));

	return ((uint16_t**)LV);
}

static void new_lcd_set_frame_subroutine(void)
{
	_lcd_set_frame();
	
	if (*framebuffer_ptr == system_framebuffer)
		*framebuffer_ptr = internal_framebuffer;
	else
		*framebuffer_ptr = system_framebuffer;
}
