package dijkstra

// Topology represents a network topology
type Topology struct {
	nodes map[Node]int64
	edges map[Node]map[Node]int64
}

// Node represents a node in a graph
type Node struct {
	Name string
}

// Edge represents a directed edge in a graph
type Edge struct {
	NodeA    Node
	NodeB    Node
	Distance int64
}

// SPT represents a shortest path tree
type SPT map[Node]Path

// Path represents a path through a graph
type Path struct {
	Edges    []Edge
	Distance int64
}

// NewTopology creates a new topology
func NewTopology(nodes []Node, edges []Edge) *Topology {
	t := &Topology{
		nodes: make(map[Node]int64),
		edges: make(map[Node]map[Node]int64),
	}

	for _, n := range nodes {
		t.nodes[n] = -1
	}

	for _, e := range edges {
		if _, ok := t.edges[e.NodeA]; !ok {
			t.edges[e.NodeA] = make(map[Node]int64)
		}

		t.edges[e.NodeA][e.NodeB] = e.Distance
	}

	return t
}

func (t *Topology) newSPT() SPT {
	spt := make(SPT)

	for n := range t.nodes {
		spt[n] = Path{
			Edges:    make([]Edge, 0),
			Distance: -1,
		}
	}

	return spt
}

// SPT calculates the shortest path tree
func (t *Topology) SPT(from Node) SPT {
	spt := t.newSPT()

	tmp := spt[from]
	tmp.Distance = 0
	spt[from] = tmp

	unmarked := make(map[Node]struct{})
	for n := range t.nodes {
		if n == from {
			continue
		}
		unmarked[n] = struct{}{}
	}

	for len(unmarked) > 0 {
		for neighbor, distance := range t.edges[from] {
			if spt[neighbor].Distance == -1 {
				tmp := spt[neighbor]
				tmp.Distance = spt[from].Distance + distance
				tmp.Edges = make([]Edge, len(spt[from].Edges)+1)
				copy(tmp.Edges, spt[from].Edges)
				tmp.Edges[len(spt[from].Edges)] = Edge{
					NodeA:    from,
					NodeB:    neighbor,
					Distance: distance,
				}
				spt[neighbor] = tmp
				continue
			}

			if spt[from].Distance+distance < spt[neighbor].Distance {
				tmp := spt[neighbor]
				tmp.Distance = spt[from].Distance + distance
				tmp.Edges = make([]Edge, len(spt[from].Edges)+1)
				copy(tmp.Edges, spt[from].Edges)
				tmp.Edges[len(spt[from].Edges)] = Edge{
					NodeA:    from,
					NodeB:    neighbor,
					Distance: distance,
				}
				spt[neighbor] = tmp
				continue
			}
		}

		var next *Node
		nextDistance := int64(0)
		for candidate := range unmarked {
			if spt[candidate].Distance == -1 {
				continue
			}

			if next == nil {
				tmp := candidate
				next = &tmp
				nextDistance = spt[candidate].Distance
				continue
			}

			if spt[candidate].Distance < nextDistance {
				tmp := candidate
				next = &tmp
				nextDistance = spt[candidate].Distance
				continue
			}
		}

		from = *next
		delete(unmarked, from)
	}

	return spt
}
