<?php

namespace ProcessWire;

/**
 * PWCommerce: FieldtypePWCommerceRuntimeMarkup
 *
 * Fieldtype that maintains a collection of product variants that are children of the product for any number of times..
 *
 * @author Francis Otieno (Kongondo) <kongondo@gmail.com> kongondo.com
 *
 *
 *
 * FieldtypePWCommerceRuntimeMarkup for PWCommerce
 * Copyright (C) 2025 by Francis Otieno
 * MIT License
 *
 */


class FieldtypePWCommerceRuntimeMarkup extends Fieldtype
{


	public static function getModuleInfo() {
		return [
			'title' => 'PWCommerce Runtime Markup: Fieldtype',
			'summary' => 'Runtime only field that facilitates the output and processng of edit forms for embedded PWCommerce pages.',
			'version' => "100",
			// 'autoload' => true,
			'installs' => 'InputfieldPWCommerceRuntimeMarkup',
		];
	}


	/**
	 * The following functions are defined as replacements to keep this fieldtype out of the DB
	 *
	 */

	public function sanitizeValue(Page $page, Field $field, $value) {
		return $value;
	}

	public function ___sleepValue(Page $page, Field $field, $value) {
		return true;
	}

	public function getLoadQuery(Field $field, DatabaseQuerySelect $query) {
		// prevent loading from DB
		return $query;
	}

	public function ___loadPageField(Page $page, Field $field) {
		// generate value at runtime rather than loading from DB
		return true;
	}

	//  public function ___savePageField(Page $page, Field $field) {
	//   // prevent saving of field
	//   return true;
	//  }

	public function ___deletePageField(Page $page, Field $field) {
		// deleting of page field not necessary
		return true;
	}

	public function ___deleteField(Field $field) {
		// deleting of field not necessary
		return true;
	}

	public function getDatabaseSchema(Field $field) {
		// no database schema necessary
		return [];
	}

	public function ___createField(Field $field) {
		// nothing necessary to create the field
		return true;
	}

	public function getMatchQuery($query, $table, $subfield, $operator, $value) {
		// we don't allow this field to be queried
		throw new WireException("Field '{$query->field->name}' is runtime and not queryable");
	}

	public function ___getCompatibleFieldtypes(Field $field) {
		// no fieldtypes are compatible
		return new Fieldtypes();
	}

	public function getLoadQueryAutojoin(Field $field, DatabaseQuerySelect $query) {
		// we don't allow this field to be autojoined
		return null;
	}

	//#######################################################

	/**
	 * Returns a unique name for a repeater page
	 *
	 * @return string
	 *
	 */
	public function getUniqueRepeaterPageName() {
		// @KONGONDO TODO: DELETE IF NOT IN USE; OTHERWISE RENAME
		static $cnt = 0;
		return str_replace('.', '-', microtime(true)) . '-' . (++$cnt);
	}

	/**
	 * Get the class for the Inputfield (template method)
	 *
	 * @return string
	 *
	 */
	protected function getInputfieldClass() {
		// @KONGONDO TODO: AMEND THIS WITH OUR CLASS!
		return 'InputfieldPWCommerceRuntimeMarkup';
	}

	/**
	 * Return an InputfieldPWCommerceRuntimeMarkup, ready to be used
	 *
	 * @param Page $page Page being edited
	 * @param Field $field Field that needs an Inputfield
	 * @return Inputfield
	 *
	 */
	public function getInputfield(Page $page, Field $field) {

		// @KONGONDO TODO: AMEND BELOW AS NEEDED
		$inputfield = $this->wire('modules')->get($this->getInputfieldClass());
		$inputfield->set('page', $page);
		$inputfield->set('field', $field);
		$inputfield->set('runtimeContext', $this->getRuntimeContext($page));
		$inputfield->set('runtimeContextPrependContentInputfields', $this->getRuntimeContextPrependContentInputfields());
		$inputfield->set('runtimeContextAppendContentInputfields', $this->getRuntimeContextAppendContentInputfields());
		$inputfield->set('runtimeContextLimitInputfields', $this->getRuntimeContextLimitInputfields());
		$inputfield->set('runtimeContextDynamicallyManageInputfields', $this->getRuntimeContextDynamicallyManageInputfields());
		// $inputfield->set('runtimeContextBuildCustomFormInputfields', $this->getRuntimeContextBuildCustomFormInputfields());
		$inputfield->set('runtimeContextBuildBlankItemInputfield', $this->getRuntimeContextBuildBlankItemInputfield());
		$inputfield->set('runtimeContextVirtualProcessInputInputfield', $this->getRuntimeContextVirtualProcessInputInputfield());
		$inputfield->set('runtimeContextPreloadInlineAssets', $this->getRuntimeContextPreloadInlineAssets());
		$inputfield->set('runtimeContextPreloadAssets', $this->getRuntimeContextPreloadAssets());
		$inputfield->set('runtimeContextJavaScriptConfigurations', $this->getRuntimeContextJavaScriptConfigurations());
		$inputfield->set('runtimeContextIsDeleteableItems', $this->getRuntimeContextIsDeleteableItems());
		$inputfield->set('runtimeContextIsPostProcessingAfterSave', $this->getRuntimeContextIsPostProcessingAfterSave());
		// for dynamically loaded product variants
		$inputfield->set('isDynamicallyLoadedContent', $this->checkisDynamicallyLoadedContent($page, $field));
		// $inputfield->set('dynamicLoadingThreshold', $field->dynamicLoadingThreshold);
		return $inputfield;
	}

	private function getRuntimeContext($page) {
		$foundContext = null;
		if ($page->id) {
			// we only allow a fixed number of contexts for use with this runtime field
			$template = \str_replace('pwcommerce-', '', $page->template->name);
			$contexts = ['product', 'shipping-zone', 'country', 'order', 'attribute', 'gift-card-product'];

			foreach ($contexts as $context) {
				if ($context === $template) {
					$foundContext = $context;
					break;
				}
			}
		}
		return $foundContext;
	}

	private function getContextSelector($context) {
		$template = null;
		$sort = 'sort';
		$selector = null;
		// product variants for product
		if ($context === 'product') {
			$template = "pwcommerce-product-variant";
		}
		// shipping rate for shipping zone
		elseif ($context === 'shipping-zone') {
			$template = "pwcommerce-shipping-rate";
		}
		// country territory for country
		elseif ($context === 'country') {
			$template = "pwcommerce-country-territory";
		}
		// order line item for order
		elseif ($context === 'order') {
			$template = "pwcommerce-order-line-item";
		}
		// attribute options for attribute
		elseif ($context === 'attribute') {
			$template = "pwcommerce-attribute-option";
		}
		// gift card product variants for gift card product
		elseif ($context === 'gift-card-product') {
			$template = "pwcommerce-gift-card-product-variant";
			$sort = "pwcommerce_product_stock.price";
		}

		//-----------------
		// append necessaries
		if ($template) {
			$selector = "template={$template},sort={$sort}, include=all";
		}

		//---------------
		return $selector;
	}

	private function getAllowedSaveTemplates() {
		// TODO: add more!
		// TODO: not sure we need  'pwcommerce-attribute-option' and 'pwcommerce-gift-card-product-variant' ?? saving happens via a virtual inputfield!
		return ['pwcommerce-product-variant', 'pwcommerce-shipping-rate', 'pwcommerce-country-territory', 'pwcommerce-order-line-item', 'pwcommerce-attribute-option', 'pwcommerce-gift-card-product-variant'];
	}

	// for contexts with specific inputfields that need to PREPEND extra content
	private function getRuntimeContextPrependContentInputfields() {
		$appendContents = [
			// @note: this key has these Inputfields (value) needing/generating extra PREPEND content
			'order' => ['InputfieldPWCommerceOrderLineItem'],
			'product' => ['InputfieldPWCommerceProductStock'],
			'gift-card-product' => ['VirtualInputfieldPWCommerceGiftCardProductVariants'],

		];
		return $appendContents;
	}

	// for contexts with specific inputfields that need to APPEND extra content
	private function getRuntimeContextAppendContentInputfields() {
		$appendContents = [
			// @note: this key has these Inputfields (value) needing/generating extra APPEND content
			'shipping-zone' => ['InputfieldPWCommerceShippingRate'],
			// 'order' => ['InputfieldPWCommerceOrderLineItem'],// @note: no longer needed as using different alpine js approach
			// @note: this is a virtual inputfield! It is just a helper class for attributes (e.g. 'Colour') to handle creation of attribute options (e.g. 'Red', 'Black') from within runtime markup
			'attribute' => ['VirtualInputfieldPWCommerceAttributeOptions'],
			'gift-card-product' => ['VirtualInputfieldPWCommerceGiftCardProductVariants'],

		];
		return $appendContents;
	}

	// for contexts that need to only display limited number of the inputfields on their pages
	// e.g. for order context, line items pages should not display their titles within the runtime markup here.
	private function getRuntimeContextLimitInputfields() {
		$limitInputfields = [
			// @note: this key has these LIMITED Inputfields (value) that will be displayed
			'order' => ['pwcommerce_order_line_item'],

		];
		return $limitInputfields;
	}

	// for contexts that dynamically manage the inputfields from their pages to display depending on certain conditions
	// e.g. for gift-card-product context, for newly added but unsaved items, they DO NOT DISPLAY the image inputfield. In addition, they always hide/remove the title fields.
	private function getRuntimeContextDynamicallyManageInputfields() {
		$dynamicallyManageInputfields = [
			// @note: this key has these Inputfields (value) needing to manage their displayed inputfields dynamically
			'gift-card-product' => 'VirtualInputfieldPWCommerceGiftCardProductVariants',

		];
		return $dynamicallyManageInputfields;
	}

	// for contexts that need to build custom forms to be rendered in InputfieldPWCommerceRuntimeMarkup
	// e.g. The custom form would contain Alpine JS attributes.
	// private function getRuntimeContextBuildCustomFormInputfields() {
	// 	// TODO @NOTE: NOT IN USE FOR NOW
	// 	$buildCustomFormInputfields = [
	// 		// @note: this key has these CUSTOM DYNAMIC CONTENT Inputfields (value)
	// 		// 'order' => ['InputfieldPWCommerceOrderLineItem'],
	// 		# TODO ADD 'GIFT CARD PRODUCT VARIANT' ???

	// 	];
	// 	return $buildCustomFormInputfields;
	// }

	# ################################
	# for contexts that need to build a new blank item to be rendered in InputfieldPWCommerceRuntimeMarkup
	# and VIA PROCESSING AJAX REQUESTS DIRECTLY IN InputfieldPWCommerceRuntimeMarkup
	# e.g. for InputfieldPWCommerceShippingRate or InputfieldPWCommerceAttributeOptions when 'add new item' is clicked.
	# @note: 'country' [InputfieldPWCommerceTaxRates.module] is not needed here since it handles its own Ajax Requests in render()
	# ################################
	// @note: each context should only return a single Inputfield!
	private function getRuntimeContextBuildBlankItemInputfield() {
		$buildBlankItemInputfields = [
			// @note: this key has this Inputfield (value) that returns a blank item on demand (typically a Page of its children's type)
			'shipping-zone' => 'InputfieldPWCommerceShippingRate',
			# >>>>>>>>>> --------- VIRTUAL INPUTFIELDS <<<<<<<<<<-------------- ##
			// @note: this is a virtual inputfield! It is just a helper class for attributes (e.g. 'Colour') to handle creation of attribute options (e.g. 'Red', 'Black') from within runtime markup
			'attribute' => 'VirtualInputfieldPWCommerceAttributeOptions',
			'gift-card-product' => 'VirtualInputfieldPWCommerceGiftCardProductVariants',

		];
		return $buildBlankItemInputfields;
	}

	// ---------------------
	// for contexts that will process inputs in virtual inputfields.
	// e.g. for InputfieldPWCommerceAttributeOptions to process new options (i.e., from 'add new item') or delete edited options.
	// @note: each context should only return a single Inputfield!
	private function getRuntimeContextVirtualProcessInputInputfield() {
		$virtualProcessInputInputfields = [
			// @note: this key has this Inputfield (value) that will process inputs in a virtual inputfield
			// @note: this is a virtual inputfield! It is just a helper class for attributes (e.g. 'Colour') to handle creation of attribute options (e.g. 'Red', 'Black') from within runtime markup
			'attribute' => 'VirtualInputfieldPWCommerceAttributeOptions',
			'gift-card-product' => 'VirtualInputfieldPWCommerceGiftCardProductVariants',
		];
		return $virtualProcessInputInputfields;
	}

	// for contexts that need to preload inline assets in their inputfields.
	// e.g. for order or product contexts, alpinejs needs to be loaded inline with 'defer' option.
	// this is mainly useful for edit pages/views that have shared assets, e.g. order and order line items.
	// also useful for cases when the assets are only needed in runtime markup context.
	#### TODO @UPDATE: NO LONGER IN USE IN THE INPUTFIELDS! ####
	private function getRuntimeContextPreloadInlineAssets() {
		$inlineAssetsInputfields = [
			// @note: this key has these  Inputfields (value) that will need inline assets to be rendered
			'product' => ['InputfieldPWCommerceProductStock'],
			'order' => ['InputfieldPWCommerceOrder'],

		];
		return $inlineAssetsInputfields;
	}

	// for contexts that need to preload assets in their inputfields.
	// e.g. for order or product contexts, htmx needs to be loaded as a script.
	// this is mainly useful for edit pages/views that have shared assets, e.g. order and order line items.
	// also useful for cases when the assets are only needed in runtime markup context.
	#### TODO @UPDATE: NO LONGER IN USE IN THE INPUTFIELDS! ####
	private function getRuntimeContextPreloadAssets() {
		$inlineAssetsInputfields = [
			// @note: this key has these  Inputfields (value) that will need inline assets to be rendered
			'product' => ['InputfieldPWCommerceProductStock'],
			'order' => ['InputfieldPWCommerceOrder'],
			'shipping-zone' => ['InputfieldPWCommerceShippingRate'],
			'country' => ['InputfieldPWCommerceTaxRates'],
			'gift-card-product' => ['VirtualInputfieldPWCommerceGiftCardProductVariants'],

		];
		return $inlineAssetsInputfields;
	}

	/**
	 * For contexts that need to send javascript configurations to client-side.
	 *
	 * E.g., for order line items running inside runtime markup, configurations need to be sent to client for saved values.
	 * @return array $javaScriptConfigurationsInputfields Fields to get configurations from.
	 */
	private function getRuntimeContextJavaScriptConfigurations() {
		$javaScriptConfigurationsInputfields = [
			// @note: this key has these  Inputfields (value) that will need configurations values built.
			'order' => ['InputfieldPWCommerceOrderLineItem'],
			'gift-card-product' => ['VirtualInputfieldPWCommerceGiftCardProductVariants'],

		];
		return $javaScriptConfigurationsInputfields;
	}

	/**
	 * For contexts that need to be able to delete runtime items.
	 *
	 * E.g., for shipping-zone, shipping rates (pages) are deleteable.
	 * @note: each context should only return a single Inputfield!
	 * @return array $isDeleteableItems Fields to handle delete of items.
	 */
	private function getRuntimeContextIsDeleteableItems() {
		$isDeleteableItems = [
			// @note: these keys are contexts that allow delete of items
			// @note: this key has these Inputfields (value) that will handle delete of items.
			'shipping-zone' => 'InputfieldPWCommerceShippingRate',
			'product' => 'InputfieldPWCommerceProductStock',
			'order' => 'InputfieldPWCommerceOrderLineItem',
			# >>>>>>>>>> --------- VIRTUAL INPUTFIELDS <<<<<<<<<<-------------- ##
			'attribute' => 'VirtualInputfieldPWCommerceAttributeOptions',
			'gift-card-product' => 'VirtualInputfieldPWCommerceGiftCardProductVariants',
		];
		return $isDeleteableItems;
	}

	/**
	 * For contexts that need to run post-processing after saving of inputfields.
	 *
	 * E.g., for orders, some order values depend on the final values of their order line items
	 * It means orders have to be post-processed to account for this, e.g. total discounts, etc.
	 * @note: each context should only return a single Inputfield!
	 * @return array $isPostProcessingAfterSave Fields to handle post processing after items are saved.
	 */
	private function getRuntimeContextIsPostProcessingAfterSave() {
		$isPostProcessingAfterSave = [
			// @note: these keys are contexts that post-process after save items
			// @note: this key has these Inputfields (value) that will handle post processing of items.
			'order' => 'InputfieldPWCommerceOrder',
		];
		return $isPostProcessingAfterSave;
	}

	/**
	 * For product context that need to load variants dynamically using htmx-ajax.
	 *
	 * E.g., for use if poduct uses and has variants > 30.
	 * @param Page $page Page being edited.
	 * @param Field $field Field of this fieldtype.
	 * @return bool $isDynamicallyLoadedContent Whether context will dynamically load its content or not.
	 */
	private function checkisDynamicallyLoadedContent($page, $field) {
		$isDynamicallyLoadedContent = false;
		// ----------
		$context = $this->getRuntimeContext($page);





		// if context if 'product' and dynamic loading threshold has been set (> -1)
		if ($context === 'product' && $field->dynamicLoadingThreshold > -1) {
			// we check if product is using and has variants
			$productSettings = $page->get(PwCommerce::PRODUCT_SETTINGS_FIELD_NAME);
			$isUseVariants = (int) $productSettings->useVariants;

			if (!empty($isUseVariants)) {
				// if using variants AND always using dynamic loading OR product variants GREATER THAN dynamic loading threshold...
				// ... we use dynamic loading
				$isDynamicallyLoadedContent = $field->dynamicLoadingThreshold === 0 || $page->numChildren > $field->dynamicLoadingThreshold;
			}
		}


		// -----------
		return $isDynamicallyLoadedContent;
	}

	// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

	// @KONGONDO TODO: AMEND AS REQUIRED
	/**
	 * Given a raw value (value as stored in DB), return the value as it would appear in a Page object
	 *
	 * Something to note is that this wakeup function is different than most in that the $value it is given
	 * is just an array like array('data' => 123, 'parent_id' => 456) -- it doesn't actually contain any of the
	 * runtimemarkup page data other than saying how many there are and the parent where they are stored. So this
	 * wakeup function can technically do it's job without even having the $value, unlike most other fieldtypes.
	 *
	 * @param Page $page
	 * @param Field $field
	 * @param array $value
	 * @return PageArray|Page $value
	 *
	 */
	public function ___wakeupValue(Page $page, Field $field, $value) {

		// this special runtime field can only be used by specific PWCommerce pages
		$context = $this->getRuntimeContext($page);
		if (empty($context)) {
			throw new WireException($this->className() . " can only be used with specific PWCommerce pages.");
		}

		// *********************************************
		// PAGEARRAY CONTEXTS

		// if it's already in the target format, leave it
		if ($value instanceof PageArray) {
			return $value;
		}

		// build the selector: find pages with our parent
		// $selector = "parent_id=$parent_id, templates_id=$template_id, sort=sort, check_access=0";
		//   $selector = "parent_id=$parent_id, templates_id=$template_id, sort=sort, include=all";
		// @kongondo: TODO ok?
		// $selector = "template=pwcommerce-product-variant, sort=sort, include=all";
		$selector = $this->getContextSelector($context);
		if (empty($selector)) {
			throw new WireException($this->className() . ": requires a valid selector for specific PWCommerce pages.");
		}
		// @KONGONDO TODO: for trying with findRaw() - NO; WE NEED PAGEARRAY SO WON'T WORK
		// $selector = "parent_id={$page->id},template=pwcommerce-product-variant, sort=sort, include=all";

		// @kongondo: get the current product page's children; they are its variants
		// @kongondo TODO: account for products without variants + unpublished variants!
		// @kongondo TODO: do we need this options?
		// @KONGONDO TODO: findRaw here or need objects? NEED PAGEARRAY HERE!
		// @kongondo @note: product variants are the children of the product page
		// @note: not all products have variants!
		// $pageArray = $page->children($selector, $options);

		// if ($page->template->name === 'pwcommerce-product') {
		//     if ($page->numChildren > 30) {
		//
		//         $variantFields = ['id', 'title'];
		//         $pageArrayRaw = $this->wire('pages')->findRaw("template='pwcommerce-product-variant,parent={$page},objects=1", $variantFields);
		//
		//
		//         return $pageArrayRaw;
		//     }
		// }

		//------------------
		// @NOTE: CONTEXT IS ALWAYS THE PAGES CHILDREN!
		$pageArray = $page->children($selector);
		$pageArray->resetTrackChanges(true);
		// TODO - @SEE NOTES in InputfieldRuntimeMarkup:;render() ABOUT MANY VARIANTS ISSUE! + IDEAS OF ON-DEMAND HTMX LOADING OF VARIANTS. WOULD HAVE TO BE IN THAT CONTEXT TO FETCH USING findRaw + check numChildren or count first! Cut off is 30 variants? or 50?


		return $pageArray;
	}

	/**
	 * Perform output formatting on the value delivered to the API
	 *
	 * This method is only used when $page->outputFormatting is true.
	 *
	 * @param Page $page
	 * @param Field $field
	 * @param PageArray $value
	 * @return PageArray
	 *
	 */
	public function ___formatValue(Page $page, Field $field, $value) {
		// TODO?
		return $value;
	}

	// @KONGONDO TODO: IMPORTANT IN ORDER FOR SAVING TO WORK! AMEND AS NEEDED AND CLEANUP
	/**
	 * Save the given field from page
	 *
	 * @param Page $page Page object to save.
	 * @param Field $field Field to retrieve from the page.
	 * @return bool True on success, false on DB save failure.
	 *
	 */
	public function ___savePageField(Page $page, Field $field) {
		if (!$page->id || !$field->id) {
			return false;
		}
		$value = $page->get($field->name);

		// pages that will be saved
		$savePages = [];

		// options to pass to save() or clone()
		$saveOptions = ['uncacheAll' => false];

		// pages that will be deleted
		$deletePages = $value->getItemsRemoved();
		// @kongondo: limited pages that can be saved using this runtime field
		$allowedSaveTemplates = $this->getAllowedSaveTemplates();

		// iterate through each page in the pageArray value
		// and determine which need to be saved
		foreach ($value as $key => $p) {

			/** @var Page $p */

			// @KONGONDO TODO AMENDED!
			//  if ($p->template->id != $template_id) {
			// TODO: REMOVE NON-CONTEXT PAGES
			//    if ($p->template->name != 'pwcommerce-product-variant') {
			if (!in_array($p->template->name, $this->getAllowedSaveTemplates())) {

				$value->remove($p);
				$this->message("Removed invalid template ({$p->template->name}) page {$p->path} from field $field", Notice::debug);
				continue;
			}
			// @KONGONDO TODO AMENDED!
			//  if ($p->parent->id != $parent_id) {
			if ($p->parent->id != $page->id) {
				// clone the individual repeater pages
				$value->remove($p);
				// $p = $this->wire('pages')->clone($p, $repeaterParent, false, $saveOptions);
				// $value->add($p);
				// $this->message("Cloned to {$p->path} from field $field", Notice::debug);
				continue;
			}

			if ($p->isNew() && !$p->name && !$p->title) {
				// if we've got a new repeater page without a name or title
				// then it's not going to save because it has no way of generating a name
				// so we will generate one for it
				$p->name = $this->getUniqueRepeaterPageName();
			}

			if ($p->isChanged() || $p->isNew()) {
				// if the page has changed or is new, then we will queue it to be saved
				$savePages[] = $p;
			} elseif ($p->id && $p->isUnpublished() && !$p->isHidden()) {
				// if the page has an ID, but is still unpublished, though not hidden, then we queue it to be saved (and published)
				$savePages[] = $p;
			}
		}

		// iterate the pages that had changes and need to be saved
		foreach ($savePages as $p) {
			if ($p->id) {
				// existing page
				$isHidden = $p->isHidden();
				$isUnpublished = $p->isUnpublished();
				$isOn = $p->hasStatus(Page::statusOn);
				$isProcessed = $p->get('_pwcommerce_runtime_processed') === true;
				$hasErrors = $p->get('_pwcommerce_runtime_errors') ? true : false;

				// @KONGONDO - TODO: IF USING ANY OF THESE, E.G. FOR ENABLED, THEN NEED TO AMEND!
				if ($isHidden && $isUnpublished) {
					continue;
				}
				// this is a 'ready' page, we can ignore

				$changes = implode(', ', $p->getChanges());
				$this->message("Saved '$field' page: {$p->path} " . ($changes ? "($changes)" : ''), Notice::debug);

				if ($isUnpublished && $isOn && $isProcessed && !$hasErrors) {
					// publish requested and allowed
					$p->removeStatus(Page::statusUnpublished);
				}
			} else {
				$this->message("Added new '$field' page", Notice::debug);
			}

			// save the runtime item page
			// TODO TRYING TO DEBUG THE FIELD SAVING ERROR AFTER CREATE ORDER LINE ITEMS VIA API IN FRONTEND
			// HARD TO TELL IF VIA API, SO FOR NOW, JUST LET IT BE
			$p->of(false);

			$this->wire('pages')->save($p, $saveOptions);
		}

		// iterate through the pages that were removed
		foreach ($deletePages as $p) {
			// if the deleted value is still present in the pageArray, then don't delete it
			if ($value->has($p)) {
				continue;
			}

			// $this->message("Deleted Repeater", Notice::debug);
			// delete the product variant page
			$this->wire('pages')->delete($p, $saveOptions);
		}

		// @KONGONDO TODO: DELETE; THROWS ERROR ABOUT THIS FIELD NOT HAVING TABLE; DO WE NEED IT?
		// $result = parent::___savePageField($page, $field);

		// ensure that any of our cloned page replacements (removes) don't get recorded any follow-up saves
		$value->resetTrackChanges();
		// @KONGONDO TODO: DELETE;
		// return $result;
	}

	/**
	 * Set the config option fields for this Field
	 * These appear in the 'Details' Tab when editing an instance of this Field
	 *
	 * @access public
	 * @param Field $field
	 * @return Inputfield $inputfields The Inputfield associated with this InputfieldRuntimeMarkup Field.
	 *
	 */
	public function ___getConfigInputfields(Field $field) {
		// TODO: @KONGONDO EDIT AS REQUIRED
		$inputfields = parent::___getConfigInputfields($field);

		### PRODUCT VARIANTS ###

		$modules = $this->wire('modules');

		// for specifying  maximum product variants before ajax dynamic loading kicks in
		$f = $modules->get("InputfieldText");
		$f->attr('name', 'dynamicLoadingThreshold');
		$f->attr('value', (int) $field->dynamicLoadingThreshold);
		$f->attr('type', 'number');
		$f->attr('step', 1);
		$f->attr('min', -1);
		$f->size = 10;
		$f->label = $this->_('Product Variants Dynamic Loading Threshold');
		$f->description .= $this->_('Specify the threshold for dynamic ajax loading of product variants. This is recommended if your shop will have lots of variants per product.');
		$f->notes = $this->_('-1 = no ajax loading; 0 = always ajax load; > 0  = ajax load if product variants quantity is greater than the specified threshold.');



		$inputfields->add($f);
		// ----------
		return $inputfields;
	}

	/**
	 * Just here to fulfill ConfigurableModule interface
	 *
	 * @param array $data
	 * @return InputfieldWrapper
	 *
	 */
	public function getModuleConfigInputfields(array $data) {
		if ($data) {
		} // ignore
		return $this->wire(new InputfieldWrapper());
	}

	/**
	 * Remove advanced options that aren't supported in variants lister
	 *
	 * @param Field $field
	 * @return InputfieldWrapper
	 *
	 */
	public function ___getConfigAdvancedInputfields(Field $field) {
		// TODO: REVISIT THIS?
		$inputfields = parent::___getConfigAdvancedInputfields($field);
		// these two are potential troublemakers when it comes to variants lister
		$inputfields->remove($inputfields->get('autojoin'));
		$inputfields->remove($inputfields->get('global'));
		return $inputfields;
	}

	/**
	 * Install the module
	 *
	 */
	public function ___install() {
	}

	/**
	 * Uninstall the module
	 *
	 */
	public function ___uninstall() {
	}
}