import { DownloadFileOptions, DownloadTask, DownloadFileProgressUpdateCallback, OnProgressDownloadResult } from '../../../interface.uts';
import { NetworkDownloadFileListener } from '../NetworkManager.uts';
import { UUID, Data, URL, URLResourceKey, URLSessionDataTask, URLSessionTask, URLSession, URLSessionConfiguration, OperationQueue, URLSessionDataDelegate, URLSessionDownloadTask, NSError, URLSessionDownloadDelegate, URLRequest, FileManager, NSString, NSTemporaryDirectory, NSHomeDirectory , CharacterSet , HTTPURLResponse } from 'Foundation';
import { } from 'CommonCrypto';
import { Int, UnsafeBufferPointer, UnsafeRawBufferPointer } from 'Swift';
import { ObjCBool } from "ObjectiveC";

class NetworkDownloadTaskImpl implements DownloadTask {
	private task : URLSessionDownloadTask | null = null;
	private listener : NetworkDownloadFileListener | null = null;
	constructor(task : URLSessionDownloadTask | null, listener : NetworkDownloadFileListener) {
		this.task = task;
		this.listener = listener;
		super();
	}

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

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



export class DownloadController implements URLSessionDownloadDelegate {
	private static instance : DownloadController | null = null

	private session : URLSession | null = null;

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

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

	public downloadFile(options : DownloadFileOptions, listener : NetworkDownloadFileListener) : DownloadTask {
		let request = this.createDownloadRequest(options, listener);
		if (request == null) {
			const task = new NetworkDownloadTaskImpl(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);
		}

		let task = this.session?.downloadTask(with = request!);
		task?.resume();

		if (task != null) {
			this.taskMap.set(task!, listener);
		}
		let requestTask = new NetworkDownloadTaskImpl(task, listener);
		listener.task = requestTask
		return requestTask;
	}
	
	private createDownloadRequest(options : DownloadFileOptions, listener : NetworkDownloadFileListener) : URLRequest | null {
		const encodeUrl = this.percentEscapedString(options.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 = options.timeout == null ? 120000 : options.timeout;
		let timeoutInterval = new Double(timeout!) / 1000;
		let request = new URLRequest(url = url!, cachePolicy = URLRequest.CachePolicy.useProtocolCachePolicy, timeoutInterval = timeoutInterval);
		request.httpShouldHandleCookies = true;
		request.httpMethod = "GET";

		let ua = UTSiOS.getUserAgent();
		request.setValue(ua, forHTTPHeaderField = "User-Agent");

		let headers = options.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 percentEscapedString(str: string): string {
		//如果url已经有部分经过encode，那么需要先decode再encode。
		return str.removingPercentEncoding?.addingPercentEncoding(withAllowedCharacters= CharacterSet.urlQueryAllowed) ?? str
	}

	private convertToMD5(param : string) : string {
		const strData = param.data(using = String.Encoding.utf8)!
		let digest = new Array<UInt8>(repeating = 0, count = new Int(CC_MD5_DIGEST_LENGTH))
		strData.withUnsafeBytes((body : UnsafeRawBufferPointer) => {
			CC_MD5(body.baseAddress, new UInt32(strData.count), UTSiOS.getPointer(digest))
		})
		let md5String = ""
		digest.forEach((byte : UInt8) => {
			md5String += new String(format = "%02x", new UInt8(byte))
		})

		return md5String
	}


	private isSandBoxPath(path : string) : boolean {
		return path.startsWith(NSHomeDirectory())
	}

	private getTempPath() : string {
		let cacheDirectory = FileManager.default.urls(for = FileManager.SearchPathDirectory.cachesDirectory, in = FileManager.SearchPathDomainMask.userDomainMask).first!
		return cacheDirectory.path
	}

	private getRealPath() : string {
		return this.getTempPath() + "/uni-download/"
	}


	private getTargetPath(options : DownloadFileOptions | null, fileName : string, listener : NetworkDownloadFileListener | null) : string | null {
		let targetPath = ""
		let specifyPath = options?.filePath ?? ""
		if(specifyPath.startsWith("unifile://")){
			specifyPath = UTSiOS.getResourceAbsolutePath(specifyPath, null);
		}
		let hasFileName = false;
		if (specifyPath != "") {
			const pos = specifyPath.lastIndexOf("/")
			if (pos == specifyPath.length - 1) {
				//如果filePath是目录
				if (this.isSandBoxPath(specifyPath)) {
					targetPath = specifyPath
				} else {
					targetPath = this.getTempPath() + "/" + specifyPath
				}
			} else {
				let path = "";
				if (this.isSandBoxPath(specifyPath)) {
					path = specifyPath;
				} else {
					path = this.getTempPath() + "/" + specifyPath
				}
				let fileManager = FileManager.default
				var isDirectory : ObjCBool = false
				if (fileManager.fileExists(atPath = path, isDirectory = UTSiOS.getPointer(isDirectory))) {
					if (isDirectory.boolValue) {
						let error = new NSError(domain = "The target file path is already a directory file, and file creation failed.", code = 602001);
						listener?.onFail(error);
						return null
					}
				}
				targetPath = path
				hasFileName = true
			}
		} else {
			targetPath = this.getRealPath()
		}
		if(!hasFileName){
			targetPath += fileName
		}
		let fileManager = FileManager.default
		if (fileManager.fileExists(atPath = targetPath)) {
			const index = targetPath.lastIndexOf(".");
			let tFileName = targetPath;
			let tFileType = "";
			if (index >= 0) {
				tFileName = targetPath.substring(0, index as Int)
				tFileType = targetPath.substring(index as Int)
			}

			var number = 1
			while (fileManager.fileExists(atPath = targetPath)) {
				targetPath = tFileName + `(${number})` + tFileType;
				number++
			}
		}

		return targetPath
	}

	private getFileName(fileName : string, url : URL | null) : string {
		var suggestedFilename = fileName

		if (suggestedFilename != "") {
			let cString = suggestedFilename.cString(using = String.Encoding.isoLatin1)
			if (cString != null) {
				suggestedFilename = new String(cString = cString!, encoding = String.Encoding.utf8) ?? suggestedFilename
			}

			let cleanUri = suggestedFilename.removingPercentEncoding
			if (cleanUri != null && cleanUri!.length > 0) {
				suggestedFilename = cleanUri!
			}

			suggestedFilename = suggestedFilename.replacingOccurrences(of = "/", with = "")
			suggestedFilename = suggestedFilename.replacingOccurrences(of = "\\", with = "")
		} else {
			if (url == null) {
				suggestedFilename = ""
			} else {
				suggestedFilename = this.convertToMD5(url!.absoluteString)
			}
		}

		if (suggestedFilename.length > 255) {
			let extensionType = (suggestedFilename as NSString).pathExtension
			suggestedFilename = this.convertToMD5((suggestedFilename as NSString).deletingPathExtension)
			if (extensionType != "") {
				suggestedFilename = (suggestedFilename as NSString).appendingPathExtension(extensionType) ?? suggestedFilename
			}
		}

		return suggestedFilename
	}


	//mark --- URLSessionDownloadDelegate

	urlSession(session : URLSession, @argumentLabel("") downloadTask : URLSessionDownloadTask, @argumentLabel("didWriteData") bytesWritten : Int64, @argumentLabel("") totalBytesWritten : Int64, @argumentLabel("") totalBytesExpectedToWrite : Int64) {
		let listener = this.taskMap.get(downloadTask);
		const progress = (Number.from(totalBytesWritten) / totalBytesExpectedToWrite) * 100
		const progressUpdate : OnProgressDownloadResult = {
			progress: progress.toInt(),
			totalBytesWritten: totalBytesWritten,
			totalBytesExpectedToWrite: totalBytesExpectedToWrite
		}
		listener?.onProgress(progressUpdate)
	}

	urlSession(session : URLSession, @argumentLabel("") downloadTask : URLSessionDownloadTask, @argumentLabel("didFinishDownloadingTo") location : URL) {
		let listener = this.taskMap.get(downloadTask);
		let suggestedFilename = downloadTask.response?.suggestedFilename
		const statusCode = (downloadTask.response as HTTPURLResponse).statusCode
		if(statusCode - 200 < 100 && statusCode - 200 >= 0) {
			const fileName = this.getFileName(suggestedFilename ?? "", downloadTask.response?.url)
			let destPath = this.getTargetPath(listener?.options, fileName, listener);
			if (destPath != null) {
				let fileManager = FileManager.default
				try {
					let directoryPath = (destPath as NSString).deletingLastPathComponent
					if (!fileManager.fileExists(atPath = directoryPath)) {
						UTSiOS.try(fileManager.createDirectory(atPath = directoryPath, withIntermediateDirectories = true, attributes = null), "?")
					}
					UTSiOS.try(fileManager.moveItem(atPath = location.path, toPath = destPath!), "?")
					listener?.onFinished(downloadTask.response as HTTPURLResponse, destPath!);
				} catch {
					let error = new NSError(domain = "file move fail", code = 602001);
					listener?.onFail(error);
				}
			}
		} else {
			let error = new NSError(domain = "request fail", code = statusCode);
			listener?.onFail(error);
		}
		this.taskMap.delete(downloadTask);
	}
	
	urlSession(session : URLSession, @argumentLabel("") task : URLSessionTask, @argumentLabel("didCompleteWithError") error : NSError | null) {
		if(error != null){
			let listener = this.taskMap.get(task);
			listener?.onFail(error as NSError);
			this.taskMap.delete(task);
		}
	}
}