'use strict';

/**
 * Gestionnaire d'envoi de fichiers.
 *
 * @license https://www.gnu.org/licenses/gpl-3.0.html
 * @link https://www.igalerie.org/
 */
function Upload(prefix, options)
{
	// Options.
	this.options = options;



	// Préfixe des éléments HTML (exemple : "upload_").
	const _prefix = prefix;

	// Raccourci pour document.querySelector().
	const _q = selector => { return document.querySelector(selector); };

	// Raccourci pour document.querySelectorAll().
	const _qAll = selector => { return document.querySelectorAll(selector); };

	// Élément HTML contenant la liste des fichiers.
	const _list = _q(`#${_prefix}list`);

	// Code HTML initial se trouvant dans la liste des fichiers.
	const _listInner = _list.innerHTML;

	// this!
	const _this = this;



	// Index du fichier courant.
	var _fileIndex = 0;

	// Liste des fichiers ajoutés.
	var _files = [];

	// Indique si l'envoi des fichiers a débuté.
	var _start = false;

	// Indique si au moins un fichier a été envoyé avec succès.
	var _success = false;

	// Total des fichiers envoyés.
	var _totalUploadFiles = {};

	// Object XMLHttpRequest.
	var _xhr;



	/**
	 * Initialisation.
	 *
	 * @return void
	 */
	(function()
	{
		_list.style.position = 'relative';

		// Bouton "Sélectionner des fichiers".
		const input_file = _q(`#${_prefix}input_file`);
		input_file.style.display = 'none';
		input_file.addEventListener('change', () => _addFiles(input_file.files));
		_q(`#${_prefix}add`).addEventListener('click', () =>
		{
			if (!_start)
			{
				input_file.click();
			}
		});

		// Bouton "Vider la liste".
		_q(`#${_prefix}clear`).addEventListener('click', _clearList);

		// Bouton "Envoyer".
		_q(`#${_prefix}start`).addEventListener('click', function()
		{
			if (this.disabled || _start || !_files.length)
			{
				return;
			}

			// Si aucun album sélectionné, on ne va pas plus loin.
			if (!_this.options.ajaxData.album_id)
			{
				alert(_this.options.text.noAlbum);
				return;
			}

			// On indique que l'on vient de démarrer l'envoi,
			// et on désactive les boutons d'ajout et d'envoi.
			_start = true;
			_disableButtons(true, 'add', 'start');

			// On scroll en haut de la liste de fichiers si
			// nécessaire avant de démarrer l'envoi des fichiers.
			function upload_file()
			{
				_list.removeEventListener('scrollend', upload_file);
				_fileIndex = 0;
				_totalUploadFiles = _getTotalFiles();
				_totalUploadFiles.loadedFiles = 0;
				_totalUploadFiles.loadedSize = 0;
				_uploadFile();
			}
			if (_list.scrollTop > 0 && _list.scrollHeight > _list.clientHeight)
			{
				_list.addEventListener('scrollend', upload_file);
				_list.scroll({top: 0, behavior: 'smooth'});
			}
			else
			{
				upload_file();
			}
		});

		// Drag & drop.
		_list.addEventListener('dragover', evt =>
		{
			evt.preventDefault();

			if (!_start)
			{
				_list.className = 'dragover';
			}
		});
		_list.addEventListener('dragleave', evt =>
		{
			evt.preventDefault();

			if (!_start)
			{
				_list.className = 'dragleave';
			}
		});
		_list.addEventListener('drop', evt =>
		{
			evt.preventDefault();

			if (!_start && evt.dataTransfer.files)
			{
				_list.className = 'drop';
				_addFiles(evt.dataTransfer.files);
			}
		});

		_updateInterface();
	})();



	/**
	 * Ajout de nouveaux fichiers.
	 *
	 * @param object files
	 *
	 * @return void
	 */
	function _addFiles(files)
	{
		if (_start)
		{
			return;
		}

		if (_this.options.maxTotalFiles == 1)
		{
			_clearList();
		}

		// On supprime tout le code HTML présent dans la liste des fichiers
		// à l'exception des fichiers eux-mêmes.
		_qAll(`#${_prefix}list > :not(.${_prefix}file)`).forEach(elem => elem.remove());

		// On désactive les boutons.
		_disableButtons(true, 'add', 'clear', 'start');

		// Vérification des fichiers.
		_checkFile(files, 0);
	}

	/**
	 * Ajoute un fichier à la liste.
	 *
	 * @param object files
	 * @param int i
	 * @param mixed error
	 *
	 * @return void
	 */
	function _addToList(files, i, error)
	{
		if (error !== null)
		{
			// Code HTML du fichier.
			const pref = `${_prefix}file`;
			const html =
				`<div id="${pref}_${_fileIndex}" class="${pref}">` +
					`<a class="${pref}_delete" href="javascript:;"></a>` +
					`<div class="${pref}_infos">` +
						`<span class="${pref}_name"></span>` +
						`<span class="${pref}_size"></span>` +
					`</div>` +
					`<div class="${pref}_progress">` +
						`${error ? `<p></p>` : '<div><div></div></div>'}` +
					`</div>` +
				`</div>`;

			// On insère le code HTML en fin de liste.
			_list.insertAdjacentHTML('beforeend', html);
			const file = _q(`#${pref}_${_fileIndex}`);

			// On insère le texte dans le code HTML.
			file.querySelector(`.${pref}_name`).textContent = files[i].name;
			file.querySelector(`.${pref}_size`).textContent = _formatFilesize(files[i].size);
			if (error)
			{
				file.classList.add(`${pref}_warning`);
				file.querySelector(`.${pref}_progress p`).textContent
					= _this.options.text.warning[error];
			}

			// On crée un événement pour la suppression du fichier.
			const file_delete = file.querySelector(`.${_prefix}file_delete`);
			file_delete.addEventListener('click', evt =>
			{
				const file = file_delete.closest(`.${_prefix}file`);

				// On supprime le fichier de la liste.
				_files[file.id.replace(`${_prefix}file_`, '')] = false;

				// On annule la requête Ajax.
				if (file.id == `${_prefix}file_${_fileIndex}`)
				{
					_xhr?.abort();
				}

				// On supprime le code HTML du fichier après avoir
				// effectué une petite animation le faisant disparaître.
				const keyframes = {opacity: 0, height: 0};
				const options = {duration: 300, easing: 'ease-in-out'};
				file.animate(keyframes, options).finished.then(() =>
				{
					file.remove();

					// S'il ne reste plus aucun fichier, on remet le code
					// initial se trouvant à l'intérieur de la liste.
					if (!_start && !_qAll(`.${_prefix}file`).length)
					{
						_list.insertAdjacentHTML('afterbegin', _listInner);
					}

					// Mise à jour du nombre de fichiers.
					_updateInterface();
				});

				// Désactive le lien.
				evt.preventDefault();
			});

			// On ajoute le fichier à la liste.
			_files[_fileIndex] = files[i];
			_files[_fileIndex].error = error;
			_fileIndex++;

			// On scroll en bas de la liste.
			_list.scrollTop = _list.scrollHeight;

			// Mise à jour du nombre de fichiers.
			_updateInterface(true);
		}

		if (typeof files[i + 1] == 'undefined')
		{
			// On réactive les boutons.
			_disableButtons(false,
				'clear',
				_this.options.albumSelected && _files.length ? 'start' : null,
				_getTotalFiles().files !== _this.options.maxTotalFiles ? 'add' : null
			);
		}
		else
		{
			_checkFile(files, i + 1);
		}
	}

	/**
	 * Change l'état d'un fichier.
	 *
	 * @parram string status
	 * @parram string text
	 *
	 * @return void
	 */
	function _changeHTMLFileStatus(status, text)
	{
		const file = _q(`#${_prefix}file_${_fileIndex}`);
		if (file && typeof text == 'string')
		{
			const progress = file.querySelector(`.${_prefix}file_progress`);
			file.classList.add(`${_prefix}file_${status}`);
			while (progress.firstChild)
			{
				progress.removeChild(progress.firstChild);
			}
			const p = document.createElement('p');
			p.textContent = text;
			progress.insertAdjacentElement('afterbegin', p);
		}
	}

	/**
	 * Vérifie un fichier.
	 *
	 * @parram object files
	 * @parram int i
	 *
	 * @return void
	 */
	function _checkFile(files, i)
	{
		const file = files[i];
		const total = _getTotalFiles();

		// Aucun fichier ?
		if (typeof file == 'undefined')
		{
			return _addToList(files, i, null);
		}

		// Si le fichier est identique à l'un des fichiers
		// déjà ajoutés à la liste, on l'ignore.
		for (let n = 0; n < _files.length; n++)
		{
			if (_files[n] !== undefined
			 && _files[n].lastModified == file.lastModified
			 && _files[n].name == file.name
			 && _files[n].size == file.size
			 && _files[n].type == file.type)
			{
				return _addToList(files, i, null);
			}
		}

		// Nombre de fichiers.
		if (total.files == _this.options.maxTotalFiles)
		{
			return _addToList(files, i, 'totalfiles');
		}

		// Nom de fichier.
		const regexp = new RegExp(`^.{1,250}\.(${_this.options.fileExts.join('|')})$`, 'i');
		if (!file.name.match(regexp))
		{
			return _addToList(files, i, 'filename');
		}

		// Type de fichier.
		if (!_this.options.fileTypes.includes(file.type))
		{
			return _addToList(files, i, 'filetype');
		}

		// Poids du fichier.
		if (file.size > _this.options.maxFileSize)
		{
			return _addToList(files, i, 'filesize');
		}

		// Poids total.
		if ((total.size + file.size) > _this.options.maxTotalSize)
		{
			return _addToList(files, i, 'totalsize');
		}

		// Dimensions de l'image.
		if (file.type.startsWith('image/'))
		{
			const img = new Image();
			img.onerror = () => _addToList(files, i, 'imagevalid');
			img.onload = () =>
			{
				URL.revokeObjectURL(img.src);
				_addToList(files, i, (img.width > _this.options.maxWidth
					|| img.height > _this.options.maxHeight)
					? 'imagesize'
					: false);
			};
			img.src = URL.createObjectURL(file);
		}
		else
		{
			_addToList(files, i, false);
		}
	}

	/**
	 * Vide la liste des fichiers.
	 *
	 * @return void
	 */
	function _clearList()
	{
		const files = _qAll(`.${_prefix}file`);

		if (!files.length)
		{
			return;
		}
		_disableButtons(true, 'clear');

		// On annule la requête Ajax.
		_xhr?.abort();

		// On réinitialise tout.
		_fileIndex = 0;
		_files = [];

		// On supprime la liste de fichiers.
		files.forEach(elem => elem.remove());

		// On remet le code initial se trouvant à l'intérieur de la liste
		_list.insertAdjacentHTML('afterbegin', _listInner);

		// Mise à jour du nombre de fichiers.
		_updateInterface();
	}

	/**
	 * Active / désactive des boutons.
	 *
	 * @param bool disable
	 * @param string buttons
	 *
	 * @return void
	 */
	function _disableButtons(disable, ...buttons)
	{
		buttons.forEach(button =>
		{
			if (typeof button == 'string')
			{
				const elem = _q(`#${_prefix}${button}`);
				elem.disabled = disable;
			}
		});
	}

	/**
	 * Formate le poids d'un fichier.
	 *
	 * @param integer filesize
	 * @param string unit
	 *
	 * @return string
	 */
	function _formatFilesize(filesize, unit)
	{
		const kb = 1024, mb = 1024 * kb, gb = 1024 * mb;

		// Forcer l'utilisation d'une unité ?
		if (unit)
		{
			unit = unit == 'kb' ? kb : mb;
			filesize = String(Math.round(filesize/unit * 100) / 100);
		}
		else
		{
			if (filesize < kb)
			{
				filesize = _this.options.text.sizeUnits[0].replace(
					'%s', filesize
				);
			}
			else if (filesize < mb)
			{
				filesize = _this.options.text.sizeUnits[1].replace(
					'%s', Math.round(filesize/kb * 100) / 100
				);
			}
			else if (filesize < gb)
			{
				filesize = _this.options.text.sizeUnits[2].replace(
					'%s', Math.round(filesize/mb * 100) / 100
				);
			}
			else
			{
				filesize = _this.options.text.sizeUnits[3].replace(
					'%s', Math.round(filesize/gb * 100) / 100
				);
			}
		}

		return filesize.replace('.', _this.options.text.decimalSeparator);
	}

	/**
	 * Retourne le nombre et le poids total des fichiers ajoutés à la liste.
	 *
	 * @return object
	 */
	function _getTotalFiles()
	{
		let n = 0, size = 0;
		for (let i = 0; i < _files.length; i++)
		{
			if (_files[i]?.error === false)
			{
				n++;
				size += _files[i].size;
			}
		}
		return { files: n, size: size };
	}

	/**
	 * Met à jour divers éléments d'interface
	 * lorsqu'on ajoute ou supprime des fichiers.
	 *
	 * @param bool only_stats
	 *
	 * @return void
	 */
	function _updateInterface(only_stats = false)
	{
		// Informations sur la liste : nombre de fichiers et poids total.
		const total = _getTotalFiles();
		const files = _q(`#${_prefix}infos_files`);
		const filesize = _q(`#${_prefix}infos_filesize`);
		if (files)
		{
			_q(`#${_prefix}infos_files`).textContent = total.files + '/' +
				_this.options.text.files.replace('%s', _this.options.maxTotalFiles);
		}
		if (filesize)
		{
			_q(`#${_prefix}infos_filesize`).textContent = _formatFilesize(total.size) + '/' +
				_formatFilesize(_this.options.maxTotalSize);
		}

		if (_start || only_stats)
		{
			return;
		}

		// Gestion de l'activation des boutons.
		const buttons =
		{
			add: total.files != _this.options.maxTotalFiles,
			clear: _qAll(`.${_prefix}file`).length,
			start: _this.options.albumSelected && total.files
		};
		for (const button in buttons)
		{
			_disableButtons(!buttons[button], button);
		}
	}

	/**
	 * Envoi un fichier.
	 *
	 * @return void
	 */
	function _uploadFile()
	{
		const f = _files[_fileIndex];

		// S'il n'y a plus de fichier, fin de l'upload.
		if (typeof f == 'undefined')
		{
			if (_success)
			{
				_q(`#${_prefix}form`).submit();
			}
			else
			{
				_start = false;
				_updateInterface();
			}
			return;
		}

		// Si le fichier a été supprimé ou n'est pas valide, on passe au suivant.
		if (f === false || (f && f.error !== false))
		{
			_fileIndex++;
			_uploadFile();
			return;
		}

		const file = _q(`#${_prefix}file_${_fileIndex}`);
		const slice_size = _this.options.sliceSize;
		const slice_max = Math.max(Math.ceil(f.size / slice_size), 1);

		let error = false;
		let filedata;
		let slice_start = 0;

		_list.scrollTop = file.offsetTop;

		send_part(1);

		function upload_error(text)
		{
			error = true;
			_files[_fileIndex] = false;
			_changeHTMLFileStatus('error', text);
			_updateInterface();
		}

		function send_part(slice_num)
		{
			filedata = f.slice(slice_start, slice_start + slice_size);

			_xhr = new XMLHttpRequest();

			// Annulation de l'upload.
			_xhr.addEventListener('abort', upload_error);

			// Erreur.
			_xhr.addEventListener('error', () =>
			{
				upload_error(_this.options.text.failed);
				console.log(_xhr.response);
			});

			// Timeout.
			_xhr.addEventListener('timeout', () =>
			{
				upload_error(_this.options.text.failed + ' (Timeout)');
			});

			// Fichier envoyé avec succès.
			_xhr.addEventListener('load', () =>
			{
				if (_xhr?.response?.status == 'error')
				{
					upload_error(_xhr.response.message);
					return;
				}

				if (slice_num < slice_max)
				{
					return;
				}

				// On indique que l'envoi a été un succès.
				_changeHTMLFileStatus('success', _this.options.text.success);

				// On retire le fichier de la liste.
				_files[_fileIndex] = false;
				_updateInterface();

				// On indique qu'au moins un fichier a été envoyé avec succès.
				_success = true;
			});

			// Envoi terminé.
			_xhr.addEventListener('loadend', () =>
			{
				// S'il n'y a pas eu d'erreur.
				if (!error)
				{
					// S'il reste des parties de fichiers à envoyer, on continue.
					slice_start += slice_size;
					if (slice_start < f.size)
					{
						send_part(slice_num + 1);
						return;
					}

					// Sinon, on ajoute le nom de fichier au formulaire.
					if (typeof _xhr.response == 'object' && _xhr.response !== null)
					{
						const message = _xhr.response.message != ''
							&& _xhr.response.message != 'OK.'
							? (_xhr.response.filename
								? encodeURI(_xhr.response.filename) + ' : '
								: '') + encodeURI(_xhr.response.message)
							: encodeURI(_xhr.response.filename);
						const input_id = `${_prefix}file_input_${_fileIndex}`;
						_list.insertAdjacentHTML('afterend',
							`<input id="${input_id}" type="hidden">`);
						_q('#' + input_id).setAttribute('name', `${_xhr.response.status}[]`);
						_q('#' + input_id).value = message;
					}
				}

				// On passe au fichier suivant.
				_totalUploadFiles.loadedSize += f.size;
				_fileIndex++;
				_uploadFile();
			});

			// Progression de l'envoi du fichier.
			_xhr.upload.addEventListener('progress', evt =>
			{
				if (evt.lengthComputable)
				{
					// Pourcentage du fichier envoyé.
					const avg = f.size / slice_max;
					const pc = (evt.loaded / evt.total) * 100;
					const filesize_loaded = (avg * (pc / 100)) + ((slice_num - 1) * avg);
					let filesize_pc = (filesize_loaded / f.size) * 100;
					if (filesize_pc > 100)
					{
						filesize_pc = 100;
					}
					file.querySelector(`.${_prefix}file_progress div div`).style.width
						= Math.round(filesize_pc) + '%';

					// Pourcentage de tous les fichiers envoyés.
					const loaded_size = _totalUploadFiles.loadedSize
						+ (f.size * (filesize_pc / 100));
					const total_pc = Math.round((loaded_size * 100)
						/ _totalUploadFiles.size);
					_q(`#${_prefix}infos_progress_pc`).textContent = total_pc + '%';
				}
			});

			// Requête.
			_xhr.open('POST', _this.options.ajaxScript);
			_xhr.responseType = 'json';
			_xhr.timeout = 5000;

			// On envoi les données.
			const form_data = new FormData();
			for (const name in _this.options.ajaxData)
			{
				form_data.set(name, _this.options.ajaxData[name]);
			}
			form_data.set('filedata', filedata);
			form_data.set('filename', encodeURI(f.name));
			form_data.set('last_part', slice_num == slice_max ? 1 : 0);
			_xhr.send(form_data);
		}
	}
}