/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 *   Mupen64plus - util.h                                                  *
 *   Mupen64Plus homepage: https://mupen64plus.org/                        *
 *   Copyright (C) 2012 Mupen64plus development team                       *
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 *   This program is distributed in the hope that it will be useful,       *
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
 *   GNU General Public License for more details.                          *
 *                                                                         *
 *   You should have received a copy of the GNU General Public License     *
 *   along with this program; if not, write to the                         *
 *   Free Software Foundation, Inc.,                                       *
 *   51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.          *
 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

#include "workqueue.h"

#include <SDL.h>
#include <SDL_thread.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>

#include "api/callbacks.h"
#include "api/m64p_types.h"
#include "main/list.h"

#define WORKQUEUE_THREADS 1

struct workqueue_mgmt_globals {
    struct list_head work_queue;
    struct list_head thread_queue;
    struct list_head thread_list;
    SDL_mutex *lock;
};

struct workqueue_thread {
    SDL_Thread *thread;
    SDL_cond *work_avail;
    struct list_head list;
    struct list_head list_mgmt;
};

static struct workqueue_mgmt_globals workqueue_mgmt;

static void workqueue_dismiss(struct work_struct *work)
{
}

static struct work_struct *workqueue_get_work(struct workqueue_thread *thread)
{
    int found = 0;
    struct work_struct *work;

    for (;;) {
        SDL_LockMutex(workqueue_mgmt.lock);
        list_del_init(&thread->list);
        if (!list_empty(&workqueue_mgmt.work_queue)) {
            found = 1;
            work = list_first_entry(&workqueue_mgmt.work_queue, struct work_struct, list);
            list_del_init(&work->list);
        } else {
            list_add(&thread->list, &workqueue_mgmt.thread_queue);
	        SDL_CondWait(thread->work_avail, workqueue_mgmt.lock);
        }
        SDL_UnlockMutex(workqueue_mgmt.lock);

        if (found)
            break;
    }

    return work;
}

static int workqueue_thread_handler(void *data)
{
    struct workqueue_thread *thread = data;
    struct work_struct *work;

    for (;;) {
        work = workqueue_get_work(thread);
        if (work->func == workqueue_dismiss) {
            free(work);
            break;
        }

        work->func(work);
    }

    return 0;
}

int workqueue_init(void)
{
    size_t i;
    struct workqueue_thread *thread;

    memset(&workqueue_mgmt, 0, sizeof(workqueue_mgmt));
    INIT_LIST_HEAD(&workqueue_mgmt.work_queue);
    INIT_LIST_HEAD(&workqueue_mgmt.thread_queue);
    INIT_LIST_HEAD(&workqueue_mgmt.thread_list);

    workqueue_mgmt.lock = SDL_CreateMutex();
    if (!workqueue_mgmt.lock) {
        DebugMessage(M64MSG_ERROR, "Could not create workqueue management");
        return -1;
    }

    SDL_LockMutex(workqueue_mgmt.lock);
    for (i = 0; i < WORKQUEUE_THREADS; i++) {
        thread = malloc(sizeof(*thread));
        if (!thread) {
            DebugMessage(M64MSG_ERROR, "Could not create workqueue thread management data");
            SDL_UnlockMutex(workqueue_mgmt.lock);
            return -1;
        }

        memset(thread, 0, sizeof(*thread));
        list_add(&thread->list_mgmt, &workqueue_mgmt.thread_list);
        INIT_LIST_HEAD(&thread->list);
        thread->work_avail = SDL_CreateCond();
        if (!thread->work_avail) {
            DebugMessage(M64MSG_ERROR, "Could not create workqueue thread work_avail condition");
            SDL_UnlockMutex(workqueue_mgmt.lock);
            return -1;
        }

#if SDL_VERSION_ATLEAST(2,0,0)
        thread->thread = SDL_CreateThread(workqueue_thread_handler, "m64pwq", thread);
#else
        thread->thread = SDL_CreateThread(workqueue_thread_handler, thread);
#endif
        if (!thread->thread) {
            DebugMessage(M64MSG_ERROR, "Could not create workqueue thread handler");
            SDL_UnlockMutex(workqueue_mgmt.lock);
            return -1;
        }
    }
    SDL_UnlockMutex(workqueue_mgmt.lock);

    return 0;
}

void workqueue_shutdown(void)
{
    size_t i;
    int status;
    struct work_struct *work;
    struct workqueue_thread *thread, *safe;

    for (i = 0; i < WORKQUEUE_THREADS; i++) {
        work = malloc(sizeof(*work));
        init_work(work, workqueue_dismiss);
        queue_work(work);
    }

    list_for_each_entry_safe_t(thread, safe, &workqueue_mgmt.thread_list, struct workqueue_thread, list_mgmt) {
        list_del(&thread->list_mgmt);
        SDL_WaitThread(thread->thread, &status);
        SDL_DestroyCond(thread->work_avail);
        free(thread);
    }

    if (!list_empty(&workqueue_mgmt.work_queue))
        DebugMessage(M64MSG_WARNING, "Stopped workqueue with work still pending");

    SDL_DestroyMutex(workqueue_mgmt.lock);
}

int queue_work(struct work_struct *work)
{
    struct workqueue_thread *thread;

    SDL_LockMutex(workqueue_mgmt.lock);
    list_add_tail(&work->list, &workqueue_mgmt.work_queue);
    if (!list_empty(&workqueue_mgmt.thread_queue)) {
        thread = list_first_entry(&workqueue_mgmt.thread_queue, struct workqueue_thread, list);
        list_del_init(&thread->list);

        SDL_CondSignal(thread->work_avail);
    }
    SDL_UnlockMutex(workqueue_mgmt.lock);

    return 0;
}
