## A set of _rules usable by a single DisplayLayer.
class_name TerrainLayer
extends Resource


## A list of which CellNeighbors to care about during terrain checking.
var terrain_neighborhood: Array = []

##[br] When a cell in a DisplayLayer needs to be recomputed,
##     the TerrainLayer needs to know which tiles surround it.
##[br] This Array stores the paths from the affected cell to the neighboring world cells.
var display_to_world_neighborhood: Array

# TODO: change Mapping to support https://github.com/pablogila/TileMapDual/issues/13
##[br] The rules that dictate which tile matches a given set of neighbors.
##[br] Used by register_rule() and apply_rule()
##[codeblock]
##  _rules: TrieNode = # The actual type of _rules
##
##  TrieNode: Dictionary{
##    key: int = # The terrain value of this neighbor
##    value: TrieNode | TrieLeaf = # The next branch of this trie
##  }
## 
##  TrieLeaf: Dictionary{
##    'mapping': Mapping = # The tile that this should now become
##  }
##
##  Mapping: Dictionary{
##    'sid': int = # The source_id of this tile
##    'tile': Vector2i = # The Atlas Coordinates of this tile
##  }
##[/codeblock]
##[br] Internally a decision "trie":
##[br] - each node branch represents a terrain neighbor
##[br] - each leaf node represents the terrain that a tile
##       with the given terrain neighborhood should become
##[br] 
##[br] How the trie is searched:
##[br] - check the next neighbor in terrain_neighbors
##[br] - check if there is a branch corresponding to the terrain of that neighbor
##[br]   - if there is a branch, search again under that branch
##[br]   - else pretend that neighbor is empty and try again
##[br]   - if there really isn't a branch, no rules exist so just return empty
##[br] - once at a leaf node, its mapping should tell us what terrain to become
##[br]
##[br] See apply_rule() for more details.
var _rules: Dictionary = {}

## Generator for random placement of tiles
var rand : RandomNumberGenerator
## Random seed used to generate tiles deterministically
var global_seed : int = 707

func _init(fields: Dictionary) -> void:
	self.terrain_neighborhood = fields.terrain_neighborhood
	self.display_to_world_neighborhood = fields.display_to_world_neighborhood
	self.rand = RandomNumberGenerator.new()
	

## Register a new rule for a specific tile in an atlas.
func _register_tile(data: TileData, mapping: Dictionary) -> void:
	if data.terrain_set != 0:
		# This was already handled as an error in the parent TerrainDual
		return
	var terrain_neighbors := terrain_neighborhood.map(data.get_terrain_peering_bit)
	# Skip tiles with no peering bits in this filter
	# They might be used for a different layer,
	# or may have no peering bits at all, which will just be ignored by all layers
	if terrain_neighbors.any(func(neighbor): return neighbor == -1):
		if terrain_neighbors.any(func(neighbor): return neighbor != -1):
			push_warning(
				"Invalid Tile Neighborhood at %s.\n" % [mapping] +
				"Expected neighborhood: %s" % [terrain_neighborhood.map(Util.neighbor_name)]
			)
		return
	mapping["prob"] = data.probability
	_register_rule(terrain_neighbors, mapping)


## Register a new rule for a set of surrounding terrain neighbors
func _register_rule(terrain_neighbors: Array, mapping: Dictionary) -> void:
	var node := _rules
	for terrain in terrain_neighbors:
		if terrain not in node:
			node[terrain] = {}
		node = node[terrain]
	if not 'mappings' in node:
		node.mappings = []
	node.mappings.append(mapping)


const TILE_EMPTY: Dictionary = {'sid': - 1, 'tile': Vector2i(-1, -1)}
## Returns the tile that should be used based on the surrounding terrain neighbors
func apply_rule(terrain_neighbors: Array, cell: Vector2i) -> Dictionary:
	var is_empty := terrain_neighbors.all(func(terrain): return terrain == -1)
	if is_empty:
		return TILE_EMPTY
	var normalized_neighbors = terrain_neighbors.map(normalize_terrain)

	var node := _rules
	for terrain in normalized_neighbors:
		if terrain not in node:
			terrain = 0
		if terrain not in node:
			return TILE_EMPTY
		node = node[terrain]
	if 'mappings' not in node:
		return TILE_EMPTY
	
	#random tile selection
	var weights: Array= []
	var index: int = 0
	for mapping in node.mappings:
		weights.append(mapping.prob)
	rand.seed = hash(str(cell)+str(global_seed))
	index = rand.rand_weighted(weights)
	return node.mappings[index]


## Coerces all empty tiles to have a terrain of 0.
static func normalize_terrain(terrain):
	return terrain if terrain != -1 else 0


## Utility function for easier printing
func _neighbors_to_dict(terrain_neighbors: Array) -> Dictionary:
	return Util.arrays_to_dict(terrain_neighborhood.map(Util.neighbor_name), terrain_neighbors)
