/*
 * Copyright (C) 2018 Andrew Gegg
 *
 *	This file is part of the Garden Notebook application
 *
 * The Garden Notebook application 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 3 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, see <http://www.gnu.org/licenses/>.
 */

/*
	Change log
	2.9.1   Convert to record
            Fix bug handling non-existant or pseudo-currencies given as a string.
            Allow lower case currency codes.
 */

package uk.co.gardennotebook.util;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.Currency;
import java.util.Locale;
//import java.util.logging.Level;
//import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.message.EntryMessage;

/**
 *	A simple representation of Money values.
 * 
 * Values are immutable but are NOT cached so are NOT unique.
 * 
 * Has methods to create, display and parse normal monetary values
 *	
 *	@author	Andy Gegg
 *	@version	2.9.1
 *	@since	1.0
 */
public record SimpleMoney(Currency currency, BigDecimal amount)
    {
	private static final Logger LOGGER = LogManager.getLogger();
    
	final static private Pattern CODERECOG = Pattern.compile("(?<currCode>[A-Z]{3})");
	final static DecimalFormatSymbols DECS = new DecimalFormatSymbols();
	final static char DECSEP = DECS.getMonetaryDecimalSeparator();
	final static private Pattern SYMBOLRECOG = Pattern.compile("(?<dispCode>[^0-9"+DECSEP+"\\s]+)");

	/**
	 *	Creates a value of zero with the local currency.
	 */
	public SimpleMoney()
	{
		this((Currency)null, null);
	}

	/**
	 *	Creates a value with the given amount in the local currency.
	 * 
	 * @param amount	the amount of money represented 
	 */	
	public SimpleMoney(BigDecimal amount)
	{
		this((Currency)null, amount);
	}

	/**
	 * Creates a value of the amount given in the specified currency.
	 * 
	 * @param currencyCode	the currency to be used
	 * @param amount	the amount of money represented
	 */
	public SimpleMoney(String currencyCode, BigDecimal amount)
	{
		this(codeConverter(currencyCode), amount);
	}
    
    private static Currency codeConverter(String currencyCode)
    {
		Currency holder;
		if (currencyCode == null)
		{
			holder = Currency.getInstance(Locale.getDefault());
		}
		else
		{
			try
			{
				holder = Currency.getInstance(currencyCode.toUpperCase());
			}
			catch (IllegalArgumentException e)
			{
                LOGGER.debug("Illegal currency string: {}, throw: {}.  Use current locale.", currencyCode, e.getMessage());
				holder = Currency.getInstance(Locale.getDefault());
			}
		}
		return holder;
        
    }

	/**
	 * Creates a value of the amount given in the specified currency.
	 *
	 * @param currency	the currency to be used
	 * @param amount	the amount of money represented
	 */
	public SimpleMoney(Currency currency, BigDecimal amount)
	{
		if (currency == null)
		{
			this.currency = Currency.getInstance(Locale.getDefault());
		}
		else
		{
			this.currency = currency;
		}
		BigDecimal temp;
		if (amount == null)
		{
			temp = BigDecimal.ZERO;
		}
		else
		{
			temp = amount;
		}
        // 2.9.1
        // Some care is needed here.  Some currencies have no fractional part (e.g. the Yen)
        // some currencies are pseudo-currencies (such as IMF drawing rights) which return -1 for their fractional part
        // it doesn't make much sense to use those but they are legal currencies!
        // If there is no fractional part and the user enters 0.12, say, a rounding is required so we must specify
        // a rounding to apply.  As a general problem this is probably insoluble but for our purposes it doesn't
        // really matter - this is not a book-keeping app!
        int fraction = this.currency.getDefaultFractionDigits();
        if (fraction < 0)
        {
            LOGGER.info("using a pseudo-currency: {}", this.currency);
            fraction = 0;
        }
		this.amount = temp.setScale(fraction, RoundingMode.UP);
	}
	
	@Override
	public String toString()
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("toString()");
		NumberFormat fmt = NumberFormat.getCurrencyInstance();
		if (this.currency == Currency.getInstance(Locale.getDefault()))
		{
			LOGGER.debug("using default currency");
			return fmt.format(this.amount.doubleValue());
		}
		fmt.setCurrency(this.currency);
		return LOGGER.traceExit(fmt.format(this.amount.doubleValue()));
	}
	
	/**
	 * Create a SimpleMoney instance from the given string.  Only positive (unsigned)
	 * values will be matched.
	 * 
	 * After stripping out any white space, the string is first matched against
	 * the Locale standard format for a monetary value.  If no match is found
	 * a check is made for a 3 letter currency code; if found, its symbol will be
	 * substituted and the resulting string will be matched against the standard
	 * format for that currency.  If a currency code was not found, a check will
	 * be made for a recognised currency symbol and then the string  will be
	 * matched against the standard format for that currency.  If none of these
	 * matches succeed a value of zero will be returned in the default Locale currency.
	 * <BR>
	 * Examples:<UL>
	 *<LI> £12.34
	 *<LI> £ 12.34
	 *<LI> £1,234,567.89
	 *<LI> £1234567.89
	 *<LI> GBP 12.34
	 *<LI> EUR 12.34
	 *</UL> 
	 * NB the currency symbol $ in a non-dollar Locale (e.g UK) will NOT be recognised
	 * as it's ambiguous.  Use USD, CND, etc.
	 * 
	 * @param text	the text to be parsed.  Null or empty Strings will return zero.
	 * @return a representation of the monetary value indicated by text
	 */
	public static SimpleMoney parse(String text)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("parse({})", text);

        if (text == null || text.isEmpty())
		{
			LOGGER.debug("empty or null text");
			return LOGGER.traceExit( new SimpleMoney() );
		}
		String newText = text.trim();
		if (newText.isEmpty())
		{
			LOGGER.debug("empty or null text");
			return LOGGER.traceExit( new SimpleMoney() );
		}
        
		//remove any whitespace
		newText = newText.replaceAll("\\s", "").toUpperCase();
        LOGGER.debug("trimmed text: {}", newText);
		//usually, the standard converter will work, so try it first
		NumberFormat fmt = NumberFormat.getCurrencyInstance();
		Number tryParse = 0;
		boolean gotIt = true;
		try {
			tryParse = fmt.parse(newText);
		} catch (ParseException ex) {
            LOGGER.debug("parse(): parse exception in standard converter looking for currency string, string not found.  Threw: {}", ex.getMessage());
			gotIt = false;
		}
		if (gotIt)
		{
			return LOGGER.traceExit( new SimpleMoney(BigDecimal.valueOf(tryParse.doubleValue())) );
		}

		Matcher mat = CODERECOG.matcher(newText);
		String currCode;
		Currency curr = Currency.getInstance(Locale.getDefault());	// need the symbol later
		if (mat.lookingAt())
		{
            LOGGER.debug("got a currency code");
			currCode = mat.group("currCode");
            LOGGER.debug("got a currency code: {}", currCode);
            LOGGER.debug("before look up (default): curr: {}, symbol: {}", curr, curr.getSymbol());
            Currency curr2;
            try {
                curr2 = Currency.getInstance(currCode);
            } catch (IllegalArgumentException ex)
            {
                LOGGER.debug("invalid currency code, use default");
                curr2 = null;
            }
            if (curr2 != null) curr = curr2;
            LOGGER.debug("curr: {}, symbol: {}", curr, curr.getSymbol());
			fmt.setCurrency(curr);
			String amendedText = newText.replace(currCode, curr.getSymbol());
            LOGGER.debug("amendedText: {}", amendedText);
			try {
				tryParse = fmt.parse(amendedText);
			} catch (ParseException ex) {
                LOGGER.debug("parse(): parse exception in standard converter with currency code.  Threw: {}", ex.getMessage());
				return LOGGER.traceExit( new SimpleMoney() );
			}
            LOGGER.debug("value: {}", tryParse);
			return LOGGER.traceExit( new SimpleMoney(curr, BigDecimal.valueOf(tryParse.doubleValue())) );
		}

		// not a currency code, try a symbol (must do this AFTER currency code as this will ALWAYS match)
		mat = SYMBOLRECOG.matcher(newText);
		if (mat.lookingAt())
		{
            LOGGER.debug("got a currency symbol");
			currCode = mat.group("dispCode");
            LOGGER.debug("got a currency symbol: {}", currCode);
			gotIt = false;
			for (Currency x : Currency.getAvailableCurrencies())
			{
				if (x.getSymbol().equalsIgnoreCase(currCode))
				{
					curr = x;
					gotIt = true;
					break;
				}
			}
			if (!gotIt)
			{
                LOGGER.debug("could not find a code for currency symbol: {}", currCode);
				return LOGGER.traceExit( new SimpleMoney() );
			}
            LOGGER.debug("got currency: {}", curr);
			fmt.setCurrency(curr);
			try {
				tryParse = fmt.parse(newText);
			} catch (ParseException ex) {
                LOGGER.debug("parse(): parse exception in standard converter with currency code.  Threw: {}", ex.getMessage());
				return LOGGER.traceExit( new SimpleMoney() );
			}
			return LOGGER.traceExit( new SimpleMoney(curr, BigDecimal.valueOf(tryParse.doubleValue())) );
		}
		// if here no symbols, just a straight number
		String amendedText = curr.getSymbol()+newText;
		try {
			tryParse = fmt.parse(amendedText);
		} catch (ParseException ex) {
            LOGGER.debug("parse(): parse exception in standard converter, plain number.  Threw: {}", ex.getMessage());
			return LOGGER.traceExit(new SimpleMoney() );
		}
        LOGGER.debug("value: {}", tryParse);
		return LOGGER.traceExit( new SimpleMoney(curr, BigDecimal.valueOf(tryParse.doubleValue())) );
	}
	
}
