<?php
/**
 * @brief		Custom Template Class
 * @author		<a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
 * @copyright	(c) Invision Power Services, Inc.
 * @license		https://www.invisioncommunity.com/legal/standards/
 * @package		Invision Community
 * @since		2nd May 2023
 */

namespace IPS\Theme;

/* To prevent PHP errors (extending class does not exist) revealing path */

use ErrorException;
use InvalidArgumentException;
use IPS\Application;
use IPS\Data\Store;
use IPS\DateTime;
use IPS\Db;
use IPS\Helpers\Form;
use IPS\Helpers\Form\Codemirror;
use IPS\Helpers\Form\Radio;
use IPS\Helpers\Form\Select;
use IPS\Helpers\Form\Text;
use IPS\Log;
use IPS\Member;
use IPS\Node\Model;
use IPS\Output;
use IPS\Patterns\ActiveRecordIterator;
use IPS\Http\Url;
use IPS\Request;
use IPS\Theme;
use LogicException;
use OutOfRangeException;
use ParseError;
use UnderflowException;
use UnexpectedValueException;
use function array_keys;
use function array_merge;
use function count;
use function defined;
use function file_get_contents;
use function implode;
use function in_array;
use function preg_match;
use function str_replace;
use const IPS\DEBUG_CUSTOM_TEMPLATES;
use const IPS\IN_DEV;
use const IPS\DEBUG_TEMPLATES;

if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
	header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
	exit;
}

/**
 * Custom Template Class
 */
class CustomTemplate extends Model
{
	/**
	 * @brief	Multiton Store
	 */
	protected static array $multitons = array();

	/**
	 * @brief	[ActiveRecord] Database Table
	 */
	public static ?string $databaseTable = 'core_theme_templates_custom';

	/**
	 * @brief	[ActiveRecord] ID Database Column
	 */
	public static string $databaseColumnId = 'id';

	/**
	 * @brief	[ActiveRecord] Database ID Fields
	 */
	protected static array $databaseIdFields = ['template_key'];

	/**
	 * @brief	Database Prefix
	 */
	public static string $databasePrefix = 'template_';

	/**
	 * @brief	[ActiveRecord] Multiton Map
	 */
	protected static array $multitonMap	= array();

	/**
	 * @brief	[Node] Node Title
	 */
	public static string $nodeTitle = 'custom_templates';

	/**
	 * @brief	[Node] Sortable
	 */
	public static bool $nodeSortable = TRUE;

	/**
	 * @brief	[Node] Positon Column
	 */
	public static ?string $databaseColumnOrder = 'order';

	/**
	 * @brief	[ActiveRecord] Caches
	 * @note	Defined cache keys will be cleared automatically as needed
	 */
	protected array $caches = array( 'custom_templates', 'custom_templates_rows' );

	/**
	 * @brief Templates already loaded and evald
	 */
	public static array $calledTemplates = array();

	/**
	 * @brief Current scope? (app/theme)
	 */
	public static string $scopeKey = '';
	public static string $scopeValue = '';

	/**
	 * Return the cleaned name, suitable for a PHP function
	 *
	 * @return string
	 */
	public function cleanName(): string
	{
		return Theme::cleanGroupName( $this->name );
	}

	/**
	 * Return all templates
	 *
	 * @return ActiveRecordIterator
	 */
	public static function all(): ActiveRecordIterator
	{
		return new ActiveRecordIterator( Db::i()->select( '*', static::$databaseTable, null, static::$databasePrefix . static::$databaseColumnOrder ), 'IPS\Theme\CustomTemplate' );
	}

	/**
	 * Get all custom templates that match a main template path (eg core/front/global/globalTemplate)
	 *
	 * @param $templatePath
	 * @return ActiveRecordIterator
	 */
	public static function byHookableTemplatePath( $templatePath ): ActiveRecordIterator
	{
		return new ActiveRecordIterator( Db::i()->select( '*', static::$databaseTable, [ Db::i()->like( 'template_hookpoint', $templatePath ) ] ), 'IPS\Theme\CustomTemplate' );
	}

	/**
	 * Get the template this custom template hooks into
	 *
	 * @param string|null $fullPath
	 * @return array|null
	 */
	public function hookedTemplate( ?string $fullPath=NULL ): ?array
	{
		$fullPath = $fullPath ?: $this->hookpoint;
		[ $path, $hook ] = explode( ':', $fullPath );
		$pathData = CustomTemplate::pathToArray( $path );

		if ( IN_DEV )
		{
			$template = Theme::i()->getTemplate( $pathData['group'], $pathData['app'], $pathData['location'] );

			$html = @file_get_contents( $template->sourceFolder . $pathData['templateName'] . '.phtml' );

			if ( ! $html )
			{
				return null;
			}

			/* Parse the header tag */
			preg_match( '/^<ips:template parameters="(.+?)?"(.+?)?\/>(\r\n?|\n)/', $html, $params );

			/* Strip it */
			$html = ( isset($params[0]) ) ? str_replace( $params[0], '', $html ) : $html;

			return [
				'template_content' => $html,
				'template_app'     => $pathData['app'],
				'template_location'=> $pathData['location'],
				'template_group'   => $pathData['group'],
				'template_name'    => $pathData['templateName'],
				'template_data'    => ( isset( $params[1] ) ) ? $params[1] : '',
				'template_set_id'  => 0
			];
		}
		else
		{
			try
			{
				return Db::i()->select( '*', 'core_theme_templates', array('template_set_id=? and template_app=? and template_location=? and template_group=? and template_name=?', 0, $pathData['app'], $pathData['location'], $pathData['group'], $pathData['templateName']) )->first();
			}
			catch ( UnderflowException $e )
			{
				return null;
			}
		}
	}

	/**
	 * We want a single template PHP function to be grouped by a common name
	 * The common name would be something like core_front_global_globalTemplate__hookpoint__hookPointType
	 *
	 * @return string
	 */
	public function groupedPhpFunctionName(): string
	{
		[ $path, $hook ] = explode( ':', $this->hookpoint );
		return str_replace( '/', '_', $path ) . '__' . $hook . '__' . str_replace( '-', '', $this->hookpoint_type );
	}

	/**
	 * Convert the path and hookpoint to a PHP function name
	 * @param string $path
	 * @param string $hookpoint
	 * @param string $hooktype
	 * @return string
	 */
	public static function pathAndHookPointToFunctionName( string $path, string $hookpoint, string $hooktype ): string
	{
		return str_replace( '/', '_', $path ) . '__' . $hookpoint . '__' . str_replace( '-', '', $hooktype );
	}

	/**
	 * Return the available params for the template this hooks into
	 *
	 * @param string|null $params
	 * @param bool $returnVariablesOnly Only return the variable names, not the ='foo' defaults
	 * @return array|null
	 */
	public function availableParams( ?string $params=null, bool $returnVariablesOnly = false ): array|null
	{
		if ( $template = $this->hookedTemplate() and $template['template_content'] )
		{
			$params = ( $params ) ?: $template['template_data'];
			if ( $returnVariablesOnly )
			{
				$matches = [];
				/* We just want the $varname, not the ='foo' defaults */
				preg_match_all( '/(\$[a-zA-Z0-9_]+)/', $params, $matches );
				return array_unique( $matches[1] );
			}
			else
			{
				// Extract key-value pairs using regular expression
				preg_match_all('/(\$[a-zA-Z_][a-zA-Z0-9_]*)(\s*=\s*("[^"]*"|\'[^\']*\'|[^\s,]*))?/', $params, $matches);

				$results = [];
				foreach( $matches[0] as $row ) {
					$results[] = $row;
				}

				return $results;
			}
		}

		return NULL;
	}

	/**
	 * Convert the stored path string to an array of app/location/group/name params
	 *
	 * @param string $path
	 * @return array
	 */
	public static function pathToArray( string $path ): array
	{
		$bits = explode( "/", $path );
		$app = array_shift( $bits );
		$location = array_shift( $bits );
		$group = array_shift( $bits );
		$templateName = implode( "_", $bits );
		//[ $app, $location, $group, $templateName ] = explode( '/', $path );

		return [
			'app' => $app,
			'location' => $location,
			'group' => $group,
			'templateName' => $templateName
		];
	}

	/**
	 * Compile custom templates into a single data store object
	 *
	 * @return	boolean|null
	 */
	public static function compileCustomTemplates(): ?bool
	{
		$flagKey = 'template_compiling_custom';
		if ( Theme::checkLock( $flagKey ) )
		{
			return NULL;
		}
		Theme::lock( $flagKey );

		$functions = [];
		$organised = [];
		foreach( static::all() as $template )
		{
			/* Only use templates that are enabled */
			if ( $template->app and ! in_array( $template->app, array_keys( Application::enabledApplications() ) ) )
			{
				continue;
			}

			if ( ! $template->hookpoint )
			{
				if ( $template->key )
				{
					/* This isn't using a hookpoint, so save it as a function we can call directly */
					$organised[ 'custom_nohook_' . $template->key ] = [ 'content' => $template->content, 'params' => '' ];
				}
			}
			else
			{
				if ( ! isset( $organised[ $template->groupedPhpFunctionName() ] ) )
				{
					$paramList = '';
					if ( $template->availableParams() )
					{
						$paramList = implode( ',', $template->availableParams() );
					}

					$organised[ $template->groupedPhpFunctionName() ] = [ 'params' => $paramList, 'content' => $template->contentForPhpFunction() ];
				}
				else
				{
					$organised[ $template->groupedPhpFunctionName() ]['content'] .= $template->contentForPhpFunction();
				}
			}
		}

		foreach( $organised as $functionName => $data )
		{
			$functions[] = Theme::_compileTemplate( $data['content'], $functionName, TRUE, FALSE, $data['params'] );
		}

		/* Put them in a class */
		$template = <<<EOF
namespace _NAMESPACE_;
class CustomTemplateClass
{
EOF;

		$template .= implode( "\n\n", $functions );
		$template .= <<<EOF
}
EOF;

		Store::i()->custom_templates = str_replace( 'namespace _NAMESPACE_', 'namespace IPS\Theme', $template );

		Theme::unlock( $flagKey );
		return TRUE;
	}

	public function contentForPhpFunction(): string
	{
		$header = "\n<!--CT: " . $this->name . "-->\n";

		if ( $this->set_id )
		{
			/* Is this for a specific theme set? */
			$content = "{{if \IPS\Theme::i()->id == {$this->set_id}}}\n" . $header . $this->content . "\n{{endif}}";
		}
		elseif ( $this->app )
		{
			/* Is this for a specific app? */
			$content = "{{if \IPS\Application::appIsEnabled( '{$this->app}' )}}\n" . $header . $this->content . "\n{{endif}}";
		}
		else
		{
			/* This is a global template */
			$content = $header . $this->content;
		}

		return $content;
	}

	/**
	 * @param string $name core/front/global/globalTemplate
	 * @param string $hookPoint body:inside-start
	 * @return string
	 * @throws ErrorException
	 */
	public static function getCustomTemplatesForHookPoint( string $name, string $hookPoint ): string
	{
		if ( $hookPoint )
		{
			[$hookName, $hookType] = explode( ':', $hookPoint );
		}
		else
		{
			$hookName = null;
			$hookType = null;
		}
		$args = [];
		/* The {customtemplate..} tag when automatically added via static::expandHookPoints() will pass in the arguments
		   to the function, so we need to grab them here. This allows those scoped variables to be used
		   in the custom templates we create */
		if ( func_num_args() > 2 )
		{
			/* The forth argument is an array of the template_data params */
			$args = func_get_arg(2);
		}

		if ( !in_array( 'custom_templates', array_keys( static::$calledTemplates ) ) )
		{
			/* If we don't have a compiled template, do that now */
			if ( !isset( Store::i()->custom_templates ) )
			{
				/* It can take a few seconds for templates to finish compiling if initiated elsewhere, so let's try a few times sleeping 1 second between attempts
				   to give the compilation time to finish */
				$attempts = 0;
				while( $attempts < 6 )
				{
					$built = CustomTemplate::compileCustomTemplates();

					if ( $built === NULL )
					{
						$attempts++;
						sleep(1);
					}
					else
					{
						break;
					}
				}

				/* Still no key? */
				if ( ! isset( Store::i()->custom_templates ) )
				{
					Log::log( "Custom templates store missing", "template_store_missing" );
					throw new ErrorException( 'template_store_missing' );
				}
			}

			if ( ! class_exists( '\IPS\Theme\CustomTemplateClass', false ) )
			{
				$storedTemplate = Store::i()->custom_templates;

				if ( DEBUG_TEMPLATES )
				{
					Theme::runDebugTemplate( 'custom_template', $storedTemplate );
				}
				else
				{
					try
					{
						if ( @eval( $storedTemplate ) === FALSE )
						{
							if ( IN_DEV )
							{
								throw new TemplateException( 'Invalid Custom Template' );
							}
						}
					}
					catch ( ParseError $e )
					{
						throw new UnexpectedValueException;
					}
				}

				/* Init */
				static::$calledTemplates['custom_template'] = new \IPS\Theme\CustomTemplateClass;
			}
		}

		if ( $hookPoint )
		{
			$functionName = CustomTemplate::pathAndHookPointToFunctionName( $name, $hookName, $hookType );
		}
		else
		{
			$functionName = "custom_nohook_" . $name;
		}

		/* If we're debugging, unset the store on shutdown */
		if ( DEBUG_CUSTOM_TEMPLATES )
		{
			register_shutdown_function( function() {
				unset( Store::i()->custom_templates );
			} );
		}

		if ( method_exists( static::$calledTemplates['custom_template'], $functionName ) )
		{
			return static::$calledTemplates['custom_template']->$functionName( ...$args );
		}
		else
		{
			return '';
		}
	}

	/* Node class handler stuff */
	/**
	 * Fetch All Root Nodes
	 *
	 * @param	string|NULL			$permissionCheck	The permission key to check for or NULl to not check permissions
	 * @param Member|NULL	$member				The member to check permissions for or NULL for the currently logged in member
	 * @param mixed|array $where				Additional WHERE clause
	 * @param	array|NULL			$limit				Limit/offset to use, or NULL for no limit (default)
	 * @return	array
	 */
	public static function roots( ?string $permissionCheck ='view', $member=NULL, $where=array(), $limit=NULL ): array
	{
		if ( !count( $where ) )
		{
			$return = array();
			foreach( static::getStore() AS $node )
			{
				if ( static::$scopeKey === 'set_id' )
				{
					if ( $node['template_set_id'] != static::$scopeValue )
					{
						continue;
					}
				}

				$return[ $node['template_id'] ] = static::constructFromData( $node );
			}

			return $return;
		}
		else
		{
			return parent::roots( $permissionCheck, $member, $where, $limit );
		}
	}

	/**
	 * Get data store
	 *
	 * @return	array
	 * @note	Note that all records are returned, even disabled promotion rules. Enable status needs to be checked in userland code when appropriate.
	 */
	public static function getStore(): array
	{
		if ( !isset( Store::i()->custom_templates_rows) )
		{
			$select = [
				'template_id',
				'template_name',
				'template_hookpoint',
				'template_updated',
				'template_version',
				'template_set_id',
				'template_app',
				'template_hookpoint_type',
				'template_key',
				'template_order'
			];

			Store::i()->custom_templates_rows = iterator_to_array( Db::i()->select( implode( ', ', $select ), static::$databaseTable, NULL, "template_order ASC" )->setKeyField( 'template_id' ) );
		}

		return Store::i()->custom_templates_rows;
	}

	/**
	 * [Node] Get Title
	 *
	 * @return	string
	 */
	protected function get__title(): string
	{
		return $this->name;
	}

	/**
	 * [Node] Return the custom badge for each row
	 *
	 * @return	NULL|array		Null for no badge, or an array of badge data (0 => CSS class type, 1 => language string, 2 => optional raw HTML to show instead of language string)
	 */
	protected function get__badge(): ?array
	{
		/* Is there an update to show? */
		$badge = [];

		if ( $this->hookpoint and $this->hookpoint_type )
		{
			[ $path, $hook ] = explode( ':', $this->hookpoint );
			$pathData = CustomTemplate::pathToArray( $path );

			$badge = array(
				0 => 'style2',
				1 => implode( "/", $pathData ) . ':' . $hook . ' ' . $this->hookpoint_type
			);
		}
		else if ( $this->key )
		{
			$badge = array(
				0 => 'style3',
				1 => $this->key
			);
		}

		return $badge;
	}

	/**
	 * [Node] Get buttons to display in tree
	 * Example code explains return value
	 *
	 * @param Url $url		Base URL
	 * @param bool $subnode	Is this a subnode?
	 * @return    array
	 */
	public function getButtons( Url $url, bool $subnode=FALSE ): array
	{
		$buttons = parent::getButtons( $url, $subnode );

		if ( isset( $buttons['copy'] ) )
		{
			unset( $buttons['copy'] );
		}

		$buttons['edit']['link'] = Url::internal( "app=core&module=customization&controller=customtemplates&do=form&" . static::$scopeKey. "=" . static::$scopeValue . "&id=" . $this->id );

		/* Remove the modal for editing */
		unset( $buttons['edit']['data']['ipsDialog'], $buttons['edit']['data']['ipsDialog-title'] );

		return $buttons;
	}

	/**
	 * [Node] Does the currently logged-in user have permission to edit permissions for this node?
	 * @note We currently do not use the permission system for templates.
	 *
	 * @return	bool
	 */
	public function canManagePermissions(): bool
	{
		return FALSE;
	}

	/**
	 * [Node] Does the currently logged-in user have permission to copy this node?
	 *
	 * @return	bool
	 */
	public function canCopy(): bool
	{
		return FALSE;
	}

	/**
	 * Form
	 *
	 * @param Form $form	The form
	 * @return	void
	 */
	public function form( Form &$form ): void
	{
		//$form->class = 'ipsForm--vertical ipsForm--custom-template ipsForm--fullWidth';
		$form->attributes['data-controller'] = 'core.admin.customization.customTemplatesForm';
		Output::i()->jsFiles = array_merge( Output::i()->jsFiles, Output::i()->js( 'admin_customization.js', 'core', 'admin' ) );

		$applications = [];
		$hookPointsSelect = [];
		$pathNamesSelect = [];
		foreach( Application::applications() as $application )
		{
			foreach ( Theme::i()->getHookPoints( [ $application->directory ] ) as $path => $hookPointNames )
			{
				$applications[ $application->directory ] = $application->_title;
				foreach ( $hookPointNames as $hookPointName )
				{
					$fixedPath = preg_replace( '#^' . $application->directory . '/#', '', $path );
					$pathNamesSelect[ $application->directory ][ str_replace( '/', '_', $fixedPath ) ] = $fixedPath;
					$hookPointsSelect[ $application->directory . '_' . str_replace( '/', '_', $fixedPath ) ][ $hookPointName ] = $hookPointName;
				}
			}
		}

		/* Work out some defaults */
		$defaultHookapp = 'core';
		$defaultPath = '';
		$defaultTemplateHook = '';
		if ( $this->id and $this->hookpoint )
		{
			$array = static::pathToArray( $this->hookpoint );

			if ( isset( $array['app'] ) )
			{
				$defaultHookapp = $array['app'];
			}

			if ( isset( $array['group'] ) and isset( $array['templateName'] ) )
			{
				[ $templateName, $defaultTemplateHook ] = explode( ':', $array['templateName'] );
				$defaultPath = $array['location'] . '_' . $array['group'] . '_' . $templateName;
			}
		}

		$form->add( new Text( 'template_name', $this->id ? $this->name : NULL, true ) );
		$form->add( new Radio( 'template_type', ( $this->id and $this->hookpoint ) ? 'hook' : 'custom', true, array( 'options' => array( 'hook' => 'template_type_hook', 'custom' => 'template_type_custom' ) ), NULL, NULL, NULL, 'template_type' ) );
		$form->add( new Select( 'template_hookapp', $defaultHookapp, FALSE, array( 'options' => $applications ), NULL, NULL, NULL, 'template_hookapp' ) );

		foreach( $pathNamesSelect as $applicationDirectory => $path )
		{
			$form->add( new Select( 'template_path_' . $applicationDirectory, $defaultPath, FALSE, array( 'options' => $path, 'parse' => 'normal' ), NULL, NULL, Theme::i()->getTemplate( 'customization', 'core', 'admin' )->previewTemplateLink(), 'template_path_' . $applicationDirectory ) );
			Member::loggedIn()->language()->words[ 'template_path_' . $applicationDirectory ] = Application::load( $applicationDirectory )->_title;
		}

		foreach( $hookPointsSelect as $data => $hookPoints )
		{
			[ $applicationDirectory, $path ] = explode( '_', $data, 2 );

			$form->add( new Select( 'template_hookpoint_' . $applicationDirectory . '_' . $path, $defaultTemplateHook, FALSE, array( 'options' => $hookPoints, 'parse' => 'normal' ), NULL, NULL, NULL, 'template_hookpoint_' . $applicationDirectory . '_' . $path ) );
			Member::loggedIn()->language()->words[ 'template_hookpoint_' . $applicationDirectory . '_' . $path ] = Member::loggedIn()->language()->get('template_hookpoint');
		}

		$form->add( new Select( 'template_hookpoint_type', $this->id ? $this->hookpoint_type : 'before', FALSE, array( 'options' => [
			'before' => 'template_custom_before',
			'inside-start' => 'template_custom_inside_start',
			'inside-end' => 'template_custom_inside_end',
			'after' => 'template_custom_after',
		] ), NULL, NULL, NULL, 'template_hookpoint_type' ) );
		$form->add( new Text( 'template_key', $this->id ? $this->key : NULL, FALSE, [], function( $val )
		{
			try
			{
				if ( ! $val )
				{
					if ( Request::i()->template_type == 'custom' )
					{
						/* This is required if we're not using a hookpoint */
						throw new InvalidArgumentException('custom_template_key_cant_be_empty');
					}

					return true;
				}

				$val = Theme::cleanGroupName( $val );

				try
				{
					$template = CustomTemplate::load( $val, 'template_key');
				}
				catch( OutOfRangeException $ex )
				{
					/* Doesn't exist? Good! */
					return true;
				}

				/* It's taken... */
				if ( Request::i()->id == $template->id )
				{
					/* But it's this one so that's ok */
					return true;
				}

				/* and if we're here, it's not... */
				throw new InvalidArgumentException('custom_template_key_not_unique');
			}
			catch ( OutOfRangeException $e )
			{
				/* Slug is OK as load failed */
				return true;
			}
		}, NULL, NULL, 'template_key' ) );

		$form->add( new Codemirror( 'template_content', $this->id ? $this->content : null, FALSE, [ 'codeModeAllowedLanguages' => [ 'ipsphtml' ] ], function( $val )
		{
			/* Test */
			try
			{
				$currentApp = Request::i()->template_hookapp;
				$params = '';

				if ( $currentApp !== 'none' )
				{
					$path = Request::i()->{'template_path_' . $currentApp};
					$hook = Request::i()->{'template_hookpoint_' . $currentApp . '_' . $path};

					$template = new CustomTemplate();
					$template->hookpoint = $currentApp . '/' . str_replace( '_', '/', $path ) . ':' . $hook;
					$template->hookpoint_type = Request::i()->template_hookpoint_type;
					$params = implode( ', ', $template->availableParams() );
				}

				Theme::checkTemplateSyntax( $val, $params );
			}
			catch( LogicException $e )
			{
				throw new LogicException('template_error_bad_syntax');
			}

		} ) );

		/* If we are editing, we can save and reload */
		if( $this->id )
		{
			$form->canSaveAndReload = true;
		}
	}

	/**
	 * [Node] Format form values from add/edit form for save
	 *
	 * @param	array	$values	Values from the form
	 * @return	array
	 */
	public function formatFormValues( array $values ): array
	{
		$values['template_updated']	= DateTime::create()->getTimestamp();
		$values['template_version']	= Application::load('core')->long_version;
		$values['template_key'] = $values['template_key'] ? Theme::cleanGroupName( $values['template_key'] ) : null;

		if ( $values['template_type'] === 'hook' and isset( $values[ 'template_path_' . $values['template_hookapp'] ] ) )
		{
			$currentApp = $values['template_hookapp'];
			$path = $values[ 'template_path_' . $currentApp ];
			$hook = $values[ 'template_hookpoint_' . $currentApp . '_' . $path ];

			$values['template_hookpoint'] = $currentApp . '/' . str_replace( '_', '/', $path ) . ':' . $hook;
		}
		else
		{
			$values['template_hookpoint'] = null;
		}

		if ( static::$scopeKey === 'set_id' )
		{
			$values['template_set_id'] = (int) static::$scopeValue;
		}
		else if ( static::$scopeKey === 'appKey' )
		{
			$values['template_app'] = static::$scopeValue;
		}

		/* Unset some things we don't need */
		foreach( $values as $k => $v )
		{
			if( $k == 'template_hookpoint_type' )
			{
				continue;
			}

			if ( stristr( $k, 'template_hookpoint_' ) or stristr( $k, 'template_path_' ) )
			{
				unset( $values[ $k ] );
			}
		}

		unset( $values['template_hookapp'], $values['template_type'] );
		return parent::formatFormValues( $values );
	}

	/**
	 * Import custom templates from a json file
	 *
	 * @param string $file
	 * @param string $appKey
	 * @param int|null $offset
	 * @param int|null $limit
	 * @return int
	 */
	public static function importFromFile( string $file, string $appKey, ?int $offset=null, ?int $limit=null ) : int
	{
		$templates = json_decode( file_get_contents( $file ), true );
		$keys = array_keys( $templates );
		$offset = $offset ?? 0;
		if( $limit !== null )
		{
			$keys = array_slice( $keys, $offset, $limit );
		}

		$current = iterator_to_array(
			Db::i()->select( '*', static::$databaseTable, [ 'template_app=?', $appKey ] )
				->setKeyField( 'template_name' )
		);

		$imported = 0;
		foreach( $keys as $name )
		{
			$data = $templates[ $name ];

			if( isset( $current[ $name ] ) )
			{
				Db::i()->update( static::$databaseTable, [
					'template_hookpoint' => $data['hookpoint'],
					'template_hookpoint_type' => $data['type'],
					'template_key' => $data['key'],
					'template_version' => $data['version'],
					'template_content' => $data['content'],
					'template_updated' => time()
				], [ 'template_id=?', $current[ $name ]['template_id'] ] );

				unset( $current[ $name ] );
			}
			else
			{
				Db::i()->insert( static::$databaseTable, [
					'template_app' => $appKey,
					'template_name' => $name,
					'template_hookpoint' => $data['hookpoint'],
					'template_hookpoint_type' => $data['type'],
					'template_key' => $data['key'],
					'template_version' => $data['version'],
					'template_content' => $data['content'],
					'template_updated' => time()
				]);
			}

			$imported++;
		}

		Db::i()->delete( static::$databaseTable, [
			[ 'template_app=?', $appKey ],
			[ Db::i()->in( 'template_name', array_keys( $current ) ) ]
		]);

		return $imported;
	}
}