package block

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

// Wall is a block similar to fences that prevents players from jumping over and is thinner than the usual block. It is
// available for many blocks and all types connect together as if they were the same type.
type Wall struct {
	transparent
	sourceWaterDisplacer

	// Block is the block to use for the type of wall.
	Block world.Block
	// NorthConnection is the type of connection in the north direction of the post.
	NorthConnection WallConnectionType
	// EastConnection is the type of connection in the east direction of the post.
	EastConnection WallConnectionType
	// SouthConnection is the type of connection in the south direction of the post.
	SouthConnection WallConnectionType
	// WestConnection is the type of connection in the west direction of the post.
	WestConnection WallConnectionType
	// Post is if the wall is extended to the full height of a block or not.
	Post bool
}

// EncodeItem ...
func (w Wall) EncodeItem() (string, int16) {
	name := encodeWallBlock(w.Block)
	return "minecraft:" + name + "_wall", 0
}

// EncodeBlock ...
func (w Wall) EncodeBlock() (string, map[string]any) {
	properties := map[string]any{
		"wall_connection_type_north": w.NorthConnection.String(),
		"wall_connection_type_east":  w.EastConnection.String(),
		"wall_connection_type_south": w.SouthConnection.String(),
		"wall_connection_type_west":  w.WestConnection.String(),
		"wall_post_bit":              boolByte(w.Post),
	}
	name := encodeWallBlock(w.Block)
	return "minecraft:" + name + "_wall", properties
}

// Model ...
func (w Wall) Model() world.BlockModel {
	return model.Wall{
		NorthConnection: w.NorthConnection.Height(),
		EastConnection:  w.EastConnection.Height(),
		SouthConnection: w.SouthConnection.Height(),
		WestConnection:  w.WestConnection.Height(),
		Post:            w.Post,
	}
}

// BreakInfo ...
func (w Wall) BreakInfo() BreakInfo {
	breakable, ok := w.Block.(Breakable)
	if !ok {
		panic("wall block is not breakable")
	}
	return newBreakInfo(breakable.BreakInfo().Hardness, pickaxeHarvestable, pickaxeEffective, oneOf(w)).withBlastResistance(breakable.BreakInfo().BlastResistance)
}

// NeighbourUpdateTick ...
func (w Wall) NeighbourUpdateTick(pos, _ cube.Pos, tx *world.Tx) {
	w, connectionsUpdated := w.calculateConnections(tx, pos)
	w, postUpdated := w.calculatePost(tx, pos)
	if connectionsUpdated || postUpdated {
		tx.SetBlock(pos, w, nil)
	}
}

// UseOnBlock ...
func (w Wall) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx *world.Tx, user item.User, ctx *item.UseContext) (used bool) {
	pos, _, used = firstReplaceable(tx, pos, face, w)
	if !used {
		return
	}
	w, _ = w.calculateConnections(tx, pos)
	w, _ = w.calculatePost(tx, pos)
	place(tx, pos, w, user, ctx)
	return placed(ctx)
}

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

// ConnectionType returns the connection type of the wall in the given direction.
func (w Wall) ConnectionType(direction cube.Direction) WallConnectionType {
	switch direction {
	case cube.North:
		return w.NorthConnection
	case cube.East:
		return w.EastConnection
	case cube.South:
		return w.SouthConnection
	case cube.West:
		return w.WestConnection
	}
	panic("unknown direction")
}

// WithConnectionType returns the wall with the given connection type in the given direction.
func (w Wall) WithConnectionType(direction cube.Direction, connection WallConnectionType) Wall {
	switch direction {
	case cube.North:
		w.NorthConnection = connection
	case cube.East:
		w.EastConnection = connection
	case cube.South:
		w.SouthConnection = connection
	case cube.West:
		w.WestConnection = connection
	}
	return w
}

// calculateConnections calculates the correct connections for the wall at a given position in a world. The updated wall
// is returned and a bool to determine if any changes were made.
func (w Wall) calculateConnections(tx *world.Tx, pos cube.Pos) (Wall, bool) {
	var updated bool
	abovePos := pos.Add(cube.Pos{0, 1, 0})
	above := tx.Block(abovePos)
	for _, face := range cube.HorizontalFaces() {
		sidePos := pos.Side(face)
		side := tx.Block(sidePos)
		// A wall can only connect to a block if the side is solid, with the only exception being thin blocks (such as
		// glass panes and iron bars) as well as the sides of fence gates.
		connected := side.Model().FaceSolid(sidePos, face.Opposite(), tx)
		if !connected {
			if _, ok := tx.Block(sidePos).(Wall); ok {
				connected = true
			} else if gate, ok := tx.Block(sidePos).(WoodFenceGate); ok {
				connected = gate.Facing.Face().Axis() != face.Axis()
			} else if _, ok := tx.Block(sidePos).Model().(model.Thin); ok {
				connected = true
			}
		}
		var connectionType WallConnectionType
		if connected {
			// If the wall is connected to the side, it has the possibility of having a tall connection. This is
			// calculated by checking for any overlapping blocks in the area of the connection.
			connectionType = ShortWallConnection()
			boxes := above.Model().BBox(abovePos, tx)
			for _, bb := range boxes {
				if bb.Min().Y() == 0 {
					xOverlap := bb.Min().X() < 0.75 && bb.Max().X() > 0.25
					zOverlap := bb.Min().Z() < 0.75 && bb.Max().Z() > 0.25
					var tall bool
					switch face {
					case cube.FaceNorth:
						tall = xOverlap && bb.Max().Z() > 0.75
					case cube.FaceEast:
						tall = bb.Min().X() < 0.25 && zOverlap
					case cube.FaceSouth:
						tall = xOverlap && bb.Min().Z() < 0.25
					case cube.FaceWest:
						tall = bb.Max().X() > 0.75 && zOverlap
					}
					if tall {
						connectionType = TallWallConnection()
						break
					}
				}
			}

		}
		if w.ConnectionType(face.Direction()) != connectionType {
			updated = true
			w = w.WithConnectionType(face.Direction(), connectionType)
		}
	}
	return w, updated
}

// calculatePost calculates the correct post bit for the wall at a given position in a world. The updated wall is
// returned and a bool to determine if any changes were made.
func (w Wall) calculatePost(tx *world.Tx, pos cube.Pos) (Wall, bool) {
	var updated bool
	abovePos := pos.Add(cube.Pos{0, 1, 0})
	above := tx.Block(abovePos)
	connections := len(sliceutil.Filter(cube.HorizontalFaces(), func(face cube.Face) bool {
		return w.ConnectionType(face.Direction()) != NoWallConnection()
	}))
	var post bool
	switch above := above.(type) {
	case Lantern:
		// Lanterns only make a wall become a post when placed on the wall and not hanging from above.
		post = !above.Hanging
	case Sign:
		// Signs only make a wall become a post when placed on the wall and not placed on the side of a block.
		post = !above.Attach.hanging
	case Torch:
		// Torches only make a wall become a post when placed on the wall and not placed on the side of a block.
		post = above.Facing == cube.FaceDown
	case WoodTrapdoor:
		// Trapdoors only make a wall become a post when they are opened and not closed and above a connection.
		if above.Open {
			switch above.Facing {
			case cube.North:
				post = w.NorthConnection != NoWallConnection()
			case cube.East:
				post = w.EastConnection != NoWallConnection()
			case cube.South:
				post = w.SouthConnection != NoWallConnection()
			case cube.West:
				post = w.WestConnection != NoWallConnection()
			}
		}
	case Wall:
		// A wall only make a wall become a post if it is a post itself.
		post = above.Post
	}
	if !post {
		// If a wall has two connections that are in different axis then it becomes a post regardless of the above block.
		if connections < 2 {
			post = true
		} else {
			switch {
			case w.NorthConnection != NoWallConnection() && w.SouthConnection != NoWallConnection():
				post = w.EastConnection != NoWallConnection() || w.WestConnection != NoWallConnection()
			case w.EastConnection != NoWallConnection() && w.WestConnection != NoWallConnection():
				post = w.NorthConnection != NoWallConnection() || w.SouthConnection != NoWallConnection()
			default:
				post = true
			}
		}
	}
	if w.Post != post {
		updated = true
		w.Post = post
	}
	return w, updated
}

// allWalls returns a list of all wall types.
func allWalls() (walls []world.Block) {
	for _, block := range WallBlocks() {
		if _, hash := block.Hash(); hash > math.MaxUint16 {
			name, _ := block.EncodeBlock()
			panic(fmt.Errorf("hash of block %s exceeds 16 bytes", name))
		}
		for _, north := range WallConnectionTypes() {
			for _, east := range WallConnectionTypes() {
				for _, south := range WallConnectionTypes() {
					for _, west := range WallConnectionTypes() {
						walls = append(walls, Wall{
							Block:           block,
							NorthConnection: north,
							EastConnection:  east,
							SouthConnection: south,
							WestConnection:  west,
							Post:            false,
						})
						walls = append(walls, Wall{
							Block:           block,
							NorthConnection: north,
							EastConnection:  east,
							SouthConnection: south,
							WestConnection:  west,
							Post:            true,
						})
					}
				}
			}
		}
	}
	return
}
