<?php
/**
 * WooCommerce Braintree Gateway
 *
 * This source file is subject to the GNU General Public License v3.0
 * that is bundled with this package in the file license.txt.
 * It is also available through the world-wide-web at this URL:
 * http://www.gnu.org/licenses/gpl-3.0.html
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@woocommerce.com so we can send you a copy immediately.
 *
 * DISCLAIMER
 *
 * Do not edit or add to this file if you wish to upgrade WooCommerce Braintree Gateway to newer
 * versions in the future. If you wish to customize WooCommerce Braintree Gateway for your
 * needs please refer to http://docs.woocommerce.com/document/braintree/
 *
 * @package   WC-Braintree/Buttons
 * @author    WooCommerce
 * @copyright Copyright: (c) 2016-2020, Automattic, Inc.
 * @license   http://www.gnu.org/licenses/gpl-3.0.html GNU General Public License v3.0
 */

namespace WC_Braintree\PayPal\Buttons;

use SkyVerge\WooCommerce\PluginFramework\v5_15_10 as Framework;
use WC_Braintree_PayPal_Payment_Form;

defined( 'ABSPATH' ) or exit;

/**
 * Abstract PayPal button class.
 *
 * @since 2.3.0
 */
abstract class Abstract_Button extends Framework\Handlers\Script_Handler {


	/**
	 * PayPal gateway instance.
	 *
	 * @var \WC_Gateway_Braintree_PayPal
	 */
	protected $gateway;

	/**
	 * Tracks whether a cancel link has been rendered from any button
	 *
	 * @var bool
	 */
	protected static $rendered_cancel_link = false;


	/**
	 * Constructs the button handler.
	 *
	 * @since 2.3.0
	 *
	 * @param \WC_Gateway_Braintree_PayPal $gateway the gateway instance.
	 */
	public function __construct( $gateway ) {

		$this->gateway = $gateway;

		parent::__construct();
	}


	/**
	 * Checks if this button should be enabled or not.
	 *
	 * @since 2.3.0
	 *
	 * @return bool
	 */
	protected function is_enabled() {
		// override in concrete classes.
		return false;
	}


	/**
	 * Adds the action and filter hooks.
	 *
	 * @since 2.3.0
	 */
	protected function add_hooks() {

		if ( $this->is_enabled() ) {

			parent::add_hooks();

			$this->add_button_hooks();
		}
	}


	/**
	 * Adds any actions and filters needed for the button, if the button is enabled.
	 *
	 * @since 2.4.0
	 */
	protected function add_button_hooks() {
		// handle the PayPal token generated by Braintree JS.
		add_action( 'woocommerce_api_' . stripslashes( strtolower( get_class( $this->get_gateway() ) ) ), [ $this, 'handle_wc_api' ] );

		// set the checkout address value if it's available from the PayPal session.
		add_filter( 'woocommerce_checkout_get_value', [ $this, 'set_checkout_value' ], 10, 2 );

		// set the "Ship to different address" checkbox based on PayPal session data.
		add_filter( 'woocommerce_ship_to_different_address_checked', [ $this, 'set_ship_to_different_address' ] );

		// disable all other gateways at checkout when confirming payment.
		add_action( 'woocommerce_available_payment_gateways', [ $this, 'disable_other_gateways' ] );

		// add styles for the cart & confirmation page.
		add_action( 'wp_head', [ $this, 'enqueue_styles' ] );

		// add a "Cancel" link below the order button at checkout confirmation.
		add_action( 'woocommerce_review_order_after_submit', [ $this, 'render_cancel_link' ] );

		// clear the session data when the cart is emptied.
		add_action( 'woocommerce_cart_emptied', [ $this, 'clear_session_data' ] );
	}


	/**
	 * Renders the button markup and JS.
	 *
	 * @since 2.3.0
	 */
	public function render() {

		$available_gateways = WC()->payment_gateways->get_available_payment_gateways();

		if ( ! isset( $available_gateways['braintree_paypal'] ) ) {
			return;
		}

		$this->clear_session_data();
		$this->render_button();
		$this->render_js();
	}

	/**
	 * Handles the request made to the WC API after the JS has generated a token.
	 *
	 * @since 2.3.0
	 */
	public function handle_wc_api() {

		if ( $this->is_wc_api_request_valid() ) {
			$this->process_wc_api_request();
		}
	}


	/**
	 * Validates a WC API request.
	 *
	 * @since 2.3.0
	 *
	 * @return bool
	 */
	abstract protected function is_wc_api_request_valid();


	/**
	 * Processes a WC API request after the JS has generated a token.
	 *
	 * @since 2.3.0
	 */
	protected function process_wc_api_request() {
		// phpcs:disable WordPress.Security.NonceVerification.Missing
		WC()->session->set( 'wc_braintree_paypal_cart_nonce', Framework\SV_WC_Helper::get_posted_value( 'nonce' ) );

		if ( ! empty( $_POST['details'] ) ) {
			$this->set_customer_data( wc_clean( wp_unslash( $_POST['details'] ) ) );
		}

		wp_send_json(
			[
				'redirect_url' => wc_get_checkout_url(),
			]
		);
		// phpcs:enable
	}


	/**
	 * Gets the array of form handler JS params.
	 *
	 * @since 2.3.0
	 *
	 * @return array
	 */
	protected function get_form_handler_params() {

		$params = $this->get_gateway()->get_payment_form_instance()->get_payment_form_handler_js_params();

		$params['cart_payment_nonce'] = $this->get_cart_nonce();

		return $params;
	}


	/**
	 * Gets the JS handler arguments.
	 *
	 * @since 2.4.0
	 *
	 * @return array
	 */
	protected function get_js_handler_args() {

		$args = array_merge(
			[
				'id'                       => $this->get_gateway()->get_id(),
				'id_dasherized'            => $this->get_gateway()->get_id_dasherized(),
				'name'                     => $this->get_gateway()->get_method_title(),
				'debug'                    => $this->get_gateway()->debug_log(),
				'client_token_nonce'       => wp_create_nonce( 'wc_' . $this->get_gateway()->get_id() . '_get_client_token' ),
				'set_payment_method_nonce' => wp_create_nonce( 'wc_' . $this->get_gateway()->get_id() . '_cart_set_payment_method' ),
				'cart_handler_url'         => add_query_arg( 'wc-api', get_class( $this->get_gateway() ), home_url() ),
			],
			$this->get_form_handler_params(),
			$this->get_additional_js_handler_params()
		);

		/**
		 * Filters the JS handler params.
		 *
		 * @since 2.3.0
		 *
		 * @param array params
		 * @param Abstract_Button button handler instance.
		 */
		return (array) apply_filters( 'wc_' . $this->get_gateway()->get_id() . '_button_js_handler_params', $args, $this );
	}

	/**
	 * Gets additional JS Handler params - Override in concrete classes.
	 *
	 * @since 2.3.0
	 *
	 * @return array
	 */
	protected function get_additional_js_handler_params() {

		return [];
	}


	/**
	 * Renders the PayPal button JS.
	 *
	 * @since 2.3.0
	 */
	protected function render_js() {

		wc_enqueue_js( $this->get_safe_handler_js() );
	}


	/**
	 * Gets the handler instantiation JS.
	 *
	 * @since 2.4.0
	 *
	 * @param array  $additional_args additional handler arguments, if any.
	 * @param string $handler_name handler name, if different from self::get_js_handler_class_name().
	 * @param string $object_name object name, if different from self::get_js_handler_object_name().
	 * @return string
	 */
	protected function get_handler_js( array $additional_args = [], $handler_name = '', $object_name = '' ) {

		$js = parent::get_handler_js( $additional_args, $handler_name, $object_name );

		$js .= sprintf( 'window.%s.init();', $object_name ?: $this->get_js_handler_object_name() );

		return $js;
	}


	/**
	 * Renders the PayPal button markup.
	 *
	 * @since 2.3.0
	 */
	public function render_button() {

		$container_style = $this->get_gateway()->get_button_container_style();

		?>

		<?php if ( $this->get_gateway()->is_paypal_pay_later_enabled() ) : ?>
			<div id="wc_braintree_paypal_pay_later_messaging_container" <?php echo wp_kses( $container_style, '' ); ?> <?php echo wp_kses( $this->get_gateway()->get_pay_later_messaging_style_attributes(), '' ); ?>></div>
		<?php endif; ?>

		<div id="<?php echo esc_attr( 'wc_' . $this->get_gateway()->get_id() . '_container' ); ?>" <?php echo wp_kses( $container_style, 'style' ); ?>></div>

		<?php foreach ( $this->get_button_params() as $key => $value ) : ?>

			<input type="hidden"
				name="<?php echo esc_attr( 'wc_' . $this->get_gateway()->get_id() . '_' . $key ); ?>"
				value="<?php echo esc_attr( $value ); ?>"
			/>

			<?php
			endforeach;
	}


	/**
	 * Gets an array of params for rendering the button HTML.
	 *
	 * @since 2.3.0
	 *
	 * @return array
	 */
	protected function get_button_params() {

		/**
		 * Filters the markup params for a button.
		 *
		 * @since 2.3.0
		 *
		 * @param array params
		 * @param Abstract_Button button handler instance
		 */
		return (array) apply_filters(
			'wc_' . $this->get_gateway()->get_id() . '_button_markup_params',
			array_merge(
				[
					'amount'         => Framework\SV_WC_Helper::number_format( $this->get_button_total() ),
					'currency'       => $this->get_button_currency(),
					'locale'         => $this->get_button_locale(),
					'single_use'     => $this->is_single_use(),
					'needs_shipping' => $this->needs_shipping(),
				],
				$this->get_additional_button_params()
			),
			$this
		);
	}


	/**
	 * Gets additional button params for the button markup - override in concrete classes.
	 *
	 * @since 2.3.0
	 *
	 * @return array
	 */
	protected function get_additional_button_params() {
		return [];
	}


	/**
	 * Determines if the PayPal token should be considered "single use".
	 *
	 * This is primarily dependent on if there are subscriptions or Pre-Orders
	 * present in the cart.
	 *
	 * @since 2.3.0
	 *
	 * @return bool
	 */
	protected function is_single_use() {

		$single_use = true;

		if ( $this->get_gateway()->get_plugin()->is_pre_orders_active() && \WC_Pre_Orders_Cart::cart_contains_pre_order() && \WC_Pre_Orders_Product::product_is_charged_upon_release( \WC_Pre_Orders_Cart::get_pre_order_product() ) ) {
			$single_use = false;
		}

		if ( $this->get_gateway()->get_plugin()->is_subscriptions_active() && ( \WC_Subscriptions_Cart::cart_contains_subscription() || wcs_cart_contains_renewal() ) ) {
			$single_use = false;
		}

		return $single_use;
	}


	/**
	 * Gets the total amount the button should charge - override in concrete classes.
	 *
	 * @since 2.3.0
	 *
	 * @return float
	 */
	protected function get_button_total() {
		return 0.0;
	}


	/**
	 * Gets the currency to be used for the button.
	 *
	 * @since 2.3.0
	 *
	 * @return string
	 */
	protected function get_button_currency() {
		return get_woocommerce_currency();
	}


	/**
	 * Gets the locale to be used for the button.
	 *
	 * @since 2.3.0
	 *
	 * @return string
	 */
	protected function get_button_locale() {
		return $this->get_gateway()->get_safe_locale();
	}

	/**
	 * Determines if shipping address needs to be enable on express checkout or not.
	 *
	 * This is primarily dependent on if cart or product needs shipping.
	 *
	 * @since 2.9.1
	 *
	 * @return bool
	 */
	protected function needs_shipping() {
		$needs_shipping = false;

		if ( is_product() ) {
			$product        = wc_get_product( get_the_ID() );
			$needs_shipping = $product instanceof \WC_Product ? $product->needs_shipping() : false;
		}

		if ( is_cart() && WC()->cart ) {
			$needs_shipping = WC()->cart->needs_shipping();
		}

		return $needs_shipping;
	}

	/**
	 * Sets the checkout address value if it's available from the PayPal session.
	 *
	 * @since 2.3.0
	 *
	 * @param null   $value the checkout field value.
	 * @param string $key the checkout field key.
	 * @return string|null
	 */
	public function set_checkout_value( $value, $key ) {

		$details = WC()->session->get( 'wc_braintree_paypal_cart_customer_details' );

		if ( Framework\SV_WC_Helper::str_starts_with( $key, 'billing' ) ) {

			$type = 'billing';
			$key  = str_replace( 'billing_', '', $key );

		} elseif ( Framework\SV_WC_Helper::str_starts_with( $key, 'shipping' ) ) {

			$type = 'shipping';
			$key  = str_replace( 'shipping_', '', $key );

		} else {

			return $value;
		}

		if ( ! empty( $details[ $type ][ $key ] ) ) {
			$value = $details[ $type ][ $key ];
		}

		return $value;
	}


	/**
	 * Sets the "Ship to different address" checkbox based on PayPal session data.
	 *
	 * @since 2.3.0
	 *
	 * @param bool $checked whether the checkbox is checked.
	 * @return bool
	 */
	public function set_ship_to_different_address( $checked ) {

		$details = WC()->session->get( 'wc_braintree_paypal_cart_customer_details', [] );

		if ( ! empty( $details ) ) {

			unset( $details['billing']['email'], $details['billing']['phone'] );

			if ( empty( $details['shipping'] ) ) {
				$checked = false;
			} elseif ( empty( $details['billing'] ) || $details['billing'] !== $details['shipping'] ) {
				$checked = true;
			}
		}

		return $checked;
	}


	/**
	 * Disables all other gateways at checkout when confirming payment.
	 *
	 * @since 2.0.0
	 * @param array $gateways Available gateways.
	 * @return array
	 */
	public function disable_other_gateways( $gateways ) {

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

		foreach ( $gateways as $id => $gateway ) {

			if ( $id !== $this->get_gateway()->get_id() ) {
				unset( $gateways[ $id ] );
			}
		}

		return $gateways;
	}


	/**
	 * Adds styles for the cart & confirmation page.
	 *
	 * @since 2.3.0
	 */
	public function enqueue_styles() {

		if ( ! is_cart() && ! $this->is_checkout_confirmation() ) {
			return;
		}

		?>

		<style>
			.payment_box.payment_method_braintree_paypal {
				padding: 0;
			}
			.payment_box.payment_method_braintree_paypal fieldset {
				display: none;
			}
			.wc-braintree-paypal-cancel {
				display: block;
				text-align: center;
				padding: 15px;
			}
		</style>

		<?php
	}


	/**
	 * Renders a "Cancel" link.
	 *
	 * @since 2.3.0
	 */
	public function render_cancel_link() {

		if ( self::$rendered_cancel_link || ! $this->is_checkout_confirmation() ) {
			return;
		}

		printf(
			'<a href="%1$s" class="wc-' . sanitize_html_class( $this->get_gateway()->get_id_dasherized() ) . '-cancel">%2$s</a>',
			esc_url( add_query_arg( [ 'wc_' . $this->get_gateway()->get_id() . '_clear_session' => true ], wc_get_cart_url() ) ),
			esc_html__( 'Cancel', 'woocommerce-gateway-paypal-powered-by-braintree' )
		);

		self::$rendered_cancel_link = true;
	}


	/**
	 * Sets the customer address session data from a PayPal token request.
	 *
	 * @since 2.3.0
	 *
	 * @param array $data the customer data.
	 */
	protected function set_customer_data( $data = [] ) {

		// if there is no data to set, bail.
		if ( ! is_array( $data ) || empty( $data ) ) {
			return;
		}

		$data = wp_parse_args(
			$data,
			[
				'email'           => '',
				'payerId'         => '',
				'firstName'       => '',
				'lastName'        => '',
				'phone'           => '',
				'countryCode'     => '',
				'shippingAddress' => [],
				'billingAddress'  => [],
			]
		);

		// https://braintree.github.io/braintree-web/current/PayPal.html#~tokenizePayload.
		$address_defaults = [
			'line1'       => '',
			'line2'       => '',
			'city'        => '',
			'state'       => '',
			'postalCode'  => '',
			'countryCode' => '',
		];

		if ( isset( $data['billingAddress'] ) && ! empty( $data['billingAddress'] ) && ! empty( $data['billingAddress']['line1'] ) ) {
			$billing_address = wp_parse_args( $data['billingAddress'], $address_defaults );
		} else {
			$billing_address = wp_parse_args( $data['shippingAddress'], $address_defaults );
		}
		$shipping_address = wp_parse_args( $data['shippingAddress'], $address_defaults );

		$details = [
			'billing'  => [
				'first_name' => $data['firstName'],
				'last_name'  => $data['lastName'],
				'email'      => $data['email'],
				'phone'      => $data['phone'],
				'country'    => $billing_address['countryCode'],
				'address_1'  => $billing_address['line1'],
				'address_2'  => $billing_address['line2'],
				'city'       => $billing_address['city'],
				'state'      => $billing_address['state'],
				'postcode'   => $billing_address['postalCode'],
			],
			'shipping' => [
				'first_name' => $data['firstName'],
				'last_name'  => $data['lastName'],
				'country'    => $shipping_address['countryCode'],
				'address_1'  => $shipping_address['line1'],
				'address_2'  => $shipping_address['line2'],
				'city'       => $shipping_address['city'],
				'state'      => $shipping_address['state'],
				'postcode'   => $shipping_address['postalCode'],
			],
		];

		WC()->session->set( 'wc_braintree_paypal_cart_customer_details', $details );
	}


	/**
	 * Determines if the current view is at Checkout, confirming the cart PayPal purchase.
	 *
	 * @since 2.3.0
	 *
	 * @return bool
	 */
	public function is_checkout_confirmation() {

		return $this->get_gateway()->get_payment_form_instance()->is_checkout_confirmation();
	}


	/**
	 * Gets the cart nonce from the session, if any.
	 *
	 * @since 2.3.0
	 *
	 * @return string
	 */
	public function get_cart_nonce() {

		return $this->get_gateway()->get_payment_form_instance()->get_cart_nonce();
	}


	/**
	 * Clears any PayPal cart session data.
	 *
	 * @since 2.3.0
	 */
	public function clear_session_data() {

		unset(
			WC()->session->wc_braintree_paypal_cart_nonce,
			WC()->session->wc_braintree_paypal_cart_customer_details
		);
	}


	/**
	 * Returns the gateway instance.
	 *
	 * @since 2.3.0
	 *
	 * @return \WC_Gateway_Braintree_PayPal
	 */
	protected function get_gateway() {
		return $this->gateway;
	}


	/**
	 * Adds a log entry.
	 *
	 * @since 2.4.0
	 *
	 * @param string $message message to log.
	 */
	protected function log_event( $message ) {

		$this->get_gateway()->add_debug_message( $message );
	}


	/**
	 * Determines whether logging is enabled.
	 *
	 * @since 2.4.0
	 *
	 * @return bool
	 */
	protected function is_logging_enabled() {

		return $this->get_gateway()->debug_log();
	}
}
