<?php

class CF7CF {
    private $hidden_fields = array();
    private $visible_groups = array();
    private $hidden_groups = array();
    private $repeaters = array();
    private $steps = array();

    function __construct() {

        // compatibility with CF7 multi-step forms by Webhead LLC.
        add_filter( 'wpcf7_posted_data', array($this,'cf7msm_merge_post_with_cookie'), 8, 1 );

        // compatibility with CF7 Multi Step by NinjaTeam https://wordpress.org/plugins/cf7-multi-step/
        add_action('wp_ajax_cf7mls_validation', array($this,'cf7mls_validation_callback'),9);
        add_action('wp_ajax_nopriv_cf7mls_validation', array($this,'cf7mls_validation_callback'),9);

        add_filter( 'wpcf7_validate', array($this, 'skip_validation_for_hidden_fields'), 2, 2 );

        add_filter( 'wpcf7_validate_file*', array($this, 'skip_validation_for_hidden_file_field'), 30, 3);
        add_filter( 'wpcf7_validate_multifile*', array($this, 'skip_validation_for_hidden_file_field'), 30, 3);

        // If acceptance_as_validation is on, then Acceptance fields inside hidden groups should not trigger an error
        add_filter( 'wpcf7_acceptance', function($accepted, $submission) {
            $acceptance_as_validation = $submission->get_contact_form()->additional_setting('acceptance_as_validation');
            return $accepted || (is_array($acceptance_as_validation) && in_array('on', $acceptance_as_validation));
        }, 20, 2 );

	    // validation messages
	    add_action('wpcf7_config_validator_validate', array($this,'wpcf7cf_config_validator_validate'));

	    add_action("wpcf7_before_send_mail", [$this, 'hide_hidden_mail_fields'], 10, 3);

        register_activation_hook(__FILE__, array($this, 'activate'));

        if (is_admin()) {
            require_once dirname(__FILE__) . '/admin.php';
        }
    }



	/**
	 * Suppress invalid mailbox syntax errors on fields that contain existing conditional groups.
     * Fields to check:
     *  - mail.recipient, mail.sender, mail.additional_headers
     *  - mail2.recipient, mail2.sender, mail2.additional_headers
	 */
    function wpcf7cf_config_validator_validate(WPCF7_ConfigValidator $wpcf7_config_validator) {

        $errors = $wpcf7_config_validator->collect_error_messages();
        
        if (empty($errors)) {
            return; // no errors, nothing to do
        }
        
        $cf = $wpcf7_config_validator->contact_form();
        
        // find all group names from the form tags
        $all_group_tags = array_filter($cf->scan_form_tags(), function($tag) { return $tag['type'] == 'group'; });
        $all_group_names = array_map(function($tag) { return $tag['name']; }, $all_group_tags);

        $available_error_codes = (array) apply_filters(
            'wpcf7_config_validator_available_error_codes',
            WPCF7_ConfigValidator::error_codes,
            $cf
        );

    	foreach ($errors as $err_type => $err) {

		    $parts = explode('.',$err_type);

		    $property = $parts[0];
            $sub_prop = $parts[1];

            $is_emailadress_property = in_array($property, ['mail', 'mail_2']) && in_array($sub_prop, ['recipient', 'sender', 'additional_headers']);

		    if ( !$is_emailadress_property ) {
                continue; // only check email address properties
            }

            if (!isset($cf->prop($property)[$sub_prop]) || empty($cf->prop($property)[$sub_prop])) {
                continue; // skip if the property is not set
            }

            // get the value of the property
		    $prop_val = $cf->prop($property)[$sub_prop];

            // strip all [group_name] and [/group_name] tags from the prop_value
            foreach ($all_group_names as $group_name) {
                $prop_val = str_replace("[$group_name]", '', $prop_val);
                $prop_val = str_replace("[/$group_name]", '', $prop_val);
            }

            // remove all current errors on the property, and then run the validation function again with the group tags stripped from the prop_value
            if (in_array($sub_prop, ['recipient', 'sender', 'additional_headers'])) {
                foreach ($available_error_codes as $error_code) {
                    $wpcf7_config_validator->remove_error($err_type, $error_code);
                }
                $validation_function = 'validate_mail_' . $sub_prop;
                $wpcf7_config_validator->$validation_function($property, $prop_val);
            }

	    }
    }

    function activate() {
        //add options with add_option and stuff
    }

    /**
     * Remove validation requirements for fields that are hidden at the time of form submission.
     * Required/invalid fields should never trigger validation errors if they are inside a hidden group during submission.
     * Called using add_filter( 'wpcf7_validate', array($this, 'skip_validation_for_hidden_fields'), 2, 2 );
     * where the priority of 2 causes this to kill any validations with a priority higher than 2
     * 
     * NOTE: CF7 is weirdly designed when it comes to validating a form with files.
     *       Only the non-file fields are considered during the wpcf7_validate filter.
     *       When validation passes for all fields (except the file fields), the files fields are validated individually.
     *       ( see skip_validation_for_hidden_file_field )
     *
     * @param $result
     * @param $tag
     *
     * @return mixed
     */
    function skip_validation_for_hidden_fields($result, $tags, $args = []) {

        if(isset($_POST)) {
            $this->set_hidden_fields_arrays($_POST);
        }

        $invalid_fields = $result->get_invalid_fields();
        $return_result = new WPCF7_Validation();

        if (count($this->hidden_fields) == 0 || !is_array($invalid_fields) || count($invalid_fields) == 0) {
            $return_result = $result;
        } else {
            foreach ($invalid_fields as $invalid_field_key => $invalid_field_data) {
                if (!in_array($invalid_field_key, $this->hidden_fields)) {
                    foreach ($tags as $tag) {
                        if ($tag['name'] === $invalid_field_key) {
                           $return_result->invalidate($tag, $invalid_field_data['reason']);
                        }
                    }
                }
            }
        }

        return apply_filters('wpcf7cf_validate', $return_result, $tags);

    }

    /**
     * Does the same thing as skip_validation_for_hidden_fields, but CF7 will check files again later
     * via the wpcf7_unship_uploaded_files function
     * so we need to skip validation a second time for individual file fields
     */
    function skip_validation_for_hidden_file_field($result, $tag, $args=[]) {

        if (!count($result->get_invalid_fields())) {
            return $result;
        }
        if(isset($_POST)) {
            $this->set_hidden_fields_arrays($_POST);
        }

        $invalid_field_keys = array_keys($result->get_invalid_fields());

        // if the current file is the only invalid tag in the result AND if the file is hidden: return a valid (blank) object
        if (isset($this->hidden_fields) && is_array($this->hidden_fields) && in_array($tag->name, $this->hidden_fields) && count($invalid_field_keys) == 1) {
            return new WPCF7_Validation();
        }

        // if the current file is not hidden, we'll just return the result (keep it invalid).
        // (Note that this might also return the hidden files as invalid, but that shouldn't matter because the form is invalid, and the notification will be inside a hidden group)
        return $result;
    }

    function cf7msm_merge_post_with_cookie($posted_data) {

        if (!function_exists('cf7msm_get') || !key_exists('cf7msm_posted_data',$_COOKIE)) return $posted_data;

        if (!$posted_data) {
            $posted_data = WPCF7_Submission::get_instance()->get_posted_data();
        }

        // this will temporarily set the hidden fields data to the posted_data.
        // later this function will be called again with the updated posted_data
        $this->set_hidden_fields_arrays($_POST);

        // get cookie data
        $cookie_data = cf7msm_get('cf7msm_posted_data');
        $cookie_data_hidden_group_fields = json_decode(stripslashes($cookie_data['_wpcf7cf_hidden_group_fields']));
        $cookie_data_hidden_groups = json_decode(stripslashes($cookie_data['_wpcf7cf_hidden_groups']));
        $cookie_data_visible_groups = json_decode(stripslashes($cookie_data['_wpcf7cf_visible_groups']));

        // remove all the currently posted data from the cookie data (we don't wanna add it twice)
        $cookie_data_hidden_group_fields = array_diff($cookie_data_hidden_group_fields, array_keys($posted_data));
        $cookie_data_hidden_groups = array_diff((array) $cookie_data_hidden_groups, $this->hidden_groups, $this->visible_groups);
        $cookie_data_visible_groups = array_diff((array) $cookie_data_visible_groups, $this->hidden_groups, $this->visible_groups);

        // update current post data with cookie data
        $posted_data['_wpcf7cf_hidden_group_fields'] = addslashes(json_encode(array_merge((array) $cookie_data_hidden_group_fields, $this->hidden_fields)));
        $posted_data['_wpcf7cf_hidden_groups'] = addslashes(json_encode(array_merge((array) $cookie_data_hidden_groups, $this->hidden_groups)));
        $posted_data['_wpcf7cf_visible_groups'] = addslashes(json_encode(array_merge((array) $cookie_data_visible_groups, $this->visible_groups)));

        return $posted_data;
    }

    // compatibility with CF7 Multi Step by NinjaTeam https://wordpress.org/plugins/cf7-multi-step/
    function cf7mls_validation_callback() {
        $this->set_hidden_fields_arrays($_POST);
    }

    /**
     * Finds the currently submitted form and set the hidden_fields variables accoringly
     *
     * @param bool|array $posted_data
     */
    function set_hidden_fields_arrays($posted_data = false) {

        if (!$posted_data) $posted_data = $_POST;

        $hidden_fields = json_decode(stripslashes($posted_data['_wpcf7cf_hidden_group_fields']));
        if (is_array($hidden_fields) && count($hidden_fields) > 0) {
            foreach ($hidden_fields as $field) {
                $this->hidden_fields[] = $field;
                if (wpcf7cf_endswith($field, '[]')) {
                    $this->hidden_fields[] = substr($field,0,strlen($field)-2);
                }
            }
        }
        $this->hidden_groups = json_decode(stripslashes($posted_data['_wpcf7cf_hidden_groups']));
        $this->visible_groups = json_decode(stripslashes($posted_data['_wpcf7cf_visible_groups']));
        $this->repeaters = json_decode(stripslashes($posted_data['_wpcf7cf_repeaters']));
        $this->steps = json_decode(stripslashes($posted_data['_wpcf7cf_steps']));
    }

	function hide_hidden_mail_fields($form,$abort,$submission) {
		$props = $form->get_properties();
		$mails = ['mail','mail_2','messages'];
		foreach ($mails as $mail) {
            if (!is_array($props[$mail])) { continue; }
			foreach ($props[$mail] as $key=>$val) {

                // remove unwanted whitespace between closing and opening groups from email
                $count = 1;
                while ($count) {
                    $val = preg_replace(WPCF7CF_REGEX_MAIL_UNWANTED_WHITESPACE, '$1$2', $val, -1, $count);
                }

                // remove hiddden groups from email
                $parser = new Wpcf7cfMailParser($val, $this->visible_groups, $this->hidden_groups, $this->repeaters, $_POST);
				$props[$mail][$key] = $parser->getParsedMail();
            }
        }


        //$props['mail']['body'] = 'xxx';
		$form->set_properties($props);
	}

    function hide_hidden_mail_fields_regex_callback ( $matches ) {
        $name = $matches[1];
        $content = $matches[2];
        if ( in_array( $name, $this->hidden_groups ) ) {
            // The tag name represents a hidden group, so replace everything from [tagname] to [/tagname] with nothing
            return '';
        } elseif ( in_array( $name, $this->visible_groups ) ) {
            // The tag name represents a visible group, so remove the tags themselves, but return everything else
            // instead of just returning the $content, return the preg_replaced content :)
            return preg_replace_callback(WPCF7CF_REGEX_MAIL_GROUP, array($this, 'hide_hidden_mail_fields_regex_callback'), $content );
        } else {
            // The tag name doesn't represent a group that was used in the form. Leave it alone (return the entire match).
            return $matches[0];
        }
    }

    public static function parse_conditions($string, $format='array') {
        // Parse stuff like "show [g1] if [field] equals 2" to Array

        preg_match_all(WPCF7CF_REGEX_CONDITIONS, $string, $matches);

        $conditions = [];

        $prev_then_field = '';
        foreach ($matches[0] as $i=>$line) {
            $then_field = $matches[1][$i];
            $if_field   = $matches[2][$i];
            $operator   = $matches[3][$i];
            $if_value   = $matches[4][$i];

            $index = count($conditions);

            if ($then_field == '') {
                $index = $index -1;
                $then_field = $prev_then_field;
            } else {
                $conditions[$index]['then_field'] = $then_field;
            }

            $conditions[$index]['and_rules'][] = [
                'if_field' => $if_field,
                'operator' => $operator,
                'if_value' => $if_value,
            ];

            $prev_then_field = $then_field;

        }

        $conditions = array_values($conditions);

        if ($format == 'array') {
            return $conditions;
        } else if ($format == 'json') {
            return json_encode($conditions);
        }
    }

    /**
     * load the conditions from the form's post_meta
     *
     * @param string $form_id
     * @return array
     */
    public static function getConditions($form_id) {
        // make sure conditions are an array.
        $options = get_post_meta($form_id,'wpcf7cf_options',true);
        if (!is_array($options)) {
            return array();
        }
        if (isset($options[0]['if_field'])) {
            // this is a legacy condition format, so we need to convert it to the new format
            foreach ($options as $i => $option) {
                $options[$i]['and_rules'] = [
                    [
                        'if_field' => $option['if_field'],
                        'operator' => $option['operator'],
                        'if_value' => $option['if_value'],
                    ]
                ];
                unset($options[$i]['if_field']);
                unset($options[$i]['operator']);
                unset($options[$i]['if_value']);
            }
        }
        return $options; // the meta key 'wpcf7cf_options' is a bit misleading at this point, because it only holds the form's conditions, no other options/settings
    }

    /**
     * load the conditions from the form's post_meta as plain text
     *
     * @param string $form_id
     * @return void
     */
    public static function getConditionsPlainText($form_id) {
        return CF7CF::serializeConditions(CF7CF::getConditions($form_id));
    }

    public static function serializeConditions($array) {

        $lines = [];

        foreach ($array as $entry) {
            $then_field = $entry['then_field'];
            $and_rules = $entry['and_rules'];
            $indent = strlen($then_field) + 4;
            foreach ($and_rules as $i => $rule) {
                $if_field = $rule['if_field'];
                $operator = $rule['operator'];
                $if_value = $rule['if_value'];

                if ($i == 0) {
                    $lines[] = "show [$then_field] if [$if_field] $operator \"$if_value\"";
                } else {
                    $lines[] = str_repeat(' ',$indent)."and if [$if_field] $operator \"$if_value\"";
                }
            }
        }
        
        return implode("\n", $lines);
    }
    
    /**
     * save the conditions to the form's post_meta
     *
     * @param string $form_id
     * @return void
     */
    public static function setConditions($form_id, $conditions) {
        return update_post_meta($form_id,'wpcf7cf_options',$conditions); // the meta key 'wpcf7cf_options' is a bit misleading at this point, because it only holds the form's conditions, no other options/settings
    }
}

new CF7CF;

add_filter( 'wpcf7_contact_form_properties', 'wpcf7cf_properties', 10, 2 );

function wpcf7cf_properties($properties, $wpcf7form) {
	// Before CF7 5.5.3, this function was called each time we call get_properties() on a contact form. Since CF7 5.5.3 this function is called only once in the WPCF7_ContactForm
	if (!is_admin() || (defined('DOING_AJAX') && DOING_AJAX)) { // TODO: kind of hacky. maybe find a better solution. Needed because otherwise the group tags will be replaced in the editor as well.
        $form = $properties['form'];

	    $form_parts = preg_split('/(\[\/?group(?:\]|\s.*?\]))/',$form, -1,PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);

	    ob_start();

	    $stack = array();

	    foreach ($form_parts as $form_part) {
	    	if (substr($form_part,0,7) == '[group ') {
	    		$tag_parts = explode(' ',rtrim($form_part,']'));

	    		array_shift($tag_parts);

	    		$tag_id = $tag_parts[0];
	    		$tag_html_type = 'div';
	    		$tag_html_data = array();
                $tag_classes = array();

	    		foreach ($tag_parts as $i => $tag_part) {
                    if ($i==0) continue;
                    $tag_part = explode(':',$tag_part);
					if ($tag_part[0] == 'inline') {
                        $tag_html_type = 'span';
                    } else if ($tag_part[0] == 'clear_on_hide') {
                        $tag_html_data[] = 'data-clear_on_hide';
                    } else if ($tag_part[0] == 'disable_on_hide' && WPCF7CF_IS_PRO) {
                        $tag_html_data[] = 'data-disable_on_hide';
                    } else if ($tag_part[0] == 'class') {
                        $tag_classes[] = $tag_part[1] ?? '';
                    }
			    }

                $class_html = 'class="' . implode(' ', array_filter($tag_classes)) . '"';

			    array_push($stack,$tag_html_type);

			    echo '<'.$tag_html_type.' data-id="'.$tag_id.'" data-orig_data_id="'.$tag_id.'" '.implode(' ',$tag_html_data).' '.$class_html.' data-class="wpcf7cf_group">';
		    } else if ($form_part == '[/group]') {
	    		echo '</'.array_pop($stack).'>';
		    } else {
	    		echo $form_part;
		    }
	    }

        $properties['form'] = ob_get_clean();
    }
    return $properties;
}

add_filter('wpcf7_form_hidden_fields', 'wpcf7cf_form_hidden_fields',10,1);

function wpcf7cf_form_hidden_fields($hidden_fields) {

    $current_form = wpcf7_get_current_contact_form();
    $current_form_id = $current_form->id();

    $options = array(
        'form_id' => $current_form_id,
        'conditions' => CF7CF::getConditions($current_form_id),
        'settings' => wpcf7cf_get_settings()
    );

    unset($options['settings']['license_key']); // don't show license key in the source code duh.

	return array_merge($hidden_fields, array(
        '_wpcf7cf_hidden_group_fields' => '[]',
        '_wpcf7cf_hidden_groups' => '[]',
        '_wpcf7cf_visible_groups' => '[]',
        '_wpcf7cf_repeaters' => '[]',
        '_wpcf7cf_steps' => '{}',
        '_wpcf7cf_options' => ''.json_encode($options),
    ));
}

function wpcf7cf_endswith($string, $test) {
    $strlen = strlen($string ?? '');
    $testlen = strlen($test ?? '');
    if ($testlen > $strlen) return false;
    return substr_compare($string, $test, $strlen - $testlen, $testlen) === 0;
}

add_filter( 'wpcf7_form_tag_data_option', 'wpcf7cf_form_tag_data_option', 10, 3 );

function wpcf7cf_form_tag_data_option($output, $args, $nog) {
	$data = array();
	return $data;
}

/* Scripts & Styles */

function wpcf7cf_load_js() {
	return apply_filters( 'wpcf7cf_load_js', WPCF7CF_LOAD_JS );
}

function wpcf7cf_load_css() {
	return apply_filters( 'wpcf7cf_load_css', WPCF7CF_LOAD_CSS );
}

add_action( 'wp_enqueue_scripts', 'wpcf7cf_do_enqueue_scripts', 20, 0 );

function wpcf7cf_do_enqueue_scripts() {
	if ( wpcf7cf_load_js() ) {
		wpcf7cf_enqueue_scripts();
	}

	if ( wpcf7cf_load_css() ) {
		wpcf7cf_enqueue_styles();
	}
}

function wpcf7cf_enqueue_scripts() {
	if (is_admin()) return;
	wp_enqueue_script('wpcf7cf-scripts', plugins_url('js/scripts.js', __FILE__), array('jquery', 'contact-form-7'), WPCF7CF_VERSION, true);
	wp_localize_script('wpcf7cf-scripts', 'wpcf7cf_global_settings',
		array(
			'ajaxurl' => admin_url('admin-ajax.php'),
		)
	);

}

function wpcf7cf_enqueue_styles() {
	if (is_admin()) return;
	wp_enqueue_style('cf7cf-style', plugins_url('style.css', __FILE__), array(), WPCF7CF_VERSION);
}

// Make sure CF7 doesn't target any disabled fields for validation
// (HTML standard: "disabled fields don't get submitted", so no need to validate them)
add_filter( 'wpcf7_feedback_response', function($response, $result) {
    foreach ($response['invalid_fields'] as $i => $inv) {
        if (isset($response['invalid_fields'][$i]['into'])) {
            $response['invalid_fields'][$i]['into'] .= ':not(.wpcf7cf-disabled)';
        }
    }
    return $response;
}, 2, 10 );