<?php

if ( ! defined( 'ABSPATH' ) ) {
	die( 'You are not allowed to call this page directly.' );
}

/**
 * @since 3.0
 */
class FrmProFieldCreditCard extends FrmFieldType {

	/**
	 * @var string
	 * @since 3.0
	 */
	protected $type = 'credit_card';

	/**
	 * @var bool
	 * @since 3.0
	 */
	protected $has_for_label = false;

	protected function field_settings_for_type() {
		$settings = array(
			'clear_on_focus' => false,
			'description'    => false,
			'visibility'     => true,
			'default'        => false,
		);

		FrmProFieldsHelper::fill_default_field_display( $settings );
		return $settings;
	}

	protected function extra_field_opts() {
		$options['save_cc'] = 4;

		$default_labels = $this->empty_value_array();
		foreach ( $default_labels as $key => $label ) {
			$options[ $key . '_desc' ] = $label;
		}

		return $options;
	}

	private static function default_labels() {
		return array(
			'cc'  => __( 'Card Number', 'formidable-pro' ),
			'cvc' => __( 'CVC', 'formidable-pro' ),
		);
	}

	/**
	 * @since 4.0
	 * @param array $args - Includes 'field', 'display', and 'values'.
	 */
	public function show_primary_options( $args ) {
		$field = $args['field'];
		include FrmProAppHelper::plugin_path() . '/classes/views/combo-fields/credit-cards/back-end-field-opts.php';

		if ( ! empty( $field['description'] ) ) {
			// This is here only for reverse compatibility.
			include FrmAppHelper::plugin_path() . '/classes/views/frm-fields/back-end/field-description.php';
		}

		parent::show_primary_options( $args );
	}

	/**
	 * @since 4.0
	 * @param array $args - Includes 'field', 'display'.
	 */
	public function show_after_default( $args ) {
		$field                = $args['field'];
		$field['placeholder'] = $this->placeholder_to_array();

		$sub_fields = $this->all_default_labels();
		foreach ( $sub_fields as $name => $field_label ) {
			include FrmProAppHelper::plugin_path() . '/classes/views/frmpro-fields/back-end/default-placeholder.php';
		}
	}

	/**
	 * @since 4.0
	 * @return array
	 */
	private function placeholder_to_array() {
		$value = FrmField::get_option( $this->field, 'placeholder' );
		if ( empty( $value ) ) {
			$value = array(
				'month' => __( 'Month', 'formidable-pro' ),
				'year'  => __( 'Year', 'formidable-pro' ),
			);
		}

		$defaults = $this->empty_value_array();
		$this->fill_values( $value, $defaults );

		return $value;
	}

	/**
	 * @since 3.06.01
	 */
	public function translatable_strings() {
		$strings   = parent::translatable_strings();
		$strings[] = 'cc';
		$strings[] = 'cvc';
		$strings[] = 'cc_desc';
		$strings[] = 'month_desc';
		$strings[] = 'year_desc';
		$strings[] = 'cvc_desc';
		return $strings;
	}

	public function show_on_form_builder( $name = '' ) {
		$field    = FrmFieldsHelper::setup_edit_vars( $this->field );
		$defaults = $this->empty_value_array();

		if ( ! is_array( $field['default_value'] ) ) {
			$field['default_value'] = $defaults;
		}

		$this->fill_values( $field['default_value'], $defaults );

		$field['value'] = $field['default_value'];
		$sub_fields     = FrmProCreditCardsController::get_sub_fields( $field );
		$field_name     = $this->html_name( $name );
		$html_id        = $this->html_id();

		include FrmProAppHelper::plugin_path() . '/classes/views/combo-fields/input-form-builder.php';
	}

	public function front_field_input( $args, $shortcode_atts ) {
		$pass_args = array(
			'errors'   => $args['errors'],
			'html_id'  => $args['html_id'],
			'field_id' => $args['field_id'],
		);

		/**
		 * Filters the callback function used to display the credit card fields in the front-end.
		 *
		 * @since 6.21 Added the $field parameter.
		 *
		 * @param string       $callback The callback function.
		 * @param array|object $field    The field data.
		 */
		$callback = apply_filters( 'frm_pro_show_card_callback', 'FrmProCreditCardsController::show_in_form', $this->field );

		ob_start();
		call_user_func( $callback, $this->field, $args['field_name'], $pass_args );
		$input_html = ob_get_contents();
		ob_end_clean();

		return $input_html;
	}

	protected function prepare_display_value( $value, $atts ) {
		if ( ! is_array( $value ) ) {
			return $value;
		}

		$new_value = '';
		if ( ! empty( $value['month'] ) ) {
			if ( ! empty( $value['cc'] ) ) {
				$new_value = $value['cc'] . ' <br/>';
			}

			$new_value .= __( 'Expiration:', 'formidable-pro' );
			$new_value .= ' ' . $value['month'] . '/' . $value['year'];
		}
		return $new_value;
	}

	/**
	 * @since 4.0
	 */
	private function all_default_labels() {
		$labels = $this->default_labels();
		return array(
			'cc'    => $labels['cc'],
			'month' => __( 'Month', 'formidable-pro' ),
			'year'  => __( 'Year', 'formidable-pro' ),
			'cvc'   => $labels['cvc'],
		);
	}

	private function empty_value_array() {
		return array(
			'cc'    => '',
			'month' => '',
			'year'  => '',
			'cvc'   => '',
		);
	}

	public function validate( $args ) {
		$this->field->temp_id = $args['id'];

		$errors = array();
		$this->validate_required_fields( $errors, $args );
		$this->validate_cc_number( $errors, $args );
		$this->validate_cc_expiration( $errors, $args );
		$this->validate_cvc( $errors, $args );

		return $errors;
	}

	/**
	 * @param array $errors
	 * @return void
	 */
	private function validate_required_fields( &$errors, $args ) {
		$values = $args['value'];
		if ( $this->should_require( $values ) ) {
			if ( empty( $values ) ) {
				$errors[ 'field' . $args['id'] ] = FrmFieldsHelper::get_error_msg( $this->field, 'blank' );
				return;
			}

			foreach ( $values as $key => $value ) {
				if ( empty( $value ) ) {
					$errors[ 'field' . $args['id'] . '-' . $key ] = '';
					$errors[ 'field' . $args['id'] ]              = FrmFieldsHelper::get_error_msg( $this->field, 'blank' );
				}
			}
		}
	}

	private function should_require( $values ) {
		$partial_fill = ! empty( $values['cc'] );
		$saving_draft = FrmProFormsHelper::saving_draft();

		if ( $saving_draft ) {
			$is_editing = false;
		} else {
			$is_editing = ( $_POST && isset( $_POST['id'] ) && is_numeric( $_POST['id'] ) );
			if ( $is_editing ) {
				$is_editing = ! FrmProEntry::is_draft( absint( $_POST['id'] ) );
			}
		}

		if ( $partial_fill && ! $is_editing ) {
			return true;
		}

		if ( ! $this->field->required || $is_editing || $saving_draft ) {
			return false;
		}

		$skip_required = FrmProEntryMeta::skip_required_validation( $this->field );
		return ! $skip_required;
	}

	private function validate_cc_number( &$errors, $args ) {
		$values = $args['value'];
		if ( ! empty( $values['cc'] ) ) {
			// if a CVC is present, then the user must have added it
			$should_validate = ! empty( $values['cvc'] ) || isset( $errors[ 'field' . $args['id'] . '-cvc' ] );
			if ( $should_validate ) {
				$is_valid_cc = $this->is_valid_cc_number( $values['cc'] );
				if ( ! $is_valid_cc ) {
					$errors[ 'field' . $args['id'] . '-cc' ] = __( 'That credit card number is invalid', 'formidable-pro' );
				}
			}
		}
	}

	private function is_valid_cc_number( $card_number ) {
		// Get the first digit
		$firstnumber = substr( $card_number, 0, 1 );

		// Make sure it is the correct amount of digits. Account for dashes being present.
		switch ( $firstnumber ) {
			case 3:
				$is_valid = preg_match( '/^3\d{3}[ \-]?\d{6}[ \-]?\d{5}$/', $card_number );
				break;
			case 4:
				$is_valid = preg_match( '/^4\d{3}[ \-]?\d{4}[ \-]?\d{4}[ \-]?\d{4}$/', $card_number );
				break;
			case 5:
				$is_valid = preg_match( '/^5\d{3}[ \-]?\d{4}[ \-]?\d{4}[ \-]?\d{4}$/', $card_number );
				break;
			case 6:
				$is_valid = preg_match( '/^6011[ \-]?\d{4}[ \-]?\d{4}[ \-]?\d{4}$/', $card_number );
				break;
			default:
				$is_valid = false;
		}

		$this->validate_luhn_algorithm( $card_number, $is_valid );

		return $is_valid;
	}

	/**
	 * @param false|int $is_valid
	 *
	 * @psalm-param 0|1|false $is_valid
	 *
	 * @return void
	 */
	private function validate_luhn_algorithm( $card_number, &$is_valid ) {
		if ( ! $is_valid ) {
			return;
		}

		$card_number = str_replace( array( '-', ' ' ), '', $card_number );
		$map         = array( 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 2, 4, 6, 8, 1, 3, 5, 7, 9 );
		$sum         = 0;
		$last        = strlen( $card_number ) - 1;
		for ( $i = 0; $i <= $last; $i++ ) {
			$sum += $map[ $card_number[ $last - $i ] + ( $i & 1 ) * 10 ];
		}

		if ( $sum % 10 != 0 ) {
			$is_valid = false;
		}

		if ( ! $is_valid ) {
			$allow    = array( '4242424242424242' );
			$is_valid = in_array( $card_number, $allow );
		}
	}

	/**
	 * Make sure the date is in the future
	 */
	private function validate_cc_expiration( &$errors, $args ) {
		$values = $args['value'];
		if ( ! empty( $values['month'] ) && ! empty( $values['year'] ) ) {
			$is_past_date = ( $values['year'] <= gmdate( 'Y' ) && $values['month'] < gmdate( 'm' ) );
			if ( $is_past_date ) {
				$errors[ 'field' . $args['id'] . '-month' ] = __( 'That credit card is expired', 'formidable-pro' );
				$errors[ 'field' . $args['id'] . '-year' ]  = '';
			}
		}
	}

	private function validate_cvc( &$errors, $args ) {
		$values = $args['value'];
		if ( ! empty( $values['cvc'] ) ) {
			$character_count   = strlen( $values['cvc'] );
			$is_correct_length = ( $character_count == 3 || $character_count == 4 );
			$is_valid          = ( is_numeric( $values['cvc'] ) && $is_correct_length );
			if ( ! $is_valid ) {
				$errors[ 'field' . $args['id'] . '-cvc' ] = __( 'Please enter a valid CVC', 'formidable-pro' );
			}
		}
	}

	/**
	 * @param array|string $value This may be a serialized value set before inserting into database.
	 */
	public function set_value_before_save( $value ) {
		if ( empty( $value ) ) {
			return $value;
		}

		$serialized = false;
		if ( ! is_array( $value ) ) {
			FrmProAppHelper::unserialize_or_decode( $value );
			$serialized = true;
		}

		if ( is_array( $value ) ) {
			self::delete_cvc( $value );
			self::remove_extra_cc_digits( $value );
			if ( $serialized ) {
				$value = serialize( $value );
			}
		}

		return $value;
	}

	/**
	 * The CVC shouldn't be stored
	 *
	 * @param array $value
	 * @return void
	 */
	private function delete_cvc( &$value ) {
		$value['cvc'] = '';
	}

	/**
	 * If the whole cc number isn't required, get rid of it
	 */
	private function remove_extra_cc_digits( &$value ) {
		$save_digits = FrmField::get_option( $this->field, 'save_cc' );

		if ( $save_digits == 0 ) {
			$value['cc'] = '';
		} elseif ( ! empty( $value['cc'] ) && $save_digits != 16 ) {
			$length      = max( strlen( $value['cc'] ) - 4, 0 );
			$value['cc'] = str_repeat( 'x', $length ) . substr( $value['cc'], -4 );
		}
	}

	/**
	 * @since 4.0.04
	 */
	public function sanitize_value( &$value ) {
		FrmAppHelper::sanitize_value( 'sanitize_text_field', $value );
	}
}
