/* $Header$ */
// Contains the Omnis JavaScript worker ZIP module implementation
// Copyright (C) OLS Holdings Ltd 2018

/*********************************** Changes History **********************************
Date			Edit				Bug					Description
28-Mar-19	jmg_unmarked						Updated to use relative path for simple modules.
21-Nov-18	rmm_jsw									OW3 component - JavaScript Worker.
**************************************************************************************/

const omnis_calls = require('omnis_calls');
const fs = require('fs');
const JSZip = require("jszip");

// Map of methods that can be called in this module
// Note that the instance of methodMapClass contains the ZIP object with which the caller is working, so you should be aware
// of this and only share the Omnis worker object where it makes sense e.g. when multiple callers are reading the same ZIP file
class methodMapClass {
	constructor() {
		this.zip = null;
	}

	newZip(obj, response) {
		// Create a new empty ZIP file
		// obj is an empty object, as no parameters are required
		this.zip = new JSZip();
		omnis_calls.sendResponse({ok:true}, response);
		return true;
	}

	addFolder(obj, response) {
		// Add a folder to the ZIP file
		// Folder paths are relative to the top of the ZIP file, so start with a name
		// This must be followed by additional folder names.  Intermediate folders which do not exist are also created
		var parts = obj.path.split('/');
		var parent = this.zip;
		for (const folder of parts){
			if (folder.length > 0)
				parent = parent.folder(folder);
		}		
		omnis_calls.sendResponse({ok:true, folder:obj.path}, response);
		return true;
	}

	addFiles(obj, response) {
		// Add a file or files to the ZIP file
		function getOptions(pathname)
		{
			var options = {};
			var stats = fs.lstatSync(pathname);
			options.date = new Date(stats.mtime);
			if (process.platform == "win32")
				options.dosPermissions = (stats.mode & 0o200) ? 0 : 1;	// 0o200: owner write bit - if zero, the file is read-only, dos permission bit 1 (see https://docs.microsoft.com/en-us/uwp/api/windows.storage.fileattributes)
			else
				options.unixPermissions = stats.mode;
			return options;
		}
		if (Array.isArray(obj)) {
			// In this case, adding files
			// The object is an array
			// Each array member is also an array, where the first member is the destination file path in the ZIP file e.g. folder1/folder2/test.txt, and the
			// second member is the full pathname of the input file.
			// You can pass this to $callmethod in the Omnis code by for example doing:
			/*
			  Do FileOps.$filelist(kFileOpsIncludeFiles,iParentPath,kFileOpsInfoFullName+kFileOpsInfoName) Returns lFileList
				Calculate lFileList.$cols.name.$name as "path"
				Calculate lFileList.$cols.fullname.$name as "inputFile"
				Calculate lLineCount as lFileList.$linecount
				For lLineNumber from 1 to lLineCount
					Calculate lFileList.[lLineNumber].path as con(iAllFilesPath,"/",lFileList.[lLineNumber].path)
				End For
				Do iJS.$callmethod("zip","addFiles",lFileList,kTrue) Returns #1
			*/
			// where iAllFilesPath is the destination folder in the ZIP file
			for (const fileToAdd of obj){
				this.zip.file(fileToAdd[0], fs.createReadStream(fileToAdd[1]), getOptions(fileToAdd[1]));
			}
		} else {
			// In this case adding a single file, specifying its destination path in the ZIP file as obj.path, and the full pathname of the input file as obj.inputFile
			this.zip.file(obj.path, fs.createReadStream(obj.inputFile), getOptions(obj.inputFile));
		}
		omnis_calls.sendResponse({ok:true, file:obj.path}, response);
		return true;
	}

	saveZip(obj, response) {
		// Save the ZIP file to the path specified in obj.outputFile, using compression level specified in obj.compressionLevel
		if (!obj.outputFile || !obj.outputFile.length){
			omnis_calls.sendResponse({ok:false});
			return true;
		}

		var options = {};
		options.type = 'nodebuffer';
		options.streamFiles = true;
		options.compression = "DEFLATE";
		if (!obj.compressionLevel)
			obj.compressionLevel = 6;
		options.compressionOptions = {level: obj.compressionLevel};
		options.platform = process.platform;

		this.zip.generateNodeStream(options).pipe(fs.createWriteStream(obj.outputFile)).on('finish', function () {
			omnis_calls.sendResponse({ok:true, file:obj.outputFile}, response);
		});
		return true;
	}

	loadZip(obj, response) {
		// Load the ZIP file at path obj.path
		// Generates a content list as the return value passed to $methodreturn
		var _this = this;
		fs.readFile(obj.path, function (err, data) {
			if (err) {
				omnis_calls.sendError(response, 400, err.toString());
			} else {
				JSZip.loadAsync(data).then(function (loadedZip) {
					_this.zip = loadedZip;
					// Build a list of the top-level contents
					var contents = [];
					_this.zip.folder("").forEach(function (relativePath, file) {
						var fileObj = {};
						fileObj.path = file.name;
						fileObj.dir = file.dir;
						fileObj.date = file.date;
						fileObj.comment = file.comment;
						fileObj.uncompressedSize = file._data.uncompressedSize;
						fileObj.compressedSize = file._data.compressedSize;
						fileObj.unixPermissions = file.unixPermissions;
						fileObj.dosPermissions = file.dosPermissions;

						contents.push(fileObj);
					});
					omnis_calls.sendResponse(contents, response);
				}).catch(function (err) {
					omnis_calls.sendError(response, 400, err.toString());
				});
			}
		});
		return true;
	}

	extractFile(obj, response) {
		// Extracts a file from the ZIP file (at path obj.fullPath in the ZIP) and sends its data to $methodreturn
		this.zip.file(obj.path).async("uint8array").then(function (data) {
			omnis_calls.sendResponseBuffer(Buffer.from(data), "application/octet-stream", response);
		}).catch(function (err) {
			omnis_calls.sendError(response, 400, err.toString());
		});
		return true;
	}
};

var zipModule = {
	methodMap: new methodMapClass(),

	call: function (method, obj, response) {
		return this.methodMap[method](obj, response);
	}
};

module.exports = zipModule;
