<?php
/**
 * @brief		IN_DEV Skin Set
 * @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		16 Apr 2013
 */

namespace IPS\Theme\Dev;

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

use DirectoryIterator;
use InvalidArgumentException;
use IPS\Application;
use IPS\Db;
use IPS\Dispatcher;
use IPS\File;
use IPS\Http\Url;
use IPS\IPS;
use IPS\Log;
use IPS\Login;
use IPS\Request;
use IPS\Settings;
use IPS\Theme as SystemTheme;
use IPS\Theme\Template;
use RuntimeException;
use function count;
use function defined;
use function file_put_contents;
use function in_array;
use function intval;
use function is_array;
use function is_dir;
use function is_string;
use function preg_match;
use function str_replace;
use function str_starts_with;
use function stristr;
use function strstr;
use const IPS\DEFAULT_THEME_ID;
use const IPS\DEV_DEBUG_CSS;
use const IPS\IPS_FOLDER_PERMISSION;
use const IPS\ROOT_PATH;

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

/**
 * IN_DEV Skin set
 */
class Theme extends SystemTheme
{
	/**
	 * @brief	Template Classes
	 */
	protected array $templates = array();
	
	/**
	 * @brief	Stored plugins
	 */
	protected static array $plugins = array();

	/**
	 * Get all available hook points in the templates
	 *
	 * @param array|string|null $app
	 * @return array
	 */
	public function getHookPoints( array|string $app=null ): array
	{
		$hookPoints = [];
		foreach( Application::applications() as $appDir => $application )
		{
			if ( $app === NULL or ( in_array( $appDir, $app ) ) )
			{
				foreach( ['front', 'global' ] as $location )
				{
					if ( is_dir( static::_getHtmlPath( $appDir, $location ) ) )
					{
						foreach( new DirectoryIterator( static::_getHtmlPath( $appDir, $location ) ) as $file )
						{
							if ( $file->isDir() and mb_substr( $file->getFilename(), 0, 1 ) !== '.' )
							{
								foreach ( new DirectoryIterator( static::_getHtmlPath( $appDir, $location, $file->getFilename() ) ) as $template )
								{
									if ( !$template->isDir() and mb_substr( $template->getFilename(), -6 ) === '.phtml' )
									{
										$contents = file_get_contents( $template->getPathname() );
										if ( stristr( $contents, 'data-ips-hook' ) )
										{
											$path = $application->directory . '/' . $location . '/' . $file . '/' . str_replace( ".phtml", "", $template->getFilename() );
											$hookPoints[$path] = static::extractHookNames( $contents );
										}
									}
								}
							}
						}
					}
				}
			}
		}

		return $hookPoints;
	}

	/**
	 * Get raw templates. Raw means HTML logic and variables are still in {{format}}
	 *
	 * @param array|string $app				Template app (e.g. core, forum)
	 * @param array|string $location			Template location (e.g. admin,global,front)
	 * @param array|string $group				Template group (e.g. login, share)
	 * @param int|null $returnType			Determines the content returned
	 * @return array
	 */
	public function getAllTemplates( array|string $app=array(), array|string $location=array(), array|string $group=array(), int $returnType=null ): array
	{
		$returnType = ( $returnType === null )  ? self::RETURN_ALL   : $returnType;
		$app        = ( is_string( $app )      AND $app != ''      ) ? array( $app )      : $app;
		$location   = ( is_string( $location ) AND $location != '' ) ? array( $location ) : $location;
		$group      = ( is_string( $group )    AND $group != ''    ) ? array( $group )    : $group;
		$where      = array();
		$templates  = array();
		
		if ( ! ( $returnType & static::RETURN_NATIVE ) )
		{
			return parent::getAllTemplates( $app, $location, $group, $returnType );
		}
		
		$fixedLocations = array( 'admin', 'front', 'global' );
		$results	    = array();
		
		foreach( Application::applications() as $appDir => $application )
		{
			if ( $app === NULL or ( in_array( $appDir, $app ) ) )
			{
				foreach( $fixedLocations as $_location )
				{
					if ( $location === NULL or ( in_array( $_location, $location ) ) ) # location?
					{
						foreach( new DirectoryIterator( static::_getHtmlPath( $appDir, $_location ) ) as $file )
						{
							if ( $file->isDir() AND mb_substr( $file->getFilename(), 0, 1 ) !== '.' )
							{
								if ( $group === NULL or ( in_array( $file->getFilename(), $group ) ) )
								{
									foreach( new DirectoryIterator( static::_getHtmlPath( $appDir, $_location, $file->getFilename() ) ) as $template )
									{
										if ( ! $template->isDir() AND mb_substr( $template->getFilename(), -6 ) === '.phtml' )
										{
											$results[] = str_replace( ".phtml", "", $template->getFilename() );
										}
									}
								}
							}
						}
					}
				}
			}
		}
		
		return array_unique( $results );
	}

	/**
	 * Get a template
	 *
	 * @param string $group				Template Group
	 * @param string|null $app				Application key (NULL for current application)
	 * @param string|null $location		    Template Location (NULL for current template location)
	 * @return    Template
	 */
	public function getTemplate( string $group, string $app=NULL, string $location=NULL ): Template
	{
		/* Do we have an application? */
		if( $app === NULL )
		{
			if ( !Dispatcher::hasInstance() )
			{
				throw new RuntimeException('NO_APP');
			}
			$app = Dispatcher::i()->application->directory;
		}
		
		/* How about a template location? */
		if( $location === NULL )
		{
			if ( !Dispatcher::hasInstance() )
			{
				throw new RuntimeException('NO_LOCATION');
			}
			$location = Dispatcher::i()->controllerLocation;
		}
		
		/* Get template */
		if ( !isset( $this->templates[ $app ][ $location ][ $group ] ) )
		{
			$class = 'Template';
			$class = static::_getTemplateNamespace() . $class;
			
			$this->templates[ $app ][ $location ][ $group ] = new $class( $app, $location, $group );
		}
		return $this->templates[ $app ][ $location ][ $group ];
	}

	/**
	 * Get CSS
	 *
	 * @param string $file		Filename
	 * @param string|null $app		Application
	 * @param string|null $location	Location (e.g. 'admin', 'front')
	 * @return	array		URLs to CSS files
	 */
	public function css( string $file, string $app=NULL, string $location=NULL ): array
	{
		$app      = $app      ?: Dispatcher::i()->application->directory;
		$location = $location ?: Dispatcher::i()->controllerLocation;

		if ( $location === 'interface' )
		{
			/* Legacy code support. The following directories were moved to /static */
			if ( ( $app === "core" or $app === null) and preg_match( "/^(?:codemirror|fontawesome)/", $file ) )
			{
				$file = "static/" . $file;
			}

			$path = ROOT_PATH . "/applications/{$app}/interface/{$file}";
		}
		else
		{
			$path = static::_getCssPath($app, $location, $file);
		}

		$isStaticInterface = (bool) ( $location === "interface" and ( $app === "core" or $app === null) and str_starts_with( $file, "static/" ) );

		if ( isset( self::$buildGrouping['css'][ $app ][ $location ] ) AND is_array( self::$buildGrouping['css'][ $app ][ $location ] ) )
		{
			foreach( self::$buildGrouping['css'][ $app ][ $location ] as $buildPath )
			{
				if ( mb_substr( $file, 0, -4 ) == $buildPath )
				{
					$path = static::_getCssPath($app, $location, $buildPath);
					$file = $buildPath;
				}
			}
		}

		$return = array();

		if ( is_dir( $path ) )
		{
			$bits     = explode( '/', $path );
			$group = array_pop( $bits );
			$fileName = $group . '.css';

			/* Load css/location/folderName/folderName.css first */
			if ( is_file( $path . '/' . $fileName ) )
			{
				$return[] = str_replace( array( 'http://', 'https://' ), '//', Settings::i()->base_url ) . ( $isStaticInterface ? "" : "applications/core/interface/css/css.php?css=" ) . ( str_replace( ROOT_PATH . '/', '', static::_getCssPath( $app, $location, $file ) ) ) . $fileName;
			}

			$csses = array();
			foreach ( new DirectoryIterator( $path ) as $f )
			{
				if ( !$f->isDot() and mb_substr( $f, -4 ) === '.css' and $f->getFileName() != $fileName )
				{
					$csses[] = ( str_replace( ROOT_PATH . '/', '', static::_getCssPath( $app, $location, $group . '/' . $f->getFilename() ) ) );
				}
			}

			sort( $csses );

			if ( count( $csses ) )
			{
				if( DEV_DEBUG_CSS or $isStaticInterface )
				{
					foreach( $csses as $cssFile )
					{
						$return[] = str_replace( array( 'http://', 'https://' ), '//', Settings::i()->base_url ) . ( $isStaticInterface ? "" : "applications/core/interface/css/css.php?css=" ) . $cssFile;
					}
				}
				else
				{
					$return[] = str_replace( array( 'http://', 'https://' ), '//', Settings::i()->base_url ) . "applications/core/interface/css/css.php?css=" . implode( ',', $csses );
				}
			}
		}
		elseif ( file_exists( $path ) )
		{
			$return[] = str_replace( ROOT_PATH . '/', '', str_replace( array( 'http://', 'https://' ), '//', Settings::i()->base_url ) . ( $isStaticInterface ? "" : "applications/core/interface/css/css.php?css=" ) . $path );
		}

		return $return;
	}

	
	/**
	 * Get JS
	 *
	 * @param string $file		Filename
	 * @param string|null $app		Application
	 * @param string|null $location	Location (e.g. 'admin', 'front')
	 * @return	array		URL to JS files
	 */
	public function js( string $file, string $app=NULL, string $location=NULL ): array
	{
		$app      = $app      ?: Request::i()->app;
		$location = $location ?: Dispatcher::i()->controllerLocation;
		
		$return = array();
			if ( $location === 'interface' )
			{
				$path = ROOT_PATH . "/applications/{$app}/interface/{$file}";
			}
			else
			{
				$path = ROOT_PATH . "/applications/{$app}/dev/js/{$location}/{$file}";
			}
					
			if ( is_dir( $path ) )
			{
				$bits     = explode( '/', $path );
				$fileName = 'ips.' . array_pop( $bits ) . '.js';
				
				if ( is_file( $path .'/' . $fileName ) )
				{
					$return[] = Settings::i()->base_url . "/applications/{$app}/dev/js/{$location}/{$file}/{$fileName}";
				}

				foreach ( new DirectoryIterator( $path ) as $f )
				{
					if ( !$f->isDot() and mb_substr( $f, -3 ) === '.js' and $f->getFileName() != $fileName )
					{
						$return[] = Settings::i()->base_url . "/applications/{$app}/dev/js/{$location}/{$file}/{$f}";
					}
				}
			}
			else
			{			
				$return[] = str_replace( ROOT_PATH, Settings::i()->base_url, $path );
			}
		
		return $return;
	}
	
	/**
	 * Get Theme Resource (resource, font, theme-specific JS, etc)
	 *
	 * @param string $path		Path to resource
	 * @param string|null $app		Application key
	 * @param string|null $location	Location
	 * @param	bool		$noProtocol	Return URL without a protocol (protocol-relative)
	 * @return	Url|NULL		URL to resource
	 */
	public function resource( string $path, string $app=NULL, string $location=NULL, $noProtocol=FALSE ): ?Url
	{
		$baseUrl = Settings::i()->base_url;
		$app = $app ?: Dispatcher::i()->application->directory;
		$location = $location ?: Dispatcher::i()->controllerLocation;
		
		if ( $location === 'interface' )
		{
			return Url::internal( "applications/{$app}/interface/{$path}", 'interface', NULL, array(), Url::PROTOCOL_RELATIVE );
		}

		/* This is a custom resource uploaded via the ACP */
		if( in_array( $app, IPS::$ipsApps ) and mb_substr( $path, 0, 7 ) === 'custom/' )
		{
			return parent::resource( $path, $app, $location, $noProtocol );
		}

		$url = Url::createFromString( $baseUrl . str_replace( '\\', '/', str_replace( ROOT_PATH . '/', '', static::_getResourcePath( $app, $location, $path ) ) ), false );
		if( $noProtocol )
		{
			$url = $url->setScheme(NULL);
		}
		return $url;
	}
	
	/**
	 * (re)import HTML templates into the template DB
	 * 
	 * @param string $app	        Application Key
	 * @param int $id	        Theme Set Id (0 if IN_DEV and not in advanced theming mode)
	 * 
	 * @return	void
	 */
	public static function importDevHtml( string $app, int $id ): void
	{
		/* Clear out existing template bits */
		Db::i()->delete( 'core_theme_templates', array( 'template_app=? AND ( template_set_id=? OR ( template_set_id=0 ) )', $app, $id ) );

		$themeLocations = Application::load( $app )->themeLocations;

		/* Get existing template bits to see if we need to import */
		if ( $id > 0 )
		{
			$currentTemplates = Theme::load( $id )->getAllTemplates( $app );
		}
		
		$path = static::_getHtmlPath( $app );

		if ( is_dir( $path ) )
		{
			foreach( new DirectoryIterator( $path ) as $location )
			{
				if ( $location->isDot() || mb_substr( $location->getFilename(), 0, 1 ) === '.' )
				{
					continue;
				}
				
				if ( $location->isDir() )
				{
					if ( ! in_array( $location->getFilename(), $themeLocations ) )
					{
						continue;
					}

					foreach( new DirectoryIterator( $path . $location->getFilename() ) as $group )
					{
						if ( $group->isDot() || mb_substr( $group->getFilename(), 0, 1 ) === '.' )
						{
							continue;
						}
						
						if ( $group->isDir() )
						{
							foreach( new DirectoryIterator( $path . $location->getFilename() . '/' . $group->getFilename() ) as $file )
							{
								if ( $file->isDot() || mb_substr( $file->getFilename(), -6 ) !== '.phtml')
								{
									continue;
								}
				
								/* Get the content */
								$html   = file_get_contents( $path . $location->getFilename() . '/' . $group->getFilename() . '/' . $file->getFilename() );
								$params = array();
								
								/* 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;

								/* Enforce \n line endings */
								if( mb_strtolower( mb_substr( PHP_OS, 0, 3 ) ) === 'win' )
								{
									$html = str_replace( "\r\n", "\n", $html );
								}
								
								$version = Application::load( $app )->long_version;
								$save = array(
									'set_id'	  => $id,
									'added_to'    => 0,
									'user_added'  => 0,
									'user_edited' => 0
								);
								
								/* If we're syncing designer mode, check for actual changes */
								$name = preg_replace( '/[^a-zA-Z0-9_]/', '', str_replace( '.phtml', '', $file->getFilename() ) );
								
								/* Prevent '.phtml' files creating empty template rows in the database */
								if ( empty( $name ) )
								{
									continue;
								}
								
								if ( $id > 0 )
								{
									if ( isset( $currentTemplates[ $app ][ $location->getFilename() ][ $group->getFilename() ][ $name ] ) )
									{
										if( Login::compareHashes( md5( trim( $html ) ), md5( trim( $currentTemplates[ $app ][ $location->getFilename() ][ $group->getFilename() ][ $name ]['template_content'] ) ) ) )
										{
											/* No change  */
											continue;
										}
										else
										{
											/* It has changed */
											$save['user_edited'] = $version;
										}
									}
									else
									{
										/* New template bit */
										$save['added_to']   = $id;
										$save['set_id']	    = 0;
										$save['user_added'] = 1;
									}
								}
								
								Db::i()->replace( 'core_theme_templates', array( 'template_set_id'      => $save['set_id'],
																					 'template_app'		    => $app,
																					 'template_location'    => $location->getFilename(),
																					 'template_group'       => $group->getFilename(),
																					 'template_name'	    => $name,
																					 'template_data'	    => ( isset( $params[1] ) ) ? $params[1] : '',
																					 'template_content'     => $html,
																					 'template_updated'     => time(),
																					 'template_version'	    => NULL ) );
							}
						}
					}
				}
			}
		}
	}
	
	/**
	 * (re)import CSS into the CSS DB
	 *
	 * @param string $app	Application Key
	 * @param int $id	Theme Set Id (0 if IN_DEV and not in advanced theming mode)
	 * @return	void
	 */
	public static function importDevCss( string $app, int $id ): void
	{
		/* Clear out existing template bits */
		Db::i()->delete( 'core_theme_css', array( 'css_app=? AND ( css_set_id=? ) ', $app, $id ) );
		
		$master = array();
		
		/* Get existing template bits to see if we need to import */
		$currentCss = [];
		
		if ( $id > 0 )
		{
			$currentCss = Theme::load( $id )->getAllCss( $app );
		}
		
		$path = static::_getCssPath($app);
	
		if ( is_dir( $path ) )
		{
			foreach( new DirectoryIterator( $path ) as $location )
			{
				if ( $location->isDot() OR mb_substr( $location->getFilename(), 0, 1 ) === '.' )
				{
					continue;
				}
	
				if ( $location->isDir() )
				{
					static::_importDevCss( $app, $id, $currentCss, $location->getFilename() );
				}
			}
		}
	}
	
	/**
	 * (re)import CSS into the CSS DB (Iterable)
	 *
	 * @param string $app		Application Key
	 * @param int $id			Theme set ID
	 * @param array $currentCss	Master CSS bits
	 * @param string $location	Location Folder Name
	 * @param string $path		Path
	 * @return	void
	 */
	protected static function _importDevCss( string $app, int $id, array $currentCss, string $location, string $path='/' ): void
	{
		$root = static::_getCssPath( $app, $location );
		
		foreach( new DirectoryIterator( $root . $path ) as $file )
		{
			if ( $file->isDot() OR mb_substr( $file->getFilename(), 0, 1 ) === '.' OR $file == 'index.html' )
			{
				continue;
			}
	
			if ( $file->isDir() )
			{
				static::_importDevCss( $app, $id, $currentCss, $location, $path . $file->getFilename() . '/' );
			}
			else
			{
				if ( mb_substr( $file->getFilename(), -4 ) !== '.css' )
				{
					continue;
				}

				/* Get the content */
				$css = file_get_contents( $root . $path . $file->getFilename() );
					
				/* Parse the header tag */
				preg_match( '#^/\*<ips:css([^>]+?)>\*/\n#', $css, $params );

				/* Strip it */
				if ( count( $params ) AND ! empty( $params[0] ) )
				{
					$css = str_replace( $params[0], '', $css );
				}

				$cssModule = '';
				$cssApp    = '';
				$cssPos    = 0;
				$cssHidden = 0;
				
				/* Tidy params */
				if ( count( $params ) AND ! empty( $params[1] ) )
				{
					preg_match_all( '#([\d\w]+?)=\"([^"]+?)"#i', $params[1], $items, PREG_SET_ORDER );
						
					foreach( $items as $id => $attr )
					{
						switch( trim( $attr[1] ) )
						{
							case 'module':
								$cssModule = trim( $attr[2] );
								break;
							case 'app':
								$cssApp = trim( $attr[2] );
								break;
							case 'position':
								$cssPos = intval( $attr[2] );
								break;
							case 'hidden':
								$cssHidden = intval( $attr[2] );
								break;
						}
					}
				}
			
				$trimmedPath = trim( $path, '/' );
				$finalPath   = ( ( ! empty( $trimmedPath ) ) ? $trimmedPath : '.' );
				$version     = Application::load( $app )->long_version;
				$save        = array(
					'set_id'	  => $id,
					'added_to'    => 0,
					'user_edited' => 0
				);

				/* Enforce \n line endings */
				if( mb_strtolower( mb_substr( PHP_OS, 0, 3 ) ) === 'win' )
				{
					$css = str_replace( "\r\n", "\n", $css );
				}
							
				/* If we're syncing designer mode, check for actual changes */
				if ( $id > 0 )
				{
					$css = str_replace( '/* No Content */', '', $css );
					if ( isset( $currentCss[ $app ][ $location ][ $finalPath ][ $file->getFilename() ] ) )
					{
						if( Login::compareHashes( md5( trim( $css ) ), md5( trim( $currentCss[ $app ][ $location ][ $finalPath ][ $file->getFilename() ]['css_content'] ) ) ) )
						{
							/* No change  */
							continue;
						}
						else
						{
							/* It has changed */
							$save['user_edited'] = $version;
							$save['added_to']   = $id;
							$save['set_id']	    = $id;
						}
					}
					else
					{
						/* New template bit */
						$save['added_to']   = $id;
						$save['set_id']	    = $id;
					}
				}
								
				Db::i()->insert( 'core_theme_css', array( 'css_set_id'    	 => $save['set_id'],
															   'css_app'		 => $app,
															   'css_location'  	 => $location,
															   'css_path'		 => $finalPath,
															   'css_name'	     => $file->getFilename(),
															   'css_attributes'  => '',
															   'css_content'	 => $css,
															   'css_modules'	 => $cssModule,
															   'css_position'	 => $cssPos,
															   'css_updated'   	 => time(),
															   'css_hidden'		 => $cssHidden ) );
			}
		}
	}
	
	/**
	 * Build Resourcess ready for non IN_DEV use
	 * 
	 * @param string $app	App (e.g. core, forum)
	 * @param int $id	Theme Set Id (0 if IN_DEV and not in advanced theming mode)
	 * @return	void
	 */
	public static function importDevResources( string $app, int $id ): void
	{
		foreach( new DirectoryIterator( ROOT_PATH . '/applications/' ) as $dir )
		{
			if ( $dir->isDot() || mb_substr( $dir->getFilename(), 0, 1 ) === '.' || $dir == 'index.html')
			{
				continue;
			}

			if ( $app === null OR $app == $dir->getFilename() )
			{
				/* When we are building, removeResources() has already taken care of this */
				if( $id !== 0 )
				{
					Theme::deleteCompiledResources( $dir->getFilename(), null, null, null, $id );
				}
						
				Db::i()->delete( 'core_theme_resources', array( 'resource_app=? AND resource_set_id=?', $dir->getFilename(), $id ) );
				
				$path = static::_getResourcePath( $dir->getFilename() );
					
				if ( is_dir( $path ) )
				{
					foreach( new DirectoryIterator( $path ) as $location )
					{
						if ( $location->isDot() || mb_substr( $location->getFilename(), 0, 1 ) === '.' )
						{
							continue;
						}
							
						if ( $location->isDir() )
						{
							static::_importDevResources( $dir->getFilename(), $id, $location->getFilename() );
						}
					}
				}
			}
		}
	}
	
	/**
	 * Build Resources ready for non IN_DEV use (Iterable)
	 * Theme resources should be raw binary data everywhere (filesystem and DB) except in the theme XML download where they are base64 encoded.
	 *
	 * @param string $app		Application Key
	 * @param int $id			Theme Set Id
	 * @param string $location	Location Folder Name
	 * @param string $path		Path
	 * @return	void
	 */
	public static function _importDevResources( string $app, int $id, string $location, string $path='/' ): void
	{
		$root   = static::_getResourcePath($app, $location);
		$master = array();
		$plugins = array();

		if ( $id )
		{
			foreach( Db::i()->select( '*', 'core_theme_resources', array( 'resource_set_id=0 and resource_app=? and resource_location=? and resource_path=?', $app, $location, $path ) ) as $resource )
			{
				$master[ $resource['resource_name'] ] = md5( $resource['resource_data'] );
			}
		}

		foreach( new DirectoryIterator( $root . $path ) as $file )
		{
			if ( $file->isDot() || mb_substr( $file->getFilename(), 0, 1 ) === '.' || $file == 'index.html' )
			{
				continue;
			}
	
			if ( $file->isDir() )
			{
				static::_importDevResources( $app, $id, $location, $path . $file->getFilename() . '/' );
			}
			else
			{
				/* files larger than 1.5mb don't base64_encode() as they may not get stored in the resources_data column */
				if ( filesize( $root . $path . $file->getFilename() ) > ( 1.5 * 1024 * 1024 ) )
				{
					Log::log( $root . $path . $file->getFilename() . " too large to import", 'designers_mode_import' );
					continue;
				}
				
				$content = file_get_contents( $root . $path . $file->getFilename() );
				
				if ( ! base64_encode( $content ) )
				{
					Log::log( $root . $path . $file->getFilename() . " could not be saved correctly", 'designers_mode_import' );
					continue;
				}
				
				$custom   = 0;
				$name     = self::makeBuiltTemplateLookupHash($app, $location, $path) . '_' . $file->getFilename();
				$fileName = (string) File::create( 'core_Theme', $name, $content, 'set_resources_' . ( $id == 0 ? DEFAULT_THEME_ID : $id ), TRUE );
				
				if ( $id !== 0 AND ( !isset( $master[ $file->getFilename() ] ) or !Login::compareHashes( md5( $content ), $master[ $file->getFilename() ] ) ) )
				{
					$custom = 1;
				}

				Db::i()->insert( 'core_theme_resources', array(
						'resource_set_id'      => ( $id == 0 ? DEFAULT_THEME_ID : $id ),
						'resource_app'         => $app,
						'resource_location'    => $location,
						'resource_path'        => $path,
						'resource_name'        => $file->getFilename(),
						'resource_added'	   => time(),
						'resource_data'        => $content,
						'resource_filename'    => $fileName,
						'resource_user_edited' => $custom
				) );

				/* Store in master table */
				if ( $id == 0 )
				{
					Db::i()->insert( 'core_theme_resources', array(
	                     'resource_set_id'   => 0,
	                     'resource_app'      => $app,
	                     'resource_location' => $location,
	                     'resource_path'     => $path,
	                     'resource_name'     => $file->getFilename(),
	                     'resource_added'	  => time(),
	                     'resource_data'     => $content,
	                     'resource_filename' => ''
	                 ) );
				}
			}
		}
	}
	
	
	/**
	 * Writes the /application/{app}/dev/{container}/ directory
	 *
	 * @param string $app		 Application Directory
	 * @param string $container	 Container directory (e.g. html/css/resources)
	 * @return string	Path created
	 * @throws	RuntimeException
	 */
	protected static function _writeThemeContainerDirectory( string $app, string $container ): string
	{
		$dirToWrite = ROOT_PATH . "/applications/" . $app . '/dev/' . $container;
	
		if ( ! is_dir( $dirToWrite ) )
		{
			if ( ! @mkdir( $dirToWrite ) )
			{
				throw new RuntimeException('core_theme_dev_cannot_make_dir,' . $dirToWrite);
			}
			else
			{
				@chmod( $dirToWrite, IPS_FOLDER_PERMISSION );
			}
		}
	
		/* Check its writeable */
		if ( ! is_writeable( $dirToWrite ) )
		{
			throw new RuntimeException('core_theme_dev_not_writeable,' . $dirToWrite);
		}
		
		/* Make sure root directory is CHMOD correctly */
		@chmod( ROOT_PATH . "/applications/" . $app . '/dev', 0777 );
	
		return $dirToWrite;
	}
	
	/**
	 * Writes the /application/{app}/dev/{container}/{path} directory
	 *
	 * @param string $app		Application Directory
	 * @param string $container	Location of path to create (e.g. admin, front)
	 * @param string $path		Path to create
	 * @return	string	Path created
	 * @throws	RuntimeException
	 */
	protected static function _writeThemePathDirectory( string $app, string $container, string $path ): string
	{
		$dirToWrite = ROOT_PATH . "/applications/" . $app . '/dev/' . $container . '/' . $path;
	
		if ( ! is_dir( $dirToWrite ) )
		{
			if ( ! @mkdir( $dirToWrite ) )
			{
				throw new RuntimeException('core_theme_dev_cannot_make_dir,' . $dirToWrite);
			}
			else
			{
				@chmod( $dirToWrite, IPS_FOLDER_PERMISSION );
			}
		}
	
		/* Check its writeable */
		if ( ! is_writeable( $dirToWrite ) )
		{
			throw new RuntimeException('core_theme_dev_not_writeable,' . $dirToWrite);
		}
	
		return $dirToWrite;
	}

	/**
	 * Write skin resources
	 * Theme resources should be raw binary data everywhere (filesystem and DB) except in the theme XML download where they are base64 encoded.
	 *
	 * @param string $app		 Application Directory
	 * @return	void
	 * @throws	RuntimeException
	 */
	public static function exportResources( string $app ): void
	{
		try
		{
			self::_writeThemeContainerDirectory( $app, 'img' );
		}
		catch( RuntimeException $e )
		{
			throw new RuntimeException( $e->getMessage() );
		}
		
		foreach( Db::i()->select( '*', 'core_theme_resources', array( 'resource_app=? AND resource_set_id=?', $app, DEFAULT_THEME_ID ) )->setKeyField('resource_id') as $resourceId => $resource )
		{
			try
			{
				$pathToWrite = self::_writeThemePathDirectory( $app, 'img', $resource['resource_location'] );
			}
			catch( RuntimeException $e )
			{
				throw new RuntimeException( $e->getMessage() );
			}
				
			if ( $resource['resource_path'] != '/' )
			{
				$_path = '';
					
				foreach( explode( '/', trim( $resource['resource_path'], '/' ) ) as $dir )
				{
					$_path .= '/' . trim( $dir, '/' );
					
					try
					{
						$pathToWrite = self::_writeThemePathDirectory( $app, 'img', $resource['resource_location'] . $_path );
					}
					catch( RuntimeException $e )
					{
						throw new RuntimeException( $e->getMessage() );
					}
				}
			}

			try
			{
				if ( ! @file_put_contents( $pathToWrite . '/' . $resource['resource_name'], $resource['resource_data'] ) )
				{
					throw new RuntimeException('core_theme_dev_cannot_write_resource,' . $pathToWrite . '/' . $resource['resource_name']);
				}
				else
				{
					@chmod( $pathToWrite . '/' . $resource['resource_name'], 0777 );
				}
			}
			catch( InvalidArgumentException $e )
			{
				
			}
		}
	}
	
	/**
	 * Write CSS into the appropriate theme directory as plain text CSS ({resource="foo.png"} intact)
	 *
	 * @param string $app		 Application Directory
	 * @return	void
	 * @throws	RuntimeException
	 */
	public static function exportCss( string $app ): void
	{
		try
		{
			self::_writeThemeContainerDirectory( $app, 'css' );
		}
		catch( RuntimeException $e )
		{
			throw new RuntimeException( $e->getMessage() );
		}
		
		$css = static::master()->getAllCss();
	
		foreach( $css as $appDir => $appData )
		{
			foreach( $css[ $appDir ] as $location => $locationData )
			{
				try
				{
					$pathToWrite = self::_writeThemePathDirectory( $app, 'css', $location );
				}
				catch( RuntimeException $e )
				{
					throw new RuntimeException( $e->getMessage() );
				}
					
				foreach( $css[ $appDir ][ $location ] as $path => $pathData )
				{
					if ( $path != '.' )
					{
						$_path = $path;
	
						if ( strstr( $path, '/' ) )
						{
							$_path = '';
								
							foreach( explode( '/', $path ) as $dir )
							{
								$_path .= '/' . trim( $dir, '/' );
								
								try
								{
									$pathToWrite = self::_writeThemePathDirectory( $app, 'css', $location . $_path );
								}
								catch( RuntimeException $e )
								{
									throw new RuntimeException( $e->getMessage() );
								}
							}
						}
						else
						{
							try
							{
								$pathToWrite = self::_writeThemePathDirectory( $app, 'css', $location . '/' . $path );
							}
							catch( RuntimeException $e )
							{
								throw new RuntimeException( $e->getMessage() );
							}
						}
					}
						
					foreach( $css[ $appDir ][ $location ][ $path ] as $name => $data )
					{
						$params = array();
						$write  = '';
	
						if ( $data['css_hidden'] )
						{
							$params[] = 'hidden="1"';
						}
	
						if ( count( $params ) )
						{
							$write  .= '/*<ips:css ' . implode( ' ', $params ) . ' />*/' . "\n";
						}
	
						$write .= ( empty( $data['css_content'] ) ) ? '/* No Content */' : $data['css_content'];
							
						if ( ! @file_put_contents( $pathToWrite . '/' . $data['css_name'], $write ) )
						{
							throw new RuntimeException('core_theme_dev_cannot_write_css,' . $pathToWrite . '/' . $data['css_name']);
						}
						else
						{
							@chmod( $pathToWrite . '/' . $data['css_name'], 0777 );
						}
					}
				}
			}
		}
	}
	
	/**
	 * Write templates into the appropriate theme directory as plain text templates ({{logic}} intact)
	 *
	 * @param string $app		 Application Directory
	 * @return	void
	 * @throws	RuntimeException
	 */
	public static function exportTemplates( string $app ): void
	{
		try
		{
			self::_writeThemeContainerDirectory( $app, 'html' );
		}
		catch( RuntimeException $e )
		{
			throw new RuntimeException( $e->getMessage() );
		}
		
		$templates = static::master()->getAllTemplates();
	
		foreach( $templates as $appDir => $appData )
		{
			foreach( $templates[ $app ] as $location => $locationData )
			{
				try
				{
					self::_writeThemePathDirectory( $app, 'html', $location );
				}
				catch( RuntimeException $e )
				{
					throw new RuntimeException( $e->getMessage() );
				}
					
				foreach( $templates[ $app ][ $location ] as $group => $groupData )
				{
					try
					{
						$pathToWrite = self::_writeThemePathDirectory( $app, 'html', $location . '/' . $group );
					}
					catch( RuntimeException $e )
					{
						throw new RuntimeException( $e->getMessage() );
					}
					
					foreach( $templates[ $app ][ $location ][ $group ] as $name => $data )
					{
						$write  = '<ips:template parameters="' . $data['template_data'] . '" />' . "\n";
						$write .= $data['template_content'];
	
						if ( ! @file_put_contents( $pathToWrite . '/' . $data['template_name'] . '.phtml', $write ) )
						{
							throw new RuntimeException('core_theme_dev_cannot_write_template,' . $pathToWrite . '/' . $data['css_name']);
						}
						else
						{
							@chmod( $pathToWrite . '/' . $data['template_name'] . '.phtml', 0777 );
						}
					}
				}
			}
		}
	}
	
	/**
	 * Remove an entire theme directory
	 *
	 * @param string $dir		Path
	 * @return  void
	 */
	public static function removeThemeDirectory( string $dir ): void
	{
		if ( is_dir( $dir ) )
		{
			foreach ( new DirectoryIterator( $dir ) as $f )
			{
				if ( !$f->isDot() )
				{
					if ( $f->isDir() )
					{
						static::removeThemeDirectory( $f->getPathname() );
					}
					else
					{
						@unlink( $f->getPathname() );
					}
				}
			}

			$handle = opendir( $dir );
			closedir( $handle );
			rmdir( $dir );
		}	
	}
	
	/**
	 * Returns the namespace for the template class
	 *
	 * @return string
	 */
	protected static function _getTemplateNamespace(): string
	{
		return 'IPS\\Theme\\Dev\\';
	}
	
	/**
	 * Returns the path for the IN_DEV .phtml files
	 *
	 * @param string $app			Application Key
	 * @param string|null $location		Location
	 * @param string|null $path			Path or Filename
	 * @return string
	 */
	protected static function _getHtmlPath( string $app, string $location=null, string $path=null ): string
	{
		return rtrim( ROOT_PATH . "/applications/{$app}/dev/html/{$location}/{$path}", '/' ) . '/';
	}
	
	/**
	 * Returns the path for the IN_DEV CSS file
	 *
	 * @param string $app			Application Key
	 * @param string|null $location		Location
	 * @param string|null $path			Path or Filename
	 * @return string
	 */
	protected static function _getCssPath( string $app, string $location=null, string $path=null ): string
	{
		/* This is the default, look for an actual CSS file */
		$path = rtrim( ROOT_PATH . "/applications/{$app}/dev/css/{$location}/{$path}", '/' ) . ( stristr( $path, '.css' ) ? '' : '/' );
		if( !file_exists( $path ) )
		{
			/* If the file doesn't exist, move one level up and return the directory */
			$bits = explode( "/", $path );
			array_pop( $bits );
			$path = implode( "/", $bits );
		}

		return $path;
	}
	
	/**
	 * Returns the path for the IN_DEV resource files
	 *
	 * @param string $app			Application Key
	 * @param string|null $location		Location
	 * @param string|null $path			Path or Filename
	 * @return string
	 */
	protected static function _getResourcePath( string $app, string $location=null, string $path=null ): string
	{
		return rtrim( ROOT_PATH . "/applications/{$app}/dev/resources/{$location}/{$path}", '/' ) . ( ( stristr( $path, '.' ) || stristr( $path, '{' ) ) ? '' : '/' );
	}
}