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

package uk.co.gardennotebook.util;

import java.math.BigDecimal;
import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.Currency;
import java.util.Locale;
import java.util.Objects;
//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	1.1
 *	@since	1.0
 */
final public class SimpleMoney_class {
	private static final Logger LOGGER = LogManager.getLogger();
    
	private final Currency currency;
	private final BigDecimal	value;
	
	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_class()
	{
		this((Currency)null, null);
	}

	/**
	 *	Creates a value with the given amount in the local currency.
	 * 
	 * @param amount	the amount of money represented 
	 */	
	public SimpleMoney_class(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_class(String currencyCode, BigDecimal amount)
	{
		Currency holder;
		if (currencyCode == null)
		{
//			this.currency = Currency.getInstance(Locale.getDefault());
			holder = Currency.getInstance(Locale.getDefault());
		}
		else
		{
			try
			{
//				this.currency = Currency.getInstance(currencyCode);
				holder = Currency.getInstance(currencyCode);
			}
			catch (IllegalArgumentException e)
			{
//System.out.println("notebook.spi.SimpleMoney.<init>(): IllegalArgumentException: "+e);
LOGGER.debug("Illegal currency string: {}, throw: {}.  Use current locale.", currencyCode, e.getMessage());
				holder = Currency.getInstance(Locale.getDefault());
			}
		}
		this.currency = holder;
		BigDecimal temp;
		if (amount == null)
		{
			temp = BigDecimal.ZERO;
		}
		else
		{
			temp = amount;
		}
		this.value = temp.setScale(this.currency.getDefaultFractionDigits());
	}
	
	/**
	 * 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_class(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;
		}
		this.value = temp.setScale(this.currency.getDefaultFractionDigits());
	}
	
	/**
	 * Returns the currency being used.
	 * 
	 * @return the currency for this value
	 */
	public Currency currency()
	{
		return currency;
	}
	
	/**
	 * Returns the amount of money represented.
	 * 
	 * @return the amount of money represented
	 */
	public BigDecimal amount()
	{
		return value;
	}
	
	@Override
	public String toString()
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("toString()");
		NumberFormat fmt = NumberFormat.getCurrencyInstance();
		if (this.currency == Currency.getInstance(Locale.getDefault()))
		{
//System.out.println("notebook.spi.SimpleMoney.toString(): using default currency");
			LOGGER.debug("using default currency");
			return fmt.format(this.value.doubleValue());
		}
		fmt.setCurrency(this.currency);
		return LOGGER.traceExit(fmt.format(this.value.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_class parse(String text)
	{
//System.out.println("notebook.spi.SimpleMoney.parse(): text: "+text);
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("parse({})", text);
		if (text == null || text.isEmpty())
		{
			LOGGER.debug("empty or null text");
			return LOGGER.traceExit(new SimpleMoney_class() );
		}
		String newText = text.trim();
		if (newText.isEmpty())
		{
			LOGGER.debug("empty or null text");
			return LOGGER.traceExit(new SimpleMoney_class() );
		}
		//remove any whitespace
		newText = newText.replaceAll("\\s", "");
//System.out.println("notebook.spi.SimpleMoney.parse(): trimmed text: "+newText);
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) {
//System.out.println("notebook.spi.SimpleMoney.parse(): 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_class(BigDecimal.valueOf(tryParse.doubleValue())) );
		}

		Matcher mat = CODERECOG.matcher(newText);
		String currCode;
		Currency curr = Currency.getInstance(Locale.getDefault());	// need the symbol later
		if (mat.lookingAt())
		{
//System.out.println("notebook.spi.SimpleMoney.parse(): got a currency code");
LOGGER.debug("got a currency code");
			currCode = mat.group("currCode");
//System.out.println("notebook.spi.SimpleMoney.parse(): currCode: "+currCode);
LOGGER.debug("got a currency code: {}", currCode);
			curr = Currency.getInstance(currCode);
			fmt.setCurrency(curr);
			String amendedText = newText.replace(currCode, curr.getSymbol());
			try {
				tryParse = fmt.parse(amendedText);
			} catch (ParseException ex) {
//System.out.println("notebook.spi.SimpleMoney.parse(): ParseException: "+ex);
//				Logger.getLogger(SimpleMoney.class.getName()).log(Level.SEVERE, null, ex);
LOGGER.debug("parse(): parse exception in standard converter with currency code.  Threw:", ex.getMessage());
				return LOGGER.traceExit(new SimpleMoney_class() );
			}
//System.out.println("notebook.spi.SimpleMoney.parse(): value: "+tryParse);
LOGGER.debug("value: {}", tryParse);
			return LOGGER.traceExit(new SimpleMoney_class(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())
		{
//System.out.println("notebook.spi.SimpleMoney.parse(): got a symbol");
LOGGER.debug("got a currency symbol");
			currCode = mat.group("dispCode");
//System.out.println("notebook.spi.SimpleMoney.parse(): dispCode: "+currCode);
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)
			{
//System.out.println("notebook.spi.SimpleMoney.parse(): could not find a code for: "+currCode);
LOGGER.debug("could not find a code for currency symbol: {}", currCode);
				return LOGGER.traceExit(new SimpleMoney_class() );
			}
//System.out.println("notebook.spi.SimpleMoney.parse(): got currency (dispCode): "+curr);
LOGGER.debug("got currency: {}", curr);
			fmt.setCurrency(curr);
			try {
				tryParse = fmt.parse(newText);
			} catch (ParseException ex) {
//System.out.println("notebook.spi.SimpleMoney.parse(): ParseException: "+ex);
//				Logger.getLogger(SimpleMoney.class.getName()).log(Level.SEVERE, null, ex);
LOGGER.debug("parse(): parse exception in standard converter with currency code.  Threw:", ex.getMessage());
				return LOGGER.traceExit(new SimpleMoney_class() );
			}
//System.out.println("notebook.spi.SimpleMoney.parse(): value: "+tryParse);
			return LOGGER.traceExit(new SimpleMoney_class(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) {
//System.out.println("notebook.spi.SimpleMoney.parse(): ParseException: "+ex);
//			Logger.getLogger(SimpleMoney.class.getName()).log(Level.SEVERE, null, ex);
LOGGER.debug("parse(): parse exception in standard converter, plain number.  Threw:", ex.getMessage());
			return LOGGER.traceExit(new SimpleMoney_class() );
		}
//System.out.println("notebook.spi.SimpleMoney.parse(): value: "+tryParse);
LOGGER.debug("value: {}", tryParse);
		return LOGGER.traceExit(new SimpleMoney_class(curr, BigDecimal.valueOf(tryParse.doubleValue())) );
	}
	
	@Override
	public boolean equals(Object o)
	{
		if (o == null) return false;
		if (this == o) return true;
		if (!(o instanceof SimpleMoney_class)) return false;
		SimpleMoney_class test = (SimpleMoney_class)o;
		return test.currency.equals(this.currency) && test.value.equals(this.value);
	}

	@Override
	public int hashCode() {
		int hash = 7;
		hash = 29 * hash + Objects.hashCode(this.currency);
		hash = 29 * hash + Objects.hashCode(this.value);
		return hash;
	}

}
