#include <sys/types.h>
#include <sys/stat.h>
#include <malloc.h>
#include <string.h>
#include <stdio.h>
#include <stdarg.h>
#include <unistd.h>
#include <locale.h>
#include <ctype.h>

#include <ogcsys.h>
#include <ogc/machine/processor.h>
#include <ogc/conf.h>
#include <debug.h>

#include "../config.h"
#include "controls.h"
#include "appentry.h"
#include "playtime.h"
#include "gfx.h"
#include "tex.h"
#include "theme.h"
#include "cursors.h"
#include "font.h"
#include "widgets.h"
#include "view.h"
#include "bubbles.h"
#include "dialogs.h"
#include "browser.h"
#include "m_main.h"
#include "loader.h"
#ifdef ENABLE_UPDATES
#include "http.h"
#include "update.h"
#endif
#include "i18n.h"
#include "dvd.h"
#include "manage.h"
#include "xml.h"
#include "title.h"

// Languages
#include "spanish_mo.h"
#include "dutch_mo.h"
#include "german_mo.h"
#include "french_mo.h"
#include "italian_mo.h"
#include "japanese_mo.h"

#ifdef DEBUG_APP
//#define GDBSTUB
#else
#undef GDBSTUB
#endif

#define WARN_SPLIT 360

extern const u8 stub_bin[];
extern const u8 stub_bin_end;
extern const u32 stub_bin_size;

u64 *conf_magic = STUB_ADDR_MAGIC;
u64 *conf_title_id = STUB_ADDR_TITLE;

static bool should_exit;
static bool shutdown;
#ifdef GDBSTUB
static bool gdb;
#endif
static const char *text_delete;
static const char *text_error_delete;

s32 __IOS_LoadStartupIOS(void) {
#if 0
	__ES_Init();
	__IOS_LaunchNewIOS(58);
#endif
	return 0;
}

#define MEM2_PROT          0x0D8B420A
#define ES_MODULE_START (u16*)0x939F0000

static const u16 ticket_check[] = {
    0x685B,               // ldr r3,[r3,#4] ; get TMD pointer
    0x22EC, 0x0052,       // movls r2, 0x1D8
    0x189B,               // adds r3, r3, r2; add offset of access rights field in TMD
    0x681B,               // ldr r3, [r3]   ; load access rights (haxxme!)
    0x4698,               // mov r8, r3  ; store it for the DVD video bitcheck later
    0x07DB                // lsls r3, r3, #31; check AHBPROT bit
};

static int patch_ahbprot_reset(void)
{
	u16 *patchme;

	if ((read32(0x0D800064) == 0xFFFFFFFF) ? 1 : 0) {
		write16(MEM2_PROT, 2);
		for (patchme=ES_MODULE_START; patchme < ES_MODULE_START+0x4000; ++patchme) {
			if (!memcmp(patchme, ticket_check, sizeof(ticket_check)))
			{
				// write16/uncached poke doesn't work for MEM2
				patchme[4] = 0x23FF; // li r3, 0xFF
				DCFlushRange(patchme+4, 2);
				return 0;
			}
		}
		return -1;
	} else {
		return -2;
	}
}

static void reset_cb(void) {
#ifdef GDBSTUB
	gdb = true;
#else
	should_exit = true;
#endif
}

static void power_cb(void) {
	should_exit = true;
	shutdown = true;
}

void config_language(void) {
	s32 language;
	const void *mo;

	// this is probably wrong, but we don't let the C library do any UTF-8 text
	// processing worth mentioning anyway, and I'm scared of what might happen
	// if I change this :P
	setlocale(LC_CTYPE, "C-ISO-8859-1");

#ifdef FORCE_LANG
	language = FORCE_LANG;
#else
	language = CONF_GetLanguage();
	if (language < 0)
		language = CONF_LANG_ENGLISH;
#endif

	mo = NULL;

	switch (language) {
		case CONF_LANG_ENGLISH:
			mo = NULL;
			break;

		case CONF_LANG_SPANISH:
			mo = spanish_mo;
			break;

		case CONF_LANG_FRENCH:
			mo = french_mo;
			break;

		case CONF_LANG_GERMAN:
			mo = german_mo;
			break;

		case CONF_LANG_DUTCH:
			mo = dutch_mo;
			break;

		case CONF_LANG_ITALIAN:
			mo = italian_mo;
			break;

		case CONF_LANG_JAPANESE:
			if (theme.langs & TLANG_JA) {
				mo = japanese_mo;
			} else {
				gprintf("language ja disabled due to missing theme support\n");
				language = CONF_LANG_ENGLISH;
			}
			break;

		default:
			gprintf("unsupported language %ld, defaulting to English\n",
					language);
			language = CONF_LANG_ENGLISH;
	}

	gprintf("configuring language: %ld with mo file at %p\n", language, mo);
	i18n_set_mo(mo);
}


static void main_pre(void) {
#ifdef DEBUG_APP
	if(usb_isgeckoalive(USBGECKO_CHANNEL))
		CON_EnableGecko(USBGECKO_CHANNEL, 1);

#ifdef GDBSTUB
	DEBUG_Init(GDBSTUB_DEVICE_USB, USBGECKO_CHANNEL);
#endif
#endif

	AUDIO_Init(NULL);
	DSP_Init();
	AUDIO_StopDMA();
	AUDIO_RegisterDMACallback(NULL);

	gfx_init_video();
	PAD_Init();

	SYS_SetResetCallback(reset_cb);
	title_init();
	SYS_SetPowerCallback(power_cb);

	gprintf("installing stub (%lu)\n", stub_bin_size);

#ifdef DEBUG_APP
	if (stub_bin_size > 0x1800)
		gprintf("WARNING: stub too big!\n");
#endif

	memcpy((u32 *) 0x80001800, stub_bin, stub_bin_size);
	DCFlushRange((u32 *) 0x80001800, stub_bin_size);

	*conf_magic = 0;

	gprintf ("startup\n");
	gprintf("IOS Version: IOS%ld %ld.%ld\n",
			IOS_GetVersion(), IOS_GetRevisionMajor(), IOS_GetRevisionMinor());

	ISFS_Initialize();

	WiiDVD_StopMotorAsync();

	gfx_init();
	app_entry_init();
	theme_xml_init();
	theme_init(NULL, 0);
	config_language();
	loader_init();
	controls_init();
	cursors_init();
	font_init();
	widgets_init();
	view_init();
	dialogs_init();
}

static void load_text(void)
{
	text_delete = _("Do you really want to delete this application?");
	text_error_delete = _("Error deleting '%s'");
}

static void refresh_theme(view *v, app_entry *app, u8 *data, u32 data_len) {
	browser_gen_view(BA_REMOVE, NULL);
	view_fade(v, TEX_LAYER_CURSOR + 1, 0, 0, 0, 0, 32, 8);
	font_clear();
	theme_deinit();
	theme_init(data, data_len);
	config_language();
	widgets_theme_reinit();
	bubbles_theme_reinit();
	view_theme_reinit();
	browser_theme_reinit();
	m_main_theme_reinit();
	dialogs_theme_reinit();
	view_fade(v, TEX_LAYER_CURSOR + 1, 0xff, 0xff, 0xff, 0xff, 31, -8);
	browser_gen_view(BA_REFRESH, app);
	load_text();
}

void main_real(void) {
	view *v_last, *v_current, *v_m_main, *v_browser, *v_detail, *v_about;

	u8 fhw;

	u32 bd, bh;
	s8 clicked;
	s16 mm;

#ifdef ENABLE_UPDATES
	s16 update_check = -1;
	bool update_in_progress = false;
	bool update_available = false;
#endif

	app_entry *app_sel;
	bool loading = false;

	loader_result ld_res;
	entry_point ep;
	bool reloced;
	bool ahb_access;
	bool launch_bootmii;

	u64 frame;
	bool exit_about;

	char charbuf[MAXPATHLEN];

	load_text();

	playtime_destroy();

	if (settings_load()) {
		app_entry_set_prefered(settings.device);
		app_entry_set_sort(settings.sort_order);

		if (settings.browse_mode == 1)
			browser_switch_mode();
	}

#ifdef ENABLE_UPDATES
	update_check = 60;
#endif

	app_sel = NULL;
	v_browser = browser_init();
	v_m_main = m_main_init ();
	view_bubbles = true;

#ifdef ENABLE_UPDATES
	http_init ();
#endif

	fhw = font_get_y_spacing(FONT_MEMO);

	should_exit = false;
	shutdown = false;
#ifdef GDBSTUB
	gdb = false;
#endif

	reloced = false;
	ahb_access = false;
	launch_bootmii = false;

	frame = 0;
	exit_about = false;

	v_last = v_current = v_browser;
	v_detail = NULL;
	v_about = NULL;

	view_fade(v_current, TEX_LAYER_CURSOR + 1, 0xff, 0xff, 0xff, 0xff, 31, -8);

	view_enable_cursor (true);

	while (!should_exit) {
#ifdef GDBSTUB
		if (gdb) {
			gprintf("attach gdb now!\n");
			_break();
			gdb = false;
			SYS_SetResetCallback (reset_cb);
		}
#endif
		memstats(false);

		if (v_current == v_browser) {
			switch (app_entry_action()) {
			case AE_ACT_NONE:
				break;

			case AE_ACT_REMOVE:
				app_sel = browser_sel();
				if (app_sel) {
					memset(settings.app_sel, 0, sizeof(settings.app_sel));
					strcpy(settings.app_sel, app_sel->dirname);
				}

				bubble_popall();
				browser_gen_view(BA_REMOVE, NULL);
				app_entries_free();
				memstats(true);
				break;

			case AE_ACT_ADD:
				if (strlen(settings.app_sel) > 0)
					app_sel = app_entry_find(settings.app_sel);
				else
					app_sel = NULL;

				view_show_throbber(false);
				browser_gen_view(BA_ADD, app_sel);
				break;
			}
		}

		if (loading != app_entry_is_loading()) {
			loading = !loading;

			view_show_throbber(loading);
		}

		if (loading)
			view_throbber_tickle();

		if ((frame % 30) == 0)
			app_entry_scan();

		view_plot (v_current, DIALOG_MASK_COLOR, &bd, &bh, NULL);

		frame++;
		if (v_last != v_current) {
			frame = 0;
			v_last = v_current;
		}

		if (bd & PADS_HOME) {
			if (v_current == v_browser) {
				m_main_update ();
				v_current = v_m_main;
				view_set_focus (v_m_main, 0);

				continue;
			} else {
				if (v_current == v_m_main)
					v_current = v_browser;

				continue;
			}
		}

		if (v_current == v_m_main) {
			if (bd & PADS_B) {
				v_current = v_browser;

				continue;
			}

			if (bd & PADS_UP)
				view_set_focus_prev (v_current);

			if (bd & PADS_DOWN)
				view_set_focus_next (v_current);

			if (bd & PADS_A) {
				switch (v_m_main->focus) {
				case 0:
					v_current = v_browser;
					continue;

				case 1:
					v_about = dialog_about (v_m_main);
					v_current = v_about;

					view_enable_cursor (false);

					dialog_fade (v_current, true);

					exit_about = false;

					continue;

				case 2:
					launch_bootmii = true;
					should_exit = true;
					break;

				case 3:
					should_exit = true;
					continue;

				case 4:
					should_exit = true;
					shutdown = true;
					break;
				}
			}

			continue;
		}

		if (v_current == v_browser) {
			widget_set_flag (&v_browser->widgets[2], WF_ENABLED,
								loader_gecko_initialized ());

			if (!loader_tcp_initializing ())
				widget_set_flag (&v_browser->widgets[3], WF_ENABLED,
									loader_tcp_initialized ());

			if (loader_tcp_initializing () && (frame % 20 == 0))
				widget_toggle_flag (&v_browser->widgets[3], WF_ENABLED);

			if (bd & PADS_NET_INIT)
				loader_tcp_init ();

#ifdef ENABLE_UPDATES
			if (loader_tcp_initialized () && (update_check > 0))
				update_check--;

			if (update_check == 0) {
				update_check = -1;
				update_in_progress = update_signal ();
			}

			if (update_in_progress) {
				if (!loading && !update_busy(&update_available))
					update_in_progress = false;
			}

			if (update_available) {
				update_available = false;

				reloced = update_execute (v_current, &ep);
				should_exit = reloced;

				continue;
			}
#endif
			if (bd & PADS_1) {
				dialog_options_result options;
				options = show_options_dialog(v_current);

				if (options.confirmed) {
					app_entry_set_sort(options.sort);
					app_entry_set_device(options.device);
					app_entry_set_prefered(options.device);
				}

				continue;
			}

			if (bd & PADS_2) {
				browser_switch_mode();
				continue;
			}

			if (bd & PADS_DPAD) {
				browser_set_focus(bd);
				continue;
			}

			if (bd & PADS_A) {
				clicked = view_widget_at_ir (v_browser);

				if (clicked == 0) {
					browser_gen_view(BA_PREV, NULL);
					continue;
				}

				if (clicked == 1) {
					browser_gen_view(BA_NEXT, NULL);
					continue;
				}

				if (clicked == 3) {
					loader_tcp_init ();
					continue;
				}

				app_sel = browser_sel();

				if (!app_sel)
					continue;

				v_detail = dialog_app (app_sel, v_browser);
				v_current = v_detail;

				dialog_fade (v_current, true);

				continue;
			}

			if ((bd | bh) & PADS_MINUS)
				browser_gen_view(BA_PREV, NULL);

			if ((bd | bh) & PADS_PLUS)
				browser_gen_view(BA_NEXT, NULL);

			if (loader_handshaked ()) {
				loader_load (&ld_res, v_browser, NULL);

				switch (ld_res.type) {
				case LT_UNKNOWN:
					break;

				case LT_EXECUTABLE:
					if (loader_load_executable(&ep, &ld_res, v_browser)) {
						reloced = true;
						ahb_access = true;
						should_exit = true;
					}

					break;

				case LT_ZIP_APP:
					if (loader_handle_zip_app(&ld_res, v_browser)) {
						app_sel = app_entry_add(ld_res.dirname);

						if (app_sel)
							browser_gen_view(BA_REFRESH, app_sel);
						memstats(true);
					}

					break;

				case LT_ZIP_THEME:
					refresh_theme(v_current, app_sel, ld_res.data,
									ld_res.data_len);
					fhw = font_get_y_spacing(FONT_MEMO);
					memstats(true);
					break;
				}

				continue;
			}

			if (!reloced)
				loader_signal_threads ();

			continue;
		}

		if (v_current == v_detail) {
			if (bd & PADS_LEFT)
				view_set_focus_prev (v_current);

			if (bd & PADS_RIGHT)
				view_set_focus_next (v_current);

			mm = 0;
			if (bd & PADS_UP)
				mm += fhw;

			if (bd & PADS_DOWN)
				mm -= fhw;

			mm += controls_sticky() / 8;

			if (v_current->drag && (v_current->drag_widget == 6)) {
				mm += -v_current->drag_y / 32;
			} else if (bd & PADS_B) {
				dialog_fade (v_current, false);

				v_current = v_browser;
				view_free (v_detail);
				v_detail = NULL;

				continue;
			}

			widget_scroll_memo_deco (&v_detail->widgets[5], mm);

			if ((bd & PADS_A) && v_detail->focus == 10) {
				dialog_fade (v_current, false);

				v_current = v_browser;
				view_free (v_detail);
				v_detail = NULL;

				continue;
			}

			if ((bd & PADS_A) && v_detail->focus == 8) {
				dialog_fade (v_current, false);

				v_current = v_browser;
				view_free (v_detail);
				v_detail = NULL;

				if (show_message (v_current, DLGMT_CONFIRM, DLGB_YESNO,
									text_delete, 1) == 1)
					continue;

				browser_gen_view(BA_REMOVE, NULL);
				if (!manage_run(v_current, app_sel->dirname, NULL, 0, 0)) {
					sprintf(charbuf, text_error_delete, app_sel->dirname);
					show_message(v_current, DLGMT_ERROR, DLGB_OK, charbuf, 0);
				} else {
					app_entry_remove(app_sel);
					app_sel = NULL;
				}
				browser_gen_view(BA_REFRESH, NULL);

				continue;
			}

			if ((bd & PADS_A) && v_detail->focus == 9) {
				dialog_fade (v_current, false);

				v_current = v_browser;

				view_free (v_detail);
				v_detail = NULL;

				loader_load(&ld_res, v_browser, app_sel);

				switch (ld_res.type) {
				case LT_UNKNOWN:
					break;

				case LT_EXECUTABLE:
					if (loader_load_executable(&ep, &ld_res, v_browser)) {
						reloced = true;

						if (app_sel && app_sel->meta)
							ahb_access = app_sel->meta->ahb_access;

						should_exit = true;
					}

					break;

				case LT_ZIP_APP:
					free(ld_res.data);
					break;

				case LT_ZIP_THEME:
					refresh_theme(v_current, app_sel, ld_res.data,
									ld_res.data_len);
					break;
				}

				continue;
			}

			continue;
		}

		if (v_current == v_about) {
			if (bd & (PADS_A | PADS_B))
				exit_about = true;

			if (exit_about) {
				dialog_fade (v_current, false);

				v_current = v_m_main;
				view_free (v_about);
				v_about = NULL;

				view_enable_cursor (true);

				continue;
			}

			if ((frame > 60 * 2) && (frame % 3) == 0)
				widget_scroll_memo (&v_current->widgets[2], -1);
		}
	}

	gprintf ("exiting\n");

	view_enable_cursor (false);

	if (v_current->sub_view)
		dialog_fade (v_current, false);

	view_fade (v_current, TEX_LAYER_CURSOR + 1, 0, 0, 0, 0, 32, 8);

	WiiDVD_ShutDown();
	controls_deinit();

	app_sel = browser_sel();
	if (app_sel) {
		size_t i, s = strlen(app_sel->dirname);
		memset(settings.app_sel, 0, sizeof(settings.app_sel));
		for (i = 0; i < s; ++i)
			settings.app_sel[i] = tolower((int) app_sel->dirname[i]);
	}

	// because the tcp thread is the only one that can block after the exit
	// command is sent, deinit it first. this gives the other threads a chance
	// to do any pending work if the tcp thread does block.
	loader_deinit ();
#ifdef ENABLE_UPDATES
        http_deinit ();
#endif
	app_entry_deinit ();

	gfx_deinit ();
	cursors_deinit ();

	browser_deinit ();
	m_main_deinit ();

	dialogs_deinit ();
	view_deinit ();
	widgets_deinit ();
	font_deinit ();
	theme_deinit();

	settings_save();

	if (launch_bootmii) {
		gprintf ("launching BootMii\n");
		__IOS_LaunchNewIOS(BOOTMII_IOS);
	}

	if (reloced) {
		if (ahb_access) {
			gprintf ("patching IOS for AHB access post-reload...\n");
			int res = patch_ahbprot_reset();
			if (res)
				gprintf ("patch failed (%d)\n", res);
		}

		gprintf ("reloading to IOS%ld...\n", APPS_IOS_VERSION);
		__IOS_LaunchNewIOS(APPS_IOS_VERSION);

		if (ahb_access) {
			gprintf ("reenabling DVD access...\n");
			mask32(0x0D800180, 1<<21, 0);
		}

		gprintf ("branching to %p\n", ep);
		loader_exec (ep);
	}

	if (shutdown) {
		gprintf ("shutting down\n");
		SYS_ResetSystem(SYS_POWEROFF, 0, 0);
	}

	gprintf ("returning to sysmenu\n");
	SYS_ResetSystem (SYS_RETURNTOMENU, 0, 0);
}

int main(int argc, char *argv[]) {
	main_pre();
	main_real();
	gprintf("uh oh\n");
	return 0;
}

