package controller

import (
	"cmp"
	"fmt"
	"iter"
	"slices"

	"galaxy/calc"
	mr "galaxy/model/report"

	"galaxy/game/internal/model/game"

	"github.com/google/uuid"
)

func (c *Cache) Report(t uint, battles []*mr.BattleReport, bombings []*mr.Bombing) iter.Seq[*mr.Report] {
	report := c.InitReport(t)
	return func(yield func(*mr.Report) bool) {
		for i := range c.listRaceActingIdx() {
			c.ReportRace(i, report, battles, bombings)
			if !yield(report) {
				break
			}
		}
	}
}

func (c *Cache) InitReport(t uint) *mr.Report {
	report := &mr.Report{
		Turn:                   t,
		Width:                  c.g.Map.Width,
		Height:                 c.g.Map.Height,
		PlanetCount:            uint32(len(c.g.Map.Planet)),
		Player:                 make([]mr.Player, len(c.g.Race)),
		LocalScience:           make([]mr.Science, 0, 10),
		OtherScience:           make([]mr.OtherScience, 0, 10),
		LocalShipClass:         make([]mr.ShipClass, 0, 20),
		OtherShipClass:         make([]mr.OthersShipClass, 0, 50),
		Battle:                 make([]mr.BattleSummary, 0, 10),
		Bombing:                make([]*mr.Bombing, 0, 10),
		IncomingGroup:          make([]mr.IncomingGroup, 0, 10),
		OnPlanetGroupCache:     make(map[uint][]int),
		InSpaceGroupRangeCache: make(map[int]map[uint]float64),
	}

	sumVote, sumPop, sumInd := make(map[int]float64), make(map[int]float64), make(map[int]float64)
	planets := make(map[int]uint16)
	for pi := range c.g.Map.Planet {
		p := &c.g.Map.Planet[pi]
		if !p.Owned() {
			continue
		}
		ri := c.RaceIndex(*p.Owner)
		sumPop[ri] += p.Population.F()
		sumInd[ri] += p.Industry.F()
		planets[ri] = planets[ri] + 1
	}

	for ri := range c.listRaceIdx() {
		r := &c.g.Race[ri]
		rr := &report.Player[ri]

		rr.ID = r.ID
		rr.Name = r.Name
		rr.Extinct = r.Extinct
		rr.Drive = mr.F(r.TechLevel(game.TechDrive))
		rr.Weapons = mr.F(r.TechLevel(game.TechWeapons))
		rr.Shields = mr.F(r.TechLevel(game.TechShields))
		rr.Cargo = mr.F(r.TechLevel(game.TechCargo))
		rr.Planets = planets[ri]
		rr.Population = mr.F(sumPop[ri])
		rr.Industry = mr.F(sumInd[ri])

		// give voices by race index
		if vi := slices.IndexFunc(c.g.Race, func(v game.Race) bool { return r.VoteFor == v.ID }); vi < 0 {
			panic(fmt.Sprintf("voting for unknown race, id=%v", r.VoteFor))
		} else {
			sumVote[vi] += r.Votes.F()
			dest := &report.Player[vi]
			dest.Votes = mr.F(sumVote[vi])
		}
	}
	slices.SortFunc(report.Player, func(a, b mr.Player) int { return cmp.Compare(a.Name, b.Name) })

	for sgi := range c.g.ShipGroups {
		sg := &c.g.ShipGroups[sgi]
		if sg.State() == game.StateInSpace {
			// pre-calculate distances from in_space ship groups to every planet
			if _, ok := report.InSpaceGroupRangeCache[sgi]; !ok {
				report.InSpaceGroupRangeCache[sgi] = make(map[uint]float64)
			}
			for pi := range c.g.Map.Planet {
				p2 := &c.g.Map.Planet[pi]
				distance := calc.ShortDistance(c.g.Map.Width, c.g.Map.Height, sg.StateInSpace.X.F(), sg.StateInSpace.Y.F(), p2.X.F(), p2.Y.F())
				report.InSpaceGroupRangeCache[sgi][p2.Number] = distance
			}
		} else {
			// collect all orbiting ship groups by planet
			report.OnPlanetGroupCache[sg.Destination] = append(report.OnPlanetGroupCache[sg.Destination], sgi)
		}
	}

	return report
}

func (c *Cache) ReportRace(ri int, rep *mr.Report, battles []*mr.BattleReport, bombings []*mr.Bombing) {
	c.validateRaceIndex(ri)
	r := &c.g.Race[ri]

	rep.Race = r.Name
	rep.RaceID = r.ID

	// votes based on population
	rep.Votes = mr.F(r.Votes.F())

	// relations
	for i := range r.Relations {
		rii := slices.IndexFunc(rep.Player, func(v mr.Player) bool { return v.ID == r.Relations[i].RaceID })
		if rii < 0 {
			panic(fmt.Sprintf("opponent race for relation not found, id=%v", r.Relations[i].RaceID))
		}
		rep.Player[rii].Relation = r.Relations[i].Relation.String()
	}
	// self-relation is undefined
	if i := slices.IndexFunc(rep.Player, func(v mr.Player) bool { return v.ID == r.ID }); i < 0 {
		panic(fmt.Sprintf("race not found in report, id=%v", r.ID))
	} else {
		rep.Player[i].Relation = "-"
	}

	// sciences
	c.ReportLocalScience(ri, rep)
	c.ReportOtherScience(ri, rep)

	// ship classes
	c.ReportLocalShipClass(ri, rep)
	c.ReportOtherShipClass(ri, rep)

	// battles
	c.ReportBattle(ri, rep, battles)

	// bombings
	c.ReportBombing(ri, rep, bombings)

	// incoming groups
	c.ReportIncomingGroup(ri, rep)

	// player's planets
	c.ReportLocalPlanet(ri, rep)

	// ships in production
	c.ReportShipProduction(ri, rep)

	// cargo routes
	c.ReportRoute(ri, rep)

	// others' planets
	c.ReportOtherPlanet(ri, rep)

	// uninhabited planets
	c.ReportUninhabitedPlanet(ri, rep)

	// unidentified planets
	c.ReportUnidentifiedPlanet(ri, rep)

	// fleets
	c.ReportLocalFleet(ri, rep)

	// player's groups
	c.ReportLocalGroup(ri, rep)

	// others' groups
	c.ReportOtherGroup(ri, rep)

	// unidentified groups
	c.ReportUnidentifiedGroup(ri, rep)
}

func (c *Cache) ReportLocalScience(ri int, rep *mr.Report) {
	c.validateRaceIndex(ri)
	r := &c.g.Race[ri]

	clear(rep.LocalScience)

	for i := range r.Sciences {
		sliceIndexValidate(&rep.LocalScience, i)
		rep.LocalScience[i].Name = r.Sciences[i].Name
		rep.LocalScience[i].Drive = mr.F(r.Sciences[i].Drive.F())
		rep.LocalScience[i].Weapons = mr.F(r.Sciences[i].Weapons.F())
		rep.LocalScience[i].Shields = mr.F(r.Sciences[i].Shields.F())
		rep.LocalScience[i].Cargo = mr.F(r.Sciences[i].Cargo.F())
	}

	slices.SortFunc(rep.LocalScience, func(a, b mr.Science) int { return cmp.Compare(a.Name, b.Name) })
}

func (c *Cache) ReportOtherScience(ri int, rep *mr.Report) {
	c.validateRaceIndex(ri)
	r := &c.g.Race[ri]

	clear(rep.OtherScience)

	i := 0
	for sg := range c.listShipGroups(ri) {
		if sg.State() != game.StateInOrbit {
			continue
		}
		p := c.MustPlanet(sg.Destination)
		if !p.Owned() || p.OwnedBy(r.ID) || p.Production.Type != game.ResearchScience {
			continue
		}
		ownerIdx := c.RaceIndex(*p.Owner)
		owner := &c.g.Race[ownerIdx]
		sc := c.mustScience(ownerIdx, *p.Production.SubjectID)

		sliceIndexValidate(&rep.OtherScience, i)
		rep.OtherScience[i].Name = owner.Name
		rep.OtherScience[i].Drive = mr.F(sc.Drive.F())
		rep.OtherScience[i].Weapons = mr.F(sc.Weapons.F())
		rep.OtherScience[i].Shields = mr.F(sc.Shields.F())
		rep.OtherScience[i].Cargo = mr.F(sc.Cargo.F())
		i++
	}

	slices.SortFunc(rep.OtherScience, func(a, b mr.OtherScience) int {
		return cmp.Or(cmp.Compare(a.Race, b.Race), cmp.Compare(a.Name, b.Name))
	})
}

func (c *Cache) ReportLocalShipClass(ri int, report *mr.Report) {
	c.validateRaceIndex(ri)

	clear(report.LocalShipClass)

	i := 0
	for st := range c.ListShipTypes(ri) {
		sliceIndexValidate(&report.LocalShipClass, i)
		report.LocalShipClass[i].Name = st.Name
		report.LocalShipClass[i].Drive = mr.F(st.Drive.F())
		report.LocalShipClass[i].Armament = st.Armament
		report.LocalShipClass[i].Weapons = mr.F(st.Weapons.F())
		report.LocalShipClass[i].Shields = mr.F(st.Shields.F())
		report.LocalShipClass[i].Cargo = mr.F(st.Cargo.F())
		report.LocalShipClass[i].Mass = mr.F(st.EmptyMass())
		i++
	}

	slices.SortFunc(report.LocalShipClass, func(a, b mr.ShipClass) int { return cmp.Compare(a.Name, b.Name) })
}

func (c *Cache) ReportOtherShipClass(ri int, rep *mr.Report) {
	c.validateRaceIndex(ri)
	r := &c.g.Race[ri]

	clear(rep.OtherShipClass)

	i := 0

	used := make(map[uuid.UUID]map[string]bool)
	skip := func(ownerID uuid.UUID, className string) bool {
		if ownerID == r.ID {
			return true
		}
		if _, ok := used[ownerID]; ok {
			if _, ok := used[ownerID][className]; ok {
				return true
			}
		} else {
			used[ownerID] = make(map[string]bool)
		}
		used[ownerID][className] = true
		return false
	}

	// add visible ship classes from battles
	// for bi := range battle {
	// 	for si := range battle[bi].Ships {
	// 		g := battle[bi].Ships[si]
	// 		if skip(g.OwnerID, g.ClassName) {
	// 			continue
	// 		}

	// 		sliceIndexValidate(&rep.OtherShipClass, i)
	// 		rep.OtherShipClass[i].Race = c.g.Race[c.RaceIndex(g.OwnerID)].Name
	// 		rep.OtherShipClass[i].Name = g.ClassName
	// 		rep.OtherShipClass[i].Drive = g.DriveTech
	// 		rep.OtherShipClass[i].Armament = g.ClassArmament
	// 		rep.OtherShipClass[i].Weapons = g.WeaponsTech
	// 		rep.OtherShipClass[i].Shields = g.ShieldsTech
	// 		rep.OtherShipClass[i].Cargo = g.CargoTech
	// 		rep.OtherShipClass[i].Mass = g.ClassMass
	// 		i++
	// 	}
	// }

	// add visible ships from owned and observed planets
	for pn := range rep.OnPlanetGroupCache {
		p := c.MustPlanet(pn)
		if p.OwnedBy(r.ID) ||
			slices.IndexFunc(rep.OnPlanetGroupCache[pn], func(sgi int) bool { return c.ShipGroup(sgi).OwnerID == r.ID }) >= 0 {
			for _, sgi := range rep.OnPlanetGroupCache[pn] {
				sg := c.ShipGroup(sgi)
				st := c.ShipGroupShipClass(sgi)
				if skip(sg.OwnerID, st.Name) {
					continue
				}

				sliceIndexValidate(&rep.OtherShipClass, i)
				rep.OtherShipClass[i].Race = c.g.Race[c.RaceIndex(sg.OwnerID)].Name
				rep.OtherShipClass[i].Name = st.Name
				rep.OtherShipClass[i].Drive = mr.F(st.Drive.F())
				rep.OtherShipClass[i].Armament = st.Armament
				rep.OtherShipClass[i].Weapons = mr.F(st.Weapons.F())
				rep.OtherShipClass[i].Shields = mr.F(st.Shields.F())
				rep.OtherShipClass[i].Cargo = mr.F(st.Cargo.F())
				rep.OtherShipClass[i].Mass = mr.F(st.EmptyMass())
				i++
			}
		}
	}

	slices.SortFunc(rep.OtherShipClass, func(a, b mr.OthersShipClass) int {
		return cmp.Or(cmp.Compare(a.Race, b.Race), cmp.Compare(a.Name, b.Name))
	})
}

func (c *Cache) ReportBattle(ri int, rep *mr.Report, br []*mr.BattleReport) {
	c.validateRaceIndex(ri)
	r := &c.g.Race[ri]

	clear(rep.Battle)

	i := 0
	for bi := range br {
		visible := false
		for k := range br[bi].Races {
			visible = visible || br[bi].Races[k] == r.ID
		}
		if !visible {
			continue
		}

		sliceIndexValidate(&rep.Battle, i)
		rep.Battle[i] = mr.BattleSummary{
			ID:     br[bi].ID,
			Planet: br[bi].Planet,
			Shots:  uint(len(br[bi].Protocol)),
		}
		i++
	}
}

func (c *Cache) ReportBombing(ri int, rep *mr.Report, bombing []*mr.Bombing) {
	c.validateRaceIndex(ri)
	r := &c.g.Race[ri]

	clear(rep.Bombing)

	i := 0
	for bi := range bombing {
		pn := bombing[bi].Number
		visible := bombing[bi].PlanetOwnedID == r.ID // planet may be bombed and wiped
		for _, sgi := range rep.OnPlanetGroupCache[pn] {
			sg := c.ShipGroup(sgi)
			visible = visible || (sg.OwnerID == r.ID && sg.Destination == pn)
		}
		if !visible {
			continue
		}

		sliceIndexValidate(&rep.Bombing, i)
		rep.Bombing[i] = bombing[bi]
		i++
	}

	slices.SortFunc(rep.Bombing, func(a, b *mr.Bombing) int {
		return cmp.Or(cmp.Compare(a.Number, b.Number), boolCompare(a.Wiped, b.Wiped))
	})
}

func (c *Cache) ReportIncomingGroup(ri int, rep *mr.Report) {
	c.validateRaceIndex(ri)
	r := &c.g.Race[ri]

	clear(rep.IncomingGroup)

	i := 0
	for sgi := range c.ShipGroupsIndex() {
		sg := c.ShipGroup(sgi)
		st := c.ShipGroupShipClass(sgi)
		if sg.OwnerID == r.ID || sg.State() != game.StateInSpace {
			continue
		}
		p1 := c.MustPlanet(sg.StateInSpace.Origin)
		p2 := c.MustPlanet(sg.Destination)
		if !p2.OwnedBy(r.ID) {
			continue
		}

		distance := calc.ShortDistance(c.g.Map.Width, c.g.Map.Height, p1.X.F(), p1.Y.F(), p2.X.F(), p2.Y.F())
		var speed, mass float64
		if sg.FleetID != nil {
			speed, mass = c.FleetSpeedAndMass(c.MustFleetIndex(*sg.FleetID))
		} else {
			speed, mass = sg.Speed(st), sg.FullMass(st)
		}

		sliceIndexValidate(&rep.IncomingGroup, i)
		rep.IncomingGroup[i].Origin = sg.StateInSpace.Origin
		rep.IncomingGroup[i].Destination = sg.Destination
		rep.IncomingGroup[i].Distance = mr.F(distance)
		rep.IncomingGroup[i].Speed = mr.F(speed)
		rep.IncomingGroup[i].Mass = mr.F(mass)
		i++
	}
}

func (c *Cache) ReportLocalPlanet(ri int, rep *mr.Report) {
	c.validateRaceIndex(ri)
	r := &c.g.Race[ri]

	clear(rep.LocalPlanet)

	i := 0
	for pi := range c.g.Map.Planet {
		p := &c.g.Map.Planet[pi]
		if !p.OwnedBy(r.ID) {
			continue
		}

		sliceIndexValidate(&rep.LocalPlanet, i)
		c.localPlanet(&rep.LocalPlanet[i], p)
		// rep.LocalPlanet[i].UnidentifiedPlanet.Number = p.Number
		// rep.LocalPlanet[i].UnidentifiedPlanet.X = mr.F(p.X.F())
		// rep.LocalPlanet[i].UnidentifiedPlanet.Y = mr.F(p.Y.F())
		// rep.LocalPlanet[i].UninhabitedPlanet.Size = mr.F(p.Size.F())
		// rep.LocalPlanet[i].UninhabitedPlanet.Name = p.Name
		// rep.LocalPlanet[i].UninhabitedPlanet.Resources = mr.F(p.Resources.F())
		// rep.LocalPlanet[i].UninhabitedPlanet.Capital = mr.F(p.Capital.F())
		// rep.LocalPlanet[i].UninhabitedPlanet.Material = mr.F(p.Material.F())
		// rep.LocalPlanet[i].Industry = mr.F(p.Industry.F())
		// rep.LocalPlanet[i].Population = mr.F(p.Population.F())
		// rep.LocalPlanet[i].Colonists = mr.F(p.Colonists.F())
		// rep.LocalPlanet[i].Production = c.PlanetProductionDisplayName(p.Number)
		// rep.LocalPlanet[i].FreeIndustry = mr.F(p.ProductionCapacity())
		// for _, sgi := range rep.PlanetGroupsCache[p.Number] {
		// 	sg := c.ShipGroup(sgi)
		// 	if sg.StateUpgrade == nil {
		// 		break
		// 	}
		// 	// between-turn report: ships upgrading on the planet decreases free indistrial potential
		// 	rep.LocalPlanet[i].FreeIndustry -= mr.F(sg.StateUpgrade.Cost())
		// }
		i++
	}
}

func (c *Cache) ReportOtherPlanet(ri int, rep *mr.Report) {
	c.validateRaceIndex(ri)
	r := &c.g.Race[ri]

	clear(rep.OtherPlanet)

	i := 0
	for sg := range c.listShipGroups(ri) {
		if sg.State() != game.StateInOrbit {
			continue
		}
		p := c.MustPlanet(sg.Destination)
		if !p.Owned() || p.OwnedBy(r.ID) {
			continue
		}

		sliceIndexValidate(&rep.OtherPlanet, i)
		c.localPlanet(&rep.OtherPlanet[i].LocalPlanet, p)
		rep.OtherPlanet[i].Owner = c.g.Race[c.RaceIndex(*p.Owner)].Name
		i++
	}
}

func (c *Cache) ReportUninhabitedPlanet(ri int, rep *mr.Report) {
	c.validateRaceIndex(ri)

	clear(rep.UninhabitedPlanet)

	i := 0
	for sg := range c.listShipGroups(ri) {
		if sg.State() != game.StateInOrbit {
			continue
		}
		p := c.MustPlanet(sg.Destination)
		if p.Owned() {
			continue
		}

		sliceIndexValidate(&rep.UninhabitedPlanet, i)
		uninhabitedPlanet(&rep.UninhabitedPlanet[i], p)
		i++
	}
}

func (c *Cache) ReportUnidentifiedPlanet(ri int, rep *mr.Report) {
	c.validateRaceIndex(ri)
	r := &c.g.Race[ri]

	clear(rep.UnidentifiedPlanet)

	i := 0
	for pi := range c.g.Map.Planet {
		p := &c.g.Map.Planet[pi]

		// skip player's owned planets
		if p.OwnedBy(r.ID) {
			continue
		}

		// skip planets where player's group are orbiting
		if slices.IndexFunc(rep.OnPlanetGroupCache[p.Number], func(sgi int) bool { return c.ShipGroup(sgi).OwnerID == r.ID }) >= 0 {
			continue
		}

		sliceIndexValidate(&rep.UnidentifiedPlanet, i)
		unidentifiedPlanet(&rep.UnidentifiedPlanet[i], p)
		i++
	}
}

func (c *Cache) ReportShipProduction(ri int, rep *mr.Report) {
	c.validateRaceIndex(ri)
	r := &c.g.Race[ri]

	clear(rep.ShipProduction)

	i := 0
	for pi := range c.g.Map.Planet {
		p := &c.g.Map.Planet[pi]
		if !p.OwnedBy(r.ID) || p.Production.Type != game.ProductionShip {
			continue
		}
		st := c.MustShipType(ri, *p.Production.SubjectID)

		sliceIndexValidate(&rep.ShipProduction, i)
		rep.ShipProduction[pi].Planet = p.Number
		rep.ShipProduction[pi].Class = st.Name
		rep.ShipProduction[pi].Cost = mr.F(calc.ShipProductionCost(st.EmptyMass()))
		rep.ShipProduction[pi].Free = mr.F(c.PlanetProductionCapacity(p.Number))
		rep.ShipProduction[pi].ProdUsed = mr.F((*p.Production.ProdUsed).F())
		rep.ShipProduction[pi].Percent = mr.F((*p.Production.Progress).F())
		i++
	}
}

func (c *Cache) ReportRoute(ri int, rep *mr.Report) {
	c.validateRaceIndex(ri)
	r := &c.g.Race[ri]

	clear(rep.Route)

	i := 0
	for pi := range c.g.Map.Planet {
		p := &c.g.Map.Planet[pi]
		if !p.OwnedBy(r.ID) || len(p.Route) == 0 {
			continue
		}

		sliceIndexValidate(&rep.Route, i)
		rep.Route[i].Planet = p.Number
		// rep.Route[i].Route = make(map[uint]string)
		for rt, dest := range p.Route {
			rep.Route[i].Route[dest] = rt.String()
		}
		i++
	}
}

func (c *Cache) ReportLocalFleet(ri int, rep *mr.Report) {
	c.validateRaceIndex(ri)

	clear(rep.LocalFleet)

	i := 0
	for fl := range c.listFleets(ri) {
		fi := c.MustFleetIndex(fl.ID)
		gid := slices.Collect(c.fleetGroupIds(ri, fi))
		if len(gid) == 0 {
			continue
		}

		speed, _ := c.FleetSpeedAndMass(fi)
		fleetState := c.FleetState(fl.ID)

		sliceIndexValidate(&rep.LocalFleet, i)
		rep.LocalFleet[i].Name = fl.Name
		rep.LocalFleet[i].Groups = uint(len(gid))
		rep.LocalFleet[i].Speed = mr.F(speed)
		rep.LocalFleet[i].State = fleetState.State.String()
		rep.LocalFleet[i].Destination = fleetState.Destination
		if inSpace, ok := fleetState.InSpace(); ok {
			rep.LocalFleet[i].Origin = &inSpace.Origin
			p2 := c.MustPlanet(rep.LocalFleet[i].Destination)
			rangeToDestination := mr.F(calc.ShortDistance(c.g.Map.Width, c.g.Map.Height, inSpace.X.F(), inSpace.Y.F(), p2.X.F(), p2.Y.F()))
			rep.LocalFleet[i].Range = &rangeToDestination
		}
		i++
	}
}

func (c *Cache) ReportLocalGroup(ri int, rep *mr.Report) {
	c.validateRaceIndex(ri)

	clear(rep.LocalGroup)

	i := 0
	for sg := range c.listShipGroups(ri) {
		sliceIndexValidate(&rep.LocalGroup, i)
		st := c.MustShipType(ri, sg.TypeID)
		c.otherGroup(&rep.LocalGroup[i].OtherGroup, sg, st)
		rep.LocalGroup[i].ID = sg.ID
		rep.LocalGroup[i].State = sg.State().String()
		if sg.FleetID != nil {
			rep.LocalGroup[i].Fleet = &c.g.Fleets[c.MustFleetIndex(*sg.FleetID)].Name
		}
		// rep.LocalGroup[i].Number = sg.Number
		// rep.LocalGroup[i].Class = st.Name
		// // rep.LocalGroup[i].Tech = make(map[string]mr.Float)
		// for t, v := range sg.Tech {
		// 	rep.LocalGroup[i].Tech[t.String()] = mr.F(v)
		// }
		// rep.LocalGroup[i].Cargo = sg.CargoString()
		// rep.LocalGroup[i].Load = mr.F(sg.Load.F())
		// rep.LocalGroup[i].Destination = sg.Destination
		// if sg.State() == game.StateInSpace {
		// 	rep.LocalGroup[i].Origin = &sg.StateInSpace.Origin
		// 	p2 := c.MustPlanet(rep.LocalGroup[i].Destination)
		// 	rangeToDestination := mr.F(util.ShortDistance(c.g.Map.Width, c.g.Map.Height, sg.StateInSpace.X.F(), sg.StateInSpace.Y.F(), p2.X.F(), p2.Y.F()))
		// 	rep.LocalGroup[i].Range = &rangeToDestination
		// }
		// rep.LocalGroup[i].Speed = mr.F(sg.Speed(st))
		// rep.LocalGroup[i].Mass = mr.F(st.EmptyMass())
		i++
	}
}

func (c *Cache) ReportOtherGroup(ri int, rep *mr.Report) {
	c.validateRaceIndex(ri)
	r := &c.g.Race[ri]

	clear(rep.OtherGroup)

	used := make(map[int]bool)
	skip := func(sgi int) bool {
		if c.ShipGroup(sgi).OwnerID == r.ID {
			return true
		}
		if _, ok := used[sgi]; ok {
			return true
		}
		used[sgi] = true
		return false
	}

	i := 0

	// visible groups from owned and observed planets
	for pn := range rep.OnPlanetGroupCache {
		p := c.MustPlanet(pn)
		if p.OwnedBy(r.ID) ||
			slices.IndexFunc(rep.OnPlanetGroupCache[pn], func(sgi int) bool { return c.ShipGroup(sgi).OwnerID == r.ID }) >= 0 {
			for _, sgi := range rep.OnPlanetGroupCache[pn] {
				sg := c.ShipGroup(sgi)
				st := c.ShipGroupShipClass(sgi)
				if skip(sgi) {
					continue
				}

				sliceIndexValidate(&rep.OtherGroup, i)
				c.otherGroup(&rep.OtherGroup[i], sg, st)
				i++
			}
		}
	}
}

func (c *Cache) ReportUnidentifiedGroup(ri int, rep *mr.Report) {
	c.validateRaceIndex(ri)
	r := &c.g.Race[ri]
	flightDistance := r.FlightDistance()

	clear(rep.UnidentifiedGroup)

	i := 0
	for sgi := range rep.InSpaceGroupRangeCache {
		sg := c.ShipGroup(sgi)
		if sg.OwnerID == rep.RaceID {
			continue
		}
		if sg.StateInSpace == nil {
			panic(fmt.Sprintf("pre-calculated distance group not in space: i=%d", sgi))
		}
		for pi := range c.g.Map.Planet {
			p := &c.g.Map.Planet[pi]
			if !p.OwnedBy(r.ID) {
				continue
			}
			if v, ok := rep.InSpaceGroupRangeCache[sgi][p.Number]; !ok {
				panic(fmt.Sprintf("distance cache not pre-calculated: i=%d p=#%d", sgi, p.Number))
			} else if v <= flightDistance {
				sliceIndexValidate(&rep.UnidentifiedGroup, i)
				rep.UnidentifiedGroup[i].X = mr.F(sg.StateInSpace.X.F())
				rep.UnidentifiedGroup[i].Y = mr.F(sg.StateInSpace.Y.F())
				i++
			}
		}
	}
}

func (c *Cache) otherGroup(v *mr.OtherGroup, sg *game.ShipGroup, st *game.ShipType) {
	v.Number = sg.Number
	v.Class = st.Name
	// rep.LocalGroup[i].Tech = make(map[string]mr.Float)
	for t, val := range sg.Tech {
		v.Tech[t.String()] = mr.F(val.F())
	}
	v.Cargo = sg.CargoString()
	v.Load = mr.F(sg.Load.F())
	v.Destination = sg.Destination
	if sg.State() == game.StateInSpace {
		v.Origin = &sg.StateInSpace.Origin
		p2 := c.MustPlanet(v.Destination)
		rangeToDestination := mr.F(calc.ShortDistance(c.g.Map.Width, c.g.Map.Height, sg.StateInSpace.X.F(), sg.StateInSpace.Y.F(), p2.X.F(), p2.Y.F()))
		v.Range = &rangeToDestination
	}
	v.Speed = mr.F(sg.Speed(st))
	v.Mass = mr.F(st.EmptyMass())
}

func (c *Cache) localPlanet(v *mr.LocalPlanet, p *game.Planet) {
	uninhabitedPlanet(&v.UninhabitedPlanet, p)
	v.Industry = mr.F(p.Industry.F())
	v.Population = mr.F(p.Population.F())
	v.Colonists = mr.F(p.Colonists.F())
	v.Production = c.PlanetProductionDisplayName(p.Number)
	// between-turn report: ships upgrading on the planet decreases free indistrial potential
	v.FreeIndustry = mr.F(c.PlanetProductionCapacity(p.Number))
}

func uninhabitedPlanet(v *mr.UninhabitedPlanet, p *game.Planet) {
	unidentifiedPlanet(&v.UnidentifiedPlanet, p)
	v.Size = mr.F(p.Size.F())
	v.Name = p.Name
	v.Resources = mr.F(p.Resources.F())
	v.Capital = mr.F(p.Capital.F())
	v.Material = mr.F(p.Material.F())
}

func unidentifiedPlanet(v *mr.UnidentifiedPlanet, p *game.Planet) {
	v.Number = p.Number
	v.X = mr.F(p.X.F())
	v.Y = mr.F(p.Y.F())
}

func sliceIndexValidate[S ~[]E, E any](s *S, i int) {
	if cap(*s) < i+1 {
		*s = slices.Grow(*s, 10)
	}
	if len(*s) < i+1 {
		*s = (*s)[:i+1]
	}
}

func boolCompare(a, b bool) int {
	if a == b {
		return 0
	}
	if a == false {
		return -1
	}
	return 1
}
