package simulation

/*
  Agent qui se dirige vers la porte la plus proche sans trop de monde (bon rapport monde/proximité )
  On normalise les valeurs de proximité et de monde pour avoir un score entre 0 et 1, on choisit la porte ayant le moins du score (le score est la somme des deux valeurs normalisées)
  Si plusieurs portes ont le même score, on choisit celle ayant le moins de monde
*/

import (
	"fmt"
	"math"
	"math/rand"
	alg "metrosim/internal/algorithms"
	req "metrosim/internal/request"
	"sort"
	"time"
)

type UsagerNormal struct {
	req *req.Request // req recue par l'agent lambda
	//once sync.Once
}

func (un *UsagerNormal) Percept(ag *Agent) {
	//un.once.Do(func() { un.SetUpDestination(ag) }) // la fonction setUp est executé à la premiere appel de la fonction Percept()
	switch {
	case ag.request != nil: //verifier si l'agent est communiqué par un autre agent, par exemple un controleur lui a demandé de s'arreter
		//print("req recue par l'agent lambda : ", ag.request.decision, "\n")
		un.req = ag.request
	default:
		ag.stuck = ag.isStuck()
		if ag.stuck {
			return

		}
	}
}

func (un *UsagerNormal) Deliberate(ag *Agent) {
	//fmt.Println("[AgentLambda Deliberate] decision :", un.req.decision)
	if un.req != nil {
		switch un.req.Decision() {
		case Stop:
			ag.decision = Stop
			return
		case Expel: // cette condition est inutile car l'usager lambda ne peut pas etre expunsé , elle est nécessaire pour les agents fraudeurs
			//fmt.Println("[AgentLambda, Deliberate] Expel")
			ag.decision = Expel
			return
		case Disappear:
			ag.decision = Disappear
			return
		case EnterMetro:
			ag.decision = EnterMetro
			return
		case Wait:
			ag.decision = Wait
			return
		case Move:
			ag.decision = Move
			return
		case YouHaveToMove:
			fmt.Println("[AgentNormal, Deliberate] J'essaye de bouger ", ag.id)
			movement := ag.MoveAgent()
			//fmt.Printf("Je suis agent %s Resultat du mouvement de la personne %t \n", ag.id, movement)
			if movement {
				fmt.Println("[AgentNormal, Deliberate] J'ai bougé ", ag.id)
				ag.decision = Done
			} else {
				ag.decision = Noop
			}
			return
		}
	} else if (ag.position != ag.departure && ag.position == ag.destination) && (ag.isOn[ag.position] == "W" || ag.isOn[ag.position] == "S") { // si l'agent est arrivé à sa destination et qu'il est sur une sortie
		//fmt.Println(ag.id, "disappear")
		ag.decision = Disappear
	} else if ag.stuck { // si l'agent est bloqué
		ag.decision = Wait
	} else {
		ag.decision = Move
		//un.setUpDestination(ag)
	}
}

func (un *UsagerNormal) Act(ag *Agent) {
	//fmt.Println("[AgentLambda Act] decision :",ag.decision)
	switch ag.decision {
	case Stop:
		time.Sleep(time.Duration(5) * time.Second)
	case Move:
		ag.MoveAgent()
	case Wait: // temps d'attente aléatoire
		n := rand.Intn(2)
		time.Sleep(time.Duration(n) * time.Second)
	case Disappear:
		//fmt.Printf("[UsagerLambda, Act] agent %s est disparu \n",ag.id)
		ag.env.RemoveAgent(ag)
	case EnterMetro:
		fmt.Printf("[UsagerNormal, Act] agent %s entre dans le Metro \n", ag.id)
		ag.env.RemoveAgent(ag)
		//fmt.Printf("Demandeur d'entrer le metro : %s \n",un.req.Demandeur())
		un.req.Demandeur() <- *req.NewRequest(ag.env.agentsChan[ag.id], ACK)
	case Expel:
		//fmt.Println("[AgentLambda, Act] Expel")
		ag.destination = ag.findNearestExit()
		fmt.Printf("[UsagerNormal, Act] destination de l'agent %s = %s \n", ag.id, ag.destination)
		ag.env.controlledAgents[ag.id] = true
		ag.path = make([]alg.Node, 0)
		ag.MoveAgent()

	case Noop:
		//Cas ou un usager impoli demande a un usager de bouger et il refuse
		un.req.Demandeur() <- *req.NewRequest(ag.env.agentsChan[ag.id], Noop)
		// nothing to do
	case Done:
		//Cas ou un usager impoli demande a un usager de bouger et il le fait
		un.req.Demandeur() <- *req.NewRequest(ag.env.agentsChan[ag.id], Done)
	case TryToMove:
		movement := ag.MoveAgent()
		fmt.Printf("Je suis %s est-ce que j'ai bougé? %t \n", ag.id, movement)
		if movement {
			un.req.Demandeur() <- *req.NewRequest(ag.env.agentsChan[ag.id], Done)
		} else {
			un.req.Demandeur() <- *req.NewRequest(ag.env.agentsChan[ag.id], Noop)
		}
	}
	ag.request = nil
}

func (un *UsagerNormal) SetUpAleaDestination(ag *Agent) {
	//t := rand.Intn(10) +1
	//time.Sleep(time.Duration(t) * time.Second) // "cool down"
	//fmt.Println("[UsagerNormal, setUpDestination] setUpDestination")
	choix_voie := rand.Intn(len(ag.env.metros)) // choix de la voie de métro aléatoire
	dest_porte := (un.findBestGate(ag, ag.env.metros[choix_voie].way.gates))
	ag.destination = dest_porte
}

func (un *UsagerNormal) findBestGate(ag *Agent, gates []alg.Coord) alg.Coord {

	uniquegates := make([]alg.Coord, 0)
	for i, gate := range gates {
		if i+1 < len(gates) && twocloseGate(gate, gates[i+1]) {
			// Si la porte est trop proche d'une autre, on l'ignore, on considère que c'est la même
			continue
		}
		uniquegates = append(uniquegates, gate)
		//gatesDistances[i] = Gate{Position: gate, Distance: float64(dist), NbAgents: nbAgents}
	}

	gatesDistances := make([]Gate, len(uniquegates))
	for i, gate := range uniquegates {
		dist := alg.Abs(ag.position[0]-gate[0]) + alg.Abs(ag.position[1]-gate[1]) // Distance de Manhattan entre l'agent et la porte
		nbAgents := float64(ag.env.getNbAgentsAround(gate))
		gatesDistances[i] = Gate{Position: gate, Distance: float64(dist), NbAgents: nbAgents}
	}

	//fmt.Println("[findBestGate] agent : ",ag.id)
	//fmt.Println("[findBestGate] agent Position : ",ag.position)
	//fmt.Println("[findBestGate] gates non normalisé : ",gatesDistances)
	normalizedGates, _, _ := normalizeGates(gatesDistances)
	//fmt.Println("[findBestGate] gates normalisé : ",normalizedGates)

	bestGates := gates_with_lowest_score(normalizedGates)
	bestGate := bestGates[0]
	if len(bestGates) > 1 {
		//on choisit la porte ayant le moins de monde
		for _, gate := range bestGates {
			if gate.NbAgents < bestGate.NbAgents {
				bestGate = gate
			}
		}
	}
	//fmt.Println("[findBestGate] bestGate : ",bestGate)

	//choix de la porte aléatoire parmi les portes adjacentes
	nearGates := ag.env.getNearGateFromGate(bestGate)
	var bestGatePos alg.Coord
	if len(nearGates) > 1 {
		bestGatePos = nearGates[rand.Intn(len(nearGates))]
	} else {
		bestGatePos = nearGates[0]
	}
	return bestGatePos
}

func twocloseGate(gate1 alg.Coord, gate2 alg.Coord) bool {
	if (gate1[0] == gate2[0]) && (gate1[1] == gate2[1]+1 || gate1[1] == gate2[1]-1) {
		return true
	}
	if (gate1[1] == gate2[1]) && (gate1[0] == gate2[0]+1 || gate1[0] == gate2[0]-1) {
		return true
	}
	return false
}

// Normalise les valeurs d'un ensemble de portes
func normalizeGates(gates []Gate) ([]Gate, float64, float64) {
	var minAgents, maxAgents float64 = math.MaxFloat64, 0
	var minDistance, maxDistance float64 = math.MaxFloat64, 0

	// Trouver les valeurs max et min pour la normalisation
	for _, gate := range gates {
		if gate.NbAgents > maxAgents {
			maxAgents = gate.NbAgents
		}
		if gate.NbAgents < minAgents {
			minAgents = gate.NbAgents
		}
		if gate.Distance > maxDistance {
			maxDistance = gate.Distance
		}
		if gate.Distance < minDistance {
			minDistance = gate.Distance
		}
	}

	// Normaliser les valeurs
	d_agt := (maxAgents - minAgents)
	if d_agt == 0 {
		d_agt = 1.0
	}
	d_dist := (maxDistance - minDistance)
	if d_dist == 0 {
		d_dist = 1.0
	}
	//fmt.Println("[normalizeGates] d_dist : ",d_dist)
	for i := range gates {
		gates[i].NbAgents = (gates[i].NbAgents - minAgents) / d_agt
		//fmt.Println("[normalizeGates] gates[i].Distance : ",gates[i].Distance)
		//fmt.Println("[normalizeGates] minDistance : ",minDistance)
		//fmt.Println("[normalizeGates] d_dist : ",d_dist)
		gates[i].Distance = (gates[i].Distance - minDistance) / d_dist
	}
	return gates, float64(maxAgents - minAgents), maxDistance - minDistance
}

// Calcul du score d'un Gate
func (g Gate) Score() float64 {
	return g.Distance + g.NbAgents
}

// sort_by_score trie une tranche de Gates par ordre croissant de leur score
func sort_by_score(gates []Gate) {
	sort.Slice(gates, func(i, j int) bool {
		return gates[i].Score() < gates[j].Score() // Ordre croissant
	})
}

// gates_with_highest_score renvoie une tranche de Gates ayant le score le moins élevé
func gates_with_lowest_score(gates []Gate) []Gate {
	if len(gates) == 0 {
		return nil
	}

	sort_by_score(gates) // D'abord, on trie les gates

	lowestScore := gates[0].Score() // Le premier gate a le score le moins élevé après le tri
	var lowestScoreGates []Gate

	for _, gate := range gates {
		if gate.Score() == lowestScore {
			lowestScoreGates = append(lowestScoreGates, gate)
		} else {
			break // Puisque les gates sont triés, pas besoin de vérifier plus loin
		}
	}

	return lowestScoreGates
}