#include <dirent.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <time.h>
#include <X11/keysym.h>
#include <X11/XF86keysym.h>
#include <X11/XKBlib.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <sys/ioctl.h>
#include <sys/time.h>
#include <sys/types.h>
#include <linux/rtc.h>

// Types
enum State {
	StateNoInput,		 // Screen on / input lock
	StateNoInputNoScreen, // Screen off / input lock
	StateSuspend,		 // Deep sleep
	StateSuspendPending,  // Suspend 'woken up', must leave state in <5s, or kicks to StateSuspend
	StateDead			 // Exit the appliation
};
enum Color {
	Red,
	Blue,
	Purple,
	Off
};

// Fn declarations
int checkrtcwake();
void configuresuspendsettingsandwakeupsources();
time_t convert_rtc_time(struct rtc_time * rtc);
void die(const char *err, ...);
int getoldbrightness();
void init_rtc();
void lockscreen(Display *dpy, int screen, int blank);
void unblankscreen();
void blankscreen();
void readinputloop(Display *dpy, int screen);
int presuspend();
void postwake();
void setpineled(enum Color c);
int setup_rtc_wakeup();
void sigterm();
void syncstate();
void usage();
void writefile(char *filepath, char *str);

// Variables
Display *dpy;
Window root;
enum State state = StateNoInput;
int suspendtimeouts = 35;
int suspendpendingsceenon = 0;
int suspendpendingtimeouts = 0;
KeySym lastkeysym = XK_Cancel;
int lastkeyn = 0;
char oldbrightness[10] = "200";
char * brightnessfile = "/sys/devices/platform/backlight/backlight/backlight/brightness";
char * powerstatefile = "/sys/power/state";
int rtc_fd = 0; //file descriptor
time_t wakeinterval = 0; //wake every x seconds
time_t waketime = 0; //next wakeup time according to the RTC clock
int slept = 0; //indicates whether the process has slept (crust) or not
int blanked = 0; //indicated whether the display blanked or not

#define RTC_DEVICE	  "/dev/rtc0"

time_t
convert_rtc_time(struct rtc_time * rtc) {
	struct tm		 tm;
	memset(&tm, 0, sizeof tm);
	tm.tm_sec = rtc->tm_sec;
	tm.tm_min = rtc->tm_min;
	tm.tm_hour = rtc->tm_hour;
	tm.tm_mday = rtc->tm_mday;
	tm.tm_mon = rtc->tm_mon;
	tm.tm_year = rtc->tm_year;
	tm.tm_isdst = -1;  /* assume the system knows better than the RTC */
	return mktime(&tm);
}

int setup_rtc_wakeup() {
	//(code adapted from util-linux's rtcwake)
	struct tm		 *tm;
	struct rtc_wkalrm	wake;
	struct rtc_time now_rtc;

	if (ioctl(rtc_fd, RTC_RD_TIME, &now_rtc) < 0) {
		fprintf(stderr, "Error reading rtc time\n");
	}
	const time_t now = convert_rtc_time(&now_rtc);
	waketime = now + wakeinterval;

	tm = localtime(&waketime);

	wake.time.tm_sec = tm->tm_sec;
	wake.time.tm_min = tm->tm_min;
	wake.time.tm_hour = tm->tm_hour;
	wake.time.tm_mday = tm->tm_mday;
	wake.time.tm_mon = tm->tm_mon;
	wake.time.tm_year = tm->tm_year;
	/* wday, yday, and isdst fields are unused by Linux */
	wake.time.tm_wday = -1;
	wake.time.tm_yday = -1;
	wake.time.tm_isdst = -1;

	fprintf(stderr, "Setting RTC wakeup to %ld: (UTC) %s", waketime, asctime(tm));

	if (ioctl(rtc_fd, RTC_ALM_SET, &wake.time) < 0) {
		fprintf(stderr, "error setting rtc alarm\n");
		return -1;
	}
	if (ioctl(rtc_fd, RTC_AIE_ON, 0) < 0) {
		fprintf(stderr, "error enabling rtc alarm\n");
		return -1;
	}
	return 0;
}

void
configuresuspendsettingsandwakeupsources()
{
	// Disable all wakeup sources
	struct dirent *wakeupsource;
	char wakeuppath[100];
	DIR *wakeupsources = opendir("/sys/class/wakeup");
	if (wakeupsources == NULL)
		die("Couldn't open directory /sys/class/wakeup\n");
	while ((wakeupsource = readdir(wakeupsources)) != NULL) {
		sprintf(
			wakeuppath,
			"/sys/class/wakeup/%.50s/device/power/wakeup",
			wakeupsource->d_name
		);
		fprintf(stderr, "Disabling wakeup source: %s", wakeupsource->d_name);
		writefile(wakeuppath, "disabled");
		fprintf(stderr, ".. ok\n");
	}
	closedir(wakeupsources);

	// Enable powerbutton wakeup source
	fprintf(stderr, "Enable powerbutton wakeup source\n");
	writefile(
		"/sys/devices/platform/soc/1f03400.rsb/sunxi-rsb-3a3/axp221-pek/power/wakeup",
		"enabled"
	);

	// Enable IRQ wakeup source (incoming call) 5.10
	fprintf(stderr, "Enable 5.10 IRQ wakeup source\n");
	writefile(
		"/sys/devices/platform/gpio-keys/power/wakeup",
		"enabled"
	 );

	 // Enable IRQ wakeup source (incoming call) 5.9 (no longer exists in 5.10)
	fprintf(stderr, "Enable 5.9 IRQ wakeup source\n");
	writefile(
		"/sys/devices/platform/soc/1c28c00.serial/serial1/serial1-0/power/wakeup",
		"enabled"
	 );

	// Enable rtc wakeup source
	fprintf(stderr, "Enable rtc wakeup source\n");
	writefile(
		"/sys/devices/platform/soc/1f00000.rtc/power/wakeup",
		"enabled"
	);

	//set RTC wake
	if (wakeinterval > 0) setup_rtc_wakeup();

	// E.g. make sure we're using CRUST
	fprintf(stderr, "Flip mem_sleep setting to use crust\n");
	writefile("/sys/power/mem_sleep", "deep");

}

void
die(const char *err, ...)
{
	fprintf(stderr, "Screenlock error: %s\n", err);
	state = StateDead;
	syncstate();
	exit(1);
}

void
sigterm()
{
	state = StateDead;
	syncstate();
	if (wakeinterval) {
		ioctl(rtc_fd, RTC_AIE_OFF, 0);
		close(rtc_fd);
	}
	fprintf(stderr, "Screenlock terminating on signal\n");
	exit(0);
}

int
getoldbrightness() {
	char * buffer = 0;
	long length;
	FILE * f = fopen(brightnessfile, "r");
	if (f) {
		fseek(f, 0, SEEK_END);
		length = ftell(f);
		fseek(f, 0, SEEK_SET);
		buffer = malloc(length);
		if (buffer) {
			fread(buffer, 1, length, f);
		}
		fclose(f);
	}
	if (buffer) {
		sprintf(oldbrightness, "%d", atoi(buffer));
	}
}


void
lockscreen(Display *dpy, int screen, int blank)
{
	// Loosely derived from suckless' slock's lockscreen binding logic but
	// alot more coarse, intentionally so can be triggered while grab_key
	// for dwm multikey path already holding..
	int i, ptgrab, kbgrab;
	root = RootWindow(dpy, screen);
	if (blank == 1) {
		blankscreen();
	}
	for (i = 0, ptgrab = kbgrab = -1; i < 9999999; i++) {
		if (ptgrab != GrabSuccess) {
			ptgrab = XGrabPointer(dpy, root, False,
				ButtonPressMask | ButtonReleaseMask |
				PointerMotionMask, GrabModeAsync,
				GrabModeAsync, None, None, CurrentTime);
		}
		if (kbgrab != GrabSuccess) {
			kbgrab = XGrabKeyboard(dpy, root, True,
				GrabModeAsync, GrabModeAsync, CurrentTime);
		}
		if (ptgrab == GrabSuccess && kbgrab == GrabSuccess) {
			XSelectInput(dpy, root, SubstructureNotifyMask);
			return;
		}
		usleep(100000);
	}
}

void
blankscreen()
{
	if (!blanked) {
		system("xset dpms force off");
		blanked = 1;
	}
}

void
unblankscreen()
{
	if (blanked) {
		system("xset dpms force on");
		blanked = 0;
	}
}

void
readinputloop(Display *dpy, int screen) {
	KeySym keysym;
	XEvent ev;
	char buf[32];
	fd_set fdset;
	int xfd;
	int selectresult;
	struct timeval xeventtimeout = {1, 0};
	xfd = ConnectionNumber(dpy);

	for (;;) {
		FD_ZERO(&fdset);
		FD_SET(xfd, &fdset);
		if (state == StateSuspendPending)
			selectresult = select(FD_SETSIZE, &fdset, NULL, NULL, &xeventtimeout);
		else
			selectresult = select(FD_SETSIZE, &fdset, NULL, NULL, NULL);

		if (FD_ISSET(xfd, &fdset) && XPending(dpy)) {
			XNextEvent(dpy, &ev);
			if (ev.type == KeyRelease) {
				XLookupString(&ev.xkey, buf, sizeof(buf), &keysym, 0);
				if (lastkeysym == keysym) {
					lastkeyn++;
				} else {
					lastkeysym = keysym;
					lastkeyn = 1;
				}

				if (lastkeyn < 3)
					continue;

				lastkeyn = 0;
				lastkeysym = XK_Cancel;
				if (slept) postwake();
				switch (keysym) {
					case XF86XK_AudioRaiseVolume:
						suspendpendingsceenon = state == StateNoInput;
						suspendpendingtimeouts = 0;
						state = StateSuspend;
						break;
					case XF86XK_AudioLowerVolume:
						if (state == StateNoInput) state = StateNoInputNoScreen;
						else if (state == StateNoInputNoScreen) state = StateNoInput;
						else if (state == StateSuspendPending && suspendpendingsceenon) state = StateNoInputNoScreen;
						else state = StateNoInput;
						break;
					case XF86XK_PowerOff:
						waketime = 0;
						state = StateDead;
						break;
				}
				syncstate();
			}
		} else if (state == StateSuspendPending) {
			suspendpendingtimeouts++;
			// # E.g. after suspendtimeouts seconds kick back into suspend
			if (suspendpendingtimeouts > suspendtimeouts) state = StateSuspend;
			syncstate();
		}


		if (state == StateDead) break;
	}
}

void
setpineled(enum Color c)
{
	if (c == Red) {
		writefile("/sys/class/leds/red:indicator/brightness", "1");
		writefile("/sys/class/leds/blue:indicator/brightness", "0");
	} else if (c == Blue) {
		writefile("/sys/class/leds/red:indicator/brightness", "0");
		writefile("/sys/class/leds/blue:indicator/brightness", "1");
	} else if (c == Purple) {
		writefile("/sys/class/leds/red:indicator/brightness", "1");
		writefile("/sys/class/leds/blue:indicator/brightness", "1");
	} else if (c == Off) {
		writefile("/sys/class/leds/red:indicator/brightness", "0");
		writefile("/sys/class/leds/blue:indicator/brightness", "0");
	}
}

int
presuspend() {
	//called prior to suspension, a non-zero return value cancels suspension
	return system("sxmo_presuspend.sh");
}

void
postwake() {
	//called after fully waking up (not used for temporary rtc wakeups)
	system("sxmo_postwake.sh");
	slept = 0;
}

int
checkrtcwake()
{
	struct rtc_time now;
	if (ioctl(rtc_fd, RTC_RD_TIME, &now) < 0) {
		fprintf(stderr, "Error reading rtc time\n");
		return -1;
	}

	const long int timediff = convert_rtc_time(&now) - waketime;
	fprintf(stderr, "Checking rtc wake? timediff=%ld\n", timediff);
	if (timediff >= 0 && timediff <= 3) {
		fprintf(stderr, "Calling RTC wake script\n");
		setpineled(Blue);
		return system("sxmo_rtcwake.sh");
	}
	return 0;
}

void
syncstate()
{
	int rtcresult;
	if (state == StateSuspend) {
		if (presuspend() != 0) {
			state = StateDead;
		} else {
			fprintf(stderr, "Screenlock entering suspend state (pred mode)\n");
			writefile(brightnessfile, "0");
			blankscreen();
			slept = 1;
			setpineled(Red);
			configuresuspendsettingsandwakeupsources();
			writefile(powerstatefile, "mem");
			//---- program blocks here due to sleep ----- //
			// Just woke up again
			fprintf(stderr, "Screenlock woke up\n");
			fprintf(stderr, "Resetting usb connection to the modem\n");
			writefile("/sys/bus/usb/drivers/usb/unbind", "3-1");
			writefile("/sys/bus/usb/drivers/usb/bind", "3-1");
			fprintf(stderr, "Lower scan interval for quicker reconnection to wireless network\n");
			writefile("/sys/module/8723cs/parameters/rtw_scan_interval_thr", "1200"); //ms
			//^-- this will be undone again by a networkmanager hook after connection has been established
			//    or by a delayed script if no connection can be established after a while (to conserve battery)
			if (waketime > 0) {
				rtcresult = checkrtcwake();
			} else {
				rtcresult = 0;
			}
			if (rtcresult == 0) {
				state = StateSuspendPending;
				suspendpendingtimeouts = 0;
			} else {
				postwake();
				state = StateDead;
			}
		}
		syncstate();
	} else if (state == StateNoInput) {
		fprintf(stderr, "Screenlock in no Input state (blue mode)\n");
		setpineled(Blue);
		unblankscreen();
		writefile(brightnessfile, oldbrightness);
	} else if (state == StateNoInputNoScreen) {
		fprintf(stderr, "Screenlock in no screen state (purple mode)\n");
		setpineled(Purple);
		writefile(brightnessfile, "0");
		blankscreen();
	} else if (state == StateSuspendPending) {
		fprintf(stderr, "Screenlock is pending suspension\n");
		if (suspendpendingsceenon) unblankscreen();
		writefile(brightnessfile, suspendpendingsceenon ? oldbrightness : "0");
		if (!suspendpendingsceenon) blankscreen();
		setpineled(Off);
		usleep(1000 * 100);
		setpineled(suspendpendingsceenon ? Blue : Purple);
	} else if (state == StateDead) {
		unblankscreen();
		writefile(brightnessfile, oldbrightness);
		setpineled(Off);
	}
}




void
writefile(char *filepath, char *str)
{
	int f;
	f = open(filepath, O_WRONLY);
	if (f != -1) {
		write(f, str, strlen(str));
		close(f);
	} else {
		fprintf(stderr, "Couldn't open filepath <%s>\n", filepath);
	}
}

void usage() {
	fprintf(stderr, "Usage: sxmo_screenlock [--screen-off] [--suspend] [--wake-interval n] [--setuid]\n");
}


void init_rtc() {
	rtc_fd = open(RTC_DEVICE, O_RDONLY);
	if (rtc_fd < 0) {
		fprintf(stderr, "Error opening rtc device: %d\n", rtc_fd);
		die("Unable to open rtc device");
		exit(EXIT_FAILURE);
	}
}

int
main(int argc, char **argv) {
	int screen;
	int i;
	enum State target = StateNoInput;

	signal(SIGTERM, sigterm);

	const char* suspendtimeouts_str = getenv("SXMO_SUSPENDTIMEOUTS");
	if (suspendtimeouts_str != NULL) suspendtimeouts = atoi(suspendtimeouts_str);

	const char* rtcwakeinterval = getenv("SXMO_RTCWAKEINTERVAL");
	if (rtcwakeinterval != NULL) wakeinterval = atoi(rtcwakeinterval);

	const char* screen_off = getenv("SXMO_LOCK_SCREEN_OFF");
	if (screen_off != NULL && atoi(screen_off)) target = StateNoInputNoScreen;

	const char* suspend = getenv("SXMO_LOCK_SUSPEND");
	if (suspend != NULL && atoi(suspend)) target = StateSuspend;

	//parse command line arguments
	for (i = 1; i < argc; i++) {
		if(!strcmp(argv[i], "-h")) {
			usage();
			return 0;
		} else if(!strcmp(argv[i], "--screen-off")) {
			target = StateNoInputNoScreen;
		} else if(!strcmp(argv[i], "--suspend")) {
			target = StateSuspend;
		} else if(!strcmp(argv[i], "--wake-interval")) {
			wakeinterval = (time_t) atoi(argv[++i]);
		} else if(!strcmp(argv[i], "--setuid")) {
			if (setuid(0))
				die("setuid(0) failed");
		} else {
			fprintf(stderr, "Invalid argument: %s\n", argv[i]);
			return 2;
		}
	}

	if (!(dpy = XOpenDisplay(NULL)))
		die("Cannot open display");

	getoldbrightness();

	if (wakeinterval) init_rtc();

	fprintf(stderr, "Screenlock starting\n");

	XkbSetDetectableAutoRepeat(dpy, True, NULL);
	screen = XDefaultScreen(dpy);
	XSync(dpy, 0);
	syncstate();
	lockscreen(dpy, screen, target == StateNoInputNoScreen || target == StateSuspend);
	if ((target == StateNoInputNoScreen) || (target == StateSuspend)) {
		state = StateNoInputNoScreen;
		syncstate();
	}
	if (target == StateSuspend) {
		state = StateSuspend;
		syncstate();
	}
	readinputloop(dpy, screen);
	if (wakeinterval) {
		ioctl(rtc_fd, RTC_AIE_OFF, 0);
		close(rtc_fd);
	}
	fprintf(stderr, "Screenlock terminating normally\n");
	return 0;
}