package gps

import (
	"strconv"
	"strings"
	"time"
)

type NMEASentenceType string

const (
	GSA NMEASentenceType = "GSA"
	GGA NMEASentenceType = "GGA"
	GLL NMEASentenceType = "GLL"
	GSV NMEASentenceType = "GSV"
	RMC NMEASentenceType = "RMC"
	VTG NMEASentenceType = "VTG"
	ZDA NMEASentenceType = "ZDA"
	TXT NMEASentenceType = "TXT"
)

// Parser for GPS NMEA sentences.
type Parser struct {
}

// Fix is a GPS location fix
type Fix struct {
	// Type is the NMEA sentence type that provided this fix.
	Type NMEASentenceType

	// Valid if the fix was valid.
	Valid bool

	// Time that the fix was taken, in UTC time.
	Time time.Time

	// Latitude is the decimal latitude. Negative numbers indicate S.
	Latitude float32

	// Longitude is the decimal longitude. Negative numbers indicate E.
	Longitude float32

	// Altitude is only returned for GGA sentences.
	Altitude int32

	// Satellites is the number of visible satellites, but is only returned for GGA sentences.
	Satellites int16

	// Speed based on reported movement. Only returned for RMC sentences.
	Speed float32

	// Heading based on reported movement. Only returned for RMC sentences.
	Heading float32
}

// NewParser returns a GPS NMEA Parser.
func NewParser() Parser {
	return Parser{}
}

// Parse parses a NMEA sentence looking for fix info.
func (parser *Parser) Parse(sentence string) (Fix, error) {
	var fix Fix
	if sentence == "" {
		return fix, ErrEmptyNMEASentence
	}
	if len(sentence) < 6 {
		return fix, ErrInvalidNMEASentenceLength
	}
	typ := sentence[3:6]
	switch typ {
	case "GSV":
		// https://docs.novatel.com/OEM7/Content/Logs/GPGSV.htm
		fields := strings.Split(sentence, ",")
		// GSV sentences have at least 4 fields, but typically 8, 12, 16, or 20 depending on satellites in view
		if len(fields) < 4 {
			return fix, errInvalidGSVSentence
		}

		fix.Type = GSV

		// Number of satellites in view is always field 3
		fix.Satellites = findSatellites(fields[3])

		// GSV does not provide position, time, or fix validity
		fix.Valid = false

		return fix, nil
	case "GGA":
		// https://docs.novatel.com/OEM7/Content/Logs/GPGGA.htm
		fields := strings.Split(sentence, ",")
		if len(fields) != 15 {
			return fix, errInvalidGGASentence
		}

		fix.Type = GGA
		fix.Time = findTime(fields[1])
		fix.Latitude = findLatitude(fields[2], fields[3])
		fix.Longitude = findLongitude(fields[4], fields[5])
		fix.Satellites = findSatellites(fields[7])
		fix.Altitude = findAltitude(fields[9])
		fix.Valid = (fix.Altitude != -99999) && (fix.Satellites > 0)

		return fix, nil
	case "GLL":
		// https://docs.novatel.com/OEM7/Content/Logs/GPGLL.htm
		fields := strings.Split(sentence, ",")
		if len(fields) != 8 {
			return fix, errInvalidGLLSentence
		}

		fix.Type = GLL
		fix.Latitude = findLatitude(fields[1], fields[2])
		fix.Longitude = findLongitude(fields[3], fields[4])
		fix.Time = findTime(fields[5])

		fix.Valid = (fields[6] == "A")

		return fix, nil
	case "RMC":
		// https://docs.novatel.com/OEM7/Content/Logs/GPRMC.htm
		fields := strings.Split(sentence, ",")
		if len(fields) != 13 {
			return fix, errInvalidRMCSentence
		}

		fix.Type = RMC
		fix.Time = findTime(fields[1])
		fix.Valid = (fields[2] == "A")
		fix.Latitude = findLatitude(fields[3], fields[4])
		fix.Longitude = findLongitude(fields[5], fields[6])
		fix.Speed = findSpeed(fields[7])
		fix.Heading = findHeading(fields[8])
		date := findDate(fields[9])
		fix.Time = date.Add(time.Duration(fix.Time.Hour())*time.Hour +
			time.Duration(fix.Time.Minute())*time.Minute +
			time.Duration(fix.Time.Second())*time.Second +
			time.Duration(fix.Time.Nanosecond())*time.Nanosecond)

		return fix, nil
	}

	return fix, ErrUnknownNMEASentence
}

// findTime returns the time from an NMEA sentence:
// $--GGA,hhmmss.ss,,,,,,,,,,,,,*xx
func findTime(val string) time.Time {
	if len(val) < 6 {
		return time.Time{}
	}

	h, _ := strconv.ParseInt(val[0:2], 10, 8)
	m, _ := strconv.ParseInt(val[2:4], 10, 8)
	s, _ := strconv.ParseInt(val[4:6], 10, 8)
	ms := int64(0)
	if len(val) > 6 {
		ms, _ = strconv.ParseInt(val[7:], 10, 16)
	}
	t := time.Date(0, 0, 0, int(h), int(m), int(s), int(ms), time.UTC)

	return t
}

// findAltitude returns the altitude from an NMEA sentence:
// $--GGA,,,,,,,,,25.8,,,,,*63
func findAltitude(val string) int32 {
	if len(val) > 0 {
		var v, _ = strconv.ParseFloat(val, 32)
		return int32(v)
	}
	return -99999
}

// findLatitude returns the Latitude from an NMEA sentence:
// $--GGA,,ddmm.mmmmm,x,,,,,,,,,,,*hh
func findLatitude(val, hemi string) float32 {
	if len(val) > 8 {
		dd := val[0:2]
		mm := val[2:]
		d, _ := strconv.ParseFloat(dd, 32)
		m, _ := strconv.ParseFloat(mm, 32)
		v := float32(d + (m / 60))
		if hemi == "S" {
			v *= -1
		}
		return v
	}
	return 0.0
}

// findLongitude returns the longitude from an NMEA sentence:
// $--GGA,,,,dddmm.mmmmm,x,,,,,,,,,*hh
func findLongitude(val, hemi string) float32 {
	if len(val) > 8 {
		var ddd = val[0:3]
		var mm = val[3:]
		var d, _ = strconv.ParseFloat(ddd, 32)
		var m, _ = strconv.ParseFloat(mm, 32)
		var v = float32(d + (m / 60))
		if hemi == "W" {
			v *= -1
		}
		return v
	}
	return 0.0
}

// findSatellites returns the satellites from an NMEA sentence:
// $--GGA,,,,,,,nn,,,,,,,*hh
func findSatellites(val string) (n int16) {
	if len(val) > 0 {
		var nn = val
		var v, _ = strconv.ParseInt(nn, 10, 32)
		n = int16(v)
		return n
	}
	return 0
}

// findDate returns the date from an RMC NMEA sentence.
func findDate(val string) time.Time {
	if len(val) < 6 {
		return time.Time{}
	}

	d, _ := strconv.ParseInt(val[0:2], 10, 8)
	m, _ := strconv.ParseInt(val[2:4], 10, 8)
	y, _ := strconv.ParseInt(val[4:6], 10, 8)
	t := time.Date(int(2000+y), time.Month(m), int(d), 0, 0, 0, 0, time.UTC)

	return t
}

// findSpeed returns the speed from an RMC NMEA sentence.
func findSpeed(val string) float32 {
	if len(val) > 0 {
		var v, _ = strconv.ParseFloat(val, 32)
		return float32(v)
	}
	return 0
}

// findHeading returns the speed from an RMC NMEA sentence.
func findHeading(val string) float32 {
	if len(val) > 0 {
		var v, _ = strconv.ParseFloat(val, 32)
		return float32(v)
	}
	return 0
}
