package atlas

import (
	"fmt"
	"log"
	"math/rand"

	"github.com/mdiluz/rove/pkg/maths"
	"github.com/mdiluz/rove/pkg/vector"
)

// Chunk represents a fixed square grid of tiles
type Chunk struct {
	// Tiles represents the tiles within the chunk
	Tiles []byte `json:"tiles"`
}

// Atlas represents a grid of Chunks
type Atlas struct {
	// Chunks represents all chunks in the world
	// This is intentionally not a 2D array so it can be expanded in all directions
	Chunks []Chunk `json:"chunks"`

	// size is the current width/height of the given atlas
	Size int `json:"size"`

	// ChunkSize is the dimensions of each chunk
	ChunkSize int `json:"chunksize"`
}

// NewAtlas creates a new empty atlas
func NewAtlas(size, chunkSize int) Atlas {
	if size%2 != 0 {
		log.Fatal("atlas size must always be even")
	}

	a := Atlas{
		Size:      size,
		Chunks:    make([]Chunk, size*size),
		ChunkSize: chunkSize,
	}

	// Initialise all the chunks
	for i := range a.Chunks {
		a.Chunks[i] = Chunk{
			Tiles: make([]byte, chunkSize*chunkSize),
		}
	}

	return a
}

// SpawnRocks peppers the world with rocks
func (a *Atlas) SpawnRocks() error {
	extent := a.ChunkSize * (a.Size / 2)

	// Pepper the current world with rocks
	for i := -extent; i < extent; i++ {
		for j := -extent; j < extent; j++ {
			if rand.Intn(16) == 0 {
				if err := a.SetTile(vector.Vector{X: i, Y: j}, TileRock); err != nil {
					return err
				}
			}
		}
	}

	return nil
}

// SpawnWalls spawns the around the world
func (a *Atlas) SpawnWalls() error {
	extent := a.ChunkSize * (a.Size / 2)

	// Surround the atlas in walls
	for i := -extent; i < extent; i++ {

		if err := a.SetTile(vector.Vector{X: i, Y: extent - 1}, TileWall); err != nil { // N
			return err
		} else if err := a.SetTile(vector.Vector{X: extent - 1, Y: i}, TileWall); err != nil { // E
			return err
		} else if err := a.SetTile(vector.Vector{X: i, Y: -extent}, TileWall); err != nil { // S
			return err
		} else if err := a.SetTile(vector.Vector{X: -extent, Y: i}, TileWall); err != nil { // W
			return err
		}
	}

	return nil
}

// SetTile sets an individual tile's kind
func (a *Atlas) SetTile(v vector.Vector, tile byte) error {
	chunk := a.toChunk(v)
	if chunk >= len(a.Chunks) {
		return fmt.Errorf("location outside of allocated atlas")
	}

	local := a.toChunkLocal(v)
	tileId := local.X + local.Y*a.ChunkSize
	if tileId >= len(a.Chunks[chunk].Tiles) {
		return fmt.Errorf("location outside of allocated chunk")
	}
	a.Chunks[chunk].Tiles[tileId] = tile
	return nil
}

// GetTile will return an individual tile
func (a *Atlas) GetTile(v vector.Vector) (byte, error) {
	chunk := a.toChunk(v)
	if chunk >= len(a.Chunks) {
		return 0, fmt.Errorf("location outside of allocated atlas")
	}

	local := a.toChunkLocal(v)
	tileId := local.X + local.Y*a.ChunkSize
	if tileId >= len(a.Chunks[chunk].Tiles) {
		return 0, fmt.Errorf("location outside of allocated chunk")
	}

	return a.Chunks[chunk].Tiles[tileId], nil
}

// toChunkLocal gets a chunk local coordinate for a tile
func (a *Atlas) toChunkLocal(v vector.Vector) vector.Vector {
	return vector.Vector{X: maths.Pmod(v.X, a.ChunkSize), Y: maths.Pmod(v.Y, a.ChunkSize)}
}

// GetChunkID gets the chunk ID for a position in the world
func (a *Atlas) toChunk(v vector.Vector) int {
	local := a.toChunkLocal(v)
	// Get the chunk origin itself
	origin := v.Added(local.Negated())
	// Divided it by the number of chunks
	origin = origin.Divided(a.ChunkSize)
	// Shift it by our size (our origin is in the middle)
	origin = origin.Added(vector.Vector{X: a.Size / 2, Y: a.Size / 2})
	// Get the ID based on the final values
	return (a.Size * origin.Y) + origin.X
}

// chunkOrigin gets the chunk origin for a given chunk index
func (a *Atlas) chunkOrigin(chunk int) vector.Vector {
	v := vector.Vector{
		X: maths.Pmod(chunk, a.Size) - (a.Size / 2),
		Y: (chunk / a.Size) - (a.Size / 2),
	}

	return v.Multiplied(a.ChunkSize)
}

// GetWorldExtent gets the min and max valid coordinates of world
func (a *Atlas) GetWorldExtents() (min, max vector.Vector) {
	min = vector.Vector{
		X: -(a.Size / 2) * a.ChunkSize,
		Y: -(a.Size / 2) * a.ChunkSize,
	}
	max = vector.Vector{
		X: -min.X - 1,
		Y: -min.Y - 1,
	}
	return
}

// Grow will return a grown copy of the current atlas
func (a *Atlas) Grow(size int) error {
	if size%2 != 0 {
		return fmt.Errorf("atlas size must always be even")
	}
	delta := size - a.Size
	if delta < 0 {
		return fmt.Errorf("cannot shrink an atlas")
	} else if delta == 0 {
		return nil
	}

	// Create a new atlas
	newAtlas := NewAtlas(size, a.ChunkSize)

	// Copy old chunks into new chunks
	for index, chunk := range a.Chunks {
		// Calculate the new chunk location and copy over the data
		newAtlas.Chunks[newAtlas.toChunk(a.chunkOrigin(index))] = chunk
	}

	// Copy the new atlas data into this one
	*a = newAtlas

	// Return the new atlas
	return nil
}
