import { UploadFileOptions, UploadTask, UploadFileProgressUpdateCallback, UploadFileOptionFiles, OnProgressUpdateResult } from '../../../interface.uts';
import { NetworkUploadFileListener } from '../NetworkManager.uts'
import { CharacterSet, HTTPURLResponse, UUID, Data, URL, URLResourceKey, URLSessionDataTask, URLSessionTask, URLSession, URLSessionConfiguration, OperationQueue, URLSessionDataDelegate, URLSessionUploadTask, NSError , NSMutableData , NSMutableSet , URLRequest} from 'Foundation';
import { UTTypeCreatePreferredIdentifierForTag, UTTypeCopyPreferredTagWithClass, kUTTagClassFilenameExtension, kUTTagClassMIMEType } from 'MobileCoreServices';
import { CFString } from 'CoreFoundation';

class NetworkUploadTaskImpl implements UploadTask {
	private task : URLSessionDataTask | null = null;
	private listener : NetworkUploadFileListener | null = null;
	constructor(task : URLSessionDataTask | null, listener : NetworkUploadFileListener) {
		this.task = task;
		this.listener = listener;
		super();
	}

	public abort() {
		this.task?.cancel()
		UTSiOS.destroyInstance(this)
	}

	public onProgressUpdate(option : UploadFileProgressUpdateCallback) {
		const kListener = this.listener;
		if (kListener != null) {
			kListener!.progressListeners.add(option);
		}
	}
}

class UploadController implements URLSessionDataDelegate {
	private static instance : UploadController | null = null

	private session : URLSession | null = null;

	private taskMap : Map<URLSessionTask, NetworkUploadFileListener> = new Map<URLSessionTask, NetworkUploadFileListener>();

	public static getInstance() : UploadController {
		if (this.instance == null) {
			this.instance = new UploadController();
		}
		return this.instance!;
	}


	public uploadFile(options : UploadFileOptions, listener : NetworkUploadFileListener) : UploadTask {
		let boundary = `----${new UUID().uuidString}`
		let request = this.createRequest(options, listener, boundary);
		if (request == null) {
			const task = new NetworkUploadTaskImpl(null, listener)
			listener.task = task
			return task;
		}

		if (this.session == null) {
			let urlSessionConfig = URLSessionConfiguration.default;

			this.session = new URLSession(configuration = urlSessionConfig, delegate = this, delegateQueue = OperationQueue.current);
		}

		const bodyData = this.createBody(boundary, options, listener);
		if (bodyData == null) {
			const task = new NetworkUploadTaskImpl(null, listener);
			listener.task = task
			return task
		}
		let task = this.session?.uploadTask(with = request!, from = bodyData!);
		task?.resume();
		if (task != null) {
			this.taskMap.set(task!, listener);
		}
		let requestTask = new NetworkUploadTaskImpl(task, listener);
		listener.task = requestTask
		return requestTask;
	}


	private createRequest(param : UploadFileOptions, listener : NetworkUploadFileListener, boundary : string) : URLRequest | null {
		const encodeUrl = this.percentEscapedString(param.url)
		let url = new URL(string = encodeUrl);
		if (url == null) {
			let error = new NSError(domain = "invalid URL", code = 600009);
			listener.onFail(error);
			return null
		}

		let timeout = param.timeout == null ? 120000 : param.timeout;
		let timeoutInterval = new Double(timeout!) / 1000;
		let request = new URLRequest(url = url!, cachePolicy = URLRequest.CachePolicy.useProtocolCachePolicy, timeoutInterval = timeoutInterval);
		request.httpShouldHandleCookies = true;

		request.httpMethod = "POST";

		request.setValue(`multipart/form-data; boundary=${boundary}`, forHTTPHeaderField = "Content-Type");

		let ua = UTSiOS.getUserAgent();
		request.setValue(ua, forHTTPHeaderField = "User-Agent");
		
		
		let headers = param.header?.toMap();
		if (headers != null) {
			for (entry in headers!) {
				let key = entry.key;
				let value = entry.value;
				let valueStr = "";
				if (value instanceof UTSJSONObject) {
					valueStr = JSON.stringify(value) ?? ""
				}else if(value instanceof Map<string, any>){
					valueStr = JSON.stringify(new UTSJSONObject(value)) ?? ""
				}else{
					valueStr = `${value}`
				}
				request.setValue(valueStr, forHTTPHeaderField = key);
			}
		}
		return request;
	}

	private createBody(boundary : string, options : UploadFileOptions, listener : NetworkUploadFileListener) : Data | null {
		let body = new NSMutableData();
		let formData = options.formData?.toMap();
		if (formData != null) {
			for (entry in formData!) {
				const key = entry.key;
				const value = entry.value;
				if (value != null && typeof (key) == 'string') {
					if (value instanceof UTSJSONObject) {
						let valueStr = JSON.stringify(value)
						if (valueStr != null) {
							this.fillTextPart(body, boundary, key, valueStr as string)
						}
					}else if(value instanceof Map<string, any>){
						let valueStr = JSON.stringify(new UTSJSONObject(value))
						if (valueStr != null) {
							this.fillTextPart(body, boundary, key, valueStr as string)
						}
					}else{
						this.fillTextPart(body, boundary, key, `${value}`)
					}
				} else {
					continue;
				}
			}
		}

		const tempFiles = options.files;
		if (tempFiles != null && tempFiles!.length > 0) {
			const files : UploadFileOptionFiles[] = tempFiles!;
			for (let i = 0; i < files.length; i++) {
				const item = files[i]
				const filePath = item.uri;
				const name = item.name ?? "file";
				if (!this.fillFilePart(body, boundary, name, filePath!, listener)) {
					return null;
				}
			}
		} else {
			const filePath = options.filePath;
			const name = options.name ?? "file";
			if (filePath == null) {
				let error = new NSError(domain = "filePath is null", code = -1);
				listener.onFail(error)
				return null;
			}
			if (!this.fillFilePart(body, boundary, name, filePath!, listener)) {
				return null
			}
		}

		body.append(`--${boundary}--\r\n`.data(using = String.Encoding.utf8)!)

		return body.copy() as Data;
	}
	
	
	private percentEscapedString(str: string): string {
		//如果url已经有部分经过encode，那么需要先decode再encode。
		return str.removingPercentEncoding?.addingPercentEncoding(withAllowedCharacters= CharacterSet.urlQueryAllowed) ?? str
	}


	private fillTextPart(body : NSMutableData, boundary : string, key : string, value : string) {
		body.append(`--${boundary}\r\n`.data(using = String.Encoding.utf8)!)
		body.append(`Content-Disposition: form-data; name=\"${key}\"\r\n`.data(using = String.Encoding.utf8)!)
		body.append("\r\n".data(using = String.Encoding.utf8)!)
		body.append(value.data(using = String.Encoding.utf8)!)
		body.append("\r\n".data(using = String.Encoding.utf8)!)
	}

	private fillFilePart(body : NSMutableData, boundary : string, name : string, filePath : string, listener : NetworkUploadFileListener) : boolean {
		const absolutePath = new URL(fileURLWithPath = UTSiOS.getResourceAbsolutePath(filePath, null))
		const fileData = UTSiOS.try(new Data(contentsOf = absolutePath), "?")
		if (fileData == null) {
			let error = new NSError(domain = "Illegal file", code = -1);
			listener.onFail(error)
			return false
		}
		const mimeType = this.getMimeType(absolutePath)
		const fileName = absolutePath.lastPathComponent

		const keys = new Swift.Set([URLResourceKey.fileSizeKey])
		const resourceValue = UTSiOS.try(absolutePath.resourceValues(forKeys = keys), "?")
		const fileSize = resourceValue?.fileSize
		body.append(`--${boundary}\r\n`.data(using = String.Encoding.utf8)!)
		body.append(`Content-Disposition: form-data; name=\"${name}\"; filename=\"${fileName}\"\r\n`.data(using = String.Encoding.utf8)!)
		body.append(`Content-Type: ${mimeType}\r\n`.data(using = String.Encoding.utf8)!)
		if (fileSize != null) {
			body.append(`Content-Length: ${fileSize!}\r\n`.data(using = String.Encoding.utf8)!)
		}
		body.append("\r\n".data(using = String.Encoding.utf8)!)
		body.append(fileData!)
		body.append("\r\n".data(using = String.Encoding.utf8)!)
		return true
	}


	private getMimeType(url : URL) : string {
		let pathExtension = url.pathExtension

		const uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension as CFString, null)?.takeRetainedValue()
		if (uti != null) {
			let mimetype = UTTypeCopyPreferredTagWithClass(uti!, kUTTagClassMIMEType)?.takeRetainedValue()
			if (mimetype != null) {
				return mimetype as string;
			}
		}
		return "application/octet-stream"
	}

	//mark --- URLSessionDataDelegate
	urlSession(session : URLSession, @argumentLabel("") task : URLSessionTask, @argumentLabel("didSendBodyData") bytesSent : Int64, @argumentLabel("") totalBytesSent : Int64, @argumentLabel("") totalBytesExpectedToSend : Int64) {
		
		let listener = this.taskMap.get(task);
		const progress = (Number.from(totalBytesSent) / totalBytesExpectedToSend) * 100
		const progressUpdate : OnProgressUpdateResult = {
			progress: progress.toInt(),
			totalBytesSent: totalBytesSent,
			totalBytesExpectedToSend: totalBytesExpectedToSend
		}
		listener?.onProgress(progressUpdate)
	}

	urlSession(session : URLSession, @argumentLabel("") dataTask : URLSessionDataTask, @argumentLabel("didReceive") data : Data) {
		let listener = this.taskMap.get(dataTask);
		listener?.onDataReceived(data);
	}

	urlSession(session : URLSession, @argumentLabel("") task : URLSessionTask, @argumentLabel("didCompleteWithError") error : NSError | null) {
		let listener = this.taskMap.get(task);

		if (error != null) {
			listener?.onFail(error as NSError);
		} else {
			listener?.onFinished(task.response as HTTPURLResponse);
		}
		this.taskMap.delete(task);
	}
}


export {
	UploadController
}