/**
 * Visual Builder JavaScript
 *
 * Handles overlay lifecycle, bidirectional field sync, iframe updates, and saving.
 */

(function($) {
	'use strict';

	var VisualBuilder = {
		$overlay: null,
		$iframe: null,
		$openButton: null,
		$closeButton: null,
		$saveButton: null,
		$loading: null,

		settings: {},
		syncManifest: {},
		debounceTimer: null,
		actionsSyncTimer: null,
		isOpen: false,
		isSyncing: false,
		previewStep: 1,

		init: function() {
			this.cacheElements();
			this.syncManifest = (typeof giveasapVisualBuilder !== 'undefined' && giveasapVisualBuilder.syncManifest) ? giveasapVisualBuilder.syncManifest : {};
			this.bindEvents();
			this.bindSyncEvents();
			this.initColorPickers();
			this.initMediaUploaders();

			if (typeof giveasapVisualBuilder !== 'undefined' && giveasapVisualBuilder.settings) {
				this.settings = $.extend({}, giveasapVisualBuilder.settings);
			}
		},

		cacheElements: function() {
			this.$overlay = $('#giveasap-visual-builder-overlay');
			this.$iframe = $('#giveasap-builder-iframe');
			this.$openButton = $('#giveasap-open-visual-builder');
			this.$closeButton = $('#giveasap-builder-close');
			this.$saveButton = $('#giveasap-builder-save');
			this.$loading = $('.giveasap-preview-loading');
		},


		giveasap_initialize_code_editor: function() {
			var self = this;
			var $context = this.$overlay && this.$overlay.length ? this.$overlay : $(document);

			$context.find('.sg-code-editor.initialized').each(function(i, el) {
				if (el.CodeMirror) {
					el.CodeMirror.refresh();
				}
				// If el.CodeMirror is missing we leave the class intact — removing it would
				// cause the init path below to fire a fresh instance on every tab switch.
			});

			if (typeof wp === 'undefined' || typeof wp.codeEditor === 'undefined') {
				return;
			}

			$context.find('.sg-code-editor:not(.initialized)').each(function() {
				var editorSettings = wp.codeEditor.defaultSettings ? _.clone(wp.codeEditor.defaultSettings) : {};
				var mode = $(this).attr('data-mode');
				if (typeof mode === 'undefined') {
					mode = false;
				}
				editorSettings.codemirror = _.extend({}, editorSettings.codemirror, {
					indentUnit: 2,
					tabSize: 2
				});
				if (mode) {
					editorSettings.codemirror.mode = mode;
				}
				wp.codeEditor.initialize($(this), editorSettings);
				$(this).addClass('initialized');
			});
		},

		bindEvents: function() {
      var self = this;

      $('.giveasap-preview-tab').on('click', function() {
        $('.giveasap-preview-tab').removeClass('active');
        $(this).addClass('active');
        self.previewStep = parseInt($(this).data('preview-step'), 10);
        self.updatePreview();
      });

      $(document).on("giveasap:prize_image_updated", function(e, data) {
        console.log(data);
      });

      // admin.js appends the new action to the metabox container (the source of
      // truth) and fires this event. Re-clone the whole list into the overlay so
      // IDs get the -vb suffix and widgets are re-initialized, instead of
      // appending the raw template HTML (which would duplicate IDs).
      $(document.body).on("sg_action_added", function() {
        self.syncRepeater('actions_repeater', 'metabox', 'overlay');

        if (self.isOpen) {
          self.collectSettings();
          self.debouncedUpdatePreview();
        }
      });

      $(document).on('click', '#sgAddPrizesFieldVisual', function() {
        var tmpl = wp.template('sg-prizes-field');
        var $container = self.$overlay.find('.sg-prizes-field-container');
        var count = $container.children('.sg-form-field').length;
        var html = tmpl({ index: count });
        $container.append(html);
        $container.find('.sg-form-field').last().addClass('open');
        self.syncRepeater('prizes_repeater', 'overlay', 'metabox');
        self.collectSettings();
        self.debouncedUpdatePreview();

        $(document.body).triggerHandler("sg_prizes_field_added");
      });

      self.$overlay.on('click', '.sg-delete-prizes-field', function() {
        setTimeout(function() {
          self.syncRepeater('prizes_repeater', 'overlay', 'metabox');
          self.collectSettings();
          self.debouncedUpdatePreview();
        }, 0);
      });

      // Add form field row in the VB overlay using the metabox template.
      // We append directly to both containers instead of calling syncRepeater,
      // so existing rows are never cleared/re-cloned (which would lose live values).
      $(document).on('click', '#sgAddFormFieldVisual', function() {
        var tmpl = wp.template('sg-form-field');
        // Use getContextElements (by sync-key, not class) so we get exactly one container each
        var $metaboxContainer = self.getContextElements('form_fields_repeater', 'metabox').first();
        var $overlayContainer = self.getContextElements('form_fields_repeater', 'overlay').first();

        if (!$metaboxContainer.length || !$overlayContainer.length) {
          return;
        }

        // Drive the index from the metabox (source of truth); overlay is always kept in sync
        var count = $metaboxContainer.children('.sg-form-field').length;

        // Add to metabox first (canonical IDs, no -vb suffix)
        $metaboxContainer.append($(tmpl({ index: count })));

        // Add to overlay with -vb suffix on IDs/for to avoid ID collisions
        var $overlayRow = $(tmpl({ index: count }));
        $overlayRow.find('[id]').each(function() {
          var id = $(this).attr('id');
          if (id && id.indexOf('-vb') === -1) {
            $(this).attr('id', id + '-vb');
          }
        });
        $overlayRow.find('[for]').each(function() {
          var forAttr = $(this).attr('for');
          if (forAttr && forAttr.indexOf('-vb') === -1) {
            $(this).attr('for', forAttr + '-vb');
          }
        });
        $overlayContainer.append($overlayRow);
        $overlayContainer.find('.sg-form-field').last().addClass('open');

        self.collectSettings();
        self.debouncedUpdatePreview();
      });

      // Delete a form field row from the overlay.
      // Stop propagation so admin.js's global handler (which uses $(.sg-form-fields-container)
      // without scoping and would corrupt the metabox) doesn't fire.
      // We remove the row and sync to metabox ourselves.
      self.$overlay.on('click', '.sg-delete-form-field', function(e) {
        e.stopPropagation();
        var $row = $(this).closest('.sg-form-field');
        $row.remove();
        setTimeout(function() {
          self.syncRepeater('form_fields_repeater', 'overlay', 'metabox');
          self.collectSettings();
          self.debouncedUpdatePreview();
        }, 0);
      });

      // Add webhook row in the VB overlay
      $(document).on('click', '#sgAddWebhookFieldVisual', function() {
        var tmpl = wp.template('sg-webhooks-field');
        var $metaboxContainer = self.getContextElements('webhooks_repeater', 'metabox').first();
        var $overlayContainer = self.getContextElements('webhooks_repeater', 'overlay').first();

        if (!$metaboxContainer.length || !$overlayContainer.length) {
          return;
        }

        var count = $metaboxContainer.children('.sg-form-field').length;

        // Add to metabox first (canonical IDs, no -vb suffix)
        $metaboxContainer.append($(tmpl({ index: count })));

        // Add to overlay with -vb suffix on IDs/for to avoid ID collisions
        var $overlayRow = $(tmpl({ index: count }));
        $overlayRow.find('[id]').each(function() {
          var id = $(this).attr('id');
          if (id && id.indexOf('-vb') === -1) {
            $(this).attr('id', id + '-vb');
          }
        });
        $overlayRow.find('[for]').each(function() {
          var forAttr = $(this).attr('for');
          if (forAttr && forAttr.indexOf('-vb') === -1) {
            $(this).attr('for', forAttr + '-vb');
          }
        });
        $overlayContainer.append($overlayRow);
        $overlayContainer.find('.sg-form-field').last().addClass('open');

        self.collectSettings();
        self.debouncedUpdatePreview();
      });

      // Delete a webhook row from the overlay
      self.$overlay.on('click', '.sg-delete-webhook-field', function(e) {
        e.stopPropagation();
        var $row = $(this).closest('.sg-form-field');
        $row.remove();
        setTimeout(function() {
          self.syncRepeater('webhooks_repeater', 'overlay', 'metabox');
          self.collectSettings();
          self.debouncedUpdatePreview();
        }, 0);
      });

      // Add webhook header row in the overlay
      self.$overlay.on('click', '.sg-add-webhook-field-headers-row', function(e) {
        e.stopPropagation();
        var tmpl = wp.template('sg-webhook-headers-field');
        var index = $(this).parents('.sg-form-field').attr('data-index');
        var html = tmpl({ index: index });
        $(this).parents('.sg-field-headers').find('.sg-field-headers-rows').append(html);
        setTimeout(function() {
          self.syncRepeater('webhooks_repeater', 'overlay', 'metabox');
        }, 0);
      });

      // Delete a webhook header row from the overlay
      self.$overlay.on('click', '.sg-delete-webhook-header-field', function(e) {
        e.stopPropagation();
        $(this).parents('.sg-field-headers-row').remove();
        setTimeout(function() {
          self.syncRepeater('webhooks_repeater', 'overlay', 'metabox');
        }, 0);
      });

      $(document).on(
        "click",
        ".giveasap-builder-sidebar li.sg-metabox a",
        function (e) {
          e.preventDefault();
          $(".giveasap-builder-sidebar-options .sg-metabox-fields").addClass(
            "hidden"
          );
          $(
            ".giveasap-builder-sidebar .sidebar-sections li.active"
          ).removeClass("active");
          var target = $(this).attr("href");
          $(target).removeClass("hidden");
          $(this).parent().addClass("active");
          $(target)
            .find(".wp-editor-wrap")
            .each(function () {
              var frame = $(this).find("iframe"),
                height = frame.height();
              if (height < 200) {
                frame.height(200);
              }
            });
          if ($(target).find(".sg-code-editor")) {
            self.giveasap_initialize_code_editor();
          }
		  if ($(target).find(".sg-select-search")) {
			$(document.body).trigger("sg_select_search_shown");
		  }
        }
      );

      this.$openButton.on("click", function (e) {
        e.preventDefault();
        self.open();
      });

      this.$closeButton.on("click", function (e) {
        e.preventDefault();
        self.close();
      });

      this.$saveButton.on("click", function (e) {
        e.preventDefault();
        self.saveSettings();
      });

      $(document).on("keydown", function (e) {
        if (e.key === "Escape" && self.isOpen) {
          self.close();
        }
      });
    },

		bindSyncEvents: function() {
			var self = this;

			$(document).on('change input codemirror:change', '[data-giveasap-sync-key]', function() {
				if (self.isSyncing) {
					return;
				}

				self.syncFromElement($(this));
			});

			// Sync repeater field edits from overlay back to the matching metabox input by name
		this.$overlay.on('change input', '.sg-prizes-field-container input, .sg-prizes-field-container select, .sg-prizes-field-container textarea, .sg-form-fields-container input, .sg-form-fields-container select, .sg-form-fields-container textarea, .sg-webhooks-fields-container input, .sg-webhooks-fields-container select, .sg-webhooks-fields-container textarea', function() {
				if (self.isSyncing || !self.isOpen) {
					return;
				}

				var name = $(this).attr('name');
				if (name) {
					// [] names (e.g. headers[key][]) cannot be matched 1:1 by name —
					// they would match ALL inputs with that name across all rows.
					// For these, sync the entire webhooks repeater instead.
					if (name.slice(-2) === '[]' && $(this).closest('.sg-webhooks-fields-container').length) {
						self.syncRepeater('webhooks_repeater', 'overlay', 'metabox');
					} else {
						var $metaboxField = $('[name="' + name + '"]').not(self.$overlay.find('[name="' + name + '"]'));
						if ($metaboxField.length) {
							$metaboxField.val($(this).val());
							if ($(this).is(':checkbox') || $(this).is(':radio')) {
								$metaboxField.prop('checked', $(this).is(':checked'));
							}
						}
					}
				}

				self.collectSettings();
				self.debouncedUpdatePreview();
			});

			// Sync webhook header edits from metabox to overlay while overlay is open
			$(document).on('change input', '.sg-webhooks-fields-container input, .sg-webhooks-fields-container select', function() {
				if (self.isSyncing || !self.isOpen || self.isOverlayElement($(this))) {
					return;
				}
				self.syncRepeater('webhooks_repeater', 'metabox', 'overlay');
				self.collectSettings();
				self.debouncedUpdatePreview();
			});

			// Sync action edits from overlay back to the metabox. Action rows hold
			// field types that cannot be matched 1:1 by input name (ajax select2
			// whose chosen <option> only exists in one context, multioption []
			// arrays, color pickers), so any edit re-clones the whole repeater.
			// irischange is fired by the color picker, which updates the input
			// value programmatically without a native change event.
			this.$overlay.on('change input irischange', '#sgEntriesActionsVisual input, #sgEntriesActionsVisual select, #sgEntriesActionsVisual textarea', function() {
				if (self.isSyncing || !self.isOpen) {
					return;
				}

				clearTimeout(self.actionsSyncTimer);
				self.actionsSyncTimer = setTimeout(function() {
					self.syncRepeater('actions_repeater', 'overlay', 'metabox');
					self.collectSettings();
					self.debouncedUpdatePreview();
				}, 300);
			});

			// Removing an action in the overlay: admin.js's document-level handler
			// removes the row and re-indexes; afterwards clone the result back to
			// the metabox so the action is removed on both ends.
			this.$overlay.on('click', '.sg-admin-action-remove', function() {
				setTimeout(function() {
					self.syncRepeater('actions_repeater', 'overlay', 'metabox');
					self.collectSettings();
					self.debouncedUpdatePreview();
				}, 0);
			});

			// Sync prize image additions/removals between metabox and overlay
			$(document).on('giveasap:prize_image_updated', function(e, data) {
				if (!self.isOpen) {
					return;
				}
				self.syncPrizeImage(data);
			});
		},

		syncPrizeImage: function(data) {
			if (!data || !data.containerId) {
				return;
			}

			var containerId = data.containerId;
			var isVbSource = /-vb$/.test(containerId);
			var counterpartId = isVbSource ? containerId.replace(/-vb$/, '') : containerId + '-vb';

			var $counterpartContainer = $('#' + counterpartId);
			var $counterpartInput = $('#' + counterpartId + '_input');

			if (!$counterpartContainer.length || !$counterpartInput.length) {
				return;
			}

			// Parse the cloned image items and fix data-gallery refs for the counterpart context
			var $items = $(data.html);
			$items.find('[data-gallery]').addBack('[data-gallery]').each(function() {
				var gallery = $(this).attr('data-gallery');
				if (isVbSource) {
					gallery = gallery.replace(/-vb$/, '');
				} else if (gallery.indexOf('-vb') === -1) {
					gallery = gallery + '-vb';
				}
				$(this).attr('data-gallery', gallery);
			});

			this.isSyncing = true;
			$counterpartContainer.empty().append($items);
			$counterpartInput.val(data.ids);
			this.isSyncing = false;

			this.collectSettings();
			this.debouncedUpdatePreview();
		},

		initColorPickers: function() {
			this.$overlay.find('.giveasap-color-picker').wpColorPicker({
				change: function(event) {
					setTimeout(function() {
						$(event.target).trigger('change');
					}, 50);
				},
				clear: function() {
					$(this).trigger('change');
				}
			});
		},

		initMediaUploaders: function() {
			this.$overlay.find('.giveasap-upload-media').on('click', function(e) {
				e.preventDefault();

				var $container = $(this).closest('.giveasap-media-uploader');
				var $preview = $container.find('.giveasap-media-preview');
				var $input = $container.find('input[type="hidden"]');
				var $removeBtn = $container.find('.giveasap-remove-media');

				var fileFrame = wp.media({
					title: 'Select Image',
					button: {
						text: 'Use this image'
					},
					multiple: false
				});

				fileFrame.on('open', function() {
					$('body').addClass('giveasap-builder-media-open');
				});

				fileFrame.on('close', function() {
					$('body').removeClass('giveasap-builder-media-open');
				});

				fileFrame.on('select', function() {
					var attachment = fileFrame.state().get('selection').first().toJSON();
					var previewUrl = attachment.sizes && attachment.sizes.thumbnail ? attachment.sizes.thumbnail.url : attachment.url;

					$input.val(attachment.id);
					$input.attr('data-giveasap-preview-url', previewUrl);
					$preview.html('<img src="' + previewUrl + '" alt="" />');
					$removeBtn.removeClass('hidden');

					$input.trigger('change');
				});

				fileFrame.open();
			});

			this.$overlay.find('.giveasap-remove-media').on('click', function(e) {
				e.preventDefault();

				var $container = $(this).closest('.giveasap-media-uploader');
				var $preview = $container.find('.giveasap-media-preview');
				var $input = $container.find('input[type="hidden"]');

				$input.val('');
				$input.removeAttr('data-giveasap-preview-url');
				$preview.empty();
				$(this).addClass('hidden');

				$input.trigger('change');
			});
		},

		open: function() {
			this.isOpen = true;
			this.$overlay.show();
			$('body').addClass('giveasap-builder-open');

			this.syncOverlayFromMetabox();
			this.syncActiveSection();
			this.initVbEditors();
			this.initVbCodeEditors();
			this.collectSettings();
			this.updatePreview();

			this.$overlay.find('.giveasap-builder-header').focus();
		},

		close: function() {
			this.removeVbCodeEditors();
			this.removeVbEditors();
			this.isOpen = false;
			this.$overlay.hide();
			$('body').removeClass('giveasap-builder-open giveasap-builder-media-open');
		},

		// Open the builder on the section that is active in the metabox (the
		// server renders the first section active, ignoring the metabox state).
		// Falls back to the first sidebar section (General) when there is no
		// match. Triggers a real click so the sidebar handler runs its
		// hidden/active toggling and editor/select re-initialization.
		syncActiveSection: function() {
			var $sidebarLinks = this.$overlay.find('.giveasap-builder-sidebar .sidebar-sections li.sg-metabox a');
			if (!$sidebarLinks.length) {
				return;
			}

			var $target = $();
			var metaboxHref = $('.sg-metaboxes li.sg-metabox.active a').first().attr('href') || '';

			if (metaboxHref.indexOf('#sg-metabox-fields-') === 0) {
				var slug = metaboxHref.replace('#sg-metabox-fields-', '');
				$target = $sidebarLinks.filter('[href="#sg-metabox-visual-fields-' + slug + '"]');
			}

			if (!$target.length) {
				$target = $sidebarLinks.first();
			}

			$target.trigger('click');
		},

		syncFromElement: function($element) {
			var syncKey = $element.data('giveasap-sync-key');
			if (!syncKey) {
				return;
			}

			var sourceContext = this.isOverlayElement($element) ? 'overlay' : 'metabox';
			var targetContext = sourceContext === 'overlay' ? 'metabox' : 'overlay';
			var sourceValue = this.readContextValue(syncKey, sourceContext);
			var targetValue = this.transformValue(syncKey, sourceValue, sourceContext, targetContext);

			this.writeContextValue(syncKey, targetContext, targetValue, true);

			if (sourceContext === 'overlay' || (sourceContext === 'metabox' && this.isOpen)) {
				this.collectSettings();
				this.debouncedUpdatePreview();
			}
		},

		syncOverlayFromMetabox: function() {
			var self = this;
			var keys = this.getOverlaySyncKeys();

			$.each(keys, function(index, syncKey) {
				var adapter = self.getAdapterForKey(syncKey, self.getContextElements(syncKey, 'overlay'));
				if (adapter === 'repeater') {
					self.syncRepeater(syncKey, 'metabox', 'overlay');
					return;
				}

				if (!self.getContextElements(syncKey, 'metabox').length) {
					return;
				}

				var metaboxValue = self.readContextValue(syncKey, 'metabox');
				var overlayValue = self.transformValue(syncKey, metaboxValue, 'metabox', 'overlay');
				self.writeContextValue(syncKey, 'overlay', overlayValue, false);
			});
		},

		syncMetaboxFromOverlay: function() {
			var self = this;
			var keys = this.getOverlaySyncKeys();

			$.each(keys, function(index, syncKey) {
				var adapter = self.getAdapterForKey(syncKey, self.getContextElements(syncKey, 'overlay'));
				if (adapter === 'repeater') {
					self.syncRepeater(syncKey, 'overlay', 'metabox');
					return;
				}

				if (!self.getContextElements(syncKey, 'metabox').length) {
					return;
				}

				var overlayValue = self.readContextValue(syncKey, 'overlay');
				var metaboxValue = self.transformValue(syncKey, overlayValue, 'overlay', 'metabox');
				self.writeContextValue(syncKey, 'metabox', metaboxValue, true);
			});
		},

		syncRepeater: function(syncKey, sourceContext, targetContext) {
			var self = this;
			var $source = this.getContextElements(syncKey, sourceContext).first();
			var $target = this.getContextElements(syncKey, targetContext).first();

			if (!$source.length || !$target.length) {
				return;
			}

			// Repeaters default to .sg-form-field rows; containers with a different
			// row markup (e.g. actions use .sg-admin-action) declare it on the element.
			var rowSelector = $source.data('giveasap-repeater-row') || '.sg-form-field';
			var sanitized = false;

			$target.empty();
			$source.find(rowSelector).each(function() {
				// Flush current DOM property values (typed text, checked state, selected option)
				// to HTML attributes so clone() captures the live state, not just the original
				// server-rendered attribute values.
				self.setHtmlFieldsToAttributes($(this));
				var $clone = $(this).clone(true, true);

				if (self.sanitizeWidgetMarkup($clone)) {
					sanitized = true;
				}

				if (targetContext === 'overlay') {
					// Suffix all IDs so they don't collide with the metabox's identical IDs.
					// The overlay PHP render already uses the -vb suffix, so cloned rows
					// must match that pattern.
					$clone.find('[id]').each(function() {
						var id = $(this).attr('id');
						if ( !/-vb(_input)?$/.test(id) ) {
							$(this).attr('id', id.replace(/_input$/, '') + (id.indexOf('_input') !== -1 ? '-vb_input' : '-vb'));
						}
					});
					$clone.find('[data-gallery]').each(function() {
						var gallery = $(this).attr('data-gallery');
						if ( gallery && gallery.indexOf('-vb') === -1 ) {
							$(this).attr('data-gallery', gallery + '-vb');
						}
					});
					$clone.find('[for]').each(function() {
						var forAttr = $(this).attr('for');
						if ( forAttr && forAttr.indexOf('-vb') === -1 ) {
							$(this).attr('for', forAttr + '-vb');
						}
					});
				}

				if (targetContext === 'metabox') {
					// Strip -vb suffix so metabox rows always use canonical (no-suffix) IDs.
					// Rows cloned from the overlay carry -vb suffixes; the metabox must not.
					$clone.find('[id]').each(function() {
						var id = $(this).attr('id');
						if (id) {
							$(this).attr('id', id.replace(/-vb_input$/, '_input').replace(/-vb$/, ''));
						}
					});
					$clone.find('[data-gallery]').each(function() {
						var gallery = $(this).attr('data-gallery');
						if (gallery) {
							$(this).attr('data-gallery', gallery.replace(/-vb$/, ''));
						}
					});
					$clone.find('[for]').each(function() {
						var forAttr = $(this).attr('for');
						if (forAttr) {
							$(this).attr('for', forAttr.replace(/-vb$/, ''));
						}
					});
				}

				$target.append($clone);
			});

			if (targetContext === 'overlay') {
				this.reinitRepeaterInOverlay(syncKey, $target);
			}

			if (sanitized) {
				// Re-create the widgets that sanitizeWidgetMarkup() stripped from
				// the clones. The init handlers live in admin.js and are idempotent.
				$(document.body).trigger('sg-color-picker-init');
				$(document.body).trigger('sg-enhanced-select-search-init');
			}
		},

		// Cloned rows can carry initialized widget markup (wpColorPicker wrappers,
		// select2 containers) whose behavior stays bound to the source elements.
		// Strip it back to the bare inputs so the widgets can be re-initialized
		// in the target context. Returns true when anything was stripped.
		sanitizeWidgetMarkup: function($clone) {
			var sanitized = false;

			$clone.find('.wp-picker-container').each(function() {
				var $input = $(this).find('input.sg-color-picker').first();
				if ($input.length) {
					$input.removeClass('wp-color-picker').removeAttr('style');
					$(this).replaceWith($input[0].outerHTML);
				} else {
					$(this).remove();
				}
				sanitized = true;
			});

			$clone.find('.select2-container').each(function() {
				$(this).remove();
				sanitized = true;
			});

			$clone.find('.select2-hidden-accessible').each(function() {
				$(this)
					.removeClass('select2-hidden-accessible enhanced')
					.removeAttr('aria-hidden tabindex style data-select2-id')
					.removeData('select2');
				sanitized = true;
			});

			return sanitized;
		},

		setHtmlFieldsToAttributes: function($container) {
			$container.find('input, textarea').each(function() {
				$(this).attr('value', $(this).val());
			});
			$container.find('input[type="checkbox"], input[type="radio"]').each(function() {
				if ($(this).is(':checked')) {
					$(this).attr('checked', 'checked');
				} else {
					$(this).removeAttr('checked');
				}
			});
			// Sync current selected option to the selected attribute on <option> elements
			$container.find('select').each(function() {
				var currentVal = $(this).val();
				$(this).find('option').each(function() {
					if ($(this).val() === currentVal) {
						$(this).attr('selected', 'selected');
					} else {
						$(this).removeAttr('selected');
					}
				});
			});
		},

		resetRepeaterFieldsIndex: function($container, fieldPrefix) {
			var self = this;
			var escapedPrefix = fieldPrefix.replace(/[\[\]\\^$.*+?{}()|]/g, '\\$&');
			var index = 0;
			$container.children().each(function() {
				var _index = parseInt($(this).attr('data-index'));
				if (_index !== index) {
					self.setHtmlFieldsToAttributes($(this));
					var html = $(this).html();
					var reg  = new RegExp(escapedPrefix + '\\[' + _index + '\\]', 'g');
					$(this).html(html.replace(reg, fieldPrefix + '[' + index + ']'));
					$(this).attr('data-index', index);
				}
				index++;
			});
		},

		resetPrizesFieldsIndex: function($container) {
			this.resetRepeaterFieldsIndex($container, 'prizes');
		},

		reinitRepeaterInOverlay: function(syncKey, $container) {
			var self = this;

			// Derive the field-name prefix from the first named input in the container.
			// e.g. name="form_fields[0][label]" → "form_fields"
			// e.g. name="prizes[0][title]"      → "prizes"
			var fieldPrefix = syncKey.replace(/_repeater$/, '');
			var $firstInput = $container.find('input[name], select[name], textarea[name]').first();
			if ($firstInput.length) {
				var nameAttr = $firstInput.attr('name');
				var match = nameAttr.match(/^([^\[]+)\[/);
				if (match) {
					fieldPrefix = match[1];
				}
			}

			if ($.fn.sortable) {
				if ($container.data('ui-sortable')) {
					$container.sortable('destroy');
				}
				$container.sortable({
					placeholder: 'ui-state-highlight',
					handle: $container.data('giveasap-repeater-handle') || '.sg-form-row-handle',
					update: function() {
						self.resetRepeaterFieldsIndex($container, fieldPrefix);
						self.syncRepeater(syncKey, 'overlay', 'metabox');
						self.collectSettings();
						self.debouncedUpdatePreview();
					}
				});
			}
		},

		getOverlaySyncKeys: function() {
			var keys = [];
			this.$overlay.find('[data-giveasap-sync-key]').each(function() {
				var key = $(this).data('giveasap-sync-key');
				if (key && $.inArray(key, keys) === -1) {
					keys.push(key);
				}
			});
			return keys;
		},

		isOverlayElement: function($element) {
			return $element.closest('#giveasap-visual-builder-overlay').length > 0;
		},

		getContextElements: function(syncKey, context) {
			var selector = '[data-giveasap-sync-key="' + syncKey + '"]';
			if (context === 'overlay') {
				return this.$overlay.find(selector);
			}

			return $(selector).not(this.$overlay.find(selector));
		},

		getAdapterForKey: function(syncKey, $elements) {
			if ($elements && $elements.length) {
				var dataAdapter = $elements.first().data('giveasap-adapter');
				if (dataAdapter) {
					return dataAdapter;
				}
			}

			if (this.syncManifest[syncKey] && this.syncManifest[syncKey].adapter) {
				return this.syncManifest[syncKey].adapter;
			}

			if ($elements && $elements.length) {
				var type = ($elements.first().attr('type') || '').toLowerCase();
				if (type === 'checkbox' || type === 'radio') {
					return type;
				}
			}

			return 'text';
		},

		readContextValue: function(syncKey, context) {
			var $elements = this.getContextElements(syncKey, context);
			if (!$elements.length) {
				return '';
			}

			var adapter = this.getAdapterForKey(syncKey, $elements);
			if (adapter === 'checkbox') {
				return $elements.first().is(':checked') ? '1' : '0';
			}

			if (adapter === 'radio') {
				var $checked = $elements.filter(':checked').first();
				return $checked.length ? $checked.val() : '';
			}

			if (adapter === 'select2') {
				var $el = $elements.first();
				return {
					values: $el.val() || [],
					options: $el.find('option').map(function() {
						return { id: $(this).val(), text: $(this).text(), selected: $(this).is(':selected') };
					}).get()
				};
			}

			return $elements.first().val();
		},

		writeContextValue: function(syncKey, context, value, triggerEvents) {
			var self = this;
			var $elements = this.getContextElements(syncKey, context);
			if (!$elements.length) {
				return;
			}

			var adapter = this.getAdapterForKey(syncKey, $elements);
			var normalizedValue = typeof value === 'undefined' || value === null ? '' : value;

			if (adapter !== 'select2' && String(this.readContextValue(syncKey, context)) === String(normalizedValue)) {
				if (adapter === 'image') {
					this.refreshImageUi(syncKey, context, normalizedValue);
				}
				return;
			}

			this.isSyncing = true;

			if (adapter === 'checkbox') {
				$elements.first().prop('checked', this.isTruthy(normalizedValue));
				if (triggerEvents) {
					$elements.first().trigger('change');
				}
			} else if (adapter === 'radio') {
				$elements.each(function() {
					$(this).prop('checked', String($(this).val()) === String(normalizedValue));
				});

				if (triggerEvents) {
					var $checked = $elements.filter(':checked').first();
					if ($checked.length) {
						$checked.trigger('change');
					}
				}
			} else if (adapter === 'select2') {
				var $el = $elements.first();
				var data = normalizedValue;
				if (data && data.options) {
					// Destroy existing select2 before modifying options. The
					// `enhanced` class alone is not proof of a live instance —
					// cloned elements keep the class but select2 instances do
					// not survive cloning, and destroy throws on them.
					if ($el.hasClass('enhanced')) {
						if ($el.data('select2')) {
							$el.select2('destroy');
						} else {
							// Stale marker from a clone: clear it so the
							// re-init below treats the element as fresh.
							$el.removeClass('enhanced select2-hidden-accessible');
						}
					}
					$el.empty();
					$.each(data.options, function(i, opt) {
						var $option = $('<option></option>').val(opt.id).text(opt.text);
						if (opt.selected) {
							$option.prop('selected', true);
						}
						$el.append($option);
					});
					$el.val(data.values);
					// Re-initialize select2
					$(document.body).trigger('sg_select_search_shown');
				}
				if (triggerEvents) {
					$el.trigger('change');
				}
			} else {
				$elements.first().val(normalizedValue);
				if (triggerEvents) {
					$elements.first().trigger('change');
				}
			}

			if (adapter === 'image') {
				this.refreshImageUi(syncKey, context, normalizedValue);
			}

			if (adapter === 'wpeditor') {
				var editorId = $elements.first().attr('id');
				if (editorId && typeof tinymce !== 'undefined' && tinymce.get(editorId)) {
					tinymce.get(editorId).setContent(normalizedValue || '');
					tinymce.get(editorId).save();
				}
			}

			if (adapter === 'code') {
				var el = $elements.first()[0];
				var cm = el.CodeMirror || $(el).data('sg-codemirror');
				if (cm) {
					cm.setValue(normalizedValue || '');
				}
			}

			window.setTimeout(function() {
				self.isSyncing = false;
			}, 0);
		},

		refreshImageUi: function(syncKey, context, value) {
			if (context === 'overlay') {
				var $input = this.getContextElements(syncKey, 'overlay').first();
				var $container = $input.closest('.giveasap-media-uploader');
				var $preview = $container.find('.giveasap-media-preview');
				var $removeBtn = $container.find('.giveasap-remove-media');

				if (!value) {
					$preview.empty();
					$removeBtn.addClass('hidden');
					return;
				}

				var previewUrl = $input.attr('data-giveasap-preview-url');
				if (!previewUrl) {
					var metaboxPreview = this.getMetaboxImagePreviewUrl(syncKey);
					if (metaboxPreview) {
						previewUrl = metaboxPreview;
					}
				}

				if (previewUrl) {
					$preview.html('<img src="' + previewUrl + '" alt="" />');
					$input.attr('data-giveasap-preview-url', previewUrl);
				}

				$removeBtn.removeClass('hidden');
			}

			if (context === 'metabox') {
				var $metaboxInput = this.getContextElements(syncKey, 'metabox').first();
				if (!$metaboxInput.length) {
					return;
				}

				var fieldName = $metaboxInput.attr('name');
				if (!fieldName) {
					return;
				}

				var $list = $('#' + fieldName);
				if (!$list.length) {
					return;
				}

				$list.empty();

				if (!value) {
					return;
				}

				var $overlayInput = this.getContextElements(syncKey, 'overlay').first();
				var overlayPreviewUrl = $overlayInput.length ? $overlayInput.attr('data-giveasap-preview-url') : '';

				if (overlayPreviewUrl) {
					var $li = $(
						'<li tabindex="0" role="checkbox" aria-label="" aria-checked="true" data-id="' + value + '" class="attachment save-ready selected details">' +
						'<div class="attachment-preview js--select-attachment type-image subtype-jpeg portrait">' +
						'<div class="thumbnail"><div class="centered">' +
						'<img src="' + overlayPreviewUrl + '" draggable="false" alt="">' +
						'</div></div></div>' +
						'<button type="button" data-gallery="#' + fieldName + '" class="button-link check remove-sortable-wordpress-gallery-image" tabindex="0">' +
						'<span class="media-modal-icon"></span><span class="screen-reader-text">Deselect</span></button>' +
						'</li>'
					);
					$list.append($li);
					$metaboxInput.attr('data-giveasap-preview-url', overlayPreviewUrl);
				}
			}
		},

		getMetaboxImagePreviewUrl: function(syncKey) {
			var $metaboxInput = this.getContextElements(syncKey, 'metabox').first();
			if (!$metaboxInput.length) {
				return '';
			}

			var cachedUrl = $metaboxInput.attr('data-giveasap-preview-url');
			if (cachedUrl) {
				return cachedUrl;
			}

			var fieldName = $metaboxInput.attr('name');
			if (!fieldName) {
				return '';
			}

			var $list = $('#' + fieldName);
			if (!$list.length) {
				return '';
			}

			var $image = $list.find('img').first();
			return $image.length ? $image.attr('src') : '';
		},

		getTransformName: function(syncKey) {
			var $overlayElement = this.getContextElements(syncKey, 'overlay').first();
			if ($overlayElement.length && $overlayElement.data('giveasap-transform')) {
				return $overlayElement.data('giveasap-transform');
			}

			var $metaboxElement = this.getContextElements(syncKey, 'metabox').first();
			if ($metaboxElement.length && $metaboxElement.data('giveasap-transform')) {
				return $metaboxElement.data('giveasap-transform');
			}

			if (this.syncManifest[syncKey] && this.syncManifest[syncKey].transform) {
				return this.syncManifest[syncKey].transform;
			}

			return '';
		},

		transformValue: function(syncKey, value, sourceContext, targetContext) {
			var transform = this.getTransformName(syncKey);
			if (!transform) {
				return value;
			}

			if (transform === 'page_layout_checkbox_enum') {
				if (sourceContext === 'overlay' && targetContext === 'metabox') {
					return value === 'show_header' ? '1' : '0';
				}

				if (sourceContext === 'metabox' && targetContext === 'overlay') {
					return this.isTruthy(value) ? 'show_header' : 'hide_header';
				}
			}

			return value;
		},

		isTruthy: function(value) {
			return value === true || value === 1 || value === '1' || value === 'true' || value === 'yes' || value === 'on';
		},

		// Flush all active overlay TinyMCE instances to their underlying textareas so
		// that collectSettings() can read the current editor content via .val().
		flushVbEditors: function() {
			if (typeof tinymce === 'undefined') {
				return;
			}
			this.$overlay.find('.giveasap-vb-editor').each(function() {
				var editorId = $(this).attr('id');
				if (editorId) {
					var editor = tinymce.get(editorId);
					if (editor) {
						editor.save();
					}
				}
			});
		},

		// Initialise CodeMirror on every .sg-code-editor textarea inside the overlay.
		// Called from open() AFTER syncOverlayFromMetabox() so the textarea already holds
		// the correct metabox value, which CodeMirror picks up on init (or via setValue).
		initVbCodeEditors: function() {
			if (typeof wp === 'undefined' || typeof wp.codeEditor === 'undefined') {
				return;
			}

			var self = this;
			this.$overlay.find('.sg-code-editor').each(function() {
				var $textarea = $(this);

				if ($textarea.hasClass('initialized')) {
					// Already initialized — push the just-synced textarea value in and refresh.
					var existingCm = this.CodeMirror || $textarea.data('sg-codemirror');
					if (existingCm) {
						existingCm.setValue($textarea.val());
						existingCm.refresh();
					}
					return;
				}

				var editorSettings = wp.codeEditor.defaultSettings ? _.clone(wp.codeEditor.defaultSettings) : {};
				var mode = $textarea.attr('data-mode');
				editorSettings.codemirror = _.extend({}, editorSettings.codemirror, {
					indentUnit: 2,
					tabSize: 2
				});
				if (mode) {
					editorSettings.codemirror.mode = mode;
				}

				var editor = wp.codeEditor.initialize($textarea, editorSettings);
				$textarea.addClass('initialized');

				var cm = editor && editor.codemirror ? editor.codemirror : this.CodeMirror;
				if (cm) {
					$textarea.data('sg-codemirror', cm);
					cm.on('change', function() {
						cm.save();
						$textarea.trigger('codemirror:change');
					});
				}
			});
		},

		// Flush CodeMirror content to underlying textareas so collectSettings() and
		// syncMetaboxFromOverlay() read the current editor content via .val().
		flushVbCodeEditors: function() {
			this.$overlay.find('.sg-code-editor.initialized').each(function() {
				var cm = this.CodeMirror || $(this).data('sg-codemirror');
				if (cm) {
					cm.save();
				}
			});
		},

		// Flush content and sync code editor values back to the matching metabox
		// textarea when the overlay closes, so a subsequent WP "Update" submits the
		// correct value.
		removeVbCodeEditors: function() {
			this.$overlay.find('.sg-code-editor.initialized').each(function() {
				var cm = this.CodeMirror || $(this).data('sg-codemirror');
				if (!cm) { return; }
				cm.save();

				var name = $(this).attr('name');
				if (!name) { return; }

				var value = $(this).val();
				var $metaboxField = $('[name="' + name + '"]').not($(this));
				if ($metaboxField.length) {
					$metaboxField.val(value);
					var metaboxCm = $metaboxField[0].CodeMirror || $metaboxField.data('sg-codemirror');
					if (metaboxCm) {
						metaboxCm.setValue(value);
					}
				}
			});
		},

		// Initialise TinyMCE on every .giveasap-vb-editor textarea inside the overlay.
		// Called from open() AFTER the overlay is visible so the iframe loads correctly.
		//
		// wp.editor.initialize() looks up tinyMCEPreInit.mceInit[editorId] for the full
		// WordPress TinyMCE config.  Because the overlay textarea was never registered via
		// wp_editor() in PHP, that key doesn't exist and the editor inits with almost no
		// settings.  Instead we call tinymce.init() directly, cloning the config that the
		// metabox's wp_editor() already registered (strip the -vb suffix to find it).
		//
		// Sync is handled directly against the metabox TinyMCE instance (or its textarea
		// when the user is on the Text tab) rather than through the generic sync layer,
		// because the metabox textarea generated by wp_editor() never carries the
		// data-giveasap-sync-key attribute that getContextElements() relies on.
		initVbEditors: function() {
			if (typeof tinymce === 'undefined') {
				return;
			}

			this.$overlay.find('.giveasap-vb-editor').each(function() {
				var $textarea = $(this);
				var editorId  = $textarea.attr('id');
				if (!editorId) {
					return;
				}

				// Remove any stale/broken instance left from a previous hidden init.
				if (tinymce.get(editorId)) {
					tinymce.get(editorId).remove();
				}

				// Clone the full WordPress TinyMCE config from the matching metabox
				// editor (registered by wp_editor() in the table render context).
				// Stripping -vb gives us the original field name / editor ID.
				var sourceId = editorId.replace(/-vb$/, '');
				var preInit  = window.tinyMCEPreInit || {};
				var mceInit  = (preInit.mceInit && preInit.mceInit[sourceId])
					? $.extend({}, preInit.mceInit[sourceId])
					: {};

				// Point the cloned config at the overlay textarea.
				mceInit.selector = '#' + editorId;

				// Preserve any existing setup callback then append our own.
				var originalSetup = mceInit.setup;
				mceInit.setup = function(editor) {
					if (typeof originalSetup === 'function') {
						originalSetup(editor);
					}

					// Metabox → overlay: on init pull the current metabox content so
					// any edits made in the metabox before opening VB are reflected.
					editor.on('init', function() {
						var metaboxEditor = tinymce.get(sourceId);
						var content;
						if (metaboxEditor) {
							metaboxEditor.save();
							content = metaboxEditor.getContent();
						} else {
							content = $('#' + sourceId).val();
						}
						if (content) {
							editor.setContent(content);
							editor.save();
						}
					});

					// Overlay → metabox: keep the metabox editor / textarea in sync
					// so the standard WP "Update" submit always has the latest content,
					// and collectSettings() reads the current value via the textarea.
					editor.on('NodeChange Change KeyUp', function() {
						editor.save();
						var content = editor.getContent();
						var metaboxEditor = tinymce.get(sourceId);
						if (metaboxEditor) {
							metaboxEditor.setContent(content);
							metaboxEditor.save();
						} else {
							$('#' + sourceId).val(content);
						}
						$textarea.trigger('change');
					});
				};

				tinymce.init(mceInit);

				// Initialise Quicktags using the same source config when available.
				if (typeof quicktags !== 'undefined') {
					var qtSettings = (preInit.qtInit && preInit.qtInit[sourceId])
						? $.extend({}, preInit.qtInit[sourceId], { id: editorId })
						: { id: editorId };
					quicktags(qtSettings);
					if (typeof QTags !== 'undefined' && QTags._buttonsInit) {
						QTags._buttonsInit();
					}
				}
			});
		},

		// Flush content and tear down TinyMCE instances when the overlay closes so the
		// next open() gets a clean slate.  Final content is pushed to the metabox so
		// a subsequent WP "Update" click submits the correct value.
		removeVbEditors: function() {
			if (typeof tinymce === 'undefined') {
				return;
			}

			this.$overlay.find('.giveasap-vb-editor').each(function() {
				var editorId = $(this).attr('id');
				if (!editorId) {
					return;
				}

				var editor = tinymce.get(editorId);
				if (editor) {
					editor.save();
					var content = editor.getContent();
					var sourceId = editorId.replace(/-vb$/, '');
					var metaboxEditor = tinymce.get(sourceId);
					if (metaboxEditor) {
						metaboxEditor.setContent(content);
						metaboxEditor.save();
					} else {
						$('#' + sourceId).val(content);
					}
					editor.remove();
				}
			});
		},

		collectSettings: function() {
			this.flushVbEditors();
			this.flushVbCodeEditors();
			var settings = {};

			this.$overlay.find('input[name], select[name], textarea[name]').each(function() {
				var $field = $(this);
				var name = $field.attr('name');
				var value = $field.val();

				// Action fields are sent as one compact giveasap_actions_json param
				// instead — dozens of bracketed names would blow up the preview URL.
				if (name.indexOf('entries_actions[') === 0) {
					return;
				}

				if ($field.is(':checkbox')) {
					value = $field.is(':checked') ? $field.val() : '';
				}

				settings[name] = value;
			});

			// Serialize prizes repeater rows as structured JSON for preview and AJAX save
			var $prizesContainer = this.$overlay.find('[data-giveasap-adapter="repeater"][data-giveasap-sync-key="prizes_repeater"]');
			if ($prizesContainer.length) {
				var prizes = [];
				$prizesContainer.find('.sg-form-field').each(function() {
					var row = {};
					$(this).find('input[name], select[name], textarea[name]').each(function() {
						var match = $(this).attr('name').match(/\[(\w+)\]$/);
						if (match) {
							row[match[1]] = $(this).val();
						}
					});
					prizes.push(row);
				});
				settings['giveasap_prizes_json'] = JSON.stringify(prizes);
			}

			// Serialize the actions repeater as structured JSON for the live preview.
			// Mirrors what a form post would produce: unchecked checkboxes are
			// omitted and [] names (multioption, mandatory actions) become arrays.
			var $actionsContainer = this.$overlay.find('[data-giveasap-adapter="repeater"][data-giveasap-sync-key="actions_repeater"]');
			if ($actionsContainer.length) {
				var actions = [];
				$actionsContainer.find('.sg-admin-action').each(function() {
					var row = {};
					$(this).find('input[name], select[name], textarea[name]').each(function() {
						var name = $(this).attr('name');

						if (($(this).is(':checkbox') || $(this).is(':radio')) && !$(this).is(':checked')) {
							return;
						}

						var arrayMatch = name.match(/\[([\w-]+)\]\[\]$/);
						if (arrayMatch) {
							if (!row[arrayMatch[1]]) {
								row[arrayMatch[1]] = [];
							}
							row[arrayMatch[1]].push($(this).val());
							return;
						}

						var match = name.match(/\[([\w-]+)\]$/);
						if (match) {
							row[match[1]] = $(this).val();
						}
					});
					actions.push(row);
				});
				settings['giveasap_actions_json'] = JSON.stringify(actions);
			}

			this.settings = settings;
		},

		debouncedUpdatePreview: function() {
			var self = this;

			clearTimeout(this.debounceTimer);
			this.debounceTimer = setTimeout(function() {
				self.updatePreview();
			}, 500);
		},

		updatePreview: function() {
			var self = this;
			var params = {
				giveasap_iframe_preview: 1,
				_wpnonce: giveasapVisualBuilder.nonce,
				giveaway_id: giveasapVisualBuilder.giveawayID,
				preview_step: this.previewStep
			};

			this.$loading.show();

			$.each(this.settings, function(key, value) {
				if (value !== '' && value !== null && typeof value !== 'undefined') {
					params[key] = value;
				}
			});

			// Navigate the iframe via a POSTed form instead of a querystring src —
			// the serialized settings (actions/prizes JSON, editor content) easily
			// exceed server URL length limits (414).
			var $form = $('<form></form>', {
				method: 'post',
				action: giveasapVisualBuilder.iframeUrl,
				target: 'giveasap-preview-frame'
			}).css('display', 'none');

			$.each(params, function(key, value) {
				$form.append($('<input/>', {
					type: 'hidden',
					name: key,
					value: value
				}));
			});

			$('body').append($form);
			// Native submit so jQuery handlers and admin-screen submit delegates
			// (autosave/dirty-state warnings) cannot intercept the navigation.
			$form[0].submit();
			$form.remove();

			this.$iframe.off('load').on('load', function() {
				self.$loading.hide();
			});
		},

		// The metabox is kept in sync with the overlay on every edit, so saving
		// is nothing more than submitting the post form — the regular backend
		// save path (validation, sanitization, meta updates) does the rest.
		saveSettings: function() {
			$( '#publish' ).trigger( 'click' );
		}
	};

	$(document).ready(function() {
		if ($('#giveasap-visual-builder-overlay').length) {
			VisualBuilder.init();
		}
	});

})(jQuery);
