package trace

import (
	"github.com/df-mc/dragonfly/server/block/cube"
	"github.com/go-gl/mathgl/mgl64"
	"math"
)

// TraverseBlocks performs a ray trace between the start and end coordinates.
// A function 'f' is passed which is called for each voxel, if f returns false, the function will return.
// TraverseBlocks panics if the start and end positions are the same.
func TraverseBlocks(start, end mgl64.Vec3, f func(pos cube.Pos) (con bool)) {
	dir := end.Sub(start)
	if mgl64.FloatEqual(dir.LenSqr(), 0) {
		panic("start and end points are the same, giving a zero direction vector")
	}
	dir = dir.Normalize()

	b := cube.PosFromVec3(start)

	step := signVec3(dir)
	stepX, stepY, stepZ := int(step[0]), int(step[1]), int(step[2])
	max := boundaryVec3(start, dir)

	delta := safeDivideVec3(step, dir)

	r := start.Sub(end).Len()
	for {
		if !f(b) {
			return
		}

		switch {
		case max[0] < max[1] && max[0] < max[2]:
			if max[0] > r {
				return
			}
			b[0] += stepX
			max[0] += delta[0]
		case max[1] < max[2]:
			if max[1] > r {
				return
			}
			b[1] += stepY
			max[1] += delta[1]
		default:
			if max[2] > r {
				return
			}
			b[2] += stepZ
			max[2] += delta[2]
		}
	}
}

// safeDivideVec3 ...
func safeDivideVec3(dividend, divisor mgl64.Vec3) mgl64.Vec3 {
	return mgl64.Vec3{
		safeDivide(dividend[0], divisor[0]),
		safeDivide(dividend[1], divisor[1]),
		safeDivide(dividend[2], divisor[2]),
	}
}

// safeDivide divides the dividend by the divisor, but if the divisor is 0, it returns 0.
func safeDivide(dividend, divisor float64) float64 {
	if divisor == 0.0 {
		return 0.0
	}
	return dividend / divisor
}

// boundaryVec3 ...
func boundaryVec3(v1, v2 mgl64.Vec3) mgl64.Vec3 {
	return mgl64.Vec3{boundary(v1[0], v2[0]), boundary(v1[1], v2[1]), boundary(v1[2], v2[2])}
}

// boundary returns the distance that must be travelled on an axis from the start point with the direction vector
// component to cross a block boundary.
func boundary(start, dir float64) float64 {
	if dir == 0.0 {
		return math.Inf(1)
	}

	if dir < 0.0 {
		start, dir = -start, -dir
		if math.Floor(start) == start {
			return 0.0
		}
	}

	return (1 - (start - math.Floor(start))) / dir
}

// signVec3 ...
func signVec3(v1 mgl64.Vec3) mgl64.Vec3 {
	return mgl64.Vec3{sign(v1[0]), sign(v1[1]), sign(v1[2])}
}

// sign ...
func sign(f float64) float64 {
	switch {
	case f > 0.0:
		return 1.0
	case f < 0.0:
		return -1.0
	}
	return 0.0
}
