#include <stdio.h>
#include <string.h>
#include <assert.h>

#include "platform.h"
#include "render/render.h"
#include "enginestate.h"
#include "lib/types.h"
#include "lib/math.h"
#include "gui/gui.h"
#include "debug/logger.h"
#include "debug/log_viewer.h"
#include "gui/layout.h"
#include "stb_image.h"

#include <time.h>
#include <sys/utsname.h>
#include <sys/sysinfo.h>
#include <NetworkManager.h>
#include <libvirt/libvirt.h>
#include <mntent.h>
#include <sys/statvfs.h>
#include <sys/stat.h>


bool process_input(); // Returns true when the program needs to exit
void process_gui();
void app_init();
void app_deinit();

struct App_Data
{
	f64 glib_iteration_t = -1;
	f64  system_sample_t = -1;
	f64 network_sample_t = -1;
	f64    virt_sample_t = -1;
	f64      fs_sample_t = -1;

	f64 glib_iteration_delta = 0.1;
	f64  system_sample_delta = 0.2;
	f64 network_sample_delta = 0.2;
	f64    virt_sample_delta = 0.5;
	f64      fs_sample_delta = 1.0;
};

struct System_Info
{
	char hostname[128];
	char kernel[128];
	time_t time;
	char cached_date_string[128];
	char cached_time_string[128];

	s32 cpus_total;
	s32 cpus_active;

	f32 load[3];
	s64 uptime;

	u64 ram_total;
	u64 ram_used;
	u64 ram_available;
};

struct Network_Device
{
	char name[128];
	NMDeviceType type;
	NMDeviceState state;
};

struct Network_Info
{
	s32 device_count;
	Network_Device devices[32];
};

struct Virt_Domain
{
	char name[128];
	s32 state;

	u32 vcpus;
	f32 cpu_usage;
	u64 prev_cpuTime;

	u64 ram_total;
	u64 ram_used;
	u64 ram_available;
};

struct Virt_Info
{
	s32 domain_count;
	Virt_Domain domains[32];
};

struct FS_Entry
{
	char device[256];
	char directory[256];
	char type[128];
	u64 bytes_total;
	u64 bytes_available;
};

struct FS_Info
{
	s32 fs_count;
	FS_Entry fs[32];
};

System_Info system_info;
Network_Info network_info;
Virt_Info virt_info;
FS_Info fs_info;
App_Data app_data;


u32 seconds_to_duration_text(char *text, f64 seconds, bool show_millis = false)
{
	u32 written = 0;
	u32 days = seconds / (24 * 3600);
	seconds -= days * (24 * 3600);
	u32 hours = seconds / 3600;
	seconds -= hours * 3600;
	u32 minutes = seconds / 60;
	seconds -= minutes * 60;

	if(days)
		written += sprintf(text + written, "%s%dd", (written ? " " : ""), days);
	if(days || hours)
		written += sprintf(text + written, "%s%dh", (written ? " " : ""), hours);
	if(days || hours || minutes)
		written += sprintf(text + written, "%s%dm", (written ? " " : ""), minutes);
	if(days || hours || minutes || seconds)
	{
		if(show_millis)
			written += sprintf(text + written, "%s%.3lfs", (written ? " " : ""), seconds);
		else
			written += sprintf(text + written, "%s%.0lfs", (written ? " " : ""), seconds);
	}

	return written;
}


void load_image(const char *filename, u8 **data, s32 *width, s32 *height, s32 *channels)
{
	stbi_set_flip_vertically_on_load(false);
	*data = stbi_load(filename, width, height, channels, 4);
}


int main(int argc, char *argv[])
{
	bool status;

	LOG_INIT();
	p_init(true);

	LogViewer log_viewer = LogViewer::Init(&global_logger);

	p_window_open();
	p_window_name((char*)"Server Monitor");

	r_init();
	gui_init();

	log_viewer.Print_New_Messages_On_Console();


	app_init();


	f64 start = p_time();
	f64 prev_t = 0;
	while(1)
	{
		// Engine
		engine.time = p_time() - start;
		engine.delta_t = engine.time - prev_t;
		//LOG(LOG_INFO, "Frame time: %.3lf ms    FPS: %.2lf", 1000*delta_t, 1/delta_t);
		r_time_update(engine.time);

		// Input
		bool exit = process_input();
		if(exit)
			break;

		// GUI
		r_framebuffer_select(&r_render_state.framebuffer_SCREEN);
		r_clear({0,0,0,0});
		gui_frame_begin(engine.time);

		process_gui();

		gui_frame_end();
		log_viewer.Print_New_Messages_On_Console();
		r_swap();
		prev_t = engine.time;
	}

	app_deinit();
	gui_deinit();
	p_window_close();

	LOG_DEINIT();
	p_deinit();
	return 0;
}



bool process_input()
{
	// Events
	Event event;
	while(p_next_event(&event))
	{
		gui_handle_event(&event);

		switch(event.type)
		{
			case EVENT_QUIT: {
				return true;
			};
			case EVENT_MOUSE_MOVE:
			{

			} break;
			case EVENT_KEY:
			{
				switch(event.key.key_code)
				{

					default: {
						// Other keys. Put default to suppress warnings
					} break;
				}
			} break;
			case EVENT_RESIZE:
			{
				//LOG(LOG_DEBUG, "New size: %u %u", signal.resize.width, signal.resize.height);
				s32 width  = event.resize.width;
				s32 height = event.resize.height;
				r_size_update(width, height);

				global_gui_state.default_context.width  = width;
				global_gui_state.default_context.height = height;

				engine.gui_scaling = minimum(width, height) * 0.03;
				global_gui_state.default_context.style.font_size = engine.gui_scaling;
				global_gui_state.default_context.style.button_radius = engine.gui_scaling / 10;

			} break;
			case EVENT_FOCUS:
			{
			} break;
			case EVENT_UNFOCUS:
			{
			} break;
		}
	}

	return false;
}




void system_info_window()
{
	Gui_Layout_Grid layout = gui_layout_grid_create_by_divisions(v2{0,0}, v2{40,6}*engine.gui_scaling, 3, 4, 0.2*engine.gui_scaling);
	gui_window_start(Rect{0.1*engine.gui_scaling, 0.1*engine.gui_scaling, layout.window_size.x, layout.window_size.y}, 0xabcdef01);

	// Host date and time
	gui_text_aligned(layout.cell(), system_info.hostname, GUI_ALIGN_LEFT);
	gui_text_aligned(layout.cell(), system_info.cached_time_string, GUI_ALIGN_CENTER);
	gui_text_aligned(layout.cell(), system_info.cached_date_string, GUI_ALIGN_RIGHT);

	// Load, Memory, Uptime
	struct sysinfo sys_info;
	sysinfo(&sys_info);
	char uptime[128] = "Uptime: "; seconds_to_duration_text(uptime + strlen("Uptime: "), sys_info.uptime);

	char load[128]; snprintf(load, 128, "Load: %.2f %.2f %.2f", system_info.load[0], system_info.load[1], system_info.load[2]);

	char processors[128]; snprintf(processors, 128, "CPUs: %d/%d", system_info.cpus_active, system_info.cpus_total);

	char ram[128]; snprintf(ram, 128, "RAM: %.2f/%.2f GiB", system_info.ram_used / (1024.0*1024.0*1024.0), system_info.ram_total / (1024.0*1024.0*1024.0));

	layout.row(2);
	gui_text_aligned(layout.cell(), processors, GUI_ALIGN_LEFT);
	gui_text_aligned(layout.cell(), load, GUI_ALIGN_CENTER);
	gui_text_aligned(layout.cell(), uptime, GUI_ALIGN_RIGHT);
	gui_text_aligned(layout.cell(), ram, GUI_ALIGN_LEFT);


	gui_window_end();
}

void network_window()
{
	Gui_Layout_Grid layout = gui_layout_grid_create_by_divisions(v2{0,0}, v2{40,18}*engine.gui_scaling, 3, 12, 0.2*engine.gui_scaling);
	gui_window_start(Rect{0.1*engine.gui_scaling, 7*engine.gui_scaling, layout.window_size.x, layout.window_size.y}, 0xabcdef02);

	Gui_Context *ctx = &global_gui_state.default_context;

	for(s32 i = 0; i < network_info.device_count; i++)
	{
		Network_Device *device = &network_info.devices[i];

		gui_button(layout.cell(), device->name);

		gui_id_stack_push(ctx, gui_id_from_pointer(ctx, device->name));

		switch(device->type)
		{
			case NM_DEVICE_TYPE_ETHERNET: gui_button(layout.cell(), "ETHERNET"); break;
			case NM_DEVICE_TYPE_WIFI: gui_button(layout.cell(), "WIFI"); break;
			case NM_DEVICE_TYPE_TUN: gui_button(layout.cell(), "TAP or TUN"); break;
			case NM_DEVICE_TYPE_BRIDGE: gui_button(layout.cell(), "BRIDGE"); break;
			case NM_DEVICE_TYPE_VLAN: gui_button(layout.cell(), "VLAN"); break;
			case NM_DEVICE_TYPE_WIREGUARD: gui_button(layout.cell(), "WIREGUARD"); break;
			default: gui_button(layout.cell(), ""); break;
		}

		switch(device->state)
		{
			case NM_DEVICE_STATE_UNKNOWN: gui_button(layout.cell(), "UNKNOWN"); break;
			case NM_DEVICE_STATE_UNMANAGED: gui_button(layout.cell(), "UNMANAGED"); break;
			case NM_DEVICE_STATE_UNAVAILABLE: gui_button(layout.cell(), "UNAVAILABLE"); break;
			case NM_DEVICE_STATE_DISCONNECTED: gui_button(layout.cell(), "DISCONNECTED"); break;
			case NM_DEVICE_STATE_PREPARE: gui_button(layout.cell(), "PREPARE"); break;
			case NM_DEVICE_STATE_CONFIG: gui_button(layout.cell(), "CONFIG"); break;
			case NM_DEVICE_STATE_NEED_AUTH: gui_button(layout.cell(), "NEED_AUTH"); break;
			case NM_DEVICE_STATE_IP_CONFIG: gui_button(layout.cell(), "IP_CONFIG"); break;
			case NM_DEVICE_STATE_IP_CHECK: gui_button(layout.cell(), "IP_CHECK"); break;
			case NM_DEVICE_STATE_SECONDARIES: gui_button(layout.cell(), "SECONDARIES"); break;
			case NM_DEVICE_STATE_ACTIVATED: gui_button(layout.cell(), "ACTIVATED"); break;
			case NM_DEVICE_STATE_DEACTIVATING: gui_button(layout.cell(), "DEACTIVATING"); break;
			case NM_DEVICE_STATE_FAILED: gui_button(layout.cell(), "FAILED"); break;
			default: gui_button(layout.cell(), ""); break;
		}

		gui_id_stack_pop(ctx);
		layout.row();
	}
	gui_window_end();
}

void vm_window()
{
	Gui_Layout_Grid layout = gui_layout_grid_create_by_divisions(v2{0,0}, v2{40,6}*engine.gui_scaling, 4, 4, 0.2*engine.gui_scaling);
	gui_window_start(Rect{0.1*engine.gui_scaling, 26*engine.gui_scaling, layout.window_size.x, layout.window_size.y}, 0xabcdef03);

	Gui_Context *ctx = &global_gui_state.default_context;


	Gui_Style old_style = ctx->style;
	for(s32 i = 0; i < virt_info.domain_count; i++)
	{
		Virt_Domain *domain = &virt_info.domains[i];

		// Name and state
		switch(domain->state)
		{
			case VIR_DOMAIN_NOSTATE:
				ctx->style.button_color = old_style.button_color;
				break;
			case VIR_DOMAIN_RUNNING:
				ctx->style.button_color = v4{0,1,0,1};
				break;
			case VIR_DOMAIN_BLOCKED:
			case VIR_DOMAIN_SHUTOFF:
			case VIR_DOMAIN_CRASHED:
				ctx->style.button_color = v4{1,0,0,1};
				break;
			case VIR_DOMAIN_SHUTDOWN:
				ctx->style.button_color = v4{0,.6,0,1};
				break;
			case VIR_DOMAIN_PAUSED:
			case VIR_DOMAIN_PMSUSPENDED:
				ctx->style.button_color = v4{.7,.7,0,1};
				break;
			default:
				ctx->style.button_color = old_style.button_color;
		}
		gui_button(layout.cell(), domain->name);

		gui_id_stack_push(ctx, gui_id_from_pointer(ctx, domain->name));

		ctx->style = old_style;

		// CPU usage
		char cpu[128]; snprintf(cpu, 128, "%.2f%%", domain->cpu_usage * 100);
		gui_button(layout.cell(), cpu);

		char ram[128]; snprintf(ram, 128, "%.2f GB", domain->ram_total / (1024.0*1024.0*1024.0));
		gui_button(layout.cell(), ram);

		char cpu_count[128]; snprintf(cpu_count, 128, "%hd vCPU", domain->vcpus);
		gui_button(layout.cell(), cpu_count);

		gui_id_stack_pop(ctx);
		layout.row();
	}

	ctx->style = old_style;

	gui_window_end();
}


void fs_window()
{
	Gui_Layout_Grid layout = gui_layout_grid_create_by_divisions(v2{0,0}, v2{16,30}*engine.gui_scaling, 2, 20, 0.2*engine.gui_scaling);
	gui_window_start(Rect{41*engine.gui_scaling, 0.1*engine.gui_scaling, layout.window_size.x, layout.window_size.y}, 0xabcdef04);

	Gui_Context *ctx = &global_gui_state.default_context;


	for(s32 i = 0; i < fs_info.fs_count; i++)
	{
		gui_id_stack_push(ctx, gui_id_from_pointer(ctx, fs_info.fs[i].device));

		gui_button(layout.cell(2), fs_info.fs[i].device);
		gui_button(layout.cell(1), fs_info.fs[i].directory);
		gui_button(layout.cell(1), fs_info.fs[i].type);
		char space[64]; snprintf(space, 64, "%.1f/%.1f GiB", (f32)(fs_info.fs[i].bytes_total - fs_info.fs[i].bytes_available) / (1024*1024*1024), (f32)fs_info.fs[i].bytes_total / (1024*1024*1024));
		char percentage[64]; snprintf(percentage, 64, "%.1f%%", (f32)(fs_info.fs[i].bytes_total - fs_info.fs[i].bytes_available) / fs_info.fs[i].bytes_total * 100);
		gui_button(layout.cell(1), space);
		gui_button(layout.cell(1), percentage);

		gui_id_stack_pop(ctx);
	}

	gui_window_end();
}


NMClient *nmclient;
virConnectPtr virt_connection;

void collect_static_data()
{
	// Hostname
	struct utsname host_info;
	uname(&host_info);

	system_info.hostname[0] = 0;
	if(host_info.nodename[0])
		strncpy(system_info.hostname, host_info.nodename, 128);
	snprintf(system_info.kernel, 128, "%s %s", host_info.sysname, host_info.release);
}

void collect_new_data_if_needed()
{
	if(engine.time - app_data.glib_iteration_t >= app_data.glib_iteration_delta)
	{
		g_main_context_iteration(NULL, false);
		app_data.glib_iteration_t = engine.time;
	}


	if(engine.time - app_data.system_sample_t >= app_data.system_sample_delta)
	{
		// Clock
		system_info.time = time(NULL);
		struct tm *time_info = localtime(&system_info.time);
		strftime(system_info.cached_date_string, 128, "%a %e %b %Y", time_info);
		strftime(system_info.cached_time_string, 128, "%H:%M:%S %Z", time_info);

		// CPU count
		system_info.cpus_total  = get_nprocs();
		system_info.cpus_active = get_nprocs_conf();

		// System info for later
		struct sysinfo sys_info;
		sysinfo(&sys_info);
		// Uptime
		system_info.uptime = sys_info.uptime;
		// Load
		system_info.load;
		for(int i = 0; i < 3; i++)
			system_info.load[i] = round((f32)sys_info.loads[i] / (1 << SI_LOAD_SHIFT) * 100) / 100;

		// Memory
		system_info.ram_total = sys_info.totalram * sys_info.mem_unit;
		system_info.ram_available = (sys_info.freeram + sys_info.bufferram) * sys_info.mem_unit;
		system_info.ram_used = sys_info.totalram * sys_info.mem_unit - system_info.ram_available;


		app_data.system_sample_t = engine.time;
	}


	if(engine.time - app_data.network_sample_t >= app_data.network_sample_delta)
	{
		const GPtrArray *devices = nm_client_get_devices(nmclient);
		network_info.device_count = devices->len;
		for(int i = 0; i < devices->len; i++)
		{
			NMDevice *device = (NMDevice*)devices->pdata[i];

			strncpy(network_info.devices[i].name, nm_device_get_iface(device), 128);
			network_info.devices[i].type  = nm_device_get_device_type(device);
			network_info.devices[i].state = nm_device_get_state(device);
		}


		app_data.network_sample_t = engine.time;
	}


	if(engine.time - app_data.virt_sample_t >= app_data.virt_sample_delta)
	{
		virDomainPtr *domains;
		virt_info.domain_count = virConnectListAllDomains(virt_connection, &domains, 0);

		for(int i = 0; i < virt_info.domain_count; i++)
		{
			Virt_Domain *dom = &virt_info.domains[i];
			// Name
			strncpy(dom->name, virDomainGetName(domains[i]), 128);

			// State
			virDomainInfo info;
			int res = virDomainGetInfo(domains[i], &info);
			dom->state = info.state;

			// CPU
			dom->vcpus = info.nrVirtCpu;

			dom->cpu_usage = (info.cpuTime - dom->prev_cpuTime) / ((engine.time - app_data.virt_sample_t) * 1e9 * dom->vcpus);
			dom->prev_cpuTime = info.cpuTime;

			dom->ram_total = info.memory * 1024; // mem * 1000 or mem * 1024 ??
			dom->ram_used = dom->ram_total;
			dom->ram_available = 0;
		}

		p_free(domains);


		app_data.virt_sample_t = engine.time;
	}


	if(engine.time - app_data.fs_sample_t >= app_data.fs_sample_delta)
	{
		FILE *file = setmntent("/proc/mounts", "r");

		fs_info.fs_count = 0;
		for(struct mntent *ent = getmntent(file); ent != NULL; ent = getmntent(file))
		{
			strncpy(fs_info.fs[fs_info.fs_count].device, ent->mnt_fsname, 256);
			strncpy(fs_info.fs[fs_info.fs_count].directory, ent->mnt_dir, 256);
			strncpy(fs_info.fs[fs_info.fs_count].type, ent->mnt_type, 128);
			fs_info.fs[fs_info.fs_count].bytes_total = 0;
			fs_info.fs[fs_info.fs_count].bytes_available = 0;

			struct stat s;
			if(stat(ent->mnt_fsname, &s) != 0 || !S_ISBLK(s.st_mode))
				continue;

			struct statvfs svfs;
			if(statvfs(ent->mnt_dir, &svfs) == 0)
			{
				fs_info.fs[fs_info.fs_count].bytes_total     = svfs.f_frsize * svfs.f_blocks;
				fs_info.fs[fs_info.fs_count].bytes_available = svfs.f_frsize * svfs.f_bavail;
			}
			fs_info.fs_count++;
		}

		endmntent(file);
	}
}

void process_gui()
{
	collect_new_data_if_needed();


	system_info_window();
	network_window();
	vm_window();
	fs_window();
}

void app_init()
{
	app_data.system_sample_t  = 0;
	app_data.network_sample_t = 0;
	app_data.virt_sample_t    = 0;
	app_data.glib_iteration_t = 0;

	nmclient = nm_client_new(NULL, NULL);
	virt_connection = virConnectOpenReadOnly("qemu:///system");

	collect_static_data();
}

void app_deinit()
{
	virConnectClose(virt_connection);
}
