<?php
namespace SabaiApps\Directories\Platform;

use SabaiApps\Directories\Application;
use SabaiApps\Directories\Context;
use SabaiApps\Directories\Request;
use SabaiApps\Directories\MainRoutingController;
use SabaiApps\Directories\Assets;
use SabaiApps\Directories\Exception;
use SabaiApps\Framework\Application\Url;
use Monolog\Logger;
use Monolog\Handler\ErrorLogHandler;
use SabaiApps\Framework\DB\AbstractDB;
use SabaiApps\Framework\User\AbstractIdentity;

/**
 * AbstractPlatform class provides the base for platform-specific implementations.
 * Handles asset management, application instantiation, rendering, user/session management, and more.
 * Concrete platforms must implement all abstract methods for integration.
 */
abstract class AbstractPlatform {

	private static $_application;
	protected $_name, $_db, $_defaultJsLoaded, $_defaultCssLoaded, $_renderCount = 0,
		$_head = array(), $_js = array(), $_jsIndex = 0, $_css = array(),
		// For tracking assets
		$_trackedAssets = array(),
		$_hasFontAwesomePro;

	/**
	 * Constructor.
	 *
	 * @param string $name Platform name.
	 */
	protected function __construct( $name ) {
		$this->_name = $name;
	}

	/**
	 * Gets the platform name.
	 *
	 * @return string
	 */
	final public function getName() {
		return $this->_name;
	}

	/**
	 * Gets the CSS relative size unit.
	 *
	 * @return string
	 */
	public function getCssRelSize() {
		return 'rem';
	}

	/**
	 * Gets the database connection.
	 *
	 * @return AbstractDB
	 */
	final public function getDB() {
		if ( ! isset( $this->_db ) ) {
			$this->_db = $this->_getDB();
		}
		return $this->_db;
	}

	/**
	 * Gets the main URL for the site.
	 *
	 * @param string|null $lang
	 * @return string
	 */
	public function getMainUrl( $lang = null ) {
		return $this->getSiteUrl();
	}

	/**
	 * Gets the application instance, optionally loading or reloading components.
	 *
	 * @param bool $loadComponents
	 * @param bool $reload
	 * @return Application
	 */
	public function getApplication( $loadComponents = true, $reload = false ) {
		if ( ! isset( self::$_application ) ) {
			self::$_application = $this->_createApplication();
		}
		if ( $loadComponents ) {
			if ( $reload ) {
				self::$_application->reloadComponents();
			} else {
				self::$_application->loadComponents();
			}
		}

		return self::$_application;
	}

	/**
	 * Creates the application instance.
	 *
	 * @return Application
	 */
	protected function _createApplication() {
		// Use Bootstrap library that comes with Directories if not exists
		define( 'DRTS_BS_PREFIX', $this->hasBootstrapCss() ? '' : 'drts-bs-' );

		$app = new Application( $this );

		// Set logger
		$logger = new Logger( 'drts' );
		// Set an error_log logger so that errors can be written to WordPress debug.log
		if ( $this->isDebugEnabled() ) {
			$logger->pushHandler( new ErrorLogHandler( ErrorLogHandler::OPERATING_SYSTEM, Logger::ERROR ) );
		}
		$app->setLogger( $logger );

		// Always add trailing slash to URL
		$app->addUrlTrailingSlash( true );

		return $app;
	}

	/**
	 * Loads default JS and CSS assets for the platform.
	 *
	 * @param bool $loadJs
	 * @param bool $loadCss
	 * @return $this
	 */
	public function loadDefaultAssets( $loadJs = true, $loadCss = true ) {
		// Make sure not to track core files
		if ( $track = $this->_trackedAssets ) {
			$this->_trackedAssets = array();
		}

		$type = $this->isAdmin() ? 'admin' : 'main';

		if ( $loadJs
			&& ! $this->_defaultJsLoaded
		) {
			$this->_loadJqueryJs( $type );
			$this->_loadCoreJs( $type );
			$this->_defaultJsLoaded = true;
		}
		if ( $loadCss
			&& ! $this->_defaultCssLoaded
		) {
			$this->_loadCoreCss( $type );
			$this->_defaultCssLoaded = true;
		}

		$this->_trackedAssets = $track;

		return $this;
	}

	/**
	 * Checks if FontAwesome Pro is available in custom assets.
	 *
	 * @return int|false
	 */
	protected function _hasFontAwesomePro() {
		if ( ! isset( $this->_hasFontAwesomePro ) ) {
			$this->_hasFontAwesomePro = false;
			foreach ( $this->getCustomAssetsDir() as $index => $dir ) {
				if ( file_exists( $dir . '/fontawesome.min.css' ) ) {
					$this->_hasFontAwesomePro = $index;
					break;
				}
			}
		}
		return $this->_hasFontAwesomePro;
	}

	/**
	 * Loads main JS assets.
	 *
	 * @return $this
	 */
	public function loadMainCoreJs() {
		$this->_loadJqueryJs( 'main' );
		$this->_loadCoreJs( 'main' );
		return $this;
	}

	/**
	 * Loads main CSS assets.
	 *
	 * @return $this
	 */
	public function loadMainCoreCss() {
		$this->_loadCoreCss( 'main' );
		return $this;
	}

	/**
	 * Loads CSS assets.
	 *
	 * @param string $type The type of CSS to load ('main' or 'admin').
	 * @return $this
	 */
	protected function _loadCoreCss( $type ) {
		// Load FontAwesome CSS
		$this->_loadFontAwesome();

		// Load Bootstrap CSS
		if ( $type === 'admin' ) {
			$this->addCssFile( 'bootstrap-' . $type . '.min.css', 'drts-bootstrap' )
				->addCssFile( $type . '.min.css', 'drts', 'drts-bootstrap' );
		} else {
			$deps = array();
			if ( ! $bs_handle = $this->_getBootstrapHandle() ) {
				$_suffix = $type;
				if ( defined( 'DRTS_THEME' ) ) {
					$_suffix .= '-' . DRTS_THEME;
				}
				$this->addCssFile( 'bootstrap-' . $_suffix . '.min.css', 'drts-bootstrap' );
				$deps[] = 'drts-bootstrap';
			} else {
				$deps[] = isset( $bs_handle['css'] ) ? $bs_handle['css'] : 'bootstrap';
			}
			$_css_url = $type;
			if ( defined( 'DRTS_THEME' ) ) {
				$_css_url .= '-' . DRTS_THEME;
			}
			$this->addCssFile( $_css_url . '.min.css', 'drts', $deps );
		}
		if ( $this->isRtl() ) {
			$this->addCssFile( 'bootstrap-rtl.min.css', 'drts-bootstrap-rtl', 'drts-bootstrap' )
				->addCssFile( $type . '-rtl.min.css', 'drts-rtl', 'drts' );
		}
		// Load plugin CSS
		$packages = $this->getPackages();
		$cache_id = 'core_css_files_' . implode( '-', $packages );
		if ( ! $css_files = $this->getCache( $cache_id ) ) {
			$css_files       = array(
				'main'  => array(),
				'admin' => array(),
				'rtl'   => array(
					'main'  => array(),
					'admin' => array(),
				),
			);
			$core_assets_dir = $this->getAssetsDir();
			foreach ( $packages as $package ) {
				$assets_dir = $this->getAssetsDir( $package );
				if ( $core_assets_dir === $assets_dir ) {
					continue;
				}

				if ( $this->_getBootstrapHandle() ) {
					// Use CSS without custom BS prefix if any
					if ( file_exists( $assets_dir . '/css/main-no-bs-prefix.min.css' ) ) {
						$css_files['main'][ $package ] = 'main-no-bs-prefix';
					} elseif ( file_exists( $assets_dir . '/css/main.min.css' ) ) {
							$css_files['main'][ $package ] = 'main';
					}
					if ( file_exists( $assets_dir . '/css/main-rtl-no-bs-prefix.min.css' ) ) {
						$css_files['rtl']['main'][ $package ] = 'main-rtl-no-bs-prefix';
					} elseif ( file_exists( $assets_dir . '/css/main-rtl.min.css' ) ) {
							$css_files['rtl']['main'][ $package ] = 'main-rtl';
					}
				} else {
					if ( file_exists( $assets_dir . '/css/main.min.css' ) ) {
						$css_files['main'][ $package ] = 'main';
					}
					if ( file_exists( $assets_dir . '/css/main-rtl.min.css' ) ) {
						$css_files['rtl']['main'][ $package ] = 'main-rtl';
					}
				}
				if ( file_exists( $assets_dir . '/css/admin.min.css' ) ) {
					$css_files['admin'][ $package ] = 'admin';
				}
				if ( file_exists( $assets_dir . '/css/admin-rtl.min.css' ) ) {
					$css_files['rtl']['admin'][ $package ] = 'admin-rtl';
				}
			}
			$this->setCache( $css_files, $cache_id );
		}
		foreach ( array_keys( $css_files[ $type ] ) as $package ) {
			$this->addCssFile( $css_files[ $type ][ $package ] . '.min.css', $package, array( 'drts' ), $package );
		}
		if ( ! empty( $css_files['rtl'][ $type ] )
			&& $this->isRtl()
		) {
			foreach ( array_keys( $css_files['rtl'][ $type ] ) as $package ) {
				$this->addCssFile( $css_files['rtl'][ $type ][ $package ] . '.min.css', $package . '-rtl', array( $package ), $package );
			}
		}
		// Load custom CSS if any
		if ( $type === 'main' ) {
			$deps = array( 'drts' );
			foreach ( $this->getCustomAssetsDir() as $index => $custom_dir ) {
				if ( @file_exists( $custom_dir . '/style.css' ) ) {
					$version = null;
					if ( $filemtime = @filemtime( $custom_dir . '/style.css' ) ) {
						$version = $filemtime;
					}
					$this->addCssFile( $this->getCustomAssetsDirUrl( $index ) . '/style.css', $handle = 'drts-custom-' . $index, $deps, false, null, false, $version );
					$deps[] = $handle;
				}
			}
			$custom_style_file = $this->_getBootstrapHandle() ? 'style-no-bs-prefix.css' : 'style.css';
			if ( @file_exists( $this->getVarDir() . '/' . $custom_style_file ) ) {
				$version = null;
				if ( $filemtime = @filemtime( $this->getVarDir() . '/' . $custom_style_file ) ) {
					$version = $filemtime;
				}
				$this->addCssFile( $this->getVarDirUrl() . '/' . $custom_style_file, $handle = 'drts-custom-var', $deps, false, null, false, $version );
			}
		}
	}

	protected function _loadFontAwesome() {
		if ( false === $dir_index = $this->_hasFontAwesomePro() ) {
			$this->addCssFile( 'fontawesome.min.css', 'drts-fontawesome' )
				->addCssFile( 'system-fontawesome.min.css', 'drts-system-fontawesome', 'drts-fontawesome' );
		} else {
			$this->addCssFile( $this->getCustomAssetsDirUrl( $dir_index ) . '/fontawesome.min.css', 'drts-fontawesome', null, false )
				->addCssFile( 'system-fontawesome-pro.min.css', 'drts-system-fontawesome', 'drts-fontawesome' );
		}
		$this->addCssFile( 'fontawesome-brands.min.css', 'drts-fontawesome-brands' );
	}

	/**
	 * Loads JavaScript assets.
	 *
	 * @param string $type The type of JS to load ('main' or 'admin').
	 * @return $this
	 */
	protected function _loadCoreJs( $type ) {
		if ( $type === 'admin'
			|| ( ! $bs_handle = $this->_getBootstrapHandle() )
		) {
			$bootstrap_handle = 'drts-bootstrap';
			$this->addJsFile( 'popper.min.js', 'drts-popper', null, null, false, true )
				->addJsFile( 'bootstrap.min.js', 'drts-bootstrap', array( 'jquery', 'drts-popper' ), null, true );
		} else {
			$bootstrap_handle = isset( $bs_handle['js'] ) ? $bs_handle['js'] : 'bootstrap';
		}
		$this->addJsFile( 'core.min.js', 'drts', array( 'jquery', $bootstrap_handle ), null, true );
		$init_js = sprintf(
			'if (typeof DRTS === "undefined") var DRTS = {url: "%s", isRTL: %s, domain: "%s", path: "%s", cookieHash: "%s", bsPrefix: "%s", hasFontAwesomePro: %s, params: {token: "%s", contentType: "%s", ajax: "%s"}, bsUseOriginal: %s, scroll: {disable: %s, offset: %d}, timeZone: "%s"};',
			rtrim( $this->getSiteUrl(), '/' ),
			$this->isRtl() ? 'true' : 'false',
			$this->getCookieDomain(),
			$this->getCookiePath(),
			$this->getCookieHash(),
			DRTS_BS_PREFIX,
			$this->_hasFontAwesomePro() !== false ? 'true' : 'false',
			Request::PARAM_TOKEN,
			Request::PARAM_CONTENT_TYPE,
			Request::PARAM_AJAX,
			$bootstrap_handle !== 'drts-bootstrap' ? 'true' : 'false',
			$this->getOption( 'system_effect_scroll_disable' ) ? 'true' : 'false',
			( $offset = $this->getOption( 'system_effect_scroll_offset' ) ) ? $offset : 0,
			$this->getTimeZone()
		);
		$this->addJsInline( 'drts', $init_js, false, 'before' )
			->addJsFile( 'sweetalert2.all.min.js', 'sweetalert2', null, null, true, true )
			->addJsFile( 'autosize.min.js', 'autosize', 'jquery', null, true, true )
			->addJsFile( 'jquery.coo_kie.min.js', 'jquery-cookie', 'jquery', null, true, true );
	}

	/**
	 * Adds a <head> element to the internal head array for later output.
	 *
	 * @param string $head The HTML to add to the head.
	 * @param string $handle Unique handle for the head element.
	 * @param int    $index Sort index for ordering.
	 * @return $this
	 */
	public function addHead( $head, $handle, $index = 10 ) {
		$this->_head[ $index ][ $handle ] = $head;
		if ( $this->_trackedAssets ) {
			foreach ( array_keys( $this->_trackedAssets ) as $track_name ) {
				$this->_trackedAssets[ $track_name ]->addHead( $handle, $head, $index );
			}
		}
		return $this;
	}

	/**
	 * Returns the HTML for all collected <head> elements.
	 *
	 * @param bool $clear Whether to clear the head elements after output.
	 * @return string
	 */
	public function getHeadHtml( $clear = true ) {
		$html = array();
		if ( ! empty( $this->_head ) ) {
			ksort( $this->_head );
			foreach ( array_keys( $this->_head ) as $i ) {
				foreach ( array_keys( $this->_head[ $i ] ) as $j ) {
					$html[] = $this->_head[ $i ][ $j ];
				}
			}
		}
		if ( $clear ) {
			$this->_head = array();
		}
		return empty( $html ) ? '' : implode( PHP_EOL, $html );
	}

	/**
	 * Adds a JavaScript file to the page.
	 *
	 * @param string      $file
	 * @param string      $handle
	 * @param mixed       $dependency
	 * @param string|null $package
	 * @param bool        $inFooter
	 * @param bool        $vendor
	 * @param string|null $version
	 * @return $this
	 */
	public function addJsFile( $file, $handle, $dependency = null, $package = null, $inFooter = true, $vendor = false, $version = null ) {
		if ( empty( $file ) ) {
			$this->_unloadJsFile( $handle );
			if ( $this->_trackedAssets ) {
				foreach ( array_keys( $this->_trackedAssets ) as $track_name ) {
					$this->_trackedAssets[ $track_name ]->addJsFile( $handle, false );
				}
			}
		} else {
			$url = $package !== false ? $this->getAssetsUrl( $package, $vendor ) . '/js/' . $file : $file;
			$this->_loadJsFile( $url, $handle, $dependency, $inFooter, $version );
			if ( $this->_trackedAssets ) {
				foreach ( array_keys( $this->_trackedAssets ) as $track_name ) {
					$this->_trackedAssets[ $track_name ]->addJsFile( $handle, $file, $dependency, $package, $inFooter, $vendor );
				}
			}
		}
		return $this;
	}

	/**
	 * Adds a JavaScript module file to the page.
	 *
	 * @param string      $file
	 * @param string      $id
	 * @param mixed       $dependency
	 * @param string|null $package
	 * @param bool        $vendor
	 * @param string|null $version
	 * @return $this
	 */
	public function addJsModuleFile( $file, $id, $dependency = null, $package = null, $vendor = false, $version = null ) {
		if ( empty( $file ) ) {
			$this->_unloadJsModuleFile( $id );
			if ( $this->_trackedAssets ) {
				foreach ( array_keys( $this->_trackedAssets ) as $track_name ) {
					$this->_trackedAssets[ $track_name ]->addJsModuleFile( $id, false );
				}
			}
		} else {
			$url = $package !== false ? $this->getAssetsUrl( $package, $vendor ) . '/js/' . $file : $file;
			$this->_loadJsModuleFile( $url, $id, $dependency, $version );
			if ( $this->_trackedAssets ) {
				foreach ( array_keys( $this->_trackedAssets ) as $track_name ) {
					$this->_trackedAssets[ $track_name ]->addJsModuleFile( $id, $file, $dependency, $package, $vendor );
				}
			}
		}
		return $this;
	}

	/**
	 * Adds JavaScript code to be output on the page.
	 *
	 * @param string   $js
	 * @param bool     $onDomReady
	 * @param int|null $index
	 * @return $this
	 */
	public function addJs( $js, $onDomReady = true, $index = null ) {
		$i                                        = isset( $index ) ? $index : ++$this->_jsIndex;
		$this->_js[ $onDomReady ? 1 : 0 ][ $i ][] = $js;
		if ( $this->_trackedAssets ) {
			foreach ( array_keys( $this->_trackedAssets ) as $track_name ) {
				$this->_trackedAssets[ $track_name ]->addJs( $js, $onDomReady, $index );
			}
		}
		return $this;
	}

	/**
	 * Adds inline JavaScript code to the page.
	 *
	 * @param string $dependency
	 * @param string $js
	 * @param bool   $addDomReady
	 * @param string $position
	 * @return $this
	 */
	public function addJsInline( $dependency, $js, $addDomReady = false, $position = 'after' ) {
		if ( $addDomReady ) {
			if ( Request::isXhr() ) {
				$js = 'jQuery(function($) { ' . $js . '});';
			} else {
				$js = 'document.addEventListener("DOMContentLoaded", function(event) { var $ = jQuery; ' . $js . '});';
			}
		}
		$this->_loadJsInline( $dependency, $js, $position );
		if ( $this->_trackedAssets ) {
			foreach ( array_keys( $this->_trackedAssets ) as $track_name ) {
				$this->_trackedAssets[ $track_name ]->addJsInline( $dependency, $js );
			}
		}
		return $this;
	}

	/**
	 * Adds a CSS file to the page.
	 *
	 * @param string      $file
	 * @param string      $handle
	 * @param mixed       $dependency
	 * @param string|null $package
	 * @param string|null $media
	 * @param bool        $vendor
	 * @param string|null $version
	 * @return $this
	 */
	public function addCssFile( $file, $handle, $dependency = null, $package = null, $media = null, $vendor = false, $version = null ) {
		if ( empty( $file ) ) {
			$this->_unloadCssFile( $handle );
			if ( $this->_trackedAssets ) {
				foreach ( array_keys( $this->_trackedAssets ) as $track_name ) {
					$this->_trackedAssets[ $track_name ]->addCssFile( $handle, false );
				}
			}
		} else {
			$url = $package !== false ? $this->getAssetsUrl( $package, $vendor ) . '/css/' . $file : $file;
			$this->_loadCssFile( $url, $handle, $dependency, isset( $media ) ? $media : 'all', $version );
			if ( $this->_trackedAssets ) {
				foreach ( array_keys( $this->_trackedAssets ) as $track_name ) {
					$this->_trackedAssets[ $track_name ]->addCssFile( $handle, $file, $dependency, $package, $media, $vendor, $version );
				}
			}
		}
		return $this;
	}

	/**
	 * Adds CSS code to the page.
	 *
	 * @param string      $css
	 * @param string|null $targetHandle
	 * @return $this
	 */
	public function addCss( $css, $targetHandle = null ) {
		$this->_css[ isset( $targetHandle ) ? $targetHandle : 'drts' ][] = $css;
		if ( $this->_trackedAssets ) {
			foreach ( array_keys( $this->_trackedAssets ) as $track_name ) {
				$this->_trackedAssets[ $track_name ]->addCss( $css, $targetHandle );
			}
		}
		return $this;
	}

	/**
	 * Returns all collected CSS code.
	 *
	 * @param bool $clear Whether to clear the CSS after output.
	 * @return array
	 */
	public function getCss( $clear = true ) {
		$css = $this->_css;
		if ( $clear ) {
			$this->_css = array();
		}
		return $css;
	}

	/**
	 * Returns the HTML for all collected JavaScript code.
	 *
	 * @param bool $clear Whether to clear the JS after output.
	 * @param bool $wrap Whether to wrap the JS in <script> tags.
	 * @return string
	 */
	public function getJsHtml( $clear = true, $wrap = true ) {
		$html = array();
		if ( ! empty( $this->_js[0] ) ) {
			ksort( $this->_js[0] );
			foreach ( array_keys( $this->_js[0] ) as $k ) {
				foreach ( array_keys( $this->_js[0][ $k ] ) as $i ) {
					$html[] = $this->_js[0][ $k ][ $i ];
				}
			}
		}
		if ( ! empty( $this->_js[1] ) ) {
			ksort( $this->_js[1] );
			if ( Request::isXhr() ) {
				$html[] = 'jQuery(function($) {';
			} else {
				$html[] = 'document.addEventListener("DOMContentLoaded", function(event) { var $ = jQuery;';
			}
			foreach ( array_keys( $this->_js[1] ) as $k ) {
				foreach ( array_keys( $this->_js[1][ $k ] ) as $i ) {
					$html[] = $this->_js[1][ $k ][ $i ];
				}
			}
			$html[] = '});';
		}
		if ( ! empty( $html ) ) {
			$html = implode( PHP_EOL, $html );
			if ( $wrap ) {
				$html = '<script type="text/javascript">' . $html . '</script>';
			}
		} else {
			$html = '';
		}
		if ( $clear ) {
			$this->_js      = array();
			$this->_jsIndex = 0;
		}
		return $html;
	}

	/**
	 * Adds a flash message to be shown to the user.
	 *
	 * @param array $flash
	 * @return $this
	 */
	public function addFlash( array $flash ) {
		return $this->addJs( sprintf( 'DRTS.flash(%s);', json_encode( $flash ) ) );
	}

	/**
	 * Loads jQuery UI JavaScript components.
	 *
	 * @param array $components
	 * @return $this
	 */
	public function loadJqueryUiJs( array $components ) {
		$this->_loadJqueryUiJs( $components );
		if ( $this->_trackedAssets ) {
			foreach ( array_keys( $this->_trackedAssets ) as $track_name ) {
				$this->_trackedAssets[ $track_name ]->addJqueryUiJs( $components );
			}
		}
		return $this;
	}

	/**
	 * Loads the imagesLoaded JavaScript library.
	 *
	 * @return $this
	 */
	public function loadImagesLoadedJs() {
		$this->_loadImagesLoadedJs();
		if ( $this->_trackedAssets ) {
			foreach ( array_keys( $this->_trackedAssets ) as $track_name ) {
				$this->_trackedAssets[ $track_name ]->addImagesLoadedJs();
			}
		}
		return $this;
	}

	/**
	 * Generates a hash for rendering based on path, attributes, and options.
	 *
	 * @param mixed $path
	 * @param array $attributes
	 * @param array $options
	 * @return string
	 */
	protected function _getRenderHash( $path, array $attributes, array $options ) {
		$attr = serialize( $attributes );
		// Append current entity ID if settings contain _current_
		if ( isset( $GLOBALS['drts_entity'] )
			&& strpos( $attr, '_current_' )
		) {
			$attr .= $GLOBALS['drts_entity']->getId();
		}
		$path = is_array( $path ) ? serialize( $path ) : (string) $path;
		return md5( $path . $attr . serialize( $options ) );
	}

	/**
	 * Enables or disables asset tracking.
	 *
	 * @param bool   $bool
	 * @param string $name
	 * @return $this
	 */
	public function trackAssets( $bool = true, $name = 'core' ) {
		if ( $bool ) {
			$this->_trackedAssets[ $name ] = new Assets();
		} else {
			unset( $this->_trackedAssets[ $name ] );
		}
		return $this;
	}

	/**
	 * Gets tracked assets by name.
	 *
	 * @param string $name
	 * @return Assets
	 */
	public function getTrackedAssets( $name = 'core' ) {
		return isset( $this->_trackedAssets[ $name ] ) ? $this->_trackedAssets[ $name ]->getAssets() : array();
	}

	/**
	 * Loads the given assets.
	 *
	 * @param mixed $assets
	 */
	public function loadAssets( $assets ) {
		if ( $assets instanceof Assets ) {
			$assets = $assets->getAssets();
		}
		Assets::load( $this, $assets );
	}

	/**
	 * Generates a unique container ID for rendering.
	 *
	 * @param bool        $appendCount
	 * @param string|null $uniqid
	 * @return string
	 */
	public function getContainerId( $appendCount = true, $uniqid = null ) {
		if ( ! isset( $uniqid ) ) {
			$uniqid = uniqid();
		}
		$id = 'drts-platform-render-' . $uniqid;
		if ( $appendCount ) {
			$id .= '-' . ++$this->_renderCount;
		}
		return $id;
	}

	/**
	 * Gets the container CSS class for rendering.
	 *
	 * @return string
	 */
	public function getContainerClass( $class = null ) {
		$ret = 'drts drts-main';
		if ( $this->isRtl() ) {
			$ret .= ' drts-rtl';
		}
		if ( isset( $class ) ) {
			$ret .= ' ' . $this->_h( $class );
		}
		return $ret;
	}

	/**
	 * Renders a path with given attributes and options.
	 *
	 * @param mixed $path
	 * @param array $attributes
	 * @param array $options
	 * @return string|null
	 */
	public function render( $path, array $attributes = array(), array $options = array() ) {
		$options += array(
			'cache'           => false,
			'render_assets'   => true,
			'title'           => null,
			'class'           => null,
			'partial'         => null,
			'group'           => null,
			'prefix'          => null,
			'suffix'          => null,
			'container_class' => true,
		);

		// Init options
		$partial = is_string( $options['partial'] ) && strlen( $options['partial'] ) ? $options['partial'] : null;
		if ( empty( $partial ) ) {
			$container = $this->getContainerId();
			$hash      = null;
		} else {
			if ( ! isset( $options['group'] ) ) {
				throw new Exception\InvalidArgumentException( 'Group option (group) required for partial.' );
			}
			// Need to cache if partial requested
			if ( empty( $options['cache'] ) ) {
				$options['cache'] = 60;
			}
			$hash      = md5( $path . $options['group'] );
			$container = $this->getContainerId( false, $hash );
		}
		unset( $options['group'], $options['partial'] );

		$rendered = $this->_doRender( $path, $attributes, $options, $container, $partial, $hash );
		$content  = $rendered['content'];

		// Load assets if needed
		if ( ! empty( $rendered['assets'] ) ) {
			Assets::load( $this, $rendered['assets'] );
		}
		if ( $options['render_assets'] ) {
			$this->loadDefaultAssets();
			if ( $js_html = $this->getJsHtml() ) {
				$content .= PHP_EOL . $js_html;
			}
			if ( $head_html = $this->getHeadHtml() ) {
				$content = $head_html . PHP_EOL . $content;
			}
		}

		if ( ! strlen( $content ) ) {
			return;
		}

		if ( false !== $options['title'] // title disabled explicitly if false
			&& isset( $rendered['title'] )
			&& strlen( $rendered['title'] )
		) {
			$content = '<h2>' . $this->_h( $rendered['title'] ) . '</h2>' . PHP_EOL . $content;
		}

		$attr   = isset( $rendered['html_attributes'] ) ? $rendered['html_attributes'] : '';
		$prefix = $options['prefix'] ?? '';
		$suffix = $options['suffix'] ?? '';
		$class  = $options['container_class'] ? $this->getContainerClass( $options['class'] ) : ( isset( $options['class'] ) ? $this->_h( $options['class'] ) : '' );
		return $prefix . '<div' . $attr . ' class="' . $class . '">' . $content . '</div>' . $suffix;
	}

	/**
	 * Internal method to perform rendering and handle caching.
	 *
	 * @param mixed       $path
	 * @param array       $attributes
	 * @param array       $options
	 * @param string      $container
	 * @param string|null $partial
	 * @param string|null $hash
	 * @return array
	 */
	protected function _doRender( $path, array $attributes, array $options, $container, $partial, $hash ) {
		// Render and cache
		if ( ( ! $cacheable = ! empty( $options['cache'] ) )
			|| ( ! $cached = $this->getCache( $cache_id = 'core_platform_render_' . ( isset( $hash ) ? $hash : $this->_getRenderHash( $path, $attributes, $options ) ), 'content' ) )
		) {
			$this->trackAssets()->addJs( 'DRTS.init($(DRTS.container("' . $container . '").selector));', true, -99 );
			if ( $path instanceof Url ) {
				if ( ! $path->route ) {
					throw new Exception\InvalidArgumentException( 'URL path may not be empty' );
				}
				$params = $path->params;
				$path   = $path->route;
			} elseif ( is_array( $path ) ) {
				$params = $path['params'];
				$path   = $path['path'];
			} else {
				$params = null;
			}
			try {
				if ( empty( $path ) ) {
					throw new Exception\InvalidArgumentException( 'Path may not be empty' );
				}

				// Create context
				$context = ( new Context() )->setContainer( $container )
					->setRequest( new Request( true, true, $params ) )
					->setAttributes( $attributes );
				if ( ! empty( $options['title'] ) ) {
					$context->setTitle( $options['title'] );
				}
				if ( isset( $partial ) ) {
					$context->setContentType( 'parts' );
				}
				// Run Sabai
				$response = $this->getApplication()->setCurrentScriptName( 'main' )->run( new MainRoutingController(), $context, $path );

				// Cacheable if response is view
				$cacheable = $context->isView();

				// Render output
				if ( $context->getContentType() === 'parts' ) {
					$content = $response->send( $context );
				} else {
					ob_start();
					$response->send( $context );
					$content = ob_get_clean();
				}
			} catch ( \Exception $e ) {
				$this->getApplication()->logError( $e );
				if ( $this->isAdministrator() ) {
					$content = sprintf(
						'<p>%s</p><p><pre>%s</pre></p>',
						$this->_h( $e->getMessage() ),
						$this->_h( $e->getTraceAsString() )
					);
				} else {
					$content = sprintf( '<p>%s</p>', 'An error occurred while processing the request. Please contact the administrator of the website for further information.' );
				}
				return array( 'content' => $content );
			}

			$html_attr = $context->getHtmlAttribute();
			foreach ( $context->getExtraHtmlAttributes() as $k => $v ) {
				$html_attr .= sprintf( ' %s="%s"', $this->_h( $k ), is_string( $v ) ? $this->_h( $v ) : '' );
			}
			$cached = array(
				'title'           => empty( $options['title'] ) ? null : $context->getTitle( false ),
				'content'         => $content,
				'assets'          => $this->getTrackedAssets(),
				'html_attributes' => $html_attr,
			);
			$this->trackAssets( false );

			if ( ! empty( $options['cache'] )
				&& $cacheable
				&& ! $this->isAdmin() // WordPress shortcodes may run on the admin side
			) {
				$this->setCache(
					array(
						'title'           => $cached['title'],
						'content'         => str_replace( $container, '%%container_id%%', isset( $partial ) ? json_encode( $cached['content'] ) : $cached['content'] ),
						'assets'          => $cached['assets'],
						'html_attributes' => str_replace( $container, '%%container_id%%', $cached['html_attributes'] ),
					),
					isset( $cache_id ) ? $cache_id : 'core_platform_render_' . ( isset( $hash ) ? $hash : $this->_getRenderHash( $path, $attributes, $options ) ),
					is_numeric( $options['cache'] ) && $options['cache'] > 1 ? $options['cache'] : 86400,
					'content'
				);
			}
			// Assets already loaded while tracking, so no need to load them again
			unset( $cached['assets'] );
		} else {
			$cached['content'] = str_replace( '%%container_id%%', $container, $cached['content'] );
			if ( isset( $partial ) ) {
				$cached['content'] = json_decode( $cached['content'], true );
			}
			$cached['html_attributes'] = isset( $cached['html_attributes'] ) ? str_replace( '%%container_id%%', $container, $cached['html_attributes'] ) : '';
		}
		if ( isset( $partial ) ) {
			if ( isset( $cached['content'][ $partial ] ) ) {
				$cached['content']          = $cached['content'][ $partial ];
				$cached['html_attributes'] .= ' data-part="' . $this->_h( $partial ) . '"';
			} else {
				$cached['content'] = '';
			}
		}

		return $cached;
	}

	/**
	 * Escapes a string for safe HTML output.
	 *
	 * @param string $string
	 * @return string
	 */
	protected function _h( $string ) {
		return htmlspecialchars( $string, ENT_QUOTES, 'UTF-8' );
	}

	/**
	 * Gets the route parameter name.
	 *
	 * @return string
	 */
	public function getRouteParam() {
		return 'q';
	}

	/**
	 * Uninstalls the platform, clearing options and cache.
	 *
	 * @param bool $removeData
	 */
	public function uninstall( $removeData ) {
		// Clear options and cache
		if ( $removeData ) {
			$this->clearOptions();
		}
		$this->clearCache();
	}

	/**
	 * Formats a number with grouped thousands.
	 *
	 * @param float $number
	 * @param int   $decimals
	 * @return string
	 */
	public function numberFormat( $number, $decimals = 0 ) {
		return number_format( $number, $decimals );
	}

	/**
	 * Gets all administrator users.
	 *
	 * @return array
	 */
	public function getAdministrators() {
		$ret = array();
		foreach ( array_keys( $this->getAdministratorRoles() ) as $role_name ) {
			$ret += $this->getUsersByRole( $role_name );
		}

		return $ret;
	}

	/**
	 * Generates a random alphanumeric ID of the given length.
	 *
	 * @param int $length
	 * @return string
	 */
	public function getRandomId( $length = 12 ) {
		$chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
		$bytes = random_bytes( $length );
		$id    = '';
		foreach ( unpack( 'C*', $bytes ) as $byte ) {
			$id .= $chars[ $byte % 36 ];
		}
		return $id;
	}

	/**
	 * Loads jQuery JavaScript files for the specified type (main/admin).
	 *
	 * @param string $type
	 */
	abstract protected function _loadJqueryJs( $type );

	/**
	 * Loads a JavaScript file.
	 *
	 * @param string      $url
	 * @param string      $handle
	 * @param mixed       $dependency
	 * @param bool        $inFooter
	 * @param string|null $version
	 */
	abstract protected function _loadJsFile( $url, $handle, $dependency, $inFooter, $version = null );

	/**
	 * Unloads a JavaScript file by handle.
	 *
	 * @param string $handle
	 */
	abstract protected function _unloadJsFile( $handle );

	/**
	 * Loads inline JavaScript code.
	 *
	 * @param string $dependency
	 * @param string $js
	 * @param string $position
	 */
	abstract protected function _loadJsInline( $dependency, $js, $position );

	/**
	 * Loads a JavaScript module file.
	 *
	 * @param string      $url
	 * @param string      $id
	 * @param mixed       $dependency
	 * @param string|null $version
	 */
	abstract protected function _loadJsModuleFile( $url, $id, $dependency, $version = null );

	/**
	 * Unloads a JavaScript module file by ID.
	 *
	 * @param string $id
	 */
	abstract protected function _unloadJsModuleFile( $id );

	/**
	 * Loads a CSS file.
	 *
	 * @param string      $url
	 * @param string      $handle
	 * @param mixed       $dependency
	 * @param string      $media
	 * @param string|null $version
	 */
	abstract protected function _loadCssFile( $url, $handle, $dependency, $media, $version = null );

	/**
	 * Unloads a CSS file by handle.
	 *
	 * @param string $handle
	 */
	abstract protected function _unloadCssFile( $handle );

	/**
	 * Loads jQuery UI JavaScript components.
	 *
	 * @param array $components
	 */
	abstract protected function _loadJqueryUiJs( array $components );

	/**
	 * Loads the imagesLoaded JavaScript library.
	 */
	abstract protected function _loadImagesLoadedJs();

	/**
	 * Gets the database connection instance.
	 *
	 * @return AbstractDB
	 */
	abstract protected function _getDB();

	/**
	 * Gets the Bootstrap handle for the platform.
	 *
	 * @return mixed
	 */
	abstract protected function _getBootstrapHandle();

	/**
	 * Gets the query parameter name for pagination.
	 *
	 * @return string
	 */
	abstract public function getPageParam();

	/**
	 * Checks if the platform has Bootstrap CSS available.
	 *
	 * @return bool
	 */
	abstract public function hasBootstrapCss();

	/**
	 * Gets the user identity fetcher instance.
	 *
	 * @return \SabaiApps\Framework\User\AbstractIdentityFetcher
	 */
	abstract public function getUserIdentityFetcher();

	/**
	 * Gets the current user object.
	 *
	 * @return AbstractIdentity
	 */
	abstract public function getCurrentUser();

	/**
	 * Sets the current user by user ID.
	 *
	 * @param int $userId
	 */
	abstract public function setCurrentUser( $userId );

	/**
	 * Checks if the given user (or current user) is an administrator.
	 *
	 * @param int|null $userId
	 * @return bool
	 */
	abstract public function isAdministrator( $userId = null );

	/**
	 * Gets an array of administrator role names.
	 *
	 * @return array
	 */
	abstract public function getAdministratorRoles();

	/**
	 * Gets an array of roles for a user or all roles.
	 *
	 * @param int|null $userId
	 * @return array
	 */
	abstract public function getRoles( $userId = null );

	/**
	 * Gets users by role name.
	 *
	 * @param string $roleName
	 * @return array
	 */
	abstract public function getUsersByRole( $roleName );

	/**
	 * Gets permissions for a user.
	 *
	 * @param int $userId
	 * @return array
	 */
	abstract public function getPermissions( $userId );

	/**
	 * Checks if a user has a specific permission.
	 *
	 * @param int    $userId
	 * @param string $permission
	 * @return bool
	 */
	abstract public function hasPermission( $userId, $permission );

	/**
	 * Checks if a guest user has a specific permission.
	 *
	 * @param string $permission
	 * @return bool
	 */
	abstract public function guestHasPermission( $permission );

	/**
	 * Gets the log directory path.
	 *
	 * @return string
	 */
	abstract public function getLogDir();

	/**
	 * Gets the variable data directory path.
	 *
	 * @return string
	 */
	abstract public function getVarDir();

	/**
	 * Gets the URL to the variable data directory.
	 *
	 * @return string
	 */
	abstract public function getVarDirUrl();

	/**
	 * Gets the site path.
	 *
	 * @return string
	 */
	abstract public function getSitePath();

	/**
	 * Gets the package path.
	 *
	 * @return string
	 */
	abstract public function getPackagePath();

	/**
	 * Gets the version of a package.
	 *
	 * @param string $package
	 * @return string
	 */
	abstract public function getPackageVersion( $package );

	/**
	 * Gets an array of package names.
	 *
	 * @return array
	 */
	abstract public function getPackages();

	/**
	 * Gets the site name.
	 *
	 * @return string
	 */
	abstract public function getSiteName();

	/**
	 * Gets the site version.
	 *
	 * @return string
	 */
	abstract public function getSiteVersion();

	/**
	 * Gets the site email address.
	 *
	 * @return string
	 */
	abstract public function getSiteEmail();

	/**
	 * Gets the site URL.
	 *
	 * @return string
	 */
	abstract public function getSiteUrl();

	/**
	 * Gets the site admin URL.
	 *
	 * @return string
	 */
	abstract public function getSiteAdminUrl();

	/**
	 * Gets the site assets directory path.
	 *
	 * @return string
	 */
	abstract public function getSiteAssetsDir();

	/**
	 * Gets the URL to the assets directory for a package or vendor.
	 *
	 * @param string|null $package
	 * @param bool        $vendor
	 * @return string
	 */
	abstract public function getAssetsUrl( $package = null, $vendor = false );

	/**
	 * Gets the assets directory path for a package or vendor.
	 *
	 * @param string|null $package
	 * @param bool        $vendor
	 * @return string
	 */
	abstract public function getAssetsDir( $package = null, $vendor = false );

	/**
	 * Gets the login URL, optionally with a redirect.
	 *
	 * @param string $redirect
	 * @return string
	 */
	abstract public function getLoginUrl( $redirect = '' );

	/**
	 * Gets the logout URL.
	 *
	 * @return string
	 */
	abstract public function getLogoutUrl();

	/**
	 * Gets the registration URL, optionally with a redirect.
	 *
	 * @param string $redirect
	 * @return string
	 */
	abstract public function getRegisterUrl( $redirect = '' );

	/**
	 * Checks if a login form is required.
	 *
	 * @return bool
	 */
	abstract public function isLoginFormRequired();

	/**
	 * Checks if a registration form is required.
	 *
	 * @return bool
	 */
	abstract public function isRegisterFormRequired();

	/**
	 * Checks if user registration is allowed.
	 *
	 * @return bool
	 */
	abstract public function isUserRegisterable();

	/**
	 * Checks if a user exists by username.
	 *
	 * @param string $username
	 * @return bool
	 */
	abstract public function userExists( $username );

	/**
	 * Registers a new user.
	 *
	 * @param string      $username
	 * @param string      $email
	 * @param string|null $password
	 * @param array       $values
	 * @return mixed
	 */
	abstract public function registerUser( $username, $email, $password = null, array $values = array() );

	/**
	 * Logs in a user.
	 *
	 * @param string $username
	 * @param string $password
	 * @param bool   $remember
	 * @param array  $values
	 * @return mixed
	 */
	abstract public function loginUser( $username, $password, $remember, array $values );

	/**
	 * Logs out the current user.
	 */
	abstract public function logoutUser();

	/**
	 * Gets the reset password key for a user identity.
	 *
	 * @param AbstractIdentity $identity
	 * @return string
	 */
	abstract public function getResetPasswordKey( AbstractIdentity $identity );

	/**
	 * Checks if a reset password key is valid for a user identity.
	 *
	 * @param string           $key
	 * @param AbstractIdentity $identity
	 * @return bool
	 */
	abstract public function checkResetPasswordKey( $key, AbstractIdentity $identity );

	/**
	 * Resets the password for a user identity.
	 *
	 * @param string           $password
	 * @param string           $key
	 * @param AbstractIdentity $identity
	 * @return bool
	 */
	abstract public function resetPassword( $password, $key, AbstractIdentity $identity );

	/**
	 * Checks if the given password is the current password for a user identity.
	 *
	 * @param string           $password
	 * @param AbstractIdentity $identity
	 * @return bool
	 */
	abstract public function isCurrentPassword( $password, AbstractIdentity $identity );

	/**
	 * Changes the password for a user identity.
	 *
	 * @param string           $password
	 * @param AbstractIdentity $identity
	 * @return bool
	 */
	abstract public function changePassword( $password, AbstractIdentity $identity );

	/**
	 * Deletes a user account.
	 *
	 * @param AbstractIdentity $identity
	 * @return bool
	 */
	abstract public function deleteAccount( AbstractIdentity $identity );

	/**
	 * Gets the privacy policy link.
	 *
	 * @return string
	 */
	abstract public function getPrivacyPolicyLink();

	/**
	 * Sends an email.
	 *
	 * @param string $to
	 * @param string $subject
	 * @param string $body
	 * @param array  $options
	 * @return bool
	 */
	abstract public function mail( $to, $subject, $body, array $options = array() );

	/**
	 * Sets a session variable.
	 *
	 * @param string $name
	 * @param mixed  $value
	 */
	abstract public function setSessionVar( $name, $value );

	/**
	 * Gets a session variable.
	 *
	 * @param string $name
	 * @return mixed
	 */
	abstract public function getSessionVar( $name );

	/**
	 * Deletes a session variable.
	 *
	 * @param string $name
	 */
	abstract public function deleteSessionVar( $name );

	/**
	 * Sets entity meta data.
	 *
	 * @param string $entityType
	 * @param int    $entityId
	 * @param string $name
	 * @param mixed  $value
	 */
	abstract public function setEntityMeta( $entityType, $entityId, $name, $value );

	/**
	 * Gets entity meta data.
	 *
	 * @param string $entityType
	 * @param int    $entityId
	 * @param string $name
	 * @return mixed
	 */
	abstract public function getEntityMeta( $entityType, $entityId, $name = '' );

	/**
	 * Checks if entity meta data exists.
	 *
	 * @param string $entityType
	 * @param int    $entityId
	 * @param string $name
	 * @return bool
	 */
	abstract public function hasEntityMeta( $entityType, $entityId, $name );

	/**
	 * Deletes entity meta data.
	 *
	 * @param string $entityType
	 * @param int    $entityId
	 * @param string $name
	 */
	abstract public function deleteEntityMeta( $entityType, $entityId, $name );

	/**
	 * Gets users by meta data value.
	 *
	 * @param string      $name
	 * @param mixed       $value
	 * @param int         $limit
	 * @param int         $offset
	 * @param string      $order
	 * @param bool        $numeric
	 * @param string|null $compare
	 * @return array
	 */
	abstract public function getUsersByMeta( $name, $value, $limit = 20, $offset = 0, $order = 'DESC', $numeric = true, $compare = null );

	/**
	 * Gets the admin URL for an entity.
	 *
	 * @param string   $entityType
	 * @param string   $bundleName
	 * @param int|null $entityId
	 * @param int|null $userId
	 * @return string
	 */
	abstract public function getEntityAdmninUrl( $entityType, $bundleName, $entityId = null, $userId = null );

	/**
	 * Sets a cache value.
	 *
	 * @param mixed    $data
	 * @param string   $id
	 * @param int|null $lifetime
	 * @param string   $group
	 * @return Platform
	 */
	abstract public function setCache( $data, $id, $lifetime = null, $group = 'settings' );

	/**
	 * Gets a cache value.
	 *
	 * @param string $id
	 * @param string $group
	 * @return mixed
	 */
	abstract public function getCache( $id, $group = 'settings' );

	/**
	 * Deletes a cache value.
	 *
	 * @param string $id
	 * @param string $group
	 * @return Platform
	 */
	abstract public function deleteCache( $id, $group = 'settings' );

	/**
	 * Clears cache for a group or all.
	 *
	 * @param string|null $group
	 * @return Platform
	 */
	abstract public function clearCache( $group = null );

	/**
	 * Gets the current locale.
	 *
	 * @return string
	 */
	abstract public function getLocale();

	/**
	 * Checks if the current language is RTL.
	 *
	 * @return bool
	 */
	abstract public function isRtl();

	/**
	 * Sets an option value.
	 *
	 * @param string $name
	 * @param mixed  $value
	 * @param bool   $autoload
	 */
	abstract public function setOption( $name, $value, $autoload = true );

	/**
	 * Gets an option value.
	 *
	 * @param string $name
	 * @param mixed  $default
	 * @return mixed
	 */
	abstract public function getOption( $name, $default = null );

	/**
	 * Deletes an option value.
	 *
	 * @param string $name
	 */
	abstract public function deleteOption( $name );

	/**
	 * Clears all options.
	 */
	abstract public function clearOptions();

	/**
	 * Gets custom assets directories.
	 *
	 * @param bool $useCache
	 * @return array
	 */
	abstract public function getCustomAssetsDir( $useCache = true );

	/**
	 * Gets the URL to a custom assets directory by index.
	 *
	 * @param int $index
	 * @return string
	 */
	abstract public function getCustomAssetsDirUrl( $index );

	/**
	 * Gets the user profile HTML for a user ID.
	 *
	 * @param int $userId
	 * @return string
	 */
	abstract public function getUserProfileHtml( $userId );

	/**
	 * Converts site time to system time.
	 *
	 * @param int $timestamp
	 * @return int
	 */
	abstract public function getSiteToSystemTime( $timestamp );

	/**
	 * Converts system time to site time.
	 *
	 * @param int $timestamp
	 * @return int
	 */
	abstract public function getSystemToSiteTime( $timestamp );

	/**
	 * Unzips a file to a destination directory.
	 *
	 * @param string $from
	 * @param string $to
	 * @return bool
	 */
	abstract public function unzip( $from, $to );

	/**
	 * Updates the database schema.
	 *
	 * @param array      $schema
	 * @param array|null $previousSchema
	 * @return bool
	 */
	abstract public function updateDatabase( $schema, $previousSchema = null );

	/**
	 * Checks if the current context is admin.
	 *
	 * @return bool
	 */
	abstract public function isAdmin();

	/**
	 * Gets the cookie domain.
	 *
	 * @return string
	 */
	abstract public function getCookieDomain();

	/**
	 * Gets the cookie path.
	 *
	 * @return string
	 */
	abstract public function getCookiePath();

	/**
	 * Gets the cookie hash.
	 *
	 * @return string
	 */
	abstract public function getCookieHash();

	/**
	 * Converts text to HTML, optionally restricting tags.
	 *
	 * @param string $text
	 * @param bool   $restrictTags
	 * @param array  $disallowedTags
	 * @param bool   $forCaching
	 * @return string
	 */
	abstract public function htmlize( $text, $restrictTags = false, array $disallowedTags = array(), $forCaching = false );

	/**
	 * Gets the start of the week (0=Sunday, 1=Monday, etc.).
	 *
	 * @return int
	 */
	abstract public function getStartOfWeek();

	/**
	 * Gets the date format string.
	 *
	 * @return string
	 */
	abstract public function getDateFormat();

	/**
	 * Gets the time format string.
	 *
	 * @return string
	 */
	abstract public function getTimeFormat();

	/**
	 * Formats a timestamp as a date string.
	 *
	 * @param string $format
	 * @param int    $timestamp
	 * @return string
	 */
	abstract public function getDate( $format, $timestamp );

	/**
	 * Gets the site time zone string for use with PHP DateTimeZone class.
	 *
	 * @return string
	 */
	abstract public function getTimeZone();

	/**
	 * Gets the month names.
	 *
	 * @param bool $short
	 * @return array
	 */
	abstract public function getMonthNames( $short = false );

	/**
	 * Registers a string for translation.
	 *
	 * @param string $str
	 * @param string $name
	 * @param string $domain
	 */
	abstract public function registerString( $str, $name, $domain = 'directories-pro' );

	/**
	 * Unregisters a string from translation.
	 *
	 * @param string $name
	 * @param string $domain
	 */
	abstract public function unregisterString( $name, $domain = 'directories-pro' );

	/**
	 * Translates a string.
	 *
	 * @param string      $str
	 * @param string      $name
	 * @param string      $domain
	 * @param string|null $lang
	 * @return string
	 */
	abstract public function translateString( $str, $name, $domain = 'directories-pro', $lang = null );

	/**
	 * Gets available languages.
	 *
	 * @return array
	 */
	abstract public function getLanguages();

	/**
	 * Gets the default language.
	 *
	 * @return string
	 */
	abstract public function getDefaultLanguage();

	/**
	 * Gets the current language.
	 *
	 * @return string
	 */
	abstract public function getCurrentLanguage();

	/**
	 * Checks if an entity type/bundle is translatable.
	 *
	 * @param string $entityType
	 * @param string $bundleName
	 * @return bool
	 */
	abstract public function isTranslatable( $entityType, $bundleName );

	/**
	 * Gets the translated ID for an entity.
	 *
	 * @param string $entityType
	 * @param string $bundleName
	 * @param int    $id
	 * @param string $lang
	 * @return int
	 */
	abstract public function getTranslatedId( $entityType, $bundleName, $id, $lang );

	/**
	 * Sets translations for an entity.
	 *
	 * @param string $entityType
	 * @param string $bundleName
	 * @param string $sourceLang
	 * @param int    $entityId
	 * @param string $transLang
	 * @param int    $transEntityId
	 */
	abstract public function setTranslations( $entityType, $bundleName, $sourceLang, $entityId, $transLang, $transEntityId );

	/**
	 * Gets the language for an entity.
	 *
	 * @param string $entityType
	 * @param string $bundleName
	 * @param int    $id
	 * @return string
	 */
	abstract public function getLanguageFor( $entityType, $bundleName, $id );

	/**
	 * Checks if admin is adding a translation.
	 *
	 * @param array $reqParams
	 * @return bool
	 */
	abstract public function isAdminAddTranslation( array $reqParams );

	/**
	 * Checks if debug mode is enabled.
	 *
	 * @return bool
	 */
	abstract public function isDebugEnabled();

	/**
	 * Checks if AMP is enabled for a bundle.
	 *
	 * @param string $bundleName
	 * @return bool
	 */
	abstract public function isAmpEnabled( $bundleName );

	/**
	 * Checks if the current context is AMP.
	 *
	 * @return bool
	 */
	abstract public function isAmp();

	/**
	 * Checks if a slug exists for a component.
	 *
	 * @param string      $component
	 * @param string      $slug
	 * @param string|null $lang
	 * @return bool
	 */
	abstract public function hasSlug( $component, $slug, $lang = null );

	/**
	 * Gets a slug for a component.
	 *
	 * @param string      $component
	 * @param string      $slug
	 * @param string|null $lang
	 * @return string
	 */
	abstract public function getSlug( $component, $slug, $lang = null );

	/**
	 * Gets a title for a component.
	 *
	 * @param string      $component
	 * @param string      $name
	 * @param string|null $lang
	 * @return string
	 */
	abstract public function getTitle( $component, $name, $lang = null );

	/**
	 * Performs a remote GET request.
	 *
	 * @param string $url
	 * @param array  $args
	 * @return mixed
	 */
	abstract public function remoteGet( $url, array $args = array() );

	/**
	 * Performs a remote POST request.
	 *
	 * @param string $url
	 * @param array  $params
	 * @param array  $args
	 * @return mixed
	 */
	abstract public function remotePost( $url, array $params = array(), array $args = array() );

	/**
	 * Anonymizes an email address.
	 *
	 * @param string $email
	 * @return string
	 */
	abstract public function anonymizeEmail( $email );

	/**
	 * Anonymizes a URL.
	 *
	 * @param string $url
	 * @return string
	 */
	abstract public function anonymizeUrl( $url );

	/**
	 * Anonymizes an IP address.
	 *
	 * @param string $ip
	 * @return string
	 */
	abstract public function anonymizeIp( $ip );

	/**
	 * Anonymizes text.
	 *
	 * @param string $text
	 * @return string
	 */
	abstract public function anonymizeText( $text );

	/**
	 * Downloads a URL to a file or outputs it.
	 *
	 * @param string      $url
	 * @param bool        $save
	 * @param string|null $title
	 * @param string|null $ext
	 * @return mixed
	 */
	abstract public function downloadUrl( $url, $save = false, $title = null, $ext = null );

	/**
	 * Uploads a file.
	 *
	 * @param string      $path
	 * @param string      $name
	 * @param string|null $title
	 * @param string|null $description
	 * @return mixed
	 */
	abstract public function uploadFile( $path, $name, $title = null, $description = null );

	/**
	 * Gets a file by ID.
	 *
	 * @param int $id
	 * @return mixed
	 */
	abstract public function getFile( $id );

	/**
	 * Encodes a string for storage.
	 *
	 * @param string $str
	 * @param string $schemaType
	 * @param string $columnName
	 * @return string
	 */
	abstract public function encodeString( $str, $schemaType, $columnName );

	/**
	 * Gets a hash of the given data.
	 *
	 * @param string $data
	 * @param string $algo
	 * @return string
	 */
	abstract public function getHash( $data, $algo = 'sha256' );

	/**
	 * Gets the scheduler instance.
	 *
	 * @return IScheduler
	 */
	abstract public function getScheduler();
}
