'use strict'

const db = require('../db')
const log = require('../log')('intval')
const storage = require('node-persist')
const fs = require('fs')

let Gpio
try {
	Gpio = require('onoff').Gpio
} catch (e) {
	log.warn('Failed including Gpio, using sim')
	Gpio = require('../../lib/onoffsim').Gpio
}


const PINS = {
	fwd : {
		pin : 13,
		dir : 'out'
	},
	bwd : {
		pin : 19,
		dir : 'out'
	},
	micro : {
		pin : 5,
		dir : 'in',
		edge : 'both'
	},
	release : {
		pin : 6,
		dir  : 'in',
		edge : 'both'
	}
}

/** Object representing the intval3 features */
const intval = {}

intval._frame = {
	open : 250, 	//delay before pausing frame in open state
	openBwd : 400,
	closed : 100,   //time that frame actually remains closed for
	expected : 530 	//expected length of frame, in ms
}
intval._release = {
	min : 20,
	seq : 1000
}
intval._microDelay = 10 // delay after stop signal before stopping motors
intval._pin = {}

/**
 *
 */

intval.init = function () {
	if (!fs.existsSync('./state')) fs.mkdirSync('./state')
	storage.init({
		dir: './state',
		stringify: JSON.stringify,
		parse: JSON.parse,
		encoding: 'utf8',
		logging: false,  // can also be custom logging function
		continuous: true, // continously persist to disk
		interval: false, // milliseconds, persist to disk on an interval
		ttl: false, // ttl* [NEW], can be true for 24h default or a number in MILLISECONDS
		expiredInterval: 2 * 60 * 1000, // [NEW] every 2 minutes the process will clean-up the expired cache
	    forgiveParseErrors: false // [NEW]
	}).then(intval._restoreState).catch((err) => { 
		log.warn('init', err) 
		intval.reset()
		intval._declarePins()
	})

	process.on('SIGINT', intval._undeclarePins)
	process.on('uncaughtException', intval._undeclarePins)
}

intval._restoreState = function (res) {
	storage.getItem('_state', 'test').then(intval._setState).catch((err) => {
		intval._setState(undefined)
		log.error('_restoreState', err)
	})
	intval._declarePins()
}

intval._setState = function (data) {
	if (typeof data !== 'undefined') {
		intval._state = data
		intval._state.frame.cb = () => {}
		log.info('_setState', 'Restored intval state from disk')
		return true
	}
	log.info('_setState', 'Setting state from defaults')
	intval._state = {
		frame : {
			dir : true, 	//forward
			start : 0, 		//time frame started, timestamp
			active : false, //should frame be running
			paused : false,
			exposure : 0, 	//length of frame exposure, in ms
			delay : 0, 		//delay before start of frame, in ms
			current : {}, //current settings
			cb : () => {}
		},
		release : {
			time: 0,
			active : false //is pressed
		},
		micro : {
			time : 0,
			primed : false //is ready to stop frame
		},
		counter : 0,
		sequence : false
	}
	intval._storeState()
}

intval._storeState = function () {
	storage.setItem('_state', intval._state)
		.then(() => {})
		.catch((err) => {
			log.error('_storeState', err)
		})
}

/**
* (internal function) Declares all Gpio pins that will be used
*
*/
intval._declarePins = function () {
	let pin
	for (let p in PINS) {
		pin = PINS[p]
		if (pin.edge) intval._pin[p] = new Gpio(pin.pin, pin.dir, pin.edge)
		if (!pin.edge) intval._pin[p] = new Gpio(pin.pin, pin.dir)
		log.info('_declarePins', { pin : pin.pin, dir : pin.dir, edge : pin.edge })
	}
	intval._pin.release.watch(intval._watchRelease)
}
/** 
* (internal function) Undeclares all Gpio in event of uncaught error
* that interupts the node process
*
*/
intval._undeclarePins = function (e) {
	log.error(e)
	if (!intval._pin) {
		log.warn('_undeclarePins', { reason : 'No pins'})
		return process.exit()
	}
	log.warn('_undeclarePins', { pin : PINS.fwd.pin, val : 0, reason : 'exiting'})
	intval._pin.fwd.writeSync(0)
	log.warn('_undeclarePins', { pin : PINS.bwd.pin, val : 0, reason : 'exiting'})
	intval._pin.bwd.writeSync(0)
	intval._pin.fwd.unexport()
	intval._pin.bwd.unexport()
	intval._pin.micro.unexport()
	intval._pin.release.unexport()
	process.exit()
}
/**
* Start motor in forward direction by setting correct pins in h-bridge
*
*/
intval._startFwd = function () {
	intval._pin.fwd.writeSync(1)
	intval._pin.bwd.writeSync(0)
}
/**
* Start motor in backward direction by setting correct pins in h-bridge
*
*/
intval._startBwd = function () {
	intval._pin.fwd.writeSync(0)
	intval._pin.bwd.writeSync(1)
}

intval._pause = function () {
	intval._pin.fwd.writeSync(0)
	intval._pin.bwd.writeSync(0)
	//log.info('_pause', 'frame paused')
}
/** 
* Stop motor by setting both motor pins to 0 (LOW)
*
*/
intval._stop = function () {
	const entry = {}
	const now = +new Date()
	const len = now - intval._state.frame.start

	intval._pin.fwd.writeSync(0)
	intval._pin.bwd.writeSync(0)

	log.info(`_stop`, { frame : len })

	intval._pin.micro.unwatch()
	intval._state.frame.active = false

	if (intval._state.frame.cb) intval._state.frame.cb(len)
	
	entry.start = intval._state.frame.start
	entry.stop = now
	entry.len = len
	entry.dir = intval._state.frame.current.dir ? 1 : 0
	entry.exposure = intval._state.frame.current.exposure
	entry.counter = intval._state.counter
	entry.sequence = intval._state.sequence ? 1 : 0

	db.insert(entry)

	intval._state.frame.current = {}
}
/**
* Callback for watching relese switch state changes.
* Using GPIO 06 on Raspberry Pi Zero W.
*
* 1) If closed AND frame active, start timer, set state primed to `true`.
* 1) If opened AND frame active, stop frame
*
* Microswitch + 10K ohm resistor 
* * 1 === open 
* * 0 === closed
*
*
* @param {object} 	err 	Error object present if problem reading pin
* @param {integer} 	val 	Current value of the pin
*
*/
intval._watchMicro = function (err, val) {
	const now = +new Date()
	if (err) {
		log.error('_watchMicro', err)
	}
	//log.info(`Microswitch val: ${val}`)
	//determine when to stop
	if (val === 0 && intval._state.frame.active) {
		if (!intval._state.micro.primed) {
			intval._state.micro.primed = true
			intval._state.micro.time = now
			log.info('Microswitch primed to stop motor')
		}
	} else if (val === 1 && intval._state.frame.active) {
		if (intval._state.micro.primed && !intval._state.micro.paused && (now - intval._state.frame.start) > intval._frame.open) {
			intval._state.micro.primed = false
			intval._state.micro.time = 0
			setTimeout( () => {
				intval._stop()
			}, intval._microDelay)
		}
	}
}
/**
* Callback for watching relese switch state changes.
* Using GPIO 05 on Raspberry Pi Zero W.
*
* 1) If closed, start timer.
* 2) If opened, check timer AND
* 3) If `press` (`now - intval._state.release.time`) greater than minimum and less than `intval._release.seq`, start frame
* 4) If `press` greater than `intval._release.seq`, start sequence
*
* Button + 10K ohm resistor 
* * 1 === open 
* * 0 === closed
*
* @param {object} 	err 	Error object present if problem reading pin
* @param {integer} 	val 	Current value of the pin
*
*/
intval._watchRelease = function (err, val) {
	const now = +new Date()
	let press = 0
	if (err) {
		return log.error(err)
	}
	//log.info(`Release switch val: ${val}`)
	if (val === 0) {
		//closed
		if (intval._releaseClosedState(now)) {
			intval._state.release.time = now
			intval._state.release.active = true //maybe unncecessary 
		}
	} else if (val === 1) {
		//opened
		if (intval._state.release.active) {
			press = now - intval._state.release.time
			if (press > intval._release.min && press < intval._release.seq) {
				intval.frame()
			} else if (press >= intval._release.seq) {
				intval.sequence()
			}
			//log.info(`Release closed for ${press}ms`)
			intval._state.release.time = 0
			intval._state.release.active = false
		}
	}
}

intval._releaseClosedState = function (now) {
	if (!intval._state.release.active && intval._state.release.time === 0) {
		return true
	}
	if (intval._state.release.active && (now - intval._state.release.time) > (intval._release.seq * 10)) {
		return true
	}
	return false
}

intval.reset = function () {
	intval._setState()
	intval._storeState()
}

/**
* Set the default direction of the camera.
* * forward = true
* * backward = false
*
* @param {boolean} 	[dir=true] 		Direction of the camera
*
*/
intval.setDir = function (val = true) {
	if (typeof val !== 'boolean') {
		return log.warn('Direction must be represented as either true or false')
	}
	intval._state.frame.dir = val
	intval._storeState()
	log.info('setDir', { direction : val ? 'forward' : 'backward' })
}

intval.setExposure = function (val = 0) {
	intval._state.frame.exposure = val
	intval._storeState()
	log.info('setExposure', { exposure : val })
} 

intval.setDelay = function (val = 0) {
	intval._state.frame.delay = val
	intval._storeState()
	log.info('setDelay', { delay : val })
}
intval.setCounter = function (val = 0) {
	intval._state.counter = val
	intval._storeState()
	log.info('setCounter', { counter : val })
}
/**
* Begin a single frame with set variables or defaults
*
* @param {?boolean} 	[dir="null"] 			(optional) Direction of the frame
* @param {?integer} 	[exposure="null"] 		(optional) Exposure time, 0 = minimum
*
*/
intval.frame = function (dir = null, exposure = null, cb = () => {}) {
	if (dir === true || (dir === null && intval._state.frame.dir === true) ) {
		dir =  true
	} else {
		dir = false
	}
	
	if (exposure === null && intval._state.frame.exposure !== 0) {
		exposure = intval._state.frame.exposure
	} else if (exposure === null) {
		exposure = 0 //default speed
	}

	intval._state.frame.start = +new Date()
	intval._state.frame.active = true
	intval._pin.micro.watch(intval._watchMicro)

	log.info('frame', {dir : dir ? 'forward' : 'backward', exposure : exposure})

	if (dir) {
		intval._startFwd()
	} else {
		intval._startBwd()
	}
	if (exposure !== 0) {
		intval._state.frame.paused = true
		if (dir) {
			setTimeout(intval._pause, intval._frame.open)
			//log.info('frame', { pausing : time + intval._frame.open })
			setTimeout( () => {
				intval._state.frame.paused = false
				intval._startFwd()
			}, exposure + intval._frame.closed)
		} else {
			setTimeout(intval._pause, intval._frame.openBwd)
			setTimeout( () => {
				//log.info('frame', 'restarting')
				intval._state.frame.paused = false
				intval._startBwd()
			}, exposure + intval._frame.closed)
		}
	}
	if (dir) {
		intval._state.frame.cb = (len) => {
			intval._state.counter++
			intval._storeState()
			cb(len)
		}
	} else {
		intval._state.frame.cb = (len) => {
			intval._state.counter--
			intval._storeState()
			cb(len)
		}
	}
	intval._state.frame.current = {
		dir: dir,
		exposure: exposure
	}
}

intval.status = function () {
	return intval._state
}

module.exports = intval