#include <xlocale.h>
#include <locale.h>
#include <glib.h>
#include <langinfo.h>
#include <stdlib.h>

struct ignore_range
{
  gunichar first;
  gunichar last;
  guint32 period;
};

struct translit_info
{
  /* Transliteration */
  const guint32  *from_index;
  guint           from_index_length;
  const gunichar *from_table;
  const guint32  *to_index;
  const gunichar *to_table;

  /* Ignore ranges */
  const struct ignore_range *ignore_ranges;
  guint                      n_ignore_ranges;

  /* Default replacement */
  const gunichar *default_;
  guint           default_len;

  /* The locale we got it from */
  locale_t        locale;
};


static struct translit_info *global_translit_info;
static GHashTable *translit_infos;
static GMutex translit_infos_lock;

static void *
good_nl_langinfo_l (nl_item  item,
                    locale_t locale)
{
  /* as per POSIX:
   *
   * The behavior is undefined if the locale argument to nl_langinfo_l()
   * is the special locale object LC_GLOBAL_LOCALE or is not a valid
   * locale object handle.
   *
   * So we must handle this specially...
   */
  if (locale == LC_GLOBAL_LOCALE)
    return nl_langinfo (item);
  else
    return nl_langinfo_l (item, locale);
}

static void
fill_info_struct (struct translit_info *info)
{
  if (info->locale != (locale_t) 0)
    {
      info->from_index = good_nl_langinfo_l (_NL_CTYPE_TRANSLIT_FROM_IDX, info->locale);
      info->from_index_length = (guint32) (gsize) good_nl_langinfo_l (_NL_CTYPE_TRANSLIT_TAB_SIZE, info->locale);
      info->from_table = good_nl_langinfo_l (_NL_CTYPE_TRANSLIT_FROM_TBL, info->locale);
      info->to_index = good_nl_langinfo_l (_NL_CTYPE_TRANSLIT_TO_IDX, info->locale);
      info->to_table = good_nl_langinfo_l (_NL_CTYPE_TRANSLIT_TO_TBL, info->locale);

      info->ignore_ranges = good_nl_langinfo_l (_NL_CTYPE_TRANSLIT_IGNORE, info->locale);
      info->n_ignore_ranges = (guint32) (gsize) good_nl_langinfo_l (_NL_CTYPE_TRANSLIT_IGNORE_LEN, info->locale);

      info->default_ = good_nl_langinfo_l (_NL_CTYPE_TRANSLIT_DEFAULT_MISSING, info->locale);
      info->default_len = (guint32) (gsize) good_nl_langinfo_l (_NL_CTYPE_TRANSLIT_DEFAULT_MISSING_LEN, info->locale);
    }

  if (info->default_len == 0)
    {
      gunichar default_ = '?';

      info->default_ = g_memdup (&default_, sizeof default_);
      info->default_len = 1;
    }
}

static const struct translit_info *
get_translit_info (const gchar *locale_name)
{
  struct translit_info *info;

  if (locale_name == NULL)
    {
      if (g_once_init_enter (&global_translit_info))
        {
          info = g_slice_new0 (struct translit_info);
          info->locale = LC_GLOBAL_LOCALE;
          fill_info_struct (info);
          g_once_init_leave (&global_translit_info, info);
        }

      return global_translit_info;
    }

  g_mutex_lock (&translit_infos_lock);
  if (!translit_infos)
    translit_infos = g_hash_table_new (g_str_hash, g_str_equal);

  info = g_hash_table_lookup (translit_infos, locale_name);

  if (!info)
    {
      info = g_slice_new0 (struct translit_info);
      info->locale = newlocale (LC_CTYPE_MASK, locale_name, (locale_t) 0);
      fill_info_struct (info);

      g_hash_table_insert (translit_infos, g_strdup (locale_name), info);
    }

  g_mutex_unlock (&translit_infos_lock);

  return info;
}

struct search_state
{
  const struct translit_info *info;
  const gunichar *str;
  guint found_len;
};

static gint
compare_entries (gconstpointer data,
                 gconstpointer item)
{
  struct search_state *state = (gpointer) data;
  guint32 entry = *(const guint32 *) item;
  const gunichar *str;
  gint i;

  str = &state->info->from_table[entry];

  for (i = 0; str[i]; i++)
    {
      if (str[i] == state->str[i])
        continue;

      else if (str[i] < state->str[i])
        return 1;

      else
        return -1;
    }

  /* Sneak this back out so we know how many chars we used */
  state->found_len = i;

  return 0;
}

static gint
transliterate (GString                    *result,
               const struct translit_info *info,
               const gunichar             *str)
{
  struct search_state state = { info, str };
  const guint32 *entry;
  gint i;

  entry = bsearch (&state, info->from_index, info->from_index_length, sizeof (guint32), compare_entries);
  if (entry != NULL)
    {
      guint32 to_offset = info->to_index[entry - info->from_index];
      gint i;

      /* Search for an all-ascii replacement */
      while (info->to_table[to_offset])
        {
          gboolean ok = TRUE;

          for (i = 0; info->to_table[to_offset + i]; i++)
            if (info->to_table[to_offset + i] > 0x7f)
              ok = FALSE;

          if (ok)
            {
              /* Got it! */
              for (i = 0; info->to_table[to_offset + i]; i++)
                g_string_append_unichar (result, info->to_table[to_offset + i]);

              return state.found_len;
            }

          to_offset += i + 1;
        }
    }

  /* Didn't find anything in the transliteration table, so try the
   * ignore ranges */
  for (i = 0; i < info->n_ignore_ranges; i++)
    {
      const struct ignore_range *range = &info->ignore_ranges[i];

      if (range->first <= *str && *str <= range->last && (*str - range->first) % range->period == 0)
        /* The character is in the ignore range.  Skip it. */
        return 1;
    }

  /* Default replacement */
  for (i = 0; i < info->default_len; i++)
    g_string_append_unichar (result, info->default_[i]);

  return 1;
}

gchar *
g_str_to_ascii (const gchar *str,
                const gchar *locale_name)
{
  const struct translit_info *info;
  gunichar *unicode_str;
  GString *result;
  gint i, n;

  if (g_str_is_ascii (str))
    return g_strdup (str);

  info = get_translit_info (locale_name);

  n = g_utf8_strlen (str, -1);
  unicode_str = g_new (gunichar, n + 1);
  for (i = 0; i < n; i++)
    {
      unicode_str[i] = g_utf8_get_char (str);
      str = g_utf8_next_char (str);
    }
  unicode_str[i] = 0;
  g_assert (i == n);

  result = g_string_new (NULL);

  for (i = 0; unicode_str[i]; i++)
    {
      if (unicode_str[i] < 0x80)
        g_string_append_c (result, unicode_str[i]);
      else
        i += transliterate (result, info, unicode_str + i) - 1;
    }

  g_free (unicode_str);

  return g_string_free (result, FALSE);
}

int
main (int argc, char **argv)
{
  setlocale (LC_ALL, "");
  g_print ("%s\n", g_str_to_ascii (argv[1], argv[2]));
  return 0;
}
