<?php
/**
 * Money Formatter
 *
 * Formats an amount of money in various ways, according to the provided currency.
 *
 * @package   easy-digital-downloads
 * @copyright Copyright (c) 2021, Sandhills Development, LLC
 * @license   GPL2+
 * @since     3.0
 */

namespace EDD\Currency;

/**
 * Class Money_Formatter
 *
 * @since 3.0
 */
class Money_Formatter {

	/**
	 * @var int|float Current working amount.
	 */
	public $amount;

	/**
	 * @var float Typed amount.
	 */
	public $typed_amount;

	/**
	 * @var float Data amount.
	 */
	public $data_amount;

	/**
	 * @var int|float Original, unmodified amount passed in via constructor.
	 */
	private $original_amount;

	/**
	 * @var Currency
	 */
	private $currency;

	/**
	 * Money_Formatter constructor.
	 *
	 * @param string   $amount   The amount to format.
	 * @param Currency $currency The currency to use for formatting.
	 */
	public function __construct( $amount, Currency $currency ) {

		if ( empty( $amount ) ) {
			$amount = 0;
		}

		$this->original_amount = $amount;
		$this->amount          = $amount;
		$this->typed_amount    = $amount;
		$this->currency        = $currency;
		$this->data_amount     = $amount;
	}

	/**
	 * Un-formats an amount.
	 * This ensures the amount is put into a state where we can perform mathematical
	 * operations on it --- that means using `.` as the decimal separator and no
	 * thousands separator.
	 *
	 * @return float|int
	 */
	private function unformat() {
		$amount = $this->original_amount;

		$sep_found = strpos( $amount, $this->currency->decimal_separator );
		if ( ',' === $this->currency->decimal_separator && false !== $sep_found ) {
			$whole  = substr( $amount, 0, $sep_found );
			$part   = substr( $amount, $sep_found + 1, ( strlen( $amount ) - 1 ) );
			$amount = $whole . '.' . $part;
		}

		// Strip "," and " " from the amount (if set as the thousands separator).
		foreach ( array( ',', ' ' ) as $thousands_separator ) {
			if ( $thousands_separator === $this->currency->thousands_separator && false !== strpos( $amount, $this->currency->thousands_separator ) ) {
				$amount = str_replace( $thousands_separator, '', $amount );
			}
		}

		return $amount;
	}

	/**
	 * Returns the number of decimals ot use for the formatted amount.
	 *
	 * Based on the currency code used when instantiating the class, determines how many
	 * decimal points the value should have once formatted.
	 *
	 * @param bool  $decimals If we should include decimals or not in the formatted amount.
	 * @param float $amount   The amount to format.
	 *
	 * @return int  The number of decimals places to use when formatting the amount.
	 */
	private function get_decimals( $decimals, $amount ) {
		/**
		 * Filter number of decimals to use for formatted amount
		 *
		 * @since unknown
		 *
		 * @param int        $number        Default 2. Number of decimals.
		 * @param int|string $amount        Amount being formatted.
		 * @param string     $currency_code Currency code being formatted.
		 */
		return apply_filters( 'edd_format_amount_decimals', $decimals ? $this->currency->number_decimals : 0, $amount, $this->currency->code );
	}

	/**
	 * Formats the amount for display.
	 * Does not apply the currency code.
	 *
	 * @since 3.0
	 *
	 * @param bool $decimals If we should include decimal places or not when formatting.
	 *
	 * @return Money_Formatter
	 */
	public function format_for_display( $decimals = true ) {
		$amount = $this->unformat();

		if ( empty( $amount ) ) {
			$amount = 0;
		}

		$decimals = $this->get_decimals( $decimals, $amount );

		// Format amount using decimals and separators (also rounds up or down).
		$formatted = number_format( (float) $amount, $decimals, $this->currency->decimal_separator, $this->currency->thousands_separator );

		/**
		 * Filter the formatted amount before returning
		 *
		 * @since unknown
		 *
		 * @param mixed  $formatted           Formatted amount.
		 * @param mixed  $amount              Original amount.
		 * @param int    $decimals            Default 2. Number of decimals.
		 * @param string $decimal_separator   Default '.'. Decimal separator.
		 * @param string $thousands_separator Default ','. Thousands separator.
		 * @param string $currency_code       Currency used for formatting.
		 */
		$this->amount = apply_filters( 'edd_format_amount', $formatted, $amount, $decimals, $this->currency->decimal_separator, $this->currency->thousands_separator, $this->currency->code );

		return $this;
	}

	/**
	 * Formats the amount for typed data returns.
	 * Does not apply the currency code and returns a foat instead of a string.
	 *
	 * @since 3.0
	 *
	 * @param bool $decimals If we should include decimal places or not when formatting.
	 *
	 * @return Money_Formatter
	 */
	public function format_for_typed( $decimals = true ) {
		$amount = $this->unformat();

		if ( empty( $amount ) ) {
			$amount = 0;
		}

		$decimals = $this->get_decimals( $decimals, $amount );

		/**
		 * Since we want to return a float value here, intentionally only supply a decimal separator.
		 *
		 * The separators here are hard coded intentionally as we're looking to get truncated, raw format of float
		 * which requires '.' for decimal separators and no thousands separator.
		 *
		 * This is also intentionally not filtered for the time being.
		 */
		$formatted = floatval( number_format( (float) $amount, $decimals, '.', '' ) );

		// Set the amount to $this->amount.
		$this->typed_amount = $formatted;

		return $this;
	}

	/**
	 * Formats the amount for data storage.
	 * Does not apply the currency code.
	 * This does set the amount to the correct number of decimal places.
	 *
	 * @since 3.3.6
	 * @param bool $decimals If we should include decimal places or not when formatting.
	 * @return Money_Formatter
	 */
	public function format_for_data( $decimals = true ) {
		$amount = $this->unformat();

		if ( empty( $amount ) ) {
			$amount = 0;
		}

		$decimals = $this->get_decimals( $decimals, $amount );

		$this->data_amount = number_format( round( floatval( (float) $amount ), $decimals ), $decimals, '.', '' );

		return $this;
	}

	/**
	 * Applies the currency prefix/suffix to the amount.
	 *
	 * @since 3.0
	 * @return string
	 */
	public function apply_symbol() {
		$amount      = $this->amount;
		$is_negative = is_numeric( $this->amount ) && $this->amount < 0;

		// Remove "-" from start.
		if ( $is_negative ) {
			$amount = substr( $amount, 1 );
		}

		$formatted = '';
		if ( ! empty( $this->currency->prefix ) ) {
			$formatted .= $this->currency->prefix;
		}

		$formatted .= $amount;

		if ( ! empty( $this->currency->suffix ) ) {
			$formatted .= $this->currency->suffix;
		}

		if ( ! empty( $this->currency->prefix ) ) {
			/**
			 * Filters the output with a prefix.
			 *
			 * @param string $formatted
			 * @param string $currency_code
			 * @param string $amount
			 */
			$formatted = apply_filters( 'edd_' . strtolower( $this->currency->code ) . '_currency_filter_before', $formatted, $this->currency->code, $amount );
		}

		if ( ! empty( $this->currency->suffix ) ) {
			/**
			 * Filters the output with a suffix.
			 *
			 * @param string $formatted
			 * @param string $currency_code
			 * @param string $amount
			 */
			$formatted = apply_filters( 'edd_' . strtolower( $this->currency->code ) . '_currency_filter_after', $formatted, $this->currency->code, $amount );
		}

		// Add the "-" sign back to the start of the string.
		if ( $is_negative ) {
			$formatted = '-' . $formatted;
		}

		return $formatted;
	}
}
