package world

import (
	"log/slog"
	"math/rand/v2"
	"time"
)

type blockRegistrySetter interface {
	// SetBlockRegistry updates the registry used by the provider to encode and decode blocks.
	// Config.New calls it with Config.Blocks after applying the default registry and finalizing it.
	SetBlockRegistry(BlockRegistry)
}

// Config may be used to create a new World. It holds a variety of fields that
// influence the World.
type Config struct {
	// Log is the Logger that will be used to log errors and debug messages to.
	// If set to nil, slog.Default() is set.
	Log *slog.Logger
	// Dim is the Dimension of the World. If set to nil, the World will use
	// Overworld as its dimension. The dimension set here influences, among
	// others, the sky colour, weather/time and liquid behaviour in that World.
	Dim Dimension
	// PortalDestination is a function that returns the destination World for a
	// portal of a specific Dimension type. If set to nil, no portals will
	// function. If the function returns a nil world for a Dimension, only
	// portals of that specific Dimension type will not function.
	PortalDestination func(dim Dimension) *World
	// Provider is the Provider implementation used to read and write World
	// data. If set to nil, the Provider used will be NopProvider, which does
	// not store any data to disk.
	Provider Provider
	// Generator is the Generator implementation used to generate new areas of
	// the World. If set to nil, the Generator used will be NopGenerator, which
	// generates completely empty chunks.
	Generator Generator
	// ReadOnly specifies if the World should be read-only, meaning no new data
	// will be written to the Provider.
	ReadOnly bool
	// SaveInterval specifies how often a World should be automatically saved to
	// disk. This includes chunks, entities and level.dat data. If ReadOnly is
	// set to false, changing SaveInterval will have no effect.
	// By default, SaveInterval is set to 10 minutes. Setting SaveInterval to
	// a negative number disables automatic saving entirely.
	SaveInterval time.Duration
	// ChunkUnloadInterval specifies how often unused chunks should be unloaded
	// from memory when no longer in use. By default, this is set to 2 minutes.
	// ChunkUnloadInterval should not be used to prevent chunks from unloading
	// altogether. This should be done using a Loader with a custom Viewer.
	ChunkUnloadInterval time.Duration
	// RandomTickSpeed specifies the rate at which blocks should be ticked in
	// the World. By default, each sub chunk has 3 blocks randomly ticked per
	// sub chunk, so the default value is 3. Setting this value to -1 or lower
	// will stop random ticking altogether, while setting it higher results in
	// faster ticking.
	RandomTickSpeed int
	// RandSource is the rand.Source used for generation of random numbers in a
	// World, such as when selecting blocks to tick or when deciding where to
	// strike lightning. If set to nil, RandSource defaults to a `rand.PCG`
	// source seeded with `time.Now().UnixNano()`. PCG is significantly faster
	// than `rand.ChaCha8` on 64-bit systems at the expense of poorer
	// statistical distribution, which is acceptable here.
	// See https://go.dev/blog/chacha8rand.
	RandSource rand.Source
	// Entities is an EntityRegistry with all Entity types registered that may
	// be added to the World.
	Entities EntityRegistry

	// Blocks is the BlockRegistry used by the World.
	// If left nil, DefaultBlockRegistry is used. For a non-default registry,
	// use NewBlockRegistry(), register blocks/states, and call Finalize().
	Blocks BlockRegistry

	// Synchronous makes the World run without any background goroutines.
	// Transactions from World.Exec run on the calling goroutine, the World is
	// not saved or unloaded automatically, and time only passes on explicit
	// World.AdvanceTick calls. This makes Synchronous Worlds deterministic and
	// well suited to unit tests that need a World to interact with.
	// A Synchronous World must be driven from one goroutine. Exec and
	// AdvanceTick are not safe to call concurrently, including from delayed
	// item or death callbacks.
	Synchronous bool
}

// New creates a new World using the Config conf. The World returned will start
// ticking as soon as a viewer is added to it and is otherwise ready for use.
func (conf Config) New() *World {
	if conf.Log == nil {
		conf.Log = slog.Default()
	}
	if conf.Dim == nil {
		conf.Dim = Overworld
	}
	if conf.SaveInterval == 0 {
		conf.SaveInterval = time.Minute * 10
	}
	if conf.ChunkUnloadInterval <= 0 {
		conf.ChunkUnloadInterval = time.Minute * 2
	}
	if conf.Generator == nil {
		conf.Generator = NopGenerator{}
	}
	if conf.Provider == nil {
		// If no provider is set, use the default settings and the default spawn position from the generator.
		s := defaultSettings()
		s.Spawn = conf.Generator.DefaultSpawn(conf.Dim)
		conf.Provider = NopProvider{Set: s}
	}
	if conf.RandomTickSpeed == 0 {
		conf.RandomTickSpeed = 3
	}
	if conf.Blocks == nil {
		conf.Blocks = DefaultBlockRegistry
	}

	// Initialize the passed block registry and also initialize the default block registry which
	// is used in some vanilla paths.
	conf.Blocks.Finalize()
	DefaultBlockRegistry.Finalize()
	if provider, ok := conf.Provider.(blockRegistrySetter); ok {
		provider.SetBlockRegistry(conf.Blocks)
	}

	if conf.RandSource == nil {
		t := uint64(time.Now().UnixNano())
		conf.RandSource = rand.NewPCG(t, t)
	}
	s := conf.Provider.Settings()
	w := &World{
		scheduledUpdates: newScheduledTickQueue(s.CurrentTick),
		entities:         make(map[*EntityHandle]ChunkPos),
		viewers:          make(map[*Loader]Viewer),
		chunks:           make(map[ChunkPos]*Column),
		queueClosing:     make(chan struct{}),
		closing:          make(chan struct{}),
		queue:            make(chan transaction, 128),
		r:                rand.New(conf.RandSource),
		advance:          s.ref.Add(1) == 1,
		conf:             conf,
		ra:               conf.Dim.Range(),
		set:              s,
	}
	w.weather = weather{w: w}
	var h Handler = NopHandler{}
	w.handler.Store(&h)

	t := ticker{interval: time.Second / 20}
	if !conf.Synchronous {
		w.queueing.Add(1)
		w.running.Add(2)

		go t.tickLoop(w)
		go w.autoSave()
		go w.handleTransactions()
	}

	<-w.Exec(t.tick)
	return w
}
