/**
 * dropzone-binding.js - Wrapper for Dropzone.js which manages potentially many file upload 
 * form elements on the current page and abstracts the form Dropzone element setup.
 */

// Disable autoDiscover; dropzones are manually setup
Dropzone.autoDiscover = false;

// See `firstDropzone.on('successmultiple', ...)` below.
window.onpopstate = function(event) {
	if (event.state.hasOwnProperty('reloadUrl')) {
		// Refresh the page if specified
		window.location.href = event.state.reloadUrl;
	}
};

/** DropzoneUtil **/
window.DropzoneUtil = {};

DropzoneUtil.activeDropzones = {};
// Stores data about the current submission being processed.
// This will either be 'anon' if a submitted button couldn't be found, or the submit button jQuery itself.
DropzoneUtil.submitRequestInProgress = null;
// Stores a jQuery instance of the last button pressed and its form, regardless if a submission is
// already in progress.
// Storing the exact button is important as some forms  have different behaviour depending
// on the clicked button (MultiButtons).
DropzoneUtil.lastClickedSubmit = {form: null, button: null};

/**
 * Generate a Dropzone FileUpload widget from the library, filling a container element
 *
 * Note: userName here is the user's display name, not login username.
 * @param {jQuery}      jQ          jquery instance.
 * @param {string}      dzSelector  A selector for the Dropzone container element
 * @param {string}      formName    The name of the form this FileUpload is attached to.
 * @param {object}      options     List of DropzoneJS options. Check out their docs.
 * @param {MockFile[]}  mockFiles   An array of existing 'mock' files to list, w/ DL links.
 * @param {boolean}     readOnly    If the Dropzone can be edited.
 * @param {object|null} historyData Necessary file version data to populate the dialog.
 * @param {WarningElem} warningElem Data about a form element which should prompt for "It seems you forgot to
 *                                  upload a file" prompt.
 * @param {ImageOptions} imageOptions Compression & resize options for images.
 *
 * historyData Structure: { <versionID>: {
 *    'files': see `mockFiles` above
 *    'details': {verDate:<str eg '22-Jun-2018 11:12'}, userName:<str> }
 * }
 * @see ops_Form_Element_FileUpload.tpl
 * @public
 */
DropzoneUtil.addDropzone = function
(
	jQ,
	dzSelector,
	formName,
	options,
	mockFiles,
	readOnly,
	historyData,
	warningElem,
	imageOptions
)
{
	var fullOptions = jQ.extend({
		"addRemoveLinks": !readOnly,
		"clickable": !readOnly,
		"dictDefaultMessage": readOnly ? 'No files available. Upload is currently not permitted.' : 'Click or drop files here to upload',
		"autoProcessQueue": false, // Prevent files from being uploaded until submit click.
		"uploadMultiple": true,
		"parallelUploads": 100,
		"formName": formName,
		"maxFiles": 100,
	}, options);

	// Dropzone takes the selector query directly, since it does not rely on jQuery.
	var dz = new Dropzone(dzSelector, fullOptions);

	dz.on('sendingmultiple', function(files, xhr, formData) {
		DropzoneUtil._processDropzoneQueue(xhr, formData, formName);
	});

	dz.options.transformFile = async (file,  done) => {
		let transformedFile = await DropzoneUtil.transformImage(file, dz.custom.imageOptions);
		done(transformedFile || file);
	};

	if (!DropzoneUtil.activeDropzones.hasOwnProperty(formName)) {
		DropzoneUtil.activeDropzones[formName] = [];
	}

	DropzoneUtil.activeDropzones[formName].push(dz);
	DropzoneUtil._setMockFiles(dz, mockFiles);

	var $buttonContainer = jQ(dzSelector).next();

	// Custom properties on Dropzone instance itself.
	dz.custom = {};
	dz.custom.reassignedVersion = null;
	dz.custom.warningElem = warningElem;
	dz.custom.$messageBox = $buttonContainer.next();
	dz.custom.imageOptions = imageOptions;

	/** Button Handlers & Message box: **/

	if (readOnly) { // Hide buttons for read-only dropzones:
		return;
	}

	dz.options.dictRemoveFileConfirmation = 'Do you want to remove this file?';

	// If data for populating the File History dialog is supplied, then build the necessary callbacks
	// to generate this dialog.
	if (historyData) {
		var $historyBtn = $buttonContainer.find('.dropzone-history');
		$historyBtn.click(function() {
			DropzoneHistoryUtil.renderHistoryDialog($(this), dz, historyData);
		});
	}

	// "Paste" button: hidden by default. Show if this browser supports the Async Clipboard API and the
	// suitable (Image) file types are permitted
	let $pasteButton = $buttonContainer.find('.dropzone-paste');
	if (DropzoneUtil.asyncPasteSupported() && DropzoneUtil.acceptsPasteType(dz) && $pasteButton.length > 0) {
		$pasteButton.removeClass('hidden'); // Show "Paste".
	}
	// "Paste" button handler, mapped for this dropzone. This will try to use the Async Clipboard API.
	$pasteButton.click(function(){
		DropzoneUtil.pasteHandler(dz);
	});
	// Ctrl-V Handler. Apply on closest parent form element container (currently taken as a "tr" element):
	$buttonContainer.closest('tr').on('paste', function (event) {
		DropzoneUtil.pasteHandler(dz, event.originalEvent);
	});

	// Prepare Dropzone message box:
	dz.custom.$messageBox.hide(); // jQuery hide, for use with slide transition.
	dz.custom.$messageBox.removeClass('hidden');

	// Reset dropzone message after any of these events:
	['drop', 'addedfile', 'removedfile'].forEach(function(item){
		dz.on(item, function(){ DropzoneUtil.resetMessage(dz); });
	});
};


/**
 * The handler being bound assumes the form has a dropzone element.
 * @param {jQuery} $form The form to bind the handler to.
 * @public
 */
DropzoneUtil.bindSubmitHandler = function($form) {

	// Prevent accidental browser file preview when dragging+dropping a file.
	disableBrowserFilePreview();

	// Override all submit click handlers so we can identify which submit was clicked when
	// submission occurs.
	// NOTE: The onClick event always occurs before onSubmit. Should an onClick handler return FALSE, then
	// onSubmit won't fire.
	$form.find('input[type=submit]').click(function(){
		DropzoneUtil.lastClickedSubmit = {form: $form, button: $(this)};
	});

	// Override the form submit to aggregate all dropzone data
	$form.submit(function(e){
		// Do not kick off the submit handler if there is already another submission going on.
		if (!DropzoneUtil._submissionInProgress()) {
			// No other submissions going on. Flag this as the current submission.
			DropzoneUtil._setCurrentSubmission($form);
			// Begin file upload / submission
			DropzoneUtil._submitHandler($form);
		}
		return false;
	});
};

/**
 * Show a message below the dropzone; feedback to the user.
 * @param {Dropzone} dz
 * @param {string} msg
 * @return {Promise} Resolves on message animation end.
 * @public
 */
DropzoneUtil.setMessage = function(dz, msg) {
	// Set message and show box.
	return new Promise(function(resolve){
		let $box = dz.custom.$messageBox;
		$box.html(msg);
		$box.slideDown(undefined, resolve);
	});
};

/**
 * Hide message box
 * @param dz
 * @return {Promise} Resolves on message animation end.
 * @public
 */
DropzoneUtil.resetMessage = function(dz){
	return new Promise(function(resolve) {
		dz.custom.$messageBox.slideUp(undefined, resolve);
	});
};

/**
 * If the current submission was anonymous i.e. was not triggered by the click of a "Submit" button.
 * @return {boolean}
 * @private
 */
DropzoneUtil._isAnonymousSubmission = function() {
	return DropzoneUtil.submitRequestInProgress === 'anon';
};

/**
 * Are we currently submitting a form?
 * @return {boolean}
 * @private
 */
DropzoneUtil._submissionInProgress = function() {
	return Boolean(DropzoneUtil.submitRequestInProgress);
};

/**
 * Flag a form as in the process of being submitted, so we can reject any other submission attempts.
 * @param $form
 * @private
 */
DropzoneUtil._setCurrentSubmission = function($form) {
	// If this submission is from a button on the same form
	if ($form.is(DropzoneUtil.lastClickedSubmit.form)) {
		this.submitRequestInProgress = DropzoneUtil.lastClickedSubmit.button;
	// Else - assume this is an anonymous submission
	} else {
		this.submitRequestInProgress = 'anon';
	}
};

/**
 * If a submission is in progress, cancel it.
 * @private
 */
DropzoneUtil._cancelSubmit = function() {

	if (!this._isAnonymousSubmission()) {
		// Unlock the submit button.
		this.submitRequestInProgress.lockSubmitReset();
	}

	this.lastClickedSubmit.form = null;
	this.lastClickedSubmit.button = null;
	this.submitRequestInProgress = null;
};

/**
 * @param $form
 * @private
 */
DropzoneUtil._submitHandler = async function ($form) {

	// Force all tinyMCE elements to save, allowing the formData aggregation later to include
	// these fields
	tinyMCE.triggerSave();

	let formId = $form.attr('id');
	var dropzoneSet = DropzoneUtil.activeDropzones[formId];
	var firstDropzone = dropzoneSet[0];

	// Check if any of the dropzones warn about the presence of words such as "attachment" or "embedded".
	// If one of these words are detected in the linked field, and that dropzone has a value, then give the
	// user a warning prompt before submission.
	var fileAttachSubstrings = ['screenshot', 'attach', 'embed', 'upload'];

	var matchedDz = null;
	var matchedSubstr = null;

	for (let dz of dropzoneSet) {
		if (matchedDz) { // Break if we find a DZ we need to warn the user about.
			break;
		}
		// If the dropzone needs attach warnings, and is missing files:
		if (dz.custom.warningElem.hasOwnProperty('id') && dz.files.length < 1) {
			var $elem = $('#'+dz.custom.warningElem.id);
			// Warning attachment must be set on a text field:
			var submittedTextToCheck = $elem.val().toLowerCase();
			for (let str of fileAttachSubstrings) {
				if (submittedTextToCheck.indexOf(str) !== -1) {
					matchedSubstr = str;
					matchedDz = dz;
					break;
				}
			}
		}
	}

	// If a dropzone is missing files and a keyword is used e.g. "screenshot".
	if (matchedDz) {
		var attachWarning =
			'It seems you forgot to attach a file.\n\n'
		    +'The field "'+matchedDz.custom.warningElem.label+'" contains the word "'
			+ matchedSubstr + '" but no files have been uploaded. Submit anyway?'
		;

		if (!confirm(attachWarning)) {
			// The user does not want to submit. Abort:
			DropzoneUtil._cancelSubmit();
			return;
		}
	}

	// Show the window unload transition (menu spinning animation)
	window.unloadTransition();

	firstDropzone.on('successmultiple', function(files, responseText, ajaxEvent){

		// Below we are going to replace the DOM state with the HTML from `responseText`.
		// To ensure that the current page (before the POST submission) is preserved in the history (back button) log,
		// add the current url to the history state.
		// Then, add a new history state storing the the POST response url, which can be different from
		// `window.location.href` if the response is a redirect.
		// This `reloadUrl` can be used to reload the current state regardless of what the browser has cached
		// after it's DOM has been replaced.
		window.history.replaceState({reloadUrl: window.location.href}, null, null);
		window.history.pushState({reloadUrl: ajaxEvent.target.responseURL}, null, ajaxEvent.target.responseURL);

		// Replace DOM HTML. Inspired by https://stackoverflow.com/a/11984907
		document.open();
		document.write(responseText);
		document.close();
	});

	// For the submitted form, aggregate the data in all dropzones in that form into the FIRST
	// dropzone.
	// See `_processDropzoneQueue` below.

	if (firstDropzone.getQueuedFiles().length > 0) {

		firstDropzone.processQueue();
	} else {
		// If no files are uploaded by the user, we need to manually invoke the upload process as
		// dropzone.js won't do it for us. The request object won't count a file named "nofiles" as a
		// valid file.
		var file = new Blob();
		file.name = 'nofiles';
		file.upload = {filename: 'nofiles'};
		firstDropzone.uploadFiles([file]);
	}
};

// This should only be called once per form, and assumes that `formData` has already been populated
// with the files from the FIRST dropzone element in that form.
DropzoneUtil._processDropzoneQueue = function(xhr, formData, formName) {

	var $form = $('#'+formName);

	// Append all the non-dropzone form data:
	// .serializeArray will convert a form into a set of key-value pairs for each element.
	$.each($form.serializeArray(), function(index, formElem){
		formData.append(formElem.name, formElem.value);
	});
	// Add the submit element, if it exists:
	if (!DropzoneUtil._isAnonymousSubmission()) {
		formData.append(
			DropzoneUtil.submitRequestInProgress.attr('name'),
			DropzoneUtil.submitRequestInProgress.attr('value')
		);
	}

	var setReassignedVer = function(dz) {
		if (dz.custom.reassignedVersion !== null) {
			formData.append(
				dz.options.paramName+'_reassigned',
				dz.custom.reassignedVersion
			);
		}
	}

	var dropzoneSet = DropzoneUtil.activeDropzones[$form.attr('id')];
	DropzoneUtil._markFilesForPreservation(dropzoneSet[0], formData);
	setReassignedVer(dropzoneSet[0]);

	// Append the files attached to all other dropzone elements.
	// Use an incrementing for loop beginning at 1 so we can skip the first dropzone,
	// which is this instance.
	for (var i = 1; i < dropzoneSet.length; i++) {
		// @var files Array
		var files = dropzoneSet[i].getQueuedFiles();

		DropzoneUtil._markFilesForPreservation(dropzoneSet[i], formData);
		setReassignedVer(dropzoneSet[i]);

		for (var fileIdx = 0; fileIdx < files.length; fileIdx++){
			formData.append(
				// _getParamName (idx) will provide a array input value POST name for a provided
				// index, e.g. "ops_Form__myDropzone[0]"
				dropzoneSet[i]._getParamName(fileIdx),
				files[fileIdx],
				files[fileIdx].name // 3rd param - Filename.
			);
		}
	}

	xhr.setRequestHeader('X-Dropzone-Request', 'true');
};

DropzoneUtil._markFilesForPreservation = function(dz, formData){
	$.each(dz.files, function(idx, f){
		if (f.hasOwnProperty('isMock')) {
			formData.append(
				dz.options.paramName+'_existing[]',
				f.name
			);
		}
	});
};

// Show 'Download' link
// See: https://github.com/enyo/dropzone/issues/694
/**
 * @param {MockFile} file
 * @private
 */
DropzoneUtil._addDownloadLink = function(file) {
	if (!file.downloadURL) {
		return;
	}
	var a = document.createElement('a');
	a.className = 'dropzone-download';
	a.target = '_blank';
	a.setAttribute('href', file['downloadURL']);

	e = $(file.previewTemplate).find('.dz-details');
	e.wrap(a);
};

/**
 *
 * Add files to an existing dropzone instance.
 * See: https://github.com/enyo/dropzone/wiki/FAQ#how-to-show-files-already-stored-on-server
 * @param {Dropzone} dz
 * @param {MockFile[]} mockFiles See PHP: `getFileArrJson`.
 * @protected
 */
DropzoneUtil._setMockFiles = function(dz, mockFiles) {
	dz.removeAllFiles();
	for (let f of mockFiles) {
		dz.files.push(f);
		if (f.kind === 'image') {
			dz.displayExistingFile(f,  f.downloadURL);
		} else {
			dz.emit("addedfile", f);
			dz.emit("complete", f);
			// Make sure that there is no progress bar, etc
		}
		DropzoneUtil._addDownloadLink(f);
	}

	// Ensure maxFiles checks are valid
	dz._updateMaxFilesReachedClass();
};

/**
 * Compress or resize images files before upload. Supports PNG, JPEG and WEBP.
 * If compression is enabled, PNG files above 5MB will be converted to JPEG.
 * @param {Blob} file
 * @param {ImageOptions} options
 * @return {?Blob}
 * @private
 */
DropzoneUtil.transformImage = async function(file, options) {

	// Avoid any further processing when not required:
	if (!options.compressEnabled && Object.entries(options).length === 1) {
		return null;
	}

	// Ignore mock (already uploaded) files, or files without name property
	if (file['isMock'] || !file['name']) {
		return null;
	}
	// Only attempt compression on files with expected extension present in file name.
	// MIME Type is not available.
	let extension = file.name.split('.').pop().toLowerCase();
	if (!['jpg', 'jpeg', 'jpe', 'png', 'webp'].includes(extension)) {
		return null;
	}

	// Compression is on by default. If disabled, manually set fields to avoid quality loss.
	if (!options.compressEnabled) {
		options.quality = 1;
		options.convertSize = Infinity; // Do not convert PNGs to JPEGs.
	}

	// Compress + transform promise:
	let compress = new Promise((resolve, reject) => {
		let currentOptions = Object.assign({
			quality: 0.8,     // Default compression level.
			success: resolve, // Promise will resolve on compress finish.
			error: reject,
		}, options)

		new Compressor(file, currentOptions);
	});

	try {
		return await compress;
	} catch (err) {
		console.error(err);
		// Avoid compression and send file as-is:
		return null;
	}
};

/**
 * @typedef {{id: ?string, label: ?string, type: ?string}} WarningElem
 */

/**
 * @typedef {{name: string, size: number, isMock: boolean, accepted: boolean, kind: string, downloadURL: ?string}} MockFile
 */

/**
 * @see ImageOptions.php Options used by CEDAR.
 * @typedef {CompressorJsOptions} ImageOptions
 * @property {boolean} compressEnabled
 */
/**
 * @see https://github.com/fengyuanchen/compressorjs#options
 * @typedef {Object} CompressorJsOptions
 * @property {?boolean} strict
 * @property {?boolean} checkOrientation
 * @property {?number} maxWidth
 * @property {?number} maxHeight
 * @property {?number} minWidth
 * @property {?number} minHeight
 * @property {?number} width
 * @property {?number} height
 * @property {?number} quality
 * @property {?string} mimeType
 * @property {?number} convertSize Minimum threshold (in bytes) for PNGs to be converted to JPEGs.
 * @property {?Function} beforeDraw
 * @property {?Function} drew
 * @property {?Function} success
 * @property {?Function} error
 */