package block

import (
	"slices"

	"github.com/df-mc/dragonfly/server/block/cube"
	"github.com/df-mc/dragonfly/server/block/model"
	"github.com/df-mc/dragonfly/server/event"
	"github.com/df-mc/dragonfly/server/world"
)

// wireNetwork implements a minimally-invasive bolt-on accelerator that performs a breadth-first search through redstone
// wires in order to more efficiently and compute new redstone wire power levels and determine the order in which other
// blocks should be updated. This implementation is heavily based off of RedstoneWireTurbo and MCHPRS.
type wireNetwork struct {
	nodes            []*wireNode
	nodeCache        map[cube.Pos]*wireNode
	updateQueue      [3][]*wireNode
	currentWalkLayer uint32
}

// wireNode is a data structure to keep track of redstone wires and neighbours that will receive updates.
type wireNode struct {
	visited bool

	pos    cube.Pos
	block  world.Block
	source cube.Pos

	neighbours []*wireNode
	oriented   bool

	xBias int32
	zBias int32

	layer uint32
}

const (
	wireHeadingNorth = 0
	wireHeadingEast  = 1
	wireHeadingSouth = 2
	wireHeadingWest  = 3
)

// updateStrongRedstone sets off the breadth-first walk through all redstone wires connected to the initial position
// triggered. This is the main entry point for the redstone update algorithm.
func updateStrongRedstone(pos cube.Pos, tx *world.Tx) {
	n := &wireNetwork{
		nodeCache: make(map[cube.Pos]*wireNode),
	}

	root := &wireNode{
		block:   tx.Block(pos),
		pos:     pos,
		visited: true,
	}
	n.nodeCache[pos] = root
	n.nodes = append(n.nodes, root)

	n.propagateChanges(tx, root, 0)
	n.breadthFirstWalk(tx)
}

// updateAroundRedstone updates redstone components around the given center position. It will also ignore any faces
// provided within the ignoredFaces parameter. This implementation is based off of RedstoneCircuit and Java 1.19.
func updateAroundRedstone(centre cube.Pos, tx *world.Tx, ignoredFaces ...cube.Face) {
	// Order matches Java 1.19's RedstoneCircuit traversal.
	for _, face := range []cube.Face{
		cube.FaceWest,
		cube.FaceEast,
		cube.FaceDown,
		cube.FaceUp,
		cube.FaceNorth,
		cube.FaceSouth,
	} {
		if slices.Contains(ignoredFaces, face) {
			continue
		}
		pos := centre.Side(face)
		updateRedstoneFrom(pos, centre, tx)
		updateRedstoneFrom(pos.Side(cube.FaceUp), centre, tx)
		updateRedstoneFrom(pos.Side(cube.FaceDown), centre, tx)
		updateReceiversAroundPoweredBlock(pos, tx, face.Opposite())
	}
}

// updateReceiversAroundPoweredBlock updates redstone receivers directly adjacent to an indirectly powered solid block.
// This keeps mechanisms behind the powered block in sync without walking corner positions around the original update.
// For example, this covers the following vertical path: torch -> stone -> note block.
func updateReceiversAroundPoweredBlock(pos cube.Pos, tx *world.Tx, ignoredFaces ...cube.Face) {
	if _, ok := tx.Block(pos).Model().(model.Solid); !ok {
		return
	}
	for _, face := range cube.Faces() {
		if slices.Contains(ignoredFaces, face) || tx.RedstonePower(pos, face, true) == 0 {
			continue
		}
		updateRedstoneFrom(pos.Side(face), pos, tx)
	}
}

// updateRedstone dispatches a cancellable redstone update to the block at pos, if it handles redstone updates.
// Prefer updateRedstone over calling RedstoneUpdate directly so HandleRedstoneUpdate gets a chance to observe
// and optionally cancel the update. Direct RedstoneUpdate calls are reserved for internal state initialisation
// (e.g. seeding a freshly placed block) where handler cancellation isn't appropriate.
func updateRedstone(pos cube.Pos, tx *world.Tx) {
	updateRedstoneFrom(pos, pos, tx)
}

// updateRedstoneFrom dispatches a cancellable redstone update and records the position that caused it.
func updateRedstoneFrom(pos, source cube.Pos, tx *world.Tx) {
	r, ok := tx.Block(pos).(world.RedstoneUpdater)
	if !ok {
		return
	}
	if redstoneUpdateCancelled(pos, tx) {
		return
	}
	tx.Redstone().WithUpdateSource(source, func() {
		r.RedstoneUpdate(pos, tx)
	})
}

// redstoneUpdateCancelled checks if the redstone update has been cancelled by the HandleRedstoneUpdate handler.
func redstoneUpdateCancelled(pos cube.Pos, tx *world.Tx) bool {
	ctx := event.C(tx)
	tx.World().Handler().HandleRedstoneUpdate(ctx, pos)
	return ctx.Cancelled()
}

// updateDirectionalRedstone updates redstone components through the given face. This implementation is based off of
// RedstoneCircuit and Java 1.19.
func updateDirectionalRedstone(pos cube.Pos, tx *world.Tx, face cube.Face) {
	updateAroundRedstone(pos, tx)
	updateAroundRedstone(pos.Side(face), tx, face.Opposite())
}

// identifyNeighbours identifies the neighbouring positions of a given node, determines their types, and links them into
// the graph. After that, based on what nodes in the graph have been visited, the neighbours are reordered left-to-right
// relative to the direction of information flow.
func (n *wireNetwork) identifyNeighbours(tx *world.Tx, node *wireNode) {
	var neighboursVisited [24]bool
	var neighbourNodes [24]*wireNode
	for i, offset := range redstoneNeighbourOffsets {
		neighbourPos := node.pos.Add(offset)
		neighbour, ok := n.nodeCache[neighbourPos]
		if !ok {
			neighbour = &wireNode{
				pos:   neighbourPos,
				block: tx.Block(neighbourPos),
			}
			n.nodeCache[neighbourPos] = neighbour
			n.nodes = append(n.nodes, neighbour)
		}
		neighbourNodes[i] = neighbour
		neighboursVisited[i] = neighbour.visited
	}

	fromWest := neighboursVisited[0] || neighboursVisited[7] || neighboursVisited[8]
	fromEast := neighboursVisited[1] || neighboursVisited[12] || neighboursVisited[13]
	fromNorth := neighboursVisited[4] || neighboursVisited[17] || neighboursVisited[20]
	fromSouth := neighboursVisited[5] || neighboursVisited[18] || neighboursVisited[21]

	var cX, cZ int32
	if fromWest {
		cX++
	}
	if fromEast {
		cX--
	}
	if fromNorth {
		cZ++
	}
	if fromSouth {
		cZ--
	}

	var heading uint32
	if cX == 0 && cZ == 0 {
		heading = computeRedstoneHeading(node.xBias, node.zBias)
		for _, neighbourNode := range neighbourNodes {
			neighbourNode.xBias = node.xBias
			neighbourNode.zBias = node.zBias
		}
	} else {
		if cX != 0 && cZ != 0 {
			if node.xBias != 0 {
				cZ = 0
			}
			if node.zBias != 0 {
				cX = 0
			}
		}
		heading = computeRedstoneHeading(cX, cZ)
		for _, neighbourNode := range neighbourNodes {
			neighbourNode.xBias = cX
			neighbourNode.zBias = cZ
		}
	}

	n.orientNeighbours(neighbourNodes, node, heading)
}

// redstoneNeighbourOffsets lists the 24 positions visited around a redstone wire node: the 6 immediate neighbours
// followed by the unique neighbours-of-neighbours, in the order west, east, down, up, north, south. The fixed
// indices here are referenced directly by identifyNeighbours and the redstoneReordering tables below.
var redstoneNeighbourOffsets = [...]cube.Pos{
	// Immediate neighbours, in the order of west, east, down, up, north, and south.
	{-1, 0, 0},
	{1, 0, 0},
	{0, -1, 0},
	{0, 1, 0},
	{0, 0, -1},
	{0, 0, 1},

	// Neighbours of neighbours, in the same order, except that duplicates are omitted.
	{-2, 0, 0},
	{-1, -1, 0},
	{-1, 1, 0},
	{-1, 0, -1},
	{-1, 0, 1},

	{2, 0, 0},
	{1, -1, 0},
	{1, 1, 0},
	{1, 0, -1},
	{1, 0, 1},

	{0, -2, 0},
	{0, -1, -1},
	{0, -1, 1},

	{0, 2, 0},
	{0, 1, -1},
	{0, 1, 1},

	{0, 0, -2},
	{0, 0, 2},
}

// redstoneReordering contains lookup tables that completely remap neighbour positions into a left-to-right ordering,
// based on the cardinal direction that is determined to be forward.
var redstoneReordering = [...][24]uint32{
	{2, 3, 16, 19, 0, 4, 1, 5, 7, 8, 17, 20, 12, 13, 18, 21, 6, 9, 22, 14, 11, 10, 23, 15},
	{2, 3, 16, 19, 4, 1, 5, 0, 17, 20, 12, 13, 18, 21, 7, 8, 22, 14, 11, 15, 23, 9, 6, 10},
	{2, 3, 16, 19, 1, 5, 0, 4, 12, 13, 18, 21, 7, 8, 17, 20, 11, 15, 23, 10, 6, 14, 22, 9},
	{2, 3, 16, 19, 5, 0, 4, 1, 18, 21, 7, 8, 17, 20, 12, 13, 23, 10, 6, 9, 22, 15, 11, 14},
}

// orientNeighbours reorders the neighbours of a node based on the direction that is determined to be forward.
func (n *wireNetwork) orientNeighbours(src [24]*wireNode, dst *wireNode, heading uint32) {
	dst.oriented = true
	dst.neighbours = make([]*wireNode, 0, 24)
	for _, i := range redstoneReordering[heading] {
		dst.neighbours = append(dst.neighbours, src[i])
	}
}

// propagateChanges propagates changes for any redstone wire in layer N, informing the neighbours to recompute their
// states in layers N + 1 and N + 2.
func (n *wireNetwork) propagateChanges(tx *world.Tx, node *wireNode, layer uint32) {
	if !node.oriented {
		n.identifyNeighbours(tx, node)
	}

	layerOne := layer + 1
	for _, neighbour := range node.neighbours {
		if layerOne > neighbour.layer {
			neighbour.layer = layerOne
			neighbour.source = node.pos
			n.updateQueue[1] = append(n.updateQueue[1], neighbour)
		}
	}

	layerTwo := layer + 2
	for _, neighbour := range node.neighbours[:4] {
		if layerTwo > neighbour.layer {
			neighbour.layer = layerTwo
			neighbour.source = node.pos
			n.updateQueue[2] = append(n.updateQueue[2], neighbour)
		}
	}
}

// breadthFirstWalk performs a breadth-first (layer by layer) traversal through redstone wires, propagating value
// changes to neighbours in the order that they are visited.
func (n *wireNetwork) breadthFirstWalk(tx *world.Tx) {
	n.shiftQueue()
	n.currentWalkLayer = 1

	for len(n.updateQueue[0]) > 0 || len(n.updateQueue[1]) > 0 {
		for _, node := range n.updateQueue[0] {
			if _, ok := node.block.(RedstoneWire); ok {
				n.updateNode(tx, node, n.currentWalkLayer)
				continue
			}
			updateRedstoneFrom(node.pos, node.source, tx)
		}

		n.shiftQueue()
		n.currentWalkLayer++
	}

	n.currentWalkLayer = 0
}

// shiftQueue shifts the update queue, moving all nodes from the current layer to the next layer. The last queue is then
// simply invalidated.
func (n *wireNetwork) shiftQueue() {
	n.updateQueue[0] = n.updateQueue[1]
	n.updateQueue[1] = n.updateQueue[2]
	n.updateQueue[2] = nil
}

// updateNode processes a node which has had neighbouring redstone wires that have experienced value changes.
func (n *wireNetwork) updateNode(tx *world.Tx, node *wireNode, layer uint32) {
	node.visited = true
	if redstoneUpdateCancelled(node.pos, tx) {
		return
	}

	newWire, changed := n.calculateCurrentChanges(tx, node)
	if !changed {
		return
	}
	node.block = newWire
	n.propagateChanges(tx, node, layer)
}

// calculateCurrentChanges computes redstone wire power levels from neighboring blocks. Modifications cut the number of
// power level changes by about 45% from vanilla, and also synergies well with the breadth-first search implementation.
// It returns the new redstone wire block and a boolean indicating whether the power level changed.
func (n *wireNetwork) calculateCurrentChanges(tx *world.Tx, node *wireNode) (RedstoneWire, bool) {
	wire := node.block.(RedstoneWire)
	i := wire.Power

	if !node.oriented {
		n.identifyNeighbours(tx, node)
	}

	j := calculateRedstoneWirePower(node.pos, tx, func(pos cube.Pos) world.Block {
		if cached, ok := n.nodeCache[pos]; ok {
			return cached.block
		}
		return tx.Block(pos)
	})

	if i == j {
		return wire, false
	}
	wire.Power = j
	tx.SetBlock(node.pos, wire, &world.SetOpts{DisableBlockUpdates: true})
	return wire, true
}

// maxRedstoneWirePower returns the greater of strength and the power level of b if it is redstone wire.
func maxRedstoneWirePower(b world.Block, strength int) int {
	if wire, ok := b.(RedstoneWire); ok {
		return max(wire.Power, strength)
	}
	return strength
}

// computeRedstoneHeading computes the cardinal direction that is "forward" given which redstone wires have been visited
// and which have not around the position currently being processed.
func computeRedstoneHeading(rX, rZ int32) uint32 {
	code := (rX + 1) + 3*(rZ+1)
	switch code {
	case 0, 1:
		return wireHeadingNorth
	case 2, 5:
		return wireHeadingEast
	case 3, 4:
		return wireHeadingWest
	case 6, 7, 8:
		return wireHeadingSouth
	}
	panic("should never happen")
}
