/**
 * @file NCDUdevManager.c
 * @author Ambroz Bizjak <ambrop7@gmail.com>
 * 
 * @section LICENSE
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of the author nor the
 *    names of its contributors may be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include <stdlib.h>
#include <string.h>

#include <misc/offset.h>
#include <base/BLog.h>

#include <udevmonitor/NCDUdevManager.h>

#include <generated/blog_channel_NCDUdevManager.h>

#define RESTART_TIMER_TIME 5000

static int event_to_map (NCDUdevMonitor *monitor, BStringMap *out_map);
static void free_event (NCDUdevClient *o, struct NCDUdevClient_event *e);
static void queue_event (NCDUdevManager *o, NCDUdevMonitor *monitor, NCDUdevClient *client);
static void queue_mapless_event (NCDUdevManager *o, const char *devpath, NCDUdevClient *client);
static void process_event (NCDUdevManager *o, NCDUdevMonitor *monitor);
static void try_monitor (NCDUdevManager *o);
static void reset_monitor (NCDUdevManager *o);
static void timer_handler (NCDUdevManager *o);
static void monitor_handler_event (NCDUdevManager *o);
static void monitor_handler_error (NCDUdevManager *o, int is_error);
static void info_monitor_handler_event (NCDUdevManager *o);
static void info_monitor_handler_error (NCDUdevManager *o, int is_error);
static void next_job_handler (NCDUdevClient *o);

static int event_to_map (NCDUdevMonitor *monitor, BStringMap *out_map)
{
    NCDUdevMonitor_AssertReady(monitor);
    
    // init map
    BStringMap_Init(out_map);
    
    // insert properties to map
    int num_properties = NCDUdevMonitor_GetNumProperties(monitor);
    for (int i = 0; i < num_properties; i++) {
        const char *name;
        const char *value;
        NCDUdevMonitor_GetProperty(monitor, i, &name, &value);
        
        if (!BStringMap_Set(out_map, name, value)) {
            BLog(BLOG_ERROR, "BStringMap_Set failed");
            goto fail1;
        }
    }
    
    return 1;
    
fail1:
    BStringMap_Free(out_map);
    return 0;
}

static void free_event (NCDUdevClient *o, struct NCDUdevClient_event *e)
{
    // remove from events list
    LinkedList1_Remove(&o->events_list, &e->events_list_node);
    
    // free map
    if (e->have_map) {
        BStringMap_Free(&e->map);
    }
    
    // free devpath
    free(e->devpath);
    
    // free structure
    free(e);
}

static void queue_event (NCDUdevManager *o, NCDUdevMonitor *monitor, NCDUdevClient *client)
{
    NCDUdevMonitor_AssertReady(monitor);
    
    // alloc event
    struct NCDUdevClient_event *e = malloc(sizeof(*e));
    if (!e) {
        BLog(BLOG_ERROR, "malloc failed");
        goto fail0;
    }
    
    // build map
    if (!event_to_map(monitor, &e->map)) {
        goto fail1;
    }
    
    // set have map
    e->have_map = 1;
    
    // get devpath
    const char *devpath = BStringMap_Get(&e->map, "DEVPATH");
    if (!devpath) {
        BLog(BLOG_ERROR, "DEVPATH missing");
        goto fail2;
    }
    
    // copy devpath
    if (!(e->devpath = strdup(devpath))) {
        BLog(BLOG_ERROR, "strdup failed");
        goto fail2;
    }
    
    // insert to client's events list
    LinkedList1_Append(&client->events_list, &e->events_list_node);
    
    // if client is running, set next job
    if (client->running) {
        BPending_Set(&client->next_job);
    }
    
    return;
    
fail2:
    BStringMap_Free(&e->map);
fail1:
    free(e);
fail0:
    return;
}

static void queue_mapless_event (NCDUdevManager *o, const char *devpath, NCDUdevClient *client)
{
    // alloc event
    struct NCDUdevClient_event *e = malloc(sizeof(*e));
    if (!e) {
        BLog(BLOG_ERROR, "malloc failed");
        goto fail0;
    }
    
    // set have no map
    e->have_map = 0;
    
    // copy devpath
    if (!(e->devpath = strdup(devpath))) {
        BLog(BLOG_ERROR, "strdup failed");
        goto fail1;
    }
    
    // insert to client's events list
    LinkedList1_Append(&client->events_list, &e->events_list_node);
    
    // if client is running, set next job
    if (client->running) {
        BPending_Set(&client->next_job);
    }
    
    return;
    
fail1:
    free(e);
fail0:
    return;
}

static void process_event (NCDUdevManager *o, NCDUdevMonitor *monitor)
{
    NCDUdevMonitor_AssertReady(monitor);
    
    // build map from event
    BStringMap map;
    if (!event_to_map(monitor, &map)) {
        BLog(BLOG_ERROR, "failed to build map");
        return;
    }
    
    // pass event to cache
    if (!NCDUdevCache_Event(&o->cache, map)) {
        BLog(BLOG_ERROR, "failed to cache");
        BStringMap_Free(&map);
        return;
    }
    
    // queue event to clients
    LinkedList1Node *list_node = LinkedList1_GetFirst(&o->clients_list);
    while (list_node) {
        NCDUdevClient *client = UPPER_OBJECT(list_node, NCDUdevClient, clients_list_node);
        queue_event(o, monitor, client);
        list_node = LinkedList1Node_Next(list_node);
    }
}

static void try_monitor (NCDUdevManager *o)
{
    ASSERT(!o->have_monitor)
    ASSERT(!BTimer_IsRunning(&o->restart_timer))
    
    int mode = (o->no_udev ? NCDUDEVMONITOR_MODE_MONITOR_KERNEL : NCDUDEVMONITOR_MODE_MONITOR_UDEV);
    
    // init monitor
    if (!NCDUdevMonitor_Init(&o->monitor, o->reactor, o->manager, mode, o,
        (NCDUdevMonitor_handler_event)monitor_handler_event,
        (NCDUdevMonitor_handler_error)monitor_handler_error
    )) {
        BLog(BLOG_ERROR, "NCDUdevMonitor_Init failed");
        
        // set restart timer
        BReactor_SetTimer(o->reactor, &o->restart_timer);
        return;
    }
    
    // set have monitor
    o->have_monitor = 1;
    
    // set not have info monitor
    o->have_info_monitor = 0;
}

static void reset_monitor (NCDUdevManager *o)
{
    ASSERT(o->have_monitor)
    ASSERT(!o->have_info_monitor)
    ASSERT(!BTimer_IsRunning(&o->restart_timer))
    
    // free monitor
    NCDUdevMonitor_Free(&o->monitor);
    
    // set have no monitor
    o->have_monitor = 0;
    
    // set restart timer
    BReactor_SetTimer(o->reactor, &o->restart_timer);
}

static void timer_handler (NCDUdevManager *o)
{
    DebugObject_Access(&o->d_obj);
    ASSERT(!o->have_monitor)
    
    // try again
    try_monitor(o);
}

static void monitor_handler_event (NCDUdevManager *o)
{
    DebugObject_Access(&o->d_obj);
    ASSERT(o->have_monitor)
    ASSERT(!o->have_info_monitor)
    ASSERT(!BTimer_IsRunning(&o->restart_timer))
    
    if (NCDUdevMonitor_IsReadyEvent(&o->monitor)) {
        BLog(BLOG_INFO, "monitor ready");
        
        // init info monitor
        if (!NCDUdevMonitor_Init(&o->info_monitor, o->reactor, o->manager, NCDUDEVMONITOR_MODE_INFO, o,
            (NCDUdevMonitor_handler_event)info_monitor_handler_event,
            (NCDUdevMonitor_handler_error)info_monitor_handler_error
        )) {
            BLog(BLOG_ERROR, "NCDUdevMonitor_Init failed");
            reset_monitor(o);
            return;
        }
        
        // set have info monitor
        o->have_info_monitor = 1;
        
        // start cache cleanup
        NCDUdevCache_StartClean(&o->cache);
        
        // hold processing monitor events until info monitor is done
        return;
    }
    
    // accept event
    NCDUdevMonitor_Done(&o->monitor);
    
    // process event
    process_event(o, &o->monitor);
}

static void monitor_handler_error (NCDUdevManager *o, int is_error)
{
    DebugObject_Access(&o->d_obj);
    ASSERT(o->have_monitor)
    ASSERT(!BTimer_IsRunning(&o->restart_timer))
    
    BLog(BLOG_ERROR, "monitor error");
    
    if (o->have_info_monitor) {
        // free info monitor
        NCDUdevMonitor_Free(&o->info_monitor);
        
        // set have no info monitor
        o->have_info_monitor = 0;
    }
    
    // reset monitor
    reset_monitor(o);
}

static void info_monitor_handler_event (NCDUdevManager *o)
{
    DebugObject_Access(&o->d_obj);
    ASSERT(o->have_monitor)
    ASSERT(o->have_info_monitor)
    ASSERT(!BTimer_IsRunning(&o->restart_timer))
    
    // accept event
    NCDUdevMonitor_Done(&o->info_monitor);
    
    // process event
    process_event(o, &o->info_monitor);
}

static void info_monitor_handler_error (NCDUdevManager *o, int is_error)
{
    DebugObject_Access(&o->d_obj);
    ASSERT(o->have_monitor)
    ASSERT(o->have_info_monitor)
    ASSERT(!BTimer_IsRunning(&o->restart_timer))
    
    if (is_error) {
        BLog(BLOG_ERROR, "info monitor error");
    } else {
        BLog(BLOG_INFO, "info monitor finished");
    }
    
    // free info monitor
    NCDUdevMonitor_Free(&o->info_monitor);
    
    // set have no info monitor
    o->have_info_monitor = 0;
    
    if (is_error) {
        // reset monitor
        reset_monitor(o);
    } else {
        // continue processing monitor events
        NCDUdevMonitor_Done(&o->monitor);
        
        // finish cache cleanup
        NCDUdevCache_FinishClean(&o->cache);
        
        // collect cleaned devices
        BStringMap map;
        while (NCDUdevCache_GetCleanedDevice(&o->cache, &map)) {
            // get devpath
            const char *devpath = BStringMap_Get(&map, "DEVPATH");
            ASSERT(devpath)
            
            // queue mapless event to clients
            LinkedList1Node *list_node = LinkedList1_GetFirst(&o->clients_list);
            while (list_node) {
                NCDUdevClient *client = UPPER_OBJECT(list_node, NCDUdevClient, clients_list_node);
                queue_mapless_event(o, devpath, client);
                list_node = LinkedList1Node_Next(list_node);
            }
            
            BStringMap_Free(&map);
        }
    }
}

static void next_job_handler (NCDUdevClient *o)
{
    DebugObject_Access(&o->d_obj);
    ASSERT(!LinkedList1_IsEmpty(&o->events_list))
    ASSERT(o->running)
    
    // get event
    struct NCDUdevClient_event *e = UPPER_OBJECT(LinkedList1_GetFirst(&o->events_list), struct NCDUdevClient_event, events_list_node);
    
    // grab map from event
    int have_map = e->have_map;
    BStringMap map = e->map;
    
    // grab devpath from event
    char *devpath = e->devpath;
    
    // remove from events list
    LinkedList1_Remove(&o->events_list, &e->events_list_node);
    
    // free structure
    free(e);
    
    // schedule next event if needed
    if (!LinkedList1_IsEmpty(&o->events_list)) {
        BPending_Set(&o->next_job);
    }
    
    // give map to handler
    o->handler(o->user, devpath, have_map, map);
    return;
}

void NCDUdevManager_Init (NCDUdevManager *o, int no_udev, BReactor *reactor, BProcessManager *manager)
{
    ASSERT(no_udev == 0 || no_udev == 1)
    
    // init arguments
    o->no_udev = no_udev;
    o->reactor = reactor;
    o->manager = manager;
    
    // init clients list
    LinkedList1_Init(&o->clients_list);
    
    // init cache
    NCDUdevCache_Init(&o->cache);
    
    // init restart timer
    BTimer_Init(&o->restart_timer, RESTART_TIMER_TIME, (BTimer_handler)timer_handler, o);
    
    // set have no monitor
    o->have_monitor = 0;
    
    DebugObject_Init(&o->d_obj);
}

void NCDUdevManager_Free (NCDUdevManager *o)
{
    DebugObject_Free(&o->d_obj);
    ASSERT(LinkedList1_IsEmpty(&o->clients_list))
    
    if (o->have_monitor) {
        // free info monitor
        if (o->have_info_monitor) {
            NCDUdevMonitor_Free(&o->info_monitor);
        }
        
        // free monitor
        NCDUdevMonitor_Free(&o->monitor);
    }
    
    // free restart timer
    BReactor_RemoveTimer(o->reactor, &o->restart_timer);
    
    // free cache
    NCDUdevCache_Free(&o->cache);
}

const BStringMap * NCDUdevManager_Query (NCDUdevManager *o, const char *devpath)
{
    DebugObject_Access(&o->d_obj);
    
    return NCDUdevCache_Query(&o->cache, devpath);
}

void NCDUdevClient_Init (NCDUdevClient *o, NCDUdevManager *m, void *user,
                         NCDUdevClient_handler handler)
{
    DebugObject_Access(&m->d_obj);
    
    // init arguments
    o->m = m;
    o->user = user;
    o->handler = handler;
    
    // insert to manager's list
    LinkedList1_Append(&m->clients_list, &o->clients_list_node);
    
    // init events list
    LinkedList1_Init(&o->events_list);
    
    // init next job
    BPending_Init(&o->next_job, BReactor_PendingGroup(m->reactor), (BPending_handler)next_job_handler, o);
    
    // set running
    o->running = 1;
    
    // queue all devices from cache
    const char *devpath = NCDUdevCache_First(&m->cache);
    while (devpath) {
        queue_mapless_event(m, devpath, o);
        devpath = NCDUdevCache_Next(&m->cache, devpath);
    }
    
    // if this is the first client, init monitor
    if (!m->have_monitor && !BTimer_IsRunning(&m->restart_timer)) {
        try_monitor(m);
    }
    
    DebugObject_Init(&o->d_obj);
}

void NCDUdevClient_Free (NCDUdevClient *o)
{
    DebugObject_Free(&o->d_obj);
    NCDUdevManager *m = o->m;
    
    // free events
    LinkedList1Node *list_node;
    while (list_node = LinkedList1_GetFirst(&o->events_list)) {
        struct NCDUdevClient_event *e = UPPER_OBJECT(list_node, struct NCDUdevClient_event, events_list_node);
        free_event(o, e);
    }
    
    // free next job
    BPending_Free(&o->next_job);
    
    // remove from manager's list
    LinkedList1_Remove(&m->clients_list, &o->clients_list_node);
}

void NCDUdevClient_Pause (NCDUdevClient *o)
{
    DebugObject_Access(&o->d_obj);
    ASSERT(o->running)
    
    // set not running
    o->running = 0;
    
    // unset next job to avoid reporting queued events
    BPending_Unset(&o->next_job);
}

void NCDUdevClient_Continue (NCDUdevClient *o)
{
    DebugObject_Access(&o->d_obj);
    ASSERT(!o->running)
    
    // set running
    o->running = 1;
    
    // set next job if we have events queued
    if (!LinkedList1_IsEmpty(&o->events_list)) {
        BPending_Set(&o->next_job);
    }
}
