class_name Crate extends StorageDestination
## Furniture container entity: holds up to CAPACITY item stacks and can be
## filtered like a StockpileZone. Built via Build → Furniture → Crate.
##
## Lifecycle:
##   - Ghost phase: placed but not yet built (build_progress < BUILD_TICKS).
##     accepts() returns false; visual is 40% alpha.
##   - Completed phase: _completed == true; accepts items up to CAPACITY.
##
## StorageDestination interface:
##   accepts()           — filter + capacity gate; false while ghost.
##   find_drop_position()— returns crate's own tile when room exists,
##                         Vector2i(-1, -1) when full or ghost.
##   covers_tile()       — single-tile container; only the crate's own tile.
##
## BuildJob interface (mirrors Wall.on_build_tick pattern):
##   is_buildable()      — true while still a ghost.
##   on_build_tick()     — increments build_progress; completes at BUILD_TICKS.
##   is_completed()      — true once built.
##
## register_item() is called by JobRunner._tick_deposit (Opus integration
## follow-up) after the deposit physically lands on the crate tile.
##
## See docs/architecture.md "Container" and Phase 5 implementation plan.

## Maximum item stacks this crate can hold.
const CAPACITY: int = 4

## Number of sim ticks a pawn must spend building to complete the crate.
const BUILD_TICKS: int = 60

## Pixel size of one tile — must match World.TILE_SIZE_PX.
const TILE_SIZE_PX: int = 16

# ── visual constants ──────────────────────────────────────────────────────────

## Body dimensions in pixels (centred on the tile).
const _BODY_W: int = 12
const _BODY_H: int = 10

## Crate colours.
const _COLOR_BODY: Color    = Color(0.45, 0.30, 0.15, 1.0)
const _COLOR_OUTLINE: Color = Color(0.25, 0.18, 0.08, 1.0)
const _COLOR_SLAT: Color    = Color(0.30, 0.20, 0.10, 1.0)
const _COLOR_FILL_DOT: Color = Color(1.0, 1.0, 1.0, 0.9)

## Ghost alpha multiplier when not yet built.
const _GHOST_ALPHA: float = 0.40

# ── exports ───────────────────────────────────────────────────────────────────

## Tile position of this crate in world-tile coordinates.
@export var tile: Vector2i = Vector2i.ZERO

## Player-facing label (inspect UI, Phase 17).
@export var label_text: String = "Crate"

# ── state ─────────────────────────────────────────────────────────────────────

## Sim ticks of construction work applied so far.
var build_progress: int = 0

## True once build_progress >= BUILD_TICKS.
var _completed: bool = false

## Live item nodes currently stored in this crate; capped at CAPACITY.
## Populated by register_item() (called from JobRunner._tick_deposit).
var _contents: Array = []


# ── lifecycle ─────────────────────────────────────────────────────────────────

func _ready() -> void:
	# Inherits StorageDestination defaults for priority / accepted_types.
	# Crates default to NORMAL priority and wildcard (accepts all types).
	priority = StorageDestination.Priority.NORMAL
	accepted_types = []
	# Register with the World stockpile pool so HaulingProvider sees us.
	World.register_stockpile(self)
	World.register_build_site(self)
	queue_redraw()


func _exit_tree() -> void:
	World.unregister_stockpile(self)
	World.unregister_build_site(self)


## One-shot initialiser called by the spawning / placement code.
## Sets tile and snaps pixel position to the tile centre.
func setup(p_tile: Vector2i) -> void:
	tile = p_tile
	position = Vector2(
		tile.x * TILE_SIZE_PX + TILE_SIZE_PX / 2.0,
		tile.y * TILE_SIZE_PX + TILE_SIZE_PX / 2.0
	)


# ── StorageDestination interface ──────────────────────────────────────────────

## Returns true if this crate can accept `item` right now.
## False while still a ghost, false if the filter rejects the type,
## false if all CAPACITY slots are taken.
func accepts(item) -> bool:
	if not _completed:
		return false
	if not _filter_accepts(item):
		return false
	return _contents.size() < CAPACITY


## Returns the crate's own tile when it can accept `item`, otherwise (-1,-1).
## All items stack into the crate's single tile — there is no 2D region.
func find_drop_position(item) -> Vector2i:
	if accepts(item):
		return tile
	return Vector2i(-1, -1)


## Returns true only when `p_tile` is exactly the crate's own tile.
func covers_tile(p_tile: Vector2i) -> bool:
	return p_tile == tile


# ── BuildJob interface ────────────────────────────────────────────────────────

## True while the crate has not yet been fully built.
func is_buildable() -> bool:
	return not _completed


## Returns the player-visible name for build-order and inspect UI.
func label() -> String:
	return label_text


## Called once per sim tick while a Construction pawn is working on this crate.
## Advances build_progress; completes the crate once BUILD_TICKS is reached.
func on_build_tick() -> void:
	if _completed:
		return
	build_progress += 1
	if build_progress >= BUILD_TICKS:
		_completed = true
		emit_signal("contents_changed")
		World.clear_designation_at(tile)
		Audit.log("crate", "built at %s (capacity %d)" % [tile, CAPACITY])
	queue_redraw()


## True once the crate has been fully built.
func is_completed() -> bool:
	return _completed


# ── inventory hooks ───────────────────────────────────────────────────────────

## Called from JobRunner._tick_deposit (Opus integration) after the item
## physically lands on the crate tile.
## Defensive: skips duplicates and over-capacity inserts (HaulingProvider may
## race ahead of capacity checks in edge cases).
func register_item(item) -> void:
	if not _completed:
		return
	if _contents.has(item) or _contents.size() >= CAPACITY:
		return
	_contents.append(item)
	emit_signal("contents_changed")


## Called when an item is removed from the crate (picked up by a pawn or via
## the Empty operation in Phase 17 inspect UI).
func unregister_item(item) -> void:
	_contents.erase(item)
	emit_signal("contents_changed")


# ── save / load ───────────────────────────────────────────────────────────────

## Serialise crate state for World save (Phase 16 will wire this).
func to_dict() -> Dictionary:
	return {
		"class_id": &"crate",
		"tile_x": tile.x,
		"tile_y": tile.y,
		"label_text": label_text,
		"build_progress": build_progress,
		"completed": _completed,
		"priority": int(priority),
		"accepted_types": accepted_types.map(func(t): return String(t)),
	}


## Restore from a dict produced by to_dict().
## Item content refs are reconnected by World.load_crates() after all items
## are spawned (Phase 16); _contents starts empty here.
func from_dict(d: Dictionary) -> void:
	tile = Vector2i(int(d.get("tile_x", 0)), int(d.get("tile_y", 0)))
	label_text = d.get("label_text", "Crate")
	build_progress = int(d.get("build_progress", 0))
	_completed = bool(d.get("completed", false))
	priority = d.get("priority", StorageDestination.Priority.NORMAL) as StorageDestination.Priority
	var raw_types: Array = d.get("accepted_types", [])
	accepted_types.clear()
	for s in raw_types:
		accepted_types.append(StringName(s))
	setup(tile)


# ── render ─────────────────────────────────────────────────────────────────────

## Procedural crate graphic; no PNG dependency.
##
## Completed crate:
##   Brown 12×10 body with a darker 1-px outline, two horizontal slat bands.
##   Four 2×2 fill-indicator dots in the top-right corner — white dots equal
##   to _contents.size() are drawn; remaining dots are drawn at low alpha.
##
## Ghost (not yet built):
##   Same shapes at GHOST_ALPHA overall alpha.
func _draw() -> void:
	var alpha_scale: float = _GHOST_ALPHA if not _completed else 1.0

	var body_color  := Color(_COLOR_BODY.r,    _COLOR_BODY.g,    _COLOR_BODY.b,    _COLOR_BODY.a    * alpha_scale)
	var outline_col := Color(_COLOR_OUTLINE.r, _COLOR_OUTLINE.g, _COLOR_OUTLINE.b, _COLOR_OUTLINE.a * alpha_scale)
	var slat_color  := Color(_COLOR_SLAT.r,    _COLOR_SLAT.g,    _COLOR_SLAT.b,    _COLOR_SLAT.a    * alpha_scale)

	# Half-extents for centering.
	var hw: int = _BODY_W / 2   # 6
	var hh: int = _BODY_H / 2   # 5

	# Body fill.
	var body_rect := Rect2(Vector2(-hw, -hh), Vector2(_BODY_W, _BODY_H))
	draw_rect(body_rect, body_color, true)

	# Outline (1 px wide; draw_rect with false = border).
	draw_rect(body_rect, outline_col, false, 1.0)

	# Two horizontal slat bands — at ⅓ and ⅔ of the body height.
	# Each band is 1 px tall, inset 1 px from the sides.
	var slat_x_start: float = -hw + 1.0
	var slat_width: float   = float(_BODY_W - 2)
	var slat_y1: float      = -hh + float(_BODY_H) / 3.0 - 0.5
	var slat_y2: float      = -hh + float(_BODY_H) * 2.0 / 3.0 - 0.5

	draw_line(
		Vector2(slat_x_start, slat_y1),
		Vector2(slat_x_start + slat_width, slat_y1),
		slat_color, 1.0
	)
	draw_line(
		Vector2(slat_x_start, slat_y2),
		Vector2(slat_x_start + slat_width, slat_y2),
		slat_color, 1.0
	)

	# Fill-level indicator: 4 dots (2×2 px each) in the top-right corner,
	# arranged in a 2×2 grid. Dots up to _contents.size() are bright white;
	# the rest are dim (10% alpha).
	var dot_size: float  = 2.0
	var dot_gap: float   = 1.0
	var dot_origin := Vector2(float(hw) - 2.0 * dot_size - dot_gap - 1.0, float(-hh) + 1.0)

	for i in range(CAPACITY):
		var col_idx: int = i % 2
		var row_idx: int = i / 2
		var dot_x: float = dot_origin.x + col_idx * (dot_size + dot_gap)
		var dot_y: float = dot_origin.y + row_idx * (dot_size + dot_gap)
		var dot_rect := Rect2(Vector2(dot_x, dot_y), Vector2(dot_size, dot_size))
		var fill_alpha: float = (1.0 if i < _contents.size() else 0.15) * alpha_scale
		var dot_col := Color(_COLOR_FILL_DOT.r, _COLOR_FILL_DOT.g, _COLOR_FILL_DOT.b, fill_alpha)
		draw_rect(dot_rect, dot_col, true)
