<?php 
/*!
 * Jetpack CRM
 * https://jetpackcrm.com
 * Copyright 2021 Automattic
 */

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

/*
	
	WIP:

		each placeholder record will have an array such as:

	{placeholder_model}:

		array( 

			'description' 	  => 'Contact ID',
			'origin'		  => 'Contact Object model',
			'replace_str'	  => '##CONTACT-ID##',
			'aliases' 		  => array('##CID##'),
			'associated_type' => ZBS_TYPE_CONTACT,
			'expected_format' => 'str' // future proofing
			'available_in'	  => array(
					
					// tooling allowed to use this, from:
					// if specified, otherwise areas may use type to identify which placeholders are available
					'system_email_templates',
					'mail_campaigns',
					'quote' e.g:
						'quote_templates',
						'quote_editor',
					'invoice' e.g:
						'invoice_editor',
					'single_email',

			)

		)

*/

/**
* jpcrm_templating_placeholders is the placeholder layer in Jetpack CRM 4+
*
* @author   Woody Hayday <hello@jetpackcrm.com>
* @version  4.0
* @access   public
* @see      https://kb.jetpackcrm.com
*/
class jpcrm_templating_placeholders {

		// default set of placeholders, to get amended on init (Custom fields etc.)
		private $placeholders = array();

	// stores common links e.g. contact fields in quotes
	// for now these repreresent the 1:1 relational links in the db
	// later, as the DAL is extended, this could take advantage of 
	// a better mapping integral to the DAL, perhaps extending
	// the $linkedToObjectTypes system.
	// for now it's an exception, so hard-typing:
	private $available_in_links = array(

			'contact' => array(
				'quote',
				'invoice',
				'transaction',
				'event'
			),
			'company' => array(
				// 'quote',
				'invoice',
				'transaction',
				'event'
			)

	);

	// ===============================================================================
	// ===========  INIT =============================================================
	function __construct($args=array()) {

		// Build out list of placeholders
		$this->build_placeholders();

	}
	// ===========  / INIT ===========================================================
	// ===============================================================================


	/**
	 * Fills out default placeholders
	 * This is executed on init because we want to include the full translatable strings
	 * ... which we cannot set as constants because we want the __()
	 *
	 * @return array of all placeholders
	 */
	private function default_placeholders(){

		$this->placeholders = array(

			// CRM global placeholders, e.g. business name (from settings)
			'global' => array(

				'biz-name' => array(

					'description' => __( 'Business name', 'zero-bs-crm' ),
					'origin' => __( 'Global', 'zero-bs-crm' ),
					'expected_format' => 'str',
					'available_in' => array(),
					'associated_type' => false,
					'replace_str' => '##BIZ-NAME##',
					'aliases'			=> array( '###BIZNAME###', '##BIZNAME##' )
				),

				'biz-state' => array(

					'description' => __( 'Business state', 'zero-bs-crm' ),
					'origin' => __( 'Global', 'zero-bs-crm' ),
					'expected_format' => 'str',
					'available_in' => array(),
					'associated_type' => false,
					'replace_str' => '##BIZ-STATE##',
					'aliases'			=> array( '##BIZSTATE##' )
				),

				'biz-logo' => array(

					'description' => __( 'Business logo', 'zero-bs-crm' ),
					'origin' => __( 'Global', 'zero-bs-crm' ),
					'expected_format' => 'html',
					'available_in' => array(),
					'associated_type' => false,
					'replace_str' => '##BIZ-LOGO##'
				),

				'biz-info' => array(

					'description' 		=> __( 'Table with your business information (HTML)', 'zero-bs-crm' ),
					'origin' 			=> __( 'Global', 'zero-bs-crm' ),
					'expected_format' 	=> 'html',
					'available_in'	 	=> array(),
					'associated_type' 	=> false,
					'replace_str'		=> '##BIZ-INFO##',
					'aliases'			=> array( '###BIZINFOTABLE###', '###BIZ-INFO###', '###FOOTERBIZDEETS###', '##INVOICE-BIZ-INFO##' )
				),

				'biz-your-name' => array(

					'description'     => __( 'Business: Your Name', 'zero-bs-crm' ),
					'origin'          => __( 'Global', 'zero-bs-crm' ),
					'expected_format' => 'str',
					'available_in'    => array(),
					'associated_type' => false,
					'replace_str'     => '##BIZ-YOUR-NAME##',
					'aliases'         => array(),
				),

				'biz-your-email' => array(

					'description'     => __( 'Business: Your Email', 'zero-bs-crm' ),
					'origin'          => __( 'Global', 'zero-bs-crm' ),
					'expected_format' => 'str',
					'available_in'    => array(),
					'associated_type' => false,
					'replace_str'     => '##BIZ-YOUR-EMAIL##',
					'aliases'         => array(),
				),

				'biz-your-url' => array(

					'description'     => __( 'Business: Your URL', 'zero-bs-crm' ),
					'origin'          => __( 'Global', 'zero-bs-crm' ),
					'expected_format' => 'str',
					'available_in'    => array(),
					'associated_type' => false,
					'replace_str'     => '##BIZ-YOUR-URL##',
					'aliases'         => array(),
				),

				'biz-extra'          => array(

					'description'     => __( 'Business: Extra Info', 'zero-bs-crm' ),
					'origin'          => __( 'Global', 'zero-bs-crm' ),
					'expected_format' => 'str',
					'available_in'    => array(),
					'associated_type' => false,
					'replace_str'     => '##BIZ-EXTRA-INFO##',
					'aliases'         => array(),
				),

				'social-links' => array(

					'description' => __( 'Social links', 'zero-bs-crm' ),
					'origin' => __( 'Global', 'zero-bs-crm' ),
					'expected_format' => 'html',
					'available_in' => array(),
					'associated_type' => false,
					'replace_str' => '##SOCIAL-LINKS##'
				),

				'unsub-line' => array(

					'description' => __( 'Unsubscribe line', 'zero-bs-crm' ),
					'origin' => __( 'Global', 'zero-bs-crm' ),
					'expected_format' => 'html',
					'available_in' => array(),
					'associated_type' => false,
					'replace_str' => '##UNSUB-LINE##',
					'aliases'	=> array( '###UNSUB-LINE###', '###UNSUB###', '###UNSUBSCRIBE###', '###FOOTERUNSUBDEETS###' )
				),

				'powered-by' => array(

					'description' => __( 'Powered by', 'zero-bs-crm' ),
					'origin' => __( 'Global', 'zero-bs-crm' ),
					'expected_format' => 'html',
					'available_in' => array(),
					'associated_type' => false,
					'replace_str' => '##POWERED-BY##',
					'aliases'	=> array( '###POWEREDBYDEETS###' )
				),

				'login-link' => array(

					'description' => __( 'CRM login link (HTML)', 'zero-bs-crm' ),
					'origin' => __( 'Global', 'zero-bs-crm' ),
					'expected_format' => 'html',
					'available_in' => array(),
					'associated_type' => false,
					'replace_str' => '##LOGIN-LINK##',
					'aliases'	=> array( '###LOGINLINK###' )
				),

				'login-button' => array(

					'description' => __( 'CRM login link button (HTML)', 'zero-bs-crm' ),
					'origin' => __( 'Global', 'zero-bs-crm' ),
					'expected_format' => 'html',
					'available_in' => array(),
					'associated_type' => false,
					'replace_str' => '##LOGIN-BUTTON##',
					'aliases'	=> array( '###LOGINBUTTON###' )
				),

				'login-url' => array(

					'description' => __( 'CRM login URL', 'zero-bs-crm' ),
					'origin' => __( 'Global', 'zero-bs-crm' ),
					'expected_format' => 'str',
					'available_in' => array(),
					'associated_type' => false,
					'replace_str' => '##LOGIN-URL##',
					'aliases'	=> array( '###LOGINURL###', '###ADMINURL###' )
				),

				'portal-link' => array(

					'description' => __( 'Portal link (HTML)', 'zero-bs-crm' ),
					'origin' => __( 'Global', 'zero-bs-crm' ),
					'expected_format' => 'html',
					'available_in' => array(),
					'associated_type' => false,
					'replace_str' => '##PORTAL-LINK##',
					'aliases'	=> array( '###PORTALLINK###' )
				),

				'portal-view-button' => array(

					'description' => __( '"View in Portal" button (HTML)', 'zero-bs-crm' ),
					'origin' => __( 'Global', 'zero-bs-crm' ),
					'expected_format' => 'html',
					'available_in' => array(),
					'associated_type' => false,
					'replace_str' => '##PORTAL-VIEW-BUTTON##',
					'aliases'	=> array( '###VIEWINPORTAL###' )
				),

				'portal-button' => array(

					'description' => __( 'Portal link button (HTML)', 'zero-bs-crm' ),
					'origin' => __( 'Global', 'zero-bs-crm' ),
					'expected_format' => 'html',
					'available_in' => array(),
					'associated_type' => false,
					'replace_str' => '##PORTAL-BUTTON##',
					'aliases'	=> array( '###PORTALBUTTON###' )
				),

				'portal-url' => array(

					'description' => __( 'Portal URL', 'zero-bs-crm' ),
					'origin' => __( 'Global', 'zero-bs-crm' ),
					'expected_format' => 'str',
					'available_in' => array(),
					'associated_type' => false,
					'replace_str' => '##PORTAL-URL##',
					'aliases'	=> array( '###PORTALURL###' )
				),


				'css' => array(

					'description' => __( 'CSS (restricted to HTML templates)', 'zero-bs-crm' ),
					'origin' => __( 'Global', 'zero-bs-crm' ),
					'expected_format' => 'html',
					'available_in' => array(),
					'associated_type' => false,
					'replace_str' => '##CSS##',
					'aliases'	=> array( '###CSS###' )
				),

				'title' => array(

					'description' => __( 'Generally used to fill HTML title tags', 'zero-bs-crm' ),
					'origin' => __( 'Global', 'zero-bs-crm' ),
					'expected_format' => 'str',
					'available_in' => array(),
					'associated_type' => false,
					'replace_str' => '##TITLE##',
					'aliases'	=> array( '###TITLE###' )
				),

				'msg-content' => array(

					'description' => __( 'Message content (restricted to some email templates)', 'zero-bs-crm' ),
					'origin' => __( 'Global', 'zero-bs-crm' ),
					'expected_format' => 'html',
					'available_in' => array(),
					'associated_type' => false,
					'replace_str' => '##MSG-CONTENT##',
					'aliases'	=> array( '###MSGCONTENT###' )
				),

				'email' => array(

					'description' => __( 'Email (generally used to insert user email)', 'zero-bs-crm' ),
					'origin' => __( 'Global', 'zero-bs-crm' ),
					'expected_format' => 'str',
					'available_in' => array(),
					'associated_type' => false,
					'replace_str' => '##EMAIL##',
					'aliases'	=> array( '###EMAIL###' )
				),

				'password' => array(

					'description' => __( 'Inserts a link where one can reset their password', 'zero-bs-crm' ),
					'origin' => __( 'Global', 'zero-bs-crm' ),
					'expected_format' => 'str',
					'available_in' => array(),
					'associated_type' => false,
					'replace_str' => '##PASSWORD##',
					'aliases'	=> array( '##PASSWORD-RESET-LINK##', '###PASSWORD###' )
				),

				'email-from-name' => array(

					'description' 		=> __( '"From" Name when sending email', 'zero-bs-crm' ),
					'origin' 			=> __( 'Global', 'zero-bs-crm' ),
					'expected_format' 	=> 'email',
					'available_in'	 	=> array(),
					'associated_type' 	=> false,
					'replace_str'		=> '##EMAIL-FROM-NAME##',
					'aliases'			=> array( '###FROMNAME###' )
				),

			), 

			'contact' => array(

				'contact-fullname' => array(

					'description' 		=> __( 'Contact full name', 'zero-bs-crm' ),
					'origin' 			=> __( 'Contact Information', 'zero-bs-crm' ),
					'available_in' 		=> array(),
					'associated_type' 	=> ZBS_TYPE_CONTACT,
					'replace_str' 		=> '##CONTACT-FULLNAME##',
					'expected_format' 	=> 'str',
					'aliases' 			=> array( '##CUSTOMERNAME##', '##CUSTOMER-FULLNAME##' )
				),

			),
			'company' => array(

			),
			'quote' => array(

				'quote-content' => array(

					'description' 		=> __( 'Quote content (HTML)', 'zero-bs-crm' ),
					'origin' 			=> __( 'Quote Builder', 'zero-bs-crm' ),
					'expected_format' 	=> 'html',
					'available_in'	 	=> array(),
					'associated_type' 	=> ZBS_TYPE_QUOTE,
					'replace_str'		=> '##QUOTE-CONTENT##',
					'aliases'			=> array( '###QUOTECONTENT###' )
				),

				'quote-title' => array(

					'description' 		=> __( 'Quote title', 'zero-bs-crm' ),
					'origin' 			=> __( 'Quote Builder', 'zero-bs-crm' ),
					'expected_format' 	=> 'str',
					'available_in'	 	=> array(),
					'associated_type' 	=> ZBS_TYPE_QUOTE,
					'replace_str'		=> '##QUOTE-TITLE##',
					'aliases'			=> array( '###QUOTETITLE###', '##QUOTETITLE##' )
				),

				'quote-value' => array(

					'description' 		=> __( 'Quote value', 'zero-bs-crm' ),
					'origin' 			=> __( 'Quote Builder', 'zero-bs-crm' ),
					'expected_format' 	=> 'str',
					'available_in'	 	=> array(),
					'associated_type' 	=> ZBS_TYPE_QUOTE,
					'replace_str'		=> '##QUOTE-VALUE##',
					'aliases'			=> array( '###QUOTEVALUE###', '##QUOTEVALUE##' )
				),

				'quote-date' => array(

					'description' 		=> __( 'Quote date', 'zero-bs-crm' ),
					'origin' 			=> __( 'Quote Builder', 'zero-bs-crm' ),
					'expected_format' 	=> 'str',
					'available_in'	 	=> array(),
					'associated_type' 	=> ZBS_TYPE_QUOTE,
					'replace_str'		=> '##QUOTE-DATE##',
					'aliases'			=> array( '###QUOTEDATE###', '##QUOTEDATE##' )
				),

				'quote-url' => array(

					'description' 		=> __( 'Quote URL', 'zero-bs-crm' ),
					'origin' 			=> __( 'Quote Builder', 'zero-bs-crm' ),
					'expected_format' 	=> 'str',
					'available_in'	 	=> array(),
					'associated_type' 	=> ZBS_TYPE_QUOTE,
					'replace_str'		=> '##QUOTE-URL##',
					'aliases'			=> array( '###QUOTEURL###' )
				),

				'quote-edit-url' => array(

					'description' 		=> __( 'Quote edit URL', 'zero-bs-crm' ),
					'origin' 			=> __( 'Quote Builder', 'zero-bs-crm' ),
					'expected_format' 	=> 'str',
					'available_in'	 	=> array(),
					'associated_type' 	=> ZBS_TYPE_QUOTE,
					'replace_str'		=> '##QUOTE-EDIT-URL##',
					'aliases'			=> array( '###QUOTEEDITURL###' )
				),

			),
			'invoice' => array(

				'invoice-title' => array(

					'description' 		=> __( 'Invoice title', 'zero-bs-crm' ),
					'origin' 			=> __( 'Invoice Builder', 'zero-bs-crm' ),
					'expected_format' 	=> 'str',
					'available_in'	 	=> array(),
					'associated_type' 	=> ZBS_TYPE_INVOICE,
					'replace_str'		=> '##INVOICE-TITLE##',
					'aliases'			=> array( '###INVOICETITLE###' )
				),

				'logo-class' => array(

					'description' 		=> __( 'Invoice logo CSS class', 'zero-bs-crm' ),
					'origin' 			=> __( 'Invoice Builder', 'zero-bs-crm' ),
					'expected_format' 	=> 'str',
					'available_in'	 	=> array(),
					'associated_type' 	=> ZBS_TYPE_INVOICE,
					'replace_str'		=> '##LOGO-CLASS##',
					'aliases'			=> array( '###LOGOCLASS###' )
				),

				'logo-url' => array(

					'description' 		=> __( 'Invoice logo URL', 'zero-bs-crm' ),
					'origin' 			=> __( 'Invoice Builder', 'zero-bs-crm' ),
					'expected_format' 	=> 'str',
					'available_in'	 	=> array(),
					'associated_type' 	=> ZBS_TYPE_INVOICE,
					'replace_str'		=> '##LOGO-URL##',
					'aliases'			=> array( '###LOGOURL###' )
				),

				'invoice-number' => array(

					'description' 		=> __( 'Invoice number', 'zero-bs-crm' ),
					'origin' 			=> __( 'Invoice Builder', 'zero-bs-crm' ),
					'expected_format' 	=> 'str',
					'available_in'	 	=> array(),
					'associated_type' 	=> ZBS_TYPE_INVOICE,
					'replace_str'		=> '##INVOICE-NUMBER##',
					'aliases'			=> array( '###INVNOSTR###' )
				),

				'invoice-date' => array(

					'description' 		=> __( 'Invoice date', 'zero-bs-crm' ),
					'origin' 			=> __( 'Invoice Builder', 'zero-bs-crm' ),
					'expected_format' 	=> 'str',
					'available_in'	 	=> array(),
					'associated_type' 	=> ZBS_TYPE_INVOICE,
					'replace_str'		=> '##INVOICE-DATE##',
					'aliases'			=> array( '###INVDATESTR###' )
				),

				'invoice-id-styles' => array(

					'description' 		=> __( 'Invoice ID styles', 'zero-bs-crm' ),
					'origin' 			=> __( 'Invoice Builder', 'zero-bs-crm' ),
					'expected_format' 	=> 'str',
					'available_in'	 	=> array(),
					'associated_type' 	=> ZBS_TYPE_INVOICE,
					'replace_str'		=> '##INVOICE-ID-STYLES##',
					'aliases'			=> array( '###INVIDSTYLES###' )
				),

				'invoice-ref' => array(

					'description' 		=> __( 'Invoice reference', 'zero-bs-crm' ),
					'origin' 			=> __( 'Invoice Builder', 'zero-bs-crm' ),
					'expected_format' 	=> 'str',
					'available_in'	 	=> array(),
					'associated_type' 	=> ZBS_TYPE_INVOICE,
					'replace_str'		=> '##INVOICE-REF##',
					'aliases'			=> array( '###REF###' )
				),

				'invoice-due-date' => array(

					'description' 		=> __( 'Invoice due date', 'zero-bs-crm' ),
					'origin' 			=> __( 'Invoice Builder', 'zero-bs-crm' ),
					'expected_format' 	=> 'str',
					'available_in'	 	=> array(),
					'associated_type' 	=> ZBS_TYPE_INVOICE,
					'replace_str'		=> '##INVOICE-DUE-DATE##',
					'aliases'			=> array( '###DUEDATE###' )
				),

				'invoice-biz-class' => array(

					'description' 		=> __( 'CSS class for table with your business info', 'zero-bs-crm' ),
					'origin' 			=> __( 'Invoice Builder', 'zero-bs-crm' ),
					'expected_format' 	=> 'str',
					'available_in'	 	=> array(),
					'associated_type' 	=> ZBS_TYPE_INVOICE,
					'replace_str'		=> '##INVOICE-BIZ-CLASS##',
					'aliases'			=> array( '###BIZCLASS###' )
				),

				'invoice-customer-info' => array(

					'description' 		=> __( 'Table with assigned contact information (HTML)', 'zero-bs-crm' ),
					'origin' 			=> __( 'Invoice Builder', 'zero-bs-crm' ),
					'expected_format' 	=> 'html',
					'available_in'	 	=> array(),
					'associated_type' 	=> ZBS_TYPE_INVOICE,
					'replace_str'		=> '##INVOICE-CUSTOMER-INFO##',
					'aliases'			=> array( '###CUSTINFOTABLE###' )
				),

				'invoice-from-name' => array(

					'description' 		=> __( 'Name of company issuing the invoice', 'zero-bs-crm' ),
					'origin' 			=> __( 'Statement Builder', 'zero-bs-crm' ),
					'expected_format' 	=> 'str',
					'available_in'	 	=> array(),
					'associated_type' 	=> ZBS_TYPE_INVOICE,
					'replace_str'		=> '##INVOICE-FROM-NAME##',
					'aliases'			=> array()
				),

				'invoice-table-headers' => array(

					'description' 		=> __( 'Table headers for invoice line items (HTML)', 'zero-bs-crm' ),
					'origin' 			=> __( 'Invoice Builder', 'zero-bs-crm' ),
					'expected_format' 	=> 'html',
					'available_in'	 	=> array(),
					'associated_type' 	=> ZBS_TYPE_INVOICE,
					'replace_str'		=> '##INVOICE-TABLE-HEADERS##',
					'aliases'			=> array( '###TABLEHEADERS###' )
				),

				'invoice-line-items' => array(

					'description' 		=> __( 'Invoice line items (HTML)', 'zero-bs-crm' ),
					'origin' 			=> __( 'Invoice Builder', 'zero-bs-crm' ),
					'expected_format' 	=> 'html',
					'available_in'	 	=> array(),
					'associated_type' 	=> ZBS_TYPE_INVOICE,
					'replace_str'		=> '##INVOICE-LINE-ITEMS##',
					'aliases'			=> array( '###LINEITEMS###' )
				),

				'invoice-totals-table' => array(

					'description' 		=> __( 'Invoice totals table (HTML)', 'zero-bs-crm' ),
					'origin' 			=> __( 'Invoice Builder', 'zero-bs-crm' ),
					'expected_format' 	=> 'html',
					'available_in'	 	=> array(),
					'associated_type' 	=> ZBS_TYPE_INVOICE,
					'replace_str'		=> '##INVOICE-TOTALS-TABLE##',
					'aliases'			=> array( '###TOTALSTABLE###' )
				),

				'pre-invoice-payment-details' => array(

					'description' 		=> __( 'Text before invoice payment details', 'zero-bs-crm' ),
					'origin' 			=> __( 'Invoice Builder', 'zero-bs-crm' ),
					'expected_format' 	=> 'html',
					'available_in'	 	=> array(),
					'associated_type' 	=> ZBS_TYPE_INVOICE,
					'replace_str'		=> '##PRE-INVOICE-PAYMENT-DETAILS##',
					'aliases'			=> array( '###PREPARTIALS###' )
				),

				'invoice-payment-details' => array(

					'description' 		=> __( 'Invoice payment details', 'zero-bs-crm' ),
					'origin' 			=> __( 'Invoice Builder', 'zero-bs-crm' ),
					'expected_format' 	=> 'html',
					'available_in'	 	=> array(),
					'associated_type' 	=> ZBS_TYPE_INVOICE,
					'replace_str'		=> '##INVOICE-PAYMENT-DETAILS##',
					'aliases'			=> array( '###PAYMENTDEETS###', '###PAYDETAILS###', )
				),

				'invoice-partials-table' => array(

					'description' 		=> __( 'Invoice partials table (HTML)', 'zero-bs-crm' ),
					'origin' 			=> __( 'Invoice Builder', 'zero-bs-crm' ),
					'expected_format' 	=> 'html',
					'available_in'	 	=> array(),
					'associated_type' 	=> ZBS_TYPE_INVOICE,
					'replace_str'		=> '##INVOICE-PARTIALS-TABLE##',
					'aliases'			=> array( '###PARTIALSTABLE###' )
				),

				'invoice-label-inv-number' => array(

					'description' 		=> __( 'Label for invoice number', 'zero-bs-crm' ),
					'origin' 			=> __( 'Invoice Builder', 'zero-bs-crm' ),
					'expected_format' 	=> 'str',
					'available_in'	 	=> array(),
					'associated_type' 	=> ZBS_TYPE_INVOICE,
					'replace_str'		=> '##INVOICE-LABEL-INV-NUMBER##',
					'aliases'			=> array( '###LANGINVNO###' )
				),

				'invoice-label-inv-date' => array(

					'description' 		=> __( 'Label for invoice date', 'zero-bs-crm' ),
					'origin' 			=> __( 'Invoice Builder', 'zero-bs-crm' ),
					'expected_format' 	=> 'str',
					'available_in'	 	=> array(),
					'associated_type' 	=> ZBS_TYPE_INVOICE,
					'replace_str'		=> '##INVOICE-LABEL-INV-DATE##',
					'aliases'			=> array( '###LANGINVDATE###' )
				),

				'invoice-label-inv-ref' => array(

					'description' 		=> __( 'Label for invoice reference', 'zero-bs-crm' ),
					'origin' 			=> __( 'Invoice Builder', 'zero-bs-crm' ),
					'expected_format' 	=> 'str',
					'available_in'	 	=> array(),
					'associated_type' 	=> ZBS_TYPE_INVOICE,
					'replace_str'		=> '##INVOICE-LABEL-INV-REF##',
					'aliases'			=> array( '###LANGINVREF###' )
				),

				'invoice-label-from' => array(

					'description' 		=> __( 'Label for name of company issuing invoice', 'zero-bs-crm' ),
					'origin' 			=> __( 'Invoice Builder', 'zero-bs-crm' ),
					'expected_format' 	=> 'str',
					'available_in'	 	=> array(),
					'associated_type' 	=> ZBS_TYPE_INVOICE,
					'replace_str'		=> '##INVOICE-LABEL-FROM##',
					'aliases'			=> array( '###LANGFROM###' )
				),

				'invoice-label-to' => array(

					'description'     => __( 'Label for name of contact receiving invoice', 'zero-bs-crm' ),
					'origin' 			=> __( 'Invoice Builder', 'zero-bs-crm' ),
					'expected_format' 	=> 'str',
					'available_in'	 	=> array(),
					'associated_type' 	=> ZBS_TYPE_INVOICE,
					'replace_str'		=> '##INVOICE-LABEL-TO##',
					'aliases'			=> array( '###LANGTO###' )
				),

				'invoice-label-due-date' => array(

					'description' 		=> __( 'Label for invoice due date', 'zero-bs-crm' ),
					'origin' 			=> __( 'Invoice Builder', 'zero-bs-crm' ),
					'expected_format' 	=> 'str',
					'available_in'	 	=> array(),
					'associated_type' 	=> ZBS_TYPE_INVOICE,
					'replace_str'		=> '##INVOICE-LABEL-DUE-DATE##',
					'aliases'			=> array( '###LANGDUEDATE###' )
				),

				'invoice-label-status' => array(

					'description' 		=> __( 'Label for invoice status', 'zero-bs-crm' ),
					'origin' 			=> __( 'Invoice Builder', 'zero-bs-crm' ),
					'expected_format' 	=> 'str',
					'available_in'	 	=> array(),
					'associated_type' 	=> ZBS_TYPE_INVOICE,
					'replace_str'		=> '##INVOICE-LABEL-STATUS##',
					'aliases'			=> array()
				),

				'invoice-html-status' => array(

					'description' 		=> __( 'Invoice status (HTML)', 'zero-bs-crm' ),
					'origin' 			=> __( 'Invoice Builder', 'zero-bs-crm' ),
					'expected_format' 	=> 'html',
					'available_in'	 	=> array(),
					'associated_type' 	=> ZBS_TYPE_INVOICE,
					'replace_str'		=> '##INVOICE-HTML-STATUS##',
					'aliases'			=> array( '###TOPSTATUS###' )
				),

				'invoice-pay-button' => array(

					'description' 		=> __( 'Invoice pay button (HTML)', 'zero-bs-crm' ),
					'origin' 			=> __( 'Invoice Builder', 'zero-bs-crm' ),
					'expected_format' 	=> 'html',
					'available_in'	 	=> array(),
					'associated_type' 	=> ZBS_TYPE_INVOICE,
					'replace_str'		=> '##INVOICE-PAY-BUTTON##',
					'aliases'			=> array( '###PAYPALBUTTON###' )
				),

				'invoice-pay-thanks' => array(

					'description' 		=> __( 'Invoice payment thanks message (HTML)', 'zero-bs-crm' ),
					'origin' 			=> __( 'Invoice Builder', 'zero-bs-crm' ),
					'expected_format' 	=> 'html',
					'available_in'	 	=> array(),
					'associated_type' 	=> ZBS_TYPE_INVOICE,
					'replace_str'		=> '##INVOICE-PAY-THANKS##',
					'aliases'			=> array( '###PAYTHANKS###' )
				),

				'invoice-pay-terms' => array(

					'description' 		=> __( 'Invoice payment terms (HTML)', 'zero-bs-crm' ),
					'origin' 			=> __( 'Invoice Builder', 'zero-bs-crm' ),
					'expected_format' 	=> 'html',
					'available_in'	 	=> array(),
					'associated_type' 	=> ZBS_TYPE_INVOICE,
					'replace_str'		=> '##INVOICE-PAY-TERMS##',
					'aliases'			=> array( '###PAYMENTTERMS###' )
				),

				'invoice-statement-html' => array(

					'description' 		=> __( 'Invoice statement (HTML)', 'zero-bs-crm' ),
					'origin' 			=> __( 'Statement Builder', 'zero-bs-crm' ),
					'expected_format' 	=> 'html',
					'available_in'	 	=> array(),
					'associated_type' 	=> ZBS_TYPE_INVOICE,
					'replace_str'		=> '##INVOICE-STATEMENT-HTML##',
					'aliases'			=> array( '###STATEMENTHTML###' )
				),

				'invoice-ref-styles' => array(

					'description' 		=> __( 'CSS attributes applied to invoice reference label', 'zero-bs-crm' ),
					'origin' 			=> __( 'Statement Builder', 'zero-bs-crm' ),
					'expected_format' 	=> 'str',
					'available_in'	 	=> array(),
					'associated_type' 	=> ZBS_TYPE_INVOICE,
					'replace_str'		=> '##INVOICE-REF-STYLES##',
					'aliases'			=> array( '###INVREFSTYLES###' )
				),

				'invoice-custom-fields' => array(

					'description' 		=> __( 'Any custom fields associated with invoice (if enabled in Invoice Settings)', 'zero-bs-crm' ),
					'origin' 			=> __( 'Invoice Builder', 'zero-bs-crm' ),
					'expected_format' 	=> 'str',
					'available_in'	 	=> array(),
					'associated_type' 	=> ZBS_TYPE_INVOICE,
					'replace_str'		=> '##INV-CUSTOM-FIELDS##',
					'aliases'			=> array()
				),



			),
			'transaction' => array(

			),
			'event' => array(

				'task-title' => array(

					'description' 		=> __( 'Task title', 'zero-bs-crm' ),
					'origin' 			=> __( 'Task Scheduler', 'zero-bs-crm' ),
					'expected_format' 	=> 'str',
					'available_in'	 	=> array(),
					'associated_type' 	=> ZBS_TYPE_TASK,
					'replace_str'		=> '##TASK-TITLE##',
					'aliases'			=> array( '###EVENTTITLE###', '##EVENT-TITLE##' )
				),

				'task-link' => array(

					'description' 		=> __( 'Task link (HTML)', 'zero-bs-crm' ),
					'origin' 			=> __( 'Task Scheduler', 'zero-bs-crm' ),
					'expected_format' 	=> 'html',
					'available_in'	 	=> array(),
					'associated_type' 	=> ZBS_TYPE_TASK,
					'replace_str'		=> '##TASK-LINK##',
					'aliases'			=> array( '###EVENTLINK###' )
				),

				'task-link-button' => array(

					'description' 		=> __( 'Task link button (HTML)', 'zero-bs-crm' ),
					'origin' 			=> __( 'Task Scheduler', 'zero-bs-crm' ),
					'expected_format' 	=> 'html',
					'available_in'	 	=> array(),
					'associated_type' 	=> ZBS_TYPE_TASK,
					'replace_str'		=> '##TASK-LINK-BUTTON##',
					'aliases'			=> array( '###EVENTLINKBUTTON###' )
				),

				'task-body' => array(

					'description' 		=> __( 'Task content (HTML)', 'zero-bs-crm' ),
					'origin' 			=> __( 'Task Scheduler', 'zero-bs-crm' ),
					'expected_format' 	=> 'html',
					'available_in'	 	=> array(),
					'associated_type' 	=> ZBS_TYPE_TASK,
					'replace_str'		=> '##TASK-BODY##',
					'aliases'			=> array( '###EVENTBODY###' )
				),


			),

			// probably not req. yet:
			/*
			'address',
			'form',
			'segment',
			'log',
			'lineitem',
			'eventreminder',
			'quotetemplate',
			*/

		);

		return $this->placeholders;

	}


	/**
	 * Builds initial list of placeholders using defaults, object-models, custom fields, and filters
	 *
	 * @return array of all placeholders
	 */
	public function build_placeholders( $include_custom_fields = true ){

		global $zbs;

		// any hard-typed defaults
		$placeholders = $this->default_placeholders();

		// load fields from DAL object models
		$placeholders = $this->load_from_object_models( $placeholders, $include_custom_fields );

		// some backward compat tweaks:
		if ( isset( $placeholders['quote']['title'] ) ) $placeholders['quote']['title']['aliases'] = array( '##QUOTE-TITLE##', '##QUOTETITLE##' );
		if ( isset( $placeholders['quote']['value'] ) ) $placeholders['quote']['value']['aliases'] = array( '##QUOTEVALUE##' );
		if ( isset( $placeholders['quote']['date'] ) ) $placeholders['quote']['date']['aliases'] = array( '##QUOTEDATE##' );

		// add setting dependent placeholders
		$placeholders = $this->add_setting_dependent_placeholders( $placeholders );

		// here we fill in any 'available-in' crossovers
		// e.g. you can reference contacts in quotes.
		$placeholders = $this->add_available_in( $placeholders );

		// filter (to allow extensions to ammend)
		$this->placeholders = apply_filters( 'jpcrm_templating_placeholders', $placeholders );

		// return
		return $this->placeholders;

	}


	/**
	 * Add setting-dependent placeholders
	 *
	 * @return array of all placeholders
	 */
	public function add_setting_dependent_placeholders( $placeholders = array() ){

		// remove tooling where inactive modules
		$placeholders = $this->strip_inactive_tooling( $placeholders );

		// if ownership model enabled, add 'owner' fields
		// this is simplistic for now and appears outwardly to only encompasses contacts
		// ... in fact it works for all object types (provided the object is passed to replace_placeholders())
		// as per #1531 this could later be expanded to encompass all objects, perhaps in sites & teams extension
    	$using_ownership = zeroBSCRM_getSetting( 'perusercustomers' );
		if ( $using_ownership ){

			if ( isset( $placeholders['global'] ) ){

				// note, these are auto-populated using the first `owner` field that our replacement function comes across in $replacement_objects
				$placeholders['global']['assigned-to-name'] = array(

					'description' 		=> __( 'Where available, the display name of the WordPress user who owns the object', 'zero-bs-crm' ),
					'origin' 			=> __( 'Global', 'zero-bs-crm' ),
					'expected_format' 	=> 'str',
					'available_in'	 	=> array(),
					'associated_type' 	=> false,
					'replace_str'		=> '##ASSIGNED-TO-NAME##',
					'aliases'			=> array( '##ASSIGNED-TO-SIGNATURE##' )
				);

				$placeholders['global']['assigned-to-email'] = array(

					'description' 		=> __( 'Where available, the email of the WordPress user who owns the object', 'zero-bs-crm' ),
					'origin' 			=> __( 'Global', 'zero-bs-crm' ),
					'expected_format' 	=> 'email',
					'available_in'	 	=> array(),
					'associated_type' 	=> false,
					'replace_str'		=> '##ASSIGNED-TO-EMAIL##',
					'aliases'			=> array()
				);

				$placeholders['global']['assigned-to-username'] = array(

					'description' 		=> __( 'Where available, the username of the WordPress user who owns the object', 'zero-bs-crm' ),
					'origin' 			=> __( 'Global', 'zero-bs-crm' ),
					'expected_format' 	=> 'email',
					'available_in'	 	=> array(),
					'associated_type' 	=> false,
					'replace_str'		=> '##ASSIGNED-TO-USERNAME##',
					'aliases'			=> array()
				);

				$placeholders['global']['assigned-to-mob'] =  array(

					'description' 		=> __( 'Where available, the mobile phone of the WordPress user who owns the object', 'zero-bs-crm' ),
					'origin' 			=> __( 'Global', 'zero-bs-crm' ),
					'expected_format' 	=> 'tel',
					'available_in'	 	=> array(),
					'associated_type' 	=> false,
					'replace_str'		=> '##ASSIGNED-TO-MOB##',
					'aliases'			=> array()
				);


			}

		}


		return $placeholders;

	}


	/**
	 * Remove object types where that module is not active, e.g. invoices
	 * This needs to fire before this->add_available_in in the build queue
	 *
	 * @return array of all placeholders
	 */
	public function strip_inactive_tooling( $placeholders = array() ){

		$setting_map = array(
			'form'             => 'feat_forms',
			'quote'      		=> 'feat_quotes',
			'invoice'        	=> 'feat_invs',
			'event'             => 'feat_calendar',
			'transaction'      	=> 'feat_transactions',
		);

		// make a quick state list for simplicity
		$tooling_states = array();
		foreach ( $setting_map as $tooling_area => $setting_key ){
			
			$tooling_states[ $tooling_area ] = (zeroBSCRM_getSetting( $setting_key ) == "1") ? true : false;


			// remove from placeholder list
			if ( !$tooling_states[ $tooling_area ] && isset( $placeholders[ $tooling_area ] ) ){

				// remove
				unset( $placeholders[ $tooling_area ] );
			}			

		}


		// remove from $available_in_links too
		$available_in = array();
		foreach ( $this->available_in_links as $object_type_key => $tooling_area_array ){

			if ( !isset( $tooling_states[ $object_type_key ] ) || $tooling_states[ $object_type_key ] ){

				$tooling_available = array();

				foreach ( $tooling_area_array as $tooling_area ){

					// add back if not in the list
					if ( !array_key_exists( $tooling_area, $tooling_states) || $tooling_states[ $tooling_area ] ){

						$tooling_available[] = $tooling_area;

					}

				}

				$available_in[ $object_type_key ] = $tooling_available;

			}


		}
		$this->available_in_links = $available_in;

		// return
		return $placeholders;

	}





	/**
	 * Appends to passed placeholder model based on DAL object models
	 * (optionally) including custom fields where applicable
	 *
	 * @param bool include_custom_fields
	 *
	 * @return int count
	 */
	private function load_from_object_models(
		$placeholders, 
		$include_custom_fields = false,
		$excluded_slugs = array(

			// global
			'zbs_site', 'zbs_team', 'zbs_owner', 'id_override', 'parent', 'send_attachments', 'hash',

			// contact
			'alias',

			// quote 
			'template', 'acceptedsigned', 'acceptedip', 

			// invoice
			'pay_via', 'allow_tip', 'allow_partial', 'hash_viewed', 'hash_viewed_count', 'portal_viewed', 'portal_viewed_count', 'pdf_template', 'portal_template', 'email_template', 'invoice_frequency', 'address_to_objtype',

			// event
			'show_on_cal', 'show_on_portal', 'title',

			) 

	){

		global $zbs;

		// retrieve object types
		$object_types = $zbs->DAL->get_object_types_by_index();
		$second_address_label = zeroBSCRM_getSetting( 'secondaddresslabel' );
		if ( empty( $second_address_label ) ) {
			$second_address_label = __( 'Second Address', 'zero-bs-crm' );
		}

		// cycle through them, adding where they have $include_in_templating
		foreach ( $object_types as $object_type_index => $object_type_key ){

			$object_layer = $zbs->DAL->getObjectLayerByType( $object_type_index );
			$object_label = $zbs->DAL->typeStr( $object_type_index );

			// if there is an object layer available, and it's included in templating:
			if ( is_object( $object_layer ) && $object_layer->is_included_in_templating() ){

				// (optionally) include custom field references
				if ( $include_custom_fields ){

					$object_model = $object_layer->objModelIncCustomFields();

				} else {

					$object_model = $object_layer->objModel();

				}

				// cycle through object model & add fields
				foreach ( $object_model as $field_index => $field_info ) {

					// deal with exclusions
					if ( !in_array( $field_index, $excluded_slugs ) ) {

						// catching legacy secondary address contact field issues
						$secondary_address_array = array( 'secaddr1', 'secaddr2', 'seccity', 'seccounty', 'secpostcode', 'seccountry' );
						if ( 'contact' === $object_type_key && in_array( $field_index, $secondary_address_array, true ) ) {
							$field_index = str_replace( 'sec', 'secaddr_', $field_index );
							$new_key     = $object_type_key . '-' . $field_index;
						} else {
							$new_key = str_replace( '_', '-', $object_type_key . '-' . $field_index );
						}

						$expected_format = '';

						// add if not present
						// e.g. $placeholders['contact']['ID']
						if ( !isset( $placeholders[ $object_type_key ][ $new_key ] ) ) {

							// prettify these
							$description = $object_label . ' ' . $field_index;
							switch ( $field_index ) {

								case 'created':
									$description = sprintf( __( 'Date %s was created', 'zero-bs-crm' ), $object_label );
									break;
								case 'lastupdated':
									$description = sprintf( __( 'Date %s was last updated', 'zero-bs-crm' ), $object_label );
									break;
								case 'lastcontacted':
									$description = sprintf( __( 'Date %s was last contacted', 'zero-bs-crm' ), $object_label );
									break;
								case 'tw':
									$description = sprintf( __( 'Twitter handle for %s', 'zero-bs-crm' ), $object_label );
									break;
								case 'li':
									$description = sprintf( __( 'LinkedIn handle for %s', 'zero-bs-crm' ), $object_label );
									break;
								case 'fb':
									$description = sprintf( __( 'Facebook page ID for %s', 'zero-bs-crm' ), $object_label );
									break;
								case 'wpid':
									$description = sprintf( __( 'WordPress ID for %s', 'zero-bs-crm' ), $object_label );
									break;

							}

							if ( isset( $field_info['label'] ) ) {
								$description = $object_label . ' ' . __( $field_info['label'], 'zero-bs-crm' );
							}

							if ( !empty( $field_info['area'] ) && $field_info['area'] == 'Second Address' ) {
								$description .= ' (' . esc_html( $second_address_label ) . ')';
							}

							// if it's a custom field we can infer format (and label):
							if ( isset( $field_info['custom-field'] ) && $field_info['custom-field'] == 1 ) {
								$field_info['format'] = $field_info[0];
								if ( $field_info['format'] === 'date' ) {
									$field_info['format'] = 'uts';
								}
								$description .= ' (' . __( 'Custom Field', 'zero-bs-crm' ) . ')';

							}

							// add {placeholder_model}
							$placeholders[ $object_type_key ][ $new_key ] = array(

								'description'     => $description,
								'origin'          => sprintf( __( '%s object model', 'zero-bs-crm' ), $object_label ),
								'available_in'    => array(),
								'associated_type' => $object_type_index,
								'replace_str'     => '##' . strtoupper( $object_type_key ) . '-' . strtoupper( $field_index ) . '##',
								'expected_format' => $expected_format,

							);

							// trying to future proof, added a few helper attributes:
							if ( isset( $field_info['format'] ) ) {
								$placeholders[ $object_type_key ][ $new_key ]['expected_format'] = $field_info['format'];
								if ( $field_info['format'] === 'uts' && empty( $field_info['autoconvert'] ) ) {
									$placeholders[ $object_type_key ][ $new_key . '_datetime_str' ] = array(

										'description'     => $description . ' (' . __( 'DateTime string', 'zero-bs-crm' ) . ')',
										'origin'          => sprintf( __( '%s object model', 'zero-bs-crm' ), $object_label ),
										'available_in'    => array(),
										'associated_type' => $object_type_index,
										'replace_str'     => '##' . strtoupper( $object_type_key ) . '-' . strtoupper( $field_index ) . '_DATETIME_STR##',
										'expected_format' => 'str',

									);
									$placeholders[ $object_type_key ][ $new_key . '_date_str' ] = array(

										'description'     => $description . ' (' . __( 'Date string', 'zero-bs-crm' ) . ')',
										'origin'          => sprintf( __( '%s object model', 'zero-bs-crm' ), $object_label ),
										'available_in'    => array(),
										'associated_type' => $object_type_index,
										'replace_str'     => '##' . strtoupper( $object_type_key ) . '-' . strtoupper( $field_index ) . '_DATE_STR##',
										'expected_format' => 'str',

									);
								}
							}

						}

					}

				}

			}
		}

		return $placeholders;

	}


	/**
	 * Adds `available_in` links to allow tooling to get all applicable fields
	 * e.g. Contact fields will be available in Quote builder
	 *
	 * @param array $placeholders placeholder array
	 *
	 * @return array $placeholders placeholder array
	 */
	private function add_available_in( $placeholders ){

		// any to add?
		if ( is_array( $this->available_in_links ) ){

			foreach ( $this->available_in_links as $object_type => $tooling_area_array ){

				// $object_type = 'contact', $tooling_area = where to point to
				if ( isset( $placeholders[ $object_type ] ) ){

					foreach ( $placeholders[ $object_type ] as $object_type_placeholder_key => $object_type_placeholder ){

						// setup if not set
						if ( !is_array( $placeholders[ $object_type ][ $object_type_placeholder_key ]['available_in'] ) ){

							$placeholders[ $object_type ][ $object_type_placeholder_key ]['available_in'] = array();

						}

						// add if not present
						foreach ( $tooling_area_array as $tooling_area){

							if ( !in_array( $tooling_area, $placeholders[ $object_type ][ $object_type_placeholder_key ]['available_in'] )){

								$placeholders[ $object_type ][ $object_type_placeholder_key ]['available_in'][] = $tooling_area;

							}

						}

					}

				}

			}

		}

		return $placeholders;

	}

	/**
	 * Returns full list of viable placeholders
	 * 
	 * @param bool separate_categories - return in multi-dim array split out by categories
	 *
	 * @return array of all placeholders
	 */
	public function get_placeholders( $separate_categories = true ){

		global $zbs;

		// if asked to return without categories, do that
		if ( !$separate_categories ){

			$no_cat_list = array();
			foreach ( $this->placeholders as $placeholder_group_key => $placeholder_group ){

				foreach ( $placeholder_group as $placeholder_key => $placeholder ){

					$no_cat_list[ $placeholder['replace_str'] ] = $placeholder;

					// any aliases too
					if ( isset( $placeholder['aliases'] ) && is_array( $placeholder['aliases'] ) ) {

						foreach ( $placeholder['aliases'] as $alias ){

							$no_cat_list[ $alias ] = $placeholder;

						}

					}

				}

			}

			return $no_cat_list;

		}

		return $this->placeholders;

	}


	/**
	 * Returns flattened list of viable placeholders
	 *
	 * @return array of all placeholders
	 */
	public function get_placeholders_shorthand(){

		global $zbs;

		$shorthand_list = array();
		foreach ( $this->placeholders as $placeholder_group_key => $placeholder_group ){

			$placeholder_group_prefix = '';

			// all objtypes basically
			if ( $zbs->DAL->isValidObjTypeID( $zbs->DAL->objTypeID( $placeholder_group_key ) ) ) {

				$placeholder_group_prefix = $placeholder_group_key . '-';

			}

			foreach ( $placeholder_group as $placeholder_key => $placeholder ){

				$shorthand_list[] = '##' . strtoupper( $placeholder_group_prefix . $placeholder_key ) . '##';

				// any aliases too
				if ( isset( $placeholder['aliases'] ) && is_array( $placeholder['aliases'] ) ) {

					foreach ( $placeholder['aliases'] as $alias ){

						$shorthand_list[] = $alias;

					}

				}

			}

		}

		return $shorthand_list;

	}


	/**
	 * Returns list of viable placeholders for specific tooling/area, or group of areas, or object types, e.g. system emails or contact
	 *
	 * @param array|string tooling area, object type, or group of
	 * @param bool hydrate_aliases - if true Aliases will be included as full records
	 * @param bool split_by_category - if true return will be split by category as per get_placeholders
	 *
	 * @return array placeholders
	 */
	public function get_placeholders_for_tooling( $tooling = array('global'), $hydrate_aliases = false, $split_by_category = false ){

		global $zbs;

		$applicable_placeholders = array();

		// we allow array or string input here, so if a string, wrap for below
		if ( is_string( $tooling ) ) $tooling = array( $tooling );

		// for MVP we let this get the whole lot then filter down, if this proves unperformant we could optimise here
		// .. or cache.
		$placeholders = $this->get_placeholders();

		// cycle through all looking at the `available_in` attribute.
		// alternatively if an object type is passed, it'll return all fields for that type
		if ( is_array( $placeholders ) ){

			foreach ( $placeholders as $placeholder_group_key => $placeholder_group ){

				$placeholder_group_prefix = '';

				// all objtypes basically
				if ( $zbs->DAL->isValidObjTypeID( $zbs->DAL->objTypeID( $placeholder_group_key ) ) ) {

					$placeholder_group_prefix = $placeholder_group_key . '-';

				}

				foreach ( $placeholder_group as $placeholder_key => $placeholder ){

					// if in object type group:
					if ( 
						in_array( $placeholder_group_key, $tooling ) 
						||
						isset( $placeholder['available_in'] ) && count( array_intersect( $tooling, $placeholder['available_in']) ) > 0
					){

						// here we've flattened the array to actual placeholders (no tooling group)
						// so use the placeholder str as key and add original id to array
						$key = '##' . strtoupper( $placeholder_group_prefix . $placeholder_key ) . '##';
						if ( isset( $placeholder['replace_str'] ) ){

							// this overrides if set (will always be the same?)
							$key = $placeholder['replace_str'];

						}

						// if return in categories
						if ( $split_by_category ){

							if ( !isset( $applicable_placeholders[ $placeholder_group_key ] ) || !is_array( $applicable_placeholders[ $placeholder_group_key ] ) ){

								$applicable_placeholders[ $placeholder_group_key ] = array();

							}

							$applicable_placeholders[ $placeholder_group_key ][ $key ] = $placeholder;

							// add original key to arr
							$applicable_placeholders[ $placeholder_group_key ][ $key ]['key'] =  $placeholder_key;

						} else {

							$applicable_placeholders[ $key ] = $placeholder;

							// add original key to arr
							$applicable_placeholders[ $key ]['key'] =  $placeholder_key;

						}

						// aliases
						if ( $hydrate_aliases && isset( $placeholder['aliases'] ) && is_array( $placeholder['aliases'] ) ) {

							foreach ( $placeholder['aliases'] as $alias ){

								// if return in categories
								if ( $split_by_category ){

									$applicable_placeholders[ $placeholder_group_key ][ $alias ] = $placeholder;

								} else {

									$applicable_placeholders[ $alias ] = $placeholder;

								}

							}

						}

					}

				}

			}


		}

		return $applicable_placeholders;

	}



	/**
	 * Returns a single placeholder info array based on a key
	 *
	 * @param string placeholder key
	 *
	 * @return array placeholder info
	 */
	public function get_single_placeholder_info( $placeholder = '' ){

		// cycle through placeholders and return our match
		foreach ( $this->placeholders as $placeholder_area => $placeholders ){

			foreach ( $placeholders as $key => $placeholder_info ){

				if ( $key == $placeholder ){

					return $placeholder_info;

				}

			}

		}

		return false;

	}

	/*
	* Returns a template-friendly placeholder array of generic replacements
	*/
	public function get_generic_replacements(){

		global $zbs;
		
		// vars
		$login_url      = admin_url( 'admin.php?page=' . $zbs->slugs['dash'] );
		$portal_url     = zeroBS_portal_link();
		$biz_name       = zeroBSCRM_getSetting( 'businessname' );
		$biz_your_name  = zeroBSCRM_getSetting( 'businessyourname' );
		$biz_your_email = zeroBSCRM_getSetting( 'businessyouremail' );
		$biz_your_url   = zeroBSCRM_getSetting( 'businessyoururl' );
		$biz_extra      = zeroBSCRM_getSetting( 'businessextra' );
		$biz_info       = zeroBSCRM_invoicing_generateInvPart_bizTable(
			array(
				'zbs_biz_name'      => $biz_name,
				'zbs_biz_yourname'  => $biz_your_name,
				'zbs_biz_extra'     => $biz_extra,
				'zbs_biz_youremail' => $biz_your_email,
				'zbs_biz_yoururl'   => $biz_your_url,
				'template'          => 'pdf',
			)
		);
		$social_links   = show_social_links();
		
		// return
		return array(

			// login
			'login-link'		=> '<a href="' . $login_url . '">' . __( 'Go to CRM', 'zero-bs-crm' ) . '</a>',
			'login-button'    	=> '<div style="text-align:center;margin:1em;margin-top:2em">'.zeroBSCRM_mailTemplate_emailSafeButton( $login_url, __( 'Go to CRM', 'zero-bs-crm' ) ).'</div>',
			'login-url'         => $login_url,

			// portal
			'portal-link'       => '<a href="' . $portal_url . '">' . $portal_url . '</a>',
			'portal-button'     => '<div style="text-align:center;margin:1em;margin-top:2em">'.zeroBSCRM_mailTemplate_emailSafeButton( $portal_url, __( 'View Portal', 'zero-bs-crm' ) ).'</div>',
			'portal-url'        => $portal_url,

			// biz stuff
			'biz-name'        => $biz_name,
			'biz-your-name'   => $biz_your_name,
			'biz-your-email'  => $biz_your_email,
			'biz-your-url'    => $biz_your_url,
			'biz-info'        => $biz_info,
			'biz-extra'       => $biz_extra,
			'biz-logo'        => jpcrm_business_logo_img( '150px' ),

			// general
			'powered-by'		=> zeroBSCRM_mailTemplate_poweredByHTML(),
			'email-from-name'	=> zeroBSCRM_mailDelivery_defaultFromname(),
			'password'			=> '<a href="' . wp_lostpassword_url() . '" title="' . __( 'Lost Password', 'zero-bs-crm' ) . '">'. __('Set Your Password', 'zero-bs-crm').'</a>',

			// social
			'social-links'    => $social_links,
		);

	}

	/**
	 * Enacts single string replacement on a passed string based on passed tooling/areas
	 * Note: It's recommended to use this function even though str_replace would be approximate
	 * - Using this accounts for multiple aliases, e.g. ###MSG-CONTENT### and ##MSG-CONTENT##, and keeps things centralised
	 *
	 * @param string string of placeholder to replace, e.g. 'msg-content'
	 * @param string string to apply replacements to
	 * @param string string to replace placeholder with
	 *
	 * @return string modified string
	 */
	public function replace_single_placeholder( $placeholder_key = '', $replace_with = '', $string = ''  ){

		// get info
		$placeholder_info = $this->get_single_placeholder_info( $placeholder_key );

		// exists?
		if ( is_array( $placeholder_info ) ) {

			// auto-gen
			$main_placeholder_key = $this->make_placeholder_str( $placeholder_key );
			
			// if replace_str is set, use that
			if ( isset( $placeholder_info['replace_str'] ) ){

				$main_placeholder_key = $placeholder_info['replace_str'];

			}

			// replace any and all variants (e.g. aliases)
			// we do these first, as often in our case the variants are backward-compat
			// and so `###BIZ-INFO###` needs replacing before `##BIZ-INFO##`
			if ( isset( $placeholder_info['aliases'] ) && is_array( $placeholder_info['aliases'] ) ){
			
				$string = str_replace( $placeholder_info['aliases'], $replace_with, $string );
			
			}

			// replace
			$string = str_replace( $main_placeholder_key, $replace_with, $string );


		} else {

			// not in index, replace requested placeholder:
			$string = str_replace( $this->make_placeholder_str( $placeholder_key ), $replace_with, $string );

		}
		
		return $string;

	}


	/**
	 * Enacts replacements on a string based on passed tooling/areas
	 * Note: Using this method allows non-passed values to be emptied,
	 * e.g. if 'global' tooling method, and no 'unsub-line' value passed, 
	 * any mentions of '##UNSUB-LINE##' will be taken out of $string
	 *
	 * @param array|string tooling - tooling area, object type, or group of
	 * @param string string - to apply replacements to
	 * @param array replacements - array of replacement values
	 * @param array|false replacement_objects - an array of objects to replace fields from (e.g. contact) Should be keyed by object type, in the format array[ZBS_TYPE_CONTACT] = contact array
	 * @param bool retain_unset_placeholders - bool whether or not to retain empty-valued placeholders (e.g. true = leaves ##BIZ-LOGO## in string if no value for 'biz-logo')
	 *
	 * @return string modified string
	 */
	public function replace_placeholders( 

		$tooling = array( 'global' ),
		$string = '',
		$replacements = array(),
		$replacement_objects = false,
		$retain_unset_placeholders = false,
		$keys_staying_unrendered = array()

	) {

		// retrieve replacements for this tooling
		$to_replace = $this->get_placeholders_for_tooling( $tooling );

		// cycle through replacements and replace where possible
		if ( is_array( $to_replace ) ) {

			foreach ( $to_replace as $replace_string => $replacement_info ) {

				// ##BIZ-STATE## -> biz-state
				$key = str_replace( '#', '', strtolower( $replace_string ) );
				if ( isset( $replacement_info['key'] ) && !empty( $replacement_info['key'] ) ) {

					$key = $replacement_info['key'];

				}

				// attempt to find value in $replacements
				$replace_with = ''; 
				if ( isset( $replacements[ $key ] ) ) {

					$replace_with = $replacements[ $key ];

				}

				// attempt to find value in $replacement_objects
				// ... if $replacements[value] not already set (that overrides)
				// here $key will be 'contact-prefix'
				// .. which would map to $replacement_objects[ZBS_TYPE_CONTACT]['prefix']
				if ( empty( $replace_with ) ){

					// attempt to pluck relative value
					$potential_value = $this->pick_from_replacement_objects( $key, $replacement_objects );

					if ( !empty( $potential_value ) ) {

						$replace_with = $potential_value;

					}

				}

				// if $key is 'assigned-to-name', 'assigned-to-email', 'assigned-to-mob'  - seek out an owner.
				// ... if $replacements[assigned-to-name] not already set (that overrides)
				if ( empty( $replace_with ) && in_array( $key, array( 'assigned-to-name', 'assigned-to-email', 'assigned-to-username', 'assigned-to-mob' ) ) && is_array( $replacement_objects ) ){

					$owner_value = '';
					$owner_info = $this->owner_from_replacement_objects( $replacement_objects );

					if ( $owner_info ){

						switch ( $key ){

							case 'assigned-to-name':
								$owner_value = $owner_info->display_name;
								break;
							case 'assigned-to-username':
								$owner_value = $owner_info->user_login;
								break;
							case 'assigned-to-email':
								$owner_value = $owner_info->user_email;
								break;
							case 'assigned-to-mob':
								$owner_value = zeroBS_getWPUsersMobile($owner_info->ID);
								break;

						}

						// got value?
						if ( !empty( $owner_value ) ) {

							$replace_with = $owner_value;

						}

					}

				}

				// replace (if not empty + retain_unset_placeholders)
				if ( !$retain_unset_placeholders || ( $retain_unset_placeholders && !empty( $replace_with ) ) ){

					// here we hit a problem, we've been wild with our ### use, so now we
					// have a mixture of ## and ### references.
					// ... wherever we have ### we must replace them first,
					// ... to avoid ###A### being pre-replaced by ##A##
					// for now, replacing aliases first solves this.

					// aliases
					if ( isset( $replacement_info['aliases'] ) && is_array( $replacement_info['aliases'] ) ){
					
						$string = str_replace( $replacement_info['aliases'], $replace_with, $string );
					
					}

					// If this is a Quote date key and is not set (Quote accepted or last viewed), let's print out a message saying the quote isn't accepted or viewed.
					if (
						in_array( $key, array( 'quote-accepted', 'quote-lastviewed' ), true ) &&
						! in_array( $replacement_info['key'], $keys_staying_unrendered, true ) &&
						( empty( $replace_with ) || jpcrm_date_str_to_uts( $replace_with ) === 0 )
						) {

						if ( $key === 'quote-accepted' ) {
							$replace_with = __( 'Quote not yet accepted', 'zero-bs-crm' );
							$string       = str_replace( '##QUOTE-ACCEPTED_DATETIME_STR##', $replace_with, $string );
							$string       = str_replace( '##QUOTE-ACCEPTED_DATE_STR##', $replace_with, $string );
						} else {
							$replace_with = __( 'Quote not yet viewed', 'zero-bs-crm' );
							$string       = str_replace( '##QUOTE-LASTVIEWED_DATETIME_STR##', $replace_with, $string );
							$string       = str_replace( '##QUOTE-LASTVIEWED_DATE_STR##', $replace_with, $string );
						}
					}

					// Replace main key.
					if ( empty( $keys_staying_unrendered ) || ! in_array( $replacement_info['key'], $keys_staying_unrendered, true ) ) {

						$string = str_replace( $replace_string, $replace_with, $string );

					}
				}

			}

		}

		return $string;

	}



	/**
	 * Takes an array of replacement_objects and a target string
	 * ... and returns a value if it can pick one from replacement_objects
	 * ... e.g. target_string = 'contact-fname'
	 * ... 		=
	 * ... 		replacement_objects[ZBS_TYPE_CONTACT]['fname']
	 *
	 * @param string target string e.g. `contact-fname`
	 * @param array|bool replacement objects, keyed to object type - (e.g. contact) Should be in the format array[ZBS_TYPE_CONTACT] = contact array
	 *
	 * @return string valid placeholder string
	 */
	private function pick_from_replacement_objects( $target_string = '', $replacement_objects = array() ) {

		global $zbs;

		// got string and replacement objects?
		if ( !empty( $target_string ) && is_array( $replacement_objects ) && count( $replacement_objects ) > 0 ) {

			// retrieve object-type from $target_string (where possible)
			$target_exploded = explode( '-', $target_string, 2 );
			if ( is_array( $target_exploded ) && count( $target_exploded ) > 1 ) {

				// convert `contact` to ZBS_TYPE_CONTACT (1)
				$object_type_id = $zbs->DAL->objTypeID( $target_exploded[0] );

				// validate obj type id and check if it's passed in replacement_objects
				if ( $zbs->DAL->isValidObjTypeID( $object_type_id ) && isset( $replacement_objects[ $object_type_id ] ) ) {

					// at this point we turn `contact-fname` into `fname` and we pluck it from `$replacement_objects[ ZBS_TYPE_CONTACT ]['fname']` if it's set
					array_shift( $target_exploded );
					$field_name = strtolower( $target_exploded[0] );

					if ( isset( $replacement_objects[ $object_type_id ][ $field_name ] ) && !empty( $replacement_objects[ $object_type_id ][ $field_name ] ) ) {

						// successful find
						return $replacement_objects[ $object_type_id ][ $field_name ];

					}

					// check for potential fallback fields

					if ( preg_match( '/_datetime_str$/', $field_name ) ) {

						$potential_uts_field = str_replace( '_datetime_str', '', $field_name );
						if ( isset( $replacement_objects[ $object_type_id ][ $potential_uts_field ] ) ) {
							$potential_uts_value = $replacement_objects[ $object_type_id ][ $potential_uts_field ];
							if ( jpcrm_is_int( $potential_uts_value ) ) {
								return jpcrm_uts_to_datetime_str( $potential_uts_value );
							} else {
								// use original value as fallback
								return $potential_uts_value;
							}
						}

					} elseif ( preg_match( '/_date_str$/', $field_name ) ) {

						$potential_uts_field = str_replace( '_date_str', '', $field_name );
						if ( isset( $replacement_objects[ $object_type_id ][ $potential_uts_field ] ) ) {
							$potential_uts_value = $replacement_objects[ $object_type_id ][ $potential_uts_field ];
							if ( jpcrm_is_int( $potential_uts_value ) ) {
								return jpcrm_uts_to_date_str( $potential_uts_value );
							} else {
								// use original value as fallback
								return $potential_uts_value;
							}
						}

					}

				}

			}

		}

		return false;

	}


	/**
	 * Takes an array of replacement_objects and returns WordPress user info 
	 * for the first owner it finds in $replacement_objects
	 *
	 * @param array|bool replacement objects, keyed to object type - (e.g. contact) Should be in the format array[ZBS_TYPE_CONTACT] = contact array
	 *
	 * @return string WordPress user info (get_userdata)
	 */
	private function owner_from_replacement_objects( $replacement_objects = array() ){

		// got string and replacement objects?
		if ( is_array( $replacement_objects ) && count( $replacement_objects ) > 0 ) { 

			foreach ( $replacement_objects as $replacement_obj_type => $replacement_object){

				// is `owner` set?
				if ( isset( $replacement_object['owner'] ) && !empty( $replacement_object['owner'] ) ){

					// one of the passed replacement objects has an owner... first passed first served						
					$user_info = get_userdata($replacement_object['owner']);

					if ( $user_info !== false){

						return $user_info;

					}

				}

			}

		}

		return false;

	}

					


	/**
	 * Takes a key (e.g. msg-content) and returns in valid placeholder
	 * format. (e.g. ##MSG-CONTENT##)
	 *
	 * @param string placeholder key
	 *
	 * @return string valid placeholder string
	 */
	public function make_placeholder_str( $placeholder_key = '' ){

		return '##' . str_replace( '_', '-', trim( strtoupper( $placeholder_key ) ) ) . '##';

	}
		


	/**
	 * Draws WYSIWYG Typeahead (Bloodhound JS)
	 *
	 * @param string select - HTML ID to give to the placeholder select
	 * @param string insert_target_id - HTML ID which the placeholder should insert into when a placeholder is selected
	 * @param array|string tooling - placeholder array of tooling areas to include, if 'all' string passed, all placeholders will be shown
	 * @param array return - return or echo?
	 *
	 * @return string valid placeholder string
	 */
	public function placeholder_selector( $id = '', $insert_target_id = '', $tooling = array('global'), $return = false, $extra_classes = '' ){

		$placeholder_list = array();

		// Simpler <select>
		if ( is_array( $tooling ) ){

			// retrieve placeholder list (divided by category)
			$placeholder_list = $this->get_placeholders_for_tooling( $tooling, false, true );

		} else {

			// retrieve placeholder list (divided by category)
			$placeholder_list = $this->get_placeholders( true );

		}

		$html = '<div class="jpcrm-placeholder-select-wrap '.$extra_classes.'">';
		$html .= '<select id="' . $id .'" data-target="' . $insert_target_id . '" class="jpcrm-placeholder-select ui compact selection dropdown">';

			// blank option
			$html .= '<option value="-1">' . __( 'Insert Placeholder', 'zero-bs-crm' ) . '</option>';
			$html .= '<option value="-1" disabled="disabled">====================</option>';

			// cycle through categories of placeholders:
			if ( is_array( $placeholder_list ) && count( $placeholder_list ) > 0 ){

				foreach ( $placeholder_list as $placeholder_group_key => $placeholder_group ){

					$html .= '<optgroup label="' . ucwords( $placeholder_group_key ) . '">';
   
					foreach ( $placeholder_group as $placeholder_key => $placeholder_info ){

						// value
						// if no replace_str attr...
						$placeholder_str = $this->make_placeholder_str( $placeholder_group_key . '-' . $placeholder_key );
						if ( isset( $placeholder_info['replace_str'] ) ){

							$placeholder_str = $placeholder_info['replace_str'];

						}

						// label
						$placeholder_label = ( isset( $placeholder_info['description'] ) && !empty( $placeholder_info['description'] ) ) ? $placeholder_info['description'] . ' (' . $placeholder_str .')' : $placeholder_str;

						$html .= '<option value="' . $placeholder_str . '">' . $placeholder_label . '</option>';


					}

   					$html .= '</optgroup>';

				}


			} else {

				$html .= '<option value="-1">' . __( 'No Placeholders Available', 'zero-bs-crm' ) . '</option>';

			}


		$html .= '</select><i class="code icon"></i></div>';


		if ( !$return ){

			echo $html;

		}

		return $html;

	}


	/**
	 * Collates and tidies the full output of placeholders into a simpler array (for placeholder selector typeahead primarily)
	 * 
	 * @param array placeholders - array of placeholders, probably from $this->get_placeholders or $this->get_placeholders_for_tooling (Note this requires non-categorised array)
	 *
	 * @return array placeholders - simplified array
	 */
	public function simplify_placeholders( $placeholders = array() ){

		$return = array();

		// cycle through and simplify
		foreach ( $placeholders as $key => $info ){

			$placeholder = $info;

			// unset non-key
			unset( $placeholder['available_in'], $placeholder['associated_type'] );

			// if return_str not set and key is, add it
			if ( !isset( $placeholder['replace_str'] ) ){

				$placeholder['replace_str'] = $key;

			}

			$return[] = $placeholder;


		}

		return $return;

	}


	/**
	 * Collates and tidies the full output of placeholders into a simpler array (designed for wysiwyg select insert - e.g. quotebuilder)
	 *
	 * @param array placeholders - array of placeholders, probably from $this->get_placeholders or $this->get_placeholders_for_tooling (Note this requires non-categorised array)
	 *
	 * @return array placeholders - simplified array (array[{text:desc,value:placeholder}])
	 */
	public function simplify_placeholders_for_wysiwyg( $placeholders = array() ){

		$return = array();

		// cycle through and simplify
		foreach ( $placeholders as $placeholder_key => $placeholder_info ){

			// label
			$placeholder_label = ( isset( $placeholder_info['description'] ) && !empty( $placeholder_info['description'] ) ) ? $placeholder_info['description'] . ' (' . $placeholder_key .')' : $placeholder_key;


			$return[] = array( 'text' => $placeholder_label, 'value' => $placeholder_key);


		}

		return $return;

	}
}
