package cube

import (
	"fmt"
	"iter"
	"math"

	"github.com/go-gl/mathgl/mgl64"
)

// Pos holds the position of a block. The position is represented as an array
// with an x, y and z value, where the y value is positive.
type Pos [3]int

// String converts the Pos to a string in the format (1,2,3) and returns it.
func (p Pos) String() string {
	return fmt.Sprintf("(%v,%v,%v)", p[0], p[1], p[2])
}

// X returns the X coordinate of the block position.
func (p Pos) X() int {
	return p[0]
}

// Y returns the Y coordinate of the block position.
func (p Pos) Y() int {
	return p[1]
}

// Z returns the Z coordinate of the block position.
func (p Pos) Z() int {
	return p[2]
}

// OutOfBounds checks if the Y value is either bigger than r[1] or smaller than
// r[0].
func (p Pos) OutOfBounds(r Range) bool {
	y := p[1]
	return y > r[1] || y < r[0]
}

// Within reports if the position lies within the inclusive minimum and maximum bounds passed.
func (p Pos) Within(min, max Pos) bool {
	return p[0] >= min[0] && p[0] <= max[0] &&
		p[1] >= min[1] && p[1] <= max[1] &&
		p[2] >= min[2] && p[2] <= max[2]
}

// Add adds two positions together and returns a new combined one.
func (p Pos) Add(pos Pos) Pos {
	return Pos{p[0] + pos[0], p[1] + pos[1], p[2] + pos[2]}
}

// Sub subtracts pos from p and returns a new one with the subtracted values.
func (p Pos) Sub(pos Pos) Pos {
	return Pos{p[0] - pos[0], p[1] - pos[1], p[2] - pos[2]}
}

// Vec3 returns a vec3 holding the same coordinates as the block position.
func (p Pos) Vec3() mgl64.Vec3 {
	return mgl64.Vec3{float64(p[0]), float64(p[1]), float64(p[2])}
}

// Vec3Middle returns a Vec3 holding the coordinates of the block position with
// 0.5 added on both horizontal axes.
func (p Pos) Vec3Middle() mgl64.Vec3 {
	return mgl64.Vec3{float64(p[0]) + 0.5, float64(p[1]), float64(p[2]) + 0.5}
}

// Vec3Centre returns a Vec3 holding the coordinates of the block position with
// 0.5 added on all axes.
func (p Pos) Vec3Centre() mgl64.Vec3 {
	return mgl64.Vec3{float64(p[0]) + 0.5, float64(p[1]) + 0.5, float64(p[2]) + 0.5}
}

// Side returns the position on the side of this block position, at a specific
// face.
func (p Pos) Side(face Face) Pos {
	switch face {
	case FaceUp:
		p[1]++
	case FaceDown:
		p[1]--
	case FaceNorth:
		p[2]--
	case FaceSouth:
		p[2]++
	case FaceWest:
		p[0]--
	case FaceEast:
		p[0]++
	}
	return p
}

// Face returns the face that the other Pos was on compared to the current Pos.
// The other Pos is assumed to be a direct neighbour of the current Pos.
func (p Pos) Face(other Pos) Face {
	switch other {
	case p.Add(Pos{0, 1}):
		return FaceUp
	case p.Add(Pos{0, -1}):
		return FaceDown
	case p.Add(Pos{0, 0, -1}):
		return FaceNorth
	case p.Add(Pos{0, 0, 1}):
		return FaceSouth
	case p.Add(Pos{-1, 0, 0}):
		return FaceWest
	case p.Add(Pos{1, 0, 0}):
		return FaceEast
	}
	return FaceUp
}

// Neighbours calls the function passed for each of the block position's
// neighbours. If the Y value is out of bounds, the function will not be called
// for that position.
func (p Pos) Neighbours(f func(neighbour Pos), r Range) {
	if p.OutOfBounds(r) {
		return
	}
	p[0]++
	f(p)
	p[0] -= 2
	f(p)
	p[0]++
	p[1]++
	if p[1] <= r[1] {
		f(p)
	}
	p[1] -= 2
	if p[1] >= r[0] {
		f(p)
	}
	p[1]++
	p[2]++
	f(p)
	p[2] -= 2
	f(p)
}

// PosFromVec3 returns a block position by a Vec3, rounding the values down
// adequately.
func PosFromVec3(vec3 mgl64.Vec3) Pos {
	return Pos{int(math.Floor(vec3[0])), int(math.Floor(vec3[1])), int(math.Floor(vec3[2]))}
}

// Min returns a new position where each coordinate is the minimum
// of input positions p1 and p2.
func Min(p1, p2 Pos) Pos {
	return Pos{min(p1[0], p2[0]), min(p1[1], p2[1]), min(p1[2], p2[2])}
}

// Max returns a new position where each coordinate is the maximum
// of input positions p1 and p2.
func Max(p1, p2 Pos) Pos {
	return Pos{max(p1[0], p2[0]), max(p1[1], p2[1]), max(p1[2], p2[2])}
}

// Range3D returns iterator that iterates all points between minimum and maximum of p1 & p2.
func Range3D(p1, p2 Pos) iter.Seq[Pos] {
	max := Max(p1, p2)
	min := Min(p1, p2)
	return func(yield func(Pos) bool) {
		for x := min[0]; x <= max[0]; x++ {
			for y := min[1]; y <= max[1]; y++ {
				for z := min[2]; z <= max[2]; z++ {
					if !yield(Pos{x, y, z}) {
						return
					}
				}
			}
		}
	}
}
