<?php
/**
 * @brief		Money Object
 * @author		<a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
 * @copyright	(c) Invision Power Services, Inc.
 * @license		https://www.invisioncommunity.com/legal/standards/
 * @package		Invision Community
 * @subpackage	Nexus
 * @since		11 Feb 2014
 */

namespace IPS\nexus;

/* To prevent PHP errors (extending class does not exist) revealing path */

use IPS\Lang;
use IPS\Math\Number;
use IPS\Member;
use IPS\Settings;
use NumberFormatter;
use function defined;
use function floatval;
use function function_exists;
use function intval;
use function is_string;
use function strlen;

if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
	header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
	exit;
}

/**
 * Money Object
 */
class Money
{	
	/**
	 * Get available currencies
	 *
	 * @return	array
	 */
	public static function currencies() : array
	{
		if ( $currencies = json_decode( Settings::i()->nexus_currency, TRUE ) )
		{
			return array_keys( $currencies );
		}
		else
		{
			return array( Settings::i()->nexus_currency );
		}
	}
	
	/**
	 * Get the number of decimal points used for a currency
	 *
	 * @param	string	$currency	Currency code
	 * @return	int
	 */
	public static function numberOfDecimalsForCurrency( string $currency ) : int
	{
		switch ( $currency )
		{
			case 'CLF':
				return 4;
				
			case 'BHD':
			case 'IQD':
			case 'JOD':
			case 'KWD':
			case 'LYD':
			case 'OMR':
			case 'TND':
				return 3;
				
			case 'MGA':
			case 'MRO':
				return 1;
				
			case 'BIF':
			case 'BYR':
			case 'CLP':
			case 'CVE':
			case 'DJF':
			case 'GNF':
			case 'ISK':
			case 'JPY':
			case 'KMF':
			case 'KRW':
			case 'PYG':
			case 'RWF':
			case 'UGX':
			case 'UYI':
			case 'VND':
			case 'VUV':
			case 'XAF':
			case 'XOF':
			case 'XPF':
				return 0;
			
			default:
				return 2;
		}
	}
		
	/**
	 * @brief	Amount
	 */
	public ?Number $amount = null;
	
	/**
	 * @brief	Currency
	 */
	public string $currency = "";
	
	/**
	 * @brief	Number of decimal points
	 */
	protected int $numberOfDecimalPlaces = 2;
	
	/**
	 * Contructor
	 *
	 * @param	mixed	$amount		Amount
	 * @param	string	$currency	Currency code
	 * @return	void
	 */
	public function __construct( mixed $amount, string $currency )
	{
		$this->currency = $currency;
		$this->numberOfDecimalPlaces = static::numberOfDecimalsForCurrency( $currency );
		
		if ( !( $amount instanceof Number ) )
		{
			if ( is_string( $amount ) )
			{
				$amount = floatval( $amount );
			}
			$amount = new Number( number_format( $amount, $this->numberOfDecimalPlaces, '.', '' ) );
		}
		
		$this->amount = $amount->round( $this->numberOfDecimalPlaces );
	}
		
	/**
	 * Amount as string (not formatted, not locale aware)
	 * Used for gateways where, for example, (float) 10.5 is not acceptable, and (string) "10.50" is required
	 *
	 * @return    string
	 */
	public function amountAsString() : string
	{
		return sprintf( "%01." . $this->numberOfDecimalPlaces . "F", (string) $this->amount );
	}
	
	/**
	 * To string
	 *
	 * @return	string
	 */
	public function __toString() : string
	{
		return $this->toString( Member::loggedIn()->language() );
	}
	
	/**
	 * To string for language
	 *
	 * @param	Lang	$language	The language to use
	 * @return	string
	 */
	public function toString( Lang $language ) : string
	{
		/* If intl is installed, use NumberFormatter */
		if( class_exists( "NumberFormatter" ) and trim( $language->locale['int_curr_symbol'] ) === $this->currency )
		{
			$formatter = new NumberFormatter( $language->short, NumberFormatter::CURRENCY );
			
			if ( floatval( (string) $this->amount ) == intval( (string) $this->amount ) )
			{
				$formatter->setAttribute( NumberFormatter::FRACTION_DIGITS, 0 );
			}

			$result = $formatter->formatCurrency( (string) $this->amount, $this->currency );

			/* @see https://bugs.php.net/bug.php?id=54538 */
			if( $result !== 'NaN' )
			{
				return $result;
			}
		}

		/* If this currency matches the locale the user is using, and we are not using PHP 7.4, AND money_format is supported (Windows doesn't support it), use that */
		if ( function_exists( 'money_format' ) and version_compare( PHP_VERSION, '7.4.0', '<' ) and trim( $language->locale['int_curr_symbol'] ) === $this->currency )
		{
			/* We need to make sure the locale is correct */
			$currentLocale = setlocale( LC_ALL, '0' );
			$language->setLocale();
			
			/* Get the value to use */
			$return = money_format( '%n', (string) $this->amount );
			
			/* Then put the locale back */
			Lang::restoreLocale( $currentLocale );
			
			/* And return */
			return $return;
		}
				
		/* If it matches any of the installed languages, we can do something with the locale data */
		foreach ( Lang::languages() as $lang )
		{
			if ( isset( $lang->locale['int_curr_symbol'] ) AND trim( $lang->locale['int_curr_symbol'] ) === $this->currency )
			{
				$currencySymbol = $lang->convertString( $lang->locale['currency_symbol'] );
				$currencySymbolPreceeds = $this->amount->isPositive() ? $lang->locale['p_cs_precedes'] : $lang->locale['n_cs_precedes'];
				$spaceBetweenCurrencySymbolAndAmount = ( $this->amount->isPositive() ? $lang->locale['p_sep_by_space'] : $lang->locale['n_sep_by_space'] ) ? ' ' : '';
				$amount = number_format( (string) $this->amount, $lang->locale['frac_digits'], $lang->locale['mon_decimal_point'], $lang->locale['mon_thousands_sep'] );
				
				$positiveNegativeSymbol = $this->amount->isPositive() ? $lang->locale['positive_sign'] : $lang->locale['negative_sign'];
				if ( $positiveNegativeSymbol )
				{
					$positiveNegativeSymbolFormat = $this->amount->isPositive() ? $lang->locale['p_sign_posn'] : $lang->locale['n_sign_posn'];
					$positiveNegativeSymbolFormatLength = strlen( $positiveNegativeSymbolFormat );
					for ( $i=0; $i < $positiveNegativeSymbolFormatLength; $i++ )
					{
						switch ( $positiveNegativeSymbolFormat[ $i ] )
						{
							case 0:
								if ( $currencySymbolPreceeds )
								{
									$currencySymbol = "({$currencySymbol}";
									$amount = "{$amount})";
								}
								else
								{
									$currencySymbol = "{$currencySymbol})";
									$amount = "({$amount}";
								}
								break;
							case 1:
								if ( $currencySymbolPreceeds )
								{
									$currencySymbol = "{$positiveNegativeSymbol}{$currencySymbol}";
								}
								else
								{
									$amount = "{$positiveNegativeSymbol}{$amount}";
								}
								break;
							case 2:
								if ( $currencySymbolPreceeds )
								{
									$amount = "{$amount}{$positiveNegativeSymbol}";
								}
								else
								{
									$currencySymbol = "{$currencySymbol}{$positiveNegativeSymbol}";
								}
								break;
							case 3:
								$currencySymbol = ( $positiveNegativeSymbol . $currencySymbol );
								break;
							case 4:
								$currencySymbol = ( $currencySymbol . $positiveNegativeSymbol );
								break;
						}
					}
				}
				
				return ( $currencySymbolPreceeds ? ( $currencySymbol . $spaceBetweenCurrencySymbolAndAmount . $amount ) : ( $amount . $spaceBetweenCurrencySymbolAndAmount . $currencySymbol ) );
			}
		}

		/* And if all else fails, just use the currency code */
		return number_format( (string) $this->amount, static::numberOfDecimalsForCurrency( $this->currency ) ) . " {$this->currency}";
	}
	
	/**
	 * Get output for API
	 *
	 * @param	Member|NULL	$authorizedMember	The member making the API request or NULL for API Key / client_credentials
	 * @return	array
	 * @apiresponse	string	currency	The currency code (e.g. 'USD')
	 * @apiresponse	string	amount		The amount
	 */
	public function apiOutput( ?Member $authorizedMember = NULL ): array
	{
		return array(
			'currency'	=> $this->currency,
			'amount'	=> $this->amountAsString(),
		);
	}
}