package bmi160

import (
	"time"

	"tinygo.org/x/drivers"
	"tinygo.org/x/drivers/internal/legacy"
	"tinygo.org/x/drivers/internal/pin"
)

// DeviceSPI is the SPI interface to a BMI160 accelerometer/gyroscope. There is
// also an I2C interface, but it is not yet supported.
type DeviceSPI struct {
	// Chip select pin
	csb pin.OutputFunc

	buf [7]byte

	// SPI bus (requires chip select to be usable).
	bus           drivers.SPI
	configurePins func()
}

// NewSPI returns a new device driver. The pin and SPI interface are not
// touched, provide a fully configured SPI object and call Configure to start
// using this device.
func NewSPI(csb pin.Output, spi drivers.SPI) *DeviceSPI {
	return &DeviceSPI{
		csb: csb.Set, // chip select
		bus: spi,
		configurePins: func() {
			legacy.ConfigurePinOut(csb)
		},
	}
}

// Configure configures the BMI160 for use. It configures the CSB pin and
// configures the BMI160, but it does not configure the SPI interface (it is
// assumed to be up and running).
func (d *DeviceSPI) Configure() error {
	if d.configurePins == nil {
		return legacy.ErrConfigBeforeInstantiated
	}
	d.configurePins()
	d.csb.High()
	// The datasheet recommends doing a register read from address 0x7F to get
	// SPI communication going:
	// > If CSB sees a rising edge after power-up, the BMI160 interface switches
	// > to SPI until a reset or the next power-up occurs. Therefore, a CSB
	// > rising edge is needed before starting the SPI communication. Hence, it
	// > is recommended to perform a SPI single read access to the ADDRESS 0x7F
	// > before the actual communication in order to use the SPI interface.
	d.readRegister(0x7F)

	// Power up the accelerometer. 0b0001_00nn is the command format, with 0b01
	// indicating normal mode.
	d.runCommand(0b0001_0001)

	// Power up the gyroscope. 0b0001_01nn is the command format, with 0b01
	// indicating normal mode.
	d.runCommand(0b0001_0101)

	// Wait until the device is fully initialized. Even after the command has
	// finished, the gyroscope may not be fully powered on. Therefore, wait
	// until we get an expected value.
	// This takes 30ms or so.
	for {
		// Wait for the acc_pmu_status and gyr_pmu_status to both be 0b01.
		if d.readRegister(reg_PMU_STATUS) == 0b0001_0100 {
			break
		}
	}

	return nil
}

// Connected check whether the device appears to be properly connected. It reads
// the CHIPID, which must be 0xD1 for the BMI160.
func (d *DeviceSPI) Connected() bool {
	return d.readRegister(reg_CHIPID) == 0xD1
}

// Reset restores the device to the state after power up. This can be useful to
// easily disable the accelerometer and gyroscope to reduce current consumption.
func (d *DeviceSPI) Reset() error {
	d.runCommand(0xB6) // softreset
	return nil
}

// ReadTemperature returns the temperature in celsius milli degrees (°C/1000).
func (d *DeviceSPI) ReadTemperature() (temperature int32, err error) {
	data := d.buf[:3]
	data[0] = 0x80 | reg_TEMPERATURE_0
	data[1] = 0
	data[2] = 0
	d.csb.Low()
	err = d.bus.Tx(data, data)
	d.csb.High()
	if err != nil {
		return
	}
	rawTemperature := int16(uint16(data[1]) | uint16(data[2])<<8)
	// 0x0000 is 23°C
	// 0x7fff is ~87°C
	// We use 0x8000 instead of 0x7fff to make the formula easier. The result
	// should be near identical and shouldn't affect the result too much (the
	// temperature sensor has an offset of around 2°C so isn't very reliable).
	// So the formula is as follows:
	// 1. Scale from 0x0000..0x8000 to 0..(87-23).
	//    rawTemperature * (87-23) / 0x8000
	// 2. Convert to centidegrees.
	//    rawTemperature * 1000 * (87-23) / 0x8000
	// 3. Add 23°C offset.
	//    rawTemperature * 1000 * (87-23) / 0x8000 + 23000
	// 4. Simplify.
	//    rawTemperature * 1000 * 64 / 0x8000 + 23000
	//    rawTemperature * 64000 / 0x8000 + 23000
	//    rawTemperature * 125 / 64 + 23000
	temperature = int32(rawTemperature)*125/64 + 23000
	return
}

// ReadAcceleration reads the current acceleration from the device and returns
// it in µg (micro-gravity). When one of the axes is pointing straight to Earth
// and the sensor is not moving the returned value will be around 1000000 or
// -1000000.
func (d *DeviceSPI) ReadAcceleration() (x int32, y int32, z int32, err error) {
	data := d.buf[:7]
	data[0] = 0x80 | reg_ACC_XL
	for i := 1; i < len(data); i++ {
		data[i] = 0
	}
	d.csb.Low()
	err = d.bus.Tx(data, data)
	d.csb.High()
	if err != nil {
		return
	}
	// Now do two things:
	// 1. merge the two values to a 16-bit number (and cast to a 32-bit integer)
	// 2. scale the value to bring it in the -1000000..1000000 range.
	//    This is done with a trick. What we do here is essentially multiply by
	//    1000000 and divide by 16384 to get the original scale, but to avoid
	//    overflow we do it at 1/64 of the value:
	//      1000000 / 64 = 15625
	//      16384   / 64 = 256
	x = int32(int16(uint16(data[1])|uint16(data[2])<<8)) * 15625 / 256
	y = int32(int16(uint16(data[3])|uint16(data[4])<<8)) * 15625 / 256
	z = int32(int16(uint16(data[5])|uint16(data[6])<<8)) * 15625 / 256
	return
}

// ReadRotation reads the current rotation from the device and returns it in
// µ°/s (micro-degrees/sec). This means that if you were to do a complete
// rotation along one axis and while doing so integrate all values over time,
// you would get a value close to 360000000.
func (d *DeviceSPI) ReadRotation() (x int32, y int32, z int32, err error) {
	data := d.buf[:7]
	data[0] = 0x80 | reg_GYR_XL
	for i := 1; i < len(data); i++ {
		data[i] = 0
	}
	d.csb.Low()
	err = d.bus.Tx(data, data)
	d.csb.High()
	if err != nil {
		return
	}
	// First the value is converted from a pair of bytes to a signed 16-bit
	// value and then to a signed 32-bit value to avoid integer overflow.
	// Then the value is scaled to µ°/s (micro-degrees per second).
	// The default is 2000°/s full scale range for -32768..32767.
	// The formula works as follows (taking X as an example):
	// 1. Scale from 32768 to 2000. This means that it is in °/s units.
	//    rawX * 2000 / 32768
	// 2. Scale to µ°/s by multiplying by 1e6.
	//    rawX * 1e6 * 2000 / 32768
	// 3. Simplify.
	//    rawX * 2e9 / 32768
	//    rawX * 1953125 / 32
	rawX := int32(int16(uint16(data[1]) | uint16(data[2])<<8))
	rawY := int32(int16(uint16(data[3]) | uint16(data[4])<<8))
	rawZ := int32(int16(uint16(data[5]) | uint16(data[6])<<8))
	x = int32(int64(rawX) * 1953125 / 32)
	y = int32(int64(rawY) * 1953125 / 32)
	z = int32(int64(rawZ) * 1953125 / 32)
	return
}

// runCommand runs a BMI160 command through the CMD register. It waits for the
// command to complete before returning.
func (d *DeviceSPI) runCommand(command uint8) {
	d.writeRegister(reg_CMD, command)
	for {
		response := d.readRegister(reg_CMD)
		if response == 0 {
			return // command was completed
		}
	}
}

// readRegister reads from a single BMI160 register. It should only be used for
// single register reads, not for reading multiple registers at once.
func (d *DeviceSPI) readRegister(address uint8) uint8 {
	// I don't know why but it appears necessary to sleep for a bit here.
	time.Sleep(time.Millisecond)

	data := d.buf[:2]
	data[0] = 0x80 | address
	data[1] = 0
	d.csb.Low()
	d.bus.Tx(data, data)
	d.csb.High()
	return data[1]
}

// writeRegister writes a single byte BMI160 register. It should only be used
// for writing to a single register.
func (d *DeviceSPI) writeRegister(address, data uint8) {
	// I don't know why but it appears necessary to sleep for a bit here.
	time.Sleep(time.Millisecond)

	buf := d.buf[:2]
	buf[0] = address
	buf[1] = data

	d.csb.Low()
	d.bus.Tx(buf, buf)
	d.csb.High()
}
