// Copyright ©2019 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 layout

import (
	"math"
	"math/rand/v2"

	"gonum.org/v1/gonum/graph"
	"gonum.org/v1/gonum/spatial/barneshut"
	"gonum.org/v1/gonum/spatial/r2"
)

// EadesR2 implements the graph layout algorithm essentially as
// described in "A heuristic for graph drawing", Congressus
// numerantium 42:149-160.
// The implementation here uses the Barnes-Hut approximation for
// global repulsion calculation, and edge weights are considered
// when calculating adjacent node attraction.
type EadesR2 struct {
	// Updates is the number of updates to perform.
	Updates int

	// Repulsion is the strength of the global
	// repulsive force between nodes in the
	// layout. It corresponds to C3 in the paper.
	Repulsion float64

	// Rate is the gradient descent rate. It
	// corresponds to C4 in the paper.
	Rate float64

	// Theta is the Barnes-Hut theta constant.
	Theta float64

	// Src is the source of randomness used
	// to initialize the nodes' locations. If
	// Src is nil, the global random number
	// generator is used.
	Src rand.Source

	nodes   graph.Nodes
	indexOf map[int64]int

	particles []barneshut.Particle2
	forces    []r2.Vec
}

// Update is the EadesR2 spatial graph update function.
func (u *EadesR2) Update(g graph.Graph, layout LayoutR2) bool {
	if u.Updates <= 0 {
		return false
	}
	u.Updates--

	if !layout.IsInitialized() {
		var rnd func() float64
		if u.Src == nil {
			rnd = rand.Float64
		} else {
			rnd = rand.New(u.Src).Float64
		}
		u.nodes = g.Nodes()
		u.indexOf = make(map[int64]int, u.nodes.Len())
		if u.nodes.Len() >= 0 {
			u.particles = make([]barneshut.Particle2, 0, u.nodes.Len())
		}
		for u.nodes.Next() {
			id := u.nodes.Node().ID()
			u.indexOf[id] = len(u.particles)
			u.particles = append(u.particles, eadesR2Node{id: id, pos: r2.Vec{X: rnd(), Y: rnd()}})
		}
		u.forces = make([]r2.Vec, len(u.particles))
	}
	u.nodes.Reset()

	// Apply global repulsion.
	plane, err := barneshut.NewPlane(u.particles)
	if err != nil {
		return false
	}
	var updated bool
	for i, p := range u.particles {
		f := r2.Scale(-u.Repulsion, plane.ForceOn(p, u.Theta, barneshut.Gravity2))
		// Prevent marginal updates that can be caused by
		// floating point error when nodes are very far apart.
		if math.Hypot(f.X, f.Y) > 1e-12 {
			updated = true
		}
		u.forces[i] = f
	}

	// Handle edge weighting for attraction.
	var weight func(uid, vid int64) float64
	if wg, ok := g.(graph.Weighted); ok {
		if _, ok := g.(graph.Directed); ok {
			weight = func(xid, yid int64) float64 {
				var w float64
				f, ok := wg.Weight(xid, yid)
				if ok {
					w += f
				}
				r, ok := wg.Weight(yid, xid)
				if ok {
					w += r
				}
				return w
			}
		} else {
			weight = func(xid, yid int64) float64 {
				w, ok := wg.Weight(xid, yid)
				if ok {
					return w
				}
				return 0
			}
		}
	} else {
		// This is only called when the adjacency is known so just return unit.
		weight = func(_, _ int64) float64 { return 1 }
	}

	seen := make(map[[2]int64]bool)
	for u.nodes.Next() {
		xid := u.nodes.Node().ID()
		xidx := u.indexOf[xid]
		to := g.From(xid)
		for to.Next() {
			yid := to.Node().ID()
			if seen[[2]int64{xid, yid}] {
				continue
			}
			seen[[2]int64{yid, xid}] = true
			yidx := u.indexOf[yid]

			// Apply adjacent node attraction.
			v := r2.Sub(u.particles[yidx].Coord2(), u.particles[xidx].Coord2())
			f := r2.Scale(weight(xid, yid)*math.Log(math.Hypot(v.X, v.Y)), v)
			if math.IsInf(f.X, 0) || math.IsInf(f.Y, 0) {
				return false
			}
			if math.Hypot(f.X, f.Y) > 1e-12 {
				updated = true
			}
			u.forces[xidx] = r2.Add(u.forces[xidx], f)
			u.forces[yidx] = r2.Sub(u.forces[yidx], f)
		}
	}

	if !updated {
		return false
	}

	rate := u.Rate
	if rate == 0 {
		rate = 0.1
	}
	for i, f := range u.forces {
		n := u.particles[i].(eadesR2Node)
		n.pos = r2.Add(n.pos, r2.Scale(rate, f))
		u.particles[i] = n
		layout.SetCoord2(n.id, n.pos)
	}
	return true
}

type eadesR2Node struct {
	id  int64
	pos r2.Vec
}

func (p eadesR2Node) Coord2() r2.Vec { return p.pos }
func (p eadesR2Node) Mass() float64  { return 1 }
