/*

   The OS agnostic file tracking API for applications
   that want to interact with potentially many files on
   the disk that could be changed by other applications.

   Created on: 29.08.2016

*/

// TOP

#include "4tech_file_track.h"
#include "4tech_file_track_general.c"

// NOTE(allen):
// There are a number of places where the "table" is built into this system.
// If a table is not actually needed we will leave it in the API, but pull
// out all of the following internal refrences to the table:
// In Linux_File_Track_Vars :
//   + void *tables; and all refrences to it
// The entire Linux_File_Track_Entry struct and all refrences to it.
// In init_track_system :
//   + enough_memory_to_init_table(table_memory_size)
//   + everything under the note "Initialize main data tables"
// Leave move_track_system unimplemented and assert when it is called
// 

typedef struct {
    void *tables;
    // NOTE(allen):
    // This struct must fit inside the opaque File_Track_System struct.
    // If it needs more room we can make File_Track_System bigger.
    //
    // Here we need:
    // 1. A mutex of some kind to make the main operations thread safe.
    //    If it turns out the operations will be thread safe without a mutex
    //    then it is not required.
    // 2. Any synchronization primatives that are needed for dequeueing
    //    change events.
    // 3. Any data structures needed for handling the listeners.
    //    This system can expect to be provided with memory from the user.
    //    Right now the way the API works, it assumes that the user will
    //    only ever add memory and never free it.  If freeing becomes necessary
    //    on Linux the API can be extended to support that.

    int inotify;
    pthread_mutex_t lock;
    char *string_mem_begin;
    char *string_mem_end;

} Linux_File_Track_Vars;

//static_assert(sizeof(Linux_File_Track_Vars) <= sizeof(File_Track_System));

typedef struct {
    File_Index hash;
    // NOTE(allen):
    // This struct must fit inside the opaque File_Track_Entry struct.
    // The table is meant to help keep track of what is already being
    // tracked.  It may turn out that this isn't needed on Linux which
    // would be fine.  If it is used hash should be the first element
    // of this struct and it should be used to uniquely identify each
    // entry because it is used as the key for the table.
    char* filename;
} Linux_File_Track_Entry;

//static_assert(sizeof(Linux_File_Track_Entry) <= sizeof(File_Track_Entry));

#define to_vars(s) ((Linux_File_Track_Vars*)(s))
#define to_tables(v) ((File_Track_Tables*)(v->tables))

FILE_TRACK_LINK File_Track_Result
init_track_system(File_Track_System *system,
                  void *table_memory, int32_t table_memory_size,
                  void *listener_memory, int32_t listener_memory_size){
    File_Track_Result result = FileTrack_MemoryTooSmall;
    Linux_File_Track_Vars *vars = to_vars(system);

    Assert(sizeof(Linux_File_Track_Entry) <= sizeof(File_Track_Entry));

    if (enough_memory_to_init_table(table_memory_size) &&
        /*if listener memory is important check it's size here*/ 1){

        // NOTE(allen): Initialize main data tables
        vars->tables = (File_Track_Tables*) table_memory;
        File_Track_Tables *tables = to_tables(vars);
        init_table_memory(tables, table_memory_size);

        vars->inotify = inotify_init1(IN_NONBLOCK);

        pthread_mutex_init(&vars->lock, NULL);

        vars->string_mem_begin = (char*)listener_memory;
        vars->string_mem_end   = (char*)listener_memory + listener_memory_size;

        result = FileTrack_Good;
    }

    LINUX_FN_DEBUG("result: %d", result);

    return(result);
}

FILE_TRACK_LINK File_Track_Result
add_listener(File_Track_System *system, char *filename){
    File_Track_Result result = FileTrack_Good;
    Linux_File_Track_Vars *vars = to_vars(system);
    File_Track_Tables *tables = to_tables(vars);

    pthread_mutex_lock(&vars->lock);

    // NOTE(allen):
    // Here do something to begin listening to changes to the file named filename.
    // On Windows it listens to the parent directory if no other file in that
    // directory is already being listened to.  We will assume that the user
    // never passes the same filename in twice, although they may pass in two
    // different names that both refer to the same file.  In this case we should
    // treat them as two separate files so that if one of the listeners is removed
    // the other on the same file keeps working.

    if(tracking_system_has_space(tables, 1)){
        size_t filename_len = strlen(filename) + 1;
        if(vars->string_mem_end - vars->string_mem_begin >= filename_len){

            // TODO(inso): which events do we want?
            int wd = inotify_add_watch(vars->inotify, filename, IN_ALL_EVENTS);
            if(wd != -1){
                File_Index key = { wd, 1 };
                File_Track_Entry *entry = tracking_system_lookup_entry(tables, key);
                Linux_File_Track_Entry *linux_entry = (Linux_File_Track_Entry*) entry;

                LINUX_FN_DEBUG("map %s to wd %d", filename, wd);

                Assert(entry_is_available(entry));

                linux_entry->hash = key;
                linux_entry->filename = vars->string_mem_begin;

                memcpy(vars->string_mem_begin, filename, filename_len);
                vars->string_mem_begin += filename_len;

                ++tables->tracked_count;
            } else {
                result = FileTrack_FileSystemError;
            }
        } else {
            result = FileTrack_OutOfListenerMemory;
        }
    } else {
        result = FileTrack_OutOfTableMemory;
    }

    pthread_mutex_unlock(&vars->lock);

    LINUX_FN_DEBUG("result: %d", result);

    return(result);
}

FILE_TRACK_LINK File_Track_Result
remove_listener(File_Track_System *system, char *filename){
    File_Track_Result result = FileTrack_Good;
    Linux_File_Track_Vars *vars = to_vars(system);
    File_Track_Tables *tables = to_tables(vars);
    File_Track_Entry *entries = (File_Track_Entry*) to_ptr(tables, tables->file_table);

    pthread_mutex_lock(&vars->lock);

    // NOTE(allen):
    // Here do something to stop listening to changes to the file named filename.
    // We assume this filename has been passed into add_listener already and that
    // if it has not it is a bug in the user's code not in this code.

    for(uint32_t i = 0; i < tables->max; ++i){
        Linux_File_Track_Entry *e = (Linux_File_Track_Entry*)(entries + i);
        if(e->hash.id[1] != 1) continue;
        if(strcmp(e->filename, filename) == 0){
            LINUX_FN_DEBUG("%s found as wd %d", filename, e->hash.id[0]);
            if(inotify_rm_watch(vars->inotify, e->hash.id[0]) == -1){
                perror("inotify_rm_watch");
                result = FileTrack_FileSystemError;
            }
            internal_free_slot(tables, (File_Track_Entry*)e);
            // NOTE(inso): associated string memory in listeners would be freed here
            break;
        }
    }

    pthread_mutex_unlock(&vars->lock);

    LINUX_FN_DEBUG("result: %d", result);

    return(result);
}

FILE_TRACK_LINK File_Track_Result
move_track_system(File_Track_System *system, void *mem, int32_t size){
    File_Track_Result result = FileTrack_Good;
    Linux_File_Track_Vars *vars = to_vars(system);

    pthread_mutex_lock(&vars->lock);

    {
        File_Track_Tables *original_tables = to_tables(vars);
        result = move_table_memory(original_tables, mem, size);
        if (result == FileTrack_Good){
            vars->tables = mem;
        }
    }

    pthread_mutex_unlock(&vars->lock);

    LINUX_FN_DEBUG("size: %d, %d", size, result);

    return(result);
}

FILE_TRACK_LINK File_Track_Result
expand_track_system_listeners(File_Track_System *system, void *mem, int32_t size){
    File_Track_Result result = FileTrack_Good;
    Linux_File_Track_Vars *vars = to_vars(system);

    pthread_mutex_lock(&vars->lock);

    // NOTE(allen): If there is a data structure for the listeners
    // this call adds more memory to that system.  If this system
    // wants to free old memory the API does not currently support
    // that but it can.

    // NOTE(inso): pointer to old string mem is lost here.
    // would need to keep it around if we want to free in the future

    // NOTE(inso): assuming PATH_MAX is a reasonable lower bound of extra memory to get

    if(size < PATH_MAX){
        result = FileTrack_MemoryTooSmall;
    } else {
        vars->string_mem_begin = (char*) mem;
        vars->string_mem_end   = (char*) mem + size;;
    }

    pthread_mutex_unlock(&vars->lock);

    LINUX_FN_DEBUG("size: %d, result: %d", size, result);

    return(result);
}

FILE_TRACK_LINK File_Track_Result
get_change_event(File_Track_System *system, char *buffer, int32_t max, int32_t *size){
    File_Track_Result result = FileTrack_NoMoreEvents;
    Linux_File_Track_Vars *vars = to_vars(system);
    File_Track_Tables *tables = to_tables(vars);

    pthread_mutex_lock(&vars->lock);

    // NOTE(allen): If there are any new file changes report them
    // by copying the name of the file to the buffer and returning
    // FileTrack_Good.  It is allowed for this system to report
    // changes to files that were not requested to be tracked, it
    // is up to the user to check the filename and see if it cares
    // about that file.  If there are no new changes the return
    // FileTrack_NoMoreEvents.

    struct inotify_event ev;

    ssize_t n = read(vars->inotify, &ev, sizeof(ev));
    if(n == -1 && errno != EAGAIN){
        perror("inotify read");
    } else if(n > 0){
        File_Index key = { ev.wd, 1 };
        File_Track_Entry *entry = tracking_system_lookup_entry(tables, key);
        Linux_File_Track_Entry *linux_entry = (Linux_File_Track_Entry*) entry;

        if(!entry_is_available(entry)){
            LINUX_FN_DEBUG("event from wd %d (%s)", ev.wd, linux_entry->filename);
            size_t filename_size = strlen(linux_entry->filename);
            if(max < filename_size){
                result = FileTrack_MemoryTooSmall;
                // NOTE(inso): this event will be dropped, needs to be stashed.
                LINUX_FN_DEBUG("max too small, event dropped");
            } else {
                memcpy(buffer, linux_entry->filename, filename_size);
                *size = filename_size;
                result = FileTrack_Good;
            }
        } else {
            LINUX_FN_DEBUG("dead event from wd %d", ev.wd);
        }
    }

    pthread_mutex_unlock(&vars->lock);

    return(result);
}

FILE_TRACK_LINK File_Track_Result
shut_down_track_system(File_Track_System *system){
    File_Track_Result result = FileTrack_Good;
    Linux_File_Track_Vars *vars = to_vars(system);

    // NOTE(allen): Close all the global track system resources.
    if(close(vars->inotify) == -1){
        result = FileTrack_FileSystemError;
    }

    pthread_mutex_destroy(&vars->lock);

    LINUX_FN_DEBUG("result: %d", result);

    return(result);
}



// BOTTOM