package block

import (
	"fmt"
	"github.com/df-mc/dragonfly/server/block/cube"
	"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/df-mc/dragonfly/server/world/sound"
	"github.com/go-gl/mathgl/mgl64"
	"strings"
	"sync"
	"time"
)

// Barrel is a fisherman's job site block, used to store items. It functions like a single chest, although
// it requires no airspace above it to be opened.
type Barrel struct {
	solid
	bass

	// Facing is the direction that the barrel is facing.
	Facing cube.Face
	// Open is whether the barrel is open or not.
	Open bool
	// CustomName is the custom name of the barrel. This name is displayed when the barrel is opened, and may
	// include colour codes.
	CustomName string

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

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

// Inventory returns the inventory of the barrel. The size of the inventory will be 27.
func (b Barrel) Inventory(*world.Tx, cube.Pos) *inventory.Inventory {
	return b.inventory
}

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

// open opens the barrel, displaying the animation and playing a sound.
func (b Barrel) open(tx *world.Tx, pos cube.Pos) {
	b.Open = true
	tx.PlaySound(pos.Vec3Centre(), sound.BarrelOpen{})
	tx.SetBlock(pos, b, nil)
}

// close closes the barrel, displaying the animation and playing a sound.
func (b Barrel) close(tx *world.Tx, pos cube.Pos) {
	b.Open = false
	tx.PlaySound(pos.Vec3Centre(), sound.BarrelClose{})
	tx.SetBlock(pos, b, nil)
}

// AddViewer adds a viewer to the barrel, so that it is updated whenever the inventory of the barrel is changed.
func (b Barrel) AddViewer(v ContainerViewer, tx *world.Tx, pos cube.Pos) {
	b.viewerMu.Lock()
	defer b.viewerMu.Unlock()
	if len(b.viewers) == 0 {
		b.open(tx, pos)
	}
	b.viewers[v] = struct{}{}
}

// RemoveViewer removes a viewer from the barrel, so that slot updates in the inventory are no longer sent to
// it.
func (b Barrel) RemoveViewer(v ContainerViewer, tx *world.Tx, pos cube.Pos) {
	b.viewerMu.Lock()
	defer b.viewerMu.Unlock()
	if len(b.viewers) == 0 {
		return
	}
	delete(b.viewers, v)
	if len(b.viewers) == 0 {
		b.close(tx, pos)
	}
}

// Activate ...
func (b Barrel) 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 (b Barrel) 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, b)
	if !used {
		return
	}
	//noinspection GoAssignmentToReceiver
	b = NewBarrel()
	b.Facing = calculateFace(user, pos)

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

// BreakInfo ...
func (b Barrel) BreakInfo() BreakInfo {
	return newBreakInfo(2.5, alwaysHarvestable, axeEffective, oneOf(b)).withBreakHandler(func(pos cube.Pos, tx *world.Tx, u item.User) {
		for _, i := range b.Inventory(tx, pos).Clear() {
			dropItem(tx, i, pos.Vec3())
		}
	})
}

// FlammabilityInfo ...
func (b Barrel) FlammabilityInfo() FlammabilityInfo {
	return newFlammabilityInfo(0, 0, true)
}

// FuelInfo ...
func (Barrel) FuelInfo() item.FuelInfo {
	return newFuelInfo(time.Second * 15)
}

// DecodeNBT ...
func (b Barrel) DecodeNBT(data map[string]any) any {
	facing := b.Facing
	//noinspection GoAssignmentToReceiver
	b = NewBarrel()
	b.Facing = facing
	b.CustomName = nbtconv.String(data, "CustomName")
	nbtconv.InvFromNBT(b.inventory, nbtconv.Slice(data, "Items"))
	return b
}

// EncodeNBT ...
func (b Barrel) EncodeNBT() map[string]any {
	if b.inventory == nil {
		facing, customName := b.Facing, b.CustomName
		//noinspection GoAssignmentToReceiver
		b = NewBarrel()
		b.Facing, b.CustomName = facing, customName
	}
	m := map[string]any{
		"Items": nbtconv.InvToNBT(b.inventory),
		"id":    "Barrel",
	}
	if b.CustomName != "" {
		m["CustomName"] = b.CustomName
	}
	return m
}

// EncodeBlock ...
func (b Barrel) EncodeBlock() (string, map[string]any) {
	return "minecraft:barrel", map[string]any{"open_bit": boolByte(b.Open), "facing_direction": int32(b.Facing)}
}

// EncodeItem ...
func (b Barrel) EncodeItem() (name string, meta int16) {
	return "minecraft:barrel", 0
}

// allBarrels ...
func allBarrels() (b []world.Block) {
	for i := cube.Face(0); i < 6; i++ {
		b = append(b, Barrel{Facing: i})
		b = append(b, Barrel{Facing: i, Open: true})
	}
	return
}
