package world

import (
	"a301_game_server/internal/entity"
	"a301_game_server/pkg/mathutil"
)

// cellKey uniquely identifies a grid cell.
type cellKey struct {
	cx, cz int
}

// GridAOI implements AOI using a spatial grid. Entities in nearby cells are considered visible.
type GridAOI struct {
	cellSize   float32
	viewRange  int
	cells      map[cellKey]map[uint64]entity.Entity
	entityCell map[uint64]cellKey
}

func NewGridAOI(cellSize float32, viewRange int) *GridAOI {
	return &GridAOI{
		cellSize:   cellSize,
		viewRange:  viewRange,
		cells:      make(map[cellKey]map[uint64]entity.Entity),
		entityCell: make(map[uint64]cellKey),
	}
}

func (g *GridAOI) posToCell(pos mathutil.Vec3) cellKey {
	cx := int(pos.X / g.cellSize)
	cz := int(pos.Z / g.cellSize)
	if pos.X < 0 {
		cx--
	}
	if pos.Z < 0 {
		cz--
	}
	return cellKey{cx, cz}
}

func (g *GridAOI) Add(ent entity.Entity) {
	cell := g.posToCell(ent.Position())
	g.addToCell(cell, ent)
	g.entityCell[ent.EntityID()] = cell
}

func (g *GridAOI) Remove(ent entity.Entity) []AOIEvent {
	eid := ent.EntityID()
	cell, ok := g.entityCell[eid]
	if !ok {
		return nil
	}

	nearby := g.getNearbyFromCell(cell, eid)
	events := make([]AOIEvent, 0, len(nearby))
	for _, observer := range nearby {
		events = append(events, AOIEvent{Observer: observer, Target: ent, Type: AOILeave})
	}

	g.removeFromCell(cell, eid)
	delete(g.entityCell, eid)
	return events
}

func (g *GridAOI) UpdatePosition(ent entity.Entity, oldPos, newPos mathutil.Vec3) []AOIEvent {
	eid := ent.EntityID()
	oldCell := g.posToCell(oldPos)
	newCell := g.posToCell(newPos)

	if oldCell == newCell {
		return nil
	}

	oldVisible := g.visibleCells(oldCell)
	newVisible := g.visibleCells(newCell)

	leaving := cellDifference(oldVisible, newVisible)
	entering := cellDifference(newVisible, oldVisible)

	var events []AOIEvent

	for _, c := range leaving {
		if cellEntities, ok := g.cells[c]; ok {
			for _, other := range cellEntities {
				if other.EntityID() == eid {
					continue
				}
				events = append(events,
					AOIEvent{Observer: other, Target: ent, Type: AOILeave},
					AOIEvent{Observer: ent, Target: other, Type: AOILeave},
				)
			}
		}
	}

	for _, c := range entering {
		if cellEntities, ok := g.cells[c]; ok {
			for _, other := range cellEntities {
				if other.EntityID() == eid {
					continue
				}
				events = append(events,
					AOIEvent{Observer: other, Target: ent, Type: AOIEnter},
					AOIEvent{Observer: ent, Target: other, Type: AOIEnter},
				)
			}
		}
	}

	g.removeFromCell(oldCell, eid)
	g.addToCell(newCell, ent)
	g.entityCell[eid] = newCell

	return events
}

func (g *GridAOI) GetNearby(ent entity.Entity) []entity.Entity {
	cell, ok := g.entityCell[ent.EntityID()]
	if !ok {
		return nil
	}
	return g.getNearbyFromCell(cell, ent.EntityID())
}

func (g *GridAOI) getNearbyFromCell(cell cellKey, excludeID uint64) []entity.Entity {
	var result []entity.Entity
	for _, c := range g.visibleCells(cell) {
		if cellEntities, ok := g.cells[c]; ok {
			for _, e := range cellEntities {
				if e.EntityID() != excludeID {
					result = append(result, e)
				}
			}
		}
	}
	return result
}

func (g *GridAOI) visibleCells(center cellKey) []cellKey {
	size := (2*g.viewRange + 1) * (2*g.viewRange + 1)
	cells := make([]cellKey, 0, size)
	for dx := -g.viewRange; dx <= g.viewRange; dx++ {
		for dz := -g.viewRange; dz <= g.viewRange; dz++ {
			cells = append(cells, cellKey{center.cx + dx, center.cz + dz})
		}
	}
	return cells
}

func (g *GridAOI) addToCell(cell cellKey, ent entity.Entity) {
	if g.cells[cell] == nil {
		g.cells[cell] = make(map[uint64]entity.Entity)
	}
	g.cells[cell][ent.EntityID()] = ent
}

func (g *GridAOI) removeFromCell(cell cellKey, eid uint64) {
	if m, ok := g.cells[cell]; ok {
		delete(m, eid)
		if len(m) == 0 {
			delete(g.cells, cell)
		}
	}
}

func cellDifference(a, b []cellKey) []cellKey {
	set := make(map[cellKey]struct{}, len(b))
	for _, c := range b {
		set[c] = struct{}{}
	}
	var diff []cellKey
	for _, c := range a {
		if _, ok := set[c]; !ok {
			diff = append(diff, c)
		}
	}
	return diff
}
