package world

import (
	"fmt"

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

// BlockRenderType is an enum describing the rendering process of a block
type BlockRenderType int

const (
	OneTexture      BlockRenderType = iota // Render with one texture of the same on all faces, "Name.png"
	ThreeTexture                           // Render with one texture on the top, one around the sides, and one on the bottom, "Name_top/side/bot.png,"
	SixTexture                             // Render with six different textures on six faces, "Name_x+/x-/y+/y-/z+/z-.png"
	CustomRendering                        // Rendering calls BlockAppearance.CustomRenderAppend()
)

// BlockAppearance describes basic appearance of a kind of block.
type BlockAppearance struct {
	Name        string // A short name, like "stone" or "dirt", used for texture lookups
	Transparent bool   // Is block transparent?
	NotSolid    bool   // Is block not solid, i.e., has no hitbox at all? (this makes the zero value reasonable)
	Light       int    // The light level it emits, 0 is none

	Hitbox []itype.Boxd // Hitbox, in block-local coordinates; empty slice means a default hitbox of 1x1x1

	RenderType BlockRenderType // Rendering type, defaults to OneTexture (zero value)

	// Called on render if RenderType == CustomRendering.
	//
	// Be sure to return vertexArray at the end of the function (in case it got reallocated)!!!!
	CustomRenderAppend func(
		position itype.Vec3i,
		aux int,
		data itype.Dataset,
		world *World,
		vertexArray []Vertex,
		vertexArrayWater []Vertex,
	) (verts []Vertex, waters []Vertex)
}

// BlockBehaviour describes a kind of block of the same Major ID.
type BlockBehaviour interface {

	// Static returns if the Behaviour is "static", i.e., the Appearance does not
	// change with position, Minor ID or Dataset. Static Behaviours are cached
	// by the renderer and only generated once.
	//
	// Static implies RequireDataset = false and RequireBlockUpdate = false.
	Static() bool

	// RequireDataset returns if the type of block requires a Dataset attached.
	RequireDataset() bool

	// RequireBlockUpdate return if BlockUpdate should be called if a neighboring
	// block has changed. Blocks not requiring BlockUpdate does not change at all.
	RequireBlockUpdate() bool

	// Appearance returns the Appearance of the block at global position Position,
	// with Minor ID aux, and Dataset data.
	//
	// If RequireDataset if false, data is nil.
	Appearance(position itype.Vec3i, aux int, data itype.Dataset, world *World) BlockAppearance

	// BlockUpdate is called when RequireBlockUpdate is true and the block at
	// global position Position, with Minor ID aux, and Dataset data has a neighbor
	// that changed state. A block will only be updated once in a tick.
	//
	// If RequireDataset if false, data is nil.
	//
	// Return true if this block also changed state, false otherwise.
	BlockUpdate(position itype.Vec3i, aux int, data itype.Dataset, world *World) bool
}

type blockBehaviourStatic struct {
	app BlockAppearance
}

func (blockBehaviourStatic) Static() bool             { return true }
func (blockBehaviourStatic) RequireDataset() bool     { return false }
func (blockBehaviourStatic) RequireBlockUpdate() bool { return false }
func (b blockBehaviourStatic) Appearance(position itype.Vec3i, aux int, data itype.Dataset, world *World) BlockAppearance {
	return b.app
}
func (blockBehaviourStatic) BlockUpdate(position itype.Vec3i, aux int, data itype.Dataset, world *World) bool {
	return false
}

// BlockBehaviourStatic returns a Static BlockBehaviour that has the given BlockAppearance.
func BlockBehaviourStatic(app BlockAppearance) BlockBehaviour {
	return blockBehaviourStatic{app: app}
}

var behaviour map[int]BlockBehaviour = make(map[int]BlockBehaviour)
var appearance map[int]BlockAppearance = make(map[int]BlockAppearance)
var behaviourDoneRegister bool

// RegisterBlockBehaviour registers behaviour with the given id.
//
// If the id is already taken, or id == 0, false is returned and nothing is done.
// Otherwise, true is returned and the block is registered.
func RegisterBlockBehaviour(id int, b BlockBehaviour) bool {
	if _, ok := behaviour[id]; behaviourDoneRegister || id == 0 || ok {
		return false
	}

	behaviour[id] = b
	return true
}

// DoneRegisteringBlockBehaviour is to be called after Registering BlockBehaviour,
// i.e., in Post-Init() initializations.
func DoneRegisteringBlockBehaviour() {
	for id, b := range behaviour {
		if b.Static() {
			appearance[id] = b.Appearance(itype.Vec3i{}, 0, nil, nil)
		}
	}

	behaviourDoneRegister = true
}

// GetBlockAppearance gets the block appearance of the given block in the fastest way possible.
func GetBlockAppearance(position itype.Vec3i, id, aux int, data itype.Dataset, world *World) BlockAppearance {
	if app, ok := appearance[id]; ok { // Cache
		if len(app.Hitbox) == 0 {
			app.Hitbox = []itype.Boxd{{
				OffX: 0, OffY: 0, OffZ: 0,
				SizeX: 1, SizeY: 1, SizeZ: 1,
			}}
		}
		return app
	}

	// Slow way
	b, ok := behaviour[id]
	if !ok {
		panic(fmt.Sprint("invalid block type ", id))
	}

	app := b.Appearance(position, aux, data, world)
	if len(app.Hitbox) == 0 {
		app.Hitbox = []itype.Boxd{{
			OffX: 0, OffY: 0, OffZ: 0,
			SizeX: 1, SizeY: 1, SizeZ: 1,
		}}
	}
	return app
}

// GetBlockBehaviour gets the block behaviour of the given id, or nil if not present.
func GetBlockBehaviour(id int) BlockBehaviour {
	return behaviour[id]
}

// Block is a structure to store and pass Blocks around.
type Block struct {
	Id, Aux   int
	Dataset   itype.Dataset
	Behaviour BlockBehaviour
	World     *World
}

// Appearance is a shortcut for Behaviour.Appearance().
// It returns the Appearance of the block with the given parameters.
func (b Block) Appearance(position itype.Vec3i) BlockAppearance {
	app := b.Behaviour.Appearance(position, b.Aux, b.Dataset, b.World)
	if !app.NotSolid && len(app.Hitbox) == 0 {
		app.Hitbox = []itype.Boxd{{
			OffX: 0, OffY: 0, OffZ: 0,
			SizeX: 1, SizeY: 1, SizeZ: 1,
		}}
	}
	return app
}

// BlockUpdate is a shortcut for Behaviour.BlockUpdate().
// It is called when RequireBlockUpdate is true and the block at
// global position Position, with Minor ID aux, and Dataset data has a neighbor
// that changed state. A block will only be updated once in a tick.
//
// If RequireDataset if false, data is nil.
//
// Return true if this block also changed state, false otherwise.
func (b Block) BlockUpdate(position itype.Vec3i) bool {
	return b.Behaviour.BlockUpdate(position, b.Aux, b.Dataset, b.World)
}