package block

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

// Vines are climbable non-solid vegetation blocks that grow on walls.
type Vines struct {
	replaceable
	transparent
	empty
	sourceWaterDisplacer

	// NorthDirection is true if the vines are attached towards north.
	NorthDirection bool
	// EastDirection is true if the vines are attached towards east.
	EastDirection bool
	// SouthDirection is true if the vines are attached towards south.
	SouthDirection bool
	// WestDirection is true if the vines are attached towards west.
	WestDirection bool
}

// CompostChance ...
func (Vines) CompostChance() float64 {
	return 0.5
}

// SideClosed ...
func (Vines) SideClosed(cube.Pos, cube.Pos, *world.Tx) bool {
	return false
}

// HasLiquidDrops ...
func (Vines) HasLiquidDrops() bool {
	return false
}

// FlammabilityInfo ...
func (Vines) FlammabilityInfo() FlammabilityInfo {
	return newFlammabilityInfo(15, 100, true)
}

// BreakInfo ...
func (v Vines) BreakInfo() BreakInfo {
	return newBreakInfo(0.2, func(t item.Tool) bool {
		return t.ToolType() == item.TypeShears
	}, axeEffective, oneOf(v))
}

// EntityInside ...
func (Vines) EntityInside(_ cube.Pos, _ *world.Tx, e world.Entity) {
	if fallEntity, ok := e.(fallDistanceEntity); ok {
		fallEntity.ResetFallDistance()
	}
}

// WithAttachment returns a Vines block with an attachment on the given cube.Direction.
func (v Vines) WithAttachment(direction cube.Direction, attached bool) Vines {
	switch direction {
	case cube.North:
		v.NorthDirection = attached
		return v
	case cube.East:
		v.EastDirection = attached
		return v
	case cube.South:
		v.SouthDirection = attached
		return v
	case cube.West:
		v.WestDirection = attached
		return v
	}
	panic("should never happen")
}

// Attachment returns the attachment of the vines at the given direction.
func (v Vines) Attachment(direction cube.Direction) bool {
	switch direction {
	case cube.North:
		return v.NorthDirection
	case cube.East:
		return v.EastDirection
	case cube.South:
		return v.SouthDirection
	case cube.West:
		return v.WestDirection
	}
	panic("should never happen")
}

// Attachments returns all attachments of the vines.
func (v Vines) Attachments() (attachments []cube.Direction) {
	for _, d := range cube.Directions() {
		if v.Attachment(d) {
			attachments = append(attachments, d)
		}
	}
	return
}

// UseOnBlock ...
func (v Vines) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx *world.Tx, user item.User, ctx *item.UseContext) bool {
	if _, ok := tx.Block(pos).Model().(model.Solid); !ok || face.Axis() == cube.Y {
		return false
	}
	pos, face, used := firstReplaceable(tx, pos, face, v)
	if !used {
		return false
	}
	if _, ok := tx.Block(pos).(Vines); ok {
		// Do not overwrite existing vine block.
		return false
	}
	//noinspection GoAssignmentToReceiver
	v = v.WithAttachment(face.Direction().Opposite(), true)

	place(tx, pos, v, user, ctx)
	return placed(ctx)
}

// NeighbourUpdateTick ...
func (v Vines) NeighbourUpdateTick(pos, _ cube.Pos, tx *world.Tx) {
	above, updated := tx.Block(pos.Side(cube.FaceUp)), false
	for _, d := range v.Attachments() {
		if !v.canSpreadTo(tx, pos.Side(d.Face())) {
			if o, ok := above.(Vines); !ok || !o.Attachment(d) {
				//noinspection GoAssignmentToReceiver
				v = v.WithAttachment(d, false)
				updated = true
			}
		}
	}
	if !updated {
		return
	}
	if len(v.Attachments()) == 0 {
		breakBlock(v, pos, tx)
		return
	}
	tx.SetBlock(pos, v, nil)
}

// RandomTick ...
func (v Vines) RandomTick(pos cube.Pos, tx *world.Tx, r *rand.Rand) {
	if r.Float64() > 0.25 {
		// Vines have a 25% chance of spreading.
		return
	}

	// Choose a random direction to spread.
	face := cube.Face(r.IntN(len(cube.Faces())))
	selectedPos := pos.Side(face)

	// If a horizontal direction was chosen and the vine block is not already
	// attached in that direction, attempt to spread in that direction.
	if face.Axis() != cube.Y && !v.Attachment(face.Direction()) {
		if !v.canSpread(tx, pos) {
			// No further attempt to spread vertically will be made.
			return
		}
		// Attempt to create a new vine block if there is a neighbouring air block
		// in the chosen direction.
		if _, ok := tx.Block(selectedPos).(Air); ok {
			rightRotatedFace := face.RotateRight()
			leftRotatedFace := face.RotateLeft()

			attachedOnRight := v.Attachment(rightRotatedFace.Direction())
			attachedOnLeft := v.Attachment(leftRotatedFace.Direction())

			rightSelectedPos := selectedPos.Side(rightRotatedFace)
			leftSelectedPos := selectedPos.Side(leftRotatedFace)

			// Four attempts to create a new vine block will be made, in the
			// following order:
			// 1) If the current vine block is attached in the direction towards
			//    the right ("clockwise") of the chosen direction, and a solid
			//    block can support a vine on that direction in the selected
			//    position, create a new vine block attached on that clockwise
			//    direction at the selected position.
			// 2) If the clockwise direction fails, try again with the left
			//    ("counter-clockwise") direction.
			// 3) If the current vine block is attached in the direction towards
			//    the right of the chosen direction, the current vine block is
			//    also backed by a solid block in that same direction, and the
			//    block neighbouring the selected position in that direction is
			//    air, spread into that air block onto the face opposite of the
			//    chosen direction. The vine jumps from one face of a block onto
			//    another as a result.
			// 4) If the clockwise direction fails, try again with the left
			//    direction.
			if attachedOnRight && v.canSpreadTo(tx, rightSelectedPos) {
				tx.SetBlock(selectedPos, (Vines{}).WithAttachment(rightRotatedFace.Direction(), true), nil)
			} else if attachedOnLeft && v.canSpreadTo(tx, leftSelectedPos) {
				tx.SetBlock(selectedPos, (Vines{}).WithAttachment(leftRotatedFace.Direction(), true), nil)
			} else if _, ok = tx.Block(rightSelectedPos).(Air); ok && attachedOnRight && v.canSpreadTo(tx, pos.Side(rightRotatedFace)) {
				tx.SetBlock(rightSelectedPos, (Vines{}).WithAttachment(face.Opposite().Direction(), true), nil)
			} else if _, ok = tx.Block(leftSelectedPos).(Air); ok && attachedOnLeft && v.canSpreadTo(tx, pos.Side(leftRotatedFace)) {
				tx.SetBlock(leftSelectedPos, (Vines{}).WithAttachment(face.Opposite().Direction(), true), nil)
			}
		} else if v.canSpreadTo(tx, selectedPos) {
			// If the neighbouring block is solid, update the vine to be attached in that direction.
			tx.SetBlock(pos, v.WithAttachment(face.Direction(), true), nil)
		}
		return
	}

	// If the chosen direction is Up and the position above is within the height
	// limit, attempt to spread upwards.
	if face == cube.FaceUp && selectedPos.OutOfBounds(tx.Range()) {
		// Vines can only spread upwards into an air block.
		if _, ok := tx.Block(selectedPos).(Air); ok {
			if !v.canSpread(tx, pos) {
				// No further attempt to spread down will be made.
				return
			}
			newVines := Vines{}
			for _, f := range cube.HorizontalFaces() {
				// For each direction the current vine block is attached on,
				// there is a 50% chance for the new above vine block to
				// attach onto the direction, if there is also a solid block
				// in that direction to support the vine.
				if r.IntN(2) == 0 && v.Attachment(f.Direction()) && v.canSpreadTo(tx, selectedPos.Side(f)) {
					newVines = newVines.WithAttachment(f.Direction(), true)
				}
			}
			if len(newVines.Attachments()) > 0 {
				tx.SetBlock(selectedPos, newVines, nil)
			}
			return
		}
	}

	// If an attempt to spread horizontally or upwards has failed but not exited
	// early, attempt to spread downwards.
	selectedPos = pos.Side(cube.FaceDown)
	if selectedPos.OutOfBounds(tx.Range()) {
		return
	}
	newVines, vines := tx.Block(selectedPos).(Vines)
	if _, ok := tx.Block(selectedPos).(Air); !ok && !vines {
		// The block under the current vine block must be air or a vine block.
		return
	}
	var changed bool
	for _, f := range cube.HorizontalFaces() {
		// For each direction the current vine block is attached on, there is a
		// 50% chance for the below vine block to attach onto the direction if
		// it is not already attached in that direction.
		if r.IntN(2) == 0 && v.Attachment(f.Direction()) && !newVines.Attachment(f.Direction()) {
			newVines, changed = newVines.WithAttachment(f.Direction(), true), true
		}
	}
	if changed {
		tx.SetBlock(selectedPos, newVines, nil)
	}
}

// EncodeItem ...
func (Vines) EncodeItem() (name string, meta int16) {
	return "minecraft:vine", 0
}

// EncodeBlock ...
func (v Vines) EncodeBlock() (string, map[string]any) {
	var bits int
	for i, ok := range []bool{v.SouthDirection, v.WestDirection, v.NorthDirection, v.EastDirection} {
		if ok {
			bits |= 1 << i
		}
	}
	return "minecraft:vine", map[string]any{"vine_direction_bits": int32(bits)}
}

// canSpreadTo returns true if the vines can spread onto the block at the
// given position. Vines may only spread onto fully solid blocks.
func (Vines) canSpreadTo(tx *world.Tx, pos cube.Pos) bool {
	_, ok := tx.Block(pos).Model().(model.Solid)
	return ok
}

// canSpread returns true if the vines can spread from the given position. Vines
// may only spread horizontally or upwards if there are fewer than 4 vines within
// a 9x9x3 area centred around the Vines.
func (v Vines) canSpread(tx *world.Tx, pos cube.Pos) bool {
	var count int
	for x := -4; x <= 4; x++ {
		for z := -4; z <= 4; z++ {
			for y := -1; y <= 1; y++ {
				if _, ok := tx.Block(pos.Add(cube.Pos{x, y, z})).(Vines); ok {
					count++
					// The centre vine is counted, for a max of 4+1=5.
					if count >= 5 {
						return false
					}
				}
			}
		}
	}
	return true
}

// allVines ...
func allVines() (b []world.Block) {
	for _, north := range []bool{true, false} {
		for _, east := range []bool{true, false} {
			for _, south := range []bool{true, false} {
				for _, west := range []bool{true, false} {
					b = append(b, Vines{
						NorthDirection: north,
						EastDirection:  east,
						SouthDirection: south,
						WestDirection:  west,
					})
				}
			}
		}
	}
	return
}
