// Command flocking is Craig Reynolds' boids — the flocking sketch from
// Processing's examples and Nature of Code. Each boid steers by three local
// rules against its neighbours: separation (avoid crowding), alignment (match
// heading), cohesion (seek the group centre). Nothing coordinates the flock;
// the structure is entirely emergent. Ported to the widget.Canvas escape hatch.
//
//	go run ./examples/flocking
package main

import (
	"log"
	"math"
	"math/rand"

	"github.com/doug/gophics/app"
	"github.com/doug/gophics/geom"
	"github.com/doug/gophics/paint"
	"github.com/doug/gophics/widget"
)

const (
	numBoids  = 200
	maxSpeed  = 2.6
	maxForce  = 0.05
	sepRadius = 26
	nbrRadius = 56
	sepWeight = 1.6
	aliWeight = 1.0
	cohWeight = 0.9
)

// --- tiny vector helpers over geom.Pt (self-contained, no framework additions) ---

func vlen(p geom.Pt) float32 { return float32(math.Hypot(float64(p.X), float64(p.Y))) }
func vnorm(p geom.Pt) geom.Pt {
	l := vlen(p)
	if l == 0 {
		return geom.Pt{}
	}
	return p.Mul(1 / l)
}
func vsetlen(p geom.Pt, l float32) geom.Pt { return vnorm(p).Mul(l) }
func vlimit(p geom.Pt, max float32) geom.Pt {
	if vlen(p) > max {
		return vsetlen(p, max)
	}
	return p
}
func vdist(a, b geom.Pt) float32 { return vlen(a.Sub(b)) }
func vangle(p geom.Pt) float64   { return math.Atan2(float64(p.Y), float64(p.X)) }
func fromAngle(a float64, l float32) geom.Pt {
	return geom.Pt{X: l * float32(math.Cos(a)), Y: l * float32(math.Sin(a))}
}

type boid struct{ pos, vel, acc geom.Pt }

type Flock struct{}

func (Flock) CreateState() widget.State { return &flockState{} }

type flockState struct {
	widget.StateBase[Flock]
	boids   []*boid
	w, h    float32
	started bool
}

func (s *flockState) Init(ctx widget.Ctx) { ctx.AddTicker(s) }

func (s *flockState) seed(w, h float32) {
	s.w, s.h = w, h
	s.boids = s.boids[:0]
	for range numBoids {
		s.boids = append(s.boids, &boid{
			pos: geom.Pt{X: rand.Float32() * w, Y: rand.Float32() * h},
			vel: fromAngle(rand.Float64()*2*math.Pi, 1+rand.Float32()*(maxSpeed-1)),
		})
	}
	s.started = true
}

// steer nudges vel toward a desired direction, capped so boids turn smoothly.
func steer(desired, vel geom.Pt) geom.Pt {
	if desired == (geom.Pt{}) {
		return geom.Pt{}
	}
	return vlimit(vsetlen(desired, maxSpeed).Sub(vel), maxForce)
}

func (s *flockState) Tick(dt float64) bool {
	if !s.started {
		return true
	}
	s.SetState(func() { s.step() })
	return true
}

func (s *flockState) step() {
	for _, b := range s.boids {
		var sep, ali, coh geom.Pt
		var nSep, nNbr float32
		for _, o := range s.boids {
			if o == b {
				continue
			}
			d := vdist(b.pos, o.pos)
			if d > 0 && d < sepRadius {
				sep = sep.Add(vnorm(b.pos.Sub(o.pos)).Mul(1 / d))
				nSep++
			}
			if d > 0 && d < nbrRadius {
				ali = ali.Add(o.vel)
				coh = coh.Add(o.pos)
				nNbr++
			}
		}
		if nSep > 0 {
			b.acc = b.acc.Add(steer(sep.Mul(1/nSep), b.vel).Mul(sepWeight))
		}
		if nNbr > 0 {
			b.acc = b.acc.Add(steer(ali.Mul(1/nNbr), b.vel).Mul(aliWeight))
			b.acc = b.acc.Add(steer(coh.Mul(1/nNbr).Sub(b.pos), b.vel).Mul(cohWeight))
		}
	}
	for _, b := range s.boids {
		b.vel = vlimit(b.vel.Add(b.acc), maxSpeed)
		b.pos = wrap(b.pos.Add(b.vel), s.w, s.h)
		b.acc = geom.Pt{}
	}
}

func wrap(p geom.Pt, w, h float32) geom.Pt {
	if p.X < 0 {
		p.X += w
	} else if p.X > w {
		p.X -= w
	}
	if p.Y < 0 {
		p.Y += h
	} else if p.Y > h {
		p.Y -= h
	}
	return p
}

var bg = paint.RGB(0.043, 0.055, 0.086)

func (s *flockState) Build(widget.Ctx) widget.Widget {
	return widget.Canvas{Clip: true, Draw: func(c paint.Canvas, size geom.Size) {
		if !s.started || s.w != size.W || s.h != size.H {
			s.seed(size.W, size.H)
		}
		c.Clear(bg)
		for _, b := range s.boids {
			a := vangle(b.vel)
			col := paint.HSV(wrapf(float32(a*180/math.Pi), 0, 360), 0.55, 1)
			// an arrowhead pointing along the velocity
			p := paint.NewPath()
			p.MoveTo(tri(b.pos, a, 7, 0)).LineTo(tri(b.pos, a, -4, 3)).LineTo(tri(b.pos, a, -4, -3))
			c.FillPath(p.Close(), col)
		}
	}}
}

// tri rotates a local (lx,ly) by angle a and offsets it to pos.
func tri(pos geom.Pt, a float64, lx, ly float32) geom.Pt {
	ca, sa := float32(math.Cos(a)), float32(math.Sin(a))
	return geom.Pt{X: pos.X + lx*ca - ly*sa, Y: pos.Y + lx*sa + ly*ca}
}

func wrapf(v, lo, hi float32) float32 {
	r := hi - lo
	for v < lo {
		v += r
	}
	for v >= hi {
		v -= r
	}
	return v
}

// hsv converts H∈[0,360), S,V∈[0,1] to a paint.Color.

func main() {
	if err := app.Run(Flock{}, app.Config{
		Title:      "gophics flocking",
		Size:       geom.Size{W: 900, H: 640},
		Background: bg,
	}); err != nil {
		log.Fatal(err)
	}
}