<?php namespace ProcessWire;

/**
 * AdminThemeUikit CSS
 * 
 * Manages selection of CSS file and determines when CSS file to be recompiled from LESS
 * source files. 
 *
 * @property bool $upgrade Set to true when upgrading core Uikit version. (default=false)
 * @property string $frameworkLessFile Full disk path to LESS file that includes the framework/Uikit.
 * @property array $baseStyles Base style options (default=[ 'reno', 'rock' ])
 * @property string $defaultStyle Default style (default='reno')
 * @property string $defaultCssFile Core CSS file to create when upgrading (relative to module root)
 * @property string $styleDir Directory where base .less files are located (relative to module root)
 * @property array $replacements Array of [find=>replace] for compiled CSS file.
 * 
 * @property string $configPhpHash Hash used internally to detect changes to $config->AdminThemeUikit settings.
 * @property string $configPhpName Name of property in $config that holds custom settings (default='AdminThemeUikit').
 * @property int $requireCssVersion
 * @property int $cssVersion
 *
 * Settings that may be specified in $config->AdminThemeUikit array:
 * 
 * @property string $style Configured style name to use, one of blank (for default), 'reno' or 'rock'.
 * @property bool $recompile Recompile all LESS to CSS now? (set to true for 1 request only)
 * @property bool $compress Compress compiled CSS? (default=true)
 * @property array $customLessFiles Custom .less file(s) to include, relative to PW root.
 * @property string $customCssFile Custom target .css file to compile custom .less file(s) to, relative to PW root.
 * 
 * @since 3.0.179
 * 
 */
class AdminThemeUikitCss extends WireData {

	/**
	 * @var AdminTheme|AdminThemeUikit
	 * 
	 */
	protected $adminTheme;
	
	/**
	 * Construct
	 * 
	 * @param AdminTheme $adminTheme
	 * @param array $options
	 *
	 */
	public function __construct(AdminTheme $adminTheme, array $options = array()) {
		$this->adminTheme = $adminTheme;
		$adminTheme->wire($this);
		$this->setArray(array_merge($this->getDefaults(), $options)); 
		parent::__construct();
	}

	/**
	 * @return array
	 * 
	 */
	public function getDefaults() {
		return array(
			'baseStyles' => array('reno', 'rock'),
			'defaultStyle' => 'reno',
			'frameworkLessFile' => __DIR__ . '/uikit-pw/pw.less',
			'defaultCssFile' => 'uikit-pw/pw.min.css',
			'styleDir' => 'uikit-pw/styles/', 
			'style' => '',
			'upgrade' => false,
			'recompile' => false,
			'compress' => true,
			'customLessFiles' => array('/site/templates/admin.less'),
			'customCssFile' => '/site/assets/admin.css',
			'configPhpName' => $this->adminTheme->className(),
			'configPhpHash' => $this->adminTheme->get('configPhpHash'),
			'replacements' => array(),
			'cssVersion' => (int) $this->adminTheme->get('cssVersion'),
			'requireCssVersion' => 0,
		);
	}
	
	/**
	 * Get the primary Uikit CSS file URL to use (whether default or custom)
	 *
	 * @param bool $getPath Get disk path rather than URL?
	 * @return string
	 *
	 */
	public function getCssFile($getPath = false) {

		$modules = $this->wire()->modules;

		if(!$modules->isInstalled('Less')) return $this->getDefaultCssFile($getPath);

		if($this->upgrade) {
			$cssFile = $this->getDefaultCssFile(true);
			$cssTime = filemtime($cssFile);
			$lessFiles = array();
			$recompile = true;
		} else {
			$lessFiles = array();
			$lessTime = 0;

			foreach($this->customLessFiles as $file) {
				$file = $this->customFile($file, 'less');
				if(!$file || !is_file($file)) continue;
				$lessFiles[] = $file;
				$mtime = filemtime($file);
				if($mtime > $lessTime) $lessTime = $mtime;
			}
			
			if(!count($lessFiles) && ($this->style === '' || $this->style === $this->defaultStyle)) {
				return $this->getDefaultCssFile($getPath);
			}

			$cssFile = $this->customFile($this->customCssFile, 'css');
			if(!$cssFile) return $this->getDefaultCssFile($getPath);
			
			$cssTime = is_file($cssFile) ? (int) filemtime($cssFile) : 0;
			$recompile = $this->recompile || $lessTime > $cssTime || $this->cssVersion < $this->requireCssVersion; 
			if(!$recompile && $this->configPhpSettingsChanged()) $recompile = true;
		}

		if($recompile) try {
			/** @var AdminThemeUikitLessInterface $less */
			$less = $modules->get('Less');
			$less->setOption('compress', $this->compress);
			$less->addFile($this->frameworkLessFile);
			$less->addFile($this->getAdminStyleLessFile());
			$less->addFiles($lessFiles);
			$options = array('replacements' => $this->replacements); 
			if(!$less->saveCss($cssFile, $options)) throw new WireException("Compile error: $cssFile");
			$messages = array(sprintf($this->_('Compiled: %s'), $cssFile));
			$cssTime = filemtime($cssFile);
			if($this->cssVersion < $this->requireCssVersion) {
				$messages[] = "(core CSS v$this->cssVersion => v$this->requireCssVersion)";
				$modules->saveConfig($this->adminTheme, 'cssVersion', $this->requireCssVersion);
				$this->adminTheme->set('cssVersion', $this->requireCssVersion);
			}
			$this->message(implode(' ', $messages), Notice::noGroup);
		} catch(\Exception $e) {
			$this->error('LESS - ' . $e->getMessage(), Notice::noGroup);
		}
	
		return $getPath ? $cssFile : $this->fileToUrl($cssFile) . "?v=$cssTime";
	}

	/**
	 * Get URL for given full path/file
	 * 
	 * @param string $file
	 * @return string
	 * 
	 */
	protected function fileToUrl($file) {
		$config = $this->wire()->config;
		return $config->urls->root . substr($file, strlen($config->paths->root));
	}

	/**
	 * Get default Uikit CSS file URL or disk path
	 *
	 * @param bool $getPath
	 * @return string
	 *
	 */
	public function getDefaultCssFile($getPath = false) {
		$config = $this->wire()->config;
		$file = $this->defaultCssFile;
		$path = $config->paths($this->adminTheme) . $file;
		if($getPath) return $path;
		$v = filemtime($path);
		$url = $config->urls($this->adminTheme) . "$file?v=$v" ;
		return $url;
	}

	/**
	 * Have the $config->AdminThemeUikit settings changed?
	 *
	 * @return bool
	 * @throws WireException
	 *
	 */
	public function configPhpSettingsChanged() {
		$settings = $this->wire()->config->get($this->configPhpName);
		unset($settings['recompile']); // recompile is runtime only setting
		$hashNow = md5(print_r($settings, true));
		$hashThen = $this->get('configPhpHash');
		if($hashNow === $hashThen) return false;
		$this->wire()->modules->saveConfig($this->adminTheme, 'configPhpHash', $hashNow);
		return true;
	}

	/**
	 * Apply custom file/path replacements
	 *
	 * @param string $file
	 * @param string $requireExtension Extension to require on given file
	 * @return string
	 *
	 */
	protected function customFile($file, $requireExtension = '') {

		$paths = $this->wire()->config->paths;
		$file = $this->wire()->files->unixFileName($file);
		
		$replacements = array(
			'/site/assets/' => $paths->assets,
			'/site/templates/' => $paths->templates,
			'/site/modules/' => $paths->siteModules,
		);

		if($requireExtension) {
			$ext = pathinfo($file, PATHINFO_EXTENSION);
			if(strtolower($ext) !== strtolower($requireExtension)) return '';
		}

		foreach($replacements as $find => $replace) {
			if(strpos($file, $find) === 0) $file = str_replace($find, $replace, $file);
		}

		if($file && strpos($file, $paths->root) !== 0) {
			$file = $paths->root . ltrim($file, '/');
		}

		return $file;
	}

	/**
	 * Get admin base style file to use
	 *
	 * @return string
	 *
	 */
	public function getAdminStyleLessFile() {

		$config = $this->wire()->config;
		$files = $this->wire()->files;
		$path = $config->paths($this->adminTheme) . $this->styleDir;
		$defaultFile = $path . $this->defaultStyle . '.less';
		$baseStyle = $this->upgrade ? $this->defaultStyle : $this->style;

		if(empty($baseStyle) || $baseStyle === $this->defaultStyle) return $defaultFile;

		if(stripos($baseStyle, '.')) {
			// style is file name relative to installation root path
			$file = $this->customFile($baseStyle, 'less');
			if($file && is_file($file) && $files->allowPath($file, $config->paths->root)) return $file;
		}

		$file = $path . basename($baseStyle) . '.less';
		if(in_array($baseStyle, $this->baseStyles) || is_file($file)) return $file;

		$this->warning(
			"config.{$this->configPhpName}[style]: " .
			sprintf($this->_('Admin base style - file not found: %s'), $file),
			Notice::debug
		);

		return $defaultFile;
	}
	
}