/*
 * 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@gmail.com>
 */

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

#include <string.h>             /* strstr(), strchr(), memcpy(), strcpy() */
#include <stdlib.h>             /* getenv() */

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

/* If you can't stand string manipulations in C, just don't look there */

gint common_prefix_length(const gchar * str1, const gchar * str2)
{
    const gchar *orig_str1 = str1;

    if (NOT_ALIGNED(str1) == NOT_ALIGNED(str2)) {
        /* We can align both pointers. */
        while (NOT_ALIGNED(str1) && *str1 == *str2 && *str1 != '\0') {
            str1++;
            str2++;
        }

        if (*str1 != '\0' && *str1 == *str2) {
            /* Both pointers are now aligned. */
            gulong *long_ptr1 = (gulong *) str1, *long_ptr2 = (gulong *) str2;

            /* Skip identical blocks in the paths. */
            while (!HAS_ZERO(*long_ptr1) && *long_ptr1 == *long_ptr2) {
                long_ptr1++;
                long_ptr2++;
            }

            str1 = (const gchar *) long_ptr1;
            str2 = (const gchar *) long_ptr2;
        }
    }

    /* Skip identical characters in the paths. */
    while (*str1 != '\0' && *str1 == *str2) {
        str1++;
        str2++;
    }

    return str1 - orig_str1;
}

#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 *long_ptr, long_int;

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

        if (*str >> 7)
            return FALSE;

        if (!NOT_ALIGNED(str))
            /* Aligned. */
            break;

        str++;
    }

    long_ptr = (gulong *) str;

    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 & LONG_MASK(0x80808080L, 0x8080808080808080L))
            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;
}

static 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;
    gchar *prev_slash = str, *next_slash = NULL;

    /*
     * "path1/./path2" is replaced with "path1///path2".
     * "./" and "//" are counted.
     */
    while ((next_slash = strchr(prev_slash + !!next_slash, '/')) != NULL) {
        *len += next_slash - prev_slash;

        switch (next_slash[1]) {
        case '.':
            if (next_slash[2] == '/') {
                next_slash[1] = '/';
                count++;
            }
            break;

        case '/':
            count++;
        }

        prev_slash = next_slash;
    }

    *len += strlen(prev_slash);
    return count;
}

static gchar *remove_double_slash(gchar * str, gint new_len)
{
    gchar *new, *new_ptr;
    gchar *prev_slash = str, *next_slash = NULL;

    if (str[0] == '/' || 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 ((next_slash = strchr(prev_slash + !!next_slash, '/')) != NULL) {
        memcpy(new_ptr, prev_slash, next_slash - prev_slash);
        new_ptr += next_slash - prev_slash;

        while (next_slash[1] == '/')
            next_slash++;

        prev_slash = next_slash;
    }

    strcpy(new_ptr, prev_slash);

    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 (copy - orig_copy >= 2) {
        /* We add a './' for relative filenames. */
        copy -= 2;
        copy[0] = '.';
        copy[1] = '/';
    }

    /* 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(copy)) {
            if (orig_copy != 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);

    g_free(orig_copy);

    return new;
}
