/**
* MDS JS lib for MiniDAPPs..
* 
* VERSION : 2.1.0
*
* @spartacusrex
*/

/**
 * The MAIN Minima Callback function 
 */
var MDS_MAIN_CALLBACK = null;

/**
 * API call id array
 */
var API_CALLS = [];

/**
 * Main MINIMA Object for all interaction
 */
var MDS = {
	
	//Main File host
	filehost : "",
	
	//RPC Host for Minima
	mainhost : "",
	
	//The MiniDAPP UID
	minidappuid : "",
	
	//Is logging RPC enabled
	logging : false,
	
	//When debuggin you can hard set the Host and port
	DEBUG_HOST : null,
	DEBUG_PORT : -1,
	
	//An allowed TEST Minidapp ID for SQL - can be overridden
	DEBUG_MINIDAPPID : "0x00",
	
	/**
	 * Minima Startup - with the callback function used for all Minima messages
	 */
	init : function(callback){
		//Log a little..
		MDS.log("Initialising Internal MDS..");
		
		//Is logging enabled.. via the URL
		if(MDS.form.getParams("MDS_LOGGING") != null){
			MDS.logging = true;
		}
		
		//Get the host and port..
		var host = window.location.hostname;
		var port =  Math.floor(window.location.port);
		
		//Get ther MiniDAPP UID
		MDS.minidappuid = MDS.form.getParams("uid");
		
		//HARD SET if debug mode - running from a file
		if(MDS.DEBUG_HOST != null){
			MDS.log("DEBUG Settings Found..");
		
			host=MDS.DEBUG_HOST;
			port=MDS.DEBUG_PORT;	
		}
		
		if(MDS.minidappuid == null){
			MDS.minidappuid = MDS.DEBUG_MINIDAPPID;
		}
		// Connect-node from another origin: node may return 500 for megapoll/cmd with 0x00 when publicmds is off.
		if(MDS.DEBUG_HOST != null && typeof window.__STABLES_MDS_SESSION_UID === 'string'){
			var suv = window.__STABLES_MDS_SESSION_UID.trim();
			if(suv){
				var um = suv.match(/[?&]uid=(0x[0-9a-fA-F]+)/);
				if(um){ suv = um[1]; }
				if(!/^0x[0-9a-fA-F]{16,}$/.test(suv)){
					var pm = suv.match(/(?:^|[\/?#&=])(0x[0-9a-fA-F]{16,})/);
					if(pm){ suv = pm[1]; }
				}
				if(/^0x[0-9a-fA-F]{16,}$/.test(suv)){
					MDS.minidappuid = suv;
				}
			}
		}
		
		//Is one specified..
		if(MDS.minidappuid == "0x00"){
			MDS.log("No MiniDAPP UID specified.. using test value");
		}
		
		//Are we HTTP or HTTPS (Connect-node / file:// must still hit Minima MDS on HTTPS :9003)
		var useHttps = window.location.protocol.startsWith("https");
		if(MDS.DEBUG_HOST != null){
			var dbgPort = Math.floor(Number(port));
			if(dbgPort === 9003){
				useHttps = true;
			}
		}
		if(useHttps){
			if(port == 0){
				MDS.filehost = "https://"+host+"/";
				MDS.mainhost = "https://"+host+"/mdscommand_/";
			}else{
				MDS.filehost = "https://"+host+":"+port+"/";
				MDS.mainhost = "https://"+host+":"+port+"/mdscommand_/";	
			}
			
		}else{
			if(port == 0){
				MDS.filehost = "http://"+host+"/";
				MDS.mainhost = "http://"+host+"/mdscommand_/";
			}else{
				MDS.filehost = "http://"+host+":"+port+"/";
				MDS.mainhost = "http://"+host+":"+port+"/mdscommand_/";	
			}
		}
		
		MDS.log("MDS HOST  : "+MDS.filehost);
		
		//Store this for poll messages
		MDS_MAIN_CALLBACK = callback;
		
		//Run the POll Listener ONCE - to get the latest Poll Counter..
		PollListener();
		
		//Now call it every ~2 secs..
		setInterval(function(){PollListener();},2500);
		
		//And Post a message
		MDSPostMessage({ "event": "inited" });
	},
	
	/**
	 * Log some data with a timestamp in a consistent manner to the console
	 */
	log : function(output){
		console.log("Minima @ "+new Date().toLocaleString()+" : "+output);
	},
	
	/**
	 * Notify the User - on Phone it pops up in status bar. On desktop appears in Logs
	 */
	notify : function(output){
		//Send via POST
		httpPostAsync("notify", output);
	},
	
	/**
	 * Cancel this MiniDAPPs notification
	 */
	notifycancel : function(){
		//Send via POST
		httpPostAsync("notifycancel", "*");
	},
	
	/**
	 * Runs a function on the Minima Command Line - same format as MInima
	 */
	cmd : function(command, callback){
		//Send via POST
		httpPostAsync("cmd", command, callback);
	},
	
	/**
	 * Runs a SQL command on this MiniDAPPs SQL Database
	 */
	sql : function(command, callback){
		//Send via POST
		httpPostAsync("sql", command, callback);
	},
	
	/**
	 * Get a link to a different Dapp. READ dapps can only get READ DAPPS. WRITE can get all dapps.
	 */
	dapplink : function(dappname, callback){
		//Send via POST
		httpPostAsync("dapplink", dappname, function(result){
			
			var linkdata 	= {};
			linkdata.status = result.status;
			 
			//Create the link..
			if(result.status){
				linkdata.uid 		= result.response.uid;
				linkdata.sessionid 	= result.response.sessionid;
				linkdata.base 		= MDS.filehost+linkdata.uid+"/index.html?uid="+result.response.sessionid;
			}else{
				//Not found..
				linkdata.error = result.error;
			}
			
			callback(linkdata);
		});
	},
	
	/**
	 * API call to a different minidapp
	 */
	api : {
		
		call : function(dappname, data, callback){
		
			//Construct a unique API request
			var rand = ""+Math.random()*1000000000;
			
			//Construct a callback list object
			var callitem 		= {};
			callitem.id 		= rand;
			callitem.callback 	= callback;
			
			//Add to the api calls..
			API_CALLS.push(callitem);
			
			//Create the single line
			var commsline = dappname+"&request&"+rand+"&"+data;		
			
			//Send via POST
			httpPostAsync("api", commsline);
		},
		
		reply : function(dappname, id, data, callback){
			
			//Create the single line
			var commsline = dappname+"&response&"+id+"&"+data;		
			
			//Send via POST
			httpPostAsync("api", commsline, callback);
		} 	
	},
	
	/**	
	 * Network Commands
	 */
	net : {
		
		/**
		 * Make a GET request
		 */
		GET : function(url, callback){
			//Send via POST
			httpPostAsync("net", url, callback);	
		},
		
		/**
		 * Make a GET request WITH a Basic Auth token header
		 */
		GETAUTH : function(url, basicauth, callback){
			
			//Create the single line
			var commsline = url+"&"+basicauth;
			
			//Send via POST
			httpPostAsync("netauth", commsline, callback);	
		},
		
		/**
		 * Make a POST request
		 */
		POST : function(url, data, callback){
			
			//Create the sinlg eline version..
			var postline = url+"&"+data;
			
			//Send via POST
			httpPostAsync("netpost", postline, callback);	
		}
		
	},
	
	/**	
	 *  Simple GET and SET key value pairs that are saved persistently
	 */
	keypair : {
		
		/**
		 * GET a value
		 */
		get : function(key, callback){
			
			//Create the single line
			var commsline = "get&"+key;		
			
			//Send via POST
			httpPostAsync("keypair", commsline, callback);	
		},
		
		/**
		 * SET a value
		 */
		set : function(key, value, callback){
			
			//Create the single line
			var commsline = "set&"+key+"&"+value;		
			
			//Send via POST
			httpPostAsync("keypair", commsline, callback);	
		}
	},
	
	/**	
	 * COMMS - send a message to ALL minidapps or JUST your own service.js
	 */
	comms : {
		
		/**
		 * PUBLIC message broadcast to ALL (callback is optional)
		 */
		broadcast : function(msg, callback){
			
			//Create the single line
			var commsline = "public&"+msg;		
			
			//Send via POST
			httpPostAsync("comms", commsline, callback);	
		},
		
		/**
		 * PRIVATE message send just to this MiniDAPP (callback is optional)
		 */
		solo : function(msg, callback){
			
			//Create the single line
			var commsline = "private&"+msg;		
			
			//Send via POST
			httpPostAsync("comms", commsline, callback);	
		}
		
	},
	
	/**
	 * File access
	 */
	file : {
		/**
		 * List file in a folder .. start at /
		 */
		list : function(folder, callback){
			
			//Create the single line
			var commsline = "list&"+folder;		
			
			//Send via POST
			httpPostAsync("file", commsline, callback);
		},
		
		/**
		 * Save text - can be text, a JSON in string format or hex encoded data
		 */
		save : function(filename, text, callback){
			
			//Create the single line
			var commsline = "save&"+filename+"&"+text;		
			
			//Send via POST
			httpPostAsync("file", commsline, callback);
		},
		
		/**
		 * Save Binary Data - supply as a HEX string
		 */
		savebinary : function(filename, hexdata, callback){
			
			//Create the single line
			var commsline = "savebinary&"+filename+"&"+hexdata;		
			
			//Send via POST
			httpPostAsync("file", commsline, callback);
		},
		
		/**
		 * Load text - can be text, a JSON in string format or hex encoded data
		 */
		load : function(filename, callback){
			
			//Create the single line
			var commsline = "load&"+filename;		
			
			//Send via POST
			httpPostAsync("file", commsline, callback);
		},
		
		/**
		 * Load Binary data - returns the HEX data
		 */
		loadbinary : function(filename, callback){
			
			//Create the single line
			var commsline = "loadbinary&"+filename;		
			
			//Send via POST
			httpPostAsync("file", commsline, callback);
		},
		
		/**
		 * Delete a file
		 */
		delete : function(filename, callback){
			
			//Create the single line
			var commsline = "delete&"+filename;		
			
			//Send via POST
			httpPostAsync("file", commsline, callback);
		},
		
		/**
		 * Get the full path - if you want to run a command on the file / import a txn / unsigned txn etc
		 */
		getpath : function(filename, callback){
			
			//Create the single line
			var commsline = "getpath&"+filename;		
			
			//Send via POST
			httpPostAsync("file", commsline, callback);
		},
		
		/**
		 * Make a directory
		 */
		makedir : function(filename, callback){
			
			//Create the single line
			var commsline = "makedir&"+filename;		
			
			//Send via POST
			httpPostAsync("file", commsline, callback);
		},
		
		/**
		 * Copy a file
		 */
		copy : function(filename, newfilename, callback){
			
			//Create the single line
			var commsline = "copy&"+filename+"&"+newfilename;		
			
			//Send via POST
			httpPostAsync("file", commsline, callback);
		},
		
		/**
		 * Move a file
		 */
		move : function(filename, newfilename, callback){
			
			//Create the single line
			var commsline = "move&"+filename+"&"+newfilename;		
			
			//Send via POST
			httpPostAsync("file", commsline, callback);
		},
		
		/**
		 * Download a File from the InterWeb - Will be put in Downloads folder
		 */
		download : function(url, callback){
			
			//Create the single line
			var commsline = "download&"+url;
			
			//Send via POST
			httpPostAsync("file", commsline, callback);	
		},
		
		/**
		 * Upload a file in chunks to the /fileupload folder		
		 */
		upload : function(file, callback){
			
			//Start the file recursion..
			_recurseUploadMDS(file,0,callback);
		},
		
		/**
		 * List file in a folder .. start at /
		 */
		listweb : function(folder, callback){
			
			//Create the single line
			var commsline = "listweb&"+folder;		
			
			//Send via POST
			httpPostAsync("file", commsline, callback);
		},
		
		/**
		 * Copy a file to your web folder
		 */
		copytoweb : function(file, webfile, callback){
			
			//Create the single line
			var commsline = "copytoweb&"+file+"&"+webfile;
			
			//Send via POST
			httpPostAsync("file", commsline, callback);	
		},
		
		/**
		 * Delete a file or folder from web folder
		 */
		deletefromweb : function(file, callback){
			
			//Create the single line
			var commsline = "deletefromweb&"+file;
			
			//Send via POST
			httpPostAsync("file", commsline, callback);	
		}
		
	}, 
	
	/**
	 * Function for GET parameters..
	 */
	form : {
		
		//Return the GET parameter by scraping the location..
		getParams : function(parameterName){
			    var result = null,
		        tmp = [];
			    var items = window.location.search.substr(1).split("&");
			    for (var index = 0; index < items.length; index++) {
			        tmp = items[index].split("=");
			        //console.log("TMP:"+tmp);
				   if (tmp[0] === parameterName) result = decodeURIComponent(tmp[1]);
			    }
			    return result;
		}		
	},
	
	/**
	 * UTILITY functions.. very useful
	 */
	util : {
		
		//Convert HEX to Base 64 - removes the 0x if necessary
		hexToBase64(hexstring) {
			//Check if starts with 0x
			var thex = hexstring;
			if(hexstring.startsWith("0x")){
				thex = hexstring.substring(2);
			}	
			
		    return btoa(thex.match(/\w{2}/g).map(function(a) {
		        return String.fromCharCode(parseInt(a, 16));
		    }).join(""));
		},
	
		//Convert Base64 to HEX	
		base64ToHex(str) {
			const raw = atob(str);
			let result = '';
			for (let i = 0; i < raw.length; i++) {
				const hex = raw.charCodeAt(i).toString(16);
				result += (hex.length === 2 ? hex : '0' + hex);
			}
			return result.toUpperCase();
		},
		
		//Convert Base64 to a Uint8Array - useful for Blobs
		base64ToArrayBuffer(base64) {
		    var binary_string = window.atob(base64);
		    var len = binary_string.length;
		    var bytes = new Uint8Array(len);
		    for (var i = 0; i < len; i++) {
		        bytes[i] = binary_string.charCodeAt(i);
		    }
		    return bytes.buffer;
		},
		
		//Return a state variable given the coin
		getStateVariable(coin,port){
			
			//Get the state vars
			var statvars = coin.state;
			var len = statvars.length;
			for (var i = 0; i < len; i++) {
				var state = statvars[i];
				if(state.port == port){
					return state.data;
				} 	
			}
			
			return undefined;
		}
	}
};

/**
 * Post a message to the Minima Event Listeners
 */
function MDSPostMessage(json){
   //And dispatch
   if(MDS_MAIN_CALLBACK){
		
		//Is this an API response call..
		if(json.event == "MDSAPI"){
			
			//Check if it is a response..
			if(!json.data.request){
				
				//Find the API CALL Object
				var found = "";
				var len = API_CALLS.length;
				for(var i=0;i<len;i++){
					if(API_CALLS[i].id == json.data.id){
						//found it..!
						found = json.data.id;
						
						//Construct a reply..
						var reply 	 = {};
						reply.status = json.data.status; 
						reply.data 	 = json.data.message;
						
						API_CALLS[i].callback(reply);
					}
				}	
				
				//Remove it..
				if(found != ""){
					API_CALLS = API_CALLS.filter(function(apic){
						return apic.id != found; 
					});
				}else{
					//MDS.log("API CALL NOT FOUND!"+JSON.stringify(json));	
				}
				
				//Response messages not forwarded - only via API call
				return;	
			}
		}
		
		//Call the main function
		MDS_MAIN_CALLBACK(json);	
   }      
}


var PollCounter = 0;
var PollSeries  = 0;
function PollListener(){
	
	//The POLL host
	var pollhost = MDS.mainhost+"megapoll?"+"uid="+MDS.minidappuid;
	var polldata = "series="+PollSeries+"&counter="+PollCounter;
	
	httpPostAsyncPoll(pollhost,polldata,function(msg){
		
		//Are we on the right Series..
		if(PollSeries != msg.series){
			
			//Reset to the right series.. 
			PollSeries  = msg.series;
			PollCounter = msg.counter;
			
		}else{
			
			//Is there a message ?
			if(msg.status == true){
				
				//Get all the messgaes
				var num = msg.response.length;
				for(var i=0;i<num;i++){
					
					//Get the message
					var mdsmsg = msg.response[i];
						
					//Get the current counter..
					PollCounter = mdsmsg.counter+1;
					
					//And Post the message..
					MDSPostMessage(mdsmsg.message);		
				}	
			}	
		}
	});
}

function postMDSFail(command, params, status){
	//Some error..
	if(MDS.logging){
		MDS.log("** An error occurred during an MDS command!");
	}
	
	//Create the message
	var errormsg = {};
	errormsg.event = "MDSFAIL";
	errormsg.data = {};
	errormsg.data.command 	= command;
	errormsg.data.params 	= params;
	errormsg.data.error 	= status;
				
	//Post it to the stack			
	MDSPostMessage(errormsg);
}

/**
 * Utility function for GET request
 * 
 * @param theUrl
 * @param callback
 * @param params
 * @returns
 */
function httpPostAsync(theUrl, params, callback){
	
	//Add the MiniDAPP UID..
	var finalurl = MDS.mainhost+theUrl+"?uid="+MDS.minidappuid;
	
	//Do we log it..
	if(MDS.logging){
		MDS.log("POST_RPC:"+finalurl+" PARAMS:"+params);
	}

	var xmlHttp = new XMLHttpRequest();
    xmlHttp.onreadystatechange = function() { 
        
		var status = xmlHttp.status;
		if (xmlHttp.readyState == XMLHttpRequest.DONE){
			if (status === 0 || (status >= 200 && status < 400)) {
			
				//Do we log it..
	        	if(MDS.logging){
	        		MDS.log("RESPONSE:"+xmlHttp.responseText);
	        	}
	
	        	//Send it to the callback function..
	        	if(callback){
	        		callback(JSON.parse(xmlHttp.responseText));
	        	}
	        
			}else{
				//Some error..
				postMDSFail(finalurl,params,xmlHttp.status);
			}
		}
    }
    xmlHttp.open("POST", finalurl, true); // true for asynchronous 
	xmlHttp.overrideMimeType('text/plain; charset=UTF-8');
    xmlHttp.send(encodeURIComponent(params));
	//xmlHttp.onerror = function () {
	//  console.log("** An error occurred during the transaction");
	//};
}

/**
 * POLLING Call
 */
function httpPostAsyncPoll(theUrl, params, callback){
	//Do we log it..
	if(MDS.logging){
		MDS.log("POST_POLL_RPC:"+theUrl+" PARAMS:"+params);
	}

	var xmlHttp = new XMLHttpRequest();
    xmlHttp.onreadystatechange = function() { 
        var status = xmlHttp.status;
		if (xmlHttp.readyState == XMLHttpRequest.DONE){
			if (status === 0 || (status >= 200 && status < 400)) {
			
				//Do we log it..
	        	if(MDS.logging){
	        		MDS.log("RESPONSE:"+xmlHttp.responseText);
	        	}
	
	        	//Send it to the callback function..
	        	if(callback){
	        		callback(JSON.parse(xmlHttp.responseText));
	        	}
	        
			}else{
				//Some error..
				postMDSFail(theUrl,params,xmlHttp.status);
			}
		}
    }
    xmlHttp.addEventListener('error', function(ev){
		MDS.log("Error Polling - reconnect in 10s");
		//setTimeout(function(){PollListener();},10000);
	});
    xmlHttp.open("POST", theUrl, true); // true for asynchronous 
	xmlHttp.overrideMimeType('text/plain; charset=UTF-8');
    xmlHttp.send(encodeURIComponent(params));
}

/**
 * Internal recursive function for file upload 
 */
function _recurseUploadMDS(thefullfile, chunk, callback){
		
	//Get some details
	var filename 	= thefullfile.name;
	var filesize 	= thefullfile.size;
	
	//1MB MAX Chunk size..
	var chunk_size 	= 1024 * 1024;
	var allchunks  	= Math.ceil(filesize / chunk_size);
	
	//Have we finished..
	if(chunk>allchunks-1){
		return;
	}
	
	var startbyte = chunk_size * chunk;
	var endbyte   = startbyte + chunk_size;
	if(endbyte>filesize){
		endbyte = filesize;
	}
	
	//Get a piece of the file
	var filepiece = thefullfile.slice(startbyte,endbyte);
	
	//Create a form..
	var formdata = new FormData();
	formdata.append("uid", MDS.minidappuid);
	
	//Filedata handled a little differently
	formdata.append("filename", filename);
	formdata.append("filesize", filesize);
	formdata.append("allchunks", allchunks);
	formdata.append("chunknum", chunk);
	formdata.append("fileupload", filepiece);
  
	var request = new XMLHttpRequest();
	request.open("POST", "/fileuploadchunk.html");
	request.onreadystatechange = function() { 
	        
		var status = request.status;
		if (request.readyState == XMLHttpRequest.DONE){
			if (status === 0 || (status >= 200 && status < 400)) {
				
				//Send it to the callback function..
	        	if(callback){
	        		var resp 		= {};
	        		resp.status		= true;
	        		resp.filename 	= filename;
	        		resp.size 		= filesize;
	        		resp.allchunks 	= allchunks;
	        		resp.chunk 		= chunk+1;
	        		resp.start 		= startbyte;
	        		resp.end 		= endbyte;
	        		
	        		callback(resp);
	        	}
	        
				//And now continue uploading..
				if(callback){
					_recurseUploadMDS(thefullfile,chunk+1, callback);	
				}else{
					_recurseUploadMDS(thefullfile,chunk+1);
				}				
				
			}else{
				if(callback){
	        		var resp 		= {};
	        		resp.status		= false;
	        		resp.error		= request.responseText;
	        		resp.filename 	= filename;
	        		resp.size 		= filesize;
	        		resp.allchunks 	= allchunks;
	        		resp.chunk 		= chunk;
	        		resp.start 		= startbyte;
	        		resp.end 		= endbyte;
	        		
	        		callback(resp);
	        	}
					
				//Some error..
				MDS.log("MDS FILEUPLOAD CHUNK ERROR: "+request.responseText);
			}
		}
    }

	//And finally send the POST request
	request.send(formdata);
}
