// Package mpu6050 allows interfacing with InvenSense mpu6050 barometric pressure sensor. This sensor
// has the ability to provided compensated temperature and pressure readings.
package mpu6050

import (
	"../linux-mpu9150/mpu"
	"log"
	"math"
	"time"
)

//https://www.olimex.com/Products/Modules/Sensors/MOD-MPU6050/resources/RM-MPU-60xxA_rev_4.pdf
const (
	pollDelay = 98 * time.Millisecond // ~10Hz
)

// MPU6050 represents a InvenSense MPU6050 sensor.
type MPU6050 struct {
	Poll time.Duration

	started bool

	pitch float64
	roll  float64

	// Calibration variables.
	calibrated    bool
	pitch_history []float64
	roll_history  []float64
	pitch_resting float64
	roll_resting  float64

	// For tracking heading (mixing GPS track and the gyro output).
	heading            float64 // Current heading.
	gps_track          float64 // Last reading directly from the gyro for comparison with current heading.
	gps_track_valid    bool
	heading_correction float64

	quit chan struct{}
}

// New returns a handle to a MPU6050 sensor.
func New() *MPU6050 {
	n := &MPU6050{Poll: pollDelay}
	n.StartUp()
	return n
}

func (d *MPU6050) StartUp() error {
	mpu_sample_rate := 10 // 10 Hz read rate of hardware IMU
	yaw_mix_factor := 0   // must be zero if no magnetometer
	mpu.InitMPU(mpu_sample_rate, yaw_mix_factor)

	d.pitch_history = make([]float64, 0)
	d.roll_history = make([]float64, 0)

	d.started = true
	d.Run()

	return nil
}

/*

func (d *MPU6050) calibrate() {
	//TODO: Error checking to make sure that the histories are extensive enough to be significant.
	//TODO: Error checking to do continuous calibrations.
	pitch_adjust := float64(0)
	for _, v := range d.pitch_history {
		pitch_adjust = pitch_adjust + v
	}
	pitch_adjust = pitch_adjust / float64(len(d.pitch_history))
	d.pitch_resting = pitch_adjust

	roll_adjust := float64(0)
	for _, v := range d.roll_history {
		roll_adjust = roll_adjust + v
	}
	roll_adjust = roll_adjust / float64(len(d.roll_history))
	d.roll_resting = roll_adjust
	log.Printf("calibrate: pitch %f, roll %f\n", pitch_adjust, roll_adjust)
	d.calibrated = true
}

*/

func normalizeHeading(h float64) float64 {
	for h < float64(0.0) {
		h = h + float64(360.0)
	}
	for h >= float64(360.0) {
		h = h - float64(360.0)
	}
	return h
}

func (d *MPU6050) getMPUData() {
	pr, rr, hr, err := mpu.ReadMPU()

	// Convert from radians to degrees.
	pitch := float64(pr) * (float64(180.0) / math.Pi)
	roll := float64(-rr) * (float64(180.0) / math.Pi)
	heading := float64(hr) * (float64(180.0) / math.Pi)
	if heading < float64(0.0) {
		heading = float64(360.0) + heading
	}

	if err == nil {
		d.pitch = pitch
		d.roll = roll

		// Heading is raw value off the IMU. Without mag compass fusion, need to apply correction bias.
		// Amount of correction is set by ResetHeading() -- doesn't necessarily have to be based off GPS.
		d.heading = normalizeHeading((heading - d.heading_correction))

	} else {
		//		log.Printf("mpu6050.calculatePitchAndRoll(): mpu.ReadMPU() err: %s\n", err.Error())
	}
}

// Temperature returns the current temperature reading.
func (d *MPU6050) PitchAndRoll() (float64, float64) {
	return (d.pitch - d.pitch_resting), (d.roll - d.roll_resting)
}

func (d *MPU6050) Heading() float64 {
	return d.heading
}

func (d *MPU6050) Run() {
	time.Sleep(d.Poll)
	go func() {
		d.quit = make(chan struct{})
		timer := time.NewTicker(d.Poll)
		//		calibrateTimer := time.NewTicker(1 * time.Minute)
		for {
			select {
			case <-timer.C:
				d.getMPUData()
				//			case <-calibrateTimer.C:
				//				d.calibrate()
				//				calibrateTimer.Stop()
			case <-d.quit:
				mpu.CloseMPU()
				return
			}
		}
	}()
	return
}

// Set heading from a known value (usually GPS true heading).
func (d *MPU6050) ResetHeading(newHeading float64, gain float64) {
	if gain < 0.001 { // sanitize our inputs!
		gain = 0.001
	} else if gain > 1 {
		gain = 1
	}

	old_hdg := d.heading // only used for debug log report
	//newHeading = float64(30*time.Now().Minute()) // demo input for testing
	newHeading = normalizeHeading(newHeading) // sanitize the inputs

	// By applying gain factor, this becomes a 1st order function that slowly converges on solution.
	// Time constant is poll rate over gain. With gain of 0.1, convergence to +/-2 deg on a 180 correction difference is about 4 sec; 0.01 converges in 45 sec.

	hdg_corr_bias := float64(d.heading - newHeading) // desired adjustment to heading_correction
	if hdg_corr_bias > 180 {
		hdg_corr_bias = hdg_corr_bias - 360
	} else if hdg_corr_bias < -180 {
		hdg_corr_bias = hdg_corr_bias + 360
	}
	hdg_corr_bias = hdg_corr_bias * gain
	d.heading_correction = normalizeHeading(d.heading_correction + hdg_corr_bias)
	log.Printf("Adjusted heading. Old: %f Desired: %f  Adjustment: %f  New: %f\n", old_hdg, newHeading, hdg_corr_bias, d.heading-hdg_corr_bias)
}

// Close.
func (d *MPU6050) Close() {
	if d.quit != nil {
		d.quit <- struct{}{}
	}
}
