<?php namespace ProcessWire;

/**
 * ProcessWire Admin Theme Module
 *
 * An abstract module intended as a base for admin themes. 
 *
 * See the Module interface (Module.php) for details about each method. 
 *
 * This file is licensed under the MIT license. 
 * https://processwire.com/about/license/mit/
 * 
 * ProcessWire 3.x, Copyright 2022 by Ryan Cramer
 * https://processwire.com
 * 
 * @property int|string $version Current admin theme version
 * @property string $url URL to admin theme
 * @property string $path Disk path to admin theme
 * 
 * @method void install()
 * @method void uninstall()
 * @method array getExtraMarkup()
 *
 */

abstract class AdminTheme extends WireData implements Module {

	/**
	 * Per the Module interface, return an array of information about the Module
	 *
	 */
	public static function getModuleInfo() {
		return array(
			'title'    => '',        // printable name/title of module
			'version'  => 1,    // version number of module
			'summary'  => '',    // 1 sentence summary of module
			'href'     => '',        // URL to more information (optional)

			// all admin themes should have this as their autoload selector:
			'autoload' => 'template=admin',
			'singular' => true
		);
	}

	/**
	 * Current admin theme version (cached from module info)
	 *
	 * @var int
	 *
	 */
	protected $version = 0;

	/**
	 * Keeps track of quantity of admin themes installed so that we know when to add profile field
	 *
	 */
	protected static $numAdminThemes = 0;

	/**
	 * Additional classes for body tag
	 *
	 * @var array
	 *
	 */
	protected $bodyClasses = array();

	/**
	 * General purpose classes indexed by name
	 *
	 * @var array
	 *
	 */
	protected $classes = array();

	/**
	 * Extra markup regions
	 * 
	 * @var array
	 * 
	 */
	protected $extraMarkup = array(
		'head' => '',
		'notices' => '',
		'body' => '',
		'masthead' => '',
		'content' => '',
		'footer' => '',
		'sidebar' => '', // sidebar not used in all admin themes
	);
	
	/**
	 * URLs to place in link prerender tags
	 * 
	 * @var array
	 * 
	 */
	protected $preRenderURLs = array();

	/**
	 * Construct
	 * 
	 */
	public function __construct() {
		// placeholder
		parent::__construct();
	}

	/**
	 * Initialize the admin theme system and determine which admin theme should be used
	 *
	 * All admin themes must call this init() method to register themselves. 
	 * 
	 * Note: this should be called after API ready. 
	 *
	 */
	public function init() { 
		self::$numAdminThemes++;
		
		$info = $this->wire()->modules->getModuleInfo($this);
		$this->version = $info['version'];
		$page = $this->wire()->page;

		// if module has been called when it shouldn't (per the 'autoload' conditional)
		// then module author probably forgot the right 'autoload' string, so this 
		// serves as secondary stopgap to keep this module from loading when it shouldn't.
		if(!$page || $page->template->name !== 'admin') return;
		
		if(self::$numAdminThemes > 1 && !$this->wire()->fields->get('admin_theme')) $this->install();

		// if admin theme has already been set, then no need to continue
		if($this->wire('adminTheme')) return; 

		$config = $this->wire()->config;
		$user = $this->wire()->user;
		$adminTheme = $user->admin_theme; /** @var string $adminTheme */
		$isCurrent = false;

		if($adminTheme) {
			// there is user specified admin theme
			// check if this is the one that should be used
			if($adminTheme == $this->className()) {
				$isCurrent = true;
				$this->setCurrent();
			}
			
		} else if($config->defaultAdminTheme == $this->className()) {
			// there is no user specified admin theme, so use this one
			$isCurrent = true;
			$this->setCurrent();
		}
		
		if($isCurrent) $this->initConfig();
	}

	/**
	 * Initialize configuration properties and JS config for when this is current admin theme
	 * 
	 * @since 3.0.173
	 * 
	 */
	protected function initConfig() {
		
		$config = $this->wire()->config;
		$user = $this->wire()->user;
		$session = $this->wire()->session;
		$page = $this->wire()->page;
		$urls = $config->urls;
		
		// adjust $config adminThumbOptions[scale] for auto detect when requested
		$o = $config->adminThumbOptions;
		if($o && isset($o['scale']) && $o['scale'] === 1) {
			$o['scale'] = $session->get('hidpi') ? 0.5 : 1.0;
			$config->adminThumbOptions = $o;
		}

		$config->jsConfig('urls', array(
			'root' => $urls->root,
			'admin' => $urls->admin,
			'modules' => $urls->modules,
			'core' => $urls->core,
			'files' => $urls->files,
			'templates' => $urls->templates,
			'adminTemplates' => $urls->adminTemplates,
		));

		$config->js('modals', true); // share at render time
		$config->jsConfig('debug', $config->debug); 
		
		if($user) {
			$userInfo = array(
				'id' => $user->id,
				'name' => $user->name,
				'roles' => array(),
			);
			$roles = $user->isLoggedin() ? $user->roles : null;
			$guestRoleID = $config->guestUserRolePageID;
			if($roles) foreach($roles as $role) {
				if($role->id !== $guestRoleID) $userInfo['roles'][] = $role->name;
			}
			$config->jsConfig('user', $userInfo);
		}
		
		if($page) {
			$config->jsConfig('page', array(
				'id' => $page->id,
				'name' => $page->name,
				'process' => (string) $page->process,
			));
		}
		
		if($session->get('hidpi')) $this->addBodyClass('hidpi-device');
		if($session->get('touch')) $this->addBodyClass('touch-device');
		
		$this->addBodyClass($this->className());
	}

	/**
	 * Get property
	 * 
	 * @param string $key
	 * @return int|mixed|null|string
	 * 
	 */
	public function get($key) {
		if($key === 'version') return $this->version;
		if($key === 'url') return $this->url();
		if($key === 'path') return $this->path();
		return parent::get($key); 
	}
	
	/**
	 * Get URL to this admin theme
	 *
	 * @return string
	 * @since 3.0.171
	 *
	 */
	public function url() {
		return $this->wire()->config->urls($this->className());
	}

	/**
	 * Get disk path to this admin theme
	 * 
	 * @return string
	 * @since 3.0.171
	 * 
	 */
	public function path() {
		$config = $this->wire()->config;
		$path = $config->paths($this->className());
		if(empty($path)) {
			$class = $this->className();
			$path = $config->paths->modules . "AdminTheme/$class/";
			if(!is_dir($path)) {
				$path = $config->paths->siteModules . "$class/";
				if(!is_dir($path)) $path = __DIR__;
			}
		}
		return $path;
	}

	/**
	 * Get predefined translated label by key for labels shared among admin themes
	 * 
	 * @param string $key
	 * @param string $val Value to return if label not available
	 * @return string
	 * @since 3.0.162
	 * 
	 */
	public function getLabel($key, $val = '') {
		switch($key) {
			case 'search-help': return $this->_('help'); // Localized term to type for search-engine help (3+ chars) 
			case 'search-tip': return $this->_('Try “help”'); // // Search tip (indicating your translated “help” term)
			case 'advanced-mode': return $this->_('Advanced Mode');
			case 'debug': return $this->_('Debug'); 
		}
		return $val;
	}

	/**
	 * Returns true if this admin theme is the one that will be used for this request
	 *
	 */
	public function isCurrent() {
		$adminTheme = $this->wire()->adminTheme;
		return $adminTheme && $adminTheme->className() === $this->className();
	}

	/**
	 * Set this admin theme as the current one
	 * 
	 */
	protected function setCurrent() {
		$config = $this->wire()->config;
		$name = $this->className();
		$config->paths->set('adminTemplates', $config->paths->get($name));
		$config->urls->set('adminTemplates', $config->urls->get($name)); 
		$config->set('defaultAdminTheme', $name);
		$this->wire('adminTheme', $this);
	}

	/**
	 * Enables hooks to append extra markup to various sections of the admin page
	 * 
	 * @return array Associative array containing the following properties, any of 
	 * which may be populated or empty: 
	 * 	- head
	 * 	- body
	 * 	- masthead
	 * 	- content
	 * 	- footer
	 * 	- sidebar
	 * 
	 */
	public function ___getExtraMarkup() {
		$parts = $this->extraMarkup;
		$isLoggedin = $this->wire()->user->isLoggedin();
		if($isLoggedin && $this->wire()->modules->isInstalled('InputfieldCKEditor') 
			&& $this->wire()->process instanceof WirePageEditor) {
			// necessary for when CKEditor is loaded via ajax
			$script = 'script';
			$parts['head'] .= "<$script>" . 
				"window.CKEDITOR_BASEPATH='" . $this->wire()->config->urls('InputfieldCKEditor') . 
				'ckeditor-' . InputfieldCKEditor::CKEDITOR_VERSION . "/';</$script>";
		}
		/*
		if($isLoggedin && $this->wire('config')->advanced) {
			$parts['footer'] = "<p class='AdvancedMode'><i class='fa fa-flask'></i> " . $this->_('Advanced Mode') . "</p>";
		}
		*/
		foreach($this->preRenderURLs as $url) {
			$parts['head'] .= "<link rel='prerender' href='$url'>";
		}
		return $parts; 
	}

	/**
	 * Add extra markup to a region in the admin theme
	 * 
	 * @param string $name
	 * @param string $value
	 * 
	 */
	public function addExtraMarkup($name, $value) {
		if(!empty($this->extraMarkup[$name])) {
			$this->extraMarkup[$name] .= "\n$value";
		} else {
			$this->extraMarkup[$name] = $value;
		}
	}

	/**
	 * Add a <body> class to the admin theme
	 * 
	 * @param string $className
	 * 
	 */
	public function addBodyClass($className) {
		$this->addClass('body', $className);
	}

	/**
	 * Get the body[class] attribute string
	 * 
	 * @return string
	 * 
	 */
	public function getBodyClass() {
		return $this->getClass('body'); 
	}

	/**
	 * Return class for a given named item or blank if none available
	 * 
	 * Omit the first argument to return all classes in an array.
	 * 
	 * @param string $name Tag or item name, i.e. “input”, or omit to return all defined [tags=classes]
	 * @param bool $getArray Specify true to return array of class name(s) rather than string (default=false). $name argument required.
	 * @return string|array Returns string or array of class names, or array of all [tags=classes] or $tagName argument is empty.
	 * 
	 */
	public function getClass($name = '', $getArray = false) {
		if(empty($name)) {
			return $this->classes;
		} else if(isset($this->classes[$name])) {
			return $getArray ? explode(' ', $this->classes[$name]) : $this->classes[$name];
		} else {
			return $getArray ? array() : '';
		}
	}

	/**
	 * Add class for given named item
	 * 
	 * Default behavior is to merge classes if existing classes are already present for given item $name.
	 * 
	 * #pw-internal
	 * 
	 * @param string $name
	 * @param string|array $class
	 * @param bool $replace Specify true to replace any existing classes rather than merging them
	 * 
	 */
	public function addClass($name, $class, $replace = false) {
		if(is_array($class)) {
			foreach($class as $c) {
				$this->addClass($name, $c);
			}
		} else if(!$replace && isset($this->classes[$name])) {
			$classes = $this->classes[$name];
			if(strpos($classes, $class) !== false) {
				// avoid re-adding class if it is already present
				if(array_search($class, explode(' ', $classes)) !== false) return; 
			}
			$this->classes[$name] = trim($classes . ' ' . ltrim($class));	
		} else {
			$this->classes[$name] = trim($class);
		}
	}

	/**
	 * Set classes for multiple tags
	 * 
	 * #pw-internal
	 * 
	 * @param array $classes Array of strings (class names) where keys are tag names
	 * @param bool $replace Specify true to replace any existing classes rather than merge them (default=false)
	 * 
	 */
	public function setClasses(array $classes, $replace = false) {
		if($replace || empty($this->classes)) {
			$this->classes = $classes;	
		} else {
			foreach($classes as $name => $class) {
				$this->addClass($name, $class);
			}
		}
	}

	/**
	 * Install the admin theme
	 *
	 * Other admin themes using an install() method must call this install before their own.
	 *
	 */
	public function ___install() { 

		// if we are the only admin theme installed, no need to add an admin_theme field
		if(self::$numAdminThemes == 0) return;
		
		$modules = $this->wire()->modules;

		// install a field for selecting the admin theme from the user's profile
		/** @var Field $field */
		$field = $this->wire()->fields->get('admin_theme'); 

		$toUseNote = $this->_('To use this theme, select it from your user profile.'); 

		// we already have this field installed, no need to continue
		if($field) {
			$this->message($toUseNote); 
		} else {
			// this will be the 2nd admin theme installed, so add a field that lets them select admin theme
			/** @var Field $field */
			$field = $this->wire(new Field());
			$field->name = 'admin_theme';
			$field->type = $modules->get('FieldtypeModule');
			$field->set('moduleTypes', array('AdminTheme'));
			$field->set('labelField', 'title');
			$field->set('inputfieldClass', 'InputfieldRadios');
			$field->label = 'Admin Theme';
			$field->flags = Field::flagSystem;
			try {
				$field->save();
			} catch(\Exception $e) {
				// $this->error("Error creating 'admin_theme' field: " . $e->getMessage());
			}
		}

		if($field && $field->id) {
			/** @var Fieldgroup $fieldgroup */
			$fieldgroup = $this->wire()->fieldgroups->get('user');
			if(!$fieldgroup->hasField($field)) {
				$fieldgroup->add($field);
				$fieldgroup->save();
				$this->message($this->_('Installed field "admin_theme" and added to user profile settings.'));
				$this->message($toUseNote);
			}
			// make this field one that the user is allowed to configure in their profile
			$data = $modules->getModuleConfigData('ProcessProfile');
			$data['profileFields'][] = 'admin_theme';
			$modules->saveModuleConfigData('ProcessProfile', $data); 
		}
	}

	/**
	 * Set a pre-render URL or get currently pre-render URL(s)
	 * 
	 * #pw-internal
	 * 
	 * @param string $url
	 * @return array
	 * 
	 */
	public function preRenderURL($url = '') {
		if(!empty($url)) $this->preRenderURLs[] = $url;
		return $this->preRenderURLs;
	}
	
	public function ___upgrade($fromVersion, $toVersion) {
		// placeholder if called by descending admin theme
	}
	
	public function ___uninstall() { 
	
		$defaultAdminTheme = $this->wire()->config->defaultAdminTheme;
		if($defaultAdminTheme == $this->className()) {
			throw new WireException(
				"Cannot uninstall this admin theme because \$config->defaultAdminTheme = '$defaultAdminTheme'; " . 
				"Please add this setting with a different value in /site/config.php"
			); 
		}

		/*
		if(self::$numAdminThemes > 1) return;

		// this is the last installed admin theme
		$field = $this->wire('fields')->get('admin_theme'); 	
		$field->flags = Field::flagSystemOverride; 
		$field->flags = 0; 
		$field->save();

		$fieldgroup = $this->wire('fieldgroups')->get('user'); 
		$fieldgroup->remove($field); 
		$fieldgroup->save();

		$this->wire('fields')->delete($field); 
		$this->message($this->_('Removed field "admin_theme" from system.')); 
		*/
	}
}

