/******************************************************************************
*
* Copyright (C) 2004-2017  Brian Berlin
* 
* 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 3 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, see <http://www.gnu.org/licenses/>
*
******************************************************************************
*
* FILENAME: util.c
*
* DESCRIPTION:  This contains utility functions
*
* AUTHORS:  bberlin
*
* CHANGE LOG
*
*  VER     DATE     CHANGED BY   DESCRIPTION OF CHANGE
* ------  --------  ----------   ------------------------------------
* 0.1     07/18/04  bberlin      Creation
* 0.2     03/09/06  bberlin      Changed display message functions to write to
*                                  log file instead of console
*                                Added functions for handling filenames and
*                                  directories
*                                Fixed 'util_get_crc' to exit on file error
*                                Integrated use of GUI into debugger
* 0.3.0   03/27/06  bberlin      Changed 'debug_print_value' to call pokey read
*                                  register function instead of using values
* 0.6.2   06/26/09  bberlin      Updated util_init to use Windows user profile
* 0.7.2   04/12/17  bberlin      Updated to allow being portable
******************************************************************************/
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <time.h>
#include <zlib.h>
#ifdef _WIN32
#include <direct.h>
#include <shlobj.h>
#define getcwd _getcwd 
#else
#include <glob.h>
#include <unistd.h>
#endif
#ifdef __APPLE__
#include <CoreFoundation/CoreFoundation.h>
#endif
#include <dirent.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <SDL3/SDL.h>
#include "util.h"
#include "md5.h"
#include "sha1.h"
#include "logger.h"
#include "../kat5200.h"

#ifdef __ANDROID__
#include <SDL3/SDL.h>
#define S_IREAD S_IRUSR
#define S_IWRITE S_IWUSR
#define S_IEXEC S_IXUSR
#endif

/*
 * Fallback seed directory for builds that don't define it
 */
#ifndef KAT_DATADIR
#define KAT_DATADIR "/usr/local/share/kat5200"
#endif
#ifndef KAT_MANUAL_PDF_NAME
#define KAT_MANUAL_PDF_NAME "kat5200-user-guide.pdf"
#endif
#define PROFILE_SEED_STAMP_FILE ".profiles_seed_version"
#define PROFILE_SEED_MAX_TARGETS 4

typedef enum {
    PROFILE_SEED_MODE_EXTRACT_ALL = 0,
    PROFILE_SEED_MODE_MERGE_MISSING,
    PROFILE_SEED_MODE_ANDROID_ASSET_EXTRACT
} t_profile_seed_mode;

static char g_program_path[KAT_PATH_MAX] = "";
static char g_app_path[KAT_PATH_MAX] = "";
int g_portable;

int util_mkdir ( const char *path, int mode )
{
#ifdef _WIN32
    (void)mode;
    return _mkdir ( path );
#else
    return mkdir ( path, mode );
#endif
}

/*
 * First-use deferred profile seeding state. 
 */
/* Flags are touched from both the UI thread and the seed worker thread, so
 * they are SDL atomics (plain volatile is not synchronization in C).        */
static SDL_AtomicInt g_seed_pending;      /* profiles still need copying      */
static SDL_AtomicInt g_seed_done;         /* worker finished                  */
static SDL_AtomicInt g_seed_error;        /* a copy step failed               */
static char g_seed_src[KAT_PATH_MAX] = "";/* <app>\defaults                   */
static char g_seed_dst[KAT_PATH_MAX] = "";/* live data dir to seed into       */
static char g_seed_zip[KAT_PATH_MAX] = "";/* explicit profiles.zip path       */
static int g_seed_remove_zip = 0;
static t_profile_seed_mode g_seed_mode = PROFILE_SEED_MODE_EXTRACT_ALL;
static int g_seed_target_count = 0;
static char g_seed_target_tree[PROFILE_SEED_MAX_TARGETS][KAT_PATH_MAX];
static char g_seed_target_dst[PROFILE_SEED_MAX_TARGETS][KAT_PATH_MAX];

/* Set once a factory reset has copied the default conf over the live one.  It
 * is only touched from the UI thread (util_reset_config / config_write_file),
 * so a plain int is sufficient.  While set, config_write_file refuses to write
 * the global conf table so the stale in-memory tree cannot clobber the freshly
 * restored defaults before the next restart.                                 */
static int g_factory_reset_done = 0;

static int util_file_exists ( const char *path );
static int util_directory_exists ( const char *path );
static int util_mkdir_recursive ( const char *path );
static int util_write_profile_seed_stamp ( const char *dir );
static int util_profile_seed_stamp_is_current ( void );
static int util_seed_full_profiles_from_zip ( const char *zip_path,
        const char *dest_dir );
#ifdef __ANDROID__
static int util_extract_asset_zip ( const char *asset_name, const char *dest_dir );
#endif

/******************************************************************************
**  Function   :  util_defer_profile_seed
**
**  Objective  :  Arm the deferred default-profile seed.  src_parent is the
**                directory that contains (or will contain) the profiles.zip
**                seed archive; dst_parent is where its profiles.5200 /
**                profiles.8bit trees are extracted.  The extraction is run later
**                on a background thread (see util_first_use_seed_worker) behind a
**                "please wait" splash, so first-run startup is not blocked.
**                Cross-platform.
******************************************************************************/
static void util_defer_profile_seed ( const char *src_parent, const char *dst_parent )
{
    if ( !src_parent || !dst_parent || !strlen(src_parent) || !strlen(dst_parent) )
        return;

    /* Refuse to re-arm while a copy is still in flight: overwriting the src/dst
     * paths out from under a running worker would corrupt its copy.           */
    if ( SDL_GetAtomicInt ( &g_seed_pending ) && !SDL_GetAtomicInt ( &g_seed_done ) )
        return;

    strncpy ( g_seed_src, src_parent, sizeof(g_seed_src) - 1 );
    strncpy ( g_seed_dst, dst_parent, sizeof(g_seed_dst) - 1 );
    g_seed_src[sizeof(g_seed_src) - 1] = '\0';
    g_seed_dst[sizeof(g_seed_dst) - 1] = '\0';
    g_seed_zip[0] = '\0';
    g_seed_remove_zip = 0;
    g_seed_mode = PROFILE_SEED_MODE_EXTRACT_ALL;
    g_seed_target_count = 0;
    SDL_SetAtomicInt ( &g_seed_error, 0 );
    SDL_SetAtomicInt ( &g_seed_done, 0 );
    SDL_SetAtomicInt ( &g_seed_pending, 1 );
}

#ifdef __ANDROID__
/******************************************************************************
**  Function   :  util_defer_android_profile_asset_seed
**
**  Objective  :  Arm deferred first-run profile seeding from the APK asset.
**                Android cannot address profiles.zip as a normal filesystem
**                seed path before copying it out, so the worker handles the
**                asset extraction directly once the GUI splash is visible.
******************************************************************************/
static void util_defer_android_profile_asset_seed ( const char *dst_parent )
{
    if ( !dst_parent || !strlen(dst_parent) )
        return;

    if ( SDL_GetAtomicInt ( &g_seed_pending ) && !SDL_GetAtomicInt ( &g_seed_done ) )
        return;

    g_seed_src[0] = '\0';
    strncpy ( g_seed_dst, dst_parent, sizeof(g_seed_dst) - 1 );
    g_seed_dst[sizeof(g_seed_dst) - 1] = '\0';
    g_seed_zip[0] = '\0';
    g_seed_remove_zip = 0;
    g_seed_mode = PROFILE_SEED_MODE_ANDROID_ASSET_EXTRACT;
    g_seed_target_count = 0;
    SDL_SetAtomicInt ( &g_seed_error, 0 );
    SDL_SetAtomicInt ( &g_seed_done, 0 );
    SDL_SetAtomicInt ( &g_seed_pending, 1 );
}
#endif

/******************************************************************************
**  Function   :  util_first_use_seed_pending / _done
**
**  Objective  :  Query the deferred first-run profile seed state so the GUI
**                can show a "please wait" splash while it runs.
******************************************************************************/
int util_first_use_seed_pending ( void )
{
    return SDL_GetAtomicInt ( &g_seed_pending );
}

int util_first_use_seed_done ( void )
{
    return SDL_GetAtomicInt ( &g_seed_done );
}

int util_first_use_seed_error ( void )
{
    return SDL_GetAtomicInt ( &g_seed_error );
}

/******************************************************************************
**  Function   :  util_factory_reset_done
**
**  Objective  :  Report whether a factory reset has restored the default conf
**                this session.  While true, config_write_file must not write
**                the global conf table (the in-memory tree is stale and would
**                clobber the restored defaults until the next restart).
******************************************************************************/
int util_factory_reset_done ( void )
{
    return g_factory_reset_done;
}

/******************************************************************************
**  Function   :  util_first_use_seed_worker
**
**  Objective  :  Perform the deferred (large) default-profile copy.  Designed
**                to be handed to pc_start_thread; clears _pending and sets
**                _done when finished.  Copy failures set the _error flag and
**                are logged so the splash does not silently report success.
******************************************************************************/
int util_first_use_seed_worker ( void *unused )
{
    char zip[KAT_PATH_MAX];
    char msg[KAT_PATH_MAX + 64];
    int err = 0;
#ifdef _WIN32
    char sep = '\\';
#else
    char sep = '/';
#endif

    (void)unused;

    if ( g_seed_mode == PROFILE_SEED_MODE_MERGE_MISSING ) {
        if ( strlen(g_seed_zip) && g_seed_target_count > 0 ) {
            t_profile_seed_target targets[PROFILE_SEED_MAX_TARGETS];
            int i;

            for ( i = 0; i < g_seed_target_count; ++i ) {
                targets[i].seed_tree = g_seed_target_tree[i];
                targets[i].dest_dir = g_seed_target_dst[i];
            }

            if ( util_merge_seed_profiles_from_zip ( g_seed_zip, targets,
                        g_seed_target_count ) ||
                    util_write_profile_seed_stamp ( g_program_path ) ) {
                snprintf ( msg, sizeof(msg),
                           "Failed to merge default profiles from: %s", g_seed_zip );
                logger_log_message ( LOG_ERROR, msg, "" );
                err = 1;
            }
        }
    }
    else if ( g_seed_mode == PROFILE_SEED_MODE_ANDROID_ASSET_EXTRACT ) {
#ifdef __ANDROID__
        if ( strlen(g_seed_dst) ) {
            if ( util_extract_asset_zip ( "profiles.zip", g_seed_dst ) ||
                    util_write_profile_seed_stamp ( g_seed_dst ) ) {
                logger_log_message ( LOG_ERROR,
                        "Failed to seed default profiles from Android asset: profiles.zip",
                        "" );
                err = 1;
            }
        }
        else {
            logger_log_message ( LOG_ERROR,
                    "Failed to seed default profiles: Android destination is empty",
                    "" );
            err = 1;
        }
#else
        logger_log_message ( LOG_ERROR,
                "Failed to seed default profiles: Android asset mode on non-Android build",
                "" );
        err = 1;
#endif
    }
    else {
        /* The default profiles ship as a single profiles.zip beside the seed conf
         * (see cmake/make_profiles_zip.cmake); extracting one archive is far
         * faster than copying the ~14k loose files individually.  The archive
         * holds the profiles.5200/ and profiles.8bit/ trees, so extracting into
         * g_seed_dst recreates them beside the live config.                    */
        if ( strlen(g_seed_src) && strlen(g_seed_dst) ) {
            snprintf ( zip, sizeof(zip), "%s%cprofiles.zip", g_seed_src, sep );
            if ( util_seed_full_profiles_from_zip ( zip, g_seed_dst ) ) {
                snprintf ( msg, sizeof(msg),
                           "Failed to seed default profiles from: %s", zip );
                logger_log_message ( LOG_ERROR, msg, "" );
                err = 1;
            }
        }
    }

    if ( g_seed_remove_zip && strlen(g_seed_zip) )
        remove ( g_seed_zip );
    g_seed_remove_zip = 0;

    if ( err )
        SDL_SetAtomicInt ( &g_seed_error, 1 );

    SDL_SetAtomicInt ( &g_seed_pending, 0 );
    SDL_SetAtomicInt ( &g_seed_done, 1 );
    return 0;
}

/******************************************************************************
**  Function   :  util_file_exists
**
**  Objective  :  Return true if a filesystem object exists.
******************************************************************************/
static int util_file_exists ( const char *path )
{
    struct stat st;

    return path && *path && stat(path, &st) == 0;
}

/******************************************************************************
**  Function   :  util_mkdir_recursive
**
**  Objective  :  Create a directory and any missing parents.
******************************************************************************/
static int util_mkdir_recursive ( const char *path )
{
    char tmp[KAT_PATH_MAX];
    char *p;
    char *start;
    size_t len;

    if ( !path || !*path )
        return -1;

    strncpy ( tmp, path, sizeof(tmp) - 1 );
    tmp[sizeof(tmp) - 1] = '\0';

    len = strlen(tmp);
    while ( len > 1 && (tmp[len - 1] == '/' || tmp[len - 1] == '\\') )
        tmp[--len] = '\0';

    start = tmp + 1;
    if ( len > 2 && tmp[1] == ':' )
        start = tmp + 3;

    for ( p = start; *p; ++p ) {
        if ( *p == '/' || *p == '\\' ) {
            char sep_ch = *p;
            *p = '\0';
            if ( *tmp )
                util_mkdir ( tmp, UTIL_DEFAULT_DIR_MODE );
            *p = sep_ch;
        }
    }

    if ( util_mkdir(tmp, UTIL_DEFAULT_DIR_MODE) && !util_directory_exists(tmp) )
        return -1;

    return 0;
}

static int util_seed_full_profiles_from_zip ( const char *zip_path,
        const char *dest_dir )
{
    if ( util_unzip_to_directory ( zip_path, dest_dir ) )
        return -1;

    return util_write_profile_seed_stamp ( dest_dir );
}

static int util_write_profile_seed_stamp ( const char *dir )
{
    char *path;
    FILE *fp;

    if ( !dir || !*dir )
        return -1;

    path = util_make_path_alloc ( (char *)dir, PROFILE_SEED_STAMP_FILE );
    if ( !path )
        return -1;

    fp = fopen ( path, "w" );
    free ( path );
    if ( !fp )
        return -1;

    fprintf ( fp, "%s\n", VERSION );
    fclose ( fp );
    return 0;
}

static int util_profile_seed_stamp_is_current ( void )
{
    char *path;
    FILE *fp;
    char buffer[128];
    char *p;
    int current = 0;

    if ( !strlen(g_program_path) )
        return 0;

    path = util_make_path_alloc ( g_program_path, PROFILE_SEED_STAMP_FILE );
    if ( !path )
        return 0;

    fp = fopen ( path, "r" );
    free ( path );
    if ( !fp )
        return 0;

    if ( fgets(buffer, sizeof(buffer), fp) ) {
        p = buffer;
        while ( *p && *p != '\r' && *p != '\n' )
            ++p;
        *p = '\0';
        current = strcmp(buffer, VERSION) == 0;
    }

    fclose ( fp );
    return current;
}

static int util_zip_entry_is_safe_profile_path ( const char *entry )
{
    const char *p;

    if ( !entry || !*entry )
        return 0;
    if ( entry[0] == '/' || entry[0] == '\\' )
        return 0;
    if ( strlen(entry) > 1 && entry[1] == ':' )
        return 0;
    if ( strchr(entry, '\\') )
        return 0;

    p = entry;
    while ( *p ) {
        const char *slash;
        size_t len;

        slash = strchr ( p, '/' );
        len = slash ? (size_t)(slash - p) : strlen(p);
        if ( (len == 1 && p[0] == '.') ||
             (len == 2 && p[0] == '.' && p[1] == '.') )
            return 0;

        if ( !slash )
            break;
        p = slash + 1;
    }

    return 1;
}

static int util_string_ends_with ( const char *value, const char *suffix )
{
    size_t value_len;
    size_t suffix_len;

    if ( !value || !suffix )
        return 0;

    value_len = strlen(value);
    suffix_len = strlen(suffix);
    if ( suffix_len > value_len )
        return 0;

    return strcmp(value + value_len - suffix_len, suffix) == 0;
}

static const char * util_profile_seed_entry_relative_path ( const char *entry,
        const t_profile_seed_target *target )
{
    size_t tree_len;

    if ( !target || !target->seed_tree || !target->dest_dir )
        return NULL;

    tree_len = strlen(target->seed_tree);
    if ( !strcmp(entry, target->seed_tree) )
        return "";
    if ( !strncmp(entry, target->seed_tree, tree_len) &&
         entry[tree_len] == '/' )
        return entry + tree_len + 1;

    return NULL;
}

static unsigned long * util_crc_list_append ( unsigned long *values,
        size_t *count, size_t *capacity, unsigned long crc )
{
    unsigned long *new_values;

    if ( *count >= *capacity ) {
        size_t new_capacity = *capacity ? *capacity * 2 : 64;
        new_values = realloc ( values, new_capacity * sizeof(*values) );
        if ( !new_values )
            return NULL;
        values = new_values;
        *capacity = new_capacity;
    }

    values[(*count)++] = crc;
    return values;
}

static int util_crc_compare ( const void *a, const void *b )
{
    unsigned long av = *(const unsigned long *)a;
    unsigned long bv = *(const unsigned long *)b;

    if ( av < bv )
        return -1;
    if ( av > bv )
        return 1;
    return 0;
}

static size_t util_crc_list_lower_bound ( const unsigned long *values,
        size_t count, unsigned long crc, int *found )
{
    size_t lo = 0;
    size_t hi = count;

    while ( lo < hi ) {
        size_t mid = lo + (hi - lo) / 2;
        if ( values[mid] < crc )
            lo = mid + 1;
        else
            hi = mid;
    }

    *found = ( lo < count && values[lo] == crc );
    return lo;
}

static int util_crc_list_contains ( const unsigned long *values,
        size_t count, unsigned long crc )
{
    int found = 0;

    util_crc_list_lower_bound ( values, count, crc, &found );
    return found;
}

static int util_crc_list_insert_sorted ( unsigned long **values,
        size_t *count, size_t *capacity, unsigned long crc )
{
    unsigned long *new_values;
    size_t pos;
    int found = 0;

    pos = util_crc_list_lower_bound ( *values, *count, crc, &found );
    if ( found )
        return 0;

    if ( *count >= *capacity ) {
        size_t new_capacity = *capacity ? *capacity * 2 : 64;
        new_values = realloc ( *values, new_capacity * sizeof(**values) );
        if ( !new_values )
            return -1;
        *values = new_values;
        *capacity = new_capacity;
    }

    memmove ( *values + pos + 1, *values + pos,
            (*count - pos) * sizeof(**values) );
    (*values)[pos] = crc;
    ++(*count);
    return 0;
}

static int util_parse_csv_crc ( const char *line, unsigned long *crc )
{
    const char *comma;
    const char *value;
    char *endptr;
    unsigned long parsed;

    if ( !line || !crc )
        return 0;

    comma = strrchr ( line, ',' );
    if ( !comma )
        return 0;

    value = comma + 1;
    while ( isspace((unsigned char)*value) )
        ++value;

    parsed = strtoul ( value, &endptr, 0 );
    if ( value == endptr )
        return 0;

    *crc = parsed;
    return 1;
}

static int util_read_line_dynamic ( FILE *fp, char **line, size_t *capacity )
{
    size_t len = 0;
    int ch;

    if ( !*line || !*capacity ) {
        *capacity = 1024;
        *line = malloc ( *capacity );
        if ( !*line )
            return -1;
    }

    while ( (ch = fgetc(fp)) != EOF ) {
        if ( len + 1 >= *capacity ) {
            char *new_line;
            size_t new_capacity = *capacity * 2;

            new_line = realloc ( *line, new_capacity );
            if ( !new_line )
                return -1;
            *line = new_line;
            *capacity = new_capacity;
        }

        (*line)[len++] = (char)ch;
        if ( ch == '\n' )
            break;
    }

    if ( len == 0 && ch == EOF )
        return 0;

    (*line)[len] = '\0';
    return 1;
}

static int util_collect_csv_crcs ( const char *path, unsigned long **crcs,
        size_t *crc_count )
{
    FILE *fp;
    char *line = NULL;
    size_t line_capacity = 0;
    unsigned long *values = NULL;
    size_t count = 0;
    size_t capacity = 0;
    int status;

    fp = fopen ( path, "r" );
    if ( !fp )
        return -1;

    while ( (status = util_read_line_dynamic(fp, &line, &line_capacity)) > 0 ) {
        unsigned long crc;

        if ( util_parse_csv_crc(line, &crc) ) {
            unsigned long *new_values = util_crc_list_append ( values, &count,
                    &capacity, crc );
            if ( !new_values ) {
                free ( values );
                free ( line );
                fclose ( fp );
                return -1;
            }
            values = new_values;
        }
    }

    free ( line );
    fclose ( fp );
    if ( status < 0 ) {
        free ( values );
        return -1;
    }

    if ( count > 1 )
        qsort ( values, count, sizeof(*values), util_crc_compare );

    *crcs = values;
    *crc_count = count;
    return 0;
}

static int util_file_needs_append_newline ( const char *path )
{
    FILE *fp;
    long size;
    int ch;

    fp = fopen ( path, "rb" );
    if ( !fp )
        return 0;

    if ( fseek(fp, 0, SEEK_END) ) {
        fclose ( fp );
        return 0;
    }

    size = ftell ( fp );
    if ( size <= 0 ) {
        fclose ( fp );
        return 0;
    }

    if ( fseek(fp, size - 1, SEEK_SET) ) {
        fclose ( fp );
        return 0;
    }

    ch = fgetc ( fp );
    fclose ( fp );
    return ch != '\n';
}

static int util_read_current_zip_file_to_memory ( unzFile zf, char **out,
        size_t *out_len )
{
    char buffer[8192];
    char *data = NULL;
    size_t len = 0;
    size_t capacity = 0;
    int n;

    *out = NULL;
    *out_len = 0;

    if ( unzOpenCurrentFile(zf) != UNZ_OK )
        return -1;

    while ( (n = unzReadCurrentFile(zf, buffer, sizeof(buffer))) > 0 ) {
        if ( len + (size_t)n + 1 > capacity ) {
            size_t new_capacity = capacity ? capacity * 2 : 16384;
            char *new_data;

            while ( len + (size_t)n + 1 > new_capacity )
                new_capacity *= 2;

            new_data = realloc ( data, new_capacity );
            if ( !new_data ) {
                free ( data );
                unzCloseCurrentFile ( zf );
                return -1;
            }

            data = new_data;
            capacity = new_capacity;
        }

        memcpy ( data + len, buffer, (size_t)n );
        len += (size_t)n;
    }

    unzCloseCurrentFile ( zf );

    if ( n < 0 ) {
        free ( data );
        return -1;
    }

    if ( !data ) {
        data = malloc ( 1 );
        if ( !data )
            return -1;
    }
    data[len] = '\0';

    *out = data;
    *out_len = len;
    return 0;
}

static int util_extract_current_zip_file_to_path ( unzFile zf,
        const char *out_path )
{
    FILE *fp;
    char parent[KAT_PATH_MAX];
    char buffer[8192];
    int n;
    int err = 0;

    strncpy ( parent, out_path, sizeof(parent) - 1 );
    parent[sizeof(parent) - 1] = '\0';
    util_strip_file_to_directory ( parent );
    if ( *parent && util_mkdir_recursive(parent) )
        return -1;

    if ( unzOpenCurrentFile(zf) != UNZ_OK )
        return -1;

    fp = fopen ( out_path, "wb" );
    if ( !fp ) {
        unzCloseCurrentFile ( zf );
        return -1;
    }

    while ( (n = unzReadCurrentFile(zf, buffer, sizeof(buffer))) > 0 ) {
        if ( fwrite(buffer, 1, (size_t)n, fp) != (size_t)n ) {
            err = -1;
            break;
        }
    }
    if ( n < 0 )
        err = -1;

    fclose ( fp );
    unzCloseCurrentFile ( zf );
    return err;
}

static int util_merge_current_zip_csv ( unzFile zf, const char *out_path )
{
    char *seed = NULL;
    size_t seed_len = 0;
    unsigned long *crcs = NULL;
    size_t crc_count = 0;
    size_t crc_capacity = 0;
    char *line;
    char *end;
    FILE *append_fp = NULL;
    int need_newline;
    int err = 0;

    if ( !util_file_exists(out_path) )
        return util_extract_current_zip_file_to_path ( zf, out_path );

    if ( util_collect_csv_crcs(out_path, &crcs, &crc_count) )
        return -1;
    crc_capacity = crc_count;

    if ( util_read_current_zip_file_to_memory(zf, &seed, &seed_len) ) {
        free ( crcs );
        return -1;
    }

    need_newline = util_file_needs_append_newline ( out_path );
    line = seed;
    end = seed + seed_len;
    while ( line < end ) {
        char *next = memchr ( line, '\n', (size_t)(end - line) );
        char *line_end = next ? next : end;
        unsigned long crc;

        while ( line_end > line &&
                (line_end[-1] == '\r' || line_end[-1] == '\n') )
            --line_end;

        *line_end = '\0';

        if ( *line && util_parse_csv_crc(line, &crc) &&
             !util_crc_list_contains(crcs, crc_count, crc) ) {
            if ( !append_fp ) {
                append_fp = fopen ( out_path, "ab" );
                if ( !append_fp ) {
                    err = -1;
                    break;
                }
                if ( need_newline ) {
                    fputc ( '\n', append_fp );
                    need_newline = 0;
                }
            }

            fwrite ( line, 1, strlen(line), append_fp );
            fputc ( '\n', append_fp );

            if ( util_crc_list_insert_sorted ( &crcs, &crc_count,
                        &crc_capacity, crc ) ) {
                err = -1;
                break;
            }
        }

        if ( !next )
            break;
        line = next + 1;
    }

    if ( append_fp )
        fclose ( append_fp );
    free ( seed );
    free ( crcs );
    return err;
}

int util_merge_seed_profiles_from_zip ( const char *zip_path,
        const t_profile_seed_target *targets, int target_count )
{
    unzFile zf;
    unz_file_info finfo;
    char entry[KAT_PATH_MAX];
    int status;
    int err = 0;

    if ( !zip_path || !*zip_path || !targets || target_count <= 0 )
        return -1;

    zf = unzOpen ( zip_path );
    if ( !zf )
        return -1;

    status = unzGoToFirstFile ( zf );
    while ( status == UNZ_OK ) {
        int matched = 0;
        int i;

        if ( unzGetCurrentFileInfo ( zf, &finfo, entry, sizeof(entry),
                                     NULL, 0, NULL, 0 ) != UNZ_OK ) {
            err = -1;
            break;
        }
        if ( finfo.size_filename >= sizeof(entry) ) {
            err = -1;
            break;
        }
        entry[finfo.size_filename] = '\0';

        if ( !util_zip_entry_is_safe_profile_path(entry) ) {
            err = -1;
            break;
        }

        for ( i = 0; i < target_count; ++i ) {
            const char *relative;
            char *out_path;
            size_t len;

            relative = util_profile_seed_entry_relative_path ( entry,
                    &targets[i] );
            if ( !relative )
                continue;

            matched = 1;
            len = strlen(relative);
            out_path = util_make_path_alloc ( (char *)targets[i].dest_dir,
                    relative );
            if ( !out_path ) {
                err = -1;
                break;
            }

            if ( !len ) {
                if ( util_mkdir_recursive(targets[i].dest_dir) )
                    err = -1;
            }
            else if ( relative[len - 1] == '/' ) {
                if ( util_mkdir_recursive(out_path) )
                    err = -1;
            }
            else if ( util_string_ends_with(relative, ".conf") ) {
                if ( !util_file_exists(out_path) &&
                     util_extract_current_zip_file_to_path(zf, out_path) )
                    err = -1;
            }
            else if ( util_string_ends_with(relative, ".csv") ) {
                const char *base = strrchr ( relative, '/' );
                base = base ? base + 1 : relative;
                if ( strcmp(base, "favorites.csv") &&
                     util_merge_current_zip_csv(zf, out_path) )
                    err = -1;
            }

            free ( out_path );
            if ( err )
                break;
        }

        if ( err )
            break;

        if ( !matched ) {
            status = unzGoToNextFile ( zf );
            continue;
        }

        status = unzGoToNextFile ( zf );
    }

    unzClose ( zf );
    return err;
}

int util_schedule_missing_profile_seed_sync ( const char *zip_path,
        const t_profile_seed_target *targets, int target_count )
{
    int i;

    if ( util_profile_seed_stamp_is_current() )
        return 0;

    if ( SDL_GetAtomicInt ( &g_seed_pending ) && !SDL_GetAtomicInt ( &g_seed_done ) )
        return 0;

    if ( !targets || target_count <= 0 ||
         target_count > PROFILE_SEED_MAX_TARGETS )
        return -1;

#ifdef __ANDROID__
    (void)zip_path;
    util_make_path ( g_program_path, ".profiles_seed_sync.zip", g_seed_zip );
    if ( util_file_copy ( "profiles.zip", g_seed_zip ) )
        return -1;
    g_seed_remove_zip = 1;
#else
    if ( !zip_path || !*zip_path )
        return -1;
    if ( !util_file_exists(zip_path) )
        return 0;
    strncpy ( g_seed_zip, zip_path, sizeof(g_seed_zip) - 1 );
    g_seed_zip[sizeof(g_seed_zip) - 1] = '\0';
    g_seed_remove_zip = 0;
#endif

    for ( i = 0; i < target_count; ++i ) {
        if ( !targets[i].seed_tree || !targets[i].dest_dir ||
             !*targets[i].seed_tree || !*targets[i].dest_dir )
            return -1;
        strncpy ( g_seed_target_tree[i], targets[i].seed_tree,
                sizeof(g_seed_target_tree[i]) - 1 );
        strncpy ( g_seed_target_dst[i], targets[i].dest_dir,
                sizeof(g_seed_target_dst[i]) - 1 );
        g_seed_target_tree[i][sizeof(g_seed_target_tree[i]) - 1] = '\0';
        g_seed_target_dst[i][sizeof(g_seed_target_dst[i]) - 1] = '\0';
    }

    g_seed_target_count = target_count;
    g_seed_src[0] = '\0';
    g_seed_dst[0] = '\0';
    g_seed_mode = PROFILE_SEED_MODE_MERGE_MISSING;
    SDL_SetAtomicInt ( &g_seed_error, 0 );
    SDL_SetAtomicInt ( &g_seed_done, 0 );
    SDL_SetAtomicInt ( &g_seed_pending, 1 );
    return 0;
}

/******************************************************************************
**  Function   :  util_init
**                                                                    
**  Objective  :  This function initializes utility stuff               
**                                                   
**  Parameters :  p_path - program calling string ( argv[0] )
**                 
**  return     :  NONE
**
******************************************************************************/
static int util_directory_exists ( const char *path )
{
    struct stat st;

    if ( !path || !strlen(path) )
        return 0;

    return stat(path, &st) == 0 && (st.st_mode & S_IFDIR);
}

/******************************************************************************
**  Function   :  util_unzip_to_directory
**
**  Objective  :  Extract every entry of a .zip into dest_dir, recreating any
**                subdirectories it contains.  Used to seed the profiles from a
**                single packaged archive (notably on Android, whose APK assets
**                cannot be walked with opendir).  Cross-platform (minizip).
**
**  Parameters :  zip_path - path to the .zip on a real filesystem
**                dest_dir - directory to extract into (created entries are
**                           rooted here)
**
**  return     :  0 on success, -1 on any error
******************************************************************************/
int util_unzip_to_directory ( const char *zip_path, const char *dest_dir )
{
    unzFile zf;
    unz_file_info finfo;
    char entry[KAT_PATH_MAX];
    char out_path[KAT_PATH_MAX];
    int err = 0, status;

    zf = unzOpen ( zip_path );
    if ( !zf )
        return -1;

    status = unzGoToFirstFile ( zf );
    while ( status == UNZ_OK ) {

        if ( unzGetCurrentFileInfo ( zf, &finfo, entry, sizeof(entry),
                                     NULL, 0, NULL, 0 ) != UNZ_OK ) {
            err = -1;
            break;
        }
        if ( finfo.size_filename >= sizeof(entry) ) {
            err = -1;
            break;
        }
        entry[finfo.size_filename] = '\0';

        util_make_path ( (char *)dest_dir, entry, out_path );

        size_t len = strlen ( entry );
        if ( len && (entry[len-1] == '/' || entry[len-1] == '\\') ) {
            /* Directory entry. */
            util_mkdir_recursive ( out_path );
        }
        else {
            if ( util_extract_current_zip_file_to_path ( zf, out_path ) )
                err = -1;
        }
        if ( err )
            break;

        status = unzGoToNextFile ( zf );
    }

    unzClose ( zf );
    return err;

} /* end util_unzip_to_directory */

#ifdef __ANDROID__
static int util_extract_asset_zip ( const char *asset_name, const char *dest_dir )
{
    char zip_path[KAT_PATH_MAX];
    int status = -1;

    util_make_path ( (char *)dest_dir, asset_name, zip_path );
    if ( util_file_copy ( (char *)asset_name, zip_path ) == 0 ) {
        status = util_unzip_to_directory ( zip_path, dest_dir );
        remove ( zip_path );
    }

    return status;
}
#endif

#ifdef _WIN32
/******************************************************************************
**  Function   :  util_win_seed_conf_from_defaults
**
**  Objective  :  First-run seeding, fast path.  Copies only kat5200.conf (a
**                single file) so config_init can read it immediately, then
**                arms the deferred profile copy: the ~14k default profile
**                files are NOT touched here.  util_first_use_seed_worker (run
**                on a background thread once the GUI is up) does that copy,
**                so the user sees the window in well under a second instead of
**                staring at a console for ~10s.
**
**  Parameters :  dest_dir - live data directory to seed into
**
**  return     :  0 if the config file copied, otherwise -1
******************************************************************************/
static int util_win_seed_conf_from_defaults ( const char *dest_dir )
{
    char src_dir[KAT_PATH_MAX];
    char src[KAT_PATH_MAX];
    char dst[KAT_PATH_MAX];
    int status = -1;

    if ( !dest_dir || !strlen(dest_dir) || !strlen(g_app_path) )
        return -1;

    sprintf ( src_dir, "%s\\defaults", g_app_path );

    /* Never copy onto ourselves */
    if ( _stricmp ( src_dir, dest_dir ) == 0 )
        return -1;

    sprintf ( src, "%s\\kat5200.conf", src_dir );
    sprintf ( dst, "%s\\kat5200.conf", dest_dir );
    if ( CopyFileA ( src, dst, 0 ) )
        status = 0;

    /* Defer the heavy profile copy to a background thread (see worker), but
     * only if the conf restored: a failed copy must not still reset profiles. */
    if ( status == 0 )
        util_defer_profile_seed ( src_dir, dest_dir );

    return status;

} /* end util_win_seed_conf_from_defaults */
#endif

void util_init ( char *p_path ) {

    char new_directory[KAT_PATH_MAX];
    FILE *fp;

    g_portable = 0;
    g_app_path[0] = '\0';

    if ( p_path && *p_path ) {
        strncpy ( g_app_path, p_path, sizeof(g_app_path) - 1 );
        g_app_path[sizeof(g_app_path) - 1] = '\0';
        util_strip_file_to_directory ( g_app_path );
    }

    /*
     * Figure out the program config path
     *   Check OS X, then Windows, then Android, then Linux for users home 
     *   directory
     */
    
#ifdef __APPLE__
    CFBundleRef mainBundle;
    CFURLRef fileURL;
    char cpath[KAT_PATH_MAX];
    const char *home;

    mainBundle = CFBundleGetMainBundle();

    /*
     * Live, writable data always lives in ~/Library/Application Support/kat5200,
     * never inside the (read-only, code-signed, possibly translocated) .app
     * bundle.  There is intentionally no "portable" mode on macOS: a relocatable
     * self-contained app IS the .app, and writing config into it is exactly the
     * anti-pattern Application Support exists to avoid.
     */
    home = getenv ( "HOME" );
    if ( !home || !*home )
        home = ".";
    snprintf ( g_program_path, sizeof(g_program_path),
               "%s/Library/Application Support/kat5200", home );

    /*
     * If the live conf already exists, adopt that directory and we are done.
     */
    snprintf ( new_directory, sizeof(new_directory),
               "%s/kat5200.conf", g_program_path );
    fp = fopen ( new_directory, "r" );
    if ( fp ) {
        fclose ( fp );
        return;
    }

    /*
     * First run: create the support dir (tolerating one that already exists --
     * e.g. an interrupted earlier run) and seed conf + bmp from the bundle's
     * Resources, then defer the heavy profile copy to the background worker.
     */
    if ( !mainBundle )
        return;

    util_mkdir ( g_program_path, UTIL_DEFAULT_DIR_MODE );

    fileURL = CFBundleCopyResourceURL ( mainBundle, CFSTR("kat5200"),
                                        CFSTR("conf"), NULL );
    if ( fileURL ) {
        CFURLGetFileSystemRepresentation ( fileURL, true,
                                           (UInt8*)cpath, sizeof(cpath) );
        CFRelease ( fileURL );
        snprintf ( new_directory, sizeof(new_directory),
                   "%s/kat5200.conf", g_program_path );
        util_file_copy ( cpath, new_directory );
    }

    fileURL = CFBundleCopyResourceURL ( mainBundle, CFSTR("kat5200"),
                                        CFSTR("bmp"), NULL );
    if ( fileURL ) {
        CFURLGetFileSystemRepresentation ( fileURL, true,
                                           (UInt8*)cpath, sizeof(cpath) );
        CFRelease ( fileURL );
        snprintf ( new_directory, sizeof(new_directory),
                   "%s/kat5200.bmp", g_program_path );
        util_file_copy ( cpath, new_directory );
    }

    {
        CFURLRef resDirURL = CFBundleCopyResourcesDirectoryURL ( mainBundle );
        if ( resDirURL ) {
            char res_path[KAT_PATH_MAX];
            CFURLGetFileSystemRepresentation ( resDirURL, true,
                                               (UInt8*)res_path, sizeof(res_path) );
            CFRelease ( resDirURL );
            util_defer_profile_seed ( res_path, g_program_path );
        }
    }

/*
 * Android
 */
#elif defined __ANDROID__

    /* Live data lives in app internal storage; seed it from the APK assets on
     * first run.  Copy only the small config synchronously, then defer the large
     * profile zip until the GUI can show the first-run wait modal. */
    char shader_dir[KAT_PATH_MAX];

    strcpy ( g_program_path, SDL_GetAndroidInternalStoragePath() );
    util_make_path ( g_program_path, "kat5200.conf", new_directory );
    SDL_IOStream *io = SDL_IOFromFile(new_directory, "rb");
    if ( !io ) {
        if ( util_file_copy ( "kat5200.conf", new_directory ) == 0 )
            util_defer_android_profile_asset_seed ( g_program_path );
        util_extract_asset_zip ( "shaders.zip", g_program_path );
    }
    else {
        SDL_CloseIO ( io );
    }

    util_make_path ( g_program_path, "shaders/bundles", shader_dir );
    if ( !util_directory_exists(shader_dir) )
        util_extract_asset_zip ( "shaders.zip", g_program_path );

/*
 * Linux
 */
#elif !defined WIN32

    int error;
    
    strcpy ( g_program_path, "" );
    glob_t glob_result;

    /*
     * Check for being portable
     */
    strcpy ( new_directory, p_path );
    util_strip_file_to_directory ( new_directory );
    strcat ( new_directory, "/portable.txt" );
    fp = fopen ( new_directory, "r" );
    if ( fp ) {
        while (fgets(new_directory, sizeof(new_directory), fp)) {
            if (new_directory[0] == '#')
                continue;

            if (strstr(new_directory, "1"))
                g_portable = 1;
        }
        fclose ( fp );
    }
    if (g_portable) {
        strcpy ( new_directory, p_path );
        util_strip_file_to_directory ( new_directory );
        strcpy ( g_program_path, new_directory );
        return;
    }

    glob ( "~", GLOB_TILDE_CHECK, NULL, &glob_result );
    sprintf ( new_directory, "%s/.config/kat5200/kat5200.conf", *(glob_result.gl_pathv) );
    fp = fopen ( new_directory, "r" );
    if ( fp ) {
        sprintf ( g_program_path, "%s/.config/kat5200", *(glob_result.gl_pathv) );
        fclose ( fp );
    }

    /*
     * If there is an /etc then use that and try to create home
     * directory for files
     */
    else {
        char seed_file[KAT_PATH_MAX];

        sprintf ( seed_file, "%s/kat5200.conf", KAT_DATADIR );
        fp = fopen ( seed_file, "r" );
        if ( fp ) {
            fclose ( fp );
            strcpy ( g_program_path, KAT_DATADIR );
            sprintf ( new_directory, "%s/.config", *(glob_result.gl_pathv) );

            util_mkdir(new_directory, UTIL_DEFAULT_DIR_MODE);

            sprintf ( new_directory, "%s/.config/kat5200", *(glob_result.gl_pathv) );

            if ( !(error=util_mkdir(new_directory, UTIL_DEFAULT_DIR_MODE)) ) {

                sprintf ( g_program_path, "%s/.config/kat5200", *(glob_result.gl_pathv) );

                sprintf ( new_directory, "%s/.config/kat5200/kat5200.conf", *(glob_result.gl_pathv) );
                sprintf ( seed_file, "%s/kat5200.conf", KAT_DATADIR );
                util_file_copy ( seed_file, new_directory );

                sprintf ( new_directory, "%s/.config/kat5200/kat5200.bmp", *(glob_result.gl_pathv) );
                sprintf ( seed_file, "%s/kat5200.bmp", KAT_DATADIR );
                util_file_copy ( seed_file, new_directory );

                /* Defer the heavy profile copy to a background thread. */
                sprintf ( new_directory, "%s/.config/kat5200", *(glob_result.gl_pathv) );
                util_defer_profile_seed ( KAT_DATADIR, new_directory );
            }
        }

    } /* else if not in home directory, create from /etc */

    globfree ( &glob_result );
    
    
/*
 * Windows
 */

#else
    char userAppDir[KAT_PATH_MAX];
    char userMinorAppDir[KAT_PATH_MAX];
    int error;

    strcpy ( g_program_path, "" );

    /*
     * Record the executable directory (app dir).  Holds the static window
     * icon and the pristine <exe>\defaults seed directory.
     */
    /*
     * Check for being portable
     */
    sprintf ( new_directory, "%s\\portable.txt", g_app_path );
    fp = fopen ( new_directory, "r" );
    if ( fp ) {
        while (fgets(new_directory, sizeof(new_directory), fp)) {
            if (new_directory[0] == '#')
                continue;

            if (strstr(new_directory, "1"))
                g_portable = 1;
        }
        fclose ( fp );
    }
    if (g_portable) {
        strcpy ( g_program_path, g_app_path );

        /* Seed live data beside the exe from defaults on first run */
        sprintf ( new_directory, "%s\\kat5200.conf", g_program_path );
        fp = fopen ( new_directory, "r" );
        if ( fp )
            fclose ( fp );
        else
            util_win_seed_conf_from_defaults ( g_program_path );
        return;
    }

    /*
     *   If Windows, check for users roaming profile
     */
    SHGetFolderPathA( NULL, CSIDL_APPDATA, NULL, 0, userMinorAppDir );
    sprintf ( userAppDir, "%s\\kat5200", userMinorAppDir );

    sprintf ( new_directory, "%s\\kat5200.conf", userAppDir );

    fp = fopen ( new_directory, "r" );
    if ( fp ) {
        strcpy ( g_program_path, userAppDir );
        fclose ( fp );
    }

    // Create roaming profile directory and seed it from <exe>\defaults
    else {

        error = util_mkdir(userAppDir, UTIL_DEFAULT_DIR_MODE);
        if ( error ) {
            DWORD attr = GetFileAttributesA(userAppDir);
            if ( attr != INVALID_FILE_ATTRIBUTES && (attr & FILE_ATTRIBUTE_DIRECTORY) )
                error = 0;
        }
        if ( !error ) {
            strcpy ( g_program_path, userAppDir );
            util_win_seed_conf_from_defaults ( userAppDir );
        }
    }

#endif

    return;

} /* end util_init */

/******************************************************************************
**  Function   :  util_reset_config                                       
**                                                                    
**  Objective  :  This function copies the default config + profiles to the
**                active live data directory (g_program_path).  On Windows the
**                source is the pristine <exe>\defaults directory, so both
**                portable and roaming installations are supported.
**
**                p_path is accepted for backward compatibility (the command
**                line passes argv[0]) but is no longer used: the source and
**                destination are derived from g_app_path / g_program_path,
**                which lets the GUI call util_reset_config(NULL) safely.
**
**  Parameters :  p_path - unused, retained for caller compatibility
**
**  return     :  0 for success, otherwise failure
**
******************************************************************************/
int util_reset_config ( char *p_path ) {

    char new_directory[KAT_PATH_MAX];
    FILE *fp;
    int status = -1;

    (void)p_path;

/*
 * Apple
 */
#ifdef __APPLE__
    CFBundleRef mainBundle;
    char  cpath[KAT_PATH_MAX];
    CFURLRef  fileURL;

    mainBundle = CFBundleGetMainBundle();

    fileURL = CFBundleCopyResourceURL( mainBundle,
                                         CFSTR("kat5200"),
                                         CFSTR("conf"),
                                         NULL );
    if ( fileURL ) {
        CFURLGetFileSystemRepresentation ( fileURL, true,
                                           (UInt8*)cpath, sizeof(cpath) );
        CFRelease ( fileURL );
        sprintf ( new_directory, "%s/kat5200.conf", g_program_path );
        status = util_file_copy ( cpath, new_directory );
    }

    /* Defer the heavy profile copy to the background seed worker, but only if
     * the conf actually restored.                                            */
    if ( status == 0 ) {
        CFURLRef resDirURL = CFBundleCopyResourcesDirectoryURL ( mainBundle );
        if ( resDirURL ) {
            char res_path[KAT_PATH_MAX];
            CFURLGetFileSystemRepresentation ( resDirURL, true,
                                               (UInt8*)res_path, sizeof(res_path) );
            CFRelease ( resDirURL );
            util_defer_profile_seed ( res_path, g_program_path );
        }
    }

/*
 * Android
 */
#elif defined __ANDROID__

    char db_file[KAT_PATH_MAX];

    /* Restore the default conf synchronously, then defer the larger profile zip
     * to the GUI seed worker.  The CLI reset path runs that worker inline. */
    util_make_path ( g_program_path, "kat5200.conf", db_file );
    status = util_file_copy ( "kat5200.conf", db_file );

    if ( status == 0 ) {
        util_defer_android_profile_asset_seed ( g_program_path );
        util_extract_asset_zip ( "shaders.zip", g_program_path );
    }

/*
 * Linux
 */
#elif !defined WIN32

    char seed_file[KAT_PATH_MAX];

    /* Copy the default conf over the active live data dir (g_program_path),
     * which util_init resolved to the real config dir for both the normal and
     * the KAT_DATADIR-fallback cases.  Defer the heavy profile copy to the
     * background seed worker so the UI does not freeze, and only do so if the
     * conf actually restored.                                                */
    sprintf ( seed_file, "%s/kat5200.conf", KAT_DATADIR );
    fp = fopen ( seed_file, "r" );
    if ( fp ) {
        fclose ( fp );

        sprintf ( new_directory, "%s/kat5200.conf", g_program_path );
        status = util_file_copy ( seed_file, new_directory );

        if ( status == 0 )
            util_defer_profile_seed ( KAT_DATADIR, g_program_path );
    }

/*
 * Windows
 */
#else

    /*
     * Restore the conf from <exe>\defaults into the active live data directory
     * and arm the deferred profile copy.  Works for both portable (live data
     * beside the exe) and roaming installs because the source is the read-only
     * defaults dir, never the live data itself.  The heavy profile copy runs
     * on the background seed worker so the UI does not freeze.
     */
    (void)fp;
    (void)new_directory;
    status = util_win_seed_conf_from_defaults ( g_program_path );

#endif

    /* Only suppress later conf writes if the default conf actually restored;
     * otherwise a failed reset would also block the normal shutdown save.    */
    if ( status == 0 )
        g_factory_reset_done = 1;

    return status;

} /* end util_reset_config */

/******************************************************************************
**  Function   :  util_factory_reset_cli
**
**  Objective  :  Command line factory reset.  Restore the default conf and
**                profiles synchronously, then return so the caller can exit
**                without starting the GUI or any emulation.  Unlike the GUI
**                reset button, the large profile copy is run inline (not on
**                the deferred background worker): the --reset_config flag is a
**                recovery path for an install that will not boot, so it must
**                not depend on the GUI ever coming up to finish the copy.
**
**                Must be called after util_init has resolved the live data
**                directory (g_program_path) and after logger_init so copy
**                failures are logged.
**
**  Parameters :  NONE
**
**  return     :  0 for success, otherwise failure
**
******************************************************************************/
int util_factory_reset_cli ( void )
{
    int status;

    /* Restore the default conf and arm the (deferred) profile copy. */
    status = util_reset_config ( NULL );
    if ( status != 0 )
        return status;

    /* Run the armed profile copy right here instead of waiting on the GUI's
     * background worker, then report any copy failure. */
    util_first_use_seed_worker ( NULL );
    if ( util_first_use_seed_error() )
        return -1;

    return 0;

} /* end util_factory_reset_cli */

/******************************************************************************
**  Function   :  util_set_program_path                                       
**                                                                    
**  Objective  :  This function sets the program path aka working directory
**                all filnames are based off this.  This would be called from
**                config to set directory where conf and profiles are.
**                                                   
**  Parameters :  p_path  - program path to set, if a file we set to directory
**                 
**  return     :  0 is success, otherwise failure
**
******************************************************************************/ 
int util_set_program_path ( char *p_path ) {

    if ( !p_path || !strlen(p_path) )
        return -1;

    strcpy ( g_program_path, p_path );

    util_strip_file_to_directory ( p_path );

    return 0;

} /* end util_set_program_path */

const char * util_get_program_path ( void ) {
    return g_program_path;
}

/******************************************************************************
**  Function   :  util_strip_file_to_directory                                       
**                                                                    
**  Objective  :  This function is used to strip a filename down to it's
**                directory name
**                                                   
**  Parameters :  filename  - pointer to the filename
**                 
**  return     :  0
**  
******************************************************************************/ 
int util_strip_file_to_directory ( char * filename ) {

    struct stat file_status;
    char *temp_ptr;

    if ( !stat ( filename, &file_status ) ) {
        if ( !(file_status.st_mode & S_IFDIR) ) {
            if ( (temp_ptr=strrchr(filename,'/')) )
                *temp_ptr = '\0';
            else if ( (temp_ptr=strrchr(filename,'\\')) )
                *temp_ptr = '\0';
            else
                *filename = '\0';
        }
    }

    /*
     * If file doesn't exist assume we just strip
     */
    else {
        if ( (temp_ptr=strrchr(filename,'/')) )
            *temp_ptr = '\0';
        else if ( (temp_ptr=strrchr(filename,'\\')) )
            *temp_ptr = '\0';
        else
            *filename = '\0';
    }

    return 0;

} /* end util_strip_file_to_directory */

/******************************************************************************
**  Function   :  util_strip_to_file                                       
**                                                                    
**  Objective  :  This function is used to strip an absolute pathname down to
**                it's filename
**                                                   
**  Parameters :  filename  - pointer to the filename
**                 
**  return     :  0
**
******************************************************************************/ 
int util_strip_to_file ( char * filename ) {

    struct stat file_status;
    char *temp_ptr;

    if ( !stat ( filename, &file_status ) ) {
        /* memmove, not strcpy: src points into filename (overlapping regions). */
        if ( (temp_ptr=strrchr(filename,'\\')) )
            memmove ( filename, temp_ptr+1, strlen(temp_ptr+1)+1 );
        if ( (temp_ptr=strrchr(filename,'/')) )
            memmove ( filename, temp_ptr+1, strlen(temp_ptr+1)+1 );
    }

    return 0;

} /* end util_strip_to_file */

/******************************************************************************
**  Function   :  util_set_file_from_relative_dir                                       
**                                                                    
**  Objective  :  This function takes an absolute filename and checks if it is
**                relative to the input directory and strips the directory off
**                if so
**                                                   
**  Parameters :  filename  - pointer to the filename to strip
**                directory - directory to check for relative path
**                 
**  return     :  0
**
******************************************************************************/ 
int util_set_file_from_relative_dir ( char *filename, char *directory ) {

    char temp[KAT_PATH_MAX];

    if ( !strlen(filename) || !strlen(directory) )
        return 0;

    if ( strstr(filename, directory) ) {
        strcpy ( temp, filename+strlen(directory)+1 );
        strcpy ( filename, temp );
    }

    return 0;

} /* end util_set_file_from_relative_dir */

/******************************************************************************
**  Function   :  util_set_file_from_program_dir                                       
**                                                                    
**  Objective  :  This function takes an absolute filename and checks if it is
**                relative to the program directory and strips the 
**                directory off if so
**                                                   
**  Parameters :  filename  - pointer to the filename to strip
**                 
**  return     :  0
**
******************************************************************************/ 
int util_set_file_from_program_dir ( char *filename ) {

    if ( !strlen(filename) )
        return 0;

    util_set_file_from_relative_dir ( filename, g_program_path );

    return 0;

} /* end util_set_file_from_program_dir */

/******************************************************************************
**  Function   :  util_set_file_to_program_dir                                       
**                                                                    
**  Objective  :  This function checks for a file being relative and if we
**                are not in the program directory it creates an absolute
**                path with the program directory.
**                                                   
**  Parameters :  filename  - pointer to the filename to strip
**                 
**  return     :  0
**
******************************************************************************/ 
int util_set_file_to_program_dir ( char *filename ) {

    char *directory;
    char *path;

    if ( !strlen(filename) )
        return 0;

    if ( (filename[0] == '/') || (filename[1] == ':') )
        return 0;

    directory = malloc ( sizeof(char)*KAT_PATH_MAX );
    getcwd ( directory, KAT_PATH_MAX );

    path = malloc ( sizeof(char)*KAT_PATH_MAX );
    strcpy ( path, g_program_path );

#ifdef _WIN32
    int i = -1;
    
    while ( path[++i] ) path[i] = tolower(g_program_path[i]);
    i = -1;
    while ( directory[++i] ) directory[i] = tolower(directory[i]);
#endif

    if ( strstr(directory,path) ){
        free(directory);
        free(path);
        return 0;
    }

    util_make_path ( g_program_path, filename, path );
    strcpy ( filename, path );

    free(directory);
    free(path);

    return 0;

} /* end util_set_file_to_program_dir */

/******************************************************************************
**  Function   :  util_set_file_to_app_dir
**
**  Objective  :  Resolve a relative filename against the executable/app dir
**                (g_app_path) rather than the live data dir.  Used for static
**                app assets that ship beside the exe and are never copied into
**                live data (e.g. the window-icon bitmap on Windows).  No-op for
**                absolute paths or when the app dir is unknown.
**
**  Parameters :  filename - pointer to the filename to resolve in place
**
**  return     :  0
******************************************************************************/
int util_set_file_to_app_dir ( char *filename ) {

    char path[KAT_PATH_MAX];

    if ( !strlen(filename) || !strlen(g_app_path) )
        return 0;

    if ( (filename[0] == '/') || (filename[0] == '\\') || (filename[1] == ':') )
        return 0;

    util_make_path ( g_app_path, filename, path );
    strcpy ( filename, path );

    return 0;

} /* end util_set_file_to_app_dir */

/******************************************************************************
**  Function   :  util_set_file_from_current_dir                                       
**                                                                    
**  Objective  :  This function takes an absolute filename and checks if it is
**                relative to the current working directory and strips the 
**                directory off if so
**                                                   
**  Parameters :  filename  - pointer to the filename to strip
**                 
**  return     :  0
**
******************************************************************************/ 
int util_set_file_from_current_dir ( char *filename ) {

    char *directory;

    directory = malloc ( sizeof(char)*KAT_PATH_MAX );
    getcwd ( directory, KAT_PATH_MAX );

    util_set_file_from_relative_dir ( filename, directory );

    free ( directory );

    return 0;

} /* end util_set_file_from_current_dir */

/******************************************************************************
**  Function   :  util_set_file_to_current_dir                                       
**                                                                    
**  Objective  :  This function checks for a file being relative and appends
**                to current working directory if so to make an absolute name
**                                                   
**  Parameters :  filename  - pointer to the filename to setup
**                 
**  return     :  0
**
******************************************************************************/ 
int util_set_file_to_current_dir ( char *filename ) {

    char *directory;
    char *path;

    if ( !strlen(filename) )
        return 0;

    if ( (filename[0] == '/') || (filename[1] == ':') )
        return 0;

    directory = malloc ( sizeof(char)*KAT_PATH_MAX );
    getcwd ( directory, KAT_PATH_MAX );

    path = malloc ( sizeof(char)*KAT_PATH_MAX );

    util_make_path ( directory, filename, path );
    strcpy ( filename, path );

    free(directory);
    free(path);

    return 0;

} /* end util_set_file_to_program_dir */

/******************************************************************************
**  Function   :  util_get_dir_seperator_character                                       
**                                                                    
**  Objective  :  This function gets the directory seperator character based
**                on the current working directory (absolute) and returns it
**                if so
**                                                   
**  Parameters :  NONE
**                 
**  return     :  seperator character
** 
******************************************************************************/ 
char util_get_dir_seperator_character ( void ) {

    char *filename;
    char tmp;

    filename = malloc ( sizeof(char)*KAT_PATH_MAX );

    getcwd ( filename, KAT_PATH_MAX );

    if ( strrchr(filename,'\\') )
        tmp = '\\';
    else
        tmp = '/';

    free ( filename );

    return tmp;

} /* end util_get_dir_seperator_character */

/******************************************************************************
**  Function   :  util_delete_delimeters                                       
**                                                                    
**  Objective  :  This function deletes the single or double quotes from the 
**                string
**                                                   
**  Parameters :  NONE
**                 
**  return     :  seperator character
** 
******************************************************************************/ 
int util_delete_delimeters ( char *string ) {

    char *tmp = 0;

    if ( string[0] == '"' || string[0] == '\'' ) {
        memmove ( string, string+1, strlen(string) );  /* overlapping: shift left in place */
    }

    if ( (tmp=strstr(string, "\"")) || (tmp=strstr(string, "'")) )
        *tmp = '\0';

    return 0;

} /* end util_delete_delimeters */

size_t util_trim_whitespace(char *out, size_t len, const char *str)
{
  if(len == 0)
    return 0;

  const char *end;
  size_t out_size;

  // Trim leading space
  while(isspace((unsigned char)*str)) str++;

  if(*str == 0)  // All spaces?
  {
    *out = 0;
    return 1;
  }

  // Trim trailing space
  end = str + strlen(str) - 1;
  while(end > str && isspace((unsigned char)*end)) end--;
  end++;

  // Set output size to minimum of trimmed string length and buffer size minus 1
  out_size = (end - str) < len-1 ? (end - str) : len-1;

  // Copy trimmed string and add null terminator
  memmove(out, str, out_size);
  out[out_size] = 0;

  return out_size;

} /* util_trim_whitespace */

/******************************************************************************
**  Function   :  util_all_whitespace
**                                                                    
**  Objective  :  This function tests a string for being all whitespace char
**                Basically copied from sqlite3 shell.c
**                                                   
**  Parameters :  z  - string to test
**                 
**  return     :  1 if all whitespace, 0 otherwise
** 
******************************************************************************/ 
int util_all_whitespace(const char *z){
  for(; *z; z++){
    if( isspace(*(unsigned char*)z) ) continue;
    if( *z=='/' && z[1]=='*' ){
      z += 2;
      while( *z && (*z!='*' || z[1]!='/') ){ z++; }
      if( *z==0 ) return 0;
      z++;
      continue;
    }
    if( *z=='-' && z[1]=='-' ){
      z += 2;
      while( *z && *z!='\n' ){ z++; }
      if( *z==0 ) return 1;
      continue;
    }
    return 0;
  }
  return 1;
}

int util_is_command_terminator(const char *zLine){
  while( isspace(*(unsigned char*)zLine) ){ zLine++; };
  if( zLine[0]=='/' && util_all_whitespace(&zLine[1]) ) return 1;  /* Oracle */
  if( tolower(zLine[0])=='g' && tolower(zLine[1])=='o'
         && util_all_whitespace(&zLine[2]) ){
    return 1;  /* SQL Server */
  }
  return 0;
}

int util_ends_with_semicolon(const char *z, int N){
  while( N>0 && isspace((unsigned char)z[N-1]) ){ N--; }
  return N>0 && z[N-1]==';';
}

char *util_strlwr(char *str) {

  unsigned char *p = (unsigned char *)str;

  while (*p) {
     *p = tolower((unsigned char)*p);
      p++;
  }

  return str;

} /* end util_strlwr */

char * util_alloc_string ( char **s_ptr, const char *value ) {

    if (*s_ptr)
        free(*s_ptr);

    *s_ptr = (char *)malloc(strlen((value)) + 1);
    strcpy ( *s_ptr, value );

    return *s_ptr;

} /* end util_alloc_string */

/******************************************************************************
**  Function   :  util_search_and_replace
**                                                                    
**  Objective  :  This function replaces sub-string with replace string
**                                                   
**  Parameters :  string  - string to change
**                find    - string to find
**                replace - replacement string
**                 
**  return     :  1 if replacements made, 0 otherwise
** 
******************************************************************************/ 
int util_search_and_replace ( char *string, const char *find, const char *replace ) {

    char *original;
    char *place;
    char *find_loc;
    int status = 0;

    original = malloc ( strlen(string) + 1 );

    strcpy ( original, string );

    place = original;
    find_loc = strstr ( place, find );
    while ( *place != '\0' ) {
        if ( place == find_loc ) {
            status = 1;
            strncpy ( string, replace, strlen(replace) );
            place += strlen(find);
            string += strlen(replace);
            find_loc = strstr ( place, find );
        }
        else {
            *string = *place;
            ++string;
            ++place;
        }
    }
    *string = '\0';

    free ( original );

    return status;

} /* end util_search_and_replace */

/******************************************************************************
**  Function   :  util_replace_and_copy
**                                                                    
**  Objective  :  This function replaces sub-string with replace string. It
**                 allocates memory that must be freed by the caller
**                                                   
**  Parameters :  string1 - string to change
**                string2 - string to copy to
**                find    - string to find
**                replace - replacement string
**                 
**  return     :  new string
** 
******************************************************************************/ 
char * util_replace_and_copy ( const char *string1, const char *find, const char *replace ) {

    char *original;
    char *original2;
    char *place;
    char *string2;
    char *find_loc;
    int num_found = 0;

    original = malloc ( strlen(string1) + 1 );
    strcpy ( original, string1 );

    /* Pre-find how many times we replace */
    place = original;
    while ( (find_loc = strstr ( place, find )) ) {
        ++num_found;
        place = find_loc + 1;
        if ( *place == '\0' )
            break;
    }

    string2 = malloc ( strlen(string1) + strlen(replace) * num_found + 1 );
    original2 = string2;
    strcpy ( string2, string1 );

    place = original;
    find_loc = strstr ( place, find );
    while ( *place != '\0' ) {
        if ( place == find_loc ) {
            strncpy ( string2, replace, strlen(replace) );
            place += strlen(find);
            string2 += strlen(replace);
            find_loc = strstr ( place, find );
        }
        else {
            *string2 = *place;
            ++string2;
            ++place;
        }
    }
    *string2 = '\0';

    free ( original );

    return original2;

} /* end util_replace_and_copy */

/******************************************************************************
**  Function   :  util_make_path
**                            
**  Objective  :  This function creates a path from a directory and filename
**
**  Parameters :  dir     - directory to make path from
**                file    - file to make path from
**                path    - return string containing path
**                                                
**  return     :  Error code
**      
******************************************************************************/
int util_make_path ( char *dir, const char *file, char *path ) {

    size_t dir_len = strlen(dir);
    int needs_sep = dir_len == 0 || (dir[dir_len-1] != '/' && dir[dir_len-1] != '\\');

    if (needs_sep)
        sprintf ( path, "%s%c%s", dir, util_get_dir_seperator_character(), file );
    else
        sprintf ( path, "%s%s", dir, file );

    return 0;

} /* end util_make_path */

char * util_make_path_alloc ( char *dir, const char *file  ) {

    char *path;
    size_t dir_len;

    if ( !dir ) dir = "";
    dir_len = strlen(dir);

    int needs_sep = dir_len == 0 || (dir[dir_len-1] != '/' && dir[dir_len-1] != '\\');
    path = malloc(dir_len + strlen(file) + 2);

    if (needs_sep)
        sprintf ( path, "%s%c%s", dir, util_get_dir_seperator_character(), file );
    else
        sprintf ( path, "%s%s", dir, file );

    return path;

} /* end util_make_path_alloc */

/******************************************************************************
**  Function   :  util_file_copy
**                            
**  Objective  :  This function copy a file
**
**  Parameters :  from    - file to copy from
**                to      - file to create
**                                                
**  return     :  Error code
**      
******************************************************************************/
int util_file_copy ( char *from, char *to ) {

    long nbytes;
    int status = -1;
#ifdef __ANDROID__
    SDL_IOStream *fd1 = SDL_IOFromFile(from, "rb");
#else
    int fd1 = open(from,(O_RDONLY));
#endif
    int fd2 = open(to,(O_WRONLY | O_CREAT | O_TRUNC),S_IWRITE | S_IREAD);
    static char buffer[512];

    if (fd1 >= 0 && fd2 >= 0) {
        status = 0;
#ifdef __ANDROID__
        while ((nbytes = SDL_ReadIO(fd1,buffer,512)) > 0)
#else
        while ((nbytes = read(fd1,buffer,512)) > 0)
#endif
            if (write(fd2,buffer,nbytes) != nbytes) {
                /* Write error */
                status = -1;
                break;       /* Write error */
            }

        /* Was there a read error? */
        if (nbytes == -1)
        status = -1;
    }

    if (fd1 >= 0)
#ifdef __ANDROID__
        SDL_CloseIO(fd1);
#else
        close(fd1);
#endif
    if (fd2 >= 0)
        close(fd2);

   return status;

} /* end util_file_copy */

/******************************************************************************
**  Function   :  util_create_program_path
**
**  Objective  :  This function creates the path to program config files
** 
**  Parameters :  path     - address to store returned file path
**                filename - filename to append to program directory
**                                               
**  return     :  path
******************************************************************************/ 
char * util_create_program_path ( char *path, char *filename ) {

    if ( strlen(g_program_path) )
        util_make_path ( g_program_path, filename, path );
    else
        sprintf ( path, "%s", filename );

    return path;

} /* end util_create_program_path */

/******************************************************************************
**  Function   :  util_create_program_path_alloc
**
**  Objective  :  This function creates the path to program config files and 
**                allocates memory for the new file.  It will free input.
** 
**  Parameters :  path     - address to store returned file path
**                filename - filename to append to program directory
**                                               
**  return     :  path
******************************************************************************/ 
char * util_create_program_path_alloc ( char *filename ) {

    char *new;

    if ( !filename )
        return NULL;

    if ( !filename[0] || (filename[0] == '/') ||
          (filename[1] && filename[1] == ':') || !strlen(g_program_path) ) {

        new = malloc ( strlen(filename) + 1 );
        strcpy ( new, filename );
    }
    else {
        new = malloc ( strlen(filename) + strlen(g_program_path) + 2 );
        util_make_path ( g_program_path, filename, new );
    }

    return new;

} /* end util_create_program_path_alloc */

/******************************************************************************
**  Function   :  util_create_seed_path_alloc
**
**  Objective  :  Create a path to package-provided seed data.  This is the
**                read-only tree that contains kat5200.conf, profiles.*, and
**                shaders/ in installed/app-bundle builds.
**
**  Parameters :  filename - filename/path below the seed root
**
**  return     :  allocated path, caller frees
******************************************************************************/
char * util_create_seed_path_alloc ( char *filename ) {

    char base[KAT_PATH_MAX];

#ifdef __APPLE__
    CFBundleRef mainBundle = CFBundleGetMainBundle();
    CFURLRef resDirURL;

    if ( mainBundle ) {
        resDirURL = CFBundleCopyResourcesDirectoryURL ( mainBundle );
        if ( resDirURL ) {
            if ( CFURLGetFileSystemRepresentation ( resDirURL, true,
                        (UInt8*)base, sizeof(base) ) ) {
                CFRelease ( resDirURL );
                return util_make_path_alloc ( base, filename );
            }
            CFRelease ( resDirURL );
        }
    }
#elif defined _WIN32
    if ( strlen(g_app_path) ) {
        snprintf ( base, sizeof(base), "%s\\defaults", g_app_path );
        return util_make_path_alloc ( base, filename );
    }
#elif defined __ANDROID__
    return util_create_program_path_alloc ( filename );
#else
    snprintf ( base, sizeof(base), "%s", KAT_DATADIR );
    return util_make_path_alloc ( base, filename );
#endif

    return util_create_program_path_alloc ( filename );

} /* end util_create_seed_path_alloc */

char * util_create_manual_path_alloc ( void )
{
    char *path;
#ifdef KAT_SOURCE_DIR
    char dev_path[KAT_PATH_MAX];
#endif

    path = util_create_seed_path_alloc ( KAT_MANUAL_PDF_NAME );
    if ( path && util_file_exists(path) )
        return path;
    free ( path );

#ifdef KAT_SOURCE_DIR
    if ( snprintf(dev_path, sizeof(dev_path), "%s/doc/guide/site/pdf/%s",
                KAT_SOURCE_DIR, KAT_MANUAL_PDF_NAME) < (int)sizeof(dev_path) &&
            util_file_exists(dev_path) ) {
        path = NULL;
        return util_alloc_string ( &path, dev_path );
    }
#endif

    return NULL;

} /* end util_create_manual_path_alloc */

static int util_file_uri_is_unreserved ( unsigned char c )
{
    return ( c >= 'A' && c <= 'Z' ) ||
           ( c >= 'a' && c <= 'z' ) ||
           ( c >= '0' && c <= '9' ) ||
           c == '.' || c == '_' || c == '~' || c == '-';
}

char * util_create_file_uri_alloc ( const char *path )
{
    static const char hex[] = "0123456789ABCDEF";
    char *uri;
    char *out;
    size_t len;
    size_t i;
    int drive_path;
    int unc_path;
    int absolute_path;
    const char *input;

    if ( !path || !*path )
        return NULL;

    drive_path = isalpha((unsigned char)path[0]) && path[1] == ':';
    unc_path = (path[0] == '/' || path[0] == '\\') &&
               (path[1] == '/' || path[1] == '\\');
    absolute_path = drive_path || unc_path || path[0] == '/' || path[0] == '\\';
    if ( !absolute_path )
        return NULL;

    len = strlen ( path );
    uri = malloc ( 8 + (len * 3) + 1 );
    if ( !uri )
        return NULL;

    strcpy ( uri, "file://" );
    out = uri + strlen(uri);

    if ( drive_path )
        *out++ = '/';

    input = unc_path ? path + 2 : path;
    for ( i = 0; input[i]; ++i ) {
        unsigned char c = (unsigned char)input[i];

        if ( c == '\\' )
            c = '/';

        if ( c == '/' || util_file_uri_is_unreserved(c) ||
                (drive_path && i == 1 && c == ':') ) {
            *out++ = (char)c;
        }
        else {
            *out++ = '%';
            *out++ = hex[c >> 4];
            *out++ = hex[c & 0x0f];
        }
    }

    *out = '\0';
    return uri;

} /* end util_create_file_uri_alloc */

/******************************************************************************
**  Function   :  util_check_for_zip_alloc
**
**  Objective  :  This function checks for the input path containing a zip file
**                It returns the path to the zip and the remaining internal
**                path.  It allocates memory
** 
**  Parameters :  path        - complete path to break into parts
**                zip_file    - output path to zip
**                path_inside - output path inside zip
**                                               
**  return     :  0 if no zip file found, 1 if found
******************************************************************************/ 
int util_check_for_zip_alloc ( char *path, char **zip_file, char **path_inside ) {

    char *tmp;

    if ( path == 0 || strlen(path) == 0 ) {
        *zip_file = 0;
        *path_inside = 0;
        return 0;
    }

    if ( tmp = strstr(path, ".zip") ) {
        if ( tmp[4] == '\0' ) {
            *zip_file = malloc( strlen(path) + 1 );
            strcpy ( *zip_file, path );
            *path_inside = 0;
            return 1;
        }
        else if ( (tmp[4] == '\\' || tmp[4] == '/') ) {
            *zip_file = malloc( tmp+3-path+2 );
            *path_inside = malloc ( strlen(tmp+5) + 1 );

            strncpy ( *zip_file, path, tmp+3-path+1 );
            (*zip_file)[tmp+3-path+1] = '\0';
            strcpy ( *path_inside, tmp+5 );

            return 1;
        }
    }

    *zip_file = 0;
    *path_inside = 0;

    return 0;

} /* end util_check_for_zip_alloc */

/******************************************************************************
**  Function   :  util_set_zip_time_info
**
**  Objective  :  This function sets up the input zip info with the current
**                time.
** 
**  Parameters :  path        - complete path to break into parts
**                zip_file    - output path to zip
**                path_inside - output path inside zip
**                                               
**  return     :  0 if no zip file found, 1 if found
******************************************************************************/ 
void util_set_zip_time_info ( zip_fileinfo *info ) {

    time_t rawtime;
    struct tm *timeinfo;

    time ( &rawtime );
    timeinfo = localtime ( &rawtime );

    info->tmz_date.tm_sec = timeinfo->tm_sec;
    info->tmz_date.tm_min = timeinfo->tm_min;
    info->tmz_date.tm_hour = timeinfo->tm_hour;
    info->tmz_date.tm_mday = timeinfo->tm_mday;
    info->tmz_date.tm_mon = timeinfo->tm_mon;
    info->tmz_date.tm_year = timeinfo->tm_year;

} /* end util_set_zip_time_info */

/******************************************************************************
**  Function   :  util_count_files_in_dir
**
**  Objective  :  This function counts regular files in directory and 
**                (optionally) sub-directories
** 
**  Parameters :  path - directory to search
**                scan_subs - 0 not to scan, otherwise scan sub-directories
**                search_str - sub_string (usually extension of file) to 
**                             search on.  If empty or null, don't use
**                                               
**  return     :  number of total files found
******************************************************************************/ 
int util_count_files_in_dir ( char *path, int scan_subs, char *search_str ) {

    DIR *dir_ptr = NULL;
    struct dirent *direntp;
    char *npath;
    struct stat st;
    if (!path) return 0;
    if( (dir_ptr = opendir(path)) == NULL ) return 0;
    int use_string = search_str && strlen(search_str);

    int count=0;

    while( (direntp = readdir(dir_ptr)))
    {
        if (strcmp(direntp->d_name,".")==0 ||
            strcmp(direntp->d_name,"..")==0) continue;

        npath=malloc(strlen(path)+strlen(direntp->d_name)+2);
        if ( !npath ) continue;
        sprintf(npath,"%s/%s",path, direntp->d_name);

        if ( stat(npath, &st) ) {
            free(npath);
            continue;
        }

        if ( S_ISREG(st.st_mode) ) {
            if ( !use_string || strstr(direntp->d_name,search_str) )
                ++count;
        }
        else if ( S_ISDIR(st.st_mode) && scan_subs ) {
            count += util_count_files_in_dir(npath,scan_subs,search_str);
        }

        free(npath);
    }
    closedir(dir_ptr);

    return count;

} /* end util_count_files_in_dir */

/******************************************************************************
**  Function   :  util_get_crc     
**
**  Objective  :  This function gets the crc32 of the input file
** 
**  Parameters :  file - filename to get crc for   
**                                               
**  return     :  crc32
******************************************************************************/ 
unsigned long util_get_crc ( char *file ) {

    gzFile rom;
    int bytes_read;
    unsigned char *buffer;
    unsigned long crc;

    /* gzopen transparently handles plain or gzipped roms */
    rom = gzopen ( file, "rb" );

    if ( !rom )
        return 0;

    buffer = malloc ( 132000 );

    if ( !buffer ) {
        gzclose ( rom );
        return 0;
    }

    /*
     * Accumulate the CRC one chunk at a time.  gzread is allowed to return
     * fewer bytes than requested even when it is not at end-of-file, so we
     * must never assume that a single (or fixed-size) read returns the whole
     * file.  Folding each chunk into the running CRC sidesteps that entirely
     * and removes any need to know the total size up front.
     */
    crc = crc32 ( 0L, Z_NULL, 0 );

    while ( (bytes_read = gzread ( rom, buffer, 132000 )) > 0 ) {
        crc = crc32 ( crc, buffer, bytes_read );
    }

    free ( buffer );

    gzclose ( rom );

    return crc;

} /* end util_get_crc */

/******************************************************************************
**  Function   :  util_get_all_hash
**
**  Objective  :  This function gets all the different hashes for the input file
** 
**  Parameters :  file - filename to get crc for   
**                crc  - crc checksum to return
**                md5  - md5 hash to return
**                sha1 - sha1 hash to return
**                                               
**  return     :  error code
******************************************************************************/ 
unsigned long util_get_all_hash ( char *file, unsigned long *crc, 
                                              unsigned char md5[16], 
                                              unsigned char sha1[20] ) {

    gzFile rom;
    int bytes_read;
    md5_context ctx;
    sha1_context ctxs;
    unsigned char *buffer;

    *crc = 0;

    /* gzopen transparently handles plain or gzipped roms */
    rom = gzopen ( file, "rb" );

    if ( !rom )
        return -1;

    buffer = malloc ( 132000 );

    if ( !buffer ) {
        gzclose ( rom );
        return -1;
    }

    /*
     * Compute all three digests in a single streaming pass. 
     */
    *crc = crc32 ( 0L, Z_NULL, 0 );
    md5_starts ( &ctx );
    sha1_starts ( &ctxs );

    while ( (bytes_read = gzread ( rom, buffer, 132000 )) > 0 ) {
        *crc = crc32 ( *crc, buffer, bytes_read );
        md5_update ( &ctx, buffer, bytes_read );
        sha1_update ( &ctxs, buffer, bytes_read );
    }

    md5_finish ( &ctx, md5 );
    sha1_finish ( &ctxs, sha1 );

    free ( buffer );

    gzclose ( rom );

    return 0;

} /* end util_get_all_hash */

/******************************************************************************
**  Function   :  util_next_power_of_two
**                                                                    
**  Objective  :  This function finds the next power of 2 from the input number
**                                                   
**  Parameters :  number
**                 
**  return     :  power of 2 equal to or above number
** 
******************************************************************************/ 
int util_next_power_of_two ( int number ) {

    int power = 1;

    while(power < number)
        power*=2;

    return power;

} /* end util_next_power_of_two */

/******************************************************************************
**  Function   :  util_save_font_file
**
**  Objective  :  This function saves the font file to a new .h file
** 
**  Parameters :  file - filename to grab data out of
**                                               
**  return     :  crc32
******************************************************************************/ 
unsigned long util_save_font_file ( char *file ) {

    FILE *rom;
    FILE *dot_h_file;
    int bytes_on_line = 0;
    long byte_was_read = 1;
    unsigned char this_byte;
    char temp_string[10];

    /* First read rom into a buffer and calculate bytes */
    rom = fopen ( file, "rb" );
    dot_h_file = fopen ( "default_font.h", "w" );

    if ( !rom )
        return 0;

    fputs ( "unsigned char default_font[]={\n", dot_h_file );

    while ( byte_was_read ) {
        byte_was_read = fread ( &this_byte, 1, 1, rom );

        if ( !byte_was_read )
            break;
        sprintf ( temp_string, "%d,", this_byte );
        fputs ( temp_string, dot_h_file );
        if ( ++bytes_on_line == 16 ) {
            bytes_on_line = 0;
            fputs ( "\n", dot_h_file );
        }
    }

    fputs ( "};\n", dot_h_file );
    fclose ( rom );
    fclose ( dot_h_file );

    return 0;

} /* end util_save_font_file */

/******************************************************************************
**  Function   :  util_check_for_line_in_file
**
**  Objective  :  This function checks for the input line being in the file
** 
**  Parameters :  file - filename to grab data out of
**                line - line to put in
**                                               
**  return     :  1 if found, 0 otherwise
******************************************************************************/
int util_check_for_line_in_file ( char *file, char *line ) {

    FILE *old_fileptr;
    char buffer[256];
    int status = 0;
    char *place;

    old_fileptr = fopen ( file, "r" );
    if ( !old_fileptr ) {
        return 0;
    }

    while ( fgets(buffer, 255, old_fileptr) ) {
        if (place = strstr(buffer, "\n"))
            *place = '\0';

        if (!strcmp(buffer, line) )
            status = 1;
    }

    fclose ( old_fileptr );

    return status;

} /* end util_check_for_line_in_file */

/******************************************************************************
**  Function   :  util_save_line_alpha_to_file
**
**  Objective  :  This function saves a line to the file putting it in
**                alphabetical order
** 
**  Parameters :  file - filename to grab data out of
**                line - line to put in, if NULL we are just deleting a line
**                search - string to search on, if we find don't write line
**                flags - whether to create file if does not exist
**                                               
**  return     :  error code
******************************************************************************/ 
int util_save_line_alpha_to_file ( char *file, char *line, char *search, int flags ) {

    FILE *old_fileptr;
    FILE *new_fileptr;
    char *new_filename;
    char buffer[256];
    char low_buffer[256];
    char *low_line;
    int i, status;
    int done = 0;

    new_filename = malloc(strlen(file)+5);
    sprintf ( new_filename, "%s.tmp", file );

    /* Open old file or create if caller desires */
    old_fileptr = fopen ( file, "r" );
    if ( !old_fileptr ) {
        if ( flags & UTIL_CREATE_FILE ) {
            old_fileptr = fopen ( file, "w" );
            fclose ( old_fileptr );
            old_fileptr = fopen ( file, "r" );
        }
        else
            return -1;
    }

    /* Open tmp file to write to */
    new_fileptr = fopen ( new_filename, "w" );

    if ( !old_fileptr || !new_fileptr ) {
        free(new_filename);
        return -1;
    }

    /* Create a lowercase string for comparison */
    if ( line ) {
        low_line = malloc(strlen(line)+1);
        strcpy ( low_line, line );
        util_strlwr(low_line );
    }
    else {
        low_line = malloc(1);
        *low_line = '\0';
    }

    /* 
     * Go through each line of file until we get where the new line goes 
     *  in case-insensitive alphabetical order and insert it in
     */
    while ( fgets(buffer, 255, old_fileptr) ) {
        strcpy ( low_buffer, buffer );
        util_strlwr ( low_buffer );
        if ( (status = strcmp(low_line, low_buffer)) < 0 && !done && line ) {
            done = 1;
            fputs(line, new_fileptr);
            fputs("\n", new_fileptr);
        }
        else if ( status == 0 ) {
            done = 1;
        }

        /* put the old line unless we delete on finding search string */
        if ( !strstr(low_buffer, search) || !(flags & UTIL_DELETE_ON_STR) )
            fputs(buffer, new_fileptr);
    }

    /* If no lines in file or this is last, put here at end */
    if ( !done && line ) {
        fputs(line, new_fileptr);
        fputs("\n", new_fileptr);
    }

    fclose ( new_fileptr );
    fclose ( old_fileptr );

    /* Move our new written file to the old one */
    rename ( new_filename, file );

    free( new_filename );
    free( low_line );

    return 0;

} /* end util_save_line_alpha_to_file */

/******************************************************************************
**  Function   :  util_save_header_file
**
**  Objective  :  This function takes data from a file, compresses it and save
**                to a new header file
** 
**  Parameters :  file        - filename to grab data out of
**                header_file - name of file to save to
**                var_name    - name of variable to save in header
**                                               
**  return     :  error code
******************************************************************************/ 
int util_save_header_file ( char *file, char *header_file, char *var_name ) {

    FILE *rom;
    FILE *dot_h_file;
    int bytes_on_line = 0;
    int i = 0;
    char temp_string[10];
    unsigned char *src_buffer;
    unsigned char *dest_buffer;
    unsigned long src_len;
    unsigned long dest_len;

    /*
     * Open file to read in and header file to write
     */
    rom = fopen ( file, "rb" );
    dot_h_file = fopen ( header_file, "w" );

    if ( !rom )
        return 0;

    fputs ( "unsigned char ", dot_h_file );
    fputs ( var_name, dot_h_file );
    fputs ( "[]={\n", dot_h_file );

    /*
     * Compress data
     */
    src_buffer = malloc(100000);
    dest_buffer = malloc(20000);
    src_len = fread ( src_buffer, 1, 100000, rom );
    compress (dest_buffer, &dest_len, src_buffer, src_len);

    while ( i < dest_len ) {
        sprintf ( temp_string, "%d,", dest_buffer[i] );
        fputs ( temp_string, dot_h_file );
        if ( ++bytes_on_line == 16 ) {
            bytes_on_line = 0;
            fputs ( "\n", dot_h_file );
        }
        ++i;
    }

    fputs ( "};\n", dot_h_file );
    fclose ( rom );
    fclose ( dot_h_file );

    free ( src_buffer );
    free ( dest_buffer );

    return 0;

} /* end util_save_header_file */

int util_inflate(const void *src, int srcLen, void *dst, int dstLen) {

    z_stream strm  = {0};
    strm.total_in  = strm.avail_in  = srcLen;
    strm.total_out = strm.avail_out = dstLen;
    strm.next_in   = (Bytef *) src;
    strm.next_out  = (Bytef *) dst;

    strm.zalloc = Z_NULL;
    strm.zfree  = Z_NULL;
    strm.opaque = Z_NULL;

    int err = -1;
    int ret = -1;

    err = inflateInit2(&strm, (15 + 32)); //15 window bits, and the +32 tells zlib to to detect if using gzip or zlib
    if (err == Z_OK) {
        err = inflate(&strm, Z_FINISH);
        if (err == Z_STREAM_END) {
            ret = strm.total_out;
        }
        else {
             inflateEnd(&strm);
             return err;
        }
    }
    else {
        inflateEnd(&strm);
        return err;
    }

    inflateEnd(&strm);
    return ret;
}

typedef struct {
    unsigned char *data;
    ZPOS64_T size;
    ZPOS64_T offset;
    int error;
} t_util_zip_memory_stream;

static voidpf ZCALLBACK util_zip_memory_open ( voidpf opaque, const void *filename, int mode )
{
    t_util_zip_memory_stream *stream = (t_util_zip_memory_stream *)opaque;

    (void)filename;

    if ( !stream )
        return NULL;

    if ( (mode & ZLIB_FILEFUNC_MODE_READWRITEFILTER) != ZLIB_FILEFUNC_MODE_READ )
        stream->error = 1;

    stream->offset = 0;
    return stream;
}

static uLong ZCALLBACK util_zip_memory_read ( voidpf opaque, voidpf stream_ptr, void *buf, uLong size )
{
    t_util_zip_memory_stream *stream = (t_util_zip_memory_stream *)stream_ptr;
    ZPOS64_T remaining;
    uLong to_read = size;

    (void)opaque;

    if ( !stream || !buf ) {
        if ( stream )
            stream->error = 1;
        return 0;
    }

    if ( stream->offset >= stream->size )
        return 0;

    remaining = stream->size - stream->offset;
    if ( (ZPOS64_T)to_read > remaining )
        to_read = (uLong)remaining;

    memcpy ( buf, stream->data + stream->offset, (size_t)to_read );
    stream->offset += to_read;

    return to_read;
}

static uLong ZCALLBACK util_zip_memory_write ( voidpf opaque, voidpf stream_ptr, const void *buf, uLong size )
{
    t_util_zip_memory_stream *stream = (t_util_zip_memory_stream *)stream_ptr;

    (void)opaque;
    (void)buf;
    (void)size;

    if ( stream )
        stream->error = 1;
    return 0;
}

static ZPOS64_T ZCALLBACK util_zip_memory_tell ( voidpf opaque, voidpf stream_ptr )
{
    t_util_zip_memory_stream *stream = (t_util_zip_memory_stream *)stream_ptr;

    (void)opaque;

    if ( !stream )
        return (ZPOS64_T)-1;

    return stream->offset;
}

static long ZCALLBACK util_zip_memory_seek ( voidpf opaque, voidpf stream_ptr, ZPOS64_T offset, int origin )
{
    t_util_zip_memory_stream *stream = (t_util_zip_memory_stream *)stream_ptr;
    ZPOS64_T new_offset;

    (void)opaque;

    if ( !stream )
        return -1;

    switch ( origin ) {
        case ZLIB_FILEFUNC_SEEK_SET:
            new_offset = offset;
            break;
        case ZLIB_FILEFUNC_SEEK_CUR:
            if ( offset > stream->size - stream->offset ) {
                stream->error = 1;
                return -1;
            }
            new_offset = stream->offset + offset;
            break;
        case ZLIB_FILEFUNC_SEEK_END:
            if ( offset != 0 ) {
                stream->error = 1;
                return -1;
            }
            new_offset = stream->size;
            break;
        default:
            stream->error = 1;
            return -1;
    }

    if ( new_offset > stream->size ) {
        stream->error = 1;
        return -1;
    }

    stream->offset = new_offset;
    return 0;
}

static int ZCALLBACK util_zip_memory_close ( voidpf opaque, voidpf stream_ptr )
{
    t_util_zip_memory_stream *stream = (t_util_zip_memory_stream *)stream_ptr;

    (void)opaque;

    if ( stream ) {
        free ( stream->data );
        free ( stream );
    }

    return 0;
}

static int ZCALLBACK util_zip_memory_error ( voidpf opaque, voidpf stream_ptr )
{
    t_util_zip_memory_stream *stream = (t_util_zip_memory_stream *)stream_ptr;

    (void)opaque;

    if ( !stream )
        return 1;

    return stream->error;
}

extern unzFile ZEXPORT util_open_zip_buffer ( const void *buffer, uLong size ) {

    t_util_zip_memory_stream *stream;
    zlib_filefunc64_def filefunc;

    if ( !buffer || !size )
        return NULL;

    stream = malloc ( sizeof(*stream) );
    if ( !stream )
        return NULL;

    stream->data = malloc ( size );
    if ( !stream->data ) {
        free ( stream );
        return NULL;
    }

    memcpy ( stream->data, buffer, size );
    stream->size = size;
    stream->offset = 0;
    stream->error = 0;

    memset ( &filefunc, 0, sizeof(filefunc) );
    filefunc.zopen64_file = util_zip_memory_open;
    filefunc.zread_file = util_zip_memory_read;
    filefunc.zwrite_file = util_zip_memory_write;
    filefunc.ztell64_file = util_zip_memory_tell;
    filefunc.zseek64_file = util_zip_memory_seek;
    filefunc.zclose_file = util_zip_memory_close;
    filefunc.zerror_file = util_zip_memory_error;
    filefunc.opaque = stream;

    /* open always returns this stream, so invalid-zip failures close it before returning NULL. */
    return unzOpen2_64 ( NULL, &filefunc );
}
