/*
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 * See the COPYING file for license information.
 *
 * Guillaume Chazarain <guichaz@yahoo.fr>
 */

/*******************************************
 * UTF-8, filenames and mnemonics handling *
 *******************************************/

#include <string.h>             /* strstr() */
#include <stdlib.h>             /* getenv() */

#include "gliv.h"
#include "str_utils.h"

#define CHECK_BYTE(ptr)   \
    do {                  \
        if (*ptr == '\0') \
            return TRUE;  \
                          \
        if (*ptr >> 7)    \
            return FALSE; \
                          \
        ptr++;            \
    } while (0)

static gboolean str_is_ascii(const gchar * str)
{
    gulong himagic, lomagic, magic_bits;
    gulong *long_ptr, long_int;
    gulong mask;

    for (;;) {
        if (*str == '\0')
            return TRUE;

        if (*str >> 7)
            return FALSE;

        if (((gulong) str & (sizeof(gulong) - 1)) == 0)
            /* Aligned. */
            break;

        str++;
    }

    long_ptr = (gulong *) str;
    INIT_MAGIC();
    mask = 0x80808080;
    if (sizeof(gulong) > 4)
        /* 64-bit */
        mask = ((mask << 16) << 16) | mask;

    for (;;) {
        long_int = *long_ptr;
        if (HAS_ZERO(long_int)) {
            /* A '\0' has been detected. */
            const gchar *char_ptr = (const gchar *) long_ptr;
            CHECK_BYTE(char_ptr);
            CHECK_BYTE(char_ptr);
            CHECK_BYTE(char_ptr);
            CHECK_BYTE(char_ptr);
            if (sizeof(gulong) > 4) {
                /* 64-bit */
                CHECK_BYTE(char_ptr);
                CHECK_BYTE(char_ptr);
                CHECK_BYTE(char_ptr);
                CHECK_BYTE(char_ptr);
            }
        } else if (long_int & mask)
            return FALSE;

        long_ptr++;
    }
    return TRUE;
}

/* From glib. */
static gboolean have_broken_filenames(void)
{
    static gboolean initialized = FALSE;
    static gboolean broken;

    if (initialized)
        return broken;

    broken = getenv("G_BROKEN_FILENAMES") != NULL;
    initialized = TRUE;

    return broken;
}

/* The returned string should not be freed. */
const gchar *filename_to_utf8(const gchar * str)
{
    static GStaticPrivate result_key = G_STATIC_PRIVATE_INIT;
    gchar **result;
    GError *err = NULL;

    if (!have_broken_filenames() || g_get_charset(NULL) || str_is_ascii(str))
        return str;

    result = g_static_private_get(&result_key);
    if (result == NULL) {
        /* First time in this thread. */
        result = g_new(gchar *, 1);
        g_static_private_set(&result_key, result, g_free);
        *result = NULL;
    }

    g_free(*result);

    *result = g_filename_to_utf8(str, -1, NULL, NULL, &err);
    if (err != NULL) {
        g_printerr("%s\n", err->message);
        g_error_free(err);

        g_free(*result);
        *result = NULL;
        return str;
    }

    return *result;
}

G_INLINE_FUNC gboolean starts_dotslash(const gchar * str)
{
    return str[0] == '.' && str[1] == '/';
}

static gboolean is_clean(const gchar * filename)
{
    if (filename[0] != '/' && !starts_dotslash(filename))
        return FALSE;

    return strstr(filename, "//") == NULL && strstr(filename, "/./") == NULL;
}

static gint count_errors(gchar * str, gint * len)
{
    gint count = 0;

    /*
     * "path1/./path2" is replaced with "path1///path2".
     * "./" and "//" are counted.
     */
    while (*str != '\0') {
        (*len)++;
        if (str[0] == '/')
            switch (str[1]) {
            case '.':
                if (str[2] == '/') {
                    str[1] = '/';
                    count++;
                }
                break;

            case '/':
                count++;
            }

        str++;
    }

    return count;
}

static gchar *remove_double_slash(gchar * str, gint new_len, gboolean absolute)
{
    gchar *new, *new_ptr;

    if (absolute || starts_dotslash(str)) {
        new = g_new(gchar, new_len);
        new_ptr = new;
    } else {
        new = g_new(gchar, new_len + 2);
        new[0] = '.';
        new[1] = '/';
        new_ptr = new + 2;
    }

    while (*str != '\0') {
        if (str[0] != '/' || str[1] != '/') {
            *new_ptr = *str;
            new_ptr++;
        }

        str++;
    }

    *new_ptr = '\0';

    return new;
}

gchar *clean_filename(const gchar * filename)
{
    gchar *orig_copy, *copy, *new;
    gint count = 0, len = 0;
    gboolean absolute, finished = FALSE;

    if (is_clean(filename))
        return g_strdup(filename);

    /* We work on a copy as we may modify it. */
    orig_copy = copy = g_strdup(filename);

    absolute = (copy[0] == '/');
    while (finished == FALSE) {
        switch (copy[0]) {
        case '.':
            if (copy[1] == '/')
                copy += 2;
            else
                finished = TRUE;
            break;

        case '/':
            copy++;
            break;

        default:
            finished = TRUE;
        }
    }

    if (absolute)
        /* We keep the last leading '/' for absolute filenames. */
        copy--;
    else if (starts_dotslash(orig_copy))
        /* We keep the last leading './' for relative filenames. */
        copy -= 2;

    /* Count the '//' and '/./'. */
    count = count_errors(copy, &len);

    if (count == 0) {
        /* The filename was clean. */

        if (absolute) {
            if (orig_copy != copy)
                /* The filename started with "//". */
                g_memmove(orig_copy, copy, len + 1);

            return orig_copy;
        }

        if (starts_dotslash(orig_copy)) {
            g_memmove(orig_copy, copy, len + 1);
            new = orig_copy;
        } else {
            /* The relative filename just lacked the "./" in the beginning. */
            new = g_strconcat("./", copy, NULL);
            g_free(orig_copy);
        }

        return new;
    }

    /* We now have to remove the "//". */
    new = remove_double_slash(copy, len - count + 1, absolute);

    g_free(orig_copy);

    return new;
}
