/* msgfmt - converts Uniforum style .po files to binary .mo files
   Copyright (C) 1995 Free Software Foundation, Inc.
   Written by Ulrich Drepper <drepper@gnu.ai.mit.edu>, April 1995.

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, 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.  */

#ifdef HAVE_CONFIG_H
# include <config.h>
#endif

#include <ctype.h>
#include <errno.h>
#include <getopt.h>
#include <stdio.h>
#include <sys/param.h>
#include <sys/types.h>

#ifdef STDC_HEADERS
# include <stdlib.h>
#endif

#ifdef HAVE_LOCALE_H
# include <locale.h>
#endif

#include "hash.h"

#include "error.h"
#include "getline.h"
#include "printf.h"
#include <system.h>

#include "gettext.h"
#include "domain.h"
#include "hash-string.h"
#include <libintl.h>
#include "po.h"

#define _(str) gettext (str)

#ifndef errno
extern int errno;
#endif

/* Define the data structure which we need to represent the data to
   be written out.  */
struct id_str_pair
{
  char *id;
  char *str;
};

/* Contains information about the definition of one translation.  */
struct msgstr_def
{
  char *msgstr;
  lex_pos_ty pos;
};

/* Alignment of strings in resulting .mo file.  */
static size_t alignment;

/* Contains exit status for case in which no premature exit occurs.  */
static int exit_status;

/* Nonzero if no hash table in .mo is wanted.  */
static int no_hash_table;

/* Specifies name of the output file.  */
static const char *output_file_name;

/* String containing name the program is called with.  */
const char *program_name;

/* We may have more than one input file.  Domains with same names in
   different files have to merged.  So we need a list of tables for
   each output file.  */
static struct msg_domain *domain;
static struct msg_domain *current_domain;

/* If not zero list duplicate message identifiers.  */
static int verbose;

/* Long options.  */
static const struct option long_options[] =
{
  { "alignment", required_argument, NULL, 'a' },
  { "help", no_argument, NULL, 'h' },
  { "no-hash", no_argument, &no_hash_table, 1 },
  { "output-file", required_argument, NULL, 'o' },
  { "strict", no_argument, NULL, 'S' },
  { "verbose", no_argument, &verbose, 1 },
  { "version", no_argument, NULL, 'V' },
  { NULL, 0, NULL, 0 }
};


#ifndef roundup
# if defined __GNUC__ && __GNUC__ >= 2
#  define roundup(x, y) ({typeof(x) _x = (x); typeof(y) _y = (y); \
			  ((_x + _y - 1) / _y) * _y; })
# else
#  define roundup(x, y) ((((x)+((y)-1))/(y))*(y))
# endif	/* GNU CC2  */
#endif /* roundup  */

/* Prototypes for local functions.  */
static void usage __P ((int status))
#if defined __GNUC__ && ((__GNUC__ == 2 && __GNUC_MINOR__ >= 5) || __GNUC__ > 2)
	__attribute__ ((noreturn))
#endif
;
static void error_print __P ((void));
static void grammar __P ((char *__filename));
static void format_directive_domain __P ((po_ty *__pop, char *__name));
static void format_directive_message __P ((po_ty *__pop, char *__msgid,
					   lex_pos_ty *__msgid_pos,
					   char *__msgstr,
					   lex_pos_ty *__msgstr_pos));
static struct msg_domain *new_domain __P ((const char *name));
static int compare_id __P ((const void *pval1, const void *pval2));
static void write_table __P ((FILE *output_file, hash_table *tab));
static void check_pair __P ((const char *msgid, const lex_pos_ty *msgid_pos,
			     const char *msgstr,
			     const lex_pos_ty *msgstr_pos));
static const char *add_mo_suffix __P ((const char *));


int
main(argc, argv)
     int argc;
     char *argv[];
{
  int optchar;
  int do_help = 0;
  int do_version = 0;
  int strict_uniforum = 0;

  /* Set default value for global variables.  */
  alignment = DEFAULT_ALIGNMENT;

  /* Set program name for messages.  */
  program_name = argv[0];
  error_print_progname = error_print;
  exit_status = EXIT_SUCCESS;

#ifdef HAVE_SETLOCALE
  /* Set locale via LC_ALL.  */
  setlocale (LC_ALL, "");
#endif

  /* Set the text message domain.  */
  bindtextdomain (PACKAGE, LOCALEDIR);
  textdomain (PACKAGE);

  while ((optchar = getopt_long (argc, argv, "a:ho:vV", long_options, NULL))
	 != EOF)
    switch (optchar)
      {
      case '\0':
	break;
      case 'a':
	{
	  char *endp;
	  size_t new_align = strtoul (optarg, &endp, 0);

	  if (endp != optarg)
	    alignment = new_align;
	}
	break;
      case 'h':
	do_help = 1;
	break;
      case 'o':
	output_file_name = optarg;
	break;
      case 'S':
	strict_uniforum = 1;
	break;
      case 'v':
	verbose = 1;
	break;
      case 'V':
	do_version = 1;
	break;
      default:
	usage (EXIT_FAILURE);
	break;
      }

  /* Version information is requested.  */
  if (do_version)
    {
      fprintf (stderr, "%s - GNU %s %s\n", program_name, PACKAGE, VERSION);
      exit (EXIT_SUCCESS);
    }

  /* Help is requested.  */
  if (do_help)
    usage (EXIT_SUCCESS);

  /* Test whether we have a .po file name as argument.  */
  if (optind >= argc)
    {
      error (EXIT_SUCCESS, 0, _("no input file given"));
      usage (EXIT_FAILURE);
    }

  /* The -o option determines the name of the domain and therefor
     the output file.  */
  if (output_file_name != NULL)
    current_domain = new_domain (output_file_name);

  /* Process all given .po files.  */
  while (argc > optind)
    {
      /* Remember that we currently have not specified any domain.  This
	 is of course not true when we saw the -o option.  */
      if (output_file_name == NULL)
	current_domain = NULL;

      /* And process the input file.  */
      grammar (argv[optind]);

      ++optind;
    }

  /* Now write out all domains.  */
  while (domain != NULL)
    {
      FILE *output_file;

      /* If no entry for this domain don't even create the file.  */
      if (domain->symbol_tab.filled != 0)
	{
	  if (strcmp (domain->domain_name, "-") == 0)
	    output_file = stdout;
	  else
	    {
	      const char *fname;

	      fname = strict_uniforum ? add_mo_suffix (domain->domain_name)
				      : domain->domain_name;

	      output_file = fopen (fname, "wb");
	      if (output_file == NULL)
		{
		  error (0, errno,
			 _("error while opening \"%s\" for writing"), fname);
		  exit_status = EXIT_FAILURE;
		}

	      if (strict_uniforum)
		free ((void *) fname);
	    }

	  if (output_file != NULL)
	    {
	      write_table (output_file, &domain->symbol_tab);
	      if (output_file != stdout)
		fclose (output_file);
	    }
	}

      domain = domain->next;
    }

  exit (exit_status);
}


/* Display usage information and exit.  */
static void
usage (status)
     int status;
{
  if (status != EXIT_SUCCESS)
    fprintf (stderr, _("Try `%s --help' for more information\n"),
	     program_name);
  else
    printf (_("\
Usage: %s [OPTION] filename.po ...\n\
Mandatory arguments to long options are mandatory for short options too.\n\
  -a, --alignment=NUMBER   align strings to NUMBER bytes (default: %d)\n\
  -h, --help               display this help and exit\n\
      --no-hash            binary file will not include the hash table\n\
  -o, --output-file=FILE   specify output file name as FILE\n\
      --strict             enable strict Uniforum mode\n\
  -v, --verbose            list input file anomalies\n\
  -V, --version            output version information and exit\n\
\n\
If input file is -, standard input is read.  If output file is -,\n\
output is written to standard output.\n"),
	    program_name, DEFAULT_ALIGNMENT);

  exit (status);
}


static struct msg_domain *
new_domain (name)
     const char *name;
{
  struct msg_domain **p_dom = &domain;

  while (*p_dom != NULL && strcmp (name, (*p_dom)->domain_name) != 0)
    p_dom = &(*p_dom)->next;

  if (*p_dom == NULL)
    {
      *p_dom = (struct msg_domain *) xmalloc (sizeof (**p_dom));

      if (init_hash (&(*p_dom)->symbol_tab, 100) != 0)
	error (EXIT_FAILURE, errno, _("while creating hash table"));
      (*p_dom)->domain_name = name;
      (*p_dom)->next = NULL;
    }

  return *p_dom;
}


/* The address of this function will be assigned to the hook in the error
   functions.  */
static void
error_print ()
{
  /* We don't want the program name to be printed in messages.  Emacs'
     compile.el does not like this.  */
}


/* Process `domain' directive from .po file.  */
static void
format_directive_domain (pop, name)
     po_ty *pop;
     char *name;
{
  /* If no output file was given, we change it with each `domain'
     directive.  */
  if (output_file_name == NULL)
    {
      size_t correct;

      correct = strcspn (name, INVALID_PATH_CHAR);
      if (name[correct] != '\0')
	{
	  exit_status = EXIT_FAILURE;
	  if (correct == 0)
	    {
	      error (0, 0, _("\
domain name \"%s\" not suitable as file name"), name);
	      return;
	    }
	  else
	    error (0, 0, _("\
domain name \"%s\" not suitable as file name: will use prefix"), name);
	  name[correct] = '\0';
	}

      /* Set new domain.  */
      current_domain = new_domain (name);
    }
  else
    {
      if (verbose)
	/* We don't change the exit status here because this is really
	   only an information.  */
	error (0, 0, _("`domain %s' directive ignored"), name);

      /* NAME was allocated in po-gram.y but is not used anywhere.  */
      free (name);
    }
}


/* Process `msgid'/`msgstr' pair from .po file.  */
static void
format_directive_message (pop, msgid_string, msgid_pos, msgstr_string,
			  msgstr_pos)
     po_ty *pop;
     char *msgid_string;
     lex_pos_ty *msgid_pos;
     char *msgstr_string;
     lex_pos_ty *msgstr_pos;
{
  struct msgstr_def *entry;

  if (msgstr_string[0] == '\0')
    {
      if (verbose)
	/* We don't change the exit status here because this is really
	   only an information.  */
	error_with_loc (0, 0, msgstr_pos->file_name, msgstr_pos->line_number,
			_("empty `msgstr' entry ignored"));

      /* Free strings allocated in po-gram.y.  */
      free (msgid_string);
      free (msgstr_string);

      return;
    }

  /* We found a valid pair of msgid/msgstr.
     Construct struct to describe msgstr definition.  */
  entry = (struct msgstr_def *) xmalloc (sizeof (*entry));

  entry->msgstr = msgstr_string;
  entry->pos = *msgstr_pos;

  /* If --verbose is selected we do some more checks on both strings.  */
  if (verbose)
    check_pair (msgid_string, msgid_pos, msgstr_string, msgstr_pos);

  /* Check whether already a domain is specified.  If not use default
     domain.  */
  if (current_domain == NULL)
    current_domain = new_domain ("messages"); /* FIXME */

  /* We insert the ID/string pair into the hashing table.  But we have
     to take care for dublicates.  */
  if (insert_entry (&current_domain->symbol_tab, msgid_string,
		    strlen (msgid_string), entry))
    {
      /* We don't need the just constructed entry.  */
      free (entry);
      /* And the parameter string (allocated in po-gram.y).  */
      free (msgstr_string);

      if (verbose)
	{
	  /* We give a warning about this.  Get the old definition for
	     reference.  */
	  find_entry (&current_domain->symbol_tab, msgid_string,
		      strlen (msgid_string), (void **) &entry);

	  gram_error_with_loc (msgid_pos, _("duplicate message ID"));
	  gram_error_with_loc (&entry->pos, _("\
this is the location of the first definition"));

	  /* FIXME Should this be always a reason for an exit status != 0?  */
	  exit_status = EXIT_FAILURE;
	}
    }

  /* We do not need the msgid string in any case.  */
  free (msgid_string);
}


static int
compare_id (pval1, pval2)
     const void *pval1;
     const void *pval2;
{
  return strcmp (((struct id_str_pair *) pval1)->id,
		 ((struct id_str_pair *) pval2)->id);
}


static void
write_table (output_file, tab)
     FILE *output_file;
     hash_table *tab;
{
  static char null = '\0';
  /* This should be explained:
     Each string has an associate hashing value V, computed by a fixed
     function.  To locate the string we use open addressing with double
     hashing.  The first index will be V % M, where M is the size of the
     hashing table.  If no entry is found, iterating with a second,
     independent hashing function takes place.  This second value will
     be 1 + V % (M - 2).
     The approximate number of probes will be

       for unsuccessful search:  (1 - N / M) ^ -1
       for successful search:    - (N / M) ^ -1 * ln (1 - N / M)

     where N is the number of keys.

     If we now choose M to be the next prime bigger than 4 / 3 * N,
     we get the values
			 4   and   1.85  resp.
     Because unsuccesful searches are unlikely this is a good value.
     Formulas: [Knuth, The Art of Computer Programming, Volume 3,
		Sorting and Searching, 1973, Addison Wesley]  */
  nls_uint32 hash_tab_size = no_hash_table ? 0
					   : next_prime ((tab->filled * 4)
							 / 3);
  nls_uint32 *hash_tab;

  /* Header of the .mo file to be written.  */
  struct mo_file_header header;
  struct id_str_pair *msg_arr;
  void *ptr;
  size_t cnt;
  char *id;
  struct msgstr_def *entry;
  struct string_desc sd;

  /* Fill the structure describing the header.  */
  header.magic = _MAGIC;		/* Magic number.  */
  header.revision = MO_REVISION_NUMBER;	/* Revision number of file format.  */
  header.nstrings = tab->filled;	/* Number of strings.  */
  header.orig_tab_offset = sizeof (header);
			/* Offset of table for original string offsets.  */
  header.trans_tab_offset = sizeof (header)
			    + tab->filled * sizeof (struct string_desc);
			/* Offset of table for translation string offsets.  */
  header.hash_tab_size = hash_tab_size;	/* Size of used hashing table.  */
  header.hash_tab_offset =
	no_hash_table ? 0 : sizeof (header)
			    + 2 * (tab->filled * sizeof (struct string_desc));
			/* Offset of hashing table.  */

  /* Write the header out.  */
  fwrite (&header, sizeof (header), 1, output_file);

  /* Allocate table for the all elements of the hashing table.  */
  msg_arr = (struct id_str_pair *) alloca (tab->filled * sizeof (msg_arr[0]));

  /* Read values from hashing table into array.  */
  for (cnt = 0, ptr = NULL;
       iterate_table (tab, &ptr, (const void **) &id, (void **) &entry) >= 0;
       ++cnt)
    {
      msg_arr[cnt].id = id;
      msg_arr[cnt].str = entry->msgstr;
    }

  /* Sort the table according to original string.  */
  qsort (msg_arr, tab->filled, sizeof (msg_arr[0]), compare_id);

  /* Set offset to first byte after all the tables.  */
  sd.offset = roundup (sizeof (header)
		       + tab->filled * sizeof (sd)
		       + tab->filled * sizeof (sd)
		       + hash_tab_size * sizeof (nls_uint32),
		       alignment);

  /* Write out length and starting offset for all original strings.  */
  for (cnt = 0; cnt < tab->filled; ++cnt)
    {
      sd.length = strlen (msg_arr[cnt].id);
      fwrite (&sd, sizeof (sd), 1, output_file);
      sd.offset += roundup (sd.length + 1, alignment);
    }

  /* Write out length and starting offset for all translation strings.  */
  for (cnt = 0; cnt < tab->filled; ++cnt)
    {
      sd.length = strlen (msg_arr[cnt].str);
      fwrite (&sd, sizeof (sd), 1, output_file);
      sd.offset += roundup (sd.length + 1, alignment);
    }

  /* Skip this part when no hash table is needed.  */
  if (!no_hash_table)
    {
      /* Allocate room for the hashing table to be written out.  */
      hash_tab = (nls_uint32 *) alloca (hash_tab_size * sizeof (nls_uint32));
      memset (hash_tab, '\0', hash_tab_size * sizeof (nls_uint32));

      /* Insert all value in the hash table, following the algorithm described
	 above.  */
      for (cnt = 0; cnt < tab->filled; ++cnt)
	{
	  nls_uint32 hash_val = hash_string (msg_arr[cnt].id);
	  nls_uint32 idx = hash_val % hash_tab_size;

	  if (hash_tab[idx] != 0)
	    {
	      /* We need the second hashing function.  */
	      nls_uint32 c = 1 + (hash_val % (hash_tab_size - 2));

	      do
		if (idx >= hash_tab_size - c)
		  idx -= hash_tab_size - c;
		else
		  idx += c;
	      while (hash_tab[idx] != 0);
	    }

	  hash_tab[idx] = cnt + 1;
	}

      /* Write the hash table out.  */
      fwrite (hash_tab, sizeof (nls_uint32), hash_tab_size, output_file);
    }

  /* Write bytes to make first string to be aligned.  */
  cnt = sizeof (header) + 2 * tab->filled * sizeof (sd)
	+ hash_tab_size * sizeof (nls_uint32);
  fwrite (&null, 1, roundup (cnt, alignment) - cnt, output_file);

  /* Now write the original strings.  */
  for (cnt = 0; cnt < tab->filled; ++cnt)
    {
      size_t len = strlen (msg_arr[cnt].id);

      fwrite (msg_arr[cnt].id, len + 1, 1, output_file);
      fwrite (&null, 1, roundup (len + 1, alignment) - (len + 1), output_file);
    }

  /* Now write the translation strings.  */
  for (cnt = 0; cnt < tab->filled; ++cnt)
    {
      size_t len = strlen (msg_arr[cnt].str);

      fwrite (msg_arr[cnt].str, len + 1, 1, output_file);
      fwrite (&null, 1, roundup (len + 1, alignment) - (len + 1), output_file);

      free (msg_arr[cnt].str);
    }

  /* Hashing table is not used anmore.  */
  delete_hash (tab);
}


static void
check_pair (msgid, msgid_pos, msgstr, msgstr_pos)
     const char *msgid;
     const lex_pos_ty *msgid_pos;
     const char *msgstr;
     const lex_pos_ty *msgstr_pos;
{
  size_t msgid_len = strlen (msgid);
  size_t msgstr_len = strlen (msgstr);
  size_t nidfmts, nstrfmts;

  /* If the msgid string is empty we have the special entry reserved for
     information about the translation.  */
  if (msgid_len == 0)
    return;

  /* Test 1: check whether both or none of the strings begin with a '\n'.  */
  if (((msgid[0] == '\n') ^ (msgstr[0] == '\n')) != 0)
    {
      error_with_loc (0, 0, msgid_pos->file_name, msgid_pos->line_number, _("\
`msgid' and `msgstr' entry does not both begin with '\\n'"));
      exit_status = EXIT_FAILURE;
    }

  /* Test 2: check whether both or none of the strings end with a '\n'.  */
  if (((msgid[msgid_len - 1] == '\n') ^ (msgstr[msgstr_len - 1] == '\n')) != 0)
    {
      error_with_loc (0, 0, msgid_pos->file_name, msgid_pos->line_number, _("\
`msgid' and `msgstr' entry does not both end with '\\n'"));
      exit_status = EXIT_FAILURE;
    }

  /* Test 3: check whether both formats strings contain the same number
     of format specifications.  */
  nidfmts = parse_printf_format (msgid, 0, NULL);
  nstrfmts = parse_printf_format (msgstr, 0, NULL);
  if (nidfmts != nstrfmts)
    {
      error_with_loc (0, 0, msgid_pos->file_name, msgid_pos->line_number, _("\
number of format specifications in `msgid' and `msgstr' does not match"));
      exit_status = EXIT_FAILURE;
    }
  else
    {
      int *id_args = (int *) alloca (nidfmts * sizeof (int));
      int *str_args = (int *) alloca (nstrfmts * sizeof (int));
      size_t cnt;

      (void) parse_printf_format (msgid, nidfmts, id_args);
      (void) parse_printf_format (msgstr, nstrfmts, str_args);

      for (cnt = 0; cnt < nidfmts; ++cnt)
	if (id_args[cnt] != str_args[cnt])
	  {
	    error_with_loc (0, 0, msgid_pos->file_name, msgid_pos->line_number,
			    _("\
format specifications for argument %u are not the same"), cnt);
	    exit_status = EXIT_FAILURE;
	  }
    }
}


/* So that the one parer can be used for multiple programs, and also
   use good data hiding and encapsulation practices, an object
   oriented approach has been taken.  An object instance is allocated,
   and all actions resulting from the parse will be through
   invokations of method functions of that object.  */

static po_method_ty format_methods =
{
  sizeof(po_ty),
  NULL, /* constructor */
  NULL, /* destructor */
  format_directive_domain,
  format_directive_message,
  NULL, /* parse_brief */
  NULL, /* parse_debrief */
  NULL, /* comment */
  NULL, /* comment_dot */
  NULL, /* comment_filepos */
};


/* Read .po file FILENAME and store translation pairs.  */
static void
grammar (filename)
     char *filename;
{
  po_ty *pop;

  pop = po_alloc (&format_methods);
  po_scan (pop, filename);
  po_free (pop);
}


static const char *
add_mo_suffix (fname)
     const char *fname;
{
  size_t len;
  char *result;

  len = strlen (fname);
  if (len > 3 && memcmp (fname + len - 3, ".mo", 3) == 0)
    return xstrdup (fname);
  if (len > 4 && memcmp (fname + len - 4, ".gmo", 4) == 0)
    return xstrdup (fname);
  result = (char *) xmalloc (len + 4);
  stpcpy (stpcpy (result, fname), ".mo");
  return result;
}


