package gps

import (
	"time"
)

// FlightModeCmd is a UBX-CFG-NAV5 command
var nav5Cmd = CfgNav5{
	Mask:                  CfgNav5Dyn | CfgNav5MinEl | CfgNav5PosFixMode,
	DynModel:              DynModeAirborne1g, // Airborne with <1g acceleration
	FixMode:               FixModeAuto,       // Auto 2D/3D
	MinElev_deg:           5,                 // Minimum elevation 5 degrees
	FixedAlt_me2:          0,                 // Not used
	FixedAltVar_m2e4:      0,                 // Not used
	PDop:                  100,               // 10.0
	TDop:                  100,               // 10.0
	PAcc_m:                5000,              // 5 meters
	TAcc_m:                5000,              // 5 meters
	StaticHoldThresh_cm_s: 0,                 // Not used
	DgnssTimeout_s:        0,                 // Not used
	CnoThreshNumSVs:       0,                 // Not used
	CnoThresh_dbhz:        0,                 // Not used
	StaticHoldMaxDist_m:   0,                 // Not used
	UtcStandard:           0,                 // Automatic
	Reserved1:             [2]byte{},
	Reserved2:             [5]byte{},
}

// SetFlightMode sends UBX-CFG-NAV5 command to set GPS into flight mode
func (d *Device) SetFlightMode() (err error) {
	nav5Cmd.DynModel = DynModeAirborne1g
	nav5Cmd.FixMode = FixModeAuto
	nav5Cmd.Put42Bytes(d.buffer[:])

	return d.SendCommand(d.buffer[:42])
}

// SetPedestrianMode sends UBX-CFG-NAV5 command to set GPS into pedestrian mode
func (d *Device) SetPedestrianMode() (err error) {
	nav5Cmd.DynModel = DynModePedestrian
	nav5Cmd.FixMode = FixModeAuto
	nav5Cmd.Put42Bytes(d.buffer[:])

	return d.SendCommand(d.buffer[:42])
}

// SetAutomotiveMode sends UBX-CFG-NAV5 command to set GPS into automotive mode
func (d *Device) SetAutomotiveMode() (err error) {
	nav5Cmd.DynModel = DynModeAutomotive
	nav5Cmd.FixMode = FixModeAuto
	nav5Cmd.Put42Bytes(d.buffer[:])

	return d.SendCommand(d.buffer[:42])
}

// SetBikeMode sends UBX-CFG-NAV5 command to set GPS into bike mode
func (d *Device) SetBikeMode() (err error) {
	nav5Cmd.DynModel = DynModeBike
	nav5Cmd.FixMode = FixModeAuto
	nav5Cmd.Put42Bytes(d.buffer[:])

	return d.SendCommand(d.buffer[:42])
}

var (
	// GGA (time, lat/lng, altitude)
	messageRateGGACmd = CfgMsg1{
		MsgClass: 0xF0,
		MsgID:    0x00,
		Rate:     1, // Every position fix
	}
	// GLL (time, lat/lng)
	messageRateGLLCmd = CfgMsg1{
		MsgClass: 0xF0,
		MsgID:    0x01,
		Rate:     1, // Every position fix
	}
	// GSA (satellite id list)
	messageRateGSACmd = CfgMsg1{
		MsgClass: 0xF0,
		MsgID:    0x02,
		Rate:     0, // Disabled
	}
	// GSV (satellite locations)
	messageRateGSVCmd = CfgMsg1{
		MsgClass: 0xF0,
		MsgID:    0x03,
		Rate:     0, // Every position fix
	}
	// RMC (time, lat/lng, speed, course)
	messageRateRMCCmd = CfgMsg1{
		MsgClass: 0xF0,
		MsgID:    0x04,
		Rate:     1, // Every position fix
	}
	// VTG (speed, course)
	messageRateVTGCmd = CfgMsg1{
		MsgClass: 0xF0,
		MsgID:    0x05,
		Rate:     0, // Disabled
	}
	// ZDA (time, timezone)
	messageRateZDACmd = CfgMsg1{
		MsgClass: 0xF0,
		MsgID:    0x08,
		Rate:     0, // Disabled
	}
	// TXT (text transmission)
	messageRateTXTCmd = CfgMsg1{
		MsgClass: 0xF0,
		MsgID:    0x41,
		Rate:     0, // Disabled
	}
)

// SetMessageRatesMinimal configures the GPS to output a minimal set of NMEA sentences:
// GSV, GGA, GLL, and RMC only.
func SetMessageRatesMinimal(d *Device) (err error) {
	commands := []CfgMsg1{
		messageRateGSACmd,
		messageRateGLLCmd,
		messageRateVTGCmd,
		messageRateZDACmd,
		messageRateTXTCmd,
	}
	for i := range commands {
		commands[i].Rate = 0 // Disable
	}
	return setCfg1s(d, commands)
}

// SetMessageRatesAllEnabled configures the GPS to output all NMEA sentences
func SetMessageRatesAllEnabled(d *Device) (err error) {
	commands := []CfgMsg1{
		messageRateGSACmd,
		messageRateGGACmd,
		messageRateGLLCmd,
		messageRateGSVCmd,
		messageRateRMCCmd,
		messageRateVTGCmd,
		messageRateZDACmd,
		messageRateTXTCmd,
	}
	for i := range commands {
		commands[i].Rate = 1 // Enable
	}
	return setCfg1s(d, commands)
}

func setCfg1s(d *Device, commands []CfgMsg1) (err error) {
	var buf [9]byte
	for _, cmd := range commands {
		cmd.Put9Bytes(buf[:])
		// TODO handle errors differently here?
		// This implementation just saves the last error and continues.
		// Due to the GPS modules sending updates asynchronously
		// the response is interleaved along with regular ASCII
		// NMEA messages.
		err = d.SendCommand(buf[:])
		time.Sleep(100 * time.Millisecond)
	}

	return
}

// gnssDisableCmd is a UBX-CFG-GNSS command to disable all GNSS but GPS
// Needed for MAX8's, not needed for MAX7
var gnssDisableCmd = CfgGnss{
	MsgVer:      0x00,
	NumTrkChHw:  0x20, // 32 channels
	NumTrkChUse: 0x20,
	ConfigBlocks: []CfgGnssConfigBlocksType{
		{GnssId: 0, ResTrkCh: 8, MaxTrkCh: 16, Flags: CfgGnssEnable | 0x010000}, // GPS enabled
		{GnssId: 1, ResTrkCh: 1, MaxTrkCh: 3, Flags: 0x010000},                  // SBAS disabled
		{GnssId: 3, ResTrkCh: 8, MaxTrkCh: 16, Flags: 0x010000},                 // BeiDou disabled
		{GnssId: 5, ResTrkCh: 0, MaxTrkCh: 3, Flags: 0x010000},                  // QZSS disabled
		{GnssId: 6, ResTrkCh: 8, MaxTrkCh: 14, Flags: 0x010000},                 // GLONASS disabled
	},
}

// SetGNSSDisable sends UBX-CFG-GNSS command to disable all GNSS but GPS
func (d *Device) SetGNSSDisable() (err error) {
	err = gnssDisableCmd.Put(d.buffer[:])
	if err != nil {
		return err
	}

	return d.SendCommand(d.buffer[:])
}

// SendCommand sends a UBX command and waits for ACK/NAK response
func (d *Device) SendCommand(command []byte) error {
	// Calculate and append checksum
	checksummed := appendChecksum(command)
	d.WriteBytes(checksummed)

	start := time.Now()
	for time.Since(start) < time.Second {
		// Look for UBX sync sequence
		if d.readNextByte() != ubxSyncChar1 {
			continue
		}
		if d.readNextByte() != ubxSyncChar2 {
			continue
		}

		// Read message class and ID
		msgClass := d.readNextByte()
		msgID := d.readNextByte()

		// Check if it's an ACK class message
		if msgClass != ubxClassACK {
			continue
		}

		// Read length (2 bytes, little-endian) - ACK is always 2 bytes payload
		lenLo := d.readNextByte()
		lenHi := d.readNextByte()
		length := uint16(lenLo) | uint16(lenHi)<<8

		if length != 2 {
			continue
		}

		// Read ACK payload: class and ID of acknowledged message
		ackClass := d.readNextByte()
		ackID := d.readNextByte()

		// Verify ACK is for our command (command[2] = class, command[3] = ID)
		if ackClass != command[2] || ackID != command[3] {
			continue
		}

		if msgID == ubxACK_ACK {
			return nil
		}
		if msgID == ubxACK_NAK {
			return errGPSCommandRejected
		}
	}

	return errNoACKToGPSCommand
}

// appendChecksum calculates UBX checksum and appends it to the message
func appendChecksum(msg []byte) []byte {
	var ckA, ckB byte
	// Checksum covers class, ID, length, and payload (skip sync chars)
	for i := 2; i < len(msg); i++ {
		ckA += msg[i]
		ckB += ckA
	}
	return append(msg, ckA, ckB)
}
