/* vi: set ts=2 shiftwidth=2 expandtab:
 *
 * Copyright (C) 2003-2008  Simon Baldwin and Mark J. Tilford
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of version 2 of the GNU General Public License
 * as published by the Free Software Foundation.
 *
 * 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
 */

/*
 * Module notes:
 *
 * o Standard libc ctype.h functions vary their results according to the
 *   currently set locale.  We want consistent Windows codepage 1252 or
 *   codepage 1251 (WinLatin1 or WinCyrillic) results.  To get this, then,
 *   we have to define the needed functions internally to SCARE.
 */

#include <assert.h>
#include <stdio.h>
#include <string.h>

#include "scare.h"
#include "scprotos.h"


/*
 * All the ctype boolean and character tables contain 256 elements, one for
 * each possible sc_char value.  This is used to size arrays and to verify
 * that range setting functions do not overrun array boundaries.
 */
enum { TABLE_SIZE = 256 };


/*
 * loc_setrange_bool()
 * loc_setranges_bool()
 *
 * Helpers for building ctype tables.  Sets all elements from start to end
 * inclusive to TRUE, and iterate this on a ranges array.
 */
static void
loc_setrange_bool (sc_int start, sc_int end, sc_bool table[])
{
  sc_int index_;

  for (index_ = start; index_ <= end; index_++)
    {
      assert (index_ > -1 && index_ < TABLE_SIZE);
      table[index_] = TRUE;
    }
}

static void
loc_setranges_bool (const sc_int ranges[], sc_bool table[])
{
  sc_int index_;

  for (index_ = 0; ranges[index_] > -1; index_ += 2)
    {
      assert (ranges[index_] <= ranges[index_ + 1]);
      loc_setrange_bool (ranges[index_], ranges[index_ + 1], table);
    }
}


/*
 * loc_setrange_char()
 * loc_setranges_char()
 *
 * Helpers for building ctype conversion tables.  Sets all elements from start
 * to end inclusive to their index value plus the given offset, and iterate
 * this on a ranges array.
 */
static void
loc_setrange_char (sc_int start, sc_int end, sc_int offset, sc_char table[])
{
  sc_int index_;

  for (index_ = start; index_ <= end; index_++)
    {
      assert (index_ > -1 && index_ < TABLE_SIZE);
      assert (index_ + offset > -1 && index_ + offset < TABLE_SIZE);
      table[index_] = index_ + offset;
    }
}

static void
loc_setranges_char (const sc_int ranges[], sc_char table[])
{
  sc_int index_;

  for (index_ = 0; ranges[index_] > -1; index_ += 3)
    {
      assert (ranges[index_] <= ranges[index_ + 1]);
      loc_setrange_char (ranges[index_],
                         ranges[index_ + 1], ranges[index_ + 2], table);
    }
}


/*
 * A locale consists of a name, ranges for each table, and signatures for
 * autodetection based on the game's compilation date.  This is the static
 * data portion of a locale.
 */
enum { RANGES_LENGTH = 32 };
enum { SIGNATURE_COUNT = 24, SIGNATURE_LENGTH = 3 };
typedef struct
{
  const sc_char *const name;
  const sc_int isspace_ranges[RANGES_LENGTH];
  const sc_int isdigit_ranges[RANGES_LENGTH];
  const sc_int isalpha_ranges[RANGES_LENGTH];
  const sc_int toupper_ranges[RANGES_LENGTH];
  const sc_int tolower_ranges[RANGES_LENGTH];
  const sc_byte signature[SIGNATURE_COUNT][SIGNATURE_LENGTH];
} sc_locale_t;


/*
 * The locale table set is built from a locale using its ranges.  There is one
 * table for each function, and a pointer to the locale used to construct the
 * table, for synchronization with changed locales.  This is the dynamic data
 * portion of a locale.
 */
typedef struct
{
  const sc_locale_t *locale;
  sc_bool isspace[TABLE_SIZE];
  sc_bool isdigit[TABLE_SIZE];
  sc_bool isalpha[TABLE_SIZE];
  sc_char toupper[TABLE_SIZE];
  sc_char tolower[TABLE_SIZE];
} sc_locale_table_t;

/*
 * Define a single static locale table set.  This set re-initializes if it
 * detects a locale change.
 */
static sc_locale_table_t loc_locale_tables = { NULL, { 0 },
                                               { 0 }, { 0 }, { 0 }, { 0 } };


/*
 * loc_synchronize_tables()
 * loc_check_tables_synchronized()
 *
 * Initialize tables for a locale.  And compare the locale tables to a locale
 * and if not for the same locale, (re-)initialize.
 */
static void
loc_synchronize_tables (const sc_locale_t *locale)
{
  /* Clear all tables and the locale pointer. */
  memset (&loc_locale_tables, 0, sizeof (loc_locale_tables));

  /* Set ranges and attach the new locale. */
  loc_setranges_bool (locale->isspace_ranges, loc_locale_tables.isspace);
  loc_setranges_bool (locale->isdigit_ranges, loc_locale_tables.isdigit);
  loc_setranges_bool (locale->isalpha_ranges, loc_locale_tables.isalpha);
  loc_setranges_char (locale->toupper_ranges, loc_locale_tables.toupper);
  loc_setranges_char (locale->tolower_ranges, loc_locale_tables.tolower);

  loc_locale_tables.locale = locale;
}

static void
loc_check_tables_synchronized (const sc_locale_t *locale)
{
  if (locale != loc_locale_tables.locale)
    loc_synchronize_tables (locale);
}


/*
 * Locale for Latin1.  The signatures in this locale are null since it is the
 * default locale; no matching required.  Also, none may be practical, as this
 * locale works for a large number of Western European languages (though in
 * practice, it seems that only English and French Adrift Latin1 games exist).
 */
static const sc_locale_t LATIN1_LOCALE = {
  "Latin1",
  { 9,13,  32,32,  160,160,  -1 },
  { 48,57,  -1 },
  { 65,90,  97,122,  192,214,  216,246,  248,255,  138,138,  140,140,
    142,142,  154,154,  156,156,  158,158,  159,159,  -1 },
  { 0,TABLE_SIZE-1,0,  97,122,-32,  224,246,-32,  248,254,-32,  154,154,-16,
    156,156,-16,  158,158,-16,  255,255,-96,  -1 },
  { 0,TABLE_SIZE-1,0,  65,90,32,  192,214,32,  216,222,32,  138,138,16,
    140,140,16,  142,142,16,  159,159,96,  -1 },
  { { 0 } }
};


/*
 * Locale for Cyrillic.  The signatures in this locale are month names in
 * both mixed case and lowercase Russian Cyrillic.
 */
static const sc_locale_t CYRILLIC_LOCALE = {
  "Cyrillic",
  { 9,13,  32,32,  160,160,  -1 },
  { 48,57,  -1 },
  { 65,90,  97,122,  168,168,  184,184,  175,175,  191,191,  178,179,
    192,255,  -1 },
  { 0,TABLE_SIZE-1,0,  97,122,-32,  184,184,-16,  191,191,-16,  179,179,-1,
    224,255,-32,  -1 },
  { 0,TABLE_SIZE-1,0,  65,90,32,  168,168,16,  175,175,16,  178,178,1,
    192,223,32,  -1 },
  { { 0xdf, 0xed, 0xe2 }, { 0xd4, 0xe5, 0xe2 }, { 0xcc, 0xe0, 0xf0 },
    { 0xc0, 0xef, 0xf0 }, { 0xcc, 0xe0, 0xe9 }, { 0xc8, 0xfe, 0xed },
    { 0xc8, 0xfe, 0xeb }, { 0xc0, 0xe2, 0xe3 }, { 0xd1, 0xe5, 0xed },
    { 0xce, 0xea, 0xf2 }, { 0xcd, 0xee, 0xff }, { 0xc4, 0xe5, 0xea },
    { 0xff, 0xed, 0xe2 }, { 0xf4, 0xe5, 0xe2 }, { 0xec, 0xe0, 0xf0 },
    { 0xe0, 0xef, 0xf0 }, { 0xec, 0xe0, 0xe9 }, { 0xe8, 0xfe, 0xed },
    { 0xe8, 0xfe, 0xeb }, { 0xe0, 0xe2, 0xe3 }, { 0xf1, 0xe5, 0xed },
    { 0xee, 0xea, 0xf2 }, { 0xed, 0xee, 0xff }, { 0xe4, 0xe5, 0xea } }
};


/* List of pointers to supported and available locales, NULL terminated. */
static const sc_locale_t *const AVAILABLE_LOCALES[] = {
  &LATIN1_LOCALE,
  &CYRILLIC_LOCALE,
  NULL };

/*
 * The locale for the game, set below explicitly or on game startup, and
 * a flag to note if it's been set explicitly, to prevent autodetection from
 * overwriting a manual setting.
 */
static const sc_locale_t *loc_locale = &LATIN1_LOCALE;
static sc_bool loc_is_autodetect_enabled = TRUE;


/*
 * loc_locate_signature_in_date()
 *
 * Checks the format of the input date to ensure it matches the format
 * "dd [Mm]mm yyyy".  Returns the address of the month part of the string, or
 * NULL if it doesn't match the expected format.
 */
static const sc_char *
loc_locate_signature_in_date (const sc_char *date)
{
  sc_int day, year, converted;
  sc_char signature[SIGNATURE_LENGTH + 1];

  /* Clear signature, and convert using a scanf format. */
  memset (signature, 0, sizeof (signature));
  converted = sscanf (date, "%2ld %3[^ 0-9] %4ld", &day, signature, &year);

  /* Valid if we converted three values, and month has three characters. */
  if (converted == 3 && strlen (signature) == SIGNATURE_LENGTH)
    return strstr (date, signature);
  else
    return NULL;
}


/*
 * loc_compare_locale_signatures()
 *
 * Search a locale's signatures for a match with the signature passed in.
 * Returns TRUE if a match found, FALSE otherwise.  Uses memcmp rather than
 * any strcasecmp() variant because the signatures are in the locale's
 * codepage, but the locale is not yet (by definition) set.
 */
static sc_bool
loc_compare_locale_signatures (const char *signature, const sc_locale_t *locale)
{
  sc_int index_;
  sc_bool is_matched;

  /* Compare signatures, stopping on the first match found. */
  is_matched = FALSE;
  for (index_ = 0; index_ < SIGNATURE_COUNT; index_++)
    {
      if (memcmp (locale->signature[index_],
                  signature, sizeof (locale->signature[0])) == 0)
        {
           is_matched = TRUE;
           break;
        }
    }

  return is_matched;
}


/*
 * loc_find_matching_locale()
 *
 * Look at the incoming date, expected to be in the format "dd [Mm]mm yyyy",
 * where "[Mm]mm" is a standard month abbreviation of the locale in which the
 * Generator was run.  Match this with locale signatures, and return the
 * first locale that matches, or NULL if none match.
 */
static const sc_locale_t *
loc_find_matching_locale (const sc_char *date,
                          const sc_locale_t *const *locales)
{
  const sc_char *signature;
  const sc_locale_t *matched = NULL;

  /* Get the month part of date, and if valid, search locale signatures. */
  signature = loc_locate_signature_in_date (date);
  if (signature)
    {
      const sc_locale_t *const *iterator;

      /* Search for this signature in the locale's signatures. */
      for (iterator = locales; *iterator; iterator++)
        {
          if (loc_compare_locale_signatures (signature, *iterator))
            {
              matched = *iterator;
              break;
            }
        }
    }

  /* Return the matching locale, NULL if none matched. */
  return matched;
}


/*
 * loc_detect_game_locale()
 *
 * Set an autodetected value for the locale based on looking at a game's
 * compilation date.
 */
void
loc_detect_game_locale (sc_prop_setref_t bundle)
{
  assert (bundle);

  /* If an explicit locale has already been set, ignore the call. */
  if (loc_is_autodetect_enabled)
    {
      sc_vartype_t vt_key[1];
      const sc_char *compile_date;
      const sc_locale_t *matched;

      /* Read the game's compilation date from the properties. */
      vt_key[0].string = "CompileDate";
      compile_date = prop_get_string (bundle, "S<-s", vt_key);

      /* Search for a matching locale based on the game compilation date. */
      matched = loc_find_matching_locale (compile_date, AVAILABLE_LOCALES);

      /* If a locale matched, set the global locale to it. */
      if (matched)
        loc_locale = matched;
    }
}


/*
 * loc_ascii_tolower()
 * loc_ascii_strncasecmp()
 *
 * The standard sc_strncasecmp() calls sc_tolower(), which is locale specific.
 * This isn't a particular problem because it will set the Latin1 locale
 * automatically before continuing.  However, since locale names should always
 * be in ascii anyway, it's slightly safer to just use an ascii-only version
 * of this function.
 */
static sc_char
loc_ascii_tolower (sc_char ch)
{
  return (ch >= 'A' && ch <= 'Z') ? ch - 'A' + 'a' : ch;
}

static sc_int
loc_ascii_strncasecmp (const sc_char *s1, const sc_char *s2, sc_int n)
{
  sc_int index_;

  for (index_ = 0; index_ < n; index_++)
    {
      sc_int diff;

      diff = loc_ascii_tolower (s1[index_]) - loc_ascii_tolower (s2[index_]);
      if (diff < 0 || diff > 0)
        return diff < 0 ? -1 : 1;
    }

  return 0;
}


/*
 * loc_set_locale()
 * loc_get_locale()
 *
 * Set a locale explicitly from the name passed in, returning TRUE if a locale
 * matched the name.  Get the current locale, which may be the default locale
 * if none yet set.
 */
sc_bool
loc_set_locale (const sc_char *name)
{
  const sc_locale_t *matched = NULL;
  const sc_locale_t *const *iterator;
  assert (name);

  /*
   * Search locales for a matching name, abbreviated if necessary.  Stop on
   * the first match found.
   */
  for (iterator = AVAILABLE_LOCALES; *iterator; iterator++)
    {
      const sc_locale_t *const locale = *iterator;

      if (loc_ascii_strncasecmp (name, locale->name, strlen (name)) == 0)
        {
          matched = locale;
          break;
        }
    }

  /* If matched, set the global locale, and lock out future autodetection. */
  if (matched)
    {
      loc_locale = matched;
      loc_is_autodetect_enabled = FALSE;
    }

  return matched ? TRUE : FALSE;
}

const sc_char *
loc_get_locale (void)
{
  return loc_locale->name;
}


/*
 * loc_bool_template()
 * loc_char_template()
 *
 * "Template" functions for locale variant ctype functions.  Synchronize
 * tables to the currently set locale, and return the value from the table.
 */
static sc_bool
loc_bool_template (sc_char character, const sc_bool table[])
{
  loc_check_tables_synchronized (loc_locale);
  return table[(sc_byte) character];
}

static sc_char
loc_char_template (sc_char character, const sc_char table[])
{
  loc_check_tables_synchronized (loc_locale);
  return table[(sc_byte) character];
}


/*
 * sc_isspace()
 * sc_isalpha()
 * sc_isdigit()
 * sc_tolower()
 * sc_toupper()
 *
 * Public entry points into locale variant ctype functions.
 */
sc_bool
sc_isspace (sc_char character)
{
  return loc_bool_template (character, loc_locale_tables.isspace);
}

sc_bool
sc_isalpha (sc_char character)
{
  return loc_bool_template (character, loc_locale_tables.isalpha);
}

sc_bool
sc_isdigit (sc_char character)
{
  return loc_bool_template (character, loc_locale_tables.isdigit);
}

sc_char
sc_toupper (sc_char character)
{
  return loc_char_template (character, loc_locale_tables.toupper);
}

sc_char
sc_tolower (sc_char character)
{
  return loc_char_template (character, loc_locale_tables.tolower);
}
