package entity

import (
	"math"
	"time"

	"edgaru089.ml/go/gl01/internal/util"
	"edgaru089.ml/go/gl01/internal/util/itype"
	"edgaru089.ml/go/gl01/internal/world"
)

func (e *Entity) worldHitbox(hitbox itype.Vec3d) itype.Boxd {
	return itype.Boxd{
		OffX:  e.pos[0] - hitbox[0]/2,
		OffY:  e.pos[1],
		OffZ:  e.pos[2] - hitbox[2]/2,
		SizeX: hitbox[0],
		SizeY: hitbox[1],
		SizeZ: hitbox[2],
	}
}

// WorldHitbox returns the hitbox of the entity, in world coordinates.
func (e *Entity) WorldHitbox() itype.Boxd {
	return e.worldHitbox(e.b.Hitbox(e.pos, e.ds))
}

// the distance between hitpoints, in blocks
const HitpointMeshLen = 0.4

func (e *Entity) boxHitpoints(points []itype.Vec3d, hitbox itype.Boxd) []itype.Vec3d {

	box := hitbox
	base := box.MinPoint()
	points = points[0:0]

	// add the bounding points first
	points = append(points,
		base,
		base.Add(itype.Vec3d{box.SizeX, 0, 0}),
		base.Add(itype.Vec3d{0, box.SizeY, 0}),
		base.Add(itype.Vec3d{0, 0, box.SizeZ}),
		base.Add(itype.Vec3d{box.SizeX, box.SizeY, 0}),
		base.Add(itype.Vec3d{0, box.SizeY, box.SizeZ}),
		base.Add(itype.Vec3d{box.SizeX, 0, box.SizeZ}),
		base.Add(itype.Vec3d{box.SizeX, box.SizeY, box.SizeZ}),
	)
	/*
		// add the surface points
		// X+ and X-
		for y := base[1] + HitpointMeshLen; y < base[1]+hitbox[1]; y += HitpointMeshLen {
			for z := base[2] + HitpointMeshLen; z < base[2]+hitbox[2]; z += HitpointMeshLen {
				points = append(points,
					base.Addv(0, y, z),
					base.Addv(box.SizeX, y, z),
				)
			}
		}
		// Y+ and Y-
		for x := base[0] + HitpointMeshLen; x < base[0]+hitbox[0]; x += HitpointMeshLen {
			for z := base[2] + HitpointMeshLen; z < base[2]+hitbox[2]; z += HitpointMeshLen {
				points = append(points,
					base.Addv(x, 0, z),
					base.Addv(x, box.SizeY, z),
				)
			}
		}
		// Z+ and Z-
		for x := base[0] + HitpointMeshLen; x < base[0]+hitbox[0]; x += HitpointMeshLen {
			for y := base[1] + HitpointMeshLen; y < base[1]+hitbox[1]; y += HitpointMeshLen {
				points = append(points,
					base.Addv(x, y, 0),
					base.Addv(x, y, box.SizeZ),
				)
			}
		}*/
	return points
}

// MoveEps is a small value used to indicate a tiny amount more than zero.
const MoveEps = 1e-7

// tells if the point is already stuck inside a block hitbox
func pointStuck(point itype.Vec3d, w *world.World) bool {
	blockid := point.Floor()
	block := w.Block(blockid)
	return block.Id != 0 &&
		block.Behaviour.Appearance(
			point.Floor(),
			block.Aux,
			block.Dataset,
		).Hitbox.Offset(blockid.ToFloat64()).Contains(point)
}

// the move series functions should be called with e.hp already filled

func (e *Entity) moveX(delta float64, hitbox itype.Boxd, w *world.World) {
	if math.Abs(delta) < MoveEps*10 {
		return
	}

	var hit bool = false
	var deltaMin float64 = 10000

	if delta > 1-MoveEps {
		delta = 1 - MoveEps
	}
	if delta < -1+MoveEps {
		delta = -1 + MoveEps
	}

	// X+ / X-
	for _, p := range e.hp {
		if pointStuck(p, w) {
			continue
		}

		dest := p.Addv(delta, 0, 0)
		blockid := dest.Floor()
		block := w.Block(blockid)

		var deltaDone float64
		if block.Id == 0 {
			deltaDone = delta
		} else { // block.Id!=0
			app := world.GetBlockAppearance(blockid, block.Id, block.Aux, block.Dataset)
			blockBox := app.Hitbox.Offset(blockid.ToFloat64())
			if !app.NotSolid && blockBox.Contains(dest) { // Hit!
				hit = true
				if delta > 0 { // moving X+, hit on the X- face
					deltaDone = blockBox.OffX - (e.pos[0] + hitbox.SizeX/2) - MoveEps
					//log.Print("Hit X+: On Block ", blockid, ", delta=", delta, ", coord=", e.pos, ", p=", p, ", dest=", dest, ", deltaDone=", deltaDone)
				} else { // moving X-, hit on the X+ face
					deltaDone = blockBox.OffX + blockBox.SizeX - (e.pos[0] - hitbox.SizeX/2) + MoveEps
					//log.Print("Hit X-: On Block ", blockid, ", delta=", delta, ", coord=", e.pos, ", p=", p, ", dest=", dest, ", deltaDone=", deltaDone)
				}
			} else {
				deltaDone = delta
			}
		}

		deltaMin = util.AbsMind(deltaMin, deltaDone)
	}

	if hit {
		e.speed[0] = 0
	}
	e.pos[0] += deltaMin
}
func (e *Entity) moveY(delta float64, hitbox itype.Boxd, w *world.World) {
	if math.Abs(delta) < MoveEps*10 {
		return
	}

	var hit bool = false
	var deltaMin float64 = 10000

	if delta > 1-MoveEps {
		delta = 1 - MoveEps
	}
	if delta < -1+MoveEps {
		delta = -1 + MoveEps
	}

	// Y+ / Y-
	for _, p := range e.hp {
		if pointStuck(p, w) {
			continue
		}

		dest := p.Addv(0, delta, 0)
		blockid := dest.Floor()
		block := w.Block(blockid)

		var deltaDone float64
		if block.Id == 0 {
			deltaDone = delta
		} else { // block.Id!=0
			app := world.GetBlockAppearance(blockid, block.Id, block.Aux, block.Dataset)
			blockBox := app.Hitbox.Offset(blockid.ToFloat64())
			if !app.NotSolid && blockBox.Contains(dest) { // Hit!
				hit = true
				if delta > 0 { // moving Y+, hit on the Y- face
					deltaDone = blockBox.OffY - (e.pos[1] + hitbox.SizeY) - MoveEps
					//log.Print("Hit Y+: On Block ", blockid, ", delta=", delta, ", coord=", e.pos, ", p=", p, ", dest=", dest,", deltaDone=",deltaDone)
				} else { // moving Y-, hit on the Y+ face (on the ground)
					deltaDone = blockBox.OffY + blockBox.SizeY - e.pos[1] + MoveEps
					//log.Print("Hit Y-: On Block ", blockid, ", delta=", delta, ", coord=", e.pos, ", p=", p, ", dest=", dest,", deltaDone=",deltaDone)
					/*if !e.onGround {
						log.Print("onGround = true")
					}*/
					e.onGround = true
				}
			} else {
				deltaDone = delta
			}
		}

		deltaMin = util.AbsMind(deltaMin, deltaDone)
	}

	if hit {
		e.speed[1] = 0
	}

	if math.Abs(deltaMin) > MoveEps*10 {
		/*if e.onGround {
			log.Print("onGround = false")
		}*/
		e.onGround = false
	}
	e.pos[1] += deltaMin
}
func (e *Entity) moveZ(delta float64, hitbox itype.Boxd, w *world.World) {
	if math.Abs(delta) < MoveEps*10 {
		return
	}

	var hit bool = false
	var deltaMin float64 = 10000

	if delta > 1-MoveEps {
		delta = 1 - MoveEps
	}
	if delta < -1+MoveEps {
		delta = -1 + MoveEps
	}

	// Z+ / Z-
	for _, p := range e.hp {
		if pointStuck(p, w) {
			continue
		}

		dest := p.Addv(0, 0, delta)
		blockid := dest.Floor()
		block := w.Block(blockid)

		var deltaDone float64
		if block.Id == 0 {
			deltaDone = delta
		} else { // block.Id!=0
			app := world.GetBlockAppearance(blockid, block.Id, block.Aux, block.Dataset)
			blockBox := app.Hitbox.Offset(blockid.ToFloat64())
			if !app.NotSolid && blockBox.Contains(dest) { // Hit!
				hit = true
				if delta > 0 { // moving Z+, hit on the Z- face
					deltaDone = util.Maxd(blockBox.OffZ-(e.pos[2]+hitbox.SizeZ/2)-MoveEps, 0)
					//log.Print("Hit Z+: On Block ", blockid, ", delta=", delta, ", coord=", e.pos, ", p=", p, ", dest=", dest, ", deltaDone=", deltaDone)
				} else { // moving Z-, hit on the Z+ face
					deltaDone = util.Mind(blockBox.OffZ+blockBox.SizeZ-(e.pos[2]-hitbox.SizeZ/2)+MoveEps, 0)
					//log.Print("Hit Z-: On Block ", blockid, ", delta=", delta, ", coord=", e.pos, ", p=", p, ", dest=", dest, ", deltaDone=", deltaDone)
				}
			} else {
				deltaDone = delta
			}
		}

		deltaMin = util.AbsMind(deltaMin, deltaDone)
	}

	if hit {
		e.speed[2] = 0
	}
	e.pos[2] += deltaMin
}

const gravity float64 = 26
const deaclc float64 = 28

func (e *Entity) Update(world *world.World, deltaTime time.Duration) {

	hitbox := e.b.Hitbox(e.pos, e.ds)
	box := e.worldHitbox(hitbox)

	e.hp = e.hp[0:0]
	e.hp = e.boxHitpoints(e.hp, box)

	e.moveX(e.speed[0]*deltaTime.Seconds(), box, world)
	e.moveY(e.speed[1]*deltaTime.Seconds(), box, world)
	e.moveZ(e.speed[2]*deltaTime.Seconds(), box, world)

	speed := math.Sqrt(e.speed[0]*e.speed[0] + e.speed[2]*e.speed[2])
	if speed > MoveEps {
		decrease := deaclc * deltaTime.Seconds()
		if !e.onGround {
			decrease /= 10
		}
		factor := util.Maxd(util.Mind(speed-decrease, 9), 0) / speed

		e.speed[0] *= factor
		e.speed[2] *= factor
	}

	e.speed[1] -= gravity * deltaTime.Seconds()

	e.b.Update(e.pos, e.ds, world, deltaTime)
}
