#include <signal.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#include "routine.h"
#include "thread_tool.h"

struct tcb *sleeping_set[THREAD_MAX];

void queue_enq(struct tcb_queue* self, struct tcb* element) {
    debug_printf("QUEUE: enqueueing thread %d\n", element->id);
    self->arr[self->size] = element;
    self->size = (self->size + 1) % THREAD_MAX;
}

struct tcb* queue_deq(struct tcb_queue* self) {
    struct tcb* ret = self->arr[self->head];
    self->head = (self->head + 1) % THREAD_MAX;
    debug_printf("QUEUE: dequeue thread %d\n", ret->id);
    return ret;
}

int queue_empty(struct tcb_queue* self) {
    return self->head == self->size;
}

struct tcb* queue_peek(struct tcb_queue* self) {
    if (queue_empty(self)) return NULL;
    return self->arr[self->head];
}

int sleeping_set_is_empty() {
    for (int i = 0; i < THREAD_MAX; i++) {
        if (sleeping_set[i] != NULL) return 0;
    }
    return 1;
}
void sleeping_set_init() {
    debug_printf("SLEEPING SET: init\n");
    for (int i = 0; i < THREAD_MAX; i++)
        sleeping_set[i] = NULL;
    debug_printf("SLEEPING SET: init done\n");
}
// Prints out the signal you received.
// This function should not return. Instead, jumps to the scheduler.
void sighandler(int signum) {
    debug_printf("[HANDLER]\n");
    if (signum == SIGTSTP) printf("caught SIGTSTP\n");
    else if (signum == SIGALRM) printf("caught SIGALRM\n");

    siglongjmp(sched_buf, HANDLER);
}

// Perfectly setting up your scheduler.
void scheduler() {
    int jmp_from;
    thread_create(idle, 0, NULL); // idle thread
    debug_printf("SCHEDULER: setting sigsetjmp...\n");
    if ((jmp_from = sigsetjmp(sched_buf, 1)) == -1) // use sigsetjmp cuz we always want the same mask in scheduler?
        perror_exit("setjmp sched_buf");
    if (jmp_from == 0) // direct (first) call, init sleeping set
        sleeping_set_init();

    // cancel previous alarms, set new alarm
    alarm(time_slice);
    // Clearing the Pending Signals
    struct sigaction sa, tstp_oact, alrm_oact;
    sa.sa_handler = SIG_IGN;
    sa.sa_flags = 0;
    if (sigaction(SIGTSTP, &sa, &tstp_oact) == -1) perror_exit("sigaction SIGTSTP sa");
    if (sigaction(SIGALRM, &sa, &alrm_oact) == -1) perror_exit("sigaction SIGALRM sa");
    if (sigaction(SIGTSTP, &tstp_oact, NULL) == -1) perror_exit("sigaction SIGTSTP oact");
    if (sigaction(SIGALRM, &alrm_oact, NULL) == -1) perror_exit("sigaction SIGALRM oact");
    debug_printf("SCHEDULER: manage sleeping threads\n");
    // Managing Sleeping Threads
    for (int i = 0; i < THREAD_MAX; i++) {
        if (sleeping_set[i] == NULL) continue;
        // Decrement by time slice if we from HANDLER
        if (jmp_from == HANDLER) {
            sleeping_set[i]->sleeping_time -= time_slice;
            debug_printf("SCHEDULER: sleeping_set[%d]->sleeping_time = %d after decreasing %d\n", i, sleeping_set[i]->sleeping_time, time_slice);
        }

        if (sleeping_set[i]->sleeping_time <= 0) {
            queue_enq(&ready_queue, sleeping_set[i]);
            sleeping_set[i] = NULL;
        }
    }
    debug_printf("SCHEDULER: Handling waiting threads\n");
    // Handling Waiting Threads
    while (!queue_empty(&waiting_queue)) {
        struct tcb* head = queue_peek(&waiting_queue);
        if (head->waiting_for == 0) 
            queue_enq(&ready_queue, queue_deq(&waiting_queue));
        else if (head->waiting_for == 1 && rwlock.write_count == 0)
            queue_enq(&ready_queue, queue_deq(&waiting_queue));
        else if (head->waiting_for == 2 && (rwlock.write_count == 0 && rwlock.read_count == 0))
            queue_enq(&ready_queue, queue_deq(&waiting_queue));
        else break;
    }
    debug_printf("SCHEDULER: Handling prev threads\n");
    // Handling Previously Running Threads
    if (jmp_from == HANDLER && current_thread->id != 0) {
        queue_enq(&ready_queue, current_thread);
    } else if (jmp_from == READ_LOCK || jmp_from == WRITE_LOCK) {
        queue_enq(&waiting_queue, current_thread);
    } else if (jmp_from == THREAD_EXIT) {
        free(current_thread->args);
        free(current_thread);
    }
    debug_printf("SCHEDULER: Selecting next threads\n");
    // Selecting the Next Thread
    if (ready_queue.head != ready_queue.size) {
        debug_printf("SCHEDULER: ready queue not empty, dequeueing...\n");
        // pop first tcb in queue
        current_thread = queue_deq(&ready_queue);
    } else { // ready queue is empty, check sleeping set
        if (!sleeping_set_is_empty()) {
            current_thread = idle_thread;
        } else {
            free(idle_thread);
            return;
        }
    }
    debug_printf("SCHEDULER: next thread: thread %d\n", current_thread->id);
    longjmp(current_thread->env, SCHEDULER);
}

