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/nbtconv"
	"github.com/df-mc/dragonfly/server/item"
	"github.com/df-mc/dragonfly/server/item/inventory"
	"github.com/df-mc/dragonfly/server/world"
	"github.com/go-gl/mathgl/mgl64"
	"strings"
	"sync"
)

// Hopper is a low-capacity storage block that can be used to collect item entities directly above it, as well as to
// transfer items into and out of other containers.
type Hopper struct {
	transparent
	sourceWaterDisplacer

	// Facing is the direction the hopper is facing.
	Facing cube.Face
	// Powered is whether the hopper is powered or not. If the hopper is powered it will be locked and will stop
	// moving items into or out of itself.
	Powered bool
	// CustomName is the custom name of the hopper. This name is displayed when the hopper is opened, and may include
	// colour codes.
	CustomName string

	// LastTick is the last world tick that the hopper was ticked.
	LastTick int64
	// TransferCooldown is the duration in ticks until the hopper can transfer items again.
	TransferCooldown int64
	// CollectCooldown is the duration in ticks until the hopper can collect items again.
	CollectCooldown int64

	inventory *inventory.Inventory
	viewerMu  *sync.RWMutex
	viewers   map[ContainerViewer]struct{}
}

// NewHopper creates a new initialised hopper. The inventory is properly initialised.
func NewHopper() Hopper {
	m := new(sync.RWMutex)
	v := make(map[ContainerViewer]struct{}, 1)
	return Hopper{
		inventory: inventory.New(5, func(slot int, _, item item.Stack) {
			m.RLock()
			defer m.RUnlock()
			for viewer := range v {
				viewer.ViewSlotChange(slot, item)
			}
		}),
		viewerMu: m,
		viewers:  v,
	}
}

// Model ...
func (Hopper) Model() world.BlockModel {
	return model.Hopper{}
}

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

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

// BreakInfo ...
func (h Hopper) BreakInfo() BreakInfo {
	return newBreakInfo(3, pickaxeHarvestable, pickaxeEffective, oneOf(Hopper{})).withBlastResistance(24).withBreakHandler(func(pos cube.Pos, tx *world.Tx, u item.User) {
		for _, i := range h.Inventory(tx, pos).Clear() {
			dropItem(tx, i, pos.Vec3())
		}
	})
}

// Inventory returns the inventory of the hopper.
func (h Hopper) Inventory(*world.Tx, cube.Pos) *inventory.Inventory {
	return h.inventory
}

// WithName returns the hopper after applying a specific name to the block.
func (h Hopper) WithName(a ...any) world.Item {
	h.CustomName = strings.TrimSuffix(fmt.Sprintln(a...), "\n")
	return h
}

// AddViewer adds a viewer to the hopper, so that it is updated whenever the inventory of the hopper is changed.
func (h Hopper) AddViewer(v ContainerViewer, _ *world.Tx, _ cube.Pos) {
	h.viewerMu.Lock()
	defer h.viewerMu.Unlock()
	h.viewers[v] = struct{}{}
}

// RemoveViewer removes a viewer from the hopper, so that slot updates in the inventory are no longer sent to it.
func (h Hopper) RemoveViewer(v ContainerViewer, _ *world.Tx, _ cube.Pos) {
	h.viewerMu.Lock()
	defer h.viewerMu.Unlock()
	delete(h.viewers, v)
}

// Activate ...
func (Hopper) Activate(pos cube.Pos, _ cube.Face, tx *world.Tx, u item.User, _ *item.UseContext) bool {
	if opener, ok := u.(ContainerOpener); ok {
		opener.OpenBlockContainer(pos, tx)
		return true
	}
	return false
}

// UseOnBlock ...
func (h Hopper) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx *world.Tx, user item.User, ctx *item.UseContext) bool {
	pos, _, used := firstReplaceable(tx, pos, face, h)
	if !used {
		return false
	}

	//noinspection GoAssignmentToReceiver
	h = NewHopper()
	h.Facing = cube.FaceDown
	if h.Facing != face {
		h.Facing = face.Opposite()
	}

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

// Tick ...
func (h Hopper) Tick(currentTick int64, pos cube.Pos, tx *world.Tx) {
	h.TransferCooldown--
	h.CollectCooldown--
	h.LastTick = currentTick

	if !h.Powered && h.TransferCooldown <= 0 {
		inserted := h.insertItem(pos, tx)
		extracted := h.extractItem(pos, tx)
		if inserted || extracted {
			h.TransferCooldown = 8
		}
	}

	tx.SetBlock(pos, h, nil)
}

// HopperInsertable represents a block that can have its contents inserted into by a hopper.
type HopperInsertable interface {
	// InsertItem handles the insert logic for that block.
	InsertItem(h Hopper, pos cube.Pos, tx *world.Tx) bool
}

// insertItem inserts an item into a block that can receive contents from the hopper.
func (h Hopper) insertItem(pos cube.Pos, tx *world.Tx) bool {
	destPos := pos.Side(h.Facing)
	dest := tx.Block(destPos)

	if e, ok := dest.(HopperInsertable); ok {
		return e.InsertItem(h, destPos, tx)
	}

	if container, ok := dest.(Container); ok {
		for sourceSlot, sourceStack := range h.inventory.Slots() {
			if sourceStack.Empty() {
				continue
			}

			_, err := container.Inventory(tx, pos).AddItem(sourceStack.Grow(-sourceStack.Count() + 1))
			if err != nil {
				// The destination is full.
				return false
			}

			_ = h.inventory.SetItem(sourceSlot, sourceStack.Grow(-1))

			if hopper, ok := dest.(Hopper); ok {
				hopper.TransferCooldown = 8
				tx.SetBlock(destPos, hopper, nil)
			}

			return true
		}
	}
	return false
}

// HopperExtractable represents a block that can have its contents extracted by a hopper.
type HopperExtractable interface {
	// ExtractItem handles the extract logic for that block.
	ExtractItem(h Hopper, pos cube.Pos, tx *world.Tx) bool
}

// extractItem extracts an item from a container into the hopper.
func (h Hopper) extractItem(pos cube.Pos, tx *world.Tx) bool {
	originPos := pos.Side(cube.FaceUp)
	origin := tx.Block(originPos)

	if e, ok := origin.(HopperExtractable); ok {
		return e.ExtractItem(h, originPos, tx)
	}

	if containerOrigin, ok := origin.(Container); ok {
		for slot, stack := range containerOrigin.Inventory(tx, originPos).Slots() {
			if stack.Empty() {
				// We don't have any items to extract.
				continue
			}

			_, err := h.inventory.AddItem(stack.Grow(-stack.Count() + 1))
			if err != nil {
				// The hopper is full.
				continue
			}

			_ = containerOrigin.Inventory(tx, originPos).SetItem(slot, stack.Grow(-1))

			if hopper, ok := origin.(Hopper); ok {
				hopper.TransferCooldown = 8
				tx.SetBlock(originPos, hopper, nil)
			}

			return true
		}
	}
	return false
}

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

// EncodeBlock ...
func (h Hopper) EncodeBlock() (string, map[string]any) {
	return "minecraft:hopper", map[string]any{
		"facing_direction": int32(h.Facing),
		"toggle_bit":       h.Powered,
	}
}

// EncodeNBT ...
func (h Hopper) EncodeNBT() map[string]any {
	if h.inventory == nil {
		facing, powered, customName := h.Facing, h.Powered, h.CustomName
		//noinspection GoAssignmentToReceiver
		h = NewHopper()
		h.Facing, h.Powered, h.CustomName = facing, powered, customName
	}
	m := map[string]any{
		"Items":            nbtconv.InvToNBT(h.inventory),
		"TransferCooldown": int32(h.TransferCooldown),
		"id":               "Hopper",
	}
	if h.CustomName != "" {
		m["CustomName"] = h.CustomName
	}
	return m
}

// DecodeNBT ...
func (h Hopper) DecodeNBT(data map[string]any) any {
	facing, powered := h.Facing, h.Powered
	//noinspection GoAssignmentToReceiver
	h = NewHopper()
	h.Facing = facing
	h.Powered = powered
	h.CustomName = nbtconv.String(data, "CustomName")
	h.TransferCooldown = int64(nbtconv.Int32(data, "TransferCooldown"))
	nbtconv.InvFromNBT(h.inventory, nbtconv.Slice(data, "Items"))
	return h
}

// allHoppers ...
func allHoppers() (hoppers []world.Block) {
	for _, f := range cube.Faces() {
		hoppers = append(hoppers, Hopper{Facing: f})
		hoppers = append(hoppers, Hopper{Facing: f, Powered: true})
	}
	return hoppers
}
