package main

import (
	"fmt"
	"image"
	"image/color"
	"log"
	"math"
	"math/rand"
	"os"
	"strings"
	"time"

	"gioui.org/app"
	"gioui.org/f32"
	"gioui.org/font/gofont"
	"gioui.org/io/key"
	"gioui.org/layout"
	"gioui.org/op"
	"gioui.org/op/clip"
	"gioui.org/op/paint"
	"gioui.org/text"
	"gioui.org/unit"
	"gioui.org/widget/material"
)

const MAX_LOOP = 100_000_000

type C = layout.Context
type D = layout.Dimensions

var darkPalette = material.Palette{
	Bg:         color.NRGBA{R: 0x28, G: 0x28, B: 0x28, A: 0xff},
	Fg:         color.NRGBA{R: 0xeb, G: 0xdb, B: 0xb2, A: 0xff},
	ContrastBg: color.NRGBA{R: 0x48, G: 0x85, B: 0x88, A: 0xff},
	ContrastFg: color.NRGBA{R: 0xeb, G: 0xdb, B: 0xb2, A: 0xff},
}

func main() {
	go func() {
		window := new(app.Window)
		window.Option(app.Title("Not-a-graph-layout-algorithm graph layout algorithm"))

		err := run(window)
		if err != nil {
			log.Fatal(err)
		}
		os.Exit(0)
	}()

	app.Main()
}

func run(window *app.Window) error {
	theme := material.NewTheme()
	theme.Palette = darkPalette
	theme.Shaper =
		text.NewShaper(text.WithCollection(gofont.Collection()))

	seed := int64(0)
	g := Graph{}
	g.random = rand.New(rand.NewSource(seed))
	g.offsets = g._offsets[:]
	g.degrees = g._degrees[:]
	g.nodes = g._nodes[:0]
	g.allocs = g._allocs[:]
	g.edges = g._edges[:0]
	g.adjacency = g._adjacency[:0]

	title := material.Body1(theme, "")
	title.Color = theme.Fg
	title.Alignment = text.Start
	title.WrapPolicy = text.WrapWords

	simPeriod := 50 * time.Millisecond
	lastStep := time.Now()
	bumpShift := 1
	lastSecond := time.Now()
	frameCnt, stepCnt := 0, 0
	fps, sps := 0, 0
	simFinishedTime := time.Now()
	simCnt := int64(0)
	var (
		stepOutput      strings.Builder
		statusText      strings.Builder
		ops             op.Ops
		screensaverMode bool = true
		finished,
		resetRequested,
		bumpRequested bool
	)
	for {
		switch e := window.Event().(type) {
		case app.DestroyEvent:
			return e.Err

		case app.FrameEvent:
			gtx := app.NewContext(&ops, e)

			// Handle input
			for {
				event, ok := e.Source.Event(
					key.Filter{Name: "B"},
					key.Filter{Name: "R"},
					key.Filter{Name: "S", Required: key.ModShift},
					key.Filter{Name: "<", Required: key.ModShift},
					key.Filter{Name: ">", Required: key.ModShift},
				)
				if !ok {
					break
				}
				switch evt := event.(type) {
				case key.Event:
					switch evt.Name {
					case "B":
						if evt.State == key.Press {
							bumpRequested = true
						}
					case "R":
						if evt.State == key.Press {
							resetRequested = true
						}
					case "S":
						if evt.State == key.Press {
							screensaverMode = !screensaverMode
						}
					case "<":
						if evt.State == key.Press {
							simPeriod += 50 * time.Millisecond
							simPeriod = min(simPeriod, 10*time.Second)
						}
					case ">":
						if evt.State == key.Press {
							simPeriod -= 50 * time.Millisecond
							simPeriod = max(simPeriod, 0)
						}
					}
				}
			}

			if resetRequested ||
				(screensaverMode && finished &&
					time.Since(simFinishedTime) >= 5*time.Second) {
				(&g).Reset()
				resetRequested = false
				finished = false
				simCnt++
			}

			if time.Since(lastStep) >= simPeriod {
				if int(g.order) == cap(g.nodes) && bumpRequested {
					bump(&g, bumpShift)
					//bumpShift++
					bumpRequested = false
				}
				stepOutput.Reset()
				stepOutput.WriteString((&g).Step())
				//statusText.WriteString((&g).Semilattice())
				lastStep = time.Now()

				addNodeAtRandom(&g)
				stepCnt++
			}

			paint.Fill(&ops, theme.Bg)

			layout.UniformInset(
				unit.Dp(10),
			).Layout(gtx, func(gtx C) D {
				return layout.Flex{
					Axis:      layout.Horizontal,
					Alignment: layout.Start,
					Spacing:   layout.SpaceEnd,
				}.Layout(gtx,
					layout.Rigid(func(gtx C) D {
						return (&g).Layout(gtx, theme)
					}),
					layout.Rigid(func(gtx C) D {
						return layout.Flex{
							Axis:      layout.Vertical,
							Alignment: layout.Start,
							Spacing:   layout.SpaceEnd,
						}.Layout(gtx,
							layout.Rigid(func(gtx C) D {
								return (&g).LayoutDist(gtx, theme)
							}),
							layout.Flexed(1.0, func(gtx C) D {
								return title.Layout(gtx)
							}),
						)
					}),
				)
			})

			if time.Since(lastSecond) >= time.Second {
				fps, sps = frameCnt, stepCnt
				frameCnt, stepCnt = 0, 0
				lastSecond = time.Now()
			}

			statusText.Reset()
			statusText.WriteString(fmt.Sprintf("seed %d, simulation %d\n", seed, simCnt+1))
			statusText.WriteString(fmt.Sprintf("simulation rate: %s per step\n", simPeriod))
			statusText.WriteString(fmt.Sprintf("fps=%d; sps=%d\n", fps, sps))

			statusText.WriteString(stepOutput.String())
			title.Text = statusText.String()

			if int(g.order) == cap(g.nodes) && !finished {
				finished = true
				simFinishedTime = time.Now()
			}
			e.Source.Execute(op.InvalidateCmd{})

			e.Frame(gtx.Ops)
			frameCnt++
		}
	}
}

func bump(g *Graph, shift int) int {
	root := -1
	for n := 0; n < int(g.order); n++ {
		if g.nodes[n] == 1 {
			root = n
			break
		}
	}
	if root < 0 {
		return root
	}
	g.nodes[root] <<= shift
	return root

	nodes := make([]NodeId, 1, g.order)
	nextNodes := make(map[NodeId]struct{}, g.order)
	nodes[0] = NodeId(root)

	for len(nodes) > 0 {
		for _, n := range nodes {
			for _, e := range g.edges[g.offsets[n]:g.offsets[n+1]] {
				nextNodes[e] = struct{}{}
			}
		}
		nodes = nodes[:len(nextNodes)]
		i := 0
		for n := range nextNodes {
			delete(nextNodes, n)
			nodes[i] = n
			i++
			if log2(g.nodes[n])+uint32(shift) >= 30 {
				log.Printf("Out-of-address-space condition for node %d", n)
				continue
			}
			g.nodes[n] <<= shift
		}
	}
	return root
}

func addNodeAtRandom(g *Graph) (order uint32) {
	if g.order == uint32(cap(g.nodes)) ||
		g.size == uint32(cap(g.edges)) {
		return g.order
	}

	/*threshold := 0.05 * float64(g.order)
	if g.random.ExpFloat64() <= threshold {
		return g.order
	}*/
	g.edges = g.edges[:cap(g.edges)]
	g.nodes = g.nodes[:cap(g.nodes)]

	i := int(g.order)
	g.nodes[i] = 1
	g.offsets[i] = EdgeId(g.size)

	for j := i + 1; j < len(g.nodes); j++ {
		g.edges[g.size] = NodeId(j)

		distance := float64(j - i)
		edgeProbability := 1 / (1 + math.Exp(0.25*distance))

		if g.random.Float64() > edgeProbability {
			continue
		}
		g.degrees[i]++
		g.size++
	}
	g.order++
	g.offsets[g.order] = EdgeId(g.size)
	g.nodes = g.nodes[:g.order]
	g.edges = g.edges[:g.size]

	return g.order
}

type NodeId uint32
type EdgeId uint32

const GraphOrder int = 128

type Graph struct {
	_adjacency [GraphOrder * (GraphOrder - 1)]NodeId
	_edges     [(GraphOrder * (GraphOrder - 1)) >> 1]NodeId
	_nodes     [GraphOrder]uint32
	_allocs    [GraphOrder]uint32
	_offsets   [GraphOrder + 1]EdgeId
	_degrees   [GraphOrder]byte

	random *rand.Rand

	adjacency []NodeId // Bidirectional adjacent node ids
	edges     []NodeId // Unidirectional adjacent node ids
	nodes     []uint32
	allocs    []uint32 // Address allocation state, per node
	offsets   []EdgeId
	degrees   []byte // Node degrees

	size   uint32 // Number of edges added to graph
	order  uint32 // Number of nodes added to graph
	source NodeId // A selected source node
	target NodeId // A selected target node
}

func (g *Graph) Reset() {
	for i := range g.offsets {
		g.offsets[i] = 0
	}
	for i := range g.nodes {
		g.nodes[i] = 0
	}
	for i := range g.allocs {
		g.allocs[i] = 0
	}
	for i := range g.edges {
		g.edges[i] = 0
	}
	g.nodes = g.nodes[:0]
	g.edges = g.edges[:0]
	g.order, g.size = 0, 0
}

func (g *Graph) Semilattice() string {
	var b strings.Builder
	for n := 0; n < len(g.nodes); n++ {
		b.WriteString(fmt.Sprintf("nodeAddr=%d neighbors=", g.nodes[n]))

		for _, e := range g.edges[g.offsets[n]:g.offsets[n+1]] {
			if uint32(e) >= g.order {
				continue
			}
			b.WriteString(fmt.Sprintf("%d, ", g.nodes[e]))
		}
		b.WriteString("\n")
	}
	return b.String()
}

func (g *Graph) Step() string {
	for n := range g.nodes {
		if g.allocs[n] == 0 {
			g.allocs[n] = 1 // Seed allocator
		}
		shift := log2(g.nodes[n]) + 1

		if log2(g.allocs[n])+shift >= 30 {
			log.Printf("Out-of-address-space condition for node %d", n)
			continue
		}
		edges := g.edges[g.offsets[n]:g.offsets[n+1]]

		shiftStart := 1
		// Shift a node containing exactly one 1
		if (n == 0 || g.nodes[n] > 1) && // root or assigned
			g.nodes[n]&(g.nodes[n]-1) == 0 { // ...contains one 1

			for i := 0; i < min(shiftStart, len(edges)); i++ {
				e := edges[i]
				if uint32(e) < g.order && g.nodes[n] >= g.nodes[e] {
					g.nodes[e] = g.nodes[n] << (i + 1)
				}
			}
		}
		// Assign addresses to neighbors
		for i := shiftStart; i < len(edges); i++ {
			nextAlloc := (g.allocs[n] << shift) | g.nodes[n]
			e := edges[i]
			if uint32(e) >= g.order ||
				(g.nodes[e] != 1 && nextAlloc >= g.nodes[e]) {
				continue
			}
			g.nodes[e] = nextAlloc
			g.allocs[n]++
		}
	}
	return fmt.Sprintf("nodes=%v\nedges=%v", g.nodes, g.edges)
}

func (g *Graph) Layout(
	gtx layout.Context,
	th *material.Theme,
) D {
	squareMax := min(
		gtx.Constraints.Max.X,
		gtx.Constraints.Max.Y,
	)
	convFactor := float32(squareMax) / 65535.0
	circle := clip.Ellipse{Max: image.Pt(16, 16)}

	// Outer margin
	defer op.Offset(image.Pt(10, 10)).Push(gtx.Ops).Pop()

	var path clip.Path
	for n := 0; n < len(g.nodes); n++ {
		x1, y1 := idToPt(g.nodes[n], convFactor)

		for e := range g.edges[g.offsets[n]:g.offsets[n+1]] {
			if uint32(e) >= g.order {
				continue
			}
			if g.nodes[n] == 1 { // Skip orphans and roots
				continue
			}
			color := th.Fg
			if distance(g.nodes[n], g.nodes[e]) > 4 {
				color = th.ContrastBg
				color.A = 0xd0
			}
			x2, y2 := idToPt(g.nodes[e], convFactor)

			path.Begin(gtx.Ops)
			path.MoveTo(f32.Pt(float32(x1), float32(y1)))
			path.LineTo(f32.Pt(float32(x2), float32(y2)))
			paint.FillShape(gtx.Ops, color,
				clip.Stroke{
					Path:  path.End(),
					Width: float32(unit.Dp(1)),
				}.Op(),
			)
		}
		// Circle size is proportional to address length.
		length := log2(g.nodes[n])
		shift := int(length >> 3)
		scaled := circle
		scaled.Max.X = scaled.Max.X >> shift
		scaled.Max.Y = scaled.Max.Y >> shift
		halfW, halfH := scaled.Max.X>>1, scaled.Max.Y>>1

		pos := image.Pt(gtx.Dp(x1)-halfW, gtx.Dp(y1)-halfH)

		macro := op.Record(gtx.Ops)
		stack := op.Offset(pos).Push(gtx.Ops)
		nodeColor := th.Fg
		if length >= 24 {
			nodeColor = color.NRGBA{R: 0xaa, G: 0, B: 0xaa, A: 0xff}
		}
		paint.FillShape(gtx.Ops, nodeColor, scaled.Op(gtx.Ops))
		stack.Pop()
		c := macro.Stop()
		op.Defer(gtx.Ops, c)
	}
	return layout.Dimensions{Size: image.Pt(squareMax, squareMax)}
}

func (g *Graph) LayoutDist(
	gtx layout.Context,
	th *material.Theme,
) D {
	dims := f32.Pt(400, 100)
	width := gtx.Dp(unit.Dp(dims.X))
	height := gtx.Dp(unit.Dp(dims.Y))

	defer op.Affine( // Flip y-axis and scale
		f32.Affine2D{}.
			Scale(f32.Pt(dims.X/2, dims.Y/2), f32.Pt(1.0, -1.0)),
	).Push(gtx.Ops).Pop()

	barWidth := 0
	if g.order > 0 {
		pxPerBucket := float32(width) / float32(g.order)
		barWidth = gtx.Dp(unit.Dp(pxPerBucket))
	}
	color := color.NRGBA{R: 0xaa, G: 0, B: 0xaa, A: 0xff}

	maxOutDegree := 0
	for n := range g.nodes {
		d := g.offsets[n+1] - g.offsets[n]
		maxOutDegree = max(maxOutDegree, int(d))
	}
	convFactor := float64(height) / float64(maxOutDegree)

	for n := range g.nodes {
		cnt := g.offsets[n+1] - g.offsets[n]
		y := gtx.Dp(unit.Dp(float64(cnt) * convFactor))
		stack := clip.Rect{
			Min: image.Pt(n*barWidth, 0),
			Max: image.Pt((n+1)*barWidth, y),
		}.Push(gtx.Ops)

		paint.ColorOp{Color: color}.Add(gtx.Ops)
		paint.PaintOp{}.Add(gtx.Ops)
		stack.Pop()
	}
	return layout.Dimensions{Size: image.Pt(width, height)}
}

func distance(a, b uint32) uint32 {
	var s, e uint32 = 1, 2
	for s < 32 && a&(e-1) == b&(e-1) {
		s++
		e <<= 1
	}
	s -= 1
	return ones(a>>s) + ones(b>>s)
}

func ones(x uint32) uint32 {
	var c uint32
	for c = 0; x != 0; c++ {
		x &= x - 1 // clear the least significant bit set
	}
	return c
}

func idToPt(id uint32, convFactor float32) (x, y unit.Dp) {
	x0, y0 := demorton(reverse32(id))

	x, y = unit.Dp(float32(x0)*convFactor),
		unit.Dp(float32(y0)*convFactor)

	return
}

func log2(x uint32) uint32 {
	b := []uint32{0x2, 0xC, 0xF0, 0xFF00, 0xFFFF0000}
	s := []uint32{1, 2, 4, 8, 16}

	var r uint32 = 0

	for i := 4; i >= 0; i-- {
		if x&b[i] != 0 {
			x >>= s[i]
			r |= s[i]
		}
	}
	return r
}

func reverse32(x uint32) uint32 {
	var rev uint32
	var mask uint32 = 0xff

	for i := 0; i < 4; i++ {
		rshift := 8 * i
		lshift := 8 * (3 - i)
		rev |= uint32(reverse((x&mask)>>rshift)) << lshift
		mask <<= 8
	}
	return rev
}

func reverse(b uint32) byte {
	b = (b * 0x0802 & 0x22110) | (b * 0x8020 & 0x88440)
	b *= 0x10101
	b >>= 16
	return byte(b)
}

func demorton(z uint32) (x, y uint16) {
	res := (uint64(z) | (uint64(z) << 31)) & 0x5555555555555555
	res = (res | (res >> 1)) & 0x3333333333333333
	res = (res | (res >> 2)) & 0x0f0f0f0f0f0f0f0f
	res = (res | (res >> 4)) & 0x00ff00ff00ff00ff
	res = res | (res >> 8)
	x = uint16(res)
	y = uint16(res >> 32)
	return
}

func morton(x, y uint16) uint32 {
	var res uint64

	res = uint64(x) | (uint64(y) << 32)
	res = (res | (res << 8)) & 0x00ff00ff00ff00ff
	res = (res | (res << 4)) & 0x0f0f0f0f0f0f0f0f
	res = (res | (res << 2)) & 0x3333333333333333
	res = (res | (res << 1)) & 0x5555555555555555

	return uint32(res | (res >> 31))
}
