<?php
/**
 * Plugin Name: Easy Digital Downloads - Advanced Sequential Order Numbers
 * Plugin URI: https://easydigitaldownloads.com/downloads/advanced-sequential-order-numbers/
 * Description: Advanced sequential order numbers for Easy Digital Downloads.
 * Version: 1.0.11
 * Author: Easy Digital Downloads
 * Author URI: https://easydigitaldownloads.com
 */

// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

if ( ! class_exists( 'EDD_Son' ) ) {

	class EDD_Son {

		private static $_instance;

		/**
		 * Get active instance
		 *
		 * @access      public
		 * @since       1.0.0
		 * @return      object self::$instance The one true EDD_Son
		 */
		public static function instance() {
			if ( self::$_instance == null ) {
				self::$_instance = new EDD_Son();
				self::$_instance->setup_constants();
				self::$_instance->includes();
				self::$_instance->load_textdomain();
				self::$_instance->hooks();
			}

			return self::$_instance;
		}

		/**
		 * Setup plugin constants
		 *
		 * @access      private
		 * @since       1.0.0
		 * @return      void
		 */
		private function setup_constants() {
			define( 'EDD_SON_PLUGIN_FILE', __FILE__ );
			define( 'EDD_SON_PLUGIN_DIR', trailingslashit( plugin_dir_path( __FILE__ ) ) );
			define( 'EDD_SON_PLUGIN_URL', trailingslashit( plugin_dir_url( __FILE__ ) ) );
			define( 'EDD_SON_VERSION', '1.0.11' );
			define( 'EDD_SON_DEBUG', false );
		}

		/**
		 * Include necessary files
		 *
		 * @access      private
		 * @since       1.0.0
		 * @return      void
		 */
		private function includes() {
			require_once EDD_SON_PLUGIN_DIR . 'includes/class-edd-son-settings.php';
			require_once EDD_SON_PLUGIN_DIR . 'includes/class-edd-son-next-order-number.php';
			require_once EDD_SON_PLUGIN_DIR . 'includes/class-edd-son-prefix.php';
			require_once EDD_SON_PLUGIN_DIR . 'includes/class-edd-son-postfix.php';
			require_once EDD_SON_PLUGIN_DIR . 'includes/admin/class-edd-son-admin-search.php';
			if ( function_exists( 'edd_get_orders' ) ) {
				require_once EDD_SON_PLUGIN_DIR . 'includes/admin/class-edd-son-migration.php';
			}
		}

		/**
		 * Run action and filter hooks
		 *
		 * @access      private
		 * @since       1.0.0
		 * @return      void
		 */
		private function hooks() {
			// Register settings.
			add_filter( 'edd_settings_misc', array( 'EDD_Son_Settings', 'remove_edd_settings' ) );
			$settings_tab = version_compare( EDD_VERSION, '2.11.3', '>=' ) ? 'gateways' : 'extensions';
			add_filter( "edd_settings_sections_{$settings_tab}", array( 'EDD_Son_Settings', 'settings_section' ) );
			add_filter( "edd_settings_{$settings_tab}", array( 'EDD_Son_Settings', 'settings' ) );

			// Admin view.
			add_action( 'edd_view_order_details_payment_meta_after', array( $this, 'admin_view_temp_order_number' ) );

			// Inject order number functionality.
			add_action( 'edd_insert_payment', array( $this, 'assign_order_number' ), 10, 2 );
			add_action( 'edd_payment_saved', array( $this, 'order_saved' ), 10, 3 );
			add_action( 'edd_transition_order_status', array( $this, 'maybe_assign_order_number' ), 5, 3 );

			// Filters on EDD core behavior.
			add_filter( 'edd_payment_number', array( $this, 'get_payment_number' ), 10, 2 );
			add_filter( 'edd_get_option_enable_sequential', array( $this, 'maybe_disable_core_sequential_numbers' ) );

			// Handle licensing.
			if ( class_exists( '\\EDD\\Extensions\\ExtensionRegistry' ) ) {
				add_action( 'edd_extension_license_init', function( \EDD\Extensions\ExtensionRegistry $registry ) {
					$registry->addExtension( __FILE__, 'Advanced Sequential Order Numbers', 544176, EDD_SON_VERSION );
				} );
			} elseif ( class_exists( 'EDD_License' ) ) {
				$license = new EDD_License( __FILE__, 'Advanced Sequential Order Numbers', EDD_SON_VERSION, 'Easy Digital Downloads', null, null, 544176 );
			}
		}

		public function admin_view_temp_order_number( $payment_id ) {
			if ( ! $this->is_active() ) {
				return;
			}

			$temp_number = edd_get_payment_meta( $payment_id, '_edd_son_temp_payment_number', true );

			if ( empty( $temp_number ) ) {
				return;
			}

			?>
			<div class="edd-order-tx-id edd-admin-box-inside">
				<p>
					<span class="label"><?php _e( 'Temporary order number:', 'edd-son' ); ?></span>&nbsp;
					<span><?php echo $temp_number; ?></span>
				</p>
			</div>
			<?php
		}

		/**
		 * If this is active, do not allow EDD core to assign sequential numbers.
		 *
		 * @since 1.0.11
		 * @param bool $enabled
		 * @return bool
		 */
		public function maybe_disable_core_sequential_numbers( $enabled ) {
			return $this->is_active() ? false : $enabled;
		}

		/**
		 * Internationalization
		 *
		 * @access      public
		 * @since       1.0.0
		 * @return      void
		 */
		public function load_textdomain() {
			// Set filter for language directory.
			$lang_dir = EDD_SON_PLUGIN_DIR . '/languages/';
			$lang_dir = apply_filters( 'edd_' . 'edd-son' . '_languages_directory', $lang_dir );

			// Traditional WordPress plugin locale filter.
			$locale = apply_filters( 'plugin_locale', get_locale(), 'edd-son' );
			$mofile = sprintf( '%1$s-%2$s.mo', 'edd-son', $locale );

			// Setup paths to current locale file.
			$mofile_local  = $lang_dir . $mofile;
			$mofile_global = WP_LANG_DIR . '/' . 'edd-son' . '/' . $mofile;

			if ( file_exists( $mofile_global ) ) {
				// Look in global /wp-content/languages/edd-plugin-name/ folder.
				load_textdomain( 'edd-son', $mofile_global );

			} elseif ( file_exists( $mofile_local ) ) {
				// Look in local /wp-content/plugins/edd-plugin-name/languages/ folder.
				load_textdomain( 'edd-son', $mofile_local );

			} else {
				// Load the default language files.
				load_plugin_textdomain( 'edd-son', false, $lang_dir );
			}
		}

		/**
		 * Are the advanced order numbers enabled?
		 *
		 * @since 1.0.0
		 * @return bool
		 */
		private function is_active() {
			return edd_get_option( 'edd_son_active' );
		}

		/**
		 * Hooks into and filters the order number
		 *
		 * @param $number
		 * @param $payment_id
		 *
		 * @see   'edd_payment_number'
		 * @since 1.0.0
		 * @return string The order number
		 */
		public function get_payment_number( $number, $payment_id ) {
			if ( ! $this->is_active() ) {
				return $number;
			}

			$complete_number = $this->get_complete_order_number( $payment_id );
			if ( $complete_number ) {
				return $complete_number;
			}

			$pending_number = edd_get_payment_meta( $payment_id, '_edd_son_temp_payment_number', true );

			return $pending_number ?: $number;
		}

		/**
		 * Update order number of a completed order.
		 *
		 * @param int          $order_id   The order ID.
		 * @param \EDD_Payment $order      The EDD Payment object.
		 *
		 * @since 1.0.9
		 * @since 1.0.11 This method is only used in EDD 2.x.
		 * @todo Deprecate when extension minimum is EDD 3.0.
		 */
		public function order_saved( $order_id, $order ) {
			if ( ! function_exists( 'edd_get_order' ) ) {
				$this->maybe_assign_order_number( $order->old_status, $order->status, $order->ID );
			}
		}

		/**
		 * Maybe assigns the ASON order number in EDD 2.x and 3.0+.
		 *
		 * @since 1.0.11
		 * @param string $old_status The previous order status.
		 * @param string $new_status The new order status.
		 * @param int    $order_id   The order/payment ID.
		 * @return void
		 */
		public function maybe_assign_order_number( $old_status, $new_status, $order_id ) {
			if ( function_exists( 'edd_get_order' ) ) {
				// Do not assign numbers to refunds in 3.0 for now.
				$order = edd_get_order( $order_id );
				if ( ! $order || 'sale' !== $order->type ) {
					edd_debug_log( sprintf( 'ASON - maybe_assign_order_number - Invalid order type: %s', $order ? $order->type : 'n/a' ) );
					return;
				}
			}

			// Verify that there isn't an old status, and that it's not assignable.
			if ( ! empty( $old_status ) && $this->is_assignable_status( $old_status ) ) {
				edd_debug_log( sprintf( 'ASON - order_saved - Old Status was assignable: %s. No order number needed for order ID %s', $old_status, $order_id ) );
				return;
			}

			// Only run this update on orders that are completed.
			edd_debug_log( sprintf( 'ASON - order_saved status: %s (order ID %s)', $new_status, $order_id ) );
			if ( ! $this->is_assignable_status( $new_status ) ) {
				return;
			}

			// (Re)assign the order number. We reassign
			// because the previously assigned number
			// was simply a "pending" order number.
			$this->assign_order_number( $order_id );
		}

		/**
		 * Update order number of a completed order
		 *
		 * @param int    $payment_id
		 * @param string $new_status
		 * @param string $old_status
		 *
		 * @see   'edd_update_payment_status'
		 * @since 1.0.0
		 * @deprecated 1.0.9
		 */
		public function order_completed( $payment_id, $new_status, $old_status ) {
			_edd_deprecated_function( __FUNCTION__, '1.0.9' );
			// Verify that there isn't an old status, and that it's not assignable.
			if ( $this->is_assignable_status( $old_status ) ) {
				edd_debug_log( sprintf( 'ASON - order_saved - Old Status was assignable: %s. No order number needed for order ID %s', $old_status, $payment_id ) );
				return;
			}

			// Only run this update on orders that are completed.
			edd_debug_log( sprintf( 'ASON - order_saved status: %s (order ID %s)', $new_status, $payment_id ) );
			if ( ! $this->is_assignable_status( $new_status ) ) {
				return;
			}

			// (Re)assign the order number. We reassign
			// because the previously assigned number
			// was simply a "pending" order number.
			$this->assign_order_number( $payment_id );
		}

		/**
		 * Assign a sequential order number to the order
		 *
		 * @param $payment_id
		 * @param $payment_data
		 *
		 * @since 1.0.0
		 */
		public function assign_order_number( $payment_id, $payment_data = null ) {
			edd_debug_log( 'ASON - assign_order_number for order ID: ' . $payment_id );

			// First check if the plugin is active.
			// If not, don't do anything!
			if ( ! $this->is_active() ) {
				return;
			}

			$payment = new EDD_Payment( $payment_id );

			// Get the next order number including
			// it's pre-/postfix.
			$number = $this->next_order_number( $payment );

			if ( false === $number ) {
				return;
			}

			// Set the order number!
			if ( function_exists( 'edd_update_order' ) ) {
				edd_update_order(
					$payment_id,
					array(
						'order_number' => $number,
					)
				);
			} else {
				edd_update_payment_meta( $payment->ID, '_edd_son_payment_number', $number );
			}

			$this->maybe_set_completed_order_flag( $payment );
		}

		/**
		 * Assign a sequential number to the recurring payment.
		 *
		 * @access public
		 * @since 1.0.8
		 *
		 * @param EDD_Payment $payment
		 * @param EDD_Subscription $subscription
		 */
		public function assign_recurring_number( $payment, $subscription ) {

			_edd_deprecated_function( __FUNCTION__, '1.0.9' );

			if ( ! $this->is_active() ) {
				return;
			}

			$number = $this->next_order_number( $payment );

			edd_update_payment_meta( $payment->ID, '_edd_son_payment_number', $number );

			$this->maybe_set_completed_order_flag( $payment );
		}

		/**
		 * If the payment is "complete", sets a flag designating that a `complete` order number has been assigned.
		 * This is to ensure we don't re-generate a complete order number in the event that a payment's status
		 * switches to `complete` multiple times.
		 *
		 * @since 1.0.10
		 *
		 * @param EDD_Payment $payment
		 */
		private function maybe_set_completed_order_flag( $payment ) {
			if ( $this->is_assignable_status( $payment->status ) ) {
				edd_debug_log( sprintf( 'ASON - Setting complete order number flag for order #%d.', $payment->ID ) );
				edd_update_payment_meta( $payment->ID, '_edd_son_completed_order', time() );
			}
		}

		/**
		 * Determines whether or not the order already has a complete order number.
		 *
		 * @since 1.0.10
		 *
		 * @param int $payment_id
		 *
		 * @return bool
		 */
		private function has_complete_order_number( $payment_id ) {
			$has_complete = edd_get_payment_meta( $payment_id, '_edd_son_completed_order', true );

			return ! empty( $has_complete );
		}

		/**
		 * Gets the complete order number, if it exists.
		 *
		 * @since 1.0.11
		 * @param int $payment_id The payment/order ID.
		 * @return string|bool
		 */
		private function get_complete_order_number( $payment_id ) {

			// In EDD 3.0, the ASON order number is added directly to the order object.
			if ( function_exists( 'edd_get_order' ) ) {
				$order = edd_get_order( $payment_id );
				if ( ! empty( $order->order_number ) ) {
					return $order->order_number;
				}
				$order_number = edd_get_order_meta( $payment_id, '_edd_son_payment_number', true );
				if ( $order_number ) {
					edd_update_order(
						$order->id,
						array(
							'order_number' => $order_number,
						)
					);

					edd_delete_order_meta( $order->id, '_edd_son_payment_number' );

					return $order_number;
				}
			}

			return edd_get_payment_meta( $payment_id, '_edd_son_payment_number', true );
		}

		/**
		 * Get the next order number, including pre-/postfix.
		 *
		 * @param EDD_Payment $payment Payment post object.
		 *
		 * @since 1.0.0
		 *
		 * @return string The next payment number, for the specific order type
		 */
		private function next_order_number( $payment ) {

			if ( ! $payment instanceof EDD_Payment ) {
				$payment = edd_get_payment( $payment->ID );
			}

			if ( ! $this->is_assignable_status( $payment->status ) ) {

				if ( $this->has_order_number( $payment, 'temp' ) ) {
					return false;
				}

				// If the order isn't completed, we set a temporary order number.
				edd_debug_log( 'ASON - Temporary Number Needed for order ID: ' . $payment->ID );
				$number = EDD_Son_Prefix::temporary() . EDD_Son_Next_Order_Number::temporary() . EDD_Son_Postfix::temporary();

				// Store the temporary payment number, for (possible)
				// later use. This is because the temp. order number
				// will be overwritten, once the order is completed.
				edd_update_payment_meta( $payment->ID, '_edd_son_temp_payment_number', $number );

				return false;
			}

			// Use different number series for free orders.
			if ( edd_get_option( 'edd_son_free_number_series' ) && 0.00 === (float) $payment->total ) {

				if ( $this->has_order_number( $payment, 'free' ) ) {
					return false;
				}

				// The order is completed now, so we should
				// set the actual prefix. First check if
				// this is a free order.
				edd_debug_log( 'ASON - Free Number Needed for order ID ' . $payment->ID );
				$number = EDD_Son_Prefix::free() . EDD_Son_Next_Order_Number::free() . EDD_Son_Postfix::free();

			} else {

				if ( $this->has_order_number( $payment, 'complete' ) ) {
					return false;
				}

				// Since the order is completed, and it's
				// not free, this must be a regular order.
				edd_debug_log( 'ASON - Completed Number Needed for order ID ' . $payment->ID );
				$number = EDD_Son_Prefix::completed() . EDD_Son_Next_Order_Number::completed() . EDD_Son_Postfix::completed();

			}

			edd_debug_log( 'ASON - Order Number: ' . $number );

			// Return the order number.
			return $number;
		}

		/**
		 * Should the given payment status be considered a 'finalized' payment.
		 *
		 * @param string $status The payment status.
		 *
		 * @since 1.0.5
		 *
		 * @return bool Whether or not the given status, is assignable to a non-temporary order number.
		 */
		private function is_assignable_status( $status ) {
			$is_assignable_status = in_array( $status, array( 'publish', 'edd_subscription', 'complete' ), true );

			edd_debug_log( 'ASON - Is Assignable Status: ' . var_export( $is_assignable_status, true ) );

			return $is_assignable_status;
		}

		/**
		 * Determine if a payment already has an order number for the provided type.
		 *
		 * By using a Regular Expression of the prefix and suffix, determines if the current order number is already assigned
		 * and if it matches the status we're getting an order number for, returns true or false.
		 *
		 * @param EDD_Payment $payment The Payment to determine the order number status of.
		 * @param string      $type    The type of order number to check (temp, free, or completed).
		 *
		 * @return bool                If the order currently has an order number that matches the pattern for the passed type.
		 */
		private function has_order_number( EDD_Payment $payment, $type = 'complete' ) {
			if ( false === $payment ) {
				return false;
			}

			if ( 'temp' === $type ) {
				$current_number = $payment->get_meta( '_edd_son_temp_payment_number' );
			} else {
				$current_number = $this->get_complete_order_number( $payment->ID );
			}

			if ( empty( $current_number ) ) {
				return false;
			}

			switch ( $type ) {
				case 'temp':
					$prefix  = EDD_Son_Prefix::temporary();
					$postfix = EDD_Son_Postfix::temporary();
					break;

				case 'free':
					$prefix  = EDD_Son_Prefix::free();
					$postfix = EDD_Son_Postfix::free();
					break;

				case 'complete':
				default:
					$prefix  = EDD_Son_Prefix::completed();
					$postfix = EDD_Son_Postfix::completed();
					break;
			}

			if ( $this->has_complete_order_number( $payment->ID ) ) {
				return true;
			}

			/*
			 * The below regex will not work as expected if both the prefix
			 * and postfix are empty. It will always just return `true` as long as the number has
			 * digits. So at this point it's safer to assume `false`.
			 * @link https://github.com/awesomemotive/edd-advanced-sequential-order-numbers/issues/27
			 */
			if ( empty( $prefix ) && empty( $postfix ) ) {
				return false;
			}

			// See if the order already has a number for this status type.
			preg_match( '/' . preg_quote( $prefix, '/' ) . '(\d+)' . preg_quote( $postfix, '/' ) . '/', $current_number, $matches );

			return ! empty( $matches );

		}
	}
} // end class exists check

function EDD_Son_load() {
	return EDD_Son::instance();
}

require_once dirname( __FILE__ ) . '/vendor/autoload.php';
\EDD\ExtensionUtils\v1\ExtensionLoader::loadOrQuit( __FILE__, 'EDD_Son_load', array(
	'php'                    => '5.3',
	'easy-digital-downloads' => '2.9',
	'wp'                     => '4.4',
) );
