<?php
/*
 * Jetpack CRM
 * https://jetpackcrm.com
 * V2.52+
 *
 * Copyright 2020 Automattic
 *
 * Date: 27/02/18
 */

defined( 'ZEROBSCRM_PATH' ) || exit( 0 );

/*
======================================================
	Parent Class for all Metabox
	====================================================== */

class zeroBS__Metabox {

	static $instance;

	public $typeInt = false; // this was added late in DAL2 migration (28/04/18) but we should migrate to this over the other $postType etc. (started with Metaboxes.TagManger.php)

	// v3.0 this was renamed objType public $postType = false; // set in child class 'zerobs_customer' // ONLY USED IN save funcs etc. maybe, potentially just legacy now.
	public $objType      = false; // set in child class 'contact' // ONLY USED IN save funcs etc. maybe, potentially just legacy now.
	public $metaboxID    = false; // set in child class 'zerobs-customer-edit';
	public $metaboxTitle = ''; // set in child class __('Customer Details',"zero-bs-crm")
	public $metaboxIcon  = ''; // Semantic ui icon e.g. "heartbeat" = <i class="heartbeat icon"></i> - since 2.98.7

	// default positions
	public $metaboxScreen   = false; // set in child class zerobs_contact
	public $metaboxScreens  = false; // added v3.0 - if set and is array, this'll override metaboxScreen in terms of WHERE it's shown - allowing multiple exposed points from one metabox (e.g. contacts + co's) - set in child class zerobs_contact
	public $metaboxArea     = false; // set in child class normal
	public $metaboxLocation = false; // set in child class high

	// style choices
	public $headless       = false; // hides header?
	public $metaboxClasses = ''; // extra classes to add to the wrappers

	// save options
	public $saveOrder      = 10; // priority for this metabox to save. MAIN RECORDS use 1 so that later metaboxes can gain ID from newly inserted via global
	public $updateMessages = array();

	// hide/show
	public $live = true; // switching this to false before initMetabox will STOP it loading (allows for 'checks')

	// screen options - defaults - note these are also used in add_meta_box for empties
	public $capabilities = array(

		'can_hide'        => true, // can be hidden
		'areas'           => array( 'normal', 'side' ), // areas can be dragged to - normal side = only areas currently
		'can_accept_tabs' => false,  // can/can't accept tabs onto it
		'can_become_tab'  => false, // can be added as tab
		'can_minimise'    => true, // can be minimised
		'can_move'        => true, // can be rearranged
		'hide_on_new'     => false, // if this is true, on "new" edit's this'll hide (e.g. logs no point till added)

	);

	// public function __construct( $plugin_file ) {
	public function initMetabox() {

		// catch old screen's and translate (in few rare cases)
		if ( $this->metaboxScreen == 'zerobs_edit_contact' ) {
			$this->metaboxScreen = 'zbs-add-edit-contact-edit';
		}

		if ( $this->live ) {

			// lazy hackaround for now, can be more classy later.
			if ( ! empty( $this->metaboxIcon ) ) {
				$this->metaboxTitle = '<i class="' . $this->metaboxIcon . ' icon"></i> ' . $this->metaboxTitle;
			}

			// self::$instance = $this;

			// Create on init, for zbs metaboxes, rather than: add_action( 'add_meta_boxes', array( $this, 'create_meta_box' ) );
			$this->create_meta_box();

			// add save func, even if will be blank :)
			// WP was using filters, I'll move to actions (makes more sense to me, having read actions vs filters)
			// add_filter( 'zerobs_save_'.$this->postType, array( $this, 'save_meta_box' ), 10, 2 );
			if ( $this->objType ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
				add_action( 'zerobs_save_' . $this->objType, array( $this, 'save_meta_box' ), $this->saveOrder, 2 );
			}
			// This is fired by edit page do_action

			// this is then set to fire after ALL other save funcs :)
			// ... by way of a 999 priority
			if ( $this->objType ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
				add_action( 'zerobs_save_' . $this->objType, array( $this, 'post_save_meta_box' ), 999, 2 );
			}
		}
	}

	public function create_meta_box() {

		if ( isset( $this->metaboxScreens ) && is_array( $this->metaboxScreens ) ) {

			foreach ( $this->metaboxScreens as $metaboxScreen ) {

				// shown in multiple screens, so add for each

				if ( ! empty( $metaboxScreen ) ) {

					zeroBSCRM_add_meta_box(
						$this->metaboxID,
						$this->metaboxTitle,
						array( $this, 'print_meta_box' ),
						$metaboxScreen, // $this->metaboxScreen,
						$this->metaboxArea,
						$this->metaboxLocation,
						array(),
						$this->headless,
						$this->metaboxClasses,
						$this->capabilities
					);

				}
			}
		} else {

			// normal, only shown in one place
			zeroBSCRM_add_meta_box(
				$this->metaboxID,
				$this->metaboxTitle,
				array( $this, 'print_meta_box' ),
				$this->metaboxScreen,
				$this->metaboxArea,
				$this->metaboxLocation,
				array(),
				$this->headless,
				$this->metaboxClasses,
				$this->capabilities
			);

		}
	}

	// use this to output data (post nonce :)
	public function html( $obj, $metabox ) {

		// child class will print box here
	}

	/*
		Not actually used yet... wh used $zbs->pageMessages for brevity, but will work (though will output IN a metabox)
	public function html_msgs($obj,$metabox){

		// This outputs any html msgs directly (probably called by the edit class above)
		if (is_array($this->updateMessages) && count($this->updateMessages) > 0) foreach ($this->updateMessages as $m) echo $m;
	}*/

	// use this to save data in child classes (nonce-checked :)
	public function save_data( $objID, $obj ) {

		// save data
	}

	// use this for post-save data in child classes (nonce-checked :)
	public function post_save_data( $objID, $obj ) {

		// do any actions needed after all metaboxes have saved data
	}

	public function print_meta_box( $obj, $metabox ) {

				// nonce output by parent class
		if ( $this->metaboxID ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
			wp_nonce_field( 'save_' . $this->metaboxID, $this->metaboxID . '_nonce' );
		}

		$this->html( $obj, $metabox );
	}

	// Parent class only, let children class simply use func 'save_data'
	public function save_meta_box( $objID, $obj ) {

		// metabox set?
		if ( ! $this->metaboxID || ! isset( $_POST[ $this->metaboxID . '_nonce' ] ) || empty( $_POST[ $this->metaboxID . '_nonce' ] ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
			return;
		}
		// autosave? (legacy, probswill never do as our own page now)
		if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
			return; }
		// final nonce check
		if ( isset( $_POST[ $this->metaboxID . '_nonce' ] ) && wp_verify_nonce( $_POST[ $this->metaboxID . '_nonce' ], 'save_' . $this->metaboxID ) ) {

			// here we check if another metabox (e.g. main save record) has fired first + inserted, leaving us an id:)
			global $zbsJustInsertedMetaboxID;
			if ( $objID == -1 && isset( $zbsJustInsertedMetaboxID ) && ! empty( $zbsJustInsertedMetaboxID ) && $zbsJustInsertedMetaboxID > 0 ) {
				$objID = $zbsJustInsertedMetaboxID;
			}

			// Good to save, this has basically 'checked' the metabox nonce for us, so avoids child-classes having to :)
			$this->save_data( $objID, $obj );

		}

		return $obj;
	}

	// Parent class only, let children class simply use func 'post_save_data'
	public function post_save_meta_box( $objID, $obj ) {

		// simply fires this
		$this->post_save_data( $objID, $obj );

		return $obj;
	}

	// outputs zbs dal notice stack:
	public function dalNoticeMessage( $messages = array() ) {

		$this->dalOutputMessage( $messages, 'notice', __( 'General Notice', 'zero-bs-crm' ) );
	}

	// outputs zbs dal error stack:
	public function dalErrorMessage( $messages = array() ) {

		$this->dalOutputMessage( $messages, 'error', __( 'General Error', 'zero-bs-crm' ) );
	}

	// outputs html for a message stack
	public function dalOutputMessage( $messages = array(), $type = 'error', $fallback = '' ) {

		global $zbs;

		// type switch
		switch ( $type ) {

			case 'notice':
				$classes = 'info green mini';
				if ( empty( $fallback ) ) {
					$fallback = __( 'General Notice', 'zero-bs-crm' );
				}
				$msgHeader = __( 'Update Notice:', 'zero-bs-crm' );
				$ico       = 'sticky note outline';
				$id        = 'generalNotice';
				break;

			// errs (Default)
			default:
				$classes = 'info orange mini';
				if ( empty( $fallback ) ) {
					$fallback = __( 'General Error', 'zero-bs-crm' );
				}
				$msgHeader = $zbs->DAL->typeStr( $zbs->DAL->objTypeKey( $this->objType ) ) . ' ' . __( 'Could not be saved', 'zero-bs-crm' );
				$ico       = 'red frown outline';
				$id        = 'couldNotUpdate';
				break;

		}

		$msgDetail = '';
		if ( is_array( $messages ) ) {
			foreach ( $messages as $message ) {

				// < v3.0
				$messageStr = $message;

				// v3.0+ $message becomes a more thoughtful array
				if ( is_array( $message ) ) {

					// set this to stop php warnings if not got 'str' attr
					$messageStr = $fallback;

					// retrieve
					if ( isset( $message['str'] ) ) {
						$messageStr = $message['str'];
					}
				}

				$msgDetail .= $messageStr . '<br />';
			}
		}

		// genericified from DAL3.0
		$msg = zeroBSCRM_UI2_messageHTML( $classes, $msgHeader, $msgDetail, $ico, $id );

		$zbs->pageMessages[] = $msg;
	}
}

/*
======================================================
	/ Parent Class for all Metabox
	====================================================== */

/*
======================================================
	Global metabox helper funcs
	====================================================== */

// ZBS splintered version of add_meta_box (wholly our own from now on, do not assume can be swapped out directly, may need integration)
// https://developer.wordpress.org/reference/functions/add_meta_box/
function zeroBSCRM_add_meta_box( $id, $title, $callback, $screen = null, $context = 'advanced', $priority = 'default', $callback_args = null, $headless = false, $extraClasses = '', $capabilities = false ) {

	// $id, $title, $callback,$headless
	// echo 'zeroBSCRM_add_meta_box '.$id.':<pre>'; print_r(array('head'=>$headless)); echo '</pre>';

	global $zbs;

	// simply maintains this array $zbs->metaboxes
	// $zbs->metaboxes[]

	if ( empty( $screen ) ) {
		$screen = get_current_screen();
	} elseif ( is_string( $screen ) ) {
		// leave as is for zbs :)
		$screen = convert_to_screen( $screen );
	} elseif ( is_array( $screen ) ) {
		foreach ( $screen as $single_screen ) {
			zeroBSCRM_add_meta_box( $id, $title, $callback, $single_screen, $context, $priority, $callback_args, $headless, $extraClasses, $capabilities );
		}
	}

	if ( ! isset( $screen->id ) ) {
		return;
	}

	$page = $screen->id;

	if ( ! isset( $zbs->metaboxes ) ) {
		$zbs->metaboxes = array();
	}
	if ( ! isset( $zbs->metaboxes[ $page ] ) ) {
		$zbs->metaboxes[ $page ] = array();
	}
	if ( ! isset( $zbs->metaboxes[ $page ][ $context ] ) ) {
		$zbs->metaboxes[ $page ][ $context ] = array();
	}

	foreach ( array_keys( $zbs->metaboxes[ $page ] ) as $a_context ) {
		foreach ( array( 'high', 'core', 'default', 'low' ) as $a_priority ) {
			if ( ! isset( $zbs->metaboxes[ $page ][ $a_context ][ $a_priority ][ $id ] ) ) {
				continue;
			}

			// If a core box was previously added or removed by a plugin, don't add.
			if ( 'core' == $priority ) {
				// If core box previously deleted, don't add
				if ( false === $zbs->metaboxes[ $page ][ $a_context ][ $a_priority ][ $id ] ) {
					return;
				}

				/*
				 * If box was added with default priority, give it core priority to
				 * maintain sort order.
				 */
				if ( 'default' == $a_priority ) {
					$zbs->metaboxes[ $page ][ $a_context ]['core'][ $id ] = $zbs->metaboxes[ $page ][ $a_context ]['default'][ $id ];
					unset( $zbs->metaboxes[ $page ][ $a_context ]['default'][ $id ] );
				}
				return;
			}
			// If no priority given and id already present, use existing priority.
			if ( empty( $priority ) ) {
				$priority = $a_priority;
				/*
				* Else, if we're adding to the sorted priority, we don't know the title
				* or callback. Grab them from the previously added context/priority.
				*/
			} elseif ( 'sorted' == $priority ) {
				$title         = $zbs->metaboxes[ $page ][ $a_context ][ $a_priority ][ $id ]['title'];
				$callback      = $zbs->metaboxes[ $page ][ $a_context ][ $a_priority ][ $id ]['callback'];
				$callback_args = $zbs->metaboxes[ $page ][ $a_context ][ $a_priority ][ $id ]['args'];
				$headless      = $zbs->metaboxes[ $page ][ $a_context ][ $a_priority ][ $id ]['headless'];
				$extraClasses  = $zbs->metaboxes[ $page ][ $a_context ][ $a_priority ][ $id ]['extraclasses'];
				$capabilities  = $zbs->metaboxes[ $page ][ $a_context ][ $a_priority ][ $id ]['capabilities'];
			}
			// An id can be in only one priority and one context.
			if ( $priority != $a_priority || $context != $a_context ) {
				unset( $zbs->metaboxes[ $page ][ $a_context ][ $a_priority ][ $id ] );
			}
		}
	}

	if ( empty( $priority ) ) {
		$priority = 'low';
	}

	// default caps
	if ( ! is_array( $capabilities ) ) {
		$capabilities = array(

			'can_hide'        => false, // can be hidden
			'areas'           => array( 'normal', 'side' ), // areas can be dragged to - normal side = only areas currently
			'can_accept_tabs' => false,  // can/can't accept tabs onto it
			'can_become_tab'  => false, // can be added as tab
			'can_minimise'    => true, // can be minimised
			'can_move'        => true, // can be moved

		);
	}

	if ( ! isset( $zbs->metaboxes[ $page ][ $context ][ $priority ] ) ) {
		$zbs->metaboxes[ $page ][ $context ][ $priority ] = array();
	}

	$zbs->metaboxes[ $page ][ $context ][ $priority ][ $id ] = array(
		'id'           => $id,
		'title'        => $title,
		'callback'     => $callback,
		'args'         => $callback_args,
		'headless'     => $headless,
		'extraclasses' => $extraClasses,
		'capabilities' => $capabilities,
	);
}
/**
 * Meta-Box template function (ZBS MODIFIED VER)
 *
 * @global array $zbs->metaboxes
 *
 * @staticvar bool $already_sorted
 *
 * @param string $screen
 * @param string $context box context
 * @param mixed  $object  gets passed to the box callback function as first parameter
 * @return int number of meta_boxes
 */
function zeroBSCRM_do_meta_boxes( $screen, $context, $object ) {
		global $zbs;
		static $already_sorted = false;

	if ( empty( $screen ) ) {
			$screen = get_current_screen();
	} elseif ( is_string( $screen ) ) {
			$screen = convert_to_screen( $screen );
	}
		$page = $screen->id;

		// is this page a new edit:
		global $zbsPage;
		$isNewEdit = false;
	if ( isset( $zbsPage['new_edit'] ) ) {
		$isNewEdit = $zbsPage['new_edit']; // echo 'pre:<pre>'.print_r($zbsPage,1).'</pre>';
	}

		printf( '<div id="zbs-%s-sortables" class="zbs-metaboxes zbs-metabox-sortables">', esc_html( $context ) );
		/*
		This was the previous wp organisation, which nearly worked for us, except we now want tab groups :)
		$hidden = get_hidden_meta_boxes( $screen );
		// Grab the ones the user has manually sorted. Pull them out of their previous context/priority and into the one the user chose
		if ( ! $already_sorted && $sorted = get_user_option( "meta-box-order_$page" ) ) {
				foreach ( $sorted as $box_context => $ids ) {
						foreach ( explode( ',', $ids ) as $id ) {
								if ( $id && 'dashboard_browser_nag' !== $id ) {
										zeroBSCRM_add_meta_box( $id, null, null, $screen, $box_context, 'sorted' );
								}
						}
				}
		}
		*/

		// echo 'Screen: '.$zbs->pageKey.' # '.$context;
		// use ours
				$screenOpts = $zbs->global_screen_options(); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
		$hidden             = array();
	if ( is_array( $screenOpts ) && isset( $screenOpts['mb_hidden'] ) && is_array( $screenOpts['mb_hidden'] ) ) {
		$hidden = $screenOpts['mb_hidden'];
	}
		$minimised = array();
	if ( is_array( $screenOpts ) && isset( $screenOpts['mb_mini'] ) && is_array( $screenOpts['mb_mini'] ) ) {
		$minimised = $screenOpts['mb_mini'];
	}

		// Basically here
		// $zbs->metaboxes[ $page ][ $context ] = DEFAULTs
		// $screenOptionsList = USER DESIRED ORDER
		zeroBSCRM_applyScreenOptions( $screenOpts, $page, $context );

		$already_sorted = true;
		$i              = 0;
	if ( isset( $zbs->metaboxes[ $page ][ $context ] ) ) {
		foreach ( array( 'high', 'sorted', 'core', 'default', 'low' ) as $priority ) {
			if ( isset( $zbs->metaboxes[ $page ][ $context ][ $priority ] ) ) {
				foreach ( (array) $zbs->metaboxes[ $page ][ $context ][ $priority ] as $boxID => $box ) {
					if ( false == $box ) { // || ! $box['title']
						continue;
					}

						// hide_on_new (if this page is edit->new, and this metabox has hide_on_new - just don't load it.)
					if ( $isNewEdit && isset( $box['capabilities'] ) && isset( $box['capabilities']['hide_on_new'] ) && $box['capabilities']['hide_on_new'] ) {
						continue;
					}

						++$i;

						// if tab group
						// https://semantic-ui.com/modules/tab.html#/examples
					if ( isset( $box['istagroup'] ) ) {

							// this puts out header
							zeroBSCRM_do_meta_box_htmlTabHead( $boxID, $box );

							// this puts out each as body to header
							$indx = 0;
						foreach ( $box['boxes'] as $subbox ) {

							// active tab
							$isActive = false;
							if ( $indx == 0 ) {
								$isActive = true;
							}

							// final html - simple normal out
							zeroBSCRM_do_meta_box_html( $subbox, $page, $hidden, $object, $minimised, true, $isActive );

							++$indx;

						}
					} else {

									// final html - simple normal out
									zeroBSCRM_do_meta_box_html( $box, $page, $hidden, $object, $minimised );

					}
				}
			}
		}
	}
		echo '</div>';
		return $i;
}

function zeroBSCRM_applyScreenOptions( $screenOpts = false, $page = '', $context = '' ) {

	global $zbs;

	if ( ! is_array( $screenOpts ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
		$screenOpts = $zbs->global_screen_options(); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
	}
	$screenOptionsList = array();
	if ( is_array( $screenOpts ) && isset( $screenOpts[ 'mb_' . $context ] ) && is_array( $screenOpts[ 'mb_' . $context ] ) ) {
		$screenOptionsList = $screenOpts[ 'mb_' . $context ];
	}
	// not needed here: $hidden = array(); if (is_array($screenOpts) && isset($screenOpts['mb_hidden']) && is_array($screenOpts['mb_hidden'])) $hidden = $screenOpts['mb_hidden'];

	// Basically here
	// $zbs->metaboxes[ $page ][ $context ] = DEFAULTs
	// $screenOptionsList = USER DESIRED ORDER

	// if default + screenoptions order:
	if ( isset( $zbs->metaboxes[ $page ][ $context ] ) && isset( $screenOptionsList ) && is_array( $screenOptionsList ) && count( $screenOptionsList ) > 0 ) {

		// DEBUG echo '<h2>Global</h2><pre>'; print_r($zbs->metaboxes[ $page ][ $context ]); echo '</pre>';
		// DEBUG echo '<h2>User</h2><pre>'; print_r($screenOptionsList); echo '</pre>';

		// so here we merge the two into a MEGA list :)
		$metaboxes   = $zbs->metaboxes[ $page ][ $context ]; // array('high'=>array(), 'sorted'=>array(), 'core'=>array(), 'default'=>array(), 'low'=>array());
		$soMetaboxes = array();

		// for the sake of our metaboxes + simplicity
		// for now we'll only use to priority slots 'high','low'
		// ... leaving the others in place.
		// So:
		// - No screen options = load as normal, based on wp priorities (as added in metabox init)
		// - Screen options = load these in order user has saved (all under 'high')
		// ... when there is a new metabox added by us (so not in 'screenoptions') this is added either 'high' or 'low'
		// ... always at the end of whatever priority it is given in its metabox init :)

		foreach ( $screenOptionsList as $metaboxID => $metaboxVal ) {

			// these'll be in priority order
			// ... so we PICK them out of wherever they are, and add them to wherever they need to be
			// but because we just store the ID in the screenopts we have to do a bit of a dance..

			// NOTE this also has to catch TABS which are csv's of mb id's
			// here we check if tab
			$tabgroup = false;
			if ( $metaboxVal != 'self' ) {

				// is tab section
				$mbIDsToAdd = explode( ',', $metaboxVal );
				$tabgroup   = true;

			} else {

				// only one
				$mbIDsToAdd = array( $metaboxID );

			}

			// then we cycle through each (provided) id
			foreach ( $mbIDsToAdd as $mbIDToAdd ) {

					// first we find it in existing arrs
					$existingMB   = false;
					$fromPriority = false;

						// through each prioty
				foreach ( $metaboxes as $priority => $metaboxArr ) {

					// log source
					$fromPriority = $priority;

					// through each mb
					foreach ( $metaboxArr as $mbID => $mb ) {

								// found it
						if ( $mbIDToAdd == $mbID ) {

							// grab item
							$existingMB = $mb;
							break 2;

						}
					}
				}

						// if found it
				if ( $existingMB !== false ) {

					// remove it from current place
					unset( $metaboxes[ $fromPriority ][ $mbIDToAdd ] );

					if ( $tabgroup ) {

						// set if not set
						if ( ! isset( $soMetaboxes[ $metaboxID ] ) ) {
							$soMetaboxes[ $metaboxID ] = array();
						}
						if ( ! isset( $soMetaboxes[ $metaboxID ]['boxes'] ) ) {
							$soMetaboxes[ $metaboxID ]['boxes'] = array();
						}

						// add to our pile (of piles)
						$soMetaboxes[ $metaboxID ]['boxes'][ $mbIDToAdd ] = $existingMB;

						// make sure this flag is set
						$soMetaboxes[ $metaboxID ]['istagroup'] = true;

					} else {

						// add to our pile
						$soMetaboxes[ $metaboxID ] = $existingMB;

					}
				}
			} // each mb id (1 or tabs multi)

		}

		// now if we have any in our pile, we shoehorn them in at the top of high (for now)
		// so... when we add a new metabox (That people wont have saved in their screen opts)
		// ... it'll do this:
		// if priority = high - add to TOP
		// if priority != high - add at bottom
		if ( count( $soMetaboxes ) > 0 ) {

			$newHighPriority = array();

			// first clone in any in from other high priority array
			// (this'll catch where we've added new metaboxes that the user doesn't have in their screen opts)
			if ( isset( $metaboxes['high'] ) && is_array( $metaboxes['high'] ) && count( $metaboxes['high'] ) > 0 ) {
				foreach ( $metaboxes['high'] as $mbID => $mb ) {
					if ( ! in_array( $mbID, array_keys( $soMetaboxes ) ) ) {
						$newHighPriority[ $mbID ] = $mb;
					}
				}
			}

			// add ours :)
			$newHighPriority = array_merge( $newHighPriority, $soMetaboxes );

			// add any in from other priority arrays
			// (this'll catch where we've added new metaboxes that the user doesn't have in their screen opts)
			foreach ( array( 'sorted', 'core', 'default', 'low' ) as $priority ) {
				if ( isset( $metaboxes[ $priority ] ) && count( $metaboxes[ $priority ] ) > 0 ) {
					foreach ( $metaboxes[ $priority ] as $mbID => $mb ) {
						if ( ! in_array( $mbID, array_keys( $soMetaboxes ) ) ) {
							$newHighPriority[ $mbID ] = $mb;
						}
					}
				}
			}

			// then overwrite it! (effectively for )
			// actually needs to be totally overwritten $metaboxes['high'] = $newHighPriority;
			$zbs->metaboxes[ $page ][ $context ] = array( 'high' => $newHighPriority );

			// and clean up
			unset( $newHighPriority, $soMetaboxes, $metaboxes );

		}
	}
}

// https://semantic-ui.com/modules/tab.html#/examples
function zeroBSCRM_do_meta_box_htmlTabHead( $tabsID = '', $tabs = false ) {

	if ( $tabs !== false ) {

		// echo '<h1>'.$tabsID.'</h1><pre>';print_r($tabs); echo '</pre>';

		echo '<div class="ui top attached tabular menu zbs-metabox-tabgroup" id="zbs-metaboxtabs-' . esc_attr( $tabsID ) . '" data-tabid="' . esc_attr( $tabsID ) . '">';

			$indx = 0;

			// this puts out each as body to header
		foreach ( $tabs['boxes'] as $subbox ) {

			echo '<a class="item';
			if ( $indx == 0 ) {
				echo ' active';
			}
			echo '" data-tab="' . esc_attr( $subbox['id'] ) . '">' . esc_html( $subbox['title'] ) . '</a>';
			++$indx;

		}

		echo '</div>';

	}
}

function zeroBSCRM_do_meta_box_html( $box, $page, $hidden, $object, $minimised, $isTabPane = false, $isActiveTab = false ) {

		/*
		HTML adapted to suit semantic, from this, the previous wp code:
		$hidden_class = in_array($box['id'], $hidden) ? ' hide-if-js' : '';
		echo '<div id="' . $box['id'] . '" class="postbox ' . postbox_classes($box['id'], $page) . $hidden_class . '" ' . '>' . "\n";
		if ( 'dashboard_browser_nag' != $box['id'] ) {
				$widget_title = $box[ 'title' ];
				if ( is_array( $box[ 'args' ] ) && isset( $box[ 'args' ][ '__widget_basename' ] ) ) {
						$widget_title = $box[ 'args' ][ '__widget_basename' ];
						// Do not pass this parameter to the user callback function.
						unset( $box[ 'args' ][ '__widget_basename' ] );
				}
				echo '<button type="button" class="handlediv" aria-expanded="true">';
				echo '<span class="screen-reader-text">' . sprintf( __( 'Toggle panel: %s', 'zero-bs-crm' ), $widget_title ) . '</span>';
				echo '<span class="toggle-indicator" aria-hidden="true"></span>';
				echo '</button>';
		}
		echo "<h2 class='hndle'><span>{$box['title']}</span></h2>\n";
		echo '<div class="inside">' . "\n";
		call_user_func($box['callback'], $object, $box);
		echo "</div>\n";
		echo "</div>\n";
		*/

		$htmlClasses = 'ui segment';

		// headless?
		$headless = false;
	if ( isset( $box['headless'] ) && $box['headless'] ) {
		$headless = true;
	}

		// extra classes
		$extraClasses = '';
	if ( isset( $box['extraclasses'] ) && $box['extraclasses'] ) {
		$extraClasses = $box['extraclasses'];
	}

		// convert capabilites into data-atr
		// these get added to headed meta only...
		// Also set vars here e.g. canMinimise
		$dataAttrStr = '';
	$canMinimise     = false;
	$canHide         = false;
	$canMove         = false;
	if ( isset( $box['capabilities'] ) && is_array( $box['capabilities'] ) ) {

		if ( isset( $box['capabilities']['can_minimise'] ) && $box['capabilities']['can_minimise'] ) {
			$canMinimise = true;
		}
		if ( isset( $box['capabilities']['can_hide'] ) && $box['capabilities']['can_hide'] ) {
			$canHide = true;
		}
		if ( isset( $box['capabilities']['can_move'] ) && $box['capabilities']['can_move'] ) {
			$canMove = true;
		}

		foreach ( $box['capabilities'] as $capKey => $capVal ) {
			if ( ! empty( $dataAttrStr ) ) {
				$dataAttrStr .= ' ';
			}

			// arrays get csv'd
			$capVStr = '';
			if ( is_array( $capVal ) ) {
				$capVStr = implode( ',', $capVal );
			} else {
				$capVStr = $capVal;
			}

			$dataAttrStr .= 'data-' . str_replace( '_', '-', $capKey ) . '="' . $capVStr . '"';
		}
	}

		// classes
		$classes = 'zbs-metabox';
	$extraAttrs  = '';

		// hidden class
		$classes .= in_array( $box['id'], $hidden ) ? ' hide-if-js zbs-hidden' : '';

		// minimised class
		$classes .= in_array( $box['id'], $minimised ) ? ' zbs-minimised' : '';

		// static class
		$classes .= $canMove ? '' : ' zbs-static';

		// if is a tab pane...
		// class="ui bottom attached tab segment active" data-tab="first"
	if ( $isTabPane ) {
		$classes .= ' ui bottom attached tab segment';
		if ( $isActiveTab ) {
			$classes .= ' active';
		}
		$extraAttrs .= ' data-tab="' . $box['id'] . '"';

		// always
		$canMinimise = false;
		$headless    = true;
		$htmlClasses = '';
	}

		echo '<div class="' . esc_attr( $classes ) . '" id="' . esc_attr( $box['id'] ) . '" ' . esc_attr( $extraAttrs . $dataAttrStr ) . '>';

			// hide/minimise option
			$hideMinimiseMenu = '';
	if ( $canMinimise ) {

		// minimise - <i class="dropdown icon"></i>
		// now inc both carets, class presence turns on/off
		$hideMinimiseMenu = '<div class="ui right item zbs-metabox-minimise"><i class="caret up icon"></i><i class="caret down icon"></i></div>';

	}

	if ( ! $headless ) {
		echo '<div id="' . esc_attr( $box['id'] ) . '-head" class="zbs-metabox-head ui top attached borderless menu ' . esc_attr( $extraClasses . postbox_classes( $box['id'], $page ) ) . '" ' . '>' . "\n"; // . $hidden_class
		/*
				not sure if we need this :)
		if ( 'dashboard_browser_nag' != $box['id'] ) {
				$widget_title = $box[ 'title' ];
				if ( is_array( $box[ 'args' ] ) && isset( $box[ 'args' ][ '__widget_basename' ] ) ) {
						$widget_title = $box[ 'args' ][ '__widget_basename' ];
						// Do not pass this parameter to the user callback function.
						unset( $box[ 'args' ][ '__widget_basename' ] );
				}
				echo '<button type="button" class="handlediv" aria-expanded="true">';
				echo '<span class="screen-reader-text">' . sprintf( __( 'Toggle panel: %s', 'zero-bs-crm' ), $widget_title ) . '</span>';
				echo '<span class="toggle-indicator" aria-hidden="true"></span>';
				echo '</button>';
		} */

			// txt
			echo '<div class="header item">' . esc_html( $box['title'] ) . '</div>' . $hideMinimiseMenu . "\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- This is hard-coded HTML with no vars.

			// right hand menu, if one
			/*
					For now, was css glitching, can look later as don't need yet
			echo '<div class="right menu">';

				// drop down
				echo '<div class="ui dropdown icon item"><i class="angle double up icon"></i>';

					echo '<div class="menu">';

						echo '<div class="item">Testing</div>';

					echo '</div>';

				echo '</div>';// / dropdown

			echo '</div>';
			*/

		echo '</div>';

		$htmlClasses .= ' bottom attached';
	}
			echo '<div id="' . esc_attr( $box['id'] ) . '-box" class="zbs-metabox-body ' . esc_attr( $htmlClasses ) . ' ' . esc_attr( $extraClasses ) . '">' . "\n"; // $hidden_class.
				call_user_func( $box['callback'], $object, $box );
			echo '</div>'; // /.zbs-metabox-body
			echo '<div id="' . esc_attr( $box['id'] ) . '-block" class="zbs-metabox-block"><div>' . esc_html( $box['title'] ) . '</div></div>'; // this is BLOCKER for drag-drop support -//<i class="arrows alternate icon"></i>
		echo "</div>\n";
}

// retrieves metabox list for current page (all contexts)
// does so without priorities (e.g. high,low)
function zeroBSCRM_getCurrentMetaboxesFlatArr() {

	$mb               = array(
		'normal' => array(),
		'side'   => array(),
	);
	$mbWithPriorities = zeroBSCRM_getCurrentMetaboxes();

	if ( is_array( $mbWithPriorities ) ) {

		foreach ( array( 'normal', 'side' ) as $context ) {

			foreach ( array( 'high', 'sorted', 'core', 'default', 'low' ) as $priority ) {

				if ( isset( $mbWithPriorities[ $context ] ) && isset( $mbWithPriorities[ $context ][ $priority ] ) && is_array( $mbWithPriorities[ $context ][ $priority ] ) ) {

					$mb[ $context ] = array_merge( $mb[ $context ], $mbWithPriorities[ $context ][ $priority ] );
				}
			}
		}
	}

	return $mb;
}

// retrieves metabox list for current page (all contexts)
function zeroBSCRM_getCurrentMetaboxes() {

	global $zbs;

	$metaboxes = array();
	$pageKey   = $zbs->pageKey;

	// cycle through context/[priority]
	foreach ( array( 'normal', 'side' ) as $context ) {

		$mb = zeroBSCRM_getMetaboxes( $pageKey, $context );
		// echo 'pagekey:'.$pageKey.'+'.$context.'<br><pre>'; print_r($mb); echo '</pre>';

		if ( is_array( $mb ) && count( $mb ) > 0 ) {
			$metaboxes[ $context ] = $mb;
		}
	}

	return $metaboxes;
}

// retrieves metabox list for a page + context
// (NOTE: Does not sort by user screen options)
function zeroBSCRM_getMetaboxes( $page = '', $context = '' ) {

	global $zbs;

	if ( isset( $zbs->metaboxes[ $page ] ) && isset( $zbs->metaboxes[ $page ][ $context ] ) ) {
		return $zbs->metaboxes[ $page ][ $context ];
	}

	return array();
}
