#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <X11/Xlib.h>

#define NILL                            INT_MIN
#define LOCKFILE                        "/var/local/dwmblocks/dwmblocks.pid"

#define LENGTH(X)                       (sizeof X / sizeof X[0])

#define DELIMITERLENGTH                 (sizeof delimiter)
#define STATUSLENGTH                    (LENGTH(blocks) * (CMDOUTLENGTH + DELIMITERLENGTH) + 1)

#include "config.h"

_Static_assert(INTERVALs >= 0, "INTERVALs must be greater than or equal to 0");
_Static_assert(INTERVALn >= 0 && INTERVALn <= 999999999, "INTERVALn must be between 0 and 999999999");

static void buttonhandler(int sig, siginfo_t *info, void *ucontext);
static void cleanup(void);
static void setupsignals(void);
static void sighandler(int sig, siginfo_t *si, void *ucontext);
static void statusloop(void);
static void termhandler(int sig);
static void updateblock(Block *block, int sigval);
static void updatestatus(void);
static void writepid(void);

static Block *dirtyblock;
static Display *dpy;
static sigset_t blocksigmask;

void
buttonhandler(int sig, siginfo_t *info, void *ucontext)
{
        sig = info->si_value.sival_int >> 8;
        for (Block *block = blocks; block->pathu; block++)
                if (block->signal == sig)
                        switch (fork()) {
                                case -1:
                                        perror("buttonhandler - fork");
                                        break;
                                case 0:
                                {
                                        char button[] = { '0' + (info->si_value.sival_int & 0xff), '\0' };
                                        char *arg[] = { block->pathc, button, NULL };

                                        setsid();
                                        execv(arg[0], arg);
                                        perror("buttonhandler - child - execv");
                                        _exit(127);
                                }
                        }
}

void
cleanup(void)
{
        unlink(LOCKFILE);
        XStoreName(dpy, DefaultRootWindow(dpy), "");
        XCloseDisplay(dpy);
}

void
setupsignals(void)
{
        struct sigaction sa;

        /* populate blocksigmask and check validity of signals */
        sigemptyset(&blocksigmask);
        sigaddset(&blocksigmask, SIGHUP);
        sigaddset(&blocksigmask, SIGINT);
        sigaddset(&blocksigmask, SIGTERM);
        for (Block *block = blocks; block->pathu; block++) {
                if (block->signal <= 0)
                        continue;
                if (block->signal > SIGRTMAX - SIGRTMIN) {
                        fprintf(stderr, "Error: SIGRTMIN + %d exceeds SIGRTMAX.\n", block->signal);
                        unlink(LOCKFILE);
                        XCloseDisplay(dpy);
                        exit(2);
                }
                sigaddset(&blocksigmask, SIGRTMIN + block->signal);
        }

        /* setup signal handlers */
        /* to handle HUP, INT and TERM */
        sa.sa_flags = SA_RESTART;
        sigemptyset(&sa.sa_mask);
        sa.sa_handler = termhandler;
        sigaction(SIGHUP, &sa, NULL);
        sigaction(SIGINT, &sa, NULL);
        sigaction(SIGTERM, &sa, NULL);

        /* to ignore unused realtime signals */
        // sa.sa_flags = SA_RESTART;
        // sigemptyset(&sa.sa_mask);
        sa.sa_handler = SIG_IGN;
        for (int i = SIGRTMIN + 1; i <= SIGRTMAX; i++)
                sigaction(i, &sa, NULL);

        /* to prevent forked children from becoming zombies */
        sa.sa_flags = SA_NOCLDSTOP | SA_NOCLDWAIT | SA_RESTART;
        // sigemptyset(&sa.sa_mask);
        sa.sa_handler = SIG_IGN;
        sigaction(SIGCHLD, &sa, NULL);

        /* to handle signals generated by dwm on click events */
        sa.sa_flags = SA_RESTART | SA_SIGINFO;
        // sigemptyset(&sa.sa_mask);
        sa.sa_sigaction = buttonhandler;
        sigaction(SIGRTMIN, &sa, NULL);

        /* to handle update signals for individual blocks */
        sa.sa_flags = SA_NODEFER | SA_RESTART | SA_SIGINFO;
        sa.sa_mask = blocksigmask;
        sa.sa_sigaction = sighandler;
        for (Block *block = blocks; block->pathu; block++)
                if (block->signal > 0)
                        sigaction(SIGRTMIN + block->signal, &sa, NULL);
}

void
sighandler(int sig, siginfo_t *info, void *ucontext)
{
        sig -= SIGRTMIN;
        for (Block *block = blocks; block->pathu; block++)
                if (block->signal == sig)
                        updateblock(block, info->si_value.sival_int);
        updatestatus();
}

void
statusloop(void)
{
        int i;
        struct timespec t;

        sigprocmask(SIG_BLOCK, &blocksigmask, NULL);
        for (Block *block = blocks; block->pathu; block++)
                if (block->interval >= 0)
                        updateblock(block, NILL);
        for (i = 1; ; i++) {
                updatestatus();
                sigprocmask(SIG_UNBLOCK, &blocksigmask, NULL);
                t.tv_sec = INTERVALs, t.tv_nsec = INTERVALn;
                while (nanosleep(&t, &t) == -1);
                sigprocmask(SIG_BLOCK, &blocksigmask, NULL);
                for (Block *block = blocks; block->pathu; block++)
                        if (block->interval > 0 && i % block->interval == 0)
                                updateblock(block, NILL);
        }
}

void
termhandler(int sig)
{
        cleanup();
        exit(0);
}

void
updateblock(Block *block, int sigval)
{
        int fd[2];

        if (pipe(fd) == -1) {
                perror("updateblock - pipe");
                cleanup();
                exit(1);
        }
        switch (fork()) {
                case -1:
                        perror("updateblock - fork");
                        close(fd[0]);
                        close(fd[1]);
                        cleanup();
                        exit(1);
                case 0:
                        close(fd[0]);
                        if (fd[1] != STDOUT_FILENO) {
                                if (dup2(fd[1], STDOUT_FILENO) != STDOUT_FILENO) {
                                        perror("updateblock - child - dup2");
                                        close(fd[1]);
                                        exit(1);
                                }
                                close(fd[1]);
                        }
                        if (sigval == NILL) {
                                char *arg[] = { block->pathu, NULL };

                                execv(arg[0], arg);
                        } else {
                                char buf[12];
                                char *arg[] = { block->pathu, buf, NULL };

                                snprintf(buf, sizeof buf, "%d", sigval);
                                execv(arg[0], arg);
                        }
                        perror("updateblock - child - execv");
                        _exit(127);
                default:
                {
                        size_t trd = 0;
                        ssize_t rd;

                        close(fd[1]);
                        do
                                rd = read(fd[0], block->curtext + trd, CMDOUTLENGTH - trd);
                        while (rd > 0 && (trd += rd) < CMDOUTLENGTH);
                        if (rd == -1) {
                                perror("updateblock - read");
                                close(fd[0]);
                                cleanup();
                                exit(1);
                        }
                        close(fd[0]);

                        block->curtext[trd] = '\0';
                        if (memcmp(block->curtext, block->prvtext, trd + 1) != 0) {
                                memcpy(block->prvtext, block->curtext, trd + 1);
                                if (!dirtyblock || block < dirtyblock)
                                        dirtyblock = block;
                        }
                        if (trd == 0)
                                block->length = 0;
                        else {
                                if (block->curtext[trd - 1] == '\n')
                                        trd--;
                                if (block->pathc)
                                        block->curtext[trd++] = block->signal;
                                memcpy(block->curtext + trd, delimiter, DELIMITERLENGTH);
                                block->length = trd + DELIMITERLENGTH;
                        }
                }
        }
}

void
updatestatus(void)
{
        static char statustext[STATUSLENGTH];
        char *s = statustext;
        Block *block;

        if (!dirtyblock)
                return;
        for (block = blocks; block < dirtyblock; block++)
                s += block->length;
        for (; block->pathu; block++) {
                memcpy(s, block->curtext, block->length);
                s += block->length;
        }
        s[s == statustext ? 0 : -DELIMITERLENGTH] = '\0';
        dirtyblock = NULL;

        XStoreName(dpy, DefaultRootWindow(dpy), statustext);
        XSync(dpy, False);
}

void
writepid(void)
{
        int fd;
        struct flock fl;

        if ((fd = open(LOCKFILE, O_RDWR | O_CREAT | O_CLOEXEC,
                       S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) {
                perror("writepid - open");
                exit(1);
        }
        fl.l_type = F_WRLCK;
        fl.l_whence = SEEK_SET;
        fl.l_start = 0;
        fl.l_len = 0;
        if (fcntl(fd, F_SETLK, &fl) == -1) {
                if (errno == EACCES || errno == EAGAIN) {
                        fputs("Error: another instance of dwmblocks is already running.\n", stderr);
                        exit(2);
                }
                perror("writepid - fcntl");
                close(fd);
                exit(1);
        }
        if (ftruncate(fd, 0) == -1) {
                perror("writepid - ftruncate");
                close(fd);
                exit(1);
        }
        if (dprintf(fd, "%ld", (long)getpid()) < 0) {
                perror("writepid - dprintf");
                close(fd);
                exit(1);
        }
}

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

        writepid();
        if (!(dpy = XOpenDisplay(NULL))) {
                fputs("Error: could not open display.\n", stderr);
                unlink(LOCKFILE);
                return 1;
        }
        xfd = ConnectionNumber(dpy);
        fcntl(xfd, F_SETFD, fcntl(xfd, F_GETFD) | FD_CLOEXEC);
        setupsignals();
        statusloop();
        cleanup();
        return 0;
}