import { encoding as textEncoding } from '../text';
import { getFileExtension, iOSNativeHelper } from '../utils';

// TODO: Implement all the APIs receiving callback using async blocks
// TODO: Check whether we need try/catch blocks for the iOS implementation
export class FileSystemAccess {
	public getLastModified(path: string): Date {
		const fileManager = NSFileManager.defaultManager;
		const attributes = fileManager.attributesOfItemAtPathError(path);

		if (attributes) {
			return attributes.objectForKey('NSFileModificationDate');
		} else {
			return new Date();
		}
	}

	public getFileSize(path: string): number {
		const fileManager = NSFileManager.defaultManager;
		const attributes = fileManager.attributesOfItemAtPathError(path);
		if (attributes) {
			return attributes.objectForKey('NSFileSize');
		} else {
			return 0;
		}
	}

	public getParent(path: string, onError?: (error: any) => any): { path: string; name: string } {
		try {
			const fileManager = NSFileManager.defaultManager;
			const nsString = NSString.stringWithString(path);

			const parentPath = nsString.stringByDeletingLastPathComponent;
			const name = fileManager.displayNameAtPath(parentPath);

			return {
				path: parentPath.toString(),
				name: name,
			};
		} catch (exception) {
			if (onError) {
				onError(exception);
			}

			return undefined;
		}
	}

	public getFile(path: string, onError?: (error: any) => any): { path: string; name: string; extension: string } {
		try {
			const fileManager = NSFileManager.defaultManager;
			const exists = fileManager.fileExistsAtPath(path);

			if (!exists) {
				const parentPath = this.getParent(path, onError).path;
				if (!fileManager.createDirectoryAtPathWithIntermediateDirectoriesAttributesError(parentPath, true, null) || !fileManager.createFileAtPathContentsAttributes(path, null, null)) {
					if (onError) {
						onError(new Error("Failed to create file at path '" + path + "'"));
					}

					return undefined;
				}
			}

			const fileName = fileManager.displayNameAtPath(path);

			return {
				path: path,
				name: fileName,
				extension: this.getFileExtension(path),
			};
		} catch (exception) {
			if (onError) {
				onError(exception);
			}

			return undefined;
		}
	}

	public getFolder(path: string, onError?: (error: any) => any): { path: string; name: string } {
		try {
			const fileManager = NSFileManager.defaultManager;
			const exists = this.folderExists(path);

			if (!exists) {
				try {
					fileManager.createDirectoryAtPathWithIntermediateDirectoriesAttributesError(path, true, null);
				} catch (ex) {
					if (onError) {
						onError(new Error("Failed to create folder at path '" + path + "': " + ex));
					}

					return undefined;
				}
			}

			const dirName = fileManager.displayNameAtPath(path);

			return {
				path: path,
				name: dirName,
			};
		} catch (ex) {
			if (onError) {
				onError(new Error("Failed to create folder at path '" + path + "'"));
			}

			return undefined;
		}
	}

	public getExistingFolder(path: string, onError?: (error: any) => any): { path: string; name: string } {
		try {
			const fileManager = NSFileManager.defaultManager;
			const exists = this.folderExists(path);

			if (exists) {
				const dirName = fileManager.displayNameAtPath(path);

				return {
					path: path,
					name: dirName,
				};
			}

			return undefined;
		} catch (ex) {
			if (onError) {
				onError(new Error("Failed to get folder at path '" + path + "'"));
			}

			return undefined;
		}
	}

	public eachEntity(path: string, onEntity: (file: { path: string; name: string; extension: string }) => any, onError?: (error: any) => any) {
		if (!onEntity) {
			return;
		}

		this.enumEntities(path, onEntity, onError);
	}

	public getEntities(path: string, onError?: (error: any) => any): Array<{ path: string; name: string; extension: string }> {
		const fileInfos = new Array<{
			path: string;
			name: string;
			extension: string;
		}>();

		const onEntity = function (entity: { path: string; name: string; extension: string }): boolean {
			fileInfos.push(entity);

			return true;
		};

		let errorOccurred;
		const localError = function (error: any) {
			if (onError) {
				onError(error);
			}

			errorOccurred = true;
		};

		this.enumEntities(path, onEntity, localError);

		if (!errorOccurred) {
			return fileInfos;
		}

		return null;
	}

	public fileExists(path: string): boolean {
		const result = this.exists(path);

		return result.exists;
	}

	public folderExists(path: string): boolean {
		const result = this.exists(path);

		return result.exists && result.isDirectory;
	}

	private exists(path: string): { exists: boolean; isDirectory: boolean } {
		const fileManager = NSFileManager.defaultManager;
		const isDirectory = new interop.Reference(interop.types.bool, false);
		const exists = fileManager.fileExistsAtPathIsDirectory(path, isDirectory);

		return { exists: exists, isDirectory: isDirectory.value };
	}

	public concatPath(left: string, right: string): string {
		return NSString.pathWithComponents(<any>[left, right]).toString();
	}

	public deleteFile(path: string, onError?: (error: any) => any) {
		this.deleteEntity(path, onError);
	}

	public deleteFolder(path: string, onError?: (error: any) => any) {
		this.deleteEntity(path, onError);
	}

	public emptyFolder(path: string, onError?: (error: any) => any) {
		const fileManager = NSFileManager.defaultManager;
		const entities = this.getEntities(path, onError);

		if (!entities) {
			return;
		}

		for (let i = 0; i < entities.length; i++) {
			try {
				fileManager.removeItemAtPathError(entities[i].path);
			} catch (ex) {
				if (onError) {
					onError(new Error("Failed to empty folder '" + path + "': " + ex));
				}

				return;
			}
		}
	}

	public rename(path: string, newPath: string, onError?: (error: any) => any) {
		const fileManager = NSFileManager.defaultManager;

		try {
			fileManager.moveItemAtPathToPathError(path, newPath);
		} catch (ex) {
			if (onError) {
				onError(new Error("Failed to rename '" + path + "' to '" + newPath + "': " + ex));
			}
		}
	}

	public getLogicalRootPath(): string {
		const mainBundlePath = NSBundle.mainBundle.bundlePath;
		const resolvedPath = NSString.stringWithString(mainBundlePath).stringByResolvingSymlinksInPath;

		return resolvedPath;
	}

	public getDocumentsFolderPath(): string {
		return this.getKnownPath(NSSearchPathDirectory.DocumentDirectory);
	}
	public getExternalDocumentsFolderPath(): string {
		return this.getDocumentsFolderPath();
	}

	public getTempFolderPath(): string {
		return this.getKnownPath(NSSearchPathDirectory.CachesDirectory);
	}

	public getCurrentAppPath(): string {
		return iOSNativeHelper.getCurrentAppPath();
	}

	public copy = this.copySync.bind(this);

	public copySync(src: string, dest: string, onError?: (error: any) => any) {
		const fileManager = NSFileManager.defaultManager;
		try {
			return fileManager.copyItemAtPathToPathError(src, dest);
		} catch (error) {
			if (error.message.indexOf('exists') > -1) {
				// check the size of file if empty remove then try copying again
				// this could be zero due to using File.fromPath passing in a new file
				let didRemove = false;
				try {
					didRemove = fileManager.removeItemAtPathError(dest);
					return fileManager.copyItemAtPathToPathError(src, dest);
				} catch (exception) {
					if (onError) {
						if (didRemove) {
							onError(error);
						} else {
							onError(exception);
						}
					}
				}
			}
			if (onError) {
				onError(error);
			}
		}

		return false;
	}

	public copyAsync(src: string, dest: string): Promise<boolean> {
		return new Promise<boolean>((resolve, reject) => {
			try {
				NSData.dataWithContentsOfFileCompletion(src, (data) => {
					if (!data) {
						reject(new Error("Failed to read file at path '" + src));
					} else {
						data.writeToFileAtomicallyCompletion(dest, true, () => {
							if (this.fileExists(dest)) {
								const size = this.getFileSize(dest);
								if (size === data.length) {
									resolve(true);
								} else {
									reject(new Error("Failed to write file at path '" + dest));
								}
							} else {
								reject(new Error("Failed to write file at path '" + dest));
							}
						});
					}
				});
			} catch (ex) {
				reject(ex);
			}
		});
	}

	public readText = this.readTextSync.bind(this);

	public readTextAsync(path: string, encoding?: any) {
		const actualEncoding = encoding || textEncoding.UTF_8;

		return new Promise<string>((resolve, reject) => {
			try {
				(NSString as any).stringWithContentsOfFileEncodingCompletion(path, actualEncoding, (result, error) => {
					if (error) {
						reject(error);
					} else {
						resolve(result.toString());
					}
				});
			} catch (ex) {
				reject(new Error("Failed to read file at path '" + path + "': " + ex));
			}
		});
	}

	public readTextSync(path: string, onError?: (error: any) => any, encoding?: any) {
		const actualEncoding = encoding || textEncoding.UTF_8;

		try {
			const nsString = NSString.stringWithContentsOfFileEncodingError(path, actualEncoding);

			return nsString.toString();
		} catch (ex) {
			if (onError) {
				onError(new Error("Failed to read file at path '" + path + "': " + ex));
			}
		}
	}

	public readBuffer = this.readBufferSync.bind(this);

	public readBufferAsync(path: string): Promise<ArrayBuffer> {
		return new Promise<ArrayBuffer>((resolve, reject) => {
			try {
				(NSData as any).dataWithContentsOfFileCompletion(path, (data) => {
					resolve(interop.bufferFromData(data));
				});
			} catch (ex) {
				reject(new Error("Failed to read file at path '" + path + "': " + ex));
			}
		});
	}

	public readBufferSync(path: string, onError?: (error: any) => any): ArrayBuffer {
		try {
			return interop.bufferFromData(NSData.dataWithContentsOfFile(path));
		} catch (ex) {
			if (onError) {
				onError(new Error("Failed to read file at path '" + path + "': " + ex));
			}
		}
	}

	public read = this.readSync.bind(this);

	public readAsync(path: string): Promise<NSData> {
		return new Promise<NSData>((resolve, reject) => {
			try {
				(NSData as any).dataWithContentsOfFileCompletion(path, resolve);
			} catch (ex) {
				reject(new Error("Failed to read file at path '" + path + "': " + ex));
			}
		});
	}

	public readSync(path: string, onError?: (error: any) => any): NSData {
		try {
			return NSData.dataWithContentsOfFile(path);
		} catch (ex) {
			if (onError) {
				onError(new Error("Failed to read file at path '" + path + "': " + ex));
			}
		}
	}

	public writeText = this.writeTextSync.bind(this);

	public writeTextAsync(path: string, content: string, encoding?: any): Promise<void> {
		const nsString = NSString.stringWithString(content);
		const actualEncoding = encoding || textEncoding.UTF_8;

		return new Promise<void>((resolve, reject) => {
			try {
				(nsString as any).writeToFileAtomicallyEncodingCompletion(path, true, actualEncoding, (error) => {
					if (error) {
						reject(error);
					} else {
						resolve();
					}
				});
			} catch (ex) {
				reject(new Error("Failed to write file at path '" + path + "': " + ex));
			}
		});
	}

	public writeTextSync(path: string, content: string, onError?: (error: any) => any, encoding?: any) {
		const nsString = NSString.stringWithString(content);

		const actualEncoding = encoding || textEncoding.UTF_8;

		// TODO: verify the useAuxiliaryFile parameter should be false
		try {
			nsString.writeToFileAtomicallyEncodingError(path, false, actualEncoding);
		} catch (ex) {
			if (onError) {
				onError(new Error("Failed to write to file '" + path + "': " + ex));
			}
		}
	}

	static getBuffer(buffer: ArrayBuffer | Uint8Array | Uint8ClampedArray): NSData {
		if (buffer instanceof ArrayBuffer) {
			return NSData.dataWithData(buffer as any);
		} else {
			const buf = NSData.dataWithData(buffer?.buffer as any);
			const len = buffer.byteLength;
			return NSData.dataWithBytesNoCopyLength((buf.bytes as interop.Pointer).add(buffer?.byteOffset ?? 0), len);
		}
	}

	public appendBuffer = this.appendBufferSync.bind(this);

	public appendBufferAsync(path: string, content: ArrayBuffer | Uint8Array | Uint8ClampedArray): Promise<void> {
		return new Promise<void>((resolve, reject) => {
			try {
				const handle = NSFileHandle.fileHandleForWritingAtPath(path);
				(handle as any).appendDataCompletion(FileSystemAccess.getBuffer(content), (error) => {
					if (error) {
						reject(error);
					} else {
						resolve();
					}
					handle.closeFile();
				});
			} catch (ex) {
				reject(new Error("Failed to write file at path '" + path + "': " + ex));
			}
		});
	}

	public appendBufferSync(path: string, content: ArrayBuffer | Uint8Array | Uint8ClampedArray, onError?: (error: any) => any) {
		try {
			const handle = NSFileHandle.fileHandleForWritingAtPath(path);
			handle.seekToEndOfFile();
			handle.writeData(FileSystemAccess.getBuffer(content));
			handle.closeFile();
		} catch (ex) {
			if (onError) {
				onError(new Error("Failed to write to file '" + path + "': " + ex));
			}
		}
	}

	public append = this.appendSync.bind(this);

	public appendAsync(path: string, content: NSData): Promise<void> {
		return new Promise<void>((resolve, reject) => {
			try {
				const handle = NSFileHandle.fileHandleForWritingAtPath(path);
				(handle as any).appendDataCompletion(content, (error) => {
					if (error) {
						reject(error);
					} else {
						resolve();
					}
					handle.closeFile();
				});
			} catch (ex) {
				reject(new Error("Failed to write file at path '" + path + "': " + ex));
			}
		});
	}

	public appendSync(path: string, content: NSData, onError?: (error: any) => any) {
		try {
			const handle = NSFileHandle.fileHandleForWritingAtPath(path);
			handle.seekToEndOfFile();
			handle.writeData(content);
			handle.closeFile();
		} catch (ex) {
			if (onError) {
				onError(new Error("Failed to write to file '" + path + "': " + ex));
			}
		}
	}

	public appendText = this.appendTextSync.bind(this);

	public appendTextAsync(path: string, content: string, encoding?: any): Promise<void> {
		const nsString = NSString.stringWithString(content);
		const actualEncoding = encoding || textEncoding.UTF_8;

		return new Promise<void>((resolve, reject) => {
			try {
				const data = nsString.dataUsingEncoding(actualEncoding);
				const handle = NSFileHandle.fileHandleForWritingAtPath(path);
				(handle as any).appendDataCompletion(data, (error) => {
					if (error) {
						reject(error);
					} else {
						resolve();
					}
					handle.closeFile();
				});
			} catch (ex) {
				reject(new Error("Failed to append file at path '" + path + "': " + ex));
			}
		});
	}

	public appendTextSync(path: string, content: string, onError?: (error: any) => any, encoding?: any) {
		const nsString = NSString.stringWithString(content);

		const actualEncoding = encoding || textEncoding.UTF_8;

		// TODO: verify the useAuxiliaryFile parameter should be false
		try {
			const data = nsString.dataUsingEncoding(actualEncoding);
			const handle = NSFileHandle.fileHandleForWritingAtPath(path);
			handle.seekToEndOfFile();
			handle.writeData(data);
			handle.closeFile();
		} catch (ex) {
			if (onError) {
				onError(new Error("Failed to append to file '" + path + "': " + ex));
			}
		}
	}

	public writeBuffer = this.writeBufferSync.bind(this);

	public writeBufferAsync(path: string, content: ArrayBuffer | Uint8Array | Uint8ClampedArray): Promise<void> {
		return new Promise<void>((resolve, reject) => {
			try {
				FileSystemAccess.getBuffer(content).writeToFileAtomicallyCompletion(path, true, () => {
					resolve();
				});
			} catch (ex) {
				reject(new Error("Failed to write file at path '" + path + "': " + ex));
			}
		});
	}

	public writeBufferSync(path: string, content: ArrayBuffer | Uint8Array | Uint8ClampedArray, onError?: (error: any) => any) {
		try {
			FileSystemAccess.getBuffer(content).writeToFileAtomically(path, true);
		} catch (ex) {
			if (onError) {
				onError(new Error("Failed to write to file '" + path + "': " + ex));
			}
		}
	}

	public write = this.writeSync.bind(this);

	public writeAsync(path: string, content: NSData): Promise<void> {
		return new Promise<void>((resolve, reject) => {
			try {
				(content as any).writeToFileAtomicallyCompletion(path, true, () => {
					resolve();
				});
			} catch (ex) {
				reject(new Error("Failed to write file at path '" + path + "': " + ex));
			}
		});
	}

	public writeSync(path: string, content: NSData, onError?: (error: any) => any) {
		try {
			content.writeToFileAtomically(path, true);
		} catch (ex) {
			if (onError) {
				onError(new Error("Failed to write to file '" + path + "': " + ex));
			}
		}
	}

	private getKnownPath(folderType: number): string {
		const fileManager = NSFileManager.defaultManager;
		const paths = fileManager.URLsForDirectoryInDomains(folderType, NSSearchPathDomainMask.UserDomainMask);

		const url = paths.objectAtIndex(0);

		return url.path;
	}

	public getFileExtension(path: string): string {
		return getFileExtension(path);
	}

	private deleteEntity(path: string, onError?: (error: any) => any) {
		const fileManager = NSFileManager.defaultManager;
		try {
			fileManager.removeItemAtPathError(path);
		} catch (ex) {
			if (onError) {
				onError(new Error("Failed to delete file at path '" + path + "': " + ex));
			}
		}
	}

	private enumEntities(path: string, callback: (entity: { path: string; name: string; extension: string }) => boolean, onError?: (error) => any) {
		try {
			const fileManager = NSFileManager.defaultManager;
			let files: NSArray<string>;
			try {
				files = fileManager.contentsOfDirectoryAtPathError(path);
			} catch (ex) {
				if (onError) {
					onError(new Error("Failed to enum files for folder '" + path + "': " + ex));
				}

				return;
			}

			for (let i = 0; i < files.count; i++) {
				const file = files.objectAtIndex(i);

				const info = {
					path: this.concatPath(path, file),
					name: file,
					extension: '',
				};

				if (!this.folderExists(this.joinPath(path, file))) {
					info.extension = this.getFileExtension(info.path);
				}

				const retVal = callback(info);
				if (retVal === false) {
					// the callback returned false meaning we should stop the iteration
					break;
				}
			}
		} catch (ex) {
			if (onError) {
				onError(ex);
			}
		}
	}

	public getPathSeparator(): string {
		return '/';
	}

	public normalizePath(path: string): string {
		const nsString: NSString = NSString.stringWithString(path);
		const normalized = nsString.stringByStandardizingPath;

		return normalized;
	}

	public joinPath(left: string, right: string): string {
		const nsString: NSString = NSString.stringWithString(left);

		return nsString.stringByAppendingPathComponent(right);
	}

	public joinPaths(paths: string[]): string {
		return iOSNativeHelper.joinPaths(...paths);
	}
}

// stub to avoid cross platform warning
export class FileSystemAccess29 extends FileSystemAccess {}
