// Copyright ©2017 The Gonum Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package topo

import (
	"gonum.org/v1/gonum/graph"
	"gonum.org/v1/gonum/graph/internal/linear"
	"gonum.org/v1/gonum/graph/internal/set"
)

// UndirectedCyclesIn returns a set of cycles that forms a cycle basis in the graph g.
// Any cycle in g can be constructed as a symmetric difference of its elements.
func UndirectedCyclesIn(g graph.Undirected) [][]graph.Node {
	// From "An algorithm for finding a fundamental set of cycles of a graph"
	// https://doi.org/10.1145/363219.363232

	var cycles [][]graph.Node
	done := make(set.Int64s)
	var tree linear.NodeStack
	for _, n := range g.Nodes() {
		id := n.ID()
		if done.Has(id) {
			continue
		}
		done.Add(id)

		tree = tree[:0]
		tree.Push(n)
		from := sets{id: set.Int64s{}}
		to := map[int64]graph.Node{id: n}

		for tree.Len() != 0 {
			u := tree.Pop()
			uid := u.ID()
			adj := from[uid]
			for _, v := range g.From(uid) {
				vid := v.ID()
				switch {
				case uid == vid:
					cycles = append(cycles, []graph.Node{u})
				case !from.has(vid):
					done.Add(vid)
					to[vid] = u
					tree.Push(v)
					from.add(uid, vid)
				case !adj.Has(vid):
					c := []graph.Node{v, u}
					adj := from[vid]
					p := to[uid]
					for !adj.Has(p.ID()) {
						c = append(c, p)
						p = to[p.ID()]
					}
					c = append(c, p, c[0])
					cycles = append(cycles, c)
					adj.Add(uid)
				}
			}
		}
	}

	return cycles
}

type sets map[int64]set.Int64s

func (s sets) add(uid, vid int64) {
	e, ok := s[vid]
	if !ok {
		e = make(set.Int64s)
		s[vid] = e
	}
	e.Add(uid)
}

func (s sets) has(uid int64) bool {
	_, ok := s[uid]
	return ok
}
