<?php
/**
 * @brief		Database Field Node
 *
 * @copyright	(c) 2001 - SVN_YYYY Invision Power Services, Inc.
 *
 * @package		IPS Social Suite
 * @since		2 Apr 2014
 * @version		SVN_VERSION_NUMBER
 */

/**
 * 
 * @todo: Shared media field type
 * @todo: Relational field type
 *
 */
namespace IPS\cms;

/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
	header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
	exit;
}

/**
 * Database Field Node
 */
class _Fields extends \IPS\CustomField implements \IPS\Node\Permissions
{
	/**
	 * @brief	[ActiveRecord] Multiton Store
	 */
	protected static $multitons = array();
		
	/**
	 * @brief	[ActiveRecord] Database Table
	 */
	public static $databaseTable = 'cms_database_fields';
	
	/**
	 * @brief	[Fields] Custom Database Id
	 */
	public static $customDatabaseId = NULL;
	
	/**
	 * @brief	[ActiveRecord] Database Prefix
	 */
	public static $databasePrefix = 'field_';
	
	/**
	 * @brief	[ActiveRecord] ID Database Column
	 */
	public static $databaseColumnId = 'id';

	/**
	 * @brief	[ActiveRecord] Database ID Fields
	 */
	protected static $databaseIdFields = array('field_id', 'field_key');

	/**
	 * @brief	[Node] Order Database Column
	 */
	public static $databaseColumnOrder = 'position';
	
	/**
	 * @brief	[CustomField] Title/Description lang prefix
	 */
	protected static $langKey = 'content_field';
	
	/**
	 * @brief	Have fetched all?
	 */
	protected static $gotAll = FALSE;
	
	/**
	 * @brief	The map of permission columns
	 */
	public static $permissionMap = array(
			'view' 				=> 'view',
			'edit'				=> 2,
			'add'               => 3
	);
	
	/**
	 * @brief	[Node] App for permission index
	*/
	public static $permApp = 'cms';
	
	/**
	 * @brief	[Node] Type for permission index
	 */
	public static $permType = 'fields';
	
	/**
	 * @brief	[Node] Prefix string that is automatically prepended to permission matrix language strings
	 */
	public static $permissionLangPrefix = 'perm_content_field_';
	
	/**
	 * @brief	[Node] ACP Restrictions
	 */
	protected static $restrictions = array(
		'app'		=> 'cms',
		'module'	=> 'database',
		'prefix'	=> 'fields_',
	);

	/**
	 * @brief	[Node] Title prefix.  If specified, will look for a language key with "{$key}_title" as the key
	 */
	public static $titleLangPrefix = 'content_field_';
	
	/**
	 * @brief	[Node] Sortable?
	 */
	public static $nodeSortable = TRUE;
	
	/**
	 * @brief	[Node] Node Title
	 */
	public static $nodeTitle = '';
	
	/**
	 * @brief	[CustomField] Database table
	 */
	protected static $contentDatabaseTable;
	
	/**
	 * @brief	[CustomField] Upload storage extension
	 */
	protected static $uploadStorageExtension = 'cms_Records';

	/**
	 * @brief	[CustomField] Cache retrieved fields
	 */
	protected static $cache = array();

	/**
	 * @brief   Custom Media fields
	 */
	protected static $mediaFields = array( 'Youtube', 'Spotify', 'Soundcloud' );

	/**
	 * @brief	Skip the title and content fields
	 */
	const FIELD_SKIP_TITLE_CONTENT = 1;
	
	/**
	 * @brief	Show only fields allowed on the comment form
	 */
	const FIELD_DISPLAY_COMMENTFORM = 2;
	
	/**
	 * @brief	Show only fields allowed on the listing view
	 */
	const FIELD_DISPLAY_LISTING = 4;
	
	/**
	 * @brief	Show only fields allowed on the record view
	 */
	const FIELD_DISPLAY_RECORD  = 8;
	
	/**
	 * @brief	Show only fields allowed to be filterable
	 */
	const FIELD_DISPLAY_FILTERS = 16;
	
	/**
	 * @brief	Fields that cannot be title fields
	 */
	public static $cannotBeTitleFields = array( 'Editor', 'CheckboxSet', 'YesNo', 'Radio' );
	
	/**
	 * @brief	Fields that cannot be title fields
	 */
	public static $cannotBeContentFields = array();
	
	/**
	 * Load Record
	 *
	 * @see		\IPS\Db::build
	 * @param	int|string	$id					ID
	 * @param	string		$idField			The database column that the $id parameter pertains to (NULL will use static::$databaseColumnId)
	 * @param	mixed		$extraWhereClause	Additional where clause(s) (see \IPS\Db::build for details)
	 * @return	static
	 * @throws	\InvalidArgumentException
	 * @throws	\OutOfRangeException
	 */
	public static function load( $id, $idField=NULL, $extraWhereClause=NULL )
	{
		if ( $idField === 'field_key' )
		{
			$extraWhereClause = array( 'field_database_id=?', static::$customDatabaseId );
		}
		
		return parent::load( $id, $idField, $extraWhereClause );
	}
	
	/**
	 * Fetch All Root Nodes
	 *
	 * @param	string|NULL			$permissionCheck	The permission key to check for or NULl to not check permissions
	 * @param	\IPS\Member|NULL	$member				The member to check permissions for or NULL for the currently logged in member
	 * @param	mixed				$where				Additional WHERE clause
	 * @return	array
	 */
	public static function roots( $permissionCheck='view', $member=NULL, $where=array() )
	{
		$permissionCheck = ( \IPS\Dispatcher::hasInstance() AND \IPS\Dispatcher::i()->controllerLocation === 'admin' ) ? NULL : $permissionCheck;

		if ( ! isset( static::$cache[ static::$customDatabaseId ][ $permissionCheck ] ) )
		{
			$langToLoad = array();
			$where[]    = array('field_database_id=?', static::$customDatabaseId );

			static::$cache[ static::$customDatabaseId ][ $permissionCheck ] = parent::roots( $permissionCheck, $member, $where );

			foreach( static::$cache[ static::$customDatabaseId ][ $permissionCheck ] as $id => $obj )
			{
				if ( ! array_key_exists( $obj->type, static::$additionalFieldTypes ) AND ( ! class_exists( '\IPS\Helpers\Form\\' . ucfirst( $obj->type ) ) AND ! class_exists( '\IPS\cms\Fields\\' . ucfirst( $obj->type ) ) ) )
				{
					unset( static::$cache[ static::$customDatabaseId ][ $permissionCheck ][ $id ] );
					continue;
				}

				$langToLoad[] = static::$langKey . '_' . $obj->id;
				$langToLoad[] = static::$langKey . '_' . $obj->id . '_desc';
				$langToLoad[] = static::$langKey . '_' . $obj->id . '_warning';
			}

			if ( count( $langToLoad ) AND \IPS\Dispatcher::i()->controllerLocation !== 'setup' )
			{
				\IPS\Member::loggedIn()->language()->get( $langToLoad );
			}
		}

		return static::$cache[ static::$customDatabaseId ][ $permissionCheck ];
	}
	
	/**
	 * Just return all field IDs in a database without permission checking
	 *
	 * @return array
	 */
	public static function databaseFieldIds()
	{
		$key = 'cms_fieldids_' . static::$customDatabaseId;
		
		if ( ! isset( \IPS\Data\Store::i()->$key ) )
		{
			\IPS\Data\Store::i()->$key = iterator_to_array( \IPS\Db::i()->select( 'field_id', 'cms_database_fields', array( array( 'field_database_id=?', static::$customDatabaseId ) ) )->setKeyField('field_id') );
		}
		
		return \IPS\Data\Store::i()->$key;
	}
	
	/**
	 * Get Field Data
	 *
	 * @param	string|NULL		            $permissionCheck	The permission key to check for or NULl to not check permissions
	 * @param	\IPS\Node\Model|NULL		$container			Parent container
	 * @param   INT                         $flags              Bit flags
	 *
	 * @return	array
	 */
	public static function data( $permissionCheck=NULL, \IPS\Node\Model $container=NULL, $flags=0 )
	{
		$fields   = array();
		$database = \IPS\cms\Databases::load( static::$customDatabaseId );
		
		foreach( static::roots( $permissionCheck ) as $row )
		{
			if ( $container !== NULL AND $database->use_categories )
			{
				if ( $container->fields !== '*' AND $container->fields !== NULL )
				{
					if ( ! in_array( $row->id, $container->fields ) AND $row->id != $database->field_title AND $row->id != $database->field_content )
					{
						continue;
					}
				}
			}

			if ( $flags & self::FIELD_SKIP_TITLE_CONTENT AND ( $row->id == $database->field_title OR $row->id == $database->field_content ) )
			{
				continue;
			}
			
			if ( $flags & self::FIELD_DISPLAY_FILTERS AND ! $row->filter )
			{
				continue;
			}
			
			$fields[ $row->id ] = $row;
		}
		
		return $fields;
	}
	
	/**
	 * Get Fields
	 *
	 * @param	array			            $values				Current values
	 * @param	string|NULL		            $permissionCheck	The permission key to check for or NULl to not check permissions
	 * @param	\IPS\Node\Model|NULL		$container			Parent container
	 * @param	int				            $flags				Bit flags
	 * @param   \IPS\cms\Records|NULL       $record             The record itself
	 * @return	array
	 */
	public static function fields( $values, $permissionCheck='view', \IPS\Node\Model $container=NULL, $flags=0, \IPS\cms\Records $record = NULL )
	{
		$fields        = array();
		$database      = \IPS\cms\Databases::load( static::$customDatabaseId );

		foreach( static::roots( $permissionCheck ) as $row )
		{
			if ( $container !== NULL AND $database->use_categories )
			{
				if ( $container->fields !== '*' AND $container->fields !== NULL )
				{
					if ( ! in_array( $row->id, $container->fields ) AND $row->id != $database->field_title AND $row->id != $database->field_content )
					{
						continue;
					}
				}
			}

			if ( $flags & self::FIELD_SKIP_TITLE_CONTENT AND ( $row->id == $database->field_title OR $row->id == $database->field_content ) )
			{
				continue;
			}
			
			if ( $flags & self::FIELD_DISPLAY_COMMENTFORM )
			{
				if ( ! $row->display_commentform )
				{
					continue;
				}
			}

			if ( $flags & self::FIELD_DISPLAY_LISTING AND ( ! $row->display_listing ) )
			{
				continue;
			}
			
			if ( $flags & self::FIELD_DISPLAY_RECORD AND ( ! $row->display_display ) )
			{
				continue;
			}
			
			if ( $flags & self::FIELD_DISPLAY_FILTERS  )
			{
				if ( ! $row->filter )
				{
					continue;
				}
				else
				{
					if ( $row->type === 'Radio' )
					{
						$row->type = 'Select';
					}

					$row->required = FALSE;
					
					if ( $row->type === 'Select' )
					{
						$row->is_multiple = true;
					}
				}
			}

			if ( isset( $values['field_' . $row->id ] ) )
			{
				$fields[ $row->id ] = $row->buildHelper( $values['field_' . $row->id ], NULL, $record, $flags );
			}
			else
			{
				$fields[ $row->id ] = $row->buildHelper( $row->default_value, NULL, $record, $flags );
			}
		}

		return $fields;
	}
	
	/**
	 * Get Values
	 *
	 * @param	array			            $values				Current values
	 * @param	string|NULL		            $permissionCheck	The permission key to check for or NULl to not check permissions
	 * @param	\IPS\Node\Model|NULL		$container			Parent container
	 * @return	array
	 */
	public static function values( $values, $permissionCheck='view', \IPS\Node\Model $container=NULL )
	{
		$fields   = array();
		$database = \IPS\cms\Databases::load( static::$customDatabaseId );
		
		foreach( static::roots( $permissionCheck ) as $row )
		{
			if ( $container !== NULL AND $database->use_categories )
			{
				if ( $container->fields !== '*' AND $container->fields !== NULL )
				{
					if ( ! in_array( $row->id, $container->fields ) AND $row->id != $database->field_title AND $row->id != $database->field_content )
					{
						continue;
					}
				}
			}
			
			if ( isset( $values[ 'field_' . $row->id ] ) )
			{
				$fields[ 'field_' . $row->id ] = $values[ 'field_' . $row->id ];
			}
		}
		
		return $fields;
	}
	
	/**
	 * Display Values
	 *
	 * @param	array			            $values				Current values
	 * @param	string|NULL		            $display			Type of display (listing/display/raw/processed).
	 * @param	\IPS\Node\Model|NULL		$container			Parent container
	 * @param   string                      $index              Field to index return array on
	 * @param	NULL|\IPS\cms\Record		$record				Record showing this field
	 * @note    Raw means the value saved from the input field, processed has the form display value method called. Listing and display take the options set by the field (badges, custom, etc)
	 * @return	array
	 */
	public static function display( $values, $display='listing', \IPS\Node\Model $container=NULL, $index='key', $record=NULL )
	{
		$fields   = array();
		$database = \IPS\cms\Databases::load( static::$customDatabaseId );
		
		foreach( static::roots('view') as $row )
		{
			if ( $display !== 'record' AND ( $row->id == $database->field_title OR $row->id == $database->field_content ) )
			{
				continue;
			}
			
			if ( $container !== NULL AND $database->use_categories )
			{
				if ( $container->fields !== '*' AND $container->fields !== NULL )
				{
					if ( ! in_array( $row->id, $container->fields ) AND $row->id != $database->field_title AND $row->id != $database->field_content )
					{
						continue;
					}
				}
			}

			$formValue = ( isset( $values[ 'field_' . $row->id ] ) AND $values[ 'field_' . $row->id ] !== '' AND $values[ 'field_' . $row->id ] !== NULL ) ? $values[ 'field_' . $row->id ] : $row->default_value;
			$value     = $row->displayValue( $formValue );

			if ( $display === 'listing' )
			{
				if ( ! $row->display_listing )
				{
					continue;
				}

				$value = $row->truncate( $value, TRUE );

				if ( $value )
				{
					$value = $row->formatForDisplay( $value, $formValue, 'listing', $record );
				}
			}
			else if ( $display === 'display' or $display === 'display_top' or $display === 'display_bottom' )
			{
				if ( ! $row->display_display )
				{
					continue;
				}

				$displayData = $row->display_json;

				if ( $display === 'display_bottom' )
				{
					if ( isset( $displayData['display']['where'] ) )
					{
						if ( $displayData['display']['where'] !== 'bottom' )
						{
							continue;
						}
					}
					else
					{
						continue;
					}
				}
				else if ( $display === 'display_top' )
				{
					if ( isset( $displayData['display']['where'] ) )
					{
						if ( $displayData['display']['where'] !== 'top' )
						{
							continue;
						}
					}
				}

				if ( $value )
				{
					$value = $row->formatForDisplay( $value, $formValue, 'display', $record );
				}
			}
			else if ( $display === 'raw' )
			{
				$value = $formValue;
			}

			$fields[ ( $index === 'id' ? $row->id : $row->key ) ] = $value;
		}

		return $fields;
	}


	/**
	 * Display the field
	 *
	 * @param   mixed        $value         Processed value
	 * @param   mixed        $formValue     Raw form value
	 * @param   string       $type          Type of display (listing/display/raw/processed).
	 * @param	NULL|\IPS\cms\Record	$record	Record showing this field
	 * @note    Raw means the value saved from the input field, processed has the form display value method called. Listing and display take the options set by the field (badges, custom, etc)
	 *
	 * @return mixed|string
	 * @throws \ErrorException
	 */
	public function formatForDisplay( $value, $formValue, $type='listing', $record=NULL )
	{
		if ( $type === 'raw' )
		{
			if ( $this->type === 'Upload' )
			{
				return (string) \IPS\File::get( static::$uploadStorageExtension, $value )->url;
			}

			if ( $this->type === 'Item' )
			{
				if ( ! is_array( $formValue ) and mbs_trstr( $formValue, ',' ) )
				{
					$value = explode( ',', $formValue );
				}
				else
				{
					$value = array( $formValue );
				}

				if ( count( $value ) and isset( $this->extra['database'] ) and $this->extra['database'] )
				{
					$results = array();
					$class   = '\IPS\cms\Records' . $this->extra['database'];
					$field   = $class::$databasePrefix . $class::$databaseColumnMap['title'];
					$where   = array( \IPS\Db::i()->in( $class::$databaseColumnId, $value ) );

					foreach ( $class::getItemsWithPermission( array( $where ), $field, NULL ) as $item )
					{
						$results[ $item->_id ] = $item;
					}

					return $results;
				}
			}

			return $formValue;
		}
		else if ( $type === 'processed' )
		{
			return $value;
		}

		$options = $this->display_json;

		if ( isset( $options[ $type ]['method'] ) AND $options[ $type ]['method'] !== 'simple' )
		{
			if ( in_array( $this->type, static::$mediaFields ) )
			{
				$template = mb_strtolower( $this->type );

				if ( $options[ $type ]['method'] === 'player' )
				{
					$class = '\IPS\cms\Fields\\' . $this->type;

					if ( method_exists( $class, 'displayValue' ) )
					{
						$value = $class::displayValue( $formValue, $this );
					}
					else
					{
						try
						{
							$value = \IPS\Theme::i()->getTemplate( 'records', 'cms', 'global' )->$template( $formValue, $this->extra );
						}
						catch( \Exception $ex )
						{
							$value = $formValue;
						}
					}
				}
				else
				{
					$value = $formValue;
				}
			}
			else
			{
				if ( $options[ $type ]['method'] == 'custom' )
				{
					if ( $this->type === 'Upload' )
					{
						if ( mb_strstr( $value, ',' ) )
						{
							$files = explode( ',', $value );
						}
						else
						{
							$files = array( $value );
						}

						$objects = array();
						foreach ( $files as $file )
						{
							$objects[] = \IPS\File::get( static::$uploadStorageExtension, (string) $file );
						}

						if ( ! $this->is_multiple )
						{
							$value = array_shift( $objects );
						}
						else
						{
							$value = $objects;
						}
					}

					$value = trim( $this->parseCustomHtml( $type, $options[ $type ]['html'], $formValue, $value ) );
				}
				else if ( $options[ $type ]['method'] !== 'none' )
				{
					$class = 'ipsBadge_style' . $options[ $type ]['method'];

					if ( isset( $options[ $type ]['right'] ) AND $options[ $type ]['right'] )
					{
						$class .= ' ' . 'ipsPos_right';
					}

					if ( $this->type === 'Address' and $formValue and isset( $options[ $type ]['map'] ) AND $options[ $type ]['map'] )
					{
						$value .= \IPS\GeoLocation::buildFromJson( $formValue )->map()->render( $options[ $type ]['mapDims'][0], $options[ $type ]['mapDims'][1] );
					}

					if ( $this->type === 'Upload' )
					{
						if ( mb_strstr( $value, ',' ) )
						{
							$files = explode( ',', $value );
						}
						else
						{
							$files = array( $value );
						}

						$parsed = array();
						foreach( $files as $file )
						{
							$file = \IPS\File::get( static::$uploadStorageExtension, (string) $file );

							if ( $file->isImage() and $type === 'display' )
							{
								\IPS\Output::i()->metaTags['og:image:url'][] = (string) $file->url;
							}

							$parsed[] = \IPS\Theme::i()->getTemplate( 'global', 'cms', 'front' )->uploadDisplay( \IPS\File::get( static::$uploadStorageExtension, $file ), $record );
						}

						$value = implode( " ", $parsed );
					}

					$value = \IPS\Theme::i()->getTemplate( 'records', 'cms', 'global' )->fieldBadge( $this->_title, $value, $class );
				}
			}
		}
		else
		{
			$value = \IPS\Theme::i()->getTemplate( 'records', 'cms', 'global' )->fieldDefault( $this->_title, $value );
		}

		return $value;
	}

	/**
	 * Parse custom HTML
	 *
	 * @parse   string  $type           Type of display
	 * @param   string  $template       The HTML to parse
	 * @param   string  $formValue      The form value (key of select box, for example)
	 * @param   string  $value          The display value
	 *
	 * @return \IPS\Theme
	 */
	public function parseCustomHtml( $type, $template, $formValue, $value )
	{
		$functionName = $this->fieldTemplateName( $type );
		$options      = $this->display_json;

		if ( $formValue and $this->type === 'Address' )
		{
			$functionName .= '_' . uniqid();
			
			$template = str_replace( '{map}'    , \IPS\GeoLocation::buildFromJson( $formValue )->map()->render( $options[ $type ]['mapDims'][0], $options[ $type ]['mapDims'][1] ), $template );
			$template = str_replace( '{address}', \IPS\GeoLocation::parseForOutput( $formValue ), $template );
			$template = \IPS\Theme::compileTemplate( $template, $functionName, '$value, $formValue, $label', true );
		}
		else
		{
			if ( $this->type === 'Upload' )
			{
				if ( is_array( $value ) )
				{
					foreach( $value as $idx => $val )
					{
						if ( $val instanceof \IPS\File\FileSystem )
						{
							$value[ $idx ] = (string) $val->url;
						}
					}
				}
				else if ( $value instanceof \IPS\File\FileSystem )
				{
					$value = (string) $value->url;
				}
			}
			if ( ! isset( \IPS\Data\Store::i()->$functionName ) )
			{
				\IPS\Data\Store::i()->$functionName = \IPS\Theme::compileTemplate( $template, $functionName, '$value, $formValue, $label', true );
			}

			$template = \IPS\Data\Store::i()->$functionName;
		}

		\IPS\Theme::runProcessFunction( $template, $functionName );

		return call_user_func( 'IPS\\Theme\\'. $functionName, $value, $formValue, $this->_title );
	}

	/**
	 * Show this form field?
	 * 
	 * @param	 string	 	$field		Field key
	 * @param	 string		$where		Where to show, form or record
	 * @param	\IPS\Member|\IPS\Member\Group|NULL	$member			The member or group to check (NULL for currently logged in member)
	 * @return	 boolean
	 */
	public static function fixedFieldFormShow( $field, $where='form', $member=NULL )
	{
		$fixedFields = \IPS\cms\Databases::load( static::$customDatabaseId )->fixed_field_perms;
		$perm        = ( $where === 'form' ) ? 'perm_2' : 'perm_view';
		
		if ( ! in_array( $field, array_keys( $fixedFields ) ) )
		{
			return FALSE;
		}
		
		$permissions = $fixedFields[ $field ];
		
		if ( empty( $permissions['visible'] ) OR empty( $permissions[ $perm ] ) )
		{
			return FALSE;
		}
		
		/* Load member */
		if ( $member === NULL )
		{
			$member = \IPS\Member::loggedIn();
		}
		
		/* Finally check permissions */
		if( $member instanceof \IPS\Member\Group )
		{
			return ( $permissions[ $perm ] === '*' or ( $permissions[ $perm ] and in_array( $member->g_id, explode( ',', $permissions[ $perm ] ) ) ) );
		}
		else
		{
			return ( $permissions[ $perm ] === '*' or ( $permissions[ $perm ] and $member->inGroup( explode( ',', $permissions[ $perm ] ) ) ) );
		}
	}
	
	/**
	 * Get fixed field permissions as an array or a *
	 * 
	 * @param	string|null		$field		Field Key
	 * @return array|string|null
	 */
	public static function fixedFieldPermissions( $field=NULL )
	{
		$fixedFields = \IPS\cms\Databases::load( static::$customDatabaseId )->fixed_field_perms;
		
		if ( $field !== NULL AND in_array( $field, array_keys( $fixedFields ) ) )
		{
			return $fixedFields[ $field ]; 
		}
		
		return ( $field !== NULL ) ? NULL : $fixedFields;
	}
	
	/**
	 * Set fixed field permissions
	 *
	 * @param	string	$field		Field Key
	 * @param	array	$values		Perm values
	 * @return  void
	 */
	public static function setFixedFieldPermissions( $field, $values )
	{
		$fixedFields = \IPS\cms\Databases::load( static::$customDatabaseId )->fixed_field_perms;

		foreach( $values as $k => $v )
		{
			$fixedFields[ $field ][ $k ] = $v;
		}

		\IPS\cms\Databases::load( static::$customDatabaseId )->fixed_field_perms = $fixedFields;
		\IPS\cms\Databases::load( static::$customDatabaseId )->save();
	}
	
	/**
	 * Set the visiblity
	 *
	 * @param	string	$field		Field Key
	 * @param	bool	$value		True/False
	 * @return  void
	 */
	public static function setFixedFieldVisibility( $field, $value )
	{
		$fixedFields = \IPS\cms\Databases::load( static::$customDatabaseId )->fixed_field_perms;
	
		$fixedFields[ $field ]['visible'] = $value;

		\IPS\cms\Databases::load( static::$customDatabaseId )->fixed_field_perms = $fixedFields;
		\IPS\cms\Databases::load( static::$customDatabaseId )->save();
	}
	
	/**
	 * Magic method to capture validateInput_{id} callbacks
	 * @param 	string 		$name		Name of method called
	 * @param	 mixed 		$arguments	Args passed
	 * @throws \InvalidArgumentException
	 */
	public static function __callStatic($name, $arguments)
	{
		if ( mb_substr( $name, 0, 14 ) === 'validateInput_' )
		{
			$id = mb_substr( $name, 14 );
			
			if ( is_numeric( $id ) )
			{
				$field = static::load( $id );
			}
			
			if ( ! empty($arguments[0]) AND $field->validator AND $field->validator_custom )
			{
				if ( ! preg_match( $field->validator_custom, $arguments[0] ) )
				{
					throw new \InvalidArgumentException( ( \IPS\Member::loggedIn()->language()->addToStack('content_field_' . $field->id . '_validation_error') === 'content_field_' . $field->id . '_validation_error' ) ? 'content_exception_invalid_custom_validation' : \IPS\Member::loggedIn()->language()->addToStack('content_field_' . $field->id . '_validation_error') );
				}
			}
		}
	}
	
	/**
	 * [ActiveRecord] Duplicate
	 *
	 * @return	void
	 */
	public function __clone()
	{
		parent::__clone();
		
		$this->key .= '_' . $this->id;
		$this->save();
	}
	
	/**
	 * Set some default values
	 * 
	 * @return void
	 */
	public function setDefaultValues()
	{
		$this->_data['extra'] = '';
		$this->_data['default_value'] = '';
		$this->_data['format_opts'] = '';
		$this->_data['validator'] = '';
		$this->_data['topic_format'] = '';
		$this->_data['allowed_extensions'] = '';
		$this->_data['validator_custom'] = '';
		$this->_data['display_json'] = array();;
	}

	/**
	 * Field custom template name
	 *
	 * @param   string  $type   Type of name to fetch
	 * @return string
	 */
	public function fieldTemplateName( $type )
	{
		return 'pages_field_custom_html_' . $type . '_' . $this->id;
	}

	/**
	 * Set the "display json" field
	 *
	 * @param string|array $value
	 * @return void
	 */
	public function set_display_json( $value )
	{
		$this->_data['display_json'] = ( is_array( $value ) ? json_encode( $value ) : $value );
	}

	/**
	 * Get the "display json" field
	 *
	 * @return array
	 */
	public function get_display_json()
	{
		return ( is_array( $this->_data['display_json'] ) ) ? $this->_data['display_json'] : json_decode( $this->_data['display_json'], TRUE );
	}

	/**
	 * Set the "Format Options" field
	 *
	 * @param string|array $value
	 * @return void
	 */
	public function set_format_opts( $value )
	{
		$this->_data['format_opts'] = ( is_array( $value ) ? json_encode( $value ) : $value );
	}
	
	/**
	 * Get the "Format Options" field
	 *
	 * @return array
	 */
	public function get_format_opts()
	{
		return json_decode( $this->_data['format_opts'], TRUE );
	}
	
	/**
	 * Set the "extra" field
	 * 
	 * @param string|array $value
	 * @return void
	 */
	public function set_extra( $value )
	{
		$this->_data['extra'] = ( is_array( $value ) ? json_encode( $value ) : $value );
	}
	
	/**
	 * Set the "allowed_extensions" field
	 *
	 * @param string|array $value
	 * @return void
	 */
	public function set_allowed_extensions( $value )
	{
		$this->_data['allowed_extensions'] = ( is_array( $value ) ? json_encode( $value ) : $value );
	}
	
	/**
	 * Get the "extra" field
	 *
	 * @return array
	 */
	public function get_extra()
	{
		return json_decode( $this->_data['extra'], TRUE );
	}
	
	/**
	 * Get the "allowed_extensions" field
	 *
	 * @return array
	 */
	public function get_allowed_extensions()
	{
		return json_decode( $this->_data['allowed_extensions'], TRUE );
	}
	
	/**
	 * [Node] Get Node Title
	 *
	 * @return	string
	 */
	protected function get__title()
	{
		if ( !$this->id )
		{
			return '';
		}
		
		try
		{
			return \IPS\Member::loggedIn()->language()->get( static::$langKey . '_' . $this->id );
		}
		catch( \UnderflowException $e )
		{
			return FALSE;
		}
	}
	
	/**
	 * [Node] Get Description
	 *
	 * @return	string|null
	 */
	protected function get__description()
	{
		try
		{
			return \IPS\Member::loggedIn()->language()->get( static::$langKey . '_' . $this->id . '_desc' );
		}
		catch( \UnderflowException $e )
		{
			return FALSE;
		}
	}
	
	/**
	 * [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()
	{
		$badge = null;

		if ( \IPS\cms\Databases::load( $this->database_id )->field_title == $this->id )
		{
			$badge = array( 0 => 'positive ipsPos_right', 1 => 'content_fields_is_title' );
		}
		else if ( \IPS\cms\Databases::load( $this->database_id )->field_content == $this->id )
		{
			$badge = array( 0 => 'positive ipsPos_right', 1 => 'content_fields_is_content' );
		}
		
		return $badge;
	}

	/**
	 * [Node] Get Icon for tree
	 *
	 * @note	Return the class for the icon (e.g. 'globe')
	 * @return	string|null
	 */
	protected function get__icon()
	{
		if ( class_exists( '\IPS\Helpers\Form\\' . ucfirst( $this->type ) ) )
		{
			return NULL;
		}
		else if ( class_exists( '\IPS\cms\Fields\\' . ucfirst( $this->type ) ) )
		{
			return NULL;
		}

		return 'warning';
	}


	/**
	 * Truncate the field value
	 * 
	 * @param	string      $text	Value to truncate
	 * @param   boolean     $oneLine    Truncate to a single line?
	 * @return	string
	 */
	public function truncate( $text, $oneLine=FALSE )
	{
		if ( ! $this->truncate )
		{
			return $text;
		}
		
		switch( ucfirst( $this->type ) )
		{
			default:
				// No truncate
			break;
			case 'Radio':
			case 'Select':
			case 'Text':
				$text = mb_substr( $text, 0, $this->truncate );
			break;
			case 'TextArea':
			case 'Editor':
				$text = preg_replace( '#</p>(\s+?)?<p>#', $oneLine ? ' $1' : '<br>$1', $text );
				$text = str_replace( array( '<p>', '</p>', '<div>', '</div>' ), '', $text );
				$text = '<div data-ipsTruncate data-ipsTruncate-size="' . $this->truncate . ' lines">' . $text . '</div>';
			break;
		}
		
		return $text;
	}

	/**
	 * Display Value
	 *
	 * @param	mixed	$value	The value
	 * @return	string
	 */
	public function displayValue( $value=NULL )
	{
		$database = \IPS\cms\Databases::load( static::$customDatabaseId );
		
		if ( class_exists( '\IPS\cms\Fields\\' . ucfirst( $this->type ) ) )
		{
			/* Is special! */
			$class = '\IPS\cms\Fields\\' . ucfirst( $this->type );
			
			if ( method_exists( $class, 'displayValue' ) )
			{
				return $class::displayValue( $value, $this );
			}
		}
		
		switch( ucfirst( $this->type ) )
		{
			case 'Upload':
				return \IPS\File::get( 'cms_Records', $value )->url;
			break;
			case 'Text':
			case 'TextArea':
				$value = $this->applyFormatter( $value );

				/* We don't want the parent adding wordbreak to the title */
				if ( $this->id == $database->field_title OR $this->id == $database->field_content )
				{
					return $value;
				}
				
				/* If we allow HTML, then do not pass to parent::displayValue as htmlspecialchars is run */
				if ( $this->html )
				{
					return $value;
				}
			break;
			case 'Select':
			case 'Radio':
			case 'CheckboxSet':
				/* This comes from a keyValue stack, so reformat */
				if ( $this->extra and isset( $this->extra[0]['key'] ) )
				{
					$extra = array();
					foreach( $this->extra as $id => $row )
					{
						$extra[ $row['key'] ] = $row['value']; 
					}
			
					$this->extra = $extra;
				}

				if ( ! is_array( $value ) and $this->is_multiple )
				{
					$value = explode( ',', $value );
				}

				if ( is_array( $value ) )
				{
					$return = array();
					foreach( $value as $key )
					{
						$return[] = isset( $this->extra[ $key ] ) ? htmlspecialchars( $this->extra[ $key ], \IPS\HTMLENTITIES, 'UTF-8', FALSE ) : htmlspecialchars( $key, \IPS\HTMLENTITIES, 'UTF-8', FALSE );
					}

					return implode( ', ', $return );
				}
				else
				{
					return ( isset( $this->extra[ $value ] ) ? htmlspecialchars( $this->extra[ $value ], \IPS\HTMLENTITIES, 'UTF-8', FALSE ) : htmlspecialchars( $value, \IPS\HTMLENTITIES, 'UTF-8', FALSE ) );
				}
			break;
			case 'Member':
				if ( ! $value )
				{
					return NULL;
				}
				else
				{
					return implode( ', ', array_map( function( $id )
					{
						return \IPS\Member::load( $id )->link();
					}, explode( ',', $value ) ) );
				}
			break;
			case 'Url':
				if ( \IPS\Dispatcher::hasInstance() AND class_exists( '\IPS\Dispatcher', FALSE ) and \IPS\Dispatcher::i()->controllerLocation === 'front' )
				{
					return ( $value ) ? \IPS\Theme::i()->getTemplate( 'global', 'core', 'global' )->basicUrl( $value ) : NULL;
				}
			break;
			case 'Date':
				if ( ! $value or ! is_numeric( $value ) )
				{
					return \IPS\Member::loggedIn()->language()->addToStack('field_no_value_entered');
				}

				$time = \IPS\DateTime::ts( $value, TRUE );
				if ( isset( $this->extra['timezone'] ) and $this->extra['timezone'] )
				{
					$time->setTimezone( new \DateTimeZone( $this->extra['timezone'] ) );
				}

				return \IPS\Lang::wordbreak( $time->localeDate() );

			break;
			case 'Item':
				if ( ! is_array( $value ) and mb_strstr( $value, ',' ) )
				{
					$value = explode( ',', $value );
				}
				else
				{
					$value = array( $value );
				}

				if ( count( $value ) and isset( $this->extra['database'] ) and $this->extra['database'] )
				{
					$results = array();
					$class   = '\IPS\cms\Records' . $this->extra['database'];
					$field   = $class::$databasePrefix . $class::$databaseColumnMap['title'];
					$where   = array( \IPS\Db::i()->in( $class::$databaseColumnId, $value ) );

					foreach( $class::getItemsWithPermission( array( $where ), $field, NULL ) as $item )
					{
						$results[] = $item;
					}

					return \IPS\Theme::i()->getTemplate( 'global', 'cms', 'front' )->basicRelationship( $results );
				}

				return NULL;
				break;
		}

		/* Formatters */
		try
		{
			return parent::displayValue( $value );
		}
		catch( \InvalidArgumentException $ex )
		{
			return NULL;
		}
	}
	
	/**
	 * Apply formatter
	 *
	 * @param	mixed	$value	The value
	 * @return	string
	 */
	public function applyFormatter( $value )
	{
		if ( is_array( $this->format_opts ) and count( $this->format_opts ) )
		{
			foreach( $this->format_opts as $id => $type )
			{
				switch( $type )
				{
					case 'strtolower':
						$value	= mb_convert_case( $value, MB_CASE_LOWER );
					break;
					
					case 'strtoupper':
						$value	= mb_convert_case( $value, MB_CASE_UPPER );
					break;
					
					case 'ucfirst':
						$value	= ( mb_strtoupper( mb_substr( $value, 0, 1 ) ) . mb_substr( $value, 1, mb_strlen( $value ) ) );
					break;
					
					case 'ucwords':
						$value	= mb_convert_case( $value, MB_CASE_TITLE );
					break;
					
					case 'punct':
						$value	= preg_replace( "/\?{1,}/"		, "?"		, $value );
						$value	= preg_replace( "/(&#33;){1,}/"	, "&#33;"	, $value );
					break;
					
					case 'numerical':
						$value	= \IPS\Member::loggedIn()->language()->formatNumber( $value );
					break;
					
					case 'bold':
						$value = '<strong>' . $value . '</strong>';
					break;
					
					case 'italic':
						$value = '<em>' . $value . '</em>';
					break;
				}
			}
		}
		
		return $value;
	}


	/**
	 * [Node] Get buttons to display in tree
	 * Example code explains return value
	 *
	 * @code
	 array(
	 array(
	 'icon'	=>	'plus-circle', // Name of FontAwesome icon to use
	 'title'	=> 'foo',		// Language key to use for button's title parameter
	 'link'	=> \IPS\Http\Url::internal( 'app=foo...' )	// URI to link to
	 'class'	=> 'modalLink'	// CSS Class to use on link (Optional)
	 ),
	 ...							// Additional buttons
	 );
	 * @endcode
	 * @param	string	$url		Base URL
	 * @param	bool	$subnode	Is this a subnode?
	 * @return	array
	 */
	public function getButtons( $url, $subnode=FALSE )
	{
		$buttons  = parent::getButtons( $url, $subnode );
		$database = \IPS\cms\Databases::load( $this->database_id );

		if ( $this->canEdit() )
		{
			if ( $this->id != $database->field_title and $this->id != $database->field_content )
			{
				if ( $this->canBeTitleField() )
				{
					$buttons['set_as_title'] = array(
						'icon'	=> 'list-ul',
						'title'	=> 'cms_set_field_as_title',
						'link'	=> $url->setQueryString( array( 'do' => 'setAsTitle', 'id' => $this->_id ) ),
						'data'	=> array()
					);
				}

				if ( $this->canBeContentField() )
				{
					$buttons['set_as_content'] = array(
						'icon'	=> 'file-text-o',
						'title'	=> 'cms_set_field_as_content',
						'link'	=> $url->setQueryString( array( 'do' => 'setAsContent', 'id' => $this->_id ) ),
						'data'	=> array()
					);
				}
			}
		}

		return $buttons;
	}

	/**
	 * Can this field be a title field?
	 *
	 * @return boolean
	 */
	public function canBeTitleField()
	{
		if ( $this->is_multiple or in_array( ucfirst( $this->type ), static::$cannotBeTitleFields ) )
		{
			return FALSE;
		}

		return TRUE;
	}

	/**
	 * Can this field be a content field?
	 *
	 * @return boolean
	 */
	public function canBeContentField()
	{
		$no = array();

		if ( $this->is_multiple or in_array( ucfirst( $this->type ), static::$cannotBeContentFields ) )
		{
			return FALSE;
		}

		return TRUE;
	}

	/**
	 * [Node] Does the currently logged in user have permission to delete this node?
	 *
	 * @return	bool
	 */
	public function canDelete()
	{
		$database = \IPS\cms\Databases::load( $this->database_id );

		if ( $this->id == $database->field_title or $this->id == $database->field_content )
		{
			return FALSE;
		}

		return parent::canDelete();
	}

	/**
	 *
	 * [Node] Does the currently logged in user have permission to edit permissions for this node?
	 *
	 * @return	bool
	 */
	public function canManagePermissions()
	{
		return true;
	}
	
	/**
	 * [Node] Add/Edit Form
	 *
	 * @param	\IPS\Helpers\Form	$form	The form
	 * @return	void
	 */
	public function form( &$form )
	{
		$form->hiddenValues['database_id'] = static::$customDatabaseId;

		if ( $this->type )
		{
			$ok = FALSE;
			if ( class_exists( '\IPS\Helpers\Form\\' . ucfirst( $this->type ) ) )
			{
				$ok = TRUE;
			}
			else if ( class_exists( '\IPS\cms\Fields\\' . ucfirst( $this->type ) ) )
			{
				$ok = TRUE;
			}

			if ( !$ok )
			{
				\IPS\Output::i()->output .= \IPS\Theme::i()->getTemplate( 'global', 'core', 'global' )->message( \IPS\Member::loggedIn()->language()->addToStack( 'cms_field_no_type_warning', NULL, array( 'sprintf' => array( $this->type ) ) ), 'warning', NULL, FALSE );
			}
		}

		$form->addTab( 'field_generaloptions' );
		$form->addHeader( 'pfield_settings' );

		$form->add( new \IPS\Helpers\Form\Translatable( 'field_title', NULL, TRUE, array( 'app' => 'core', 'key' => ( $this->id ? static::$langKey . '_' . $this->id : NULL ) ) ) );
		$form->add( new \IPS\Helpers\Form\Translatable( 'field_description', NULL, FALSE, array( 'app' => 'core', 'key' => ( $this->id ? static::$langKey . '_' . $this->id . '_desc' : NULL ) ) ) );

		$displayDefaults = array( 'field_display_listing_json_badge', 'field_display_listing_json_badge_right', 'field_display_listing_json_custom', 'field_display_display_json_badge', 'field_display_display_json_custom', 'field_display_display_json_where' );

		$options = array_merge( array(
            'Address'    => 'pf_type_Address',
            'Checkbox'   => 'pf_type_Checkbox',
            'CheckboxSet' => 'pf_type_CheckboxSet',
            'Codemirror' => 'pf_type_Codemirror',
            'Item'       => 'pf_type_Relational',
            'Date'		 => 'pf_type_Date',
            'Editor'	 => 'pf_type_Editor',
            'Email'		 => 'pf_type_Email',
            'Member'     => 'pf_type_Member',
            'Number'	 => 'pf_type_Number',
            'Password'	 => 'pf_type_Password',
            'Radio'		 => 'pf_type_Radio',
            'Select'	 => 'pf_type_Select',
            'Tel'		 => 'pf_type_Tel',
            'Text'		 => 'pf_type_Text',
            'TextArea'	 => 'pf_type_TextArea',
            'Upload'	 => 'pf_type_Upload',
            'Url'		 => 'pf_type_Url',
            'YesNo'		 => 'pf_type_YesNo',
            'Youtube'    => 'pf_type_Youtube',
            'Spotify'    => 'pf_type_Spotify',
            'Soundcloud' => 'pf_type_Soundcloud',
        ), static::$additionalFieldTypes );

		$toggles = array(
			'Address'	=> array_merge( array( 'field_show_map_listing', 'field_show_map_display', 'field_show_map_listing_dims', 'field_show_map_display_dims' ), $displayDefaults ),
			'Codemirror'=> array_merge( array( 'field_default_value', 'field_truncate' ), $displayDefaults ),
			'Checkbox'  => array_merge( array( 'field_default_value', 'field_truncate' ), $displayDefaults ),
			'CheckboxSet' => array_merge( array( 'field_extra', 'field_default_value', 'field_filter', 'field_truncate' ), $displayDefaults ),
			'Date'		=> array_merge( array( 'field_default_value', 'field_date_time_override' ), $displayDefaults ),
			'Editor'    => array_merge( array( 'field_max_length', 'field_default_value', 'field_truncate' ), $displayDefaults ),
			'Email'		=> array_merge( array( 'field_max_length', 'field_default_value' ), $displayDefaults ),
			'Item'      => array_merge( array( 'field_is_multiple', 'field_relational_db' ), $displayDefaults ),
			'Member'    => array_merge( array( 'field_is_multiple' ), $displayDefaults ),
			'Number'    => array_merge( array( 'field_default_value', 'field_number_decimals_on', 'field_number_decimals' ), $displayDefaults ),
			'Password'  => array_merge( array( 'field_default_value' ), $displayDefaults ),
			'Radio'     => array_merge( array( 'field_extra', 'field_default_value', 'field_filter', 'field_truncate' ), $displayDefaults ),
			'Select'    => array_merge( array( 'field_extra', 'field_default_value', 'field_filter', 'field_is_multiple', 'field_truncate' ), $displayDefaults ),
			'Tel'		=> array_merge( array( 'field_default_value' ), $displayDefaults ),
			'Text'		=> array_merge( array( 'field_validator', 'field_format_opts_on', 'field_max_length', 'field_default_value', 'field_html', 'field_truncate' ), $displayDefaults ),
			'TextArea'	=> array_merge( array( 'field_validator', 'field_format_opts_on', 'field_max_length', 'field_default_value', 'field_html', 'field_truncate' ), $displayDefaults ),
			'Upload'    => array_merge( array( 'field_upload_is_image', 'field_upload_is_multiple' ), $displayDefaults ),
			'Url'		=> array_merge( array( 'field_default_value' ), $displayDefaults ),
			'YesNo'		=> array_merge( array( 'field_default_value' ), $displayDefaults ),
			'Youtube'   => array( 'media_params', 'media_display_listing_method', 'media_display_display_method' ),
			'Spotify'   => array( 'media_params', 'media_display_listing_method', 'media_display_display_method' ),
			'Soundcloud'=> array( 'media_params', 'media_display_listing_method', 'media_display_display_method' )
		);

		foreach ( static::$additionalFieldTypes as $k => $v )
		{
			$toggles[ $k ] = isset( static::$additionalFieldToggles[ $k ] ) ? static::$additionalFieldToggles[ $k ] : array( 'pf_not_null' );
		}
		
		/* Title or content? */
		if ( $this->id )
		{
			$database = \IPS\cms\Databases::load( static::$customDatabaseId );
		
			if ( $this->id == $database->field_title )
			{
				foreach( static::$cannotBeTitleFields as $type )
				{
					unset( $options[ $type ] );
					unset( $toggles[ $type ] );
				}
			}
			else if ( $this->id == $database->field_content )
			{
				foreach( static::$cannotBeContentFields as $type )
				{
					unset( $options[ $type ] );
					unset( $toggles[ $type ] );
				}
			}
		}
		
		ksort( $options );

		if ( !$this->_new )
		{
			\IPS\Member::loggedIn()->language()->words['field_type_warning'] = \IPS\Member::loggedIn()->language()->addToStack('custom_field_change');

			foreach ( $toggles as $k => $_toggles )
			{
				if ( !$this->canKeepValueOnChange( $k ) )
				{
					$toggles[ $k ][] = 'form_' . $this->id . '_field_type_warning';
				}
			}
		}

		$form->add( new \IPS\Helpers\Form\Select( 'field_type', $this->id ? \ucfirst( $this->type ) : 'Text', TRUE, array(
				'options' => $options,
				'toggles' => $toggles
		) ) );

		/* Relational specific */
		$databases = array();
		$disabled  = array();
		foreach( \IPS\cms\Databases::databases() as $db )
		{
			if ( $db->page_id and $db->id !== static::$customDatabaseId )
			{
				$databases[ $db->id ] = $db->_title;
			}
		}

		if ( ! count( $databases ) )
		{
			$databases[0] = \IPS\Member::loggedIn()->language()->addToStack('cms_relational_field_no_dbs');
			$disabled[] = 0;
		}

		$form->add( new \IPS\Helpers\Form\Select( 'field_relational_db', ( isset( $this->extra['database'] ) ? $this->extra['database'] : NULL ), FALSE, array( 'options' => $databases, 'disabled' => $disabled ), NULL, NULL, NULL, 'field_relational_db' ) );
		
		/* Number specific */
		$form->add( new \IPS\Helpers\Form\YesNo( 'field_number_decimals_on', $this->id ? ( ( isset( $this->extra['on'] ) and $this->extra['on'] ) ? TRUE : FALSE ) : FALSE, FALSE, array( 'togglesOn' => array( 'field_number_decimals' ) ), NULL, NULL, NULL, 'field_number_decimals_on' ) );
		$form->add( new \IPS\Helpers\Form\Number( 'field_number_decimals', $this->id ? ( ( isset( $this->extra['places'] ) and $this->extra['places'] ) ? $this->extra['places'] : 0 ) : 0, FALSE, array( 'max' => 6 ), NULL, NULL, NULL, 'field_number_decimals' ) );
		
		/* Upload specific */
		$form->add( new \IPS\Helpers\Form\YesNo( 'field_upload_is_multiple', $this->id ? $this->is_multiple : 0, FALSE, array( ), NULL, NULL, NULL, 'field_upload_is_multiple' ) );

		$form->add( new \IPS\Helpers\Form\Radio( 'field_upload_is_image', $this->id ? ( ( isset( $this->extra['type'] ) and $this->extra['type'] == 'image' ) ? 'yes' : 'no' ) : 'yes', TRUE, array(
			'options'	=> array(
				'yes' => 'cms_upload_field_is_image',
				'no'  => 'cms_upload_field_is_not_image',

			),
			'toggles' => array(
				'yes' => array( 'field_image_size' ),
				'no'  => array( 'field_allowed_extensions' )
			)
		), NULL, NULL, NULL, 'field_upload_is_image' ) );

		$widthHeight = NULL;
		if ( isset( $this->extra['type'] ) and $this->extra['type'] === 'image' )
		{
			$widthHeight = $this->extra['maxsize'];
		}

		$form->add( new \IPS\Helpers\Form\WidthHeight( 'field_image_size', $this->id ? $widthHeight : array( 0, 0 ), FALSE, array( 'resizableDiv' => FALSE, 'unlimited' => array( 0, 0 ) ), NULL, NULL, NULL, 'field_image_size' ) );

		$form->add( new \IPS\Helpers\Form\Text( 'field_allowed_extensions', $this->id ? ( $this->allowed_extensions ?: NULL ) : NULL, FALSE, array(
			'autocomplete' => array( 'unique' => 'true' ),
			'nullLang'     => 'content_any_extensions'
		), NULL, NULL, NULL, 'field_allowed_extensions' ) );

		/* Date specific */
		$form->add( new \IPS\Helpers\Form\YesNo( 'field_date_time_override', ( isset( $this->extra['timezone'] ) ? $this->extra['timezone'] : FALSE ), FALSE, array( 'togglesOn' => array( 'field_date_timezone' ) ), NULL, NULL, NULL, 'field_date_time_override' ) );
		$timezones = array();
		foreach ( \DateTimeZone::listIdentifiers() as $tz )
		{
			if ( $pos = mb_strpos( $tz, '/' ) )
			{
				$timezones[ 'timezone__' . mb_substr( $tz, 0, $pos ) ][ $tz ] = 'timezone__' . $tz;
			}
			else
			{
				$timezones[ $tz ] = 'timezone__' . $tz;
			}
		}
		$form->add( new \IPS\Helpers\Form\Select( 'field_date_timezone', ( isset( $this->extra['timezone'] ) ? $this->extra['timezone'] : \IPS\Member::loggedIn()->timezone ), FALSE, array( 'options' => $timezones ), NULL, NULL, NULL, 'field_date_timezone' ) );

		$form->add( new \IPS\Helpers\Form\YesNo( 'field_is_multiple', $this->id ? $this->is_multiple : 0, FALSE, array(), NULL, NULL, NULL, 'field_is_multiple' ) );
		
		$form->add( new \IPS\Helpers\Form\TextArea( 'field_default_value', $this->id ? $this->default_value : '', FALSE, array(), NULL, NULL, NULL, 'field_default_value' ) );

		if ( ! $this->_new )
		{
			$form->add( new \IPS\Helpers\Form\YesNo( 'field_default_update_existing', FALSE, FALSE, array(), NULL, NULL, NULL, 'field_default_update_existing' ) );
		}

		$form->add( new \IPS\Helpers\Form\Number( 'field_max_length', $this->id ? $this->max_length : NULL, FALSE, array( 'unlimited' => 0 ), NULL, NULL, NULL, 'field_max_length' ) );
		
		$form->add( new \IPS\Helpers\Form\YesNo( 'field_validator', $this->id ? intval( $this->validator ) : 0, FALSE, array(
			'togglesOn' =>array( 'field_validator_custom', 'field_validator_error' )
		), NULL, NULL, NULL, 'field_validator' ) );
		
		$form->add( new \IPS\Helpers\Form\Text( 'field_validator_custom', $this->id ? $this->validator_custom : NULL, FALSE, array( 'placeholder' => '/[A-Z0-9]+/i' ), NULL, NULL, NULL, 'field_validator_custom' ) );
		$form->add( new \IPS\Helpers\Form\Translatable( 'field_validator_error', NULL, FALSE, array( 'app' => 'core', 'key' => ( $this->id ? static::$langKey . '_' . $this->id . '_validation_error' : NULL ) ), NULL, NULL, NULL, 'field_validator_error' ) );
		
		$form->add( new \IPS\Helpers\Form\YesNo( 'field_format_opts_on', $this->id ? $this->format_opts : 0, FALSE, array( 'togglesOn' => array('field_format_opts') ), NULL, NULL, NULL, 'field_format_opts_on' ) );
		
		$form->add( new \IPS\Helpers\Form\Select( 'field_format_opts', $this->id ? $this->format_opts : 'none', FALSE, array(
				'options' => array(
						'strtolower' => 'content_format_strtolower',
						'strtoupper' => 'content_format_strtoupper',
						'ucfirst'    => 'content_format_ucfirst',
						'ucwords'    => 'content_format_ucwords',
						'punct'	     => 'content_format_punct',
						'numerical'	 => 'content_format_numerical',
						'bold'		 => 'content_format_bold',
						'italic'	 => 'content_format_italic'
				),
				'multiple' => true
		), NULL, NULL, NULL, 'field_format_opts' ) );
		
		$extra = array();
		if ( $this->id AND $this->extra )
		{
			foreach( $this->extra as $k => $v )
			{
				$extra[] = array( 'key' => $k, 'value' => $v );
			}
		}
		
		$form->add( new \IPS\Helpers\Form\Stack( 'field_extra', $extra, FALSE, array( 'stackFieldType' => 'KeyValue'), NULL, NULL, NULL, 'field_extra' ) );

		/* Media specific stack */
		$form->add( new \IPS\Helpers\Form\Stack( 'media_params', $extra, FALSE, array( 'stackFieldType' => 'KeyValue'), NULL, NULL, NULL, 'media_params' ) );

		$form->addheader( 'pfield_options' );

		$form->add( new \IPS\Helpers\Form\YesNo( 'field_filter', $this->id ? $this->filter : 0, FALSE, array(), NULL, NULL, NULL, 'field_filter' ) );

		/* Until we have a mechanism for other field searching, remove this $form->add( new \IPS\Helpers\Form\YesNo( 'field_is_searchable', $this->id ? $this->is_searchable : 0, FALSE, array(), NULL, NULL, NULL, 'field_is_searchable' ) );*/

		if ( isset( \IPS\Request::i()->database_id ) AND \IPS\cms\Databases::load( \IPS\Request::i()->database_id )->forum_record )
		{
			$form->add( new \IPS\Helpers\Form\TextArea( 'field_topic_format', $this->id ? $this->topic_format : '', FALSE, array( 'placeholder' => "<strong>{title}:</strong> {value}" ) ) );
		}

		if ( ! ( $this->id AND $this->id === \IPS\cms\Databases::load( static::$customDatabaseId )->field_title ) )
		{
			$form->add( new \IPS\Helpers\Form\YesNo( 'field_required', $this->id ? $this->required : TRUE, FALSE ) );
		}

		$form->add( new \IPS\Helpers\Form\YesNo( 'field_html', $this->id ? $this->html : FALSE, FALSE, array( 'togglesOn' => array( 'field_html_warning' ) ), NULL, NULL, NULL, 'field_html' ) );

		$form->addTab( 'field_displayoptions' );

		$isTitleOrContent = FALSE;
		if ( $this->id AND ( $this->id == \IPS\cms\Databases::load( static::$customDatabaseId )->field_title OR $this->id == \IPS\cms\Databases::load( static::$customDatabaseId )->field_content ) )
		{
			$isTitleOrContent = TRUE;

			if ( $this->id == \IPS\cms\Databases::load( static::$customDatabaseId )->field_title )
			{
				$form->addMessage( 'field_display_opts_title', 'ipsMessage ipsMessage_info' );
			}

			if ( $this->id == \IPS\cms\Databases::load( static::$customDatabaseId )->field_content )
			{
				$form->addMessage( 'field_display_opts_content', 'ipsMessage ipsMessage_info' );
			}
		}

		$form->add( new \IPS\Helpers\Form\Text( 'field_key', $this->id ? $this->key : FALSE, FALSE, array(), function( $val )
		{
			try
			{
				if ( ! $val )
				{
					return true;
				}

				$class = '\IPS\cms\Fields' . \IPS\Request::i()->database_id;

				try
				{
					$testField = $class::load( $val, 'field_key');
				}
				catch( \OutOfRangeException $ex )
				{
					/* Doesn't exist? Good! */
					return true;
				}

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

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

			return true;
		} ) );

		$displayToggles = array( 'custom' => array( 'field_display_display_json_custom' ) );
		$listingToggles = array( 'custom' => array( 'field_display_listing_json_custom' ) );
		$displayJson    = $this->display_json;
		$displayDefault = isset( $displayJson['display']['method'] ) ? $displayJson['display']['method'] : '1';
		$listingDefault = isset( $displayJson['listing']['method'] ) ? $displayJson['listing']['method'] : '1';
		$mediaDisplayDefault = isset( $displayJson['display']['method'] ) ? $displayJson['display']['method'] : 'player';
		$mediaListingDefault = isset( $displayJson['listing']['method'] ) ? $displayJson['listing']['method'] : 'url';
		$mapDisplay = isset( $displayJson['display']['map'] ) ? $displayJson['display']['map'] : FALSE;
		$mapListing = isset( $displayJson['listing']['map'] ) ? $displayJson['listing']['map'] : FALSE;
		$mapDisplayDims = isset( $displayJson['display']['mapDims'] ) ? $displayJson['display']['mapDims'] : array( 200, 200 );
		$mapListingDims = isset( $displayJson['listing']['mapDims'] ) ? $displayJson['listing']['mapDims'] : array( 100, 100 );
		$listingOptions = $displayOptions = array();

		foreach( range( 1, 7 ) as $id )
		{
			$displayOptions[ $id ] = $listingOptions[ $id ] = \IPS\Theme::i()->getTemplate( 'records', 'cms', 'global' )->fieldBadge( \IPS\Member::loggedIn()->language()->addToStack('cms_badge_label'), \IPS\Member::loggedIn()->language()->addToStack('cms_badge_value'), 'ipsBadge_style' . $id );
			$listingToggles[ $id ] = array( 'field_display_listing_json_badge_right' );
		}

		$displayOptions['simple'] = $listingOptions['simple'] = \IPS\Theme::i()->getTemplate( 'records', 'cms', 'global' )->fieldDefault( \IPS\Member::loggedIn()->language()->addToStack('cms_badge_label'), \IPS\Member::loggedIn()->language()->addToStack('cms_badge_value' ) );
		$displayOptions['custom'] = $listingOptions['custom'] = \IPS\Member::loggedIn()->language()->addToStack('field_display_custom');
		$displayOptions['none'] = $listingOptions['none']     = \IPS\Member::loggedIn()->language()->addToStack('field_display_none');

		if ( ! $isTitleOrContent )
		{
			$form->add( new \IPS\Helpers\Form\YesNo( 'field_display_listing', $this->id ? $this->display_listing : 1, FALSE, array( 'togglesOn' => array('field_truncate', 'field_display_listing_json_badge') ), NULL, NULL, NULL, 'field_display_listing' ) );
		}

		$form->add( new \IPS\Helpers\Form\Radio( 'field_display_listing_json_badge', $listingDefault, FALSE, array( 'options' => $listingOptions, 'toggles' => $listingToggles ), NULL, NULL, NULL, 'field_display_listing_json_badge' ) );

		if ( ! $isTitleOrContent )
		{
			$form->add( new \IPS\Helpers\Form\YesNo( 'field_display_listing_json_badge_right', ( isset( $displayJson['listing']['right'] ) ? $displayJson['listing']['right'] : 0 ), FALSE, array(), NULL, NULL, NULL, 'field_display_listing_json_badge_right' ) );
		}

		$form->add( new \IPS\Helpers\Form\YesNo( 'field_show_map_listing', $mapListing, FALSE, array(), NULL, NULL, NULL, 'field_show_map_listing' ) );
		$form->add( new \IPS\Helpers\Form\WidthHeight( 'field_show_map_listing_dims', $mapListingDims, FALSE, array( 'resizableDiv' => FALSE ), NULL, NULL, NULL, 'field_show_map_listing_dims' ) );

		$form->add( new \IPS\Helpers\Form\Codemirror( 'field_display_listing_json_custom', ( isset( $displayJson['listing']['html'] ) ? $displayJson['listing']['html'] : NULL ), FALSE, array( 'placeholder' => '{label}: {value}' ), function( $val )
        {
            /* Test */
            try
            {
	            \IPS\Theme::checkTemplateSyntax( $val );
            }
            catch( \LogicException $e )
            {
	            throw new \LogicException('cms_field_error_bad_syntax');
            }

        }, NULL, NULL, 'field_display_listing_json_custom' ) );

		/* Media listing */
		$mediaListingOptions = array( 'player' => 'media_display_as_player', 'url' => 'media_display_as_url' );
		$form->add( new \IPS\Helpers\Form\Radio( 'media_display_listing_method', $mediaListingDefault, FALSE, array( 'options' => $mediaListingOptions ), NULL, NULL, NULL, 'media_display_listing_method' ) );

		if ( ! $isTitleOrContent )
		{
			$form->add( new \IPS\Helpers\Form\Number( 'field_truncate', $this->id ? $this->truncate : 0, FALSE, array( 'unlimited' => 0 ), NULL, NULL, NULL, 'field_truncate' ) );
		}

		$form->addSeparator();

		if ( ! $isTitleOrContent )
		{
			$form->add( new \IPS\Helpers\Form\YesNo( 'field_display_display', $this->id ? $this->display_display : 1, FALSE, array( 'togglesOn' => array( 'field_display_display_json_badge' ) ), NULL, NULL, NULL, 'field_display_display' ) );
		}

		$form->add( new \IPS\Helpers\Form\Radio( 'field_display_display_json_badge', $displayDefault, FALSE, array( 'options' => $displayOptions, 'toggles' => $displayToggles ), NULL, NULL, NULL, 'field_display_display_json_badge' ) );

		$form->add( new \IPS\Helpers\Form\YesNo( 'field_show_map_display', $mapDisplay, FALSE, array(), NULL, NULL, NULL, 'field_show_map_display' ) );
		$form->add( new \IPS\Helpers\Form\WidthHeight( 'field_show_map_display_dims', $mapDisplayDims, FALSE, array( 'resizableDiv' => FALSE ), NULL, NULL, NULL, 'field_show_map_display_dims' ) );

		$form->add( new \IPS\Helpers\Form\Codemirror( 'field_display_display_json_custom', ( isset( $displayJson['display']['html'] ) ? $displayJson['display']['html'] : NULL ), FALSE, array( 'placeholder' => '{label}: {value}' ), function( $val )
        {
            /* Test */
            try
            {
	            \IPS\Theme::checkTemplateSyntax( $val );
            }
            catch( \LogicException $e )
            {
	            throw new \LogicException('cms_field_error_bad_syntax');
            }

        }, NULL, NULL, 'field_display_display_json_custom' ) );

		/* Display where? */
		$form->add( new \IPS\Helpers\Form\Radio( 'field_display_display_json_where', ( isset( $displayJson['display']['where'] ) ? $displayJson['display']['where'] : 'top' ), FALSE, array( 'options' => array( 'top' => 'cms_field_display_top', 'bottom' => 'cms_field_display_bottom' ) ), NULL, NULL, NULL, 'field_display_display_json_where' ) );

		/* Media display */
		$form->add( new \IPS\Helpers\Form\Radio( 'media_display_display_method', $mediaDisplayDefault, FALSE, array( 'options' => $mediaListingOptions ), NULL, NULL, NULL, 'media_display_display_method' ) );

		$form->addSeparator();

		$form->add( new \IPS\Helpers\Form\YesNo( 'field_display_commentform', $this->id ? $this->display_commentform : 0, FALSE, array(), NULL, NULL, NULL, 'field_display_commentform' ) );
		\IPS\Output::i()->globalControllers[]  = 'cms.admin.fields.form';
		\IPS\Output::i()->jsFiles  = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'admin_fields.js', 'cms' ) );

		\IPS\Output::i()->title  = ( $this->id ) ? \IPS\Member::loggedIn()->language()->addToStack('cms_edit_field', FALSE, array( 'sprintf' => array( $this->_title ) ) ) : \IPS\Member::loggedIn()->language()->addToStack('cms_add_field');
	}

	/**
	 * @brief	Disable the copy button - useful when the forms are very distinctly different
	 */
	public $noCopyButton	= TRUE;

	/**
	 * @brief	Update the default value in records
	 */
	protected $_updateDefaultValue = FALSE;

	/**
	 * @brief	Stores the old default value after a change
	 */
	protected $_oldDefaultValue = NULL;

	/**
	 * [Node] Format form values from add/edit form for save
	 *
	 * @param	array	$values	Values from the form
	 * @return	array
	 */
	public function formatFormValues( $values )
	{
		static::$contentDatabaseTable = 'cms_custom_database_' . static::$customDatabaseId;

		$values['field_max_length'] = ( isset( $values['field_max_length'] ) ) ? intval( $values['field_max_length'] ) : 0;
		
		/* Work out the column definition */
		if( isset( $values['field_type'] ) )
		{
			$columnDefinition = array( 'name' => "field_{$this->id}" );
			switch ( $values['field_type'] )
			{
				case 'CheckboxSet':
				case 'Member':
				case 'Radio':
				case 'Select':
					/* Reformat keyValue pairs */
					if ( isset( $values['field_extra'] ) )
					{
						$extra = array();
						foreach( $values['field_extra'] as $row )
						{
							if ( isset( $row['key'] ) )
							{
								$extra[ $row['key'] ] = $row['value'];
							}
						}

						if ( count( $extra ) )
						{
							$values['field_extra'] = $extra;
						}
					}
					if ( $values['field_type'] === 'Select' )
					{
						$columnDefinition['type'] = 'TEXT';
					}
					else
					{
						$columnDefinition['type']	= 'VARCHAR';
						$columnDefinition['length']	= 255;
					}
					break;
				case 'Youtube':
				case 'Spotify':
				case 'Soundcloud':
					/* Reformat keyValue pairs */
					if ( isset( $values['media_params'] ) )
					{
						$extra = array();
						foreach( $values['media_params'] as $row )
						{
							if ( isset( $row['key'] ) )
							{
								$extra[ $row['key'] ] = $row['value'];
							}
						}

						if ( count( $extra ) )
						{
							$values['field_extra'] = $extra;
						}
					}
					$columnDefinition['type'] = 'TEXT';
					break;
				case 'Date':
				case 'Number':
					if ( isset( $values['field_number_decimals_on'] ) and $values['field_number_decimals_on'] and $values['field_number_decimals'] )
					{
						$columnDefinition['type'] = 'DECIMAL(20,' . $values['field_number_decimals'] . ')';
						$values['field_extra'] = '';
					}
					else
					{
						$columnDefinition['type'] = 'INT';
						$columnDefinition['length'] = 10;
						$values['field_extra'] = '';
						break;
					}
					break;
				case 'YesNo':
					$columnDefinition['type'] = 'INT';
					$columnDefinition['length'] = 10;
					$values['field_extra'] = '';
					break;
				
				case 'Address':
				case 'Codemirror':
				case 'Editor':
				case 'TextArea':
				case 'Upload':
					$columnDefinition['type'] = 'MEDIUMTEXT';
					$values['field_extra'] = '';
					break;
				
				case 'Email':
				case 'Password':
				case 'Tel':
				case 'Text':
				case 'Url':
				case 'Checkbox':
					$columnDefinition['type'] = 'VARCHAR';
					$columnDefinition['length'] = 255;
					$values['field_extra'] = '';
					break;
				default:
					$columnDefinition['type'] = 'TEXT';
					break;
			}
			
			if ( ! empty( $values['field_max_length'] ) AND empty( $columnDefinition['length'] ) )
			{
				$columnDefinition['length'] = $values['field_max_length'];
			}
			else if ( empty( $columnDefinition['length'] ) )
			{
				$columnDefinition['length'] = NULL;
			}
		}

		if ( isset( $values['media_params'] ) )
		{
			unset( $values['media_params'] );
		}

		/* Add/Update the content table */
		if ( !$this->id )
		{
			$this->database_id = static::$customDatabaseId;
			$values['database_id']	= $this->database_id;
			
			$this->save();
			
			$columnDefinition['name'] = "field_{$this->id}";
			
			if ( isset( static::$contentDatabaseTable ) )
			{
				\IPS\Db::i()->addColumn( static::$contentDatabaseTable, $columnDefinition );

				if ( ! empty( $values['field_filter'] ) and $values['field_type'] != 'Upload' )
				{
					try
					{
						if ( $columnDefinition['type'] == 'TEXT' )
						{
							\IPS\Db::i()->addIndex( static::$contentDatabaseTable, array( 'type' => 'fulltext', 'name' => "field_{$this->id}", 'columns' => array( "field_{$this->id}" ) ) );
						}
						else
						{
							\IPS\Db::i()->addIndex( static::$contentDatabaseTable, array( 'type' => 'key', 'name' => "field_{$this->id}", 'columns' => array( "field_{$this->id}" ) ) );
						}
					}
					catch( \IPS\Db\Exception $e )
					{
						if ( $e->getCode() !== 1069 )
						{
							# 1069: MyISAM can only have 64 indexes per table. This should be really rare though so we silently ignore it.
							throw $e;
						}
					}
				}
			}
		}
		elseif( !$this->canKeepValueOnChange( $values['field_type'] ) )
		{
			try
			{
				\IPS\Db::i()->dropIndex( static::$contentDatabaseTable, "field_{$this->id}" );
				\IPS\Db::i()->dropColumn( static::$contentDatabaseTable, "field_{$this->id}" );
			}
			catch ( \IPS\Db\Exception $e ) { }

			\IPS\Db::i()->addColumn( static::$contentDatabaseTable, $columnDefinition );

			if ( $values['field_type'] != 'Upload' )
			{
				try
				{
					if ( $columnDefinition['type'] == 'TEXT' )
					{
						\IPS\Db::i()->addIndex( static::$contentDatabaseTable, array( 'type' => 'fulltext', 'name' => "field_{$this->id}", 'columns' => array( "field_{$this->id}" ) ) );
					}
					else
					{
						\IPS\Db::i()->addIndex( static::$contentDatabaseTable, array( 'type' => 'key', 'name' => "field_{$this->id}", 'columns' => array( "field_{$this->id}" ) ) );
					}
				}
				catch( \IPS\Db\Exception $e )
				{
					if ( $e->getCode() !== 1069 )
					{
						# 1069: MyISAM can only have 64 indexes per table. This should be really rare though so we silently ignore it.
						throw $e;
					}
				}
			}
		}
		elseif ( isset( static::$contentDatabaseTable ) AND isset( $columnDefinition ) )
		{
			\IPS\Db::i()->dropIndex( static::$contentDatabaseTable, "field_{$this->id}" );
			\IPS\Db::i()->changeColumn( static::$contentDatabaseTable, "field_{$this->id}", $columnDefinition );

			if ( $values['field_filter'] and $values['field_type'] != 'Upload' )
			{
				try
				{
					if ( $columnDefinition['type'] == 'TEXT' )
					{
						\IPS\Db::i()->addIndex( static::$contentDatabaseTable, array( 'type' => 'fulltext', 'name' => "field_{$this->id}", 'columns' => array( "field_{$this->id}" ) ) );
					}
					else
					{
						\IPS\Db::i()->addIndex( static::$contentDatabaseTable, array( 'type' => 'key', 'name' => "field_{$this->id}", 'columns' => array( "field_{$this->id}" ) ) );
					}
				}
				catch( \IPS\Db\Exception $e )
				{
					if ( $e->getCode() !== 1069 )
					{
						# 1069: MyISAM can only have 64 indexes per table. This should be really rare though so we silently ignore it.
						throw $e;
					}
				}
			}
		}
		
		/* Save the name and desctipn */
		if( isset( $values['field_title'] ) )
		{
			\IPS\Lang::saveCustom( 'cms', static::$langKey . '_' . $this->id, $values['field_title'] );
		}
		
		if ( isset( $values['field_description'] ) )
		{
			\IPS\Lang::saveCustom( 'cms', static::$langKey . '_' . $this->id . '_desc', $values['field_description'] );
			unset( $values['field_description'] );
		}
		
		if ( array_key_exists( 'field_validator_error', $values ) )
		{
			\IPS\Lang::saveCustom( 'cms', static::$langKey . '_' . $this->id . '_validation_error', $values['field_validator_error'] );
			unset( $values['field_validator_error'] );
		}

		if ( isset( $values['field_format_opts_on'] ) AND ! $values['field_format_opts_on'] )
		{
			$values['field_format_opts'] = NULL;
		}

		if ( isset( $values['field_key'] ) AND ! $values['field_key'] )
		{
			if ( is_array( $values['field_title'] ) )
			{
				$values['field_key'] = \IPS\Http\Url::seoTitle( $values['field_title'][ key( $values['field_title'] ) ] );
			}
			else
			{
				$values['field_key'] = \IPS\Http\Url::seoTitle( $values['field_title'] );
			}

			/* Now test it */
			$class = '\IPS\cms\Fields' . \IPS\Request::i()->database_id;

			try
			{
				$testField = $class::load( $this->key, 'field_key');

				/* It's taken... */
				if ( $this->id != $testField->id )
				{
					$this->key .= '_' . uniqid();
				}
			}
			catch( \OutOfRangeException $ex )
			{
				/* Doesn't exist? Good! */
			}
		}

		if( isset( $values['field_type'] ) AND ( !isset( $values['_skip_formatting'] ) OR $values['_skip_formatting'] !== TRUE ) )
		{
			$displayJson = array( 'display' => array( 'method' => NULL ), 'listing' => array( 'method' => NULL ) );

			/* Listing */
			if ( in_array( $values['field_type'], static::$mediaFields ) )
			{
				$displayJson['listing']['method'] = $values['media_display_listing_method'];
				$displayJson['display']['method'] = $values['media_display_display_method'];
			}
			else
			{
				if ( $values['field_type'] === 'Address' )
				{
					if ( isset( $values['field_show_map_listing'] ) )
					{
						$displayJson['listing']['map'] = (boolean) $values['field_show_map_listing'];
					}

					if ( isset( $values['field_show_map_listing_dims'] ) )
					{
						$displayJson['listing']['mapDims'] = $values['field_show_map_listing_dims'];
					}

					if ( isset( $values['field_show_map_display'] ) )
					{
						$displayJson['display']['map'] = (boolean) $values['field_show_map_display'];
					}

					if ( isset( $values['field_show_map_display_dims'] ) )
					{
						$displayJson['display']['mapDims'] = $values['field_show_map_display_dims'];
					}
				}

				if ( isset( $values['field_display_listing_json_badge'] ) )
				{
					if( isset( $values['field_display_listing_json_custom'] ) )
					{
						$displayJson['listing']['html'] = $values['field_display_listing_json_custom'];
						unset( $values['field_display_listing_json_custom'] );
					}
					else
					{
						$displayJson['listing']['html'] = NULL;
					}

					if ( $values['field_display_listing_json_badge'] === 'custom' )
					{
						$displayJson['listing']['method'] = 'custom';
					}
					else
					{
						$displayJson['listing']['method'] = $values['field_display_listing_json_badge'];
						$displayJson['listing']['right']  = isset( $values['field_display_listing_json_badge_right'] ) ? $values['field_display_listing_json_badge_right'] : FALSE;
					}
				}

				/* Display */
				if ( isset( $values['field_display_display_json_badge'] ) )
				{
					if( isset( $values['field_display_display_json_custom'] ) )
					{
						$displayJson['display']['html'] = $values['field_display_display_json_custom'];
						unset( $values['field_display_display_json_custom'] );
					}
					else
					{
						$displayJson['display']['html'] = NULL;
					}

					if ( $values['field_display_display_json_badge'] === 'custom' )
					{
						$displayJson['display']['method'] = 'custom';
					}
					else
					{
						$displayJson['display']['method'] = $values['field_display_display_json_badge'];
					}

					if ( isset( $values['field_display_display_json_where'] ) )
					{
						$displayJson['display']['where'] = $values['field_display_display_json_where'];
					}
				}
			}

			$values['display_json'] = json_encode( $displayJson );
		}

		/* If we are importing a database we skip the json formatting as it gets set after */
		if( array_key_exists( '_skip_formatting', $values ) )
		{
			unset( $values['_skip_formatting'] );
		}

		/* Special upload stuffs */
		if ( isset( $values['field_type'] ) AND $values['field_type'] === 'Upload' )
		{
			if ( isset( $values['field_upload_is_image'] ) and $values['field_upload_is_image'] === 'yes')
			{
				$values['extra'] = array( 'type' => 'image', 'maxsize' => $values['field_image_size'] );
			}
			else
			{
				$values['extra'] = array( 'type' => 'any' );
			}

			if ( isset( $values['field_upload_is_multiple'] ) and $values['field_upload_is_multiple'] )
			{
				$values['field_is_multiple'] = 1;
			}
			else
			{
				$values['field_is_multiple'] = 0;
			}

			$values['field_default_value'] = NULL;
		}

		/* Special date stuff */
		if ( isset( $values['field_type'] ) AND $values['field_type'] === 'Date' )
		{
			if ( isset( $values['field_date_time_override'] ) and $values['field_date_time_override'] )
			{
				$values['extra'] = array( 'timezone' => $values['field_date_timezone'] );
			}
			else
			{
				$values['extra'] = array();
			}
		}

		/* Special relational stuff */
		if ( isset( $values['field_type'] ) AND $values['field_type'] === 'Item' )
		{
			if ( array_key_exists( 'field_relational_db', $values ) and empty( $values['field_relational_db'] ) )
			{
				throw new \LogicException( \IPS\Member::loggedIn()->language()->addToStack('cms_relational_field_no_db_selected') );
			}

			$values['extra'] = array( 'database' => $values['field_relational_db'] );
		}
		
		/* Special number stuff */
		if ( isset( $values['field_type'] ) AND $values['field_type'] === 'Number' AND isset( $values['field_number_decimals_on'] ) )
		{
			$values['extra'] = array( 'on' => (boolean) $values['field_number_decimals_on'], 'places' => $values['field_number_decimals'] );
		}
		
		if ( ! $this->new AND isset( $values['field_default_update_existing'] ) AND $values['field_default_update_existing'] and $values['field_default_value'] !== $this->default_value )
		{
			$this->_updateDefaultValue = TRUE;
			$this->_oldDefaultValue    = $this->default_value;
		}

		foreach( array( 'field_number_decimals_on', 'field_number_decimals', 'field_format_opts_on', 'field_relational_db', 'field_upload_is_multiple', 'field_default_update_existing', 'field_date_time_override', 'field_date_timezone', 'field_upload_is_image', 'field_image_size', 'field_title', 'field_display_display_json_badge', 'field_display_display_json_custom', 'field_display_listing_json_badge', 'field_display_listing_json_custom', 'field_display_listing_json_badge_right', 'media_display_listing_method', 'media_display_display_method', 'field_show_map_listing', 'field_show_map_listing_dims', 'field_show_map_display', 'field_show_map_display_dims', 'field_display_display_json_where' ) as $field )
		{
			if ( array_key_exists( $field, $values ) )
			{
				unset( $values[ $field ] );
			}
		}
		
		return parent::formatFormValues( $values );
	}

	/**
	 * [Node] Perform actions after saving the form
	 *
	 * @param	array	$values	Values from the form
	 * @return	void
	 */
	public function postSaveForm( $values )
	{
		/* Ensure it has some permissions */
		$this->permissions();

		if ( $this->_updateDefaultValue )
		{
			static::$contentDatabaseTable = 'cms_custom_database_' . static::$customDatabaseId;

			$field = 'field_' . $this->id;
			\IPS\Db::i()->update( static::$contentDatabaseTable, array( $field => $this->default_value ), array( $field . '=?  OR ' . $field . ' IS NULL', $this->_oldDefaultValue ) );
		}
	}

	/**
	 * Does the change mean wiping the value?
	 *
	 * @param	string	$newType	The new type
	 * @return	array
	 */
	protected function canKeepValueOnChange( $newType )
	{
		$custom = array( 'Youtube', 'Spotify', 'Soundcloud', 'Relational' );

		if ( ! in_array( $this->type, $custom ) )
		{
			return parent::canKeepValueOnChange( $newType );
		}

		switch ( $this->type )
		{
			case 'Youtube':
				return in_array( $newType, array( 'Youtube', 'Text', 'TextArea' ) );

			case 'Spotify':
				return in_array( $newType, array( 'Spotify', 'Text', 'TextArea' ) );

			case 'Soundcloud':
				return in_array( $newType, array( 'Soundcloud', 'Text', 'TextArea' ) );
		}

		return FALSE;
	}

	/**
	 * [ActiveRecord] Save Record
	 *
	 * @return	void
	 */
	public function save()
	{
		static::$contentDatabaseTable = 'cms_custom_database_' . static::$customDatabaseId;
		static::$cache = array();

		$functionName = $this->fieldTemplateName('listing');

		if ( isset( \IPS\Data\Store::i()->$functionName ) )
		{
			unset( \IPS\Data\Store::i()->$functionName );
		}

		$functionName = $this->fieldTemplateName('display');

		if ( isset( \IPS\Data\Store::i()->$functionName ) )
		{
			unset( \IPS\Data\Store::i()->$functionName );
		}
		
		$key = 'cms_fieldids_' . static::$customDatabaseId;
		
		if ( isset( \IPS\Data\Store::i()->$key ) )
		{
			unset( \IPS\Data\Store::i()->$key );
		}

		parent::save();
	}
	
	/**
	 * [ActiveRecord] Delete Record
	 *
	 * @param	bool	$skipDrop	Skip dropping the column/index, useful when we are deleting the entire table
	 * @return	void
	 */
	public function delete( $skipDrop=FALSE )
	{
		static::$contentDatabaseTable = 'cms_custom_database_' . static::$customDatabaseId;
		static::$cache = array();
		
		$key = 'cms_fieldids_' . static::$customDatabaseId;
		
		if ( isset( \IPS\Data\Store::i()->$key ) )
		{
			unset( \IPS\Data\Store::i()->$key );
		}
		
		parent::delete();

		if( $skipDrop === TRUE )
		{
			return;
		}
		
		try
		{

			\IPS\Db::i()->dropIndex( static::$contentDatabaseTable, "field_{$this->id}" );
		}
		catch ( \IPS\Db\Exception $e ) {}

		try
		{
			\IPS\Db::i()->dropColumn( static::$contentDatabaseTable, "field_{$this->id}" );
		}
		catch( \IPS\Db\Exception $e ) { }
	}

	/**
	 * Build Form Helper
	 *
	 * @param	mixed	$value	                    The value
	 * @param	callback	$customValidationCode	Custom validation code
	 * @param   \IPS\cms\Records|NULL   $record     The record
	 * @param	int				        $flags		Bit flags
	 * @return \IPS\Helpers\Form\FormAbstract
	 */
	public function buildHelper( $value=NULL, $customValidationCode=NULL, \IPS\cms\Records $record = NULL, $flags=0 )
	{
		if ( class_exists( '\IPS\cms\Fields\\' . ucfirst( $this->type ) ) )
		{
			/* Is special! */
			$class = '\IPS\cms\Fields\\' . ucfirst( $this->type );
		}
		else if ( class_exists( '\IPS\Helpers\Form\\' . ucfirst( $this->type ) ) )
		{
			$class = '\IPS\Helpers\Form\\' . ucfirst( $this->type );

			if ( !is_array( $this->extra ) )
			{
				if ( method_exists( $class, 'formatOptions' ) )
				{
					$options = $class->formatOptions( json_decode( $this->extra ) );
				}
				else
				{
					$options = json_decode( $this->extra );
				}
			}
		}
		else
		{
			/* Fail safe */
			$this->type = 'Text';
			$class = '\IPS\Helpers\Form\Text';
		}

		$options    = array();
		switch ( ucfirst( $this->type ) )
		{
			case 'Editor':
				$options['app']         = 'cms';
				$options['key']         = 'Records';
				$options['autoSaveKey'] = 'RecordField_' . ( $record === NULL ? 'new' : $record->_id ) . '_' . $this->id;
				$options['attachIds']   = ( $record === NULL ) ? NULL : array( $record->_id, $this->id,  static::$customDatabaseId );
				break;
			case 'Email':
			case 'Password':
			case 'Tel':
			case 'Text':
			case 'TextArea':
			case 'Url':
				$options['maxLength']	= $this->max_length ?: NULL;
				$options['regex']		= $this->input_format ?: NULL;
				break;
			case 'Upload':
				$options['storageExtension'] = static::$uploadStorageExtension;

				if ( isset( $this->extra['type'] ) )
				{
					if ( $this->extra['type'] === 'image' )
					{
						$options['image'] = array( 'maxWidth' => $this->extra['maxsize'][0], 'maxHeight' => $this->extra['maxsize'][1] );
					}
					else
					{
						$options['allowedFileTypes'] = $this->allowed_extensions ?: NULL;
					}
				}
				else
				{
					$options['allowedFileTypes'] = $this->allowed_extensions ?: NULL;
				}

				if ( $this->is_multiple )
				{
					$options['multiple'] = TRUE;
				}

				if( $value )
				{
					if ( mb_strstr( $value, ',' ) )
					{
						$files = explode( ',', $value );

						$return = array();
						foreach( $files as $file )
						{
							try
							{
								$return[] = \IPS\File::get( static::$uploadStorageExtension, $file );
							}
							catch ( \OutOfRangeException $e ) { }
						}

						$value = $return;
					}
					else
					{
						try
						{
							$value = array( \IPS\File::get( static::$uploadStorageExtension, $value ) );
						}
						catch ( \OutOfRangeException $e )
						{
							$value = NULL;
						}
					}
				}
				break;
			case 'Select':
			case 'CheckboxSet':
				$options['multiple'] = ( ucfirst( $this->type ) == 'CheckboxSet' ) ? TRUE : $this->is_multiple;
				
				if ( $flags & self::FIELD_DISPLAY_FILTERS or ( ! $this->default_value and ! $this->required ) )
				{
					$options['noDefault'] = true;
				}

				if ( $flags & self::FIELD_DISPLAY_COMMENTFORM )
				{
					$options['noDefault'] = true;
					$this->required       = false;
				}

				if ( $options['multiple'] and ! is_array( $value ) )
				{
					$exp   = explode( ',', $value );
					$value = array();
					foreach( $exp as $val )
					{
						if ( is_numeric( $val ) and intval( $val ) == $val )
						{
							$value[] = intval( $val );
						}
						else
						{
							$value[] = $val;
						}
					}
				}
				else
				{
					if ( is_numeric( $value ) and intval( $value ) == $value )
					{
						$value = intval( $value );
					}
				}

				$json = $this->extra;
				$options['options'] = ( $json ) ? $json : array();
				break;
			case 'Radio':
				$json = $this->extra;
				$options['options'] = ( $json ) ? $json : array();
				$options['multiple'] = FALSE;
				break;
			case 'Address':
				$value = \IPS\GeoLocation::buildFromJson( $value );
				break;
			
			case 'Member':
				if ( ! $value )
				{
					$value = NULL;
				}

				$options['multiple'] = $this->is_multiple ? NULL : 1;

				if ( is_string( $value ) )
				{
					$value = array_map( function( $id )
					{
						return \IPS\Member::load( $id );
					}, explode( ',', $value ) );
				}
				break;
			case 'Date':
				if ( is_numeric( $value ) )
				{
					$value = \IPS\DateTime::ts( $value );
				}
				
				if ( isset( $this->extra['timezone'] ) and $this->extra['timezone'] )
				{
					$options['timezone'] = new \DateTimeZone( $this->extra['timezone'] );
					
					if ( $value instanceof \IPS\DateTime )
					{
						$value->setTimezone( $options['timezone'] );
					}
				}

				break;
			case 'Item':
				$options['maxItems'] = ( $this->is_multiple ) ? NULL : 1;
				$options['class']    = '\IPS\cms\Records' . $this->extra['database'];
				break;
			case 'Number':
				if ( $this->extra['on'] and $this->extra['places'] )
				{
					$options['decimals'] = $this->extra['places'];
				}
				break;
		}
		
		if ( $this->validator AND $this->validator_custom )
		{
			switch( ucfirst( $this->type ) )
			{
				case 'Text':
				case 'TextArea':
					$customValidationCode = 'IPS\cms\Fields' . static::$customDatabaseId . '::validateInput_' . $this->id;
				break;
			}
		}

		return new $class( 'content_field_' . $this->id, $value, $this->required, $options, $customValidationCode );
	}
}