package block

import (
	"math"
	"sync"

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

// LiquidRemovable represents a block that may be removed by a liquid flowing into it. When this happens, the
// block's drops are dropped at the position if HasLiquidDrops returns true.
type LiquidRemovable interface {
	HasLiquidDrops() bool
}

// sourceWaterDisplacer may be embedded to allow displacing water source blocks.
type sourceWaterDisplacer struct{}

// CanDisplace returns true if the world.Liquid passed is of the type Water, not falling and has a depth of 8.
func (s sourceWaterDisplacer) CanDisplace(b world.Liquid) bool {
	w, ok := b.(Water)
	return ok && !w.Falling && w.Depth == 8
}

// flowingWaterDisplacer may be embedded to allow displacing water source blocks or flowing water.
type flowingWaterDisplacer struct{}

// CanDisplace returns true if the world.Liquid passed is of the type Water.
func (s flowingWaterDisplacer) CanDisplace(b world.Liquid) bool {
	_, ok := b.(Water)
	return ok
}

// tickLiquid ticks the liquid block passed at a specific position in the world. Depending on the surroundings
// and the liquid block, the liquid will either spread or decrease in depth. Additionally, the liquid might
// be turned into a solid block if a different liquid is next to it.
func tickLiquid(b world.Liquid, pos cube.Pos, tx *world.Tx) {
	if !source(b) && !sourceAround(b, pos, tx) {
		var res world.Liquid
		if b.LiquidDepth()-4 > 0 {
			res = b.WithDepth(b.LiquidDepth()-2*b.SpreadDecay(), false)
		}
		ctx := event.C(tx)
		if tx.World().Handler().HandleLiquidDecay(ctx, pos, b, res); ctx.Cancelled() {
			return
		}
		tx.SetLiquid(pos, res)
		return
	}
	displacer, _ := tx.Block(pos).(world.LiquidDisplacer)

	canFlowBelow := canFlowInto(b, tx, pos.Side(cube.FaceDown), false)
	if b.LiquidFalling() && !canFlowBelow {
		b = b.WithDepth(8, true)
	} else if canFlowBelow {
		below := pos.Side(cube.FaceDown)
		if displacer == nil || !displacer.SideClosed(pos, below, tx) {
			flowInto(b.WithDepth(8, true), pos, below, tx, true)
		}
	}

	depth, decay := b.LiquidDepth(), b.SpreadDecay()
	if depth <= decay {
		// Current depth is smaller than the decay, so spreading will result in nothing.
		return
	}
	if source(b) || !canFlowBelow {
		paths := calculateLiquidPaths(b, pos, tx, displacer)
		if len(paths) == 0 {
			spreadOutwards(b, pos, tx, displacer)
			return
		}

		smallestLen := len(paths[0])
		for _, path := range paths {
			if len(path) <= smallestLen {
				flowInto(b, pos, path[0], tx, false)
			}
		}
	}
}

// source checks if a liquid is a source block.
func source(b world.Liquid) bool {
	return b.LiquidDepth() == 8 && !b.LiquidFalling()
}

// spreadOutwards spreads the liquid outwards into the horizontal directions.
func spreadOutwards(b world.Liquid, pos cube.Pos, tx *world.Tx, displacer world.LiquidDisplacer) {
	pos.Neighbours(func(neighbour cube.Pos) {
		if neighbour[1] == pos[1] {
			if displacer == nil || !displacer.SideClosed(pos, neighbour, tx) {
				flowInto(b, pos, neighbour, tx, false)
			}
		}
	}, tx.Range())
}

// sourceAround checks if there is a source in the blocks around the position passed.
func sourceAround(b world.Liquid, pos cube.Pos, tx *world.Tx) (sourcePresent bool) {
	pos.Neighbours(func(neighbour cube.Pos) {
		if neighbour[1] == pos[1]-1 {
			// We don't care about water below this one.
			return
		}
		side, ok := tx.Liquid(neighbour)
		if !ok || side.LiquidType() != b.LiquidType() {
			return
		}
		if displacer, ok := tx.Block(neighbour).(world.LiquidDisplacer); ok && displacer.SideClosed(neighbour, pos, tx) {
			// The side towards this liquid was closed, so this cannot function as a source for this
			// liquid.
			return
		}
		if neighbour[1] == pos[1]+1 || source(side) || side.LiquidDepth() > b.LiquidDepth() {
			sourcePresent = true
		}
	}, tx.Range())
	return
}

// flowInto makes the liquid passed flow into the position passed in a world. If successful, the block at that
// position will be broken and the liquid with a lower depth will replace it.
func flowInto(b world.Liquid, src, pos cube.Pos, tx *world.Tx, falling bool) bool {
	newDepth := b.LiquidDepth() - b.SpreadDecay()
	if falling {
		newDepth = b.LiquidDepth()
	}
	if newDepth <= 0 && !falling {
		return false
	}
	existing := tx.Block(pos)
	if existingLiquid, alsoLiquid := existing.(world.Liquid); alsoLiquid && existingLiquid.LiquidType() == b.LiquidType() {
		if existingLiquid.LiquidDepth() >= newDepth || existingLiquid.LiquidFalling() {
			// The existing liquid had a higher depth than the one we're propagating, or it was falling
			// (basically considered full depth), so no need to continue.
			return true
		}
		ctx := event.C(tx)
		if tx.World().Handler().HandleLiquidFlow(ctx, src, pos, b.WithDepth(newDepth, falling), existing); ctx.Cancelled() {
			return false
		}
		tx.SetLiquid(pos, b.WithDepth(newDepth, falling))
		return true
	} else if alsoLiquid {
		existingLiquid.Harden(pos, tx, &src)
		return false
	}
	displacer, isDisplacer := existing.(world.LiquidDisplacer)
	if isDisplacer {
		if _, ok := tx.Liquid(pos); ok {
			// We've got a liquid displacer, and it's got a liquid within it, so we can't flow into this.
			return false
		}
	}
	_, isRemovable := existing.(LiquidRemovable)
	if !isRemovable && (!isDisplacer || !displacer.CanDisplace(b.WithDepth(newDepth, falling))) {
		// Can't flow into this block.
		return false
	}
	ctx := event.C(tx)
	if tx.World().Handler().HandleLiquidFlow(ctx, src, pos, b.WithDepth(newDepth, falling), existing); ctx.Cancelled() {
		return false
	}

	if isRemovable {
		if _, air := existing.(Air); !air {
			tx.SetBlock(pos, nil, nil)
			b.LiquidRemoveBlock(pos, tx, existing)
		}
	}
	tx.SetLiquid(pos, b.WithDepth(newDepth, falling))
	return true
}

// liquidPath represents a path to an empty lower block or a block that can be flown into by a liquid, which
// the liquid tends to flow into. All paths with the lowest length will be filled with water.
type liquidPath []cube.Pos

// calculateLiquidPaths calculates paths in the world that the liquid passed can flow in to reach lower
// grounds, starting at the position passed.
// If none of these paths can be found, the returned slice has a length of 0.
func calculateLiquidPaths(b world.Liquid, pos cube.Pos, tx *world.Tx, displacer world.LiquidDisplacer) []liquidPath {
	queue := liquidQueuePool.Get().(*liquidQueue)
	defer func() {
		queue.Reset()
		liquidQueuePool.Put(queue)
	}()
	queue.PushBack(liquidNode{x: pos[0], z: pos[2], depth: int8(b.LiquidDepth())})
	decay := int8(b.SpreadDecay())

	paths := make([]liquidPath, 0, 3)
	first := true

	for queue.Len() != 0 {
		node := queue.Front()
		neighA, neighB, neighC, neighD := node.neighbours(decay * 2)
		if !first || (displacer == nil || !displacer.SideClosed(pos, cube.Pos{neighA.x, pos[1], neighA.z}, tx)) {
			if spreadNeighbour(b, pos, tx, neighA, queue) {
				queue.shortestPath = neighA.Len()
				paths = append(paths, neighA.Path(pos))
			}
		}
		if !first || (displacer == nil || !displacer.SideClosed(pos, cube.Pos{neighB.x, pos[1], neighB.z}, tx)) {
			if spreadNeighbour(b, pos, tx, neighB, queue) {
				queue.shortestPath = neighB.Len()
				paths = append(paths, neighB.Path(pos))
			}
		}
		if !first || (displacer == nil || !displacer.SideClosed(pos, cube.Pos{neighC.x, pos[1], neighC.z}, tx)) {
			if spreadNeighbour(b, pos, tx, neighC, queue) {
				queue.shortestPath = neighC.Len()
				paths = append(paths, neighC.Path(pos))
			}
		}
		if !first || (displacer == nil || !displacer.SideClosed(pos, cube.Pos{neighD.x, pos[1], neighD.z}, tx)) {
			if spreadNeighbour(b, pos, tx, neighD, queue) {
				queue.shortestPath = neighD.Len()
				paths = append(paths, neighD.Path(pos))
			}
		}
		first = false
	}
	return paths
}

// spreadNeighbour attempts to spread a path node into the neighbour passed. Note that this does not spread
// the liquid, it only spreads the node used to calculate flow paths.
func spreadNeighbour(b world.Liquid, src cube.Pos, tx *world.Tx, node liquidNode, queue *liquidQueue) bool {
	if node.depth+3 <= 0 {
		// Depth has reached zero or below, can't spread any further.
		return false
	}
	if node.Len() > queue.shortestPath {
		// This path is longer than any existing path, so don't spread any further.
		return false
	}
	pos := cube.Pos{node.x, src[1], node.z}
	if !canFlowInto(b, tx, pos, true) {
		// Can't flow into this block, can't spread any further.
		return false
	}
	pos[1]--
	if canFlowInto(b, tx, pos, false) {
		return true
	}
	queue.PushBack(node)
	return false
}

// canFlowInto checks if a liquid can flow into the block present in the world at a specific block position.
func canFlowInto(b world.Liquid, tx *world.Tx, pos cube.Pos, sideways bool) bool {
	bl := tx.Block(pos)
	if _, air := bl.(Air); air {
		// Fast route for air: A type assert to a concrete type is much faster than a type assert to an interface.
		return true
	}
	if _, ok := bl.(LiquidRemovable); ok {
		if liq, ok := bl.(world.Liquid); ok && sideways {
			if (liq.LiquidDepth() == 8 && !liq.LiquidFalling()) || liq.LiquidType() != b.LiquidType() {
				// Can't flow into a liquid if it has a depth of 8 or if it doesn't have the same type.
				return false
			}
		}
		return true
	}
	if dis, ok := bl.(world.LiquidDisplacer); ok {
		res := b.WithDepth(b.LiquidDepth()-b.SpreadDecay(), !sideways)
		if dis.CanDisplace(res) {
			return true
		}
	}
	return false
}

// liquidNode represents a position that is part of a flow path for a liquid.
type liquidNode struct {
	x, z     int
	depth    int8
	previous *liquidNode
}

// neighbours returns the four horizontal neighbours of the node with decreased depth.
func (node liquidNode) neighbours(decay int8) (a, b, c, d liquidNode) {
	return liquidNode{x: node.x - 1, z: node.z, depth: node.depth - decay, previous: &node},
		liquidNode{x: node.x + 1, z: node.z, depth: node.depth - decay, previous: &node},
		liquidNode{x: node.x, z: node.z - 1, depth: node.depth - decay, previous: &node},
		liquidNode{x: node.x, z: node.z + 1, depth: node.depth - decay, previous: &node}
}

// Len returns the length of the path created by the node.
func (node liquidNode) Len() int {
	i := 1
	for {
		if node.previous == nil {
			return i - 1
		}
		//noinspection GoAssignmentToReceiver
		node = *node.previous
		i++
	}
}

// Path converts the liquid node into a path.
func (node liquidNode) Path(src cube.Pos) liquidPath {
	l := node.Len()
	path := make(liquidPath, l)
	i := l - 1
	for {
		if node.previous == nil {
			return path
		}
		path[i] = cube.Pos{node.x, src[1], node.z}

		//noinspection GoAssignmentToReceiver
		node = *node.previous
		i--
	}
}

// liquidQueuePool is use to re-use liquid node queues.
var liquidQueuePool = sync.Pool{
	New: func() any {
		return &liquidQueue{
			nodes:        make([]liquidNode, 0, 64),
			shortestPath: math.MaxInt8,
		}
	},
}

// liquidQueue represents a queue that may be used to push nodes into and take them out of it.
type liquidQueue struct {
	nodes        []liquidNode
	i            int
	shortestPath int
}

func (q *liquidQueue) PushBack(node liquidNode) {
	q.nodes = append(q.nodes, node)
}

func (q *liquidQueue) Front() liquidNode {
	v := q.nodes[q.i]
	q.i++
	return v
}

func (q *liquidQueue) Len() int {
	return len(q.nodes) - q.i
}

func (q *liquidQueue) Reset() {
	q.nodes = q.nodes[:0]
	q.i = 0
	q.shortestPath = math.MaxInt8
}
