package controller

import (
	"fmt"
	"iter"
	"slices"

	"galaxy/calc"
	"galaxy/util"

	e "galaxy/error"

	"galaxy/game/internal/model/game"

	"github.com/google/uuid"
)

func (c *Cache) PlanetRename(ri int, number int, name string) error {
	n, ok := util.ValidateTypeName(name)
	if !ok {
		return e.NewEntityTypeNameValidationError("%q", n)
	}
	if number < 0 {
		return e.NewPlanetNumberError(number)
	}
	p, ok := c.Planet(uint(number))
	if !ok {
		return e.NewEntityNotExistsError("planet #%d", number)
	}
	if !p.OwnedBy(c.g.Race[ri].ID) {
		return e.NewEntityNotOwnedError("planet #%d", number)
	}
	c.g.Map.Planet[c.MustPlanetIndex(p.Number)].Name = n
	return nil
}

func (c *Cache) PlanetProduce(ri int, number int, prod game.ProductionType, subj string) error {
	c.validateRaceIndex(ri)
	if number < 0 {
		return e.NewPlanetNumberError(number)
	}
	p, ok := c.Planet(uint(number))
	if !ok {
		return e.NewEntityNotExistsError("planet #%d", number)
	}
	if !p.OwnedBy(c.g.Race[ri].ID) {
		return e.NewEntityNotOwnedError("planet #%d", number)
	}
	var subjectID *uuid.UUID
	if prod == game.ResearchScience || prod == game.ProductionShip {
		if _, ok := util.ValidateTypeName(subj); !ok {
			return e.NewEntityTypeNameValidationError("%s=%q", prod, subj)
		}
	}

	if prod == game.ResearchScience {
		i := slices.IndexFunc(c.g.Race[ri].Sciences, func(s game.Science) bool { return s.Name == subj })
		if i < 0 {
			return e.NewEntityNotExistsError("science %q", subj)
		}
		subjectID = &c.g.Race[ri].Sciences[i].ID
	}

	if prod == game.ProductionShip {
		st, _, ok := c.ShipClass(ri, subj)
		if !ok {
			return e.NewEntityNotExistsError("ship type %q", subj)
		}
		if p.Production.Type == game.ProductionShip &&
			p.Production.SubjectID != nil &&
			*p.Production.SubjectID == st.ID {
			// Planet already produces this ship type, keeping progress intact
			return nil
		}
		subjectID = &st.ID
	}

	if p.Production.Type == game.ProductionShip && (prod != game.ProductionShip || *subjectID != *p.Production.SubjectID) {
		p.ReleaseMaterial(c.MustShipType(ri, *p.Production.SubjectID).EmptyMass())
	} else if prod == game.ProductionShip {
		// new ship class to produce; otherwise we must have been returned from the func earlier
		p.Production.Progress = new(game.Float)
		p.Production.ProdUsed = new(game.Float)
	}

	if prod != game.ProductionShip {
		p.Production.Progress = nil
		p.Production.ProdUsed = nil
	}

	p.Production.Type = prod
	p.Production.SubjectID = subjectID
	return nil
}

func (c *Cache) PlanetProductionDisplayName(pn uint) string {
	p := c.MustPlanet(pn)
	if !p.Owned() {
		return "-"
	}
	ri := c.RaceIndex(*p.Owner)
	switch pt := p.Production.Type; pt {
	case game.ResearchDrive:
		return "Drive"
	case game.ResearchWeapons:
		return "Weapons"
	case game.ResearchShields:
		return "Shields"
	case game.ResearchCargo:
		return "Cargo"
	case game.ProductionMaterial:
		return "Material"
	case game.ProductionCapital:
		return "Capital"
	case game.ProductionShip:
		return c.MustShipType(ri, *p.Production.SubjectID).Name
	case game.ResearchScience:
		i := slices.IndexFunc(c.g.Race[ri].Sciences, func(sc game.Science) bool { return sc.ID == *p.Production.SubjectID })
		if i < 0 {
			panic("researching science not found")
		}
		return c.g.Race[ri].Sciences[i].Name
	default:
		return string(pt)
	}
}

func (c *Cache) Planet(planetNumber uint) (*game.Planet, bool) {
	if c.cachePlanetByPlanetNumber == nil {
		c.cachePlanetByPlanetNumber = make(map[uint]*game.Planet)
		for p := range c.g.Map.Planet {
			c.cachePlanetByPlanetNumber[c.g.Map.Planet[p].Number] = &c.g.Map.Planet[p]
		}
	}
	if v, ok := c.cachePlanetByPlanetNumber[planetNumber]; ok {
		return v, true
	} else {
		return nil, false
	}
}

func (c *Cache) MustPlanet(pn uint) *game.Planet {
	if v, ok := c.Planet(pn); ok {
		return v
	} else {
		panic(fmt.Sprintf("planet not found by number=%d", pn))
	}
}

func (c *Cache) MustPlanetIndex(pn uint) int {
	if idx := slices.IndexFunc(c.g.Map.Planet, func(p game.Planet) bool { return p.Number == pn }); idx < 0 {
		panic(fmt.Sprintf("planet not found by number=%d", pn))
	} else {
		return idx
	}
}

// Свободный "Производственный Потенциал" (L)
// промышленность * 0.75 + население * 0.25
// за вычетом затрат, расходуемых в течение хода на модернизацию кораблей
func (c *Cache) PlanetProductionCapacity(planetNumber uint) float64 {
	p := c.MustPlanet(planetNumber)
	var busyResources float64
	for sg := range c.shipGroupsInUpgrade(p.Number) {
		busyResources += sg.StateUpgrade.Cost()
	}
	return p.ProductionCapacity() - busyResources
}

func (c *Cache) TurnPlanetProductions() {
	for sgi := range c.ShipGroupsIndex() {
		sg := c.ShipGroup(sgi)
		// cancel upgrade for groups on wiped planets
		if sg.State() == game.StateUpgrade && !c.MustPlanet(sg.Destination).Owned() {
			sg.StateUpgrade = nil
		}
	}

	for pn := range c.listProducingPlanets() {
		p := c.MustPlanet(pn)
		ri := c.RaceIndex(*p.Owner)
		r := &c.g.Race[ri]

		// upgrade groups and return to in_orbit state
		productionAvailable := c.PlanetProductionCapacity(pn)
		for sg := range c.shipGroupsInUpgrade(p.Number) {
			cost := sg.StateUpgrade.Cost()
			if productionAvailable >= cost {
				for i := range sg.StateUpgrade.UpgradeTech {
					sg.Tech = sg.Tech.Set(sg.StateUpgrade.UpgradeTech[i].Tech, util.Fixed3(sg.StateUpgrade.UpgradeTech[i].Level.F()))
				}
				productionAvailable -= cost
			}
			sg.StateUpgrade = nil
		}

		switch pt := p.Production.Type; pt {
		case game.ProductionShip:
			st := c.MustShipType(ri, *p.Production.SubjectID)
			if ships := ProduceShip(p, productionAvailable, st.EmptyMass()); ships > 0 {
				c.unsafeCreateShips(ri, st.ID, p.Number, ships)
			}
		case game.ResearchScience:
			sc := c.mustScience(ri, *p.Production.SubjectID)
			ResearchTech(r, productionAvailable, sc.Drive.F(), sc.Weapons.F(), sc.Shields.F(), sc.Cargo.F())
		case game.ResearchDrive:
			ResearchTech(r, productionAvailable, 1., 0, 0, 0)
		case game.ResearchWeapons:
			ResearchTech(r, productionAvailable, 0, 1., 0, 0)
		case game.ResearchShields:
			ResearchTech(r, productionAvailable, 0, 0, 1., 0)
		case game.ResearchCargo:
			ResearchTech(r, productionAvailable, 0, 0, 0, 1.)
		case game.ProductionMaterial:
			p.ProduceMaterial(productionAvailable)
		case game.ProductionCapital:
			p.ProduceIndustry(productionAvailable)
		default:
			panic(fmt.Sprintf("unprocessed production type: '%v' for planet: #%d owner=%v", pt, pn, p.Owner))
		}

		// last step: increase population / colonists
		p.ProducePopulation()
	}
	c.TurnMergeEqualShipGroups()
}

// listProducingPlanets iterates over all inhabited planet numbers with defined production type.
// Planets producing ships guaranteed to be iterated first for correct turn actions order.
func (c *Cache) listProducingPlanets() iter.Seq[uint] {
	ordered := make([]int, 0)
	for i := range c.g.Map.Planet {
		if !c.g.Map.Planet[i].Owned() || c.g.Map.Planet[i].Production.Type == game.ProductionNone {
			continue
		}
		ordered = append(ordered, i)
	}
	slices.SortFunc(ordered, func(l, r int) int {
		if c.g.Map.Planet[l].Production.Type == game.ProductionShip && c.g.Map.Planet[r].Production.Type != game.ProductionShip {
			return -1
		}
		if c.g.Map.Planet[l].Production.Type != game.ProductionShip && c.g.Map.Planet[r].Production.Type == game.ProductionShip {
			return 1
		}
		return 0
	})
	return func(yield func(uint) bool) {
		for _, i := range ordered {
			if !yield(c.g.Map.Planet[i].Number) {
				return
			}
		}
	}
}

// Internal funcs

func (c *Cache) putPopulation(pn uint, v float64) {
	c.MustPlanet(pn).Pop(v)
}

func (c *Cache) putColonists(pn uint, v float64) {
	c.MustPlanet(pn).Col(v)
}

func (c *Cache) putMaterial(pn uint, v float64) {
	c.MustPlanet(pn).Mat(v)
}

// ProduceShip returns number of ships with shipMass planet p can produce in one turn
func ProduceShip(p *game.Planet, productionAvailable, shipMass float64) uint {
	if productionAvailable <= 0 {
		return 0
	}
	ships := uint(0)
	pa := productionAvailable
	var MATneed, totalCost float64
	for {
		MATneed = shipMass - float64(p.Material)
		if MATneed < 0 {
			MATneed = 0
		}
		totalCost = calc.ShipBuildCost(shipMass, float64(p.Material), float64(p.Resources))
		if pa < totalCost {
			progress := pa / totalCost
			pval := game.F(progress)
			if p.Production.Progress != nil {
				pval += *p.Production.Progress
			}
			p.Production.Progress = &pval
			fval := game.F(pa)
			p.Production.ProdUsed = &fval
			return ships
		} else {
			pa -= totalCost
			p.Mat(float64(p.Material) - shipMass + MATneed)
			ships += 1
		}
	}
}
