/*****************************************************************************************
Module usage in SoilFluxPro:
	1. Open the console in SoilFluxPro: 
		up up down down left right left right b a
	2. Require the module with SoilFluxPro application directory:
		let module = require('path/storage.js')(__dirname)
	
Functions:	
	module.verbose(show_messages)
		Turns on or off status messages for other functions. Messages are enabled by 
		default. Pass true for show_messages to enable, or false to disable. Omitting a 
		value for show_messages will return the current state.

	module.workingDirectory(path)
		Sets the working directory to look for files in and store files to. By default, 
		the working directory is the /storage_profiles subdirectory under the user’s 
		Desktop. Pass the full path as a string enclosed in quotes for path. Omitting path 
		will return the current working directory being used.

	module.loadHeightsFromFile(use_port, file, path)
		Creates an object containing intake port heights from a .csv file. The .csv file 
		should include no headers, and two or three columns of values. The first column 
		should be either the LI-8250 port number or the VERTICAL metadata value. If an 
		8250-01 was used and the data are being mapped by port, then the second column 
		should contain the 8250-01 port number. The final column should be the intake 
		height in meters. Pass true for use_port to map the intake heights to port 
		numbers. Omit or pass false to map them to the metadata item VERTICAL. By default, 
		intake heights are expected in a file named intake_heights.csv, located in the 
		/storage_profiles subdirectory on the user’s Desktop. An alternative file name and 
		location can be specified by passing values for file and path as strings in 
		quotes. A non-empty intake heights object is required. 

	module.returnHeights()
		Returns the current value of the intake heights object.

	module.rawStorageProfileExporter(station, logger, group, path)
		Generates combined daily files of raw one second time series data following a 
		pseudo-ICOS file standard. Output files will be formatted as comma separated files 
		and either named with the LI-8250 serial number and date (YYYYMMDD), or if station 
		and logger are not null, an ICOS compliant file name. The metadata item VERTICAL 
		will be mapped to the ICOS variable LEVEL and all reported flow rates will be 
		appended with _VOLRATE following the ICOS standard naming convention for storage 
		data. Omitting a value for group will default to using the first file group in 
		SoilFluxPro’s file group list. Otherwise, pass the file group name as a string in 
		quotes to specify as specific group. Path sets the directory to write output files 
		to. By default, files will be written in the /raw subdirectory under the working 
		directory.

	module.meanStorageProfileExporter(half_hour, heights, group, path)
		Generates half-hourly files of averaged data for each port, in a nested 
		/year/month/day directory structure. Files are comma separated and named with the 
		LI-8250 serial number and a time stamp (82m-xxxx_YYYYMMDDHHMM.csv). The time stamp 
		will represent the end of the averaging interval if half_hour is set to true and 
		the middle of the averaging interval if it is set to false or omitted. Omit 
		heights to use the intake heights object created by loadHeightsFromFile(), or pass 
		a properly structed object containing the heights. Omitting a value for group will 
		default to using the first file group in SoilFluxPro’s file group list. Otherwise, 
		pass the file group name as a string in quotes to specify as specific group. Path 
		sets the directory to write output files to. By default, files in the nested 
		structure will be written in the /means subdirectory under the working directory.

	module.computeStorageFlux(path)
		Calculates storage terms following Montagnani et al. 2018, International 
		Agrophysics, from the mean data generated by meanStorageProfileExporter(). 
		Computed parameters are output to a comma separated file named with the LI-8250 
		serial number and the time stamp of either the end or middle, depending on how the 
		means were calculated, of the first averaging interval 
		(82m-xxxx_STORAGE_YYYYMMDDHHMM.csv). Path sets the directory to search for the 
		/means subdirectory in and where to write the output file to. By default, this is 
		the working directory.

	Note: All gases and storage related values are calculated as intake height weighted 
	averages, and all diagnostic variables are calculated as non-weight averages. The 
	LI-8250 is assumed to be at ground level for all calculations.  Chamber temperature is 
	assumed to represent the air temperature at the intake. If an alternative temperature 
	is available, this should be mapped to chamber temperature in SoilFluxPro’s main 
	interface using the Import and Transform tools prior to exporting mean data. Dry air 
	parameters are calculated for all analyzers that provide a water vapor measurement, 
	and those parameters are used for all storage calculations for that specific analyzer. 

*****************************************************************************************/
module.exports=function(sfp_path=__dirname){
	const 	version='0.3_beta',
			revision='September 20, 2022',
			author='jason.hupp@licor.com';
	
	let sfp=require(sfp_path.slice(0,-8)+'sfp.node');
	let fs=require('fs');
	let pth=require('path');
	
	var intake_heights=new Array(),working_directory=pth.join(require('os').homedir(),'Desktop','storage_profiles'),_verbose=true;
	
	function log(x){if(_verbose){console.log(x)};};
	
	function timeString(d,t,l0='000000'){return d+l0.substr(0,6-String(t).length)+t;};
	
	function getEpoch(d){return new Date(d.substr(0,4),d.substr(4,2)-1,d.substr(6,2),d.substr(8,2),d.substr(10,2),d.substr(12)).getTime();}
	
	function utcToIcosDate(s){
		var d=new Date(s)
		return d.getFullYear()+('0'+(d.getMonth()+1)).slice(-2)+('0'+d.getDate()).slice(-2)+('0'+d.getHours()).slice(-2)+('0'+d.getMinutes()).slice(-2)+('0'+d.getSeconds()).slice(-2);
	}
	
	function fileHeader(d){
		var g=new Array(),v=new Array(),u=new Array();
		g.push('LI-8250','LI-8250','LI-8250','LI-8250','LI-8250','LI-8250');
		v.push('DATE','LEVEL','PA','FLOW_VOLRATE','SUB_FLOW_VOLRATE','PORT');
		u.push('[YYYYMMDDHHMMSS]','[#]','[kPa]','[L+1M-1]','[L+1M-1]','[#]');
		if(d['instrument_metadata'].hasOwnProperty('8250-01')){
			g.push('8250-01');
			v.push('PORT');
			u.push('[#]')
		}
		g.push('CHAMBER');
		v.push('TA');
		u.push('[C]');
		if(d['instrument_metadata'].hasOwnProperty('LI-870')){
			g.push('LI-870','LI-870','LI-870','LI-870','LI-870');
			v.push('CO2_DRY','H2O','T_CELL','PA_CELL','FLOW_VOLRATE');
			u.push('[umol+1mol-1]','[mmol+1mol-1]','[C]','[kPa]','[L+1M-1]');
		}
		if(d['instrument_metadata'].hasOwnProperty('LI-850')){
			g.push('LI-850','LI-850','LI-850','LI-850','LI-850');
			v.push('CO2_DRY','H2O','T_CELL','PA_CELL','FLOW_VOLRATE');
			u.push('[umol+1mol-1]','[mmol+1mol-1]','[C]','[kPa]','[L+1M-1]');
		}
		if(d['instrument_metadata'].hasOwnProperty('LI-7810')){
			g.push('LI-7810','LI-7810','LI-7810','LI-7810','LI-7810','LI-7810');
			v.push('CH4_DRY','CO2_DRY','H2O','T_CELL','PA_CELL','DIAGNOSTIC');
			u.push('[nmol+1mol-1]','[umol+1mol-1]','[mmol+1mol-1]','[C]','[kPa]','[#]');
		}
		if(d['instrument_metadata'].hasOwnProperty('LI-7815')){
			g.push('LI-7815','LI-7815','LI-7815','LI-7815','LI-7815');
			v.push('CO2_DRY','H2O','T_CELL','PA_CELL','DIAGNOSTIC');
			u.push('[umol+1mol-1]','[mmol+1mol-1]','[C]','[kPa]','[#]');
		}
		if(d['instrument_metadata'].hasOwnProperty('LI-7820')){
			g.push('LI-7820','LI-7820','LI-7820','LI-7820','LI-7820');
			v.push('N2O_DRY','H2O','T_CELL','PA_CELL','DIAGNOSTIC');
			u.push('[nmol+1mol-1]','[mmol+1mol-1]','[C]','[kPa]','[#]');
		}
		return {'header':g.toString()+'\n'+v.toString()+'\n'+u.toString()+'\n','groups':g,'labels':v};
	}
	
	function dailyRawFile(d,t,s='',l='',p=working_directory){
		p=pth.join(p,'raw');
		if(!fs.existsSync(p)){fs.mkdirSync(p,{recursive:true});}
		var fn='',h={};
		if(!s || s.length===0){fn+=d['instrument_metadata']['LI-8250']['serial_number'];}
		fn+=s+'_ST_'+t.substr(0,8);
		if(l || l.length>0){fn+='_L'+l+'_F01';}
		p=pth.join(p,fn+'.csv');
		h=fileHeader(d);
		fs.writeFileSync(p,h['header'],{flag:'w+'},err=>{if(err){console.error(err);}});
		return {'file':p,'groups':h['groups'],'labels':h['labels']};
	};
	
	function halfHourMeanFile(d,t,hh=false,p=working_directory){
		var g=new Array(),v=new Array(),u=new Array(),h={};
		p=pth.join(p,'means',t.substr(0,4),t.substr(4,2),t.substr(6,2));
		if(!fs.existsSync(p)){fs.mkdirSync(p,{recursive:true});}
		if((hh && t.substr(10,2)>=30) || (!hh && t.substr(10,2)>=45)){
			p=pth.join(p,d['instrument_metadata']['LI-8250']['serial_number']+'_'+utcToIcosDate(getEpoch(t)+(60*60*1000)).substr(0,10)+'00.csv');
		}else if(!hh && t.substr(10,2)<15){
			p=pth.join(p,d['instrument_metadata']['LI-8250']['serial_number']+'_'+t.substr(0,10)+'00.csv');
		}else{
			p=pth.join(p,d['instrument_metadata']['LI-8250']['serial_number']+'_'+t.substr(0,10)+'30.csv');
		}
		h=fileHeader(d);
		fs.writeFileSync(p,h['header'].replace('DATE','HEIGHT').replace('YYYYMMDDHHMMSS','m'),{flag:'w+'},err=>{if(err){console.error(err);}});
		h['labels'][0]='HEIGHT';
		return {'end':parseInt(hh?p.substr(-16,12)+'00':utcToIcosDate(getEpoch(p.substr(-16,12)+'00')+(15*60*1000))),'file':p,'groups':h['groups'],'labels':h['labels']};
	};
	
	function getIndex(d,g,v,e=-9999){
		if(v.includes('_VOLRATE')){v=v.slice(0,-8);} //Drop ICOS suffix to get actual flows
		if(v.includes('_CELL') && g.includes('LI-78')){v=v.replace('CELL','CAVITY');} //Map ICOS CELL to CAVITY for LI-78xx
		for(i=0;i<d['headers'].length;i++){if(d['headers'][i]==v && d['instruments'][i]==g){e=i;};}
		return e;
	};
	
	function meansToDataObj(f,p=working_directory){
		var pk={},g=new Array(),l=new Array(),u=new Array();
		if(fs.existsSync(pth.join(p,f))){
			var dl=fs.readFileSync(pth.join(p,f),'utf8').replace(/'\r'/g,'\n').replace(/'\n\n'/g,'\n').split('\n');
			g=dl[0].split(',');
			l=dl[1].split(',');
			u=dl[2].split(',');
			pk['header']={};
			for(i=0;i<g.length;i++){pk['header'][g[i]]={};}
			for(i=0;i<l.length;i++){pk['header'][g[i]][l[i]]=u[i];}
			for(i=3;i<dl.length;i++){
				var d_ar=dl[i].split(','),d_ob={};
				if(d_ar.length>1){
					for(i1=0;i1<g.length;i1++){d_ob[g[i1]]={};}
					for(i1=0;i1<d_ar.length;i1++){d_ob[g[i1]][l[i1]]=parseFloat(d_ar[i1]);}
					pk[d_ob['LI-8250']['HEIGHT']]=d_ob;
				}
			}
			pk['time']=f.substr(-16,12);
		}	
		return pk;
	};
	
	function weightedAverage(z,v){
		var zv=zt=0;
		for(i=0;i<z.length;i++){
			zv+=z[i]*v[i];
			zt+=z[i];
		}
		return zv/zt;
	};
	
	function findLower(v,a){
		var l=new Array();
		a.forEach(i => l.push(i<v?i:0));
		return Math.max(...l);
	};
	
	function findUpper(v,a){
		var l=new Array();
		a.forEach(i => l.push(i>v?i:9999));
		return Math.min(...l)==9999?v:Math.min(...l);
	};
	
	log('Storage profile module for SoilFluxPro 5.2\nVersion: '+version+'\nLast revison: '+revision+'\nAuthor: '+author);
	
	var modules={};
	
	modules.verbose=function(show_messages='current'){return _verbose=(show_messages=='current')?_verbose:show_messages;};
	
	modules.workingDirectory=function(path='current'){return working_directory=(path!='current')?path:working_directory;};
	
	modules.returnHeights=function(){return intake_heights;};
	
	modules.loadHeightsFromFile=function(use_port=false,file='intake_heights.csv',path=working_directory){
		if(!fs.existsSync(pth.join(path,file))){console.error('No intake heights file found!');}
		else{
			intake_heights.length=0;
			var d=fs.readFileSync(pth.join(path,file),'utf8').replace(/'\r'/g,'\n').replace(/'\n\n'/g,'\n').split('\n');
			for(i=0;i<d.length;i++){
				var l=d[i].split(','),li={};
				if(use_port){
					li['LI-8250']=parseInt((l[0]>0 && l[0]<9)?l[0]:-9999);
					if(l.length>2){
						log('Mapping intake heights to both LI-8250 and 8250-01 metadata items PORT.');
						li['8250-01']=parseInt((l[1]>0 && l[1]<9)?l[1]:-9999);
						li['h']=parseFloat(l[2]);}
					else{
						log('Mapping intake heights to LI-8250 metadata item PORT.');
						li['h']=parseFloat(l[1]);
					}
				}else{
					log('Mapping intake heights to metadata item VERTICAL.');
					li['vertical']=parseInt(l[0]>0?l[0]:-9999);
					li['h']=parseFloat(l[1]);
				}
				intake_heights.push(li);
			}
		}
		return intake_heights;
	};
	
	modules.rawStorageProfileExporter=function(station='',logger='',group=sfp.store.getGroupNames()[0],path=working_directory){
		var obs=(group === undefined)?[]:sfp.store.getObservationIdsByGroup(group),file_form,day=-1;
		if(obs.length>0){
			log('Generating daily files of 1 Hz raw data using the ICOS data labeling standard.\nNote that the metadata item VERTICAL will be mapped to LI-8250 LEVEL.');
			var indexs=new Array();
			for(i1=0;i1<obs.length;i1++){
				var x=sfp.store.getObservationById(obs[i1]);
				for(i2=0;i2<x['data'].length;i2++){
					var nl=new Array();
					if(x['data'][i2][0]!=day){
						//Split files at midnight
						day=x['data'][i2][0];
						file_form=dailyRawFile(x,timeString(x['data'][i2][0],x['data'][i2][1]),station,logger,path);
						indexs.length=0;
						for(i3=0;i3<file_form['labels'].length;i3++){indexs.push(getIndex(x,file_form['groups'][i3],file_form['labels'][i3]));}
						log(file_form);
					}
					//LI-8250 DATE and LEVEL are special cases
					nl.push(timeString(x['data'][i2][0],x['data'][i2][1]),x['metadata']['vertical']);
					for(i3=2;i3<file_form['labels'].length;i3++){
						//PORTs are special cases too
						if(file_form['labels'][i3]=='PORT'){nl.push(x['instrument_metadata'][file_form['groups'][i3]]['port'] || -9999);}
						else{nl.push(x['data'][i2][indexs[i3]]=(x['data'][i2][indexs[i3]] === undefined)? -9999:x['data'][i2][indexs[i3]]);}
					}
					fs.appendFileSync(file_form['file'],nl.toString()+'\n',err=>{if(err){console.error(err);}});	
				}
			}
		}else{console.error('No file group available! Open data in SoilFluxPro main application.');}
	};
	
	modules.meanStorageProfileExporter=function(half_hour=false,heights=intake_heights,group=sfp.store.getGroupNames()[0],path=working_directory){
		if(heights.length>0){
			var obs=(group === undefined)?[]:sfp.store.getObservationIdsByGroup(group),file_form={},run_total={},heights_keyed={},hf=true;
			for(i1=0;i1<heights.length;i1++){
				if(heights[i1].hasOwnProperty('LI-8250')){heights_keyed[heights[i1].hasOwnProperty('8250-01')?String(heights[i1]['LI-8250'])+String(heights[i1]['8250-01']):String(heights[i1]['LI-8250'])]=heights[i1]['h'];}
				else{
					hf=false;
					heights_keyed[String(heights[i1]['vertical'])]=heights[i1]['h'];
				}
			}
			if(obs.length>0){
				log('Generating half hourly files in nested directory structure. The actual intake height will be mapped to LI-8250 HEIGHT.')
				log(half_hour?'LI-8250 DATE is middle of the averaging interval.':'LI-8250 DATE is the end point of averaging interval.');
				var indexs=new Array();
				for(i1=0;i1<obs.length;i1++){
					var x=sfp.store.getObservationById(obs[i1]),
					port_key=x['instrument_metadata'].hasOwnProperty('8250-01')?String(x['instrument_metadata']['LI-8250']['port'])+String(x['instrument_metadata']['8250-01']['port']):String(x['instrument_metadata']['LI-8250']['port']);
					for(i2=0;i2<x['data'].length;i2++){
						if(!file_form.hasOwnProperty('end') || parseInt(timeString(x['data'][i2][0],x['data'][i2][1]))>file_form['end']){ 
							if(file_form.hasOwnProperty('file') && Object.keys(run_total).length>0){
								for(const p_k in run_total){
									var means=new Array();
									for(i3=0;i3<file_form['labels'].length;i3++){means.push((run_total[p_k][i3]['n']==0 || run_total[p_k][i3]['sum']==-9999)? run_total[p_k][i3]['sum']:(run_total[p_k][i3]['sum']/run_total[p_k][i3]['n']).toFixed(2));}
									fs.appendFileSync(file_form['file'],means.toString()+'\n',err=>{if(err){console.error(err);}});
								}
							}
							run_total={};
							file_form=halfHourMeanFile(x,timeString(x['data'][i2][0],x['data'][i2][1]),half_hour,path);
							for(i3=0;i3<file_form['labels'].length;i3++){indexs.push(getIndex(x,file_form['groups'][i3],file_form['labels'][i3]));}
							log(file_form);
						}
						if(!run_total.hasOwnProperty(port_key)){
							run_total[port_key]=new Array;
							for(i3=0;i3<file_form['labels'].length;i3++){run_total[port_key].push({'sum':0,'n':0});}
						}else{
							//HEIGHT and LEVEL are special cases
							if(hf){run_total[port_key][0]['sum']=(heights_keyed[port_key]||-9999);}
							else{run_total[port_key][0]['sum']=(heights_keyed[String(x['metadata']['vertical'])]||-9999);}
							run_total[port_key][1]['sum']=x['metadata']['vertical'];
							for(i3=2;i3<file_form['labels'].length;i3++){
								//PORTs are special cases too
								if(file_form['labels'][i3]=='PORT'){run_total[port_key][i3]['sum']=(x['instrument_metadata'][file_form['groups'][i3]]['port'] || -9999);}
								else{
									run_total[port_key][i3]['sum']+=(x['data'][i2][indexs[i3]] === undefined)? 0:x['data'][i2][indexs[i3]];
									run_total[port_key][i3]['n']+=(x['data'][i2][indexs[i3]] === undefined)? 0:1;
								}
							}
						}
					}
				}
				for(const p_k in run_total){
					var means=new Array();
					for(i3=0;i3<file_form['labels'].length;i3++){means.push((run_total[p_k][i3]['n']==0 || run_total[p_k][i3]['sum']==-9999)? run_total[p_k][i3]['sum']:(run_total[p_k][i3]['sum']/run_total[p_k][i3]['n']).toFixed(2));}
					fs.appendFileSync(file_form['file'],means.toString()+'\n',err=>{if(err){console.error(err);}});
				}	
			}else{console.error('No file group');}	
		}else{console.error('Intake heights empty! loadHeights()');}
	};
	
	modules.computeStorageFlux=function(path=working_directory){
		var output_file='';
		if(!fs.existsSync(pth.join(path,'means'))){console.error(pth.join('No ','means found! meanStorageProfileExporter()'));}
		else{
			var file_list=new Array(),YYYY=fs.readdirSync(pth.join(path,'means'),'utf8');
			if(YYYY.length==0){console.error(pth.join('No data in ','means!'))}
			else{
				for(y=0;y<YYYY.length;y++){
					if(fs.existsSync(pth.join(path,'means',YYYY[y])) && fs.lstatSync(pth.join(path,'means',YYYY[y])).isDirectory()){
						var MM=fs.readdirSync(pth.join(path,'means',YYYY[y]),'utf8');
						if(MM.length>0){
							for(m=0;m<MM.length;m++){
								if(fs.existsSync(pth.join(path,'means',YYYY[y],MM[m])) && fs.lstatSync(pth.join(path,'means',YYYY[y],MM[m])).isDirectory()){
									var DD=fs.readdirSync(pth.join(path,'means',YYYY[y],MM[m]),'utf8');
									if(DD.length>0){
										for(d=0;d<DD.length;d++){
											if(fs.existsSync(pth.join(path,'means',YYYY[y],MM[m],DD[d])) && fs.lstatSync(pth.join(path,'means',YYYY[y],MM[m],DD[d])).isDirectory()){
												var HHMM=fs.readdirSync(pth.join(path,'means',YYYY[y],MM[m],DD[d]),'utf8');
												if(HHMM.length>0){
													for(h=0;h<HHMM.length;h++){
														if(HHMM[h].includes('.csv')){file_list.push(pth.join('means',YYYY[y],MM[m],DD[d],HHMM[h]));}
													}
												}
											}
										}	
									}
								}
							}
						}
					}
				}
			}
			if(file_list.length>2){
				log('Storage terms calculated following Montagnani et al. (2018), International Agrophysics.\nNote that calculated storage variables and gases are all intake height weighted means.');
				for(f=1;f<file_list.length;f++){
					var data={'mean':{'STORAGE':{}},'time1':meansToDataObj(file_list[f-1],path),'time2':meansToDataObj(file_list[f],path)};
					if(Object.keys(data['time1']).length==Object.keys(data['time2']).length && data['time1'].hasOwnProperty('header')){
						data['time2']['header']['STORAGE']={};
						var z=new Array(),d=new Array();
						for(const h in data['time2']){
							if(h!='header' && h!='time'){
								z.push(parseFloat(h));
								d.push(((data['time1'][h]['CHAMBER']['TA']+data['time2'][h]['CHAMBER']['TA'])/2)+273.15);
							}
						}
						data['mean']['STORAGE']['DATE']=data['time2']['time'];
						data['time2']['header']['STORAGE']['DATE']='[YYYYMMDDHHMM]';
						data['mean']['STORAGE']['TA']=weightedAverage(z,d);//Compute a height weighted average air temperature in K
						data['time2']['header']['STORAGE']['TA']='[K]';
						data['mean']['STORAGE']['SCALE_HEIGHT']=(data['mean']['STORAGE']['TA']*287.06)/9.8;//Compute scale height
						data['time2']['header']['STORAGE']['SCALE_HEIGHT']='[m]';
						for(const g in data['time2']['header']){
							if(g!='CHAMBER' && g!='8250-01'&& g!='STORAGE'){
								data['mean'][g]={};
								for(const l in data['time2']['header'][g]){
									if(l!='HEIGHT' && l!='LEVEL' && l!='PORT'){
										d.length=0;
										z.forEach(i => d.push(data['time2'][i][g][l]));
										if(l.includes('_DRY') || l=='H2O'){data['mean'][g][l]=weightedAverage(z,d);}//Compute height weighted averages of the gases
										else{data['mean'][g][l]=weightedAverage(Array(d.length).fill(1),d);}//Take normal averages of diagnostic variables
										//Storage pressure is a special case where the height weighted average is estimated from a single measurement level
										//This assumes the pressure sensor is at ground level!!!
										if(l=='PA' && g=='LI-8250'){
											data['mean']['STORAGE']['PA']=data['mean'][g][l]*(1.0-((0.5*Math.max(...z))/data['mean']['STORAGE']['SCALE_HEIGHT']));
											data['time2']['header']['STORAGE']['PA']='[kPa]';
										}
									}
								}
							}
						}
						for(const g in data['mean']){
							//Compute a height weighted average vapor pressure and dry air properties for any H2O analyzer
							if(data['mean'][g].hasOwnProperty('H2O')){
								data['mean'][g]['VAPOR_PRESSURE']=data['mean']['STORAGE']['PA']*(data['mean'][g]['H2O']/1000.0);
								data['time2']['header'][g]['VAPOR_PRESSURE']='[kPa]';
								data['mean'][g]['PA_DRY']=data['mean']['STORAGE']['PA']-data['mean'][g]['VAPOR_PRESSURE'];
								data['time2']['header'][g]['PA_DRY']='[kPa]';
								data['mean'][g]['RHO_AIR_DRY']=(data['mean'][g]['PA_DRY']*1000.0)/(8.3144598*data['mean']['STORAGE']['TA']);
								data['time2']['header'][g]['RHO_AIR_DRY']='[mol+1m-3]';
							}
						}
						var dz={};
						d.length=0;
						z.forEach(i => d.push((i+findUpper(i,z))/2));//Find mean measurement height for each layer
						for(i=0;i<d.length;i++){dz[z[i]]=(d[i]-findLower(d[i],d));} //Find delta_z for each measurement height
						for(const g in data['mean']){
						//Compute storage terms assuming anything reporting H2O has other gas measurements
							if(data['mean'][g].hasOwnProperty('H2O')){
								for(const l in data['mean'][g]){
									if(l.includes('_DRY') && !l.includes('PA') && !l.includes('RHO')){//Look for just the dry gases
										d.length=0;
										for(const zk in dz){d.push(((data['time2'][zk][g][l]-data['time1'][zk][g][l])/1800.0)*dz[zk]);}
										data['mean'][g][l.substr(0,l.indexOf('_'))+'_STORAGE_FLUX']=(d.reduce((a, b) => a + b, 0))*data['mean'][g]['RHO_AIR_DRY'];
										data['time2']['header'][g][l.substr(0,l.indexOf('_'))+'_STORAGE_FLUX']=data['time2']['header'][g][l].substr(0,data['time2']['header'][g][l].indexOf('+'))+'+1m-2s-1]';
									}
								}
							}
						}
						if(output_file==''){
							output_file=pth.join(path,file_list[f].substr(file_list[f].indexOf('82m'),file_list[f].indexOf('_')-file_list[f].indexOf('82m'))+'_STORAGE_'+data['mean']['STORAGE']['DATE']+'.csv');
							var group=new Array(),label=new Array(),unit=new Array();
							group.push('STORAGE');
							label.push('DATE');
							unit.push(data['time2']['header']['STORAGE']['DATE']);
							for(const l in data['mean']['STORAGE']){
								if(l!='DATE'){
									group.push('STORAGE');
									label.push(l);
									unit.push(data['time2']['header']['STORAGE'][l]);
								}
							}
							for(const l in data['mean']['LI-8250']){
								group.push('LI-8250');
								label.push(l);
								unit.push(data['time2']['header']['LI-8250'][l]);
							}
							for(const g in data['mean']){
								if(g!='STORAGE' && g!='LI-8250'){
									for(const l in data['mean'][g]){
										group.push(g);
										label.push(l);
										unit.push(data['time2']['header'][g][l]);
									}
								}
							}
							fs.writeFileSync(output_file,group.toString()+'\n'+label.toString()+'\n'+unit.toString()+'\n',{flag:'w+'},err=>{if(err){console.error(err);}});
						}
						d.length=0;
						d.push(parseInt(data['mean']['STORAGE']['DATE']));
						for(const l in data['mean']['STORAGE']){if(l!='DATE'){d.push(data['mean']['STORAGE'][l].toFixed(2));}}
						for(const l in data['mean']['LI-8250']){d.push(data['mean']['LI-8250'][l].toFixed(2));}
						for(const g in data['mean']){if(g!='STORAGE' && g!='LI-8250'){for(const l in data['mean'][g]){d.push(data['mean'][g][l].toFixed((l.includes('VAPOR') || l.includes('RHO') || l.includes('STORAGE'))? 10:2));}}}
						fs.appendFileSync(output_file,d.toString()+'\n',err=>{if(err){console.error(err);}});
						log(data['mean']);
					}else{console.error('Missmatched profile between '+file_list[f-1]+' and '+file_list[f]+'\nNo storage terms calculated for this period!');}
				}
			}else{console.error('Not enough data to compute storage terms!');}
		}
	};
	
	return modules;
}