<?php namespace ProcessWire;

/**
 * ProcessWire Image Select + Edit Process
 *
 * Provides the image selecting and editing capability for rich text editors (TinyMCE/CKEditor)
 * 
 * ProcessWire 3.x, Copyright 2016 by Ryan Cramer
 * https://processwire.com
 * 
 * @property int $hidpiDefault HiDPI/Retina checkbox default checked?
 * @property int $noThumbs Do not generate thumbnail images (make image selection will use full-size images).
 * @property string skipFields Space separated field names to skip for selection of images. 
 * @property int $noSizeAttrs Skip width attributes on image tags? (not recommended if you want HiDPI support)
 * @property string $alignLeftClass Align left class (align_left recommended)
 * @property string $alignRightClass Align right class (align_right recommended)
 * @property string $alignCenterClass Align center class (align_center recommended)
 * 
 * @method string execute()
 * @method string executeEdit()
 * 
 */

class ProcessPageEditImageSelect extends Process implements ConfigurableModule {
	
	public static function getModuleInfo() {
		return array(
			'title' => 'Page Edit Image',
			'summary' => 'Provides image manipulation functions for image fields and rich text editors.',
			'version' => 120,
			'permanent' => true,
			'permission' => 'page-edit',
		);
	}
	
	protected $maxImageWidth = 835; 
	protected $page = null;
	protected $masterPage = null;
	protected $editorPage = null;
	protected $defaultClass = 'Pageimage';
	protected $file = '';
	protected $editWidth = 0;
	protected $editHeight = 0;
	protected $hidpi = false;
	protected $extensions = 'jpg|jpeg|gif|png|svg';
	protected $labels = array();
	protected $rte = true; // in RTE mode?
	protected $field = null;
	protected $fieldName = '';
	protected $original = '';
	protected $caption = '';

	protected static $defaultConfig = array(
		'hidpiDefault' => 0, 
		'skipFields' => '',
		'noSizeAttrs' => 0,
		'noThumbs' => 0, 
		'alignLeftClass' => 'align_left',
		'alignRightClass' => 'align_right',
		'alignCenterClass' => 'align_center',
		);

	/**
	 * Construct and set default configuration
	 * 
	 */
	public function __construct() {
		foreach(self::$defaultConfig as $key => $value) $this->set($key, $value);
		$this->labels = array(
			'width' => $this->_('Width:'),
			'height' => $this->_('Height:'),
			'top' => $this->_('Top:'),
			'left' => $this->_('Left:'),
			'hidpi' => $this->_('HiDPI/Retina'),
			'description' => $this->_('Image description (alt attribute)'),
			'linkOriginal' => $this->_('Link to larger/original version'),
			'resize' => $this->_('Resize'),
			'useResize' => $this->_('Save at this size?'), 
			'crop' => $this->_('Crop'),
			'saveCrop' => $this->_('Apply'),
			'max' => $this->_('Maximize/full width'),
			'min' => $this->_('Minimize/fit to screen'),
			'cancel' => $this->_('Cancel'),
			'alignLeft' => $this->_('Align Left'),
			'alignRight' => $this->_('Align Right'),
			'alignCenter' => $this->_('Align Center'),
			'saveReplace' => $this->_('Save and Replace'),
			'saveCopy' => $this->_('Save as Copy'), 
			'saving' => $this->_('Saving...'),
			'updating' => $this->_('Updating...'), 
			'yes' => $this->_('Yes'), 
			'noUse' => $this->_('No, use %s'),
			'caption' => $this->_('Caption?'), 
			'captionTip' => $this->_('Caption text is entered in the editor after you insert the image.'), 
			'captionText' => $this->_('Caption text here'),
			'rotateRight' => $this->_('Rotate to the right'), 
			'rotateLeft' => $this->_('Rotate to the left'), 
			'flipHorizontal' => $this->_('Flip horizontal'),
			'flipVertical' => $this->_('Flip vertical'),
			'noAccess' => $this->_('You do not have access to edit images on this page.'),
			'demoMode' => "Image editing functions are disabled in demo mode",
		);
	}

	/**
	 * Initialize and populate required variables from GET variables
	 * 
	 * @throws WireException
	 * 
	 */
	public function init() {

		// throw new WireException($this->labels['demoMode']);
		if($this->config->demo) {
			if($this->wire('input')->urlSegmentStr != 'variations') {
				throw new WireException($this->labels['demoMode']);
			}
		}
		
		$this->modules->get("ProcessPageList");
	
		$this->rte = $this->input->get('rte') !== null && $this->input->get('rte') == "0" ? false : true;
		$id = (int) $this->input->get->id; 
		$editID = (int) $this->input->get->edit_page_id; 
		
		if($editID) {
			$this->wire('session')->set($this, 'edit_page_id', $editID); 
		} else {
			$editID = (int) $this->wire('session')->get($this, 'edit_page_id'); 
		}
		
		if($editID) {
			$this->editorPage = $this->wire('pages')->get($editID); 
			if(!$this->editorPage->editable()) {
				$this->editorPage = null;
				$this->wire('session')->remove($this, 'edit_page_id'); 
			}
		}
	
		$fieldName = $this->wire('sanitizer')->fieldName($this->wire('input')->get('field')); 
		if($fieldName) {
			$this->fieldName = $fieldName; // original
			if(strpos($fieldName, '_repeater') && preg_match('/_repeater\d+$/', $fieldName, $matches)) {
				$fieldName = str_replace($matches[0], '', $fieldName);
			} else if(strpos($fieldName, '_LPID')) {
				list($fieldName, $lpid) = explode('_LPID', $fieldName);
			}
			$this->field = $this->wire('fields')->get($fieldName); 
			if(!$this->field) throw new WireException("Unknown field $fieldName"); 
			if(!$this->field->type instanceof FieldtypeImage) throw new WireException("Field $fieldName is not an instance of FieldtypeImage"); 
		}
		
		// if no ID was specified, then retrieive ID from filename path, if it's there
		if($this->input->get->file && preg_match('{[/,]}', $this->input->get->file)) {
			
			if(preg_match('{(\d+)[/,][^/,]+\.(' . $this->extensions . ')$}iD', $this->input->get->file, $matches)) {
				// ..........ID.../,filename.ext
				// format: 123/filename.jpg OR 123,filename.jpg
				// also covers new pagefileSecure URLs
				$id = (int) $matches[1]; 
				
			} else if(preg_match('{(/[\d/]+)/([-_.a-z0-9]+)\.(' . $this->extensions . ')$}iD', $this->input->get->file, $matches)) {
				// .................ID........filename........ext
				// extended asset path format: 1/2/3/filename.jpg
				$id = PagefilesManager::dirToPageID($matches[1]); 

			} else if(preg_match('{^' . $this->wire('config')->urls->root . '([-_./a-zA-Z0-9]*/)' . $this->wire('config')->pagefileUrlPrefix . '[^/]+\.(' . $this->extensions . ')$}iD', $this->input->get->file, $matches)) {
				// .............................../....................path/to/page.........................-?....................filename..........ext
				// legacy pagefileSecure URL format: /path/to/page/filename.jpg
				// @todo: does this still need to be here or can it be dropped?
				$this->page = $this->wire('pages')->get('/' . $matches[1]);
				$id = $this->page->id;
			}
		}
		
		if(!$id) throw new WireException("No page specified");
		if(!$this->page) $this->page = $this->pages->get($id); 
		if(!$this->page) throw new WireException("No page specified"); 
		if(!$this->page->id) throw new WireException("Unknown page"); 
		if(!$this->editorPage) $this->editorPage = $this->page;
		
		// if $this->page is a repeater item (for example), $this->masterPage is page it lives on
		$p = null;
		if(wireInstanceOf($this->page, 'RepeaterPage')) {
			/** @var RepeaterPage $p */
			$p = $this->page;
			while(wireInstanceOf($p, 'RepeaterPage')) {
				$p = $p->getForPage();
			}
		}
		$this->masterPage = $p && $p->id ? $p : $this->page; 
		
		// note we use hasPermission('page-view') rather than viewable() here because
		// we want to allow pages without template files
		$user = $this->wire('user');
		if(!$user->hasPermission('page-view', $this->page)) {
			if($this->page->id === $user->id && $fieldName
				&& $this->wire('modules')->get('PagePermissions')->userFieldEditable($fieldName)) {
				// user editing allowed images field in their profile
			} else if(wireInstanceOf($this->page, 'RepeaterPage')) {
				if(!$this->masterPage->editable()) {
					throw new WireException($this->labels['noAccess']);
				}
			} else {
				throw new WireException($this->labels['noAccess']);
			}
		}

		if($this->input->get->winwidth) $this->maxImageWidth = ((int) $this->input->get->winwidth) - 70; 
		if($this->maxImageWidth < 400) $this->maxImageWidth = 400; 
		
		$this->hidpi = ((int) $this->wire('input')->get('hidpi')) ? true : false;
		$hidpi = $this->wire('input')->get('hidpi');
		if($hidpi === null && $this->hidpiDefault) $hidpi = true; 
		if(!$this->rte) $hidpi = false; // hidpi not applicable outside of RTE mode
		$this->hidpi = $hidpi ? true : false;
	
		if($this->rte) $this->caption = $this->wire('input')->get('caption') ? true : false; 
		
		// original used by InputfieldImage 
		$original = $this->wire('input')->get('original'); 
		if($original) {
			$original = $this->wire('sanitizer')->filename($original);
			if(is_file($this->page->filesManager()->path . $original)) {
				$this->original = $original;
			}
		}

		parent::init();
	}
	
	/**
	 * Return the Pageimage object being edited
	 * 
	 * @param bool $getVariation Returns the variation specified in the URL. Otherwise returns original (default). 
	 * @return Pageimage
	 * @throws WireException
	 * 
	 */
	public function getPageimage($getVariation = false) {

		$images = $this->getImages($this->page);
		$file = basename($this->input->get->file);
		$variationFilename = '';
		
		if(strpos($file, ',') === false) {
			// prepend ID if it's not there, needed for ajax in-editor resize
			$originalFilename = $file;
			$file = $this->page->id . ',' . $file; 
		} else {
			// alrelady has a "123," at beginning
			list($unused, $originalFilename) = explode(',', $file);
		}
		
		$originalFilename = $this->wire('sanitizer')->filename($originalFilename, false, 1024);
		
		// if requested file does not match one of our allowed extensions, abort
		if(!preg_match('/\.(' . $this->extensions . ')$/iD', $file, $matches)) throw new WireException("Unknown image file"); 

		// get the original, non resized version, if present
		// format:            w x h    crop       -suffix
		if(preg_match('/(\.(\d+)x(\d+)([a-z0-9]*)(-[-_.a-z0-9]+)?)\.' . $matches[1] . '$/', $file, $matches)) {
			// filename referenced in $_GET['file'] IS a variation
			// Follows format: original.600x400-suffix1-suffix2.ext
			$this->editWidth = (int) $matches[2];
			$this->editHeight = (int) $matches[3];
			$variationFilename = $originalFilename; 
			$originalFilename = str_replace($matches[1], '', $originalFilename); // remove dimensions and optional suffix
		} else {
			// filename referenced in $_GET['file'] is NOT a variation
			$getVariation = false; 
		}

		// update $file as sanitized version and with original filename only
		$file = "{$this->page->id},$originalFilename"; 
		
		// if requested file is not one that we have, abort
		if(!array_key_exists($file, $images)) throw new WireException("Cannot find image file '$originalFilename' on page: {$this->page->path}");

		// return original
		if(!$getVariation) return $images[$file];
		
		// get variation
		$original = $images[$file]; 
		$variationPathname = $original->pagefiles->path() . $variationFilename; 
		$pageimage = null;
		if(is_file($variationPathname)) $pageimage = $this->wire(new Pageimage($original->pagefiles, $variationPathname)); 
		if(!$pageimage) throw new WireException("Unrecognized variation file: $file"); 
		
		return $pageimage; 
	}

	/**
	 * Get all Pageimage objects on page
	 * 
	 * @param Page $page
	 * @param array|WireArray $fields
	 * @param int $level Recursion level (internal use)
	 * @return array
	 * 
	 */
	public function getImages(Page $page, $fields = array(), $level = 0) {
		
		$allImages = array();
		if(!$page->id) return $allImages; 
		
		$numImages = 0;
		$numImageFields = 0;
		$skipFields = $this->wire('input')->urlSegment1 ? array() : explode(' ', $this->skipFields);
	
		if(empty($fields)) {
			if($this->field) {
				$fields = array($this->field);
				$skipFields = array();
			} else {
				$fields = $page->fields;
			}
		}

		foreach($fields as $field) {

			if(in_array($field->name, $skipFields)) continue; 

			if(wireInstanceOf($field->type, 'FieldtypeRepeater')) {
				// get images that are possibly in a repeater
				$repeaterValue = $page->get($field->name); 
				if($repeaterValue instanceof Page) $repeaterValue = array($repeaterValue); 
				if($repeaterValue) foreach($repeaterValue as $p) {
					$images = $this->getImages($p, $p->fields, $level+1); 
					if(!count($images)) continue; 
					$allImages = array_merge($allImages, $images);
					$numImages += count($images); 
					$numImageFields++;
				}
				continue; 
			}

			if(!$field->type instanceof FieldtypeImage) continue; 
			$numImageFields++;
			$images = $page->getUnformatted($field->name); 
			if(!count($images)) continue; 

			foreach($images as $image) {
				$numImages++;
				$key = $page->id . ',' . $image->basename;  // page_id,basename for repeater support
				$allImages[$key] = $image; 
			}
		}

		if(!$level) {
			if(!$numImageFields) $this->message($this->_("There are no image fields on this page. Choose another page to select images from.")); // Message when page has no image fields
				else if(!$numImages) $this->message($this->_("There are no images present on this page. Upload an image, or select images from another page."));  // Message when page has no images
		}

		return $allImages; 
	}

	/**
	 * Get all editable image fields on the page
	 * 
	 * @param Page $page
	 * @param bool $excludeFullFields Exclude fields that are already full? (i.e. can't add more images to them)
	 * @return array of Field objects for image fields
	 * 
	 */
	public function getImageFields(Page $page, $excludeFullFields = true) {
		
		$skipFields = explode(' ', $this->skipFields);
		$imageFields = array();

		foreach($page->fields as $field) {
			if(!$field->type instanceof FieldtypeImage) continue;
			if(in_array($field->name, $skipFields)) continue;
			if(!$page->editable($field->name)) continue; 
			if($excludeFullFields && $field->maxFiles > 0) {
				if(count($page->get($field->name)) >= $field->maxFiles) continue; 
			}
			$imageFields[$field->name] = $field;
		}
		return $imageFields; 
	}

	/**
	 * Default execute: display list of images on page for selection
	 * 
	 * @return string
	 * @throws WireException
	 * 
	 */
	public function ___execute() {

		if($this->config->demo) throw new WireException("Sorry, image editing functions are disabled in demo mode"); 
		if(!$this->page) {
			$error = "No page provided";
			$this->error($error);
			return "<p>$error</p>";
		}

		if($this->input->get->file) return $this->executeEdit();
		
		$images = $this->getImages($this->page, $this->page->fields);

		$out = '';

		if(count($images)) {
			$winwidth = (int) $this->input->get->winwidth;
			$in = $this->wire('modules')->get('InputfieldImage'); 
			$in->adminThumbs = true;
			
			foreach($images as $image) {
				if($this->noThumbs) {
					$width = $image->width();
					$alt = $this->wire('sanitizer')->entities1($image->description); 
					if($width > $this->maxImageWidth) $width = $this->maxImageWidth;
					$img = "<img src='$image->URL' width='$width' alt=\"$alt\" />";
				} else {
					$image->set('_requireHeight', true); // recognized by InputfieldImage
					$info = $in->getAdminThumb($image); 
					$img = $info['markup'];
				}
				$out .= "\n\t<li><a href='./edit?file={$image->page->id},{$image->basename}" . 
					"&amp;modal=1&amp;id={$this->page->id}&amp;winwidth=$winwidth'>$img</a></li>";
			}

			$class = $this->noThumbs ? "" : " class='thumbs'";
			$out = "\n<ul id='select_images'$class>$out\n</ul>";

		}

		/** @var InputfieldForm $form */
		$form = $this->modules->get("InputfieldForm");
		$form->action = "./"; 
		$form->method = "get";

		/** @var InputfieldPageListSelect $field */
		$field = $this->modules->get("InputfieldPageListSelect"); 
		$field->label = $this->_("Images on Page:") . ' ' . $this->page->get("title") . " (" . $this->page->path . ")"; // Headline for page selection, precedes current page title/url
		$field->description = $this->_("If you would like to select images from another page, select the page below."); // Instruction on how to select another page
		$field->attr('id+name', 'page_id'); 
		$field->value = $this->page->id; 
		$field->parent_id = 0; 
		$field->collapsed = count($images) ? Inputfield::collapsedYes : Inputfield::collapsedNo;
		$field->required = true; 
		$form->append($field);
		
		// locate any image fields
		$imageFields = $this->getImageFields($this->page);
		if(count($imageFields)) {
			$imageFieldNames = implode(',', array_keys($imageFields)); 
			$btn = $this->modules->get('InputfieldButton');
			$uploadOnlyMode = "$this->page" === "$this->editorPage" ? 1 : 2;
			$btn->href = "../edit/?modal=1&id={$this->page->id}&fields=$imageFieldNames&uploadOnlyMode=$uploadOnlyMode";
			$btn->value = $this->_('Upload Image');
			$btn->addClass('upload pw-modal-button pw-modal-button-visible'); 
			$btn->icon = 'upload';
			$changes = $this->wire('input')->get('changes'); 
			if($changes) foreach(explode(',', $changes) as $name) {
				$name = $this->wire('sanitizer')->fieldName($name); 
				$field = $this->wire('fields')->get($name); 
				if(!$field) continue;
				$out .= "<script>refreshPageEditField('$name');</script>";
			}
		} else $btn = null;

		$out = $form->render() . $out;
		if($btn) $out .= $btn->render();
		
		return "<div id='ProcessPageEditImageSelect'>" . $out . "\n</div>";
	}
	
	/**
	 * Given a Pageimage, return the closest/last cropped version of it
	 * 
	 * This is so that resizes can use the highest quality version to resize from. 
	 * 
	 * @param Pageimage $image
	 * @return Pageimage
	 * 
	 */
	protected function getLastCrop(Pageimage $image) {
		$info = $image->isVariation($image->name, true);
		if(!$info || !empty($info['crop'])) {
			return $image;
		}
		$cropName = null;
		$parentInfo = $info;
		while(!empty($parentInfo['parent'])) {
			$parentInfo = $parentInfo['parent'];
			if(!empty($parentInfo['crop'])) {
				$cropName = $parentInfo['name'];
				break;
			}
		}
		
		$original = $image->getOriginal();
		if(!$original) return $image; 
		if(filemtime($original->filename) > filemtime($image->filename)) return $image; 
		
		// if no last crop found, return original
		if(!$cropName) return $original;
		
		$variations = $original->getVariations();
		
		// return last found crop
		$cropImage = $variations->get($cropName);
		if($cropImage && filemtime($cropImage->filename) > filemtime($image->filename)) return $image;
		return $cropImage ? $cropImage : $original; 	
	}

	/**
	 * Make a image edit URL
	 * 
	 * @param string $file
	 * @param array $parts Additional variables you want to add
	 * @return string
	 * 
	 */
	protected function makeEditURL($file, $parts = array()) {
		
		$file = basename($file); 
		$id = isset($parts['id']) ? (int) $parts['id'] : $this->page->id; 
		
		if(!isset($parts['modal'])) $parts['modal'] = 1;
		if(!isset($parts['edit_page_id']) && $this->editorPage) $parts['edit_page_id'] = $this->editorPage->id; 
		if(!isset($parts['hidpi'])) $parts['hidpi'] = (int) $this->hidpi; 
		if(!isset($parts['original'])) {
			if($this->original) {
				$parts['original'] = $this->original;
			} else {
				$parts['original'] = $file;
			}
		}
		if(!isset($parts['class']) && $this->wire('input')->get('class')) {
			$class = $this->wire('input')->get('class'); 
			if($class) {
				$validClasses = array($this->alignLeftClass, $this->alignCenterClass, $this->alignRightClass);
				$classes = array();
				foreach(explode(' ', $class) as $c) {
					if(in_array($c, $validClasses)) $classes[] = $c; 
				}
				if(count($classes)) $parts['class'] = urlencode(implode(' ' , $classes)); 
			}
		}
		if(!$this->rte) $parts['rte'] = '0';
		if($this->field) $parts['field'] = $this->fieldName; 
		
		if(!isset($parts['winwidth'])) {
			$winwidth = (int) $this->wire('input')->get('winwidth'); 
			if($winwidth) $parts['winwidth'] = $winwidth; 
		}
		
		if(!isset($parts['caption']) && $this->rte && $this->caption) {
			$parts['caption'] = 1; 
		}
		
		// @todo should we check for 'class' ?
		
		unset($parts['id'], $parts['file']); // in case they are set here
		
		$url = $this->wire('config')->urls->admin . "page/image/edit/?id=$id&file=$id,$file";
		
		foreach($parts as $key => $value) $url .= "&$key=$value";
		
		return $url;
	}

	/**
	 * Check if user has image edit permission and throw exception if they don't
	 *
	 * @param bool $throw Specify false if you only want this method to return a true|false rather than throw exception
	 * @throws WirePermissionException
	 * @return bool if the $throw argument was false
	 *
	 */
	public function checkImageEditPermission($throw = true) {
		if(!$this->rte && !$this->wire('user')->hasPermission('page-edit-images', $this->masterPage)) {
			if($throw) {
				throw new WirePermissionException($this->labels['noAccess']);
			} else {
				return false;
			}
		}
		return true;
	}

	/**
	 * Edit a selected image
	 * 
	 * Required GET variables:
	 * 	- file (string): URL of image, or preferably "123,basename.jpg" where 123 is ID. 
	 *  - id (int): ID of page images is from, optional if specified in 'file'.
	 *
	 * Optional GET variables:
	 * 	- edit_page_id (int): ID of the page being edited, where image will be placed.
	 * 	- width (int): Current width of image in the editor
	 * 	- height (int): Current height of image in the editor
	 * 	- winwidth (int): Current width of client-side window in pixels
	 * 	- id (int): ID of the page the image lives on (if omitted, we attempt to determine from 'file' path).
	 * 	- class (string): Class name to pass along to the editor (i.e. align_left, align_right, etc.)
	 * 	- hidpi (int): Whether hidpi mode is used or not (0=no, 1=yes)
	 * 	- link: (string): URL that image is linking to in the editor.
	 * 	- description: (string): Text for description "alt" tag. 
	 * 	- rte (int): Rich text editor mode (0=off, 1 or omit=on)
	 * 	- field (string): Name of field (for non-rte mode)
	 * 
	 * Optional GET variables that must come together as a group. When present, the image editor 
	 * will start in crop mode and show the original image with this portion selected for crop:
	 * 	- crop_x (int): Predefined crop left position
	 * 	- crop_y (int): Predefined crop top position
	 * 	- crop_w (int): Predefined crop width
	 * 	- crop_h (int): Predefined crop height
	 * 
	 * @return string
	 * @throws \Exception|WireException
	 * 
	 */
	public function ___executeEdit() {

		$this->checkImageEditPermission();

		if($this->wire('input')->post('submit_crop')) {
			$crop = $this->processCrop();
			$parts = $this->hidpi ? array('width' => $crop->hidpiWidth()) : array();
			$this->wire('session')->redirect($this->makeEditURL($crop->basename, $parts)); 
			return;
		} else if($this->wire('input')->post('submit_save_replace')) {
			return $this->processSave(true); 
		} else if($this->wire('input')->post('submit_save_copy')) {
			return $this->processSave(false);
		}
	
		$path = $this->wire('config')->urls->ProcessPageEditImageSelect; 
		$this->wire('config')->styles->add($path . 'cropper/cropper.min.css');
		$this->wire('config')->scripts->add($path . 'cropper/cropper.min.js'); 
	
		$labels =& $this->labels;
		$formClasses = array();
		
		$icons = array(
			'top' => 'long-arrow-up',
			'left' => 'long-arrow-left', 
			'width' => 'arrows-h', 
			'height' => 'arrows-v', 
		);
		foreach($icons as $key => $value) {
			$icons[$key] = "<i class='fa fa-fw fa-$value ui-priority-secondary'></i>";
		}

		$optionalClasses = array(
			$this->alignLeftClass => $labels['alignLeft'], 
			$this->alignRightClass => $labels['alignRight'], 
			$this->alignCenterClass => $labels['alignCenter'], 
		);

		$cropX = null; 
		$cropY = null; 
		$cropWidth = null; 
		$cropHeight = null;
		$isCropped = preg_match_all('/\.(\d+)x(\d+).*?-crop[xy](\d+)[xy](\d+)[-.]/', $this->wire('input')->get('file'), $matches); 
		
		if($isCropped) { // get last coordinates present
			$cropWidth = (int) array_pop($matches[1]);
			$cropHeight = (int) array_pop($matches[2]);
			$cropX = (int) array_pop($matches[3]); 
			$cropY = (int) array_pop($matches[4]); 
		}
		
		$isCroppable = !$isCropped;
		try {
			$original = $this->getPageimage(false);
		} catch(\Exception $e) {
			if($this->rte) {
				// when RTE mode, go to image selection again when invalid image
				$this->error($e->getMessage());
				$this->wire('session')->redirect("./?id={$this->page->id}");
				return;
			} else {
				throw $e; 
			}
		}
		
		if($isCropped) {
			$image = $this->getPageimage(true); 
			$recropURL = $this->makeEditURL($original->basename, array(
				'crop_x' => $cropX,
				'crop_y' => $cropY, 
				'crop_w' => $cropWidth, 
				'crop_h' => $cropHeight
			));
		} else {
			$recropURL = "";
			$image = $original;
		}
		
		if(!is_file($image->filename)) throw new WireException("Image file does not exist"); 
		
		if($this->field) {
			$pageimages = $this->page->get($this->field->name);
			if(!$pageimages || !$pageimages->get($original->name)) {
				throw new WireException("Please save the page before editing newly uploaded images."); 
			}
		}
		
		$basename = $image->basename;
		$fullname = $image->page->id . ',' . $basename;  // "123,basename.jpg"

		$originalWidth = $image->width();
		$originalHeight = $image->height();

		
		// attributes for #selected_image
		$attrs = array(
			'class' => '',
			'width' => 0,
			'height' => 0,
			'data-origwidth' => $originalWidth,
			'data-origheight' => $originalHeight,
			'data-nosize' => ($this->noSizeAttrs ? '1' : ''),
		);
		
		if(!$this->rte && $this->field) {
			$minWidth = $this->field->minWidth ? (int) $this->field->minWidth : 1;
			$minHeight = $this->field->minHeight ? (int) $this->field->minHeight : 1;
		} else {
			$minWidth = 1; 
			$minHeight = 1;
		}
		
		// bundle in crop information to image attributes, if specified
		if($this->wire('input')->get('crop_x')) {
			$attrs['data-crop'] =
				((int) $this->wire('input')->get('crop_x')) . ',' .
				((int) $this->wire('input')->get('crop_y')) . ',' .
				((int) $this->wire('input')->get('crop_w')) . ',' .
				((int) $this->wire('input')->get('crop_h'));
		}

		// determine width/height we will display image at in editor, so that it fits
		$width = (int) $this->input->get('width'); 
		$height = (int) $this->input->get('height'); 
		if(!$width) $width = $this->editWidth; 
		if(!$width) $width = '';
		if(!$height) $height = $this->editHeight; 
		if(!$height) $height = '';
		
		// if width not specified in edit URL and image exceeds the window width, then scale to fit window
		if(!$width) {
			$width = $image->width; 
		}
		$attrs['width'] = $width;
		$attrs['data-fit'] = '1';
		$this->wire('processBrowserTitle', $basename); 

		// if they aren't already working with a resized image, and it's being scaled down,
		// then add the 'resized' class to ensure that our RTE 'pwimage' plugin knows to perform the resize
		// by checking for the 'resized' classname on the image
		if(basename($this->input->get->file) == $fullname && $originalWidth > $width) $attrs['class'] .= " resized";
	
		// if hidpi specified keep it checed
		$hidpiChecked = $this->hidpi ? " checked='checked'" : "";
		$captionChecked = $this->caption ? " checked='checked'" : "";
	
		// alignment class options
		$classOptions = '';
		foreach($optionalClasses as $class => $label) {
			$labelKey = array_search($label, $labels); 
			$selected = strpos($this->input->get('class'), $class) !== false ? " selected='selected'" : '';
			if($selected) $attrs['class'] .= " $class";
			$classOptions .= "<option$selected data-label='$labelKey' value='$class'>$label</option>";
		}
	
		// convert $attrs to an attribute string for placement in #selected_image markup
		$attrStr = ''; 
		foreach($attrs as $key => $value) {
			if(!$value && $key != 'data-nosize') continue; // skip most empty attributes
			$attrStr .= "$key='" . trim($value) . "' ";
		}

		// prepare description (alt)
		$description = isset($_GET['description']) ? $this->input->get('description') : ''; // $image->description;
		if(strlen($description) > 8192) $description = substr($description, 0, 8192);
		$description = $this->wire('sanitizer')->entities($description); 
		
		// if dealing with a variation size or crop provide the option to link to the original (larger)
		$linkOriginalChecked = '';
		if($image->name != $original->name || $this->wire('input')->get('link')) {
			if($image->name != $original->name) $formClasses[] = 'not-original';
			$imgURL = str_replace($this->wire('config')->urls->root, '/', $original->url);
			$imgLinkURL = trim($this->wire('input')->get('link'));
			if($imgLinkURL) {
				if($imgLinkURL === "1") $imgLinkURL = $imgURL; // if in toggle mode, substitute URL
				// determine if 'link to original' checkbox should be checked 
				$test = substr($imgLinkURL, -1 * strlen($imgURL));
				$linkOriginalChecked = $test == $imgURL ? " checked='checked' data-was-checked='1'" : "";
				unset($imgLinkURL); 
			}
		} else {
			$formClasses[] = 'original';
		}
		
		$resizeYesChecked = $this->rte ? " checked='checked'" : "";
		$resizeNoChecked = !$this->rte ? " checked='checked'" : "";

		if($this->field && $this->field->maxFiles == 1) $formClasses[] = 'maxfiles1';
			
		// form attributes
		$formClasses[] = $isCroppable ? 'croppable' : 'not-croppable';
		$formClasses[] = $this->rte ? 'rte' : 'not-rte';
		$formClass = implode(' ', $formClasses); 
		$formActionURL = $this->makeEditURL($basename);
		$originalDimension = $originalWidth . 'x' . $originalHeight; 
		$imageURL = $this->rte ? $image->url : $image->URL;

		// form header and inputs
		// @todo move all this markup to a separate template file
		$out =	
			"<form id='selected_image_settings' class='$formClass' method='post' action='$formActionURL'>" .
			"<small class='ui-helper-clearfix'>" . 
			"<p id='wrap_info'>" .
				"<select id='selected_image_class' name='class'>" .
					"<option value=''>" . $this->_('No Alignment') . "</option>" .
					$classOptions .
				"</select>" .
				"<span id='action_icons' class='hide_when_crop hide_when_processing'>" .
					"<span title='$labels[resize]' id='resize_action'><i class='fa fa-fw fa-arrows-alt'></i></span>" .
					"<span title='$labels[crop]' data-recrop='$recropURL' id='crop_action'><i class='fa fa-fw fa-crop'></i></span>" .
					//"<span title='$labels[rotateLeft]' id='rotate_left_action'><i class='fa fa-fw fa-rotate-left'></i></span>" . 
					//"<span title='$labels[rotateRight]' id='rotate_right_action'><i class='fa fa-fw fa-rotate-right'></i></span>" .
					//"<span title='$labels[flipHorizontal]' id='flip_horizontal_action'><i class='fa fa-fw fa-exchange'></i></span>" .
					//"<span title='$labels[flipVertical]' id='flip_vertical_action'><i class='fa fa-fw fa-exchange fa-rotate-90'></i></span>" .
					"<span title='$labels[max] ({$originalWidth}x{$originalHeight})' id='max_action'><i class='fa fa-fw fa-expand'></i></span>" .
					"<span title='$labels[min]' id='min_action'><i class='fa fa-fw fa-compress'></i></span>" .
					"<span title='$labels[alignLeft]' data-label='alignLeft' class='align_action show_when_rte' id='align_left_action'><i class='fa fa-fw fa-align-left'></i></span>" .
					"<span title='$labels[alignCenter]' data-label='alignCenter' class='align_action show_when_rte' id='align_center_action'><i class='fa fa-fw fa-align-center'></i></span>" .
					"<span title='$labels[alignRight]' data-label='alignRight' class='align_action show_when_rte' id='align_right_action'><i class='fa fa-fw fa-align-right'></i></span>" .
					"<span title='$labels[description]' id='description_action' class='show_when_rte'><i class='fa fa-fw fa-quote-left'></i></span>" .
				"</span>" .
				"<span id='selected_image_pixels' class='hide_when_crop'>" . 
					"<label><input title='$labels[width]' name='width' id='input_width' class='input_pixels' " .
						"type='number' size='6' step='1' data-min='$minWidth' data-max='$originalWidth' value='$width' /></label>" .
					"&nbsp;<i class='fa fa-times'></i>&nbsp;" . 
					"<label><input title='$labels[height]' name='height' id='input_height' class='input_pixels' " . 
						"type='number' size='6' step='1' data-min='$minHeight' data-max='$originalHeight' value='$height' /></label>" . 
				"</span> " .
				"<span id='selected_image_checkboxes' class='hide_when_crop'>" . 
					"<span id='wrap_link_original' class='show_when_rte'>" .
						"<label class='checkbox'>" .
							"<input title='$labels[linkOriginal]' type='checkbox'$linkOriginalChecked " .
								"name='selected_image_link' id='selected_image_link' value='{$original->url}' /> " .
							"<i class='fa fa-link ui-priority-secondary'></i>&nbsp;" .
							"{$original->width}x{$original->height}" .
						"</label>" .
					"</span>" . 
					"<span id='wrap_caption' class='show_when_rte'>" .
						"<label class='checkbox'>" .
							"<input$captionChecked id='selected_image_caption' type='checkbox' value='1' title='$labels[captionTip]' />&nbsp;$labels[caption]" .
						"</label>" .
					"</span>" . 
					"<span id='wrap_hidpi' class='show_when_rte'>" .
						"<label class='checkbox'>" . 
							"<input$hidpiChecked id='selected_image_hidpi' type='checkbox' value='1' />&nbsp;$labels[hidpi]" . 
						"</label>" .
					"</span>" .
				"</span>" . // selected_image_checkboxes
				"<span id='selected_image_resize' class='hide_when_rte hide_when_crop hide_when_processing'>" .
					"<i class='fa fa-angle-left ui-priority-secondary'></i>&nbsp;" .
					"$labels[useResize]&nbsp;&nbsp;" .
					"<label class='checkbox'><input$resizeYesChecked id='selected_image_resize_yes' name='use_resize' type='radio' value='1' />&nbsp;$labels[yes]</label>&nbsp;&nbsp;" .
					"<label class='checkbox'><input$resizeNoChecked id='selected_image_resize_no' name='use_resize' type='radio' value='0' />&nbsp;" .
					sprintf($labels['noUse'], $originalDimension) . "</label>" .
				"</span>" . 
				"<button type='button' class='ui-button ui-state-active show_when_processing' id='button_saving'>" .
					"<span class='ui-button-text'><i class='fa fa-spin fa-spinner'></i> $labels[saving]</span>" .
				"</button> " .
				"<span class='show_when_crop'>" .
					"<button type='submit' class='ui-button ui-state-default hide_when_processing' id='button_crop' name='submit_crop' value='1'>" .
						"<span class='ui-button-text'><i class='fa fa-crop'></i> $labels[saveCrop]</span>" .
					"</button> " .
					"<button type='button' class='hide_when_processing ui-button ui-state-default ui-priority-secondary' id='button_cancel_crop'>" .
						"<span class='ui-button-text'><i class='fa fa-times-circle'></i> $labels[cancel]</span>" .
					"</button> &nbsp; " . 
					"<span id='crop_coordinates'>" . 
						"<label>$labels[left]&nbsp;<input name='crop_x' id='crop_x' class='input_pixels' type='number' size='6' step='1' min='0' /></label>&nbsp; " .
						"<label>$labels[top]&nbsp;<input name='crop_y' id='crop_y' class='input_pixels' type='number' size='6' step='1' min='0' /></label>&nbsp; " .
						"<label>$labels[width]&nbsp;<input name='crop_w' id='crop_w' class='input_pixels' type='number' size='6' step='1' min='0' /></label>&nbsp; " .
						"<label>$labels[height]&nbsp;<input name='crop_h' id='crop_h' class='input_pixels' type='number' size='6' step='1' min='0' /></label>&nbsp; " .
					"</span>" . 
				"</span>" .
				"<button type='button' id='loading_button' class='ui-button ui-state-default'>" .
					"<span class='ui-button-text'>" . $this->_('Loading...')  ."</span>" .
				"</button>" . 
			"</p>" . 
			"<p id='wrap_description' class='show_when_rte'>" . 
				"<label for='selected_image_description'>$labels[description]</label>" . 
				"<input type='text' name='selected_image_description' " . 
					"id='selected_image_description' title='$description' class='InputfieldNoFocus' value=\"$description\" />" . 
			"</p>" . 
			"</small>" . 
			//"<p class='notes hide_when_rte'>Please note: this feature is still in development.</p>" . 
			"<figure id='selected_image_container'>" . 
				"<img id='selected_image' $attrStr alt='' " . 
					"data-idname='{$image->page->id},{$image->basename}' src='{$imageURL}' />" . 
				"<span id='resize_tips'>" .
					"<span class='resize_tip' id='resize_tip_nw'><i class='fa fa-fw fa-expand fa-flip-horizontal'></i></span>" .
					"<span class='resize_tip' id='resize_tip_n'><i class='fa fa-fw fa-arrows-v'></i></span>" .
					"<span class='resize_tip' id='resize_tip_ne'><i class='fa fa-fw fa-expand'></i></span>" .
					"<span class='resize_tip' id='resize_tip_e'><i class='fa fa-fw fa-arrows-h'></i></span>" .
					"<span class='resize_tip' id='resize_tip_se'><i class='fa fa-fw fa-expand fa-flip-horizontal'></i></span>" .
					"<span class='resize_tip' id='resize_tip_s'><i class='fa fa-fw fa-arrows-v'></i></span>" .
					"<span class='resize_tip' id='resize_tip_sw'><i class='fa fa-fw fa-expand'></i></span>" .
					"<span class='resize_tip' id='resize_tip_w'><i class='fa fa-fw fa-arrows-h'></i></span>" . 
				"</span>" .
				"<figcaption id='caption_preview' class='show_when_rte'>" . 
					"<a class='tooltip' title='$labels[captionTip]'>$labels[captionText]</a>" . 
				"</figcaption>" . 
			"</figure>" .
			"<p id='latin'>" . 
				"<span id='latin-fade'></span>" . $this->getLatin(2) . 
			"</p>" . 
			"<span id='selected_image_filename'>$basename</span>" .
			"<input type='hidden' name='page_id' id='page_id' value='{$this->page->id}' />" .
			"<input type='hidden' name='selected_image_rotate' id='selected_image_rotate' value='0' />" .
			"<p id='non_rte_dialog_buttons' class='hide_when_rte'>" . 
				(!in_array('maxfiles1', $formClasses) ? (
					"<button type='submit' name='submit_save_copy' value='1' class='submit_save_copy ui-button ui-state-default hide_when_maxfiles1'>" .
						"<span class='ui-button-text'><i class='fa fa-fw fa-paste'></i> $labels[saveCopy]</span>" .
					"</button>" 
					) : "") . 
				"<button type='submit' name='submit_save_replace' value='1' class='submit_save_replace ui-button ui-state-default'>" .
					"<span class='ui-button-text'><i class='fa fa-fw fa-cut'></i> $labels[saveReplace]</span>" .
				"</button>" .
				"<button type='button' id='non_rte_cancel' class='ui-button ui-state-default ui-priority-secondary'>" . 
					"<span class='ui-button-text'><i class='fa fa-fw fa-times-circle'></i> $labels[cancel]</span>" . 
				"</button>" . 
			"</p>"  .
			"</form>";
		
		// if($this->wire('config')->debug) $out .= "<p class='detail'>$basename</p>";
		
		return $out; 
		
	}
	
	protected function getLatin($n = 1) {
		$latin = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. In malesuada magna ex. Donec sed consectetur felis, 
			ac molestie massa. Duis fermentum rutrum vehicula. Praesent consequat efficitur dolor quis vestibulum.
			Sed vulputate, nisi et efficitur semper, erat odio scelerisque massa, nec convallis leo ipsum ac eros. Donec a 
			augue eleifend, pellentesque sapien a, tristique orci. Sed ac leo vel libero iaculis facilisis in at mauris.
			Etiam vitae mi cursus nisl gravida vulputate. Nam in magna risus. Nullam vel suscipit lacus. Ut sit amet malesuada 
			massa. Etiam rhoncus, tellus vel porta dignissim, elit massa cursus tortor, vitae fermentum dolor augue ac tortor.
			Aliquam eleifend mi at dictum pulvinar. Sed bibendum dolor non mi placerat, euismod euismod est commodo.
			Vivamus ultrices orci arcu, sed pulvinar tellus molestie quis. Quisque pharetra velit a metus mattis, vitae 
			fermentum orci hendrerit. Fusce id libero ac urna ultrices consequat. Donec lorem lacus, dignissim vitae faucibus 
			eget, tincidunt sit amet felis. Aenean tempor quis sapien vitae euismod. Donec mattis mi at sem egestas interdum.
			Integer sed diam finibus, volutpat lectus ac, ornare dui. Suspendisse potenti. Suspendisse sagittis interdum 
			elit sodales commodo. Maecenas porttitor mi sit amet velit porta, sit amet eleifend massa malesuada. Phasellus 
			luctus nibh id sem venenatis, dictum fermentum turpis hendrerit. Nam tempor, ex eu sagittis gravida, mi lorem 
			maximus ex, aliquam commodo nunc leo et lacus. Nunc vel faucibus lacus, non pellentesque elit. Nulla non rutrum 
			magna, nec porta augue. ";
		$out = '';
		for($c = 1; $c <= $n; $c++) $out .= $latin;
		return $out; 
	}
	
	protected function processCrop() {
	
		$post = $this->wire('input')->post; 
		
		$cropX = (int) $post->crop_x;
		$cropY = (int) $post->crop_y; 
		$cropW = (int) $post->crop_w;
		$cropH = (int) $post->crop_h; 
		
		$image = $this->getPageimage();
		if(!$image) throw new WireException("Unable to load image");

		
		$suffix = $this->rte ? array('is') : array();
		$options = array('cleanFilename' => true, 'suffix' => $suffix);
		$crop = $image->crop($cropX, $cropY, $cropW, $cropH, $options); 
		
		return $crop; 
	}
	
	protected function processSave($replace = false) {
		
		if(!$this->field) throw new WireException("Field is not defined"); 
		if(!$this->original) throw new WireException("Original is not set");
		if($this->page->hasStatus(Page::statusLocked)) throw new WireException("Page is locked for edits"); 
		
		$image = $this->getPageimage(false);
		if($this->input->get('file') != "$this->page,$image->name") $image = $this->getPageimage(true); 
		$width = (int) $this->wire('input')->post('width');
		if(!$width) throw new WireException("Width not specified"); 

		$rebuildVariations = preg_match('/-cropx\d+y\d+/', $image->name); 
		$useResize = ((int) $this->wire('input')->post('use_resize')) == 1;
		
		// image2 = resized version
		if($useResize) {
			$image2 = $image->width($width);
			if(!$image2 || !$image2->width) throw new WireException('Unable to complete resize');
		} else {
			$image2 = $image; 
		}
		
		$pageimages = $this->page->getUnformatted($this->field->name);
		$path = $pageimages->path();
		$fileID = '';
		$isNew = 0;
		$headline = '';
		$body = '';

		if($replace) {
			// replace original image
			
			if($this->original && $this->original != $image2->basename() && $original = $image->pagefiles->get($this->original)) {
				$fileID = 'file_' . $original->hash; 
				
				if($rebuildVariations && $this->field->adminThumbs) {
					// remove original thumbnail
					$inputfield = $this->field->getInputfield($this->page);
					if($inputfield) {
						$thumb = $inputfield->getAdminThumb($original);
						$thumb = $thumb['thumb'];
						if($thumb->url != $original->url) {
							// there is a thumbnail, distinct from the original image
							unlink($thumb->filename);
						}
					}
				}
				
				// replace original image
				$target = $path . $this->original;
				unlink($target); 
				rename($image2->filename(), $target);
				
				$this->wire('pages')->uncacheAll();	
				$page = $this->wire('pages')->get($this->page->id); 
				$value = $page->getUnformatted($this->field->name); 
				
				if($rebuildVariations) {
					$finalImage = $value->get($this->original);
					$variationInfo = $finalImage->rebuildVariations(0, array('is', 'hidpi'));
					foreach($variationInfo as $type => $files) {
						if(!count($files)) continue;
						$body .= "<h3>" . ucfirst($type) . "</h3>";
						$body .= "<p>" . implode('<br />', $files) . "</p>";
						// $this->wire('log')->save('images', "$type: [ " . implode(' ], [ ', $files) . ' ]');
					}
				} else {
					$body .= "<p>" . $this->_('No changes necessary to image variations.') . "</p>";
				}
				
				$headline = $this->labels['updating'];
				
			} else {
				$headline = $this->_('No changes made'); 
			}

		} else {
			// save a new copy
			
			$n = 0;
			do {
				$n++;
				$basename = basename($this->original); 
				preg_match('/^([^.]+)(\..+)$/', $basename, $matches);
				$basename = $matches[1];
				$aftername = $matches[2];
				if(strpos($basename, '-v')) $basename = preg_replace('/-v[0-9]+$/', '', $basename);
				$basename .= "-v$n";
				$basename .= $aftername;
				$pathname = $path . $basename;
			} while(is_file($pathname));
			
			if(!$useResize && !$rebuildVariations) {
				$this->wire('files')->copy($image2->filename(), $pathname);
			} else {
				rename($image2->filename(), $pathname);
			}
			$pageimages->add($pathname);
			$pageimage = $pageimages->last();
			$pageimage->isTemp(true);
			$this->page->save($this->field->name);
			$fileID = "file_$pageimage->hash";
			$isNew = 1; 
			$headline = $this->labels['updating'];
		}
	
		$out = "
			<h2><i class='fa fa-spin fa-spinner fa-2x fa-fw ui-priority-secondary'></i> $headline</h2>
			$body
			<script>
			$(document).ready(function() { setupProcessSave('$this->fieldName', '$fileID', $isNew); });	
			</script>
			";
		
		return $out; 
	}

	/**
	 * Resize image and output basic markup with information about the resize
	 * 
	 * The output is intended to be read by the rich text editor for insertion into the text. 
	 * 
	 * Required GET variables: 
	 * 	- file (string): URL of image
	 * 	- width (int): Target resize width
	 * 	- edit_page_id (int): ID of the page being edited, where image will be placed. 
	 * 
	 * Optional GET variables:
	 * 	- id (int): ID of the page the image lives on (if omitted, we attempt to determine from 'file' path). 
	 * 	- class (string): Class name to pass along to the editor (i.e. align_left, align_right, etc.) 
	 * 	- hidpi (int): Whether hidpi mode is used or not (0=no, 1=yes)
	 * 	- json (int): Specify 1 to provide output in JSON (if not specified, output is a paragraph of markup)
	 * 
	 * @return string
	 * @throws WireException
	 * 
	 */
	public function ___executeResize() {

		$this->checkImageEditPermission();

		$width = (int) $this->input->get('width'); 
		$class = $this->sanitizer->name($this->input->get->class);
		$hidpi = $this->hidpi;
		$json = (int) $this->input->get('json'); // 1 or 0 (for json output mode on or off)
		$rotate = (int) $this->wire('input')->get('rotate');
		$flip = $this->wire('input')->get('flip'); 
		if($flip != 'v' && $flip != 'h') $flip = '';
		
		if(strpos($class, 'hidpi') !== false) {
			if(!$hidpi) $hidpi = true; 
		} else {
			if($hidpi) $class = trim("$class hidpi");
		}
		
		$image = $this->getPageimage(true);
		if(strpos($image->basename, '-cropx') !== false) $image = $this->getLastCrop($image); 
		
		if(	(!$hidpi && $width < $image->width) || 
			($hidpi && $width < $image->hidpiWidth()) ||
			$flip || $rotate) {
			
			$suffix = array('is');  // is=image select
			if($this->editorPage && $this->editorPage->id != $this->page->id) $suffix[] = "pid$this->editorPage"; // identify page that is using the variation
			$options = array(
				'suffix' => $suffix,
				'hidpi' => $hidpi,
			);
			if($rotate) $options['rotate'] = $rotate; 
			if($flip) $options['flip'] = $flip;
			//$this->log(print_r($options, true)); 
			$resized = $image->width($width, $options); 
			$height = $resized->height();
		} else {
			$width = $image->width; 
			$height = $image->height; 
			$resized = $image; 
		}
	
		if($hidpi) {
			$width = $resized->hidpiWidth();
			$height = $resized->hidpiHeight();
			$hidpiChecked = " checked='checked'";
		} else {
			$hidpiChecked = '';
		}
		
		$nosize = $this->noSizeAttrs ? 1 : '';
		
		if($json) {
			
			$data = array(
				'src' => $resized->url,
				'name' => $image->basename, 
				'width' => $width,
				'height' => $height, 
				'class' => $class, 
				'page' => $image->page->id,
				'hidpi' => (bool) $hidpi,
				'nosize' => (bool) $nosize, 
			);
			
			header("Content-Type: application/json"); 
			$out = json_encode($data); 
			
		} else {
			// note IE8 won't properly read the width/height attrs via ajax
			// so we provide the width/height in separate fields

			$out =
				"<p>" .
				"<span id='selected_image_width'>$width</span>x" .
				"<span id='selected_image_height'>$height</span> " .
				"<label><input type='checkbox' id='selected_image_hidpi'$hidpiChecked />hidpi</label><br />" .
				"<img " . 
					"id='selected_image' " . 
					"class='$class' " . 
					"data-idname='{$image->page->id},{$image->basename}' " . 
					"data-nosize='$nosize' " . 
					"src='{$resized->url}' " . 
					"width='$width' " . 
					"height='$height' " .
				"/>" . 
				"</p>";
		}
		//$this->log($out); 

		return $out; 
	}

	/**
	 * Show all variations for the image provided in GET var 'file'
	 * 
	 */
	public function ___executeVariations() {
		
		$pageimage = $this->getPageimage();
		
		if(!$this->page || !$pageimage) throw new WireException("No file provided"); 
		if(!$this->masterPage->editable()) throw new WireException($this->labels['noAccess']);
		$hasEditPermission = $this->wire('user')->hasPermission('page-edit-images', $this->masterPage);
		
		$variations = $pageimage->getVariations(array('info' => true));
		$cnt = count($variations);
		
		$delete = $this->wire('input')->post('delete');
		if(is_array($delete) && count($delete) && $hasEditPermission) {
			foreach($delete as $name) {
				if(!isset($variations[$name])) continue; 
				$info = $variations[$name]; 
				if(is_file($info['path']) && unlink($info['path'])) {
					$this->message($this->_('Deleted image variation') . " - $info[url]"); 
					unset($variations[$name]); 
				} else {
					$this->error($this->_('Error deleting image variation') . " - $info[url]"); 
				}
			}
		}
		
		$headline = sprintf(
				$this->_n('%1$d variation for image %2$s', '%1$d variations for image %2$s', $cnt), 
				//$cnt, "<a class='preview' href='$pageimage->URL'>$pageimage->basename</a>"
				$cnt, $pageimage->basename
			);
		$this->headline($headline); 
		
		if(!$cnt) return "<h2>$headline</h2>";
		
		$adminThumbOptions = $this->wire('config')->adminThumbOptions;
		
		$table = $this->wire('modules')->get('MarkupAdminDataTable'); 
		$table->setEncodeEntities(false);
		$table->headerRow(array(
			'#', 
			$this->_x('Image', 'th'), 
			$this->_x('File', 'th'),
			$this->_x('Size', 'th'),
			$this->_x('Modified', 'th'),  
			$this->_x('Notes', 'th'),
			($hasEditPermission ? "<label><input type='checkbox' id='delete_all' value='1' /></label>" : "&nbsp;")
		));
		
		$cnt = 0;
		
		foreach($variations as $name => $info) {
			
			$notes = array();
			if(in_array('is', $info['suffix'])) $notes[] = $this->_x('Created for placement in textarea', 'notes'); 
			if(in_array('hidpi', $info['suffix'])) $notes[] = $this->_x('HiDPI/Retina', 'notes'); 
			if(!count($notes) && $info['width'] == $adminThumbOptions['width'] && $info['height'] == $adminThumbOptions['height']) {
				$notes[] = $this->_x('Auto-generated admin thumbnail', 'notes'); 
			}
			if(!count($notes)) $notes[] = $this->_x('API-generated variation', 'notes');
			if($info['crop']) $notes[] = $this->_x('Cropped version', 'notes');
		
			// identify pid suffixes
			foreach($info['suffix'] as $suffix) {
				if(strpos($suffix, 'pid') === 0) {
					$suffix = ltrim($suffix, 'pid'); 
					if(ctype_digit($suffix)) $refpage = $this->wire('pages')->get((int) $suffix); 
					if($refpage->id && $this->wire('user')->hasPermission('page-view', $refpage)) {
						$notes[] = $this->_x('Inserted from page:', 'notes') . " <a target='_blank' href='$refpage->url'>$refpage->path</a>";
					}
				}
			}
		
			$width = (int) $info['width'];
			$height = (int) $info['height'];
			if(!$width || !$height) list($width, $height) = getimagesize($info['path']);
			$dimensions = $width . 'x' . $height; 
			if(in_array('hidpi', $info['suffix'])) $dimensions .= " ($info[hidpiWidth]x$info[hidpiHeight] {$this->labels['hidpi']})";
			$filesize = filesize($info['path']); 
			$filesizeStr = wireBytesStr($filesize);
			$mtime = filemtime($info['path']); 
			$modified = date('Y-m-d H:i:s', $mtime); 
			$url = "$info[url]?nc=$mtime";
			
			$table->row(array(
				++$cnt, 
				"<a class='preview' href='$url'><img src='$url' alt='$name' style='max-width: 100px;' /></a>",
				"<a class='preview' href='$url'>$name</a><br /><span class='detail'>{$width}x{$height}</span>", 
				"<span style='display: none;'>$filesize</span>$filesizeStr", 
				$modified, 
				implode('<br />', $notes),
				($hasEditPermission ? "<label><input type='checkbox' class='delete' name='delete[]' value='$name' /> &nbsp;</label>" : "&nbsp;")
			));
		}
		
		$varcnt = $this->wire('sanitizer')->entities($this->wire('input')->get('varcnt')); 

		$form = $this->wire('modules')->get('InputfieldForm');
		$form->attr('id', 'ImageVariations'); 
		$form->action = "./?id={$this->page->id}&file=$pageimage->basename&varcnt=$varcnt";
		$form->prependMarkup = $table->render();

		if($hasEditPermission) {
			$submit = $this->wire('modules')->get('InputfieldSubmit');
			$submit->attr('value', $this->_('Delete Checked'));
			$submit->addClass('delete-checked');
			$submit->icon = 'trash';
			$form->add($submit);
		}
		
		$button = $this->wire('modules')->get('InputfieldButton'); 
		$button->attr('value', $this->_('Close')); 
		$button->addClass('pw-modal-cancel'); 
		$button->icon = 'times-circle';
		$form->add($button); 
		
		$hidden = $this->wire('modules')->get('InputfieldHidden');
		$hidden->attr('id+name', 'varcnt_id'); 
		$hidden->attr('value', $varcnt); 
		$hidden->attr('data-cnt', count($variations)); 
		$form->add($hidden); 
		
		$this->wire('modules')->get('JqueryMagnific'); 
		
		$out = $form->render(); 
		
		if($this->wire('config')->demo) {
			$out = "<p class='detail'>Note: " . $this->labels['demoMode'] . "</p>" . $out;
		} else {
			$out = "<br />" . $out;
		}
		
		return $out; 
		
	}

	/**
	 * Module configuration
	 * 
	 * @param array $data
	 * @return InputfieldWrapper
	 * 
	 */
	public function getModuleConfigInputfields(array $data) {
		$inputfields = $this->wire(new InputfieldWrapper());
		$data = array_merge(self::$defaultConfig, $data); 
		
		$f = $this->wire('modules')->get('InputfieldCheckbox'); 
		$f->attr('name', 'hidpiDefault'); 
		$f->label = $this->_('HiDPI/Retina checkbox default checked?'); 
		$f->description = $this->_('Check this box to have the HiDPI/Retina checkbox checked by default for newly inserted images.'); 
		if(!empty($data['hidpiDefault'])) $f->attr('checked', 'checked'); 
		$f->columnWidth = 50;
		$inputfields->add($f); 
		
		$f = $this->wire('modules')->get('InputfieldCheckbox'); 
		$f->attr('name', 'noThumbs'); 
		$f->label = $this->_('Do not generate thumbnail images'); 
		$f->description = $this->_('When checked, image selection will use full-size images rather than thumbnail images.'); 
		if(!empty($data['noThumbs'])) $f->attr('checked', 'checked');
		$f->columnWidth = 50;
		$inputfields->add($f); 

		$f = $this->wire('modules')->get('InputfieldText'); 
		$f->attr('name', 'skipFields'); 
		$f->attr('value', isset($data['skipFields']) ? $data['skipFields'] : ''); 
		$f->label = $this->_('Field names to skip for selection'); 
		$f->description = $this->_('Enter the names of any image fields (separated by a space) that you do not want to allow for selection with this module.'); 
		$inputfields->add($f); 

		$f = $this->wire('modules')->get('InputfieldCheckbox');
		$f->attr('name', 'noSizeAttrs'); 
		$f->attr('value', 1);
		if(!empty($data['noSizeAttrs'])) $f->attr('checked', 'checked');
		$f->label = $this->_('Skip width attributes on image tags?', __FILE__); // noSizeAttr label
		$f->description = $this->_('By default, this module will include width attributes in the <img> tag. If you are using responsive images, you might want to disable this behavior. Check the box to disable width and height attributes.', __FILE__); // noSizeAttr description
		$f->notes = $this->_('We do not recommend checking this box as it will interfere with some features (like use of HiDPI/retina images).'); 
		$inputfields->add($f);

		$notes = $this->_('Recommended value:') . ' ';
		$f = $this->wire('modules')->get('InputfieldText'); 
		$f->attr('name', 'alignLeftClass');
		$f->attr('value', $data['alignLeftClass']); 
		$f->label = $this->_('Align Image Left Class'); 
		$f->notes = $notes . '**align_left**';
		$f->columnWidth = 33; 
		$inputfields->add($f);
		
		$f = $this->wire('modules')->get('InputfieldText'); 
		$f->attr('name', 'alignCenterClass');
		$f->attr('value', $data['alignCenterClass']); 
		$f->label = $this->_('Align Image Center Class');
		$f->notes = $notes . '**align_center**';
		$f->columnWidth = 34; 
		$inputfields->add($f);
		
		$f = $this->wire('modules')->get('InputfieldText');
		$f->attr('name', 'alignRightClass');
		$f->attr('value', $data['alignRightClass']);
		$f->label = $this->_('Align Image Right Class');
		$f->notes = $notes . '**align_right**';
		$f->columnWidth = 33;
		$inputfields->add($f);

		return $inputfields;
	}

}

