/**
 * Weigh Scales Driver
 * @module weigh
 * @desc This driver implements the protocol to retreive samples from the ADC in the weigh scales.
 * The weight is periodically sampled and an event fired to all listeners when the weight changes.
 */

const brewdefs = require('../../../common/brewdefs.js');
const brewlog = require('../../../common/brewlog.js');
const broker = require('../../../common/broker.js');
const calibrationFile = require('json-fs-store')(`${brewdefs.installDir}/brewstack/equipmentDrivers/weight`);
const mraa = require('../../nodeDrivers/i2c/i2c_mraa.js');
const temp = require('../../nodeDrivers/therm/temp.js');

let calibration;
let LOG_RAW_SAMPLES; //"./raw_weight.log"; //undefine to turn off

const EVENT_INTERVAL_SECS = 10 * 60;
const NAME = "Fermenter Weight";
const MAX_DELTA = 256;
const RESOLUTION = 10;//round to the nearest grams
const SENSOR_NAME = "Kg";
const MAX_KG = 50;

const current = {temp:undefined, kg:undefined};
let timer;
let prevKg;

const ADC_DATA_HIGH = 1;
const ADC_CLK_HIGH = 1;
const ADC_CLK_LOW = 0;

let publishWeight;
let ADC_clk;
let ADC_data;

const i2c_mraa 	= require('../../nodeDrivers/i2c/i2c_mraa.js');
			
/**
 * @desc Take a sample by driving the GPIO pins connected to the ADC.
 * The protocol is:
 * 0 - Drive CLK low to leave low power mode.
 * 1 - Wait until the ADO pin goes low. 
 * 2 - Drive CLK high.
 * 3 - Drive CLK low.
 * 4 - Read ADO
 * 5 - Repeat 2,3,4 for each of the 24 bits.
 * 7 - Drive CLK high
 * 8 - Drive CLK low
 * 9 - Drive CLK high to enter low power mode.
 */
function sample(){
  const TIMEOUT_ITERATIONS = 100000;
  let i;
  let count = -1;
  let t = 0;
  ADC_clk.write(ADC_CLK_LOW);//Active
  //console.log("waiting for DO ("+brewdefs.GPIO_ADC_DATA+ ") to go low");
  while ((ADC_data.read() == ADC_DATA_HIGH) && (t++ < TIMEOUT_ITERATIONS)){
	  //wait for DO to go low
  }
  
  if (t < TIMEOUT_ITERATIONS) {
	count=0;
	for(i=0;i<24;i++){
		ADC_clk.write(ADC_CLK_HIGH);
		ADC_clk.write(ADC_CLK_LOW);
		count=count<<1;
		if (ADC_data.read() == ADC_DATA_HIGH){
			count++;
		}
	}
  }
  else{
	  brewlog.error("Timeout waiting upon ADDO.");
	  return -1;
  }
  
  ADC_clk.write(ADC_CLK_HIGH);
  ADC_clk.write(ADC_CLK_LOW);
  ADC_clk.write(ADC_CLK_HIGH);//Low power
  
  return count;
}

//Take n samples
function samples(n){
	let i = 0;
	const s = [];
	let w;
	let w1;
	
	while (i++ < n){
		w = sample();
		if (w !== -1) {
			if ((w & 0x0800000) !== 0x0800000){
				//skip negative values
				w1 = (w & 0x00FFFFFF) >> 0;
				s.push(w1);
				
				 if (LOG_RAW_SAMPLES){			
					let Kg = (w1 - calibration.c)/ calibration.m;
					brewlog.info("Raw Weight=", Kg);
				}
			}
		}else{
			return -1;
		}
	}		
	
	return s;
}

//average and filter a number of samples
function average(){
	const AVG = 10;//average over this number of samples
	
	const s = samples(AVG);
	if (s === -1) {
		return -1;
	}
	
	//filter by limiting the difference between samples.
	let total = 0;
	let n = 0;
	let prev = 0;
	s.forEach(weight => {
		if (prev !== 0){
			if (Math.abs(weight-prev) <= MAX_DELTA){
				n++;
				total += weight;
			}
		}else{
			n = 1;
			total = weight;
		}
		prev = weight;
	});
	
	if (n === 0){
		return -1;
	}else {
		return total/n;	
	}
}

/*
Measurements showed that as 
temp   varied from 19.6 to 7.3 (12.3DegC) , the 
weight varied from 20.6 to 22.0 (+6.8%)
i.e. 114g/C
*/
function tempAdjust(kg, calTemp, temp){
	//console.log(kg, calTemp, temp)
	let offset = (temp - calTemp) * 0.114; 	
	//console.log("offset=",offset)	
	const result = Math.ceil((kg+offset)*1000/RESOLUTION) * RESOLUTION / 1000;
	return result < 0 ? 0 : result;
}

function weigh() {	
	if (calibration === undefined){
		brewlog.error("Attempted to weight prior to calibration data being loaded");
		return;		
	}
	
	const x = average();
	if (x === -1){
		brewlog.error("Error taking weight");
		return;
	}else{			
		if (x >= 0){
			const g = (1000*(x - calibration.c))/ calibration.m;
			var kg;
			if (g < 0){
				kg = 0;
			}else{
				kg = (Math.ceil(g/RESOLUTION)*RESOLUTION)/1000;
			}	
		}
		
		if (kg < MAX_KG){
			//console.log("Raw weight =",kg)
			kg = tempAdjust(kg, calibration.temp, current.temp);
			if (kg !== prevKg)
			{	
				current.kg = kg;
				publishWeight(kg);
				prevKg = kg;
			}

			return current.kg;
		}
		
		return undefined;
	}
}

function currentWeight() {
	return new Promise((resolve, reject) => {		
		const kg  = weigh();
		if (kg === -1){
			reject("Failed to measure weight");
		}else{			
			resolve(kg);
		}
	});
}
	
function tempChange({value}) {
	current.temp = value;
	current.kg = tempAdjust(current.kg, calibration.temp, current.temp);
}

module.exports = {
	sensorName: SENSOR_NAME,
	
	start(opt) {
		return new Promise((resolve, reject) => {
			i2c_mraa.start(opt);

			ADC_clk = new mraa.Gpio(brewdefs.GPIO_ADC_CLK);
			ADC_data  = new mraa.Gpio(brewdefs.GPIO_ADC_DATA);
			ADC_clk.dir(mraa.DIR_OUTPUT);
			ADC_data.dir(mraa.DIR_INPUT);

			temp.start(opt)
			.then((opt) => {
				temp.getTemp("TempGlycol")
				.then(t => {
					current.temp = t;
					
					calibrationFile.load('cal', (err, cal) => {
						if (err){
							console.log("err=",err)
							reject(err);
						}
						
						calibration = cal;
						
						publishWeight = broker.create(SENSOR_NAME);

						if (timer){
							clearInterval(timer);
						}
						timer = setInterval(weigh, EVENT_INTERVAL_SECS * 1000);
	
						//on temp change update weight
						broker.subscribe("TempGlycol", tempChange);

						resolve(opt);
					});
				})
				.catch(err => {console.log(err)});
			});
		});
	},

	
	/* Free memory associated with the GPIO pins.
	*/ 
	stop() {
		return new Promise((resolve, reject) => {
		
			temp.stop()
			.then(function(){
				if (timer){
					clearInterval(timer);
					timer = null;
				}

				broker.unSubscribe(tempChange);
				broker.destroy(SENSOR_NAME);
				

			})
			.then(resolve);
		});
	},
	
	/** 
	 * @desc Assume that the weight is zero. Take a measurement and adjust the calibration data (offset).
	 * @param function Callback
	*/
	tare() {
		return new Promise((resolve, reject) => {	
			const kg = average();
			if (kg === undefined){
				reject("tare is undefined");
				return;
			}

			calibrationFile.load('cal', (err, cal) => {
				const kg = average();
				if (kg !== undefined){
					cal.c = kg;
					cal.temp = current.temp;
					calibrationFile.add(cal, err => {
						if (err){
							reject(err);
						}else{
							// called when the file has been written 
							calibration = cal;
							resolve(cal);
						}
					});
				}else{
					reject("Undefined measurement");
				}			
			});
		});
	},
	
	/** 
	 * @desc For a specific weight, take a measurement and adjust the calibration data (slope).
	 * @param {number} refKg - Reference weight
	 * @param {function} Callback
	 */
	calibrate(refKg) {
		return new Promise((resolve, reject) => {	
			//y=mx+c
			const kg = refKg;
			calibrationFile.load('cal', (err, cal) => {
				const meanX = average();
				if (meanX === -1) {
					reject();
				}
				if (kg == 0){
					cal.m = 1;
				}else{
					cal.m = (meanX - cal.c) / kg;
				}

				cal.temp = current.temp;

				calibrationFile.add(cal, err => {
					calibration = cal;
					resolve(cal);
				});
			});
		});
	},

	currentWeight
}