import "filepond-plugin-file-poster/dist/filepond-plugin-file-poster.css";
import "filepond-plugin-image-preview/dist/filepond-plugin-image-preview.css";
import "filepond/dist/filepond.css";

import { getLocalizedString } from "@olenbetong/appframe-core";
import type { DataObject } from "@olenbetong/appframe-data";
import { useData, useFetchData } from "@olenbetong/appframe-react";

import {
	create,
	type FilePondErrorDescription,
	type FilePondFile,
	type FilePondInitialFile,
	type FilePond as FilePondInstance,
	type FilePondOptions,
	FileStatus,
	registerPlugin,
} from "filepond";
import FilePondPluginImageExifOrientation from "filepond-plugin-image-exif-orientation";
import FilePondPluginImagePreview from "filepond-plugin-image-preview";
import FilePondPluginImageResize from "filepond-plugin-image-resize";
import FilePondPluginImageTransform from "filepond-plugin-image-transform";
import { type RefObject, useEffect, useMemo, useRef } from "react";

export { FileStatus, registerPlugin };

registerPlugin(
	FilePondPluginImageExifOrientation,
	FilePondPluginImageResize,
	FilePondPluginImagePreview,
	FilePondPluginImageTransform,
);

let isMostLikelyMobile =
	navigator.userAgent.match(/Android/i) ||
	navigator.userAgent.match(/iPhone/i) ||
	navigator.userAgent.match(/iPad/i) ||
	navigator.userAgent.match(/iPod/i);

const labels = {
	labelIdle: isMostLikelyMobile
		? getLocalizedString("Take a photo, or upload an existing image/file")
		: getLocalizedString('Drag & Drop your files or <span class="filepond--label-action"> Browse </span>'),
	labelInvalidField: getLocalizedString("Field contains invalid files"),
	labelFileWaitingForSize: getLocalizedString("Waiting for size"),
	labelFileSizeNotAvailable: getLocalizedString("Size not available"),
	labelFileLoading: getLocalizedString("Loading"),
	labelFileLoadError: getLocalizedString("Error during load"),
	labelFileProcessing: getLocalizedString("Uploading"),
	labelFileProcessingComplete: getLocalizedString("Upload complete"),
	labelFileProcessingAborted: getLocalizedString("Upload cancelled"),
	labelFileProcessingError: getLocalizedString("Error during upload"),
	labelFileProcessingRevertError: getLocalizedString("Error during revert"),
	labelFileRemoveError: getLocalizedString("Error during remove"),
	labelTapToCancel: getLocalizedString("tap to cancel"),
	labelTapToRetry: getLocalizedString("tap to retry"),
	labelTapToUndo: getLocalizedString("tap to undo"),
	labelButtonRemoveItem: getLocalizedString("Remove"),
	labelButtonAbortItemLoad: getLocalizedString("Abort"),
	labelButtonRetryItemLoad: getLocalizedString("Retry"),
	labelButtonAbortItemProcessing: getLocalizedString("Cancel"),
	labelButtonUndoItemProcessing: getLocalizedString("Undo"),
	labelButtonRetryItemProcessing: getLocalizedString("Retry"),
	labelButtonProcessItem: getLocalizedString("Upload"),
	labelMaxFileSizeExceeded: getLocalizedString("File is too large"),
	labelMaxFileSize: getLocalizedString("Maximum file size is {filesize}"),
	labelMaxTotalFileSizeExceeded: getLocalizedString("Maximum total size exceeded"),
	labelMaxTotalFileSize: getLocalizedString("Maximum total file size is {filesize}"),
	labelFileTypeNotAllowed: getLocalizedString("File of invalid type"),
	fileValidateTypeLabelExpectedTypes: getLocalizedString("Expects {allButLastType} or {lastType}"),
	imageValidateSizeLabelFormatError: getLocalizedString("Image type not supported"),
	imageValidateSizeLabelImageSizeTooSmall: getLocalizedString("Image is too small"),
	imageValidateSizeLabelImageSizeTooBig: getLocalizedString("Image is too big"),
	imageValidateSizeLabelExpectedMinSize: getLocalizedString("Minimum size is {minWidth} × {minHeight}"),
	imageValidateSizeLabelExpectedMaxSize: getLocalizedString("Maximum size is {maxWidth} × {maxHeight}"),
	imageValidateSizeLabelImageResolutionTooLow: getLocalizedString("Resolution is too low"),
	imageValidateSizeLabelImageResolutionTooHigh: getLocalizedString("Resolution is too high"),
	imageValidateSizeLabelExpectedMinResolution: getLocalizedString("Minimum resolution is {minResolution}"),
	imageValidateSizeLabelExpectedMaxResolution: getLocalizedString("Maximum resolution is {maxResolution}"),
};

type FilePondProps<T extends Record<string, unknown>> = React.HTMLProps<HTMLInputElement> & {
	dataObject: DataObject<T>;
	fields: Partial<T>;
	onUploadStart: () => void;
	onFileUploaded: (error: FilePondErrorDescription | null, file: FilePondFile) => void;
	onFinishedUploads: () => void;
	onError: (error: string) => void;
	onFileRemoved?: (primKey: string) => void;
	/**
	 * If the file should be uploaded to a blob field, set the field name here
	 */
	fieldName?: string;
	/**
	 * If the file should be added to an existing record, set the prim key here
	 */
	primKey?: string;
	/**
	 * If a reference to the file input element is needed, pass a ref here.
	 */
	inputRef?: RefObject<HTMLInputElement>;
	/**
	 * Options to pass to FilePond
	 */
	filePondOptions?: Partial<FilePondOptions>;
	attachmentPrimKeys?: string[];
	whereClause?: string;
};

function getFileUrl<T extends Record<string, unknown>>(
	dataObject: DataObject<T>,
	fieldName?: string,
	primKey?: string,
	action = "upload",
) {
	let url =
		action === "upload"
			? dataObject.dataHandler.getUploadUrl()
			: dataObject.dataHandler.getFileStoreURL(primKey ?? "", fieldName ?? "");

	if (action === "upload") {
		url += "/";
	}

	return url?.replaceAll("//", "/") ?? "";
}

type FileStoreDataObjectRecord = {
	PrimKey: string;
	FileName: string | null;
	FileSize: number | null;
	FileRef: string | null;
	ElementRef: string | null;
	Damage: boolean;
	DamageID: number | null;
};

export function FilePond<T extends FileStoreDataObjectRecord>({
	dataObject,
	fields,
	onUploadStart,
	onFileUploaded,
	onFinishedUploads,
	onFileRemoved,
	onError,
	fieldName,
	filePondOptions,
	primKey,
	attachmentPrimKeys,
	whereClause,
	inputRef: inputRefProp,
	...props
}: FilePondProps<T>) {
	let inputRef = useRef<HTMLInputElement>(null);
	let pondRef = useRef<FilePondInstance | null>(null);
	let filesData = useData(dataObject);
	let { data: filesFetched } = useFetchData(dataObject, whereClause ?? "1=2");
	let files = useMemo(() => {
		if (whereClause) {
			return filesFetched;
		}
		return filesData;
	}, [whereClause, filesFetched, filesData]);
	let clearFilesRef = useRef(false);

	useEffect(() => {
		return () => {
			pondRef.current?.destroy();
			pondRef.current = null;
		};
	}, []);

	useEffect(() => {
		async function removeUploadedFile(source: string, load: () => void, error: (error: string) => void) {
			// this method is also called when we clear the files on data object
			// refresh. Haven't found any elegant way to work around that, so instead
			// we set this ref to true when we remove the files to have a way
			// to discover why the files are being removed
			if (clearFilesRef.current) return;

			let { dataHandler } = dataObject;
			try {
				let result = await dataHandler.destroy({
					PrimKey: source,
				} as Partial<T>);

				if (result) {
					onFileRemoved?.(source);
					load();
				} else {
					error("Failed to delete file on server. Delete request returned false");
				}
			} catch (err: any) {
				error(
					err.responseText ??
						err.statusText ??
						err.message ??
						"Failed to delete file on server but no error was returned.",
				);
			}
		}

		let options: FilePondOptions = {
			allowMultiple: Boolean(props.multiple),
			allowImageResize: true,
			imageResizeMode: "contain",
			imageResizeTargetWidth: 2400,
			imageResizeTargetHeight: 2400,
			imageResizeUpscale: false,
			...labels,
			onaddfilestart: (file) => {
				if (file.status !== 5 && file.status !== 6 && file.status !== 10) {
					onUploadStart();
				}
			},
			onprocessfile: (error, file) => {
				onFileUploaded(error, file);

				let refFiles = pondRef.current?.getFiles();
				let found = refFiles?.find(
					(file) => file.status !== 2 && file.status !== 5 && file.status !== 6 && file.status !== 10,
				);

				if (found === undefined) {
					onFinishedUploads();
				}
			},
			onprocessfiles: () => onFinishedUploads(),
			onerror: (error) => onError(error.body),
			beforeRemoveFile: () => {
				if (confirm(getLocalizedString("Are you sure you want to remove this file?"))) {
					return true;
				}

				return false;
			},
			server: {
				load: getFileUrl(dataObject, fieldName, primKey, "view"),
				restore: getFileUrl(dataObject, fieldName, primKey, "view"),

				revert: removeUploadedFile,
				remove: removeUploadedFile,

				process: {
					url: getFileUrl(dataObject, fieldName),
					ondata: (formData) => {
						let masterChildCriteria = dataObject.getParameter("masterChildCriteria");
						let allFields = { ...masterChildCriteria, ...fields };

						for (let [field, value] of Object.entries(allFields)) {
							if (value !== undefined) {
								formData.append(field, String(value));
							}
						}

						return formData;
					},
					onload: (response) => JSON.parse(response),
				},
			},
			...filePondOptions,
		};

		if (pondRef.current) {
			pondRef.current.setOptions(options);
		} else {
			pondRef.current = create(inputRefProp?.current ?? inputRef.current ?? undefined, options);
		}
	}, [
		dataObject,
		props.multiple,
		fields,
		fieldName,
		primKey,
		inputRefProp,
		filePondOptions,
		onError,
		onFileUploaded,
		onUploadStart,
		onFinishedUploads,
		onFileRemoved,
	]);

	useEffect(() => {
		if (pondRef.current) {
			files.forEach((file, index) => {
				let options: { index: number } & Partial<FilePondInitialFile["options"]> = {
					index,
					type: "local",
				};

				pondRef.current?.addFile(file.PrimKey, options);
			});
		}

		return () => {
			// Indicate that we are clearing the files because data has changed,
			// to prevent the remove callback from deleting the files
			clearFilesRef.current = true;
			pondRef.current?.removeFiles();
			clearFilesRef.current = false;
		};
	}, [files]);

	return (
		<div className="filepond--wrapper">
			{attachmentPrimKeys && <input type="hidden" name="AttachmentPrimkeys" value={attachmentPrimKeys} />}

			<input type="file" {...props} name="File" ref={inputRefProp ?? inputRef} />
		</div>
	);
}
