/********************************************************************\
 * gnc-ui-util.c -- utility functions for the GnuCash UI            *
 * Copyright (C) 2000 Dave Peticolas <dave@krondo.com>              *
 *                                                                  *
 * 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, contact:                        *
 *                                                                  *
 * Free Software Foundation           Voice:  +1-617-542-5942       *
 * 59 Temple Place - Suite 330        Fax:    +1-617-542-2652       *
 * Boston, MA  02111-1307,  USA       gnu@gnu.org                   *
\********************************************************************/

#include "config.h"

#include <ctype.h>
#include <errno.h>
#include <glib.h>
#include <limits.h>
#include <locale.h>
#include <stdarg.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#include "Account.h"
#include "EuroUtils.h"
#include "FileDialog.h"
#include "Group.h"
#include "global-options.h"
#include "gnc-ui.h"
#include "gnc-ui-util.h"
#include "gnc-common.h"
#include "gnc-component-manager.h"
#include "gnc-engine.h"
#include "gnc-engine-util.h"
#include "messages.h"


static short module = MOD_GUI;

static gboolean auto_decimal_enabled = FALSE;
static int auto_decimal_places = 2;    /* default, can be changed */


/********************************************************************\
 * gnc_color_deficits                                               *
 *   return a boolean value indicating whether deficit quantities   *
 *   should be displayed using the gnc_get_deficit_color().         *
 *                                                                  *
 * Args: none                                                       *
 * Returns: boolean deficit color indicator                         *
 \*******************************************************************/
gboolean
gnc_color_deficits (void)
{
  return gnc_lookup_boolean_option ("General",
                                    "Display negative amounts in red",
                                    TRUE);
}


/********************************************************************\
 * gnc_get_account_separator                                        *
 *   returns the current account separator character                *
 *                                                                  *
 * Args: none                                                       *
 * Returns: account separator character                             *
 \*******************************************************************/
char
gnc_get_account_separator (void)
{
  char separator = ':';
  char *string;

  string = gnc_lookup_multichoice_option("General",
                                         "Account Separator",
                                         "colon");

  if (safe_strcmp(string, "colon") == 0)
    separator = ':';
  else if (safe_strcmp(string, "slash") == 0)
    separator = '/';
  else if (safe_strcmp(string, "backslash") == 0)
    separator = '\\';
  else if (safe_strcmp(string, "dash") == 0)
    separator = '-';
  else if (safe_strcmp(string, "period") == 0)
    separator = '.';

  if (string != NULL)
    free(string);

  return separator;
}


const char *
gnc_ui_account_get_field_name (AccountFieldCode field)
{
  g_return_val_if_fail ((field >= 0) && (field < NUM_ACCOUNT_FIELDS), NULL);

  switch (field)
  {
    case ACCOUNT_TYPE :
      return _("Type");
      break;
    case ACCOUNT_NAME :
      return _("Account Name");
      break;
    case ACCOUNT_CODE :
      return _("Account Code");
      break;
    case ACCOUNT_DESCRIPTION :
      return _("Description");
      break;
    case ACCOUNT_NOTES :
      return _("Notes");
      break;
    case ACCOUNT_COMMODITY :
      return _("Commodity");
      break;
    case ACCOUNT_BALANCE :
      return _("Balance");
      break;
    case ACCOUNT_BALANCE_EURO :
      return _("Balance");
      break;
    case ACCOUNT_TOTAL :
      return _("Total");
      break;
    case ACCOUNT_TOTAL_EURO :
      return _("Total");
      break;
    case ACCOUNT_TAX_INFO :
      return _("Tax Info");
    default:
      break;
  }

  return NULL;
}


static gnc_numeric
gnc_account_get_balance_in_currency (Account *account,
                                     gnc_commodity *currency)
{
  GNCBook *book;
  GNCPriceDB *pdb;
  GNCPrice *price;
  gnc_numeric balance;
  GNCAccountType atype;
  gnc_commodity *commodity;

  if (!account || !currency)
    return gnc_numeric_zero ();

  balance = xaccAccountGetBalance (account);
  commodity = xaccAccountGetCommodity (account);

  if (gnc_numeric_zero_p (balance) ||
      gnc_commodity_equiv (currency, commodity))
    return balance;

  book = gncGetCurrentBook ();
  pdb = gnc_book_get_pricedb (book);

  price = gnc_pricedb_lookup_latest (pdb, commodity, currency);
  if (!price)
    return gnc_numeric_zero ();

  balance = gnc_numeric_mul (balance, gnc_price_get_value (price),
                             gnc_commodity_get_fraction (currency),
                             GNC_RND_ROUND);

  gnc_price_unref (price);

  return balance;
}


typedef struct
{
  gnc_commodity *currency;
  gnc_numeric balance;
} CurrencyBalance;


static gpointer
balance_helper (Account *account, gpointer data)
{
  CurrencyBalance *cb = data;
  gnc_numeric balance;

  if (!cb->currency)
    return NULL;

  balance = gnc_account_get_balance_in_currency (account, cb->currency);

  cb->balance = gnc_numeric_add (cb->balance, balance,
                                 gnc_commodity_get_fraction (cb->currency),
                                 GNC_RND_ROUND);

  return NULL;
}

gnc_numeric
gnc_ui_account_get_balance (Account *account, gboolean include_children)
{
  gnc_numeric balance;
  gnc_commodity *commodity;

  if (account == NULL)
    return gnc_numeric_zero ();

  commodity = xaccAccountGetCommodity (account);

  balance = gnc_account_get_balance_in_currency (account, commodity);

  if (include_children)
  {
    AccountGroup *children;
    CurrencyBalance cb = { commodity, balance };

    children = xaccAccountGetChildren (account);

    xaccGroupForEachAccount (children, balance_helper, &cb, TRUE);

    balance = cb.balance;
  }

  /* reverse sign if needed */
  if (gnc_reverse_balance (account))
    balance = gnc_numeric_neg (balance);

  return balance;
}


char *
gnc_ui_account_get_field_value_string (Account *account,
                                       AccountFieldCode field)
{
  g_return_val_if_fail ((field >= 0) && (field < NUM_ACCOUNT_FIELDS), NULL);

  if (account == NULL)
    return NULL;

  switch (field)
  {
    case ACCOUNT_TYPE :
      return g_strdup (xaccAccountGetTypeStr(xaccAccountGetType(account)));

    case ACCOUNT_NAME :
      return g_strdup (xaccAccountGetName(account));

    case ACCOUNT_CODE :
      return g_strdup (xaccAccountGetCode(account));

    case ACCOUNT_DESCRIPTION :
      return g_strdup (xaccAccountGetDescription(account));

    case ACCOUNT_NOTES :
      return g_strdup (xaccAccountGetNotes(account));

    case ACCOUNT_COMMODITY :
      return
        g_strdup
        (gnc_commodity_get_printname(xaccAccountGetCommodity(account)));

    case ACCOUNT_BALANCE :
      {
        gnc_numeric balance = gnc_ui_account_get_balance(account, FALSE);

        return g_strdup
          (xaccPrintAmount (balance, gnc_account_print_info (account, TRUE)));
      }

    case ACCOUNT_BALANCE_EURO :
      {
	gnc_commodity * commodity = xaccAccountGetCommodity(account);
        gnc_numeric balance = gnc_ui_account_get_balance(account, FALSE);
	gnc_numeric euro_balance = gnc_convert_to_euro(commodity, balance);

        return g_strdup
          (xaccPrintAmount(euro_balance,
                           gnc_commodity_print_info (gnc_get_euro (), TRUE)));
      }

    case ACCOUNT_TOTAL :
      {
	gnc_numeric balance = gnc_ui_account_get_balance(account, TRUE);

        return g_strdup
          (xaccPrintAmount(balance, gnc_account_print_info (account, TRUE)));
      }

    case ACCOUNT_TOTAL_EURO :
      {
	gnc_commodity * commodity = xaccAccountGetCommodity(account);
	gnc_numeric balance = gnc_ui_account_get_balance(account, TRUE);
	gnc_numeric euro_balance = gnc_convert_to_euro(commodity, balance);

	return g_strdup
          (xaccPrintAmount(euro_balance,
                           gnc_commodity_print_info (gnc_get_euro (), TRUE)));
      }

    case ACCOUNT_TAX_INFO:
      return gnc_ui_account_get_tax_info_string (account);

    default:
      break;
  }

  return NULL;
}


/********************************************************************\
 * gnc_get_reconcile_str                                            *
 *   return the i18n'd string for the given reconciled flag         *
 *                                                                  *
 * Args: reconciled_flag - the flag to stringize                    *
 * Returns: the i18n'd reconciled string                            *
\********************************************************************/
const char *
gnc_get_reconcile_str (char reconciled_flag)
{
  switch (reconciled_flag)
  {
    case NREC: return _("not cleared:n") + 12;
    case CREC: return _("cleared:c") + 8;
    case YREC: return _("reconciled:y") + 11;
    case FREC: return _("frozen:f") + 7;
    default:
      PERR("Bad reconciled flag\n");
      return NULL;
  }
}


/* Get the full name of a quote source */
const char *
gnc_get_source_name (PriceSourceCode source)
{
  switch (source)
  {
    case SOURCE_NONE :
      return _("(none)");
    case SOURCE_YAHOO :
      return "Yahoo";
    case SOURCE_YAHOO_EUROPE :
      return "Yahoo Europe";
    case SOURCE_FIDELITY :
      return "Fidelity";
    case SOURCE_TROWEPRICE :
      return "T. Rowe Price";
    case SOURCE_VANGUARD :
      return "Vanguard";
    case SOURCE_ASX :
      return "ASX";
    case SOURCE_TIAA_CREF :
      return "TIAA-CREF";
    default:
      break;
  }

  PWARN("Unknown source");
  return NULL;
}

/* Get the codename string of a quote source */
const char *
gnc_get_source_code_name (PriceSourceCode source)
{
  switch (source)
  {
    case SOURCE_NONE :
      return NULL;
    case SOURCE_YAHOO :
      return "YAHOO";
    case SOURCE_YAHOO_EUROPE :
      return "YAHOO_EUROPE";
    case SOURCE_FIDELITY :
      return "FIDELITY";
    case SOURCE_TROWEPRICE :
      return "TRPRICE";
    case SOURCE_VANGUARD :
      return "VANGUARD";
    case SOURCE_ASX :
      return "ASX";
    case SOURCE_TIAA_CREF :
      return "TIAACREF";
    default:
      break;
  }

  PWARN("Unknown source");
  return NULL;
}

/* Get the codename string of a source */
PriceSourceCode
gnc_get_source_code (const char * codename)
{
  gint i;

  if (codename == NULL)
    return SOURCE_NONE;

  if (safe_strcmp(codename, "") == 0)
    return SOURCE_NONE;

  for (i = 1; i < NUM_SOURCES; i++)
    if (safe_strcmp(codename, gnc_get_source_code_name(i)) == 0)
      return i;

  PWARN("Unknown source");
  return SOURCE_NONE;
}

static const char *
equity_base_name (GNCEquityType equity_type)
{
  switch (equity_type)
  {
    case EQUITY_OPENING_BALANCE:
      return N_("Opening Balances");

    case EQUITY_RETAINED_EARNINGS:
      return N_("Retained Earnings");

    default:
      return NULL;
  }
}

Account *
gnc_find_or_create_equity_account (AccountGroup *group,
                                   GNCEquityType equity_type,
                                   gnc_commodity *currency)
{
  Account *parent;
  Account *account;
  gboolean name_exists;
  gboolean base_name_exists;
  const char *base_name;
  char *name;

  g_return_val_if_fail (equity_type >= 0, NULL);
  g_return_val_if_fail (equity_type < NUM_EQUITY_TYPES, NULL);
  g_return_val_if_fail (currency != NULL, NULL);

  if (!group)
    group = gncGetCurrentGroup ();

  base_name = equity_base_name (equity_type);

  account = xaccGetAccountFromName (group, base_name);
  if (account && xaccAccountGetType (account) != EQUITY)
    account = NULL;

  if (!account)
  {
    base_name = _(base_name);

    account = xaccGetAccountFromName (group, base_name);
    if (account && xaccAccountGetType (account) != EQUITY)
      account = NULL;
  }

  base_name_exists = (account != NULL);

  if (account &&
      gnc_commodity_equiv (currency, xaccAccountGetCommodity (account)))
    return account;

  name = g_strconcat (base_name, " - ",
                      gnc_commodity_get_mnemonic (currency), NULL);
  account = xaccGetAccountFromName (group, name);
  if (account && xaccAccountGetType (account) != EQUITY)
    account = NULL;

  name_exists = (account != NULL);

  if (account &&
      gnc_commodity_equiv (currency, xaccAccountGetCommodity (account)))
    return account;

  /* Couldn't find one, so create it */
  if (name_exists && base_name_exists)
  {
    PWARN ("equity account with unexpected currency");
    g_free (name);
    return NULL;
  }

  if (!base_name_exists &&
      gnc_commodity_equiv (currency, gnc_default_currency ()))
  {
    g_free (name);
    name = g_strdup (base_name);
  }

  parent = xaccGetAccountFromName (group, _("Equity"));
  if (parent && xaccAccountGetType (parent) != EQUITY)
    parent == NULL;

  account = xaccMallocAccount ();

  xaccAccountBeginEdit (account);

  xaccAccountSetName (account, name);
  xaccAccountSetType (account, EQUITY);
  xaccAccountSetCommodity (account, currency);

  if (parent)
  {
    xaccAccountBeginEdit (parent);
    xaccAccountInsertSubAccount (parent, account);
    xaccAccountCommitEdit (parent);
  }
  else
    xaccGroupInsertAccount (group, account);

  xaccAccountCommitEdit (account);

  g_free (name);

  return account;
}

gboolean
gnc_account_create_opening_balance (Account *account,
                                    gnc_numeric balance,
                                    time_t date)
{
  Account *equity_account;
  Transaction *trans;
  Split *split;

  if (gnc_numeric_zero_p (balance))
    return TRUE;

  g_return_val_if_fail (account != NULL, FALSE);

  equity_account =
    gnc_find_or_create_equity_account (xaccGetAccountRoot (account),
                                       EQUITY_OPENING_BALANCE,
                                       xaccAccountGetCommodity (account));
  if (!equity_account)
    return FALSE;

  xaccAccountBeginEdit (account);
  xaccAccountBeginEdit (equity_account);

  trans = xaccMallocTransaction ();

  xaccTransBeginEdit (trans);

  xaccTransSetCurrency (trans, xaccAccountGetCommodity (account));
  xaccTransSetDateSecs (trans, date);
  xaccTransSetDescription (trans, _("Opening Balance"));

  split = xaccMallocSplit ();

  xaccTransAppendSplit (trans, split);
  xaccAccountInsertSplit (account, split);

  xaccSplitSetAmount (split, balance);
  xaccSplitSetValue (split, balance);

  balance = gnc_numeric_neg (balance);

  split = xaccMallocSplit ();

  xaccTransAppendSplit (trans, split);
  xaccAccountInsertSplit (equity_account, split);

  xaccSplitSetAmount (split, balance);
  xaccSplitSetValue (split, balance);

  xaccTransCommitEdit (trans);
  xaccAccountCommitEdit (equity_account);
  xaccAccountCommitEdit (account);

  return TRUE;
}

static void
gnc_lconv_set (char **p_value, char *default_value)
{
  char *value = *p_value;

  if ((value == NULL) || (value[0] == 0))
    *p_value = default_value;

  *p_value = g_strdup (*p_value);
}

static void
gnc_lconv_set_char (char *p_value, char default_value)
{
  if ((p_value != NULL) && (*p_value == CHAR_MAX))
    *p_value = default_value;
}

struct lconv *
gnc_localeconv (void)
{
  static struct lconv lc;
  static gboolean lc_set = FALSE;

  if (lc_set)
    return &lc;

  lc = *localeconv();

  gnc_lconv_set(&lc.decimal_point, ".");
  gnc_lconv_set(&lc.thousands_sep, ",");
  gnc_lconv_set(&lc.grouping, "\003");
  gnc_lconv_set(&lc.int_curr_symbol, "USD ");
  gnc_lconv_set(&lc.currency_symbol, "$");
  gnc_lconv_set(&lc.mon_decimal_point, ".");
  gnc_lconv_set(&lc.mon_thousands_sep, ",");
  gnc_lconv_set(&lc.mon_grouping, "\003");
  gnc_lconv_set(&lc.negative_sign, "-");

  gnc_lconv_set_char(&lc.frac_digits, 2);
  gnc_lconv_set_char(&lc.int_frac_digits, 2);
  gnc_lconv_set_char(&lc.p_cs_precedes, 1);
  gnc_lconv_set_char(&lc.p_sep_by_space, 0);
  gnc_lconv_set_char(&lc.n_cs_precedes, 1);
  gnc_lconv_set_char(&lc.n_sep_by_space, 0);
  gnc_lconv_set_char(&lc.p_sign_posn, 1);
  gnc_lconv_set_char(&lc.n_sign_posn, 1);

  lc_set = TRUE;

  return &lc;
}

gnc_commodity *
gnc_locale_default_currency (void)
{
  static gnc_commodity * currency;
  struct lconv         * lc;
  static gboolean      got_it = FALSE;

  if (!got_it)
  {
    char *symbol;

    lc = gnc_localeconv();

    symbol = g_strdup (lc->int_curr_symbol);

    /* The int_curr_symbol includes a space at the end! Note: you
     * can't just change "USD " to "USD" in gnc_localeconv, because
     * that is only used if int_curr_symbol was not defined in the
     * current locale. If it was, it will have the space! */
    g_strstrip (symbol);

    currency = gnc_commodity_table_lookup (gnc_engine_commodities(),
                                           GNC_COMMODITY_NS_ISO,
                                           symbol);

    if (!currency)
      currency = gnc_commodity_table_lookup (gnc_engine_commodities(),
                                             GNC_COMMODITY_NS_ISO,
                                             "USD");

    g_free (symbol);
    got_it = TRUE;
  }

  return currency;
}


/* Return the number of decimal places for this locale. */
int 
gnc_locale_decimal_places (void)
{
  static gboolean got_it = FALSE;
  static int places;
  struct lconv *lc;

  if (got_it)
    return places;

  lc = gnc_localeconv();
  places = lc->frac_digits;

  /* frac_digits is already initialized by gnc_localeconv, hopefully
   * to a reasonable default. */
  got_it = TRUE;

  return places;
}


static GList *locale_stack = NULL;

void
gnc_push_locale (const char *locale)
{
  char *saved_locale;

  g_return_if_fail (locale != NULL);

  saved_locale = g_strdup (setlocale (LC_ALL, NULL));
  locale_stack = g_list_prepend (locale_stack, saved_locale);
  setlocale (LC_ALL, locale);
}

void
gnc_pop_locale (void)
{
  char *saved_locale;
  GList *node;

  g_return_if_fail (locale_stack != NULL);

  node = locale_stack;
  saved_locale = node->data;

  setlocale (LC_ALL, saved_locale);

  locale_stack = g_list_remove_link (locale_stack, node);
  g_list_free_1 (node);
  g_free (saved_locale);
}

GNCPrintAmountInfo
gnc_default_print_info (gboolean use_symbol)
{
  static GNCPrintAmountInfo info;
  static gboolean got_it = FALSE;

  struct lconv *lc;

  info.use_symbol = use_symbol ? 1 : 0;
  if (got_it)
    return info;

  lc = gnc_localeconv ();

  info.commodity = gnc_default_currency ();

  info.max_decimal_places = lc->frac_digits;
  info.min_decimal_places = lc->frac_digits;

  info.use_separators = 1;
  info.use_locale = 1;
  info.monetary = 1;

  got_it = TRUE;

  return info;
}

static gboolean
is_decimal_fraction (int fraction, guint8 *max_decimal_places_p)
{
  guint8 max_decimal_places = 0;

  if (fraction <= 0)
    return FALSE;

  while (fraction != 1)
  {
    if (fraction % 10 != 0)
      return FALSE;

    fraction = fraction / 10;
    max_decimal_places += 1;
  }

  if (max_decimal_places_p)
    *max_decimal_places_p = max_decimal_places;

  return TRUE;
}

GNCPrintAmountInfo
gnc_commodity_print_info (const gnc_commodity *commodity,
                          gboolean use_symbol)
{
  GNCPrintAmountInfo info;
  gboolean is_iso;

  if (commodity == NULL)
    return gnc_default_print_info (use_symbol);

  info.commodity = commodity;

  is_iso = (safe_strcmp (gnc_commodity_get_namespace (commodity),
                         GNC_COMMODITY_NS_ISO) == 0);

  if (is_decimal_fraction (gnc_commodity_get_fraction (commodity),
                           &info.max_decimal_places))
  {
    if (is_iso)
      info.min_decimal_places = info.max_decimal_places;
    else
      info.min_decimal_places = 0;
  }
  else
    info.max_decimal_places = info.min_decimal_places = 0;

  info.use_separators = 1;
  info.use_symbol = use_symbol ? 1 : 0;
  info.use_locale = is_iso ? 1 : 0;
  info.monetary = 1;

  return info;
}

static GNCPrintAmountInfo
gnc_account_print_info_helper(Account *account, gboolean use_symbol,
                              gnc_commodity * (*efffunc)(Account *),
                              int (*scufunc)(Account*))
{
  GNCPrintAmountInfo info;
  gboolean is_iso;
  int scu;

  if (account == NULL)
    return gnc_default_print_info (use_symbol);

  info.commodity = efffunc (account);

  is_iso = (safe_strcmp (gnc_commodity_get_namespace (info.commodity),
                         GNC_COMMODITY_NS_ISO) == 0);

  scu = scufunc (account);

  if (is_decimal_fraction (scu, &info.max_decimal_places))
  {
    if (is_iso)
      info.min_decimal_places = info.max_decimal_places;
    else
      info.min_decimal_places = 0;
  }
  else
    info.max_decimal_places = info.min_decimal_places = 0;

  info.use_separators = 1;
  info.use_symbol = use_symbol ? 1 : 0;
  info.use_locale = is_iso ? 1 : 0;
  info.monetary = 1;

  return info;
}

GNCPrintAmountInfo
gnc_account_print_info (Account *account, gboolean use_symbol)
{
    return gnc_account_print_info_helper(account, use_symbol,
                                         xaccAccountGetCommodity,
                                         xaccAccountGetCommoditySCU);
}

GNCPrintAmountInfo
gnc_split_amount_print_info (Split *split, gboolean use_symbol)
{
  if (!split)
  {
    GNCPrintAmountInfo info = gnc_default_share_print_info ();
    info.use_symbol = use_symbol;
    return info;
  }

  return gnc_account_print_info (xaccSplitGetAccount (split), use_symbol);
}

GNCPrintAmountInfo
gnc_split_value_print_info (Split *split, gboolean use_symbol)
{
  Transaction *trans;

  if (!split) return gnc_default_print_info (use_symbol);

  trans = xaccSplitGetParent (split);

  return gnc_commodity_print_info (xaccTransGetCurrency (trans), use_symbol);
}

static GNCPrintAmountInfo
gnc_default_print_info_helper (int decplaces)
{
    GNCPrintAmountInfo info;

    info.commodity = NULL;

    info.max_decimal_places = decplaces;
    info.min_decimal_places = 0;

    info.use_separators = 1;
    info.use_symbol = 0;
    info.use_locale = 1;
    info.monetary = 1;

    return info;
}

GNCPrintAmountInfo
gnc_default_share_print_info (void)
{
  static GNCPrintAmountInfo info;
  static gboolean got_it = FALSE;

  if (!got_it)
  {
      info = gnc_default_print_info_helper (5);
      got_it = TRUE;
  }

  return info;
}

GNCPrintAmountInfo
gnc_default_price_print_info (void)
{
  static GNCPrintAmountInfo info;
  static gboolean got_it = FALSE;

  if (!got_it)
  {
    info = gnc_default_print_info_helper (6);
    got_it = TRUE;
  }

  return info;
}

GNCPrintAmountInfo
gnc_integral_print_info (void)
{
  static GNCPrintAmountInfo info;
  static gboolean got_it = FALSE;

  if (!got_it)
  {
    info = gnc_default_print_info_helper (0);
    got_it = TRUE;
  }

  return info;
}

/* Utility function for printing non-negative amounts */
static int
PrintAmountInternal(char *buf, gnc_numeric val, const GNCPrintAmountInfo *info)
{
  struct lconv *lc = gnc_localeconv();
  int num_whole_digits;
  char temp_buf[64];
  gnc_numeric whole;

  g_assert (info != NULL);

  if (gnc_numeric_check (val))
  {
    PWARN ("Bad numeric.");
    *buf = '\0';
    return 0;
  }

  /* print the absolute value */
  val = gnc_numeric_abs (val);

  /* calculate the integer part and the remainder */
  whole = gnc_numeric_create (val.num / val.denom, 1);
  val = gnc_numeric_sub (val, whole, val.denom, GNC_RND_NEVER);
  if (gnc_numeric_check (val))
  {
    PWARN ("Problem with remainder.");
    *buf = '\0';
    return 0;
  }

  /* print the integer part without separators */
  sprintf(temp_buf, "%lld", (long long int) whole.num);
  num_whole_digits = strlen (temp_buf);

  if (!info->use_separators)
    strcpy (buf, temp_buf);
  else
  {
    int group_count;
    char separator;
    char *temp_ptr;
    char *buf_ptr;
    char *group;

    if (info->monetary)
    {
      separator = lc->mon_thousands_sep[0];
      group = lc->mon_grouping;
    }
    else
    {
      separator = lc->thousands_sep[0];
      group = lc->grouping;
    }

    buf_ptr = buf;
    temp_ptr = &temp_buf[num_whole_digits - 1];
    group_count = 0;

    while (temp_ptr != temp_buf)
    {
      *buf_ptr++ = *temp_ptr--;

      if (*group != CHAR_MAX)
      {
        group_count++;

        if (group_count == *group)
        {
          *buf_ptr++ = separator;
          group_count = 0;

          /* Peek ahead at the next group code */
          switch (group[1])
          {
            /* A null char means repeat the last group indefinitely */
            case '\0':
              break;
            /* CHAR_MAX means no more grouping allowed */
            case CHAR_MAX:
              /* fall through */
            /* Anything else means another group size */
            default:
              group++;
              break;
          }
        }
      }
    }

    /* We built the string backwards, now reverse */
    *buf_ptr++ = *temp_ptr;
    *buf_ptr = '\0';
    g_strreverse(buf);
  } /* endif */

  /* at this point, buf contains the whole part of the number */

  /* If it's not decimal, print the fraction as an expression */
  if (!is_decimal_fraction (val.denom, NULL))
  {
    if (!gnc_numeric_zero_p (val))
    {
      val = gnc_numeric_reduce (val);

      sprintf (temp_buf, " + %lld / %lld",
               (long long int) val.num,
               (long long int) val.denom);

      strcat (buf, temp_buf);
    }
  }
  else
  {
    guint8 num_decimal_places = 0;
    char *temp_ptr = temp_buf;

    *temp_ptr++ = info->monetary ?
      lc->mon_decimal_point[0] : lc->decimal_point[0];

    while (!gnc_numeric_zero_p (val) && val.denom != 1)
    {
      gint64 digit;

      val.denom = val.denom / 10;

      digit = val.num / val.denom;

      *temp_ptr++ = digit + '0';
      num_decimal_places++;

      val.num = val.num - (digit * val.denom);
    }

    /* add in needed zeros */
    while (num_decimal_places < info->min_decimal_places)
    {
      *temp_ptr++ = '0';
      num_decimal_places++;
    }

    /* cap the end and move to the last character */
    *temp_ptr-- = '\0';

    /* Here we strip off trailing decimal zeros per the argument. */
    while (*temp_ptr == '0' && num_decimal_places > info->min_decimal_places)
    {
      *temp_ptr-- = '\0';
      num_decimal_places--;
    }

    if (num_decimal_places > info->max_decimal_places)
    {
      PWARN ("max_decimal_places too small");
    }

    if (num_decimal_places > 0)
      strcat (buf, temp_buf);
  }

  return strlen(buf);
}

int
xaccSPrintAmount (char * bufp, gnc_numeric val, GNCPrintAmountInfo info)
{
   struct lconv *lc;

   char *orig_bufp = bufp;
   const char *currency_symbol;
   const char *sign;

   char cs_precedes;
   char sep_by_space;
   char sign_posn;

   gboolean print_sign = TRUE;
   gboolean is_shares = FALSE;

   if (!bufp)
     return 0;

   lc = gnc_localeconv();

   if (info.use_symbol)
   {
     if (gnc_commodity_equiv (info.commodity, gnc_locale_default_currency ()))
       currency_symbol = lc->currency_symbol;
     else
     {
       if (info.commodity &&
           safe_strcmp (GNC_COMMODITY_NS_ISO,
                        gnc_commodity_get_namespace (info.commodity)) != 0)
         is_shares = TRUE;

       currency_symbol = gnc_commodity_get_mnemonic (info.commodity);
       info.use_locale = 0;
     }

     if (currency_symbol == NULL)
       currency_symbol = "";
   }
   else
     currency_symbol = NULL;

   if (!info.use_locale)
   {
     cs_precedes = is_shares ? 0 : 1;
     sep_by_space = 1;
   }
   else
   {
     if (gnc_numeric_negative_p (val))
     {
       cs_precedes  = lc->n_cs_precedes;
       sep_by_space = lc->n_sep_by_space;
     }
     else
     {
       cs_precedes  = lc->p_cs_precedes;
       sep_by_space = lc->p_sep_by_space;
     }
   }

   if (gnc_numeric_negative_p (val))
   {
     sign = lc->negative_sign;
     sign_posn = lc->n_sign_posn;
   }
   else
   {
     sign = lc->positive_sign;
     sign_posn = lc->p_sign_posn;
   }

   if (gnc_numeric_zero_p (val) || (sign == NULL) || (sign[0] == 0))
     print_sign = FALSE;

   /* See if we print sign now */
   if (print_sign && (sign_posn == 1))
     bufp = gnc_stpcpy(bufp, sign);

   /* Now see if we print currency */
   if (cs_precedes)
   {
     /* See if we print sign now */
     if (print_sign && (sign_posn == 3))
       bufp = gnc_stpcpy(bufp, sign);

     if (info.use_symbol)
     {
       bufp = gnc_stpcpy(bufp, currency_symbol);
       if (sep_by_space)
         bufp = gnc_stpcpy(bufp, " ");
     }

     /* See if we print sign now */
     if (print_sign && (sign_posn == 4))
       bufp = gnc_stpcpy(bufp, sign);
   }

   /* Now see if we print parentheses */
   if (print_sign && (sign_posn == 0))
     bufp = gnc_stpcpy(bufp, "(");

   /* Now print the value */
   bufp += PrintAmountInternal(bufp, val, &info);

   /* Now see if we print parentheses */
   if (print_sign && (sign_posn == 0))
     bufp = gnc_stpcpy(bufp, ")");

   /* Now see if we print currency */
   if (!cs_precedes)
   {
     /* See if we print sign now */
     if (print_sign && (sign_posn == 3))
       bufp = gnc_stpcpy(bufp, sign);

     if (info.use_symbol)
     {
       if (sep_by_space)
         bufp = gnc_stpcpy(bufp, " ");
       bufp = gnc_stpcpy(bufp, currency_symbol);
     }

     /* See if we print sign now */
     if (print_sign && (sign_posn == 4))
       bufp = gnc_stpcpy(bufp, sign);
   }

   /* See if we print sign now */
   if (print_sign && (sign_posn == 2))
     bufp = gnc_stpcpy(bufp, sign);

   /* return length of printed string */
   return (bufp - orig_bufp);
}

const char *
xaccPrintAmount (gnc_numeric val, GNCPrintAmountInfo info)
{
  /* hack alert -- this is not thread safe ... */
  static char buf[1024];

  xaccSPrintAmount (buf, val, info);

  /* its OK to return buf, since we declared it static */
  return buf;
}

const char *
DxaccPrintAmount (double dval, GNCPrintAmountInfo info)
{
  gnc_numeric val;

  val = double_to_gnc_numeric (dval, GNC_DENOM_AUTO,
                               GNC_DENOM_SIGFIGS(6) | GNC_RND_ROUND);

  return xaccPrintAmount (val, info);
}

int
DxaccSPrintAmount (char * bufp, double dval, GNCPrintAmountInfo info)
{
  gnc_numeric val;

  val = double_to_gnc_numeric (dval, GNC_DENOM_AUTO, 
                               GNC_DENOM_SIGFIGS(6) | GNC_RND_ROUND);

  return xaccSPrintAmount (bufp, val, info);
}


/********************************************************************\
 * xaccParseAmount                                                  *
 *   parses amount strings using locale data                        *
 *                                                                  *
 * Args: in_str   -- pointer to string rep of num                   *
 *       monetary -- boolean indicating whether value is monetary   *
 *       result   -- pointer to result location, may be NULL        *
 *       endstr   -- used to store first digit not used in parsing  *
 * Return: gboolean -- TRUE if a number found and parsed            *
 *                     If FALSE, result is not changed              *
\********************************************************************/

/* Parsing state machine states */
typedef enum
{
  START_ST,       /* Parsing initial whitespace */
  NEG_ST,         /* Parsed a negative sign or a left paren */
  PRE_GROUP_ST,   /* Parsing digits before grouping and decimal characters */
  START_GROUP_ST, /* Start of a digit group encountered (possibly) */
  IN_GROUP_ST,    /* Within a digit group */
  FRAC_ST,        /* Parsing the fractional portion of a number */
  DONE_ST,        /* Finished, number is correct module grouping constraints */
  NO_NUM_ST       /* Finished, number was malformed */
} ParseState;

#define done_state(state) (((state) == DONE_ST) || ((state) == NO_NUM_ST))

G_INLINE_FUNC long long int multiplier (int num_decimals);

G_INLINE_FUNC long long int
multiplier (int num_decimals)
{
  switch (num_decimals)
  {
    case 8:
      return 100000000;
    case 7:
      return 10000000;
    case 6:
      return 1000000;
    case 5:
      return 100000;
    case 4:
      return 10000;
    case 3:
      return 1000;
    case 2:
      return 100;
    case 1:
      return 10;
    default:
      PERR("bad fraction length");
      g_assert_not_reached();
      break;
  }

  return 1;
}

gboolean
DxaccParseAmount (const char * in_str, gboolean monetary, double *result,
                  char **endstr)
{
  gnc_numeric answer;

  if (!xaccParseAmount (in_str, monetary, &answer, endstr))
    return FALSE;

  if (result)
    *result = gnc_numeric_to_double (answer);

  return TRUE;
}

gboolean
xaccParseAmount (const char * in_str, gboolean monetary, gnc_numeric *result,
                 char **endstr)
{
  struct lconv *lc = gnc_localeconv();
  gboolean is_negative;
  gboolean got_decimal;
  gboolean need_paren;
  GList * group_data;
  long long int numer;
  long long int denom;
  int group_count;

  ParseState state;

  char negative_sign;
  char decimal_point;
  char group_separator;

  const char *in;
  char *out_str;
  char *out;

  /* Initialize *endstr to in_str */
  if (endstr != NULL)
    *endstr = (char *) in_str;

  if (in_str == NULL)
    return FALSE;

  negative_sign = lc->negative_sign[0];
  if (monetary)
  {
    group_separator = lc->mon_thousands_sep[0];
    decimal_point = lc->mon_decimal_point[0];
  }
  else
  {
    group_separator = lc->thousands_sep[0];
    decimal_point = lc->decimal_point[0];
  }

  /* 'out_str' will be used to store digits for numeric conversion.
   * 'out' will be used to traverse out_str. */
  out = out_str = g_new(char, strlen(in_str) + 1);

  /* 'in' is used to traverse 'in_str'. */
  in = in_str;

  is_negative = FALSE;
  got_decimal = FALSE;
  need_paren = FALSE;
  group_data = NULL;
  group_count = 0;
  numer = 0;
  denom = 1;

  /* Initialize the state machine */
  state = START_ST;

  /* This while loop implements a state machine for parsing numbers. */
  while (TRUE)
  {
    ParseState next_state = state;

    /* Note we never need to check for then end of 'in_str' explicitly.
     * The 'else' clauses on all the state transitions will handle that. */
    switch (state)
    {
      /* START_ST means we have parsed 0 or more whitespace characters */
      case START_ST:
        if (isdigit(*in))
        {
          *out++ = *in; /* we record the digits themselves in out_str
                         * for later conversion by libc routines */
          next_state = PRE_GROUP_ST;
        }
        else if (*in == decimal_point)
        {
          next_state = FRAC_ST;
        }
        else if (isspace(*in))
        {
        }
        else if (*in == negative_sign)
        {
          is_negative = TRUE;
          next_state = NEG_ST;
        }
        else if (*in == '(')
        {
          is_negative = TRUE;
          need_paren = TRUE;
          next_state = NEG_ST;
        }
        else
        {
          next_state = NO_NUM_ST;
        }

        break;

      /* NEG_ST means we have just parsed a negative sign. For now,
       * we only recognize formats where the negative sign comes first. */
      case NEG_ST:
        if (isdigit(*in))
        {
          *out++ = *in;
          next_state = PRE_GROUP_ST;
        }
        else if (*in == decimal_point)
        {
          next_state = FRAC_ST;
        }
        else if (isspace(*in))
        {
        }
        else
        {
          next_state = NO_NUM_ST;
        }

        break;

      /* PRE_GROUP_ST means we have started parsing the number, but
       * have not encountered a decimal point or a grouping character. */
      case PRE_GROUP_ST:
        if (isdigit(*in))
        {
          *out++ = *in;
        }
        else if (*in == decimal_point)
        {
          next_state = FRAC_ST;
        }
        else if (*in == group_separator)
        {
          next_state = START_GROUP_ST;
        }
        else if (*in == ')' && need_paren)
        {
          next_state = DONE_ST;
          need_paren = FALSE;
        }
        else
        {
          next_state = DONE_ST;
        }

        break;

      /* START_GROUP_ST means we have just parsed a group character.
       * Note that group characters might be whitespace!!! In general,
       * if a decimal point or a group character is whitespace, we
       * try to interpret it in the fashion that will allow parsing
       * of the current number to continue. */
      case START_GROUP_ST:
        if (isdigit(*in))
        {
          *out++ = *in;
          group_count++; /* We record the number of digits
                          * in the group for later checking. */
          next_state = IN_GROUP_ST;
        }
        else if (*in == decimal_point)
        {
          /* If we now get a decimal point, and both the decimal
           * and the group separator are also whitespace, assume
           * the last group separator was actually whitespace and
           * stop parsing. Otherwise, there's a problem. */
          if (isspace(group_separator) && isspace(decimal_point))
            next_state = DONE_ST;
          else
            next_state = NO_NUM_ST;
        }
        else if (*in == ')' && need_paren)
        {
          if (isspace(group_separator))
          {
            next_state = DONE_ST;
            need_paren = FALSE;
          }
          else
            next_state = NO_NUM_ST;
        }
        else
        {
          /* If the last group separator is also whitespace,
           * assume it was intended as such and stop parsing.
           * Otherwise, there is a problem. */
          if (isspace(group_separator))
            next_state = DONE_ST;
          else
            next_state = NO_NUM_ST;
        }
        break;

      /* IN_GROUP_ST means we are in the middle of parsing
       * a group of digits. */
      case IN_GROUP_ST:
        if (isdigit(*in))
        {
          *out++ = *in;
          group_count++; /* We record the number of digits
                          * in the group for later checking. */
        }
        else if (*in == decimal_point)
        {
          next_state = FRAC_ST;
        }
        else if (*in == group_separator)
        {
          next_state = START_GROUP_ST;
        }
        else if (*in == ')' && need_paren)
        {
          next_state = DONE_ST;
          need_paren = FALSE;
        }
        else
        {
          next_state = DONE_ST;
        }

        break;

      /* FRAC_ST means we are now parsing fractional digits. */
      case FRAC_ST:
        if (isdigit(*in))
        {
          *out++ = *in;
        }
        else if (*in == decimal_point)
        {
          /* If a subsequent decimal point is also whitespace,
           * assume it was intended as such and stop parsing.
           * Otherwise, there is a problem. */
          if (isspace(decimal_point))
            next_state = DONE_ST;
          else
            next_state = NO_NUM_ST;
        }
        else if (*in == group_separator)
        {
          /* If a subsequent group separator is also whitespace,
           * assume it was intended as such and stop parsing.
           * Otherwise, there is a problem. */
          if (isspace(group_separator))
            next_state = DONE_ST;
          else
            next_state = NO_NUM_ST;
        }
        else if (*in == ')' && need_paren)
        {
          next_state = DONE_ST;
          need_paren = FALSE;
        }
        else
        {
          next_state = DONE_ST;
        }

        break;

      default:
        PERR("bad state");
        g_assert_not_reached();
        break;
    }

    /* If we're moving out of the IN_GROUP_ST, record data for the group */
    if ((state == IN_GROUP_ST) && (next_state != IN_GROUP_ST))
    {
      group_data = g_list_prepend(group_data, GINT_TO_POINTER(group_count));
      group_count = 0;
    }

    /* If we're moving into the FRAC_ST or out of the machine
     * without going through FRAC_ST, record the integral value. */
    if (((next_state == FRAC_ST) && (state != FRAC_ST)) ||
        ((next_state == DONE_ST) && !got_decimal))
    {
      *out = '\0';

      if (*out_str != '\0' && sscanf(out_str, GNC_SCANF_LLD, &numer) < 1)
      {
        next_state = NO_NUM_ST;
      }
      else if (next_state == FRAC_ST)
      {
        /* reset the out pointer to record the fraction */
        out = out_str;
        *out = '\0';

        got_decimal = TRUE;
      }
    }

    state = next_state;
    if (done_state (state))
      break;

    in++;
  }

  /* If there was an error, just quit */
  if (need_paren || (state == NO_NUM_ST))
  {
    g_free(out_str);
    g_list_free(group_data);
    return FALSE;
  }

  /* If there were groups, validate them */
  if (group_data != NULL)
  {
    gboolean good_grouping = TRUE;
    GList *node;
    char *group;

    group = monetary ? lc->mon_grouping : lc->grouping;

    /* The groups were built in reverse order. This
     * is the easiest order to verify them in. */
    for (node = group_data; node; node = node->next)
    {
      /* Verify group size */
      if (*group != GPOINTER_TO_INT(node->data))
      {
        good_grouping = FALSE;
        break;
      }

      /* Peek ahead at the next group code */
      switch (group[1])
      {
        /* A null char means repeat the last group indefinitely */
        case '\0':
          break;
        /* CHAR_MAX means no more grouping allowed */
        case CHAR_MAX:
          if (node->next != NULL)
            good_grouping = FALSE;
          break;
        /* Anything else means another group size */
        default:
          group++;
          break;
      }

      if (!good_grouping)
        break;
    }

    g_list_free(group_data);

    if (!good_grouping)
    {
      g_free(out_str);
      return FALSE;
    }
  }

  /* Cap the end of the fraction string, if any */
  *out = '\0';

  /* Add in fractional value */
  if (got_decimal && (*out_str != '\0'))
  {
    size_t len;
    long long int fraction;

    len = strlen(out_str);

    if (len > 8)
    {
      out_str[8] = '\0';
      len = 8;
    }

    if (sscanf (out_str, GNC_SCANF_LLD, &fraction) < 1)
    {
      g_free(out_str);
      return FALSE;
    }

    denom = multiplier(len);
    numer *= denom;
    numer += fraction;
  }
  else if (auto_decimal_enabled && !got_decimal)
  {
    if ((auto_decimal_places > 0) && (auto_decimal_places < 9))
    {
      denom = multiplier(auto_decimal_places);

      /* No need to multiply numer by denom at this point,
       * since by specifying the auto decimal places the
       * user has effectively determined the scaling factor
       * for the numerator they entered.
       */
    }
  }

  if (result != NULL)
  {
    *result = gnc_numeric_create (numer, denom);
    if (is_negative)
      *result = gnc_numeric_neg (*result);
  }

  if (endstr != NULL)
    *endstr = (char *) in;

  g_free (out_str);

  return TRUE;
}

/* enable/disable the auto_decimal_enabled option */
void
gnc_set_auto_decimal_enabled(gboolean enabled)
{
  auto_decimal_enabled = enabled;
}

/* set the number of auto decimal places to use */
void
gnc_set_auto_decimal_places( int places )
{
  auto_decimal_places = places;
}

/* These implementations are rather lame. */
#ifndef HAVE_TOWUPPER
gint32
towupper (gint32 wc)
{
  if (wc > 127)
    return wc;

  return toupper ((int) wc);
}

int
iswlower (gint32 wc)
{
  if (wc > 127)
    return 1;

  return islower ((int) wc);
}
#endif
