extends RefCounted
## House + garden rules (docs/LIFE_SKILLS.md section 4, docs/HOUSING_V2.md), server side. Since Housing v2 every
## house is a plot of a community lane (server/housing.gd): commands work on the SENDER's own plot record, the player
## must stand on that plot, cells are plot-local (1 m grid, origin = plot centre, +z = towards the street).
## A piece {"f": furniture id, "c": [cx, cz], "r": 0..3} covers cells x in [cx, cx + W), z in [cz, cz + D), where
## W x D = the piece size, swapped for r = 1 / 3. Indoor pieces go inside the house (minus its walls / partitions),
## outdoor pieces in the yard; never on the door, the path, the garden beds or another piece.
## colliders(): the v1 Homestead walls (old map, kept for old saves) + every house of a community map.

const Life = preload("res://scripts/server/life.gd")
const Housing = preload("res://scripts/server/housing.gd")


static func H() -> Dictionary:
	return Life.housing()


static func home_map() -> String:
	return str(H().get("map", "homestead"))


static func furn(id: String) -> Dictionary:
	return H().get("furniture", {}).get(id, {})


## [W, D] cells of a piece after rotation.
static func dims(fid: String, r: int) -> Vector2i:
	var s: Array = furn(fid).get("size", [1, 1])
	var w := int(s[0])
	var d := int(s[1])
	return Vector2i(d, w) if r % 2 == 1 else Vector2i(w, d)


## Plot-local rect of a piece.
static func rect_of(fid: String, cell: Vector2i, r: int) -> Rect2:
	var dm := dims(fid, r)
	return Rect2(float(cell.x), float(cell.y), float(dm.x), float(dm.y))


## Plot-local centre of a piece.
static func center_of(fid: String, cell: Vector2i, r: int) -> Vector2:
	return rect_of(fid, cell, r).get_center()


static func _contains(outer: Rect2, inner: Rect2) -> bool:
	return inner.position.x >= outer.position.x - 0.001 and inner.position.y >= outer.position.y - 0.001 \
		and inner.end.x <= outer.end.x + 0.001 and inner.end.y <= outer.end.y + 0.001


static func _overlap(a: Rect2, b: Rect2) -> bool:
	return a.position.x < b.end.x - 0.001 and b.position.x < a.end.x - 0.001 and a.position.y < b.end.y - 0.001 and b.position.y < a.end.y - 0.001


## Rects nothing may cover (plot-local): door (inside + outside), the front path, garden beds, partitions.
static func keepouts(lv: int) -> Array:
	var hr := Housing.house_rect(lv)
	var out: Array = [[Rect2(-1.2, hr.end.y - 1.6, 2.4, 3.4), "the door"],
		[Rect2(-1.3, hr.end.y + 1.0, 2.6, Housing.plot_size() / 2.0 - hr.end.y), "the front path"]]
	for g in Housing.garden_plots(lv):
		var p: Vector2 = g
		out.append([Rect2(p.x - 1.25, p.y - 1.25, 2.5, 2.5), "a garden bed"])
	for pr in Housing.partitions(lv):
		out.append([pr, "a wall"])
	return out


## "" when the piece fits on record r at cell / rotation (skip = index of a piece being moved), else the reason.
static func fits_rec(r: Dictionary, fid: String, cell: Vector2i, rot: int, skip: int = -1) -> String:
	var fd := furn(fid)
	if fd.is_empty():
		return "That is not furniture."
	var lv := int(r.get("lv", 1))
	var rc := rect_of(fid, cell, rot)
	var place := str(fd.get("place", "in"))
	var inner := Housing.house_rect(lv).grow(-0.2)
	var inside := _contains(inner, rc)
	var outside := _contains(Housing.yard_rect(lv), rc) and not _overlap(rc, Housing.house_rect(lv).grow(0.4))
	if place == "in" and not inside:
		return "This piece goes inside the house."
	if place == "out" and not outside:
		return "This piece goes in the garden."
	if place == "any" and not (inside or outside):
		return "It has to be inside the house or in the garden."
	for k in keepouts(lv):
		if _overlap(rc, k[0]):
			return "Something is in the way (%s)." % str(k[1])
	var items: Array = r.get("items", [])
	for i in items.size():
		if i == skip:
			continue
		var it: Dictionary = items[i]
		if _overlap(rc, rect_of(str(it["f"]), Vector2i(int(it["c"][0]), int(it["c"][1])), int(it["r"]))):
			# P6: a rug / tatami (floor layer) lies under tables and seats
			if (str(fd.get("layer", "")) == "floor") != (str(furn(str(it["f"])).get("layer", "")) == "floor"):
				continue
			return "It overlaps your %s." % GameData.item_name(str(it["f"]))
	return ""


## Old callers (edit-mode ghost): the character's own record.
static func fits(c, fid: String, cell: Vector2i, r: int, skip: int = -1) -> String:
	return fits_rec(Housing.own_rec_client(c), fid, cell, r, skip)


static func _cell(v) -> Vector2i:
	if v is Array and (v as Array).size() >= 2:
		return Vector2i(int(v[0]), int(v[1]))
	if v is Vector2i:
		return v
	return Vector2i(99999, 99999)


# ---------------------------------------------------------------- commands

static func command(pe, t: String, cmd: Dictionary) -> Dictionary:
	var c = pe.ch
	Life.ensure(c)
	Housing.load_store(pe.server)
	var r := Housing.own_rec(c)
	if r.is_empty():
		return {"ok": false, "err": "You have no house yet - the Hearthvale Gate in Aldoria.  ·  ยังไม่มีบ้าน"}
	var comm := str(c.life["house"]["comm"])
	var pi := int(c.life["house"]["plot"])
	if pe.map == null or pe.map.map_id != comm or not Housing.on_plot(pi, pe.global_position):
		return {"ok": false, "err": "Go to your own house first (Hearthvale Lane %s, no. %d).  ·  ไปที่บ้านของคุณก่อน" % [comm.get_slice("_", 1), pi + 1]}
	var items: Array = r["items"]
	var out: Dictionary
	match t:
		"home_place":
			var fid := str(cmd.get("f", ""))
			var cell := _cell(cmd.get("cell", null))
			var rot := posmod(int(cmd.get("r", 0)), 4)
			var Home = load("res://scripts/server/home.gd")
			if Home.owned(c, fid) <= 0:
				return {"ok": false, "err": "You have no %s (bag or furniture storage)." % GameData.item_name(fid)}
			if int(furn(fid).get("lv", 1)) > int(r["lv"]):
				return {"ok": false, "err": "%s needs house level %d." % [GameData.item_name(fid), int(furn(fid).get("lv", 1))]}
			var cap := Housing.max_items(int(r["lv"]))
			if items.size() >= cap:
				return {"ok": false, "err": "Your house holds %d pieces at this level - level it up for more." % cap}
			var err := fits_rec(r, fid, cell, rot)
			if err != "":
				return {"ok": false, "err": err}
			Home.take_piece(c, fid)
			items.append({"f": fid, "c": [cell.x, cell.y], "r": rot})
			Home.progress(c, "place")
			out = {"ok": true, "i": items.size() - 1}
		"home_move":
			var i := int(cmd.get("i", -1))
			if i < 0 or i >= items.size():
				return {"ok": false, "err": "No such piece."}
			var fid2 := str(items[i]["f"])
			var cell2 := _cell(cmd.get("cell", null))
			var r2 := posmod(int(cmd.get("r", int(items[i]["r"]))), 4)
			var err2 := fits_rec(r, fid2, cell2, r2, i)
			if err2 != "":
				return {"ok": false, "err": err2}
			items[i] = {"f": fid2, "c": [cell2.x, cell2.y], "r": r2}
			load("res://scripts/server/home.gd").progress(c, "place")
			out = {"ok": true, "i": i}
		"home_remove":
			var i3 := int(cmd.get("i", -1))
			if i3 < 0 or i3 >= items.size():
				return {"ok": false, "err": "No such piece."}
			var fid3 := str(items[i3]["f"])
			items.remove_at(i3)
			# picked-up pieces go to the furniture storage, not the bag (server/home.gd)
			load("res://scripts/server/home.gd").store_piece(c, fid3)
			out = {"ok": true}
		"garden_plant":
			out = plant(pe, r, pi, int(cmd.get("plot", -1)), str(cmd.get("seed", "")))
		"garden_harvest":
			out = harvest(pe, r, pi, int(cmd.get("plot", -1)))
		"garden_water", "garden_pest":
			# Housing v2 P3 (server/garden2.gd)
			var k := int(cmd.get("plot", -1))
			var errw := _bed_check(pe, r, pi, k)
			if errw != "":
				return {"ok": false, "err": errw}
			var G = load("res://scripts/server/garden2.gd")
			out = G.water(pe, r, k) if t == "garden_water" else G.clear_pest(pe, r, k)
		_:
			return {"ok": false, "err": "unknown home command"}
	if bool(out.get("ok", false)):
		Housing.refresh_comfort(r, c)
		out["comfort"] = int(r["comfort"])
		c.changed.emit()
		Housing.changed(pe.server, comm, pi)
	return out


# ---------------------------------------------------------------- garden

static func crop_of(seed_id: String) -> Dictionary:
	return Life.garden().get("crops", {}).get(seed_id, {})


## Seconds a seed needs (life.json minutes x Life.grow_scale).
static func grow_time(seed_id: String) -> float:
	return float(crop_of(seed_id).get("min", 10)) * 60.0 * Life.grow_scale


## 0..1 growth of garden plot k of a record (0 when empty).
static func growth_rec(r: Dictionary, k: int) -> float:
	var g: Array = r.get("garden", [])
	if k < 0 or k >= g.size() or not (g[k] is Dictionary) or str(g[k].get("seed", "")) == "":
		return 0.0
	var p: Dictionary = g[k]
	return clampf((Life.unix() - float(p.get("t", 0.0))) / maxf(0.001, grow_time(str(p["seed"]))), 0.0, 1.0)


static func secs_left_rec(r: Dictionary, k: int) -> float:
	var g: Array = r.get("garden", [])
	if k < 0 or k >= g.size() or not (g[k] is Dictionary) or str(g[k].get("seed", "")) == "":
		return 0.0
	var p: Dictionary = g[k]
	return maxf(0.0, float(p.get("t", 0.0)) + grow_time(str(p["seed"])) - Life.unix())


## World position of garden plot k of plot pi at house level lv.
static func garden_world(pi: int, lv: int, k: int) -> Vector3:
	var gp: Array = Housing.garden_plots(lv)
	if k < 0 or k >= gp.size():
		return Vector3(99999, 0, 99999)
	var w := Housing.to_world(pi, gp[k])
	return Vector3(w.x, 0, w.y)


static func _bed_check(pe, r: Dictionary, pi: int, k: int) -> String:
	if k < 0 or k >= (r.get("garden", []) as Array).size():
		return "No such garden bed."
	if pe.flat_dist(garden_world(pi, int(r["lv"]), k)) > float(H().get("plot_reach", 3.0)) + float(H().get("plot_size", 2.2)) / 2.0:
		return "Walk up to the garden bed first."
	return ""


static func plant(pe, r: Dictionary, pi: int, k: int, seed_id: String) -> Dictionary:
	var c = pe.ch
	var err := _bed_check(pe, r, pi, k)
	if err != "":
		return {"ok": false, "err": err}
	if crop_of(seed_id).is_empty():
		return {"ok": false, "err": "That cannot be planted."}
	if c.count(seed_id) <= 0:
		return {"ok": false, "err": "You have no %s." % GameData.item_name(seed_id)}
	var g: Array = r["garden"]
	if g[k] is Dictionary and str(g[k].get("seed", "")) != "":
		return {"ok": false, "err": "Something is already growing there."}
	c.remove_item(seed_id)
	g[k] = {"seed": seed_id, "t": Life.unix()}
	load("res://scripts/server/garden2.gd").on_plant(g[k])
	load("res://scripts/server/home.gd").progress(c, "plant")
	c.msg("Planted %s. Ripe in %d min.  ·  ปลูกแล้ว" % [GameData.item_name(seed_id), int(ceil(grow_time(seed_id) / 60.0))], Color(0.6, 1, 0.6))
	return {"ok": true, "plot": k}


static func harvest(pe, r: Dictionary, pi: int, k: int) -> Dictionary:
	var c = pe.ch
	var err := _bed_check(pe, r, pi, k)
	if err != "":
		return {"ok": false, "err": err}
	var g: Array = r["garden"]
	var seed_id := str(g[k].get("seed", "")) if g[k] is Dictionary else ""
	if seed_id == "":
		return {"ok": false, "err": "Nothing is planted there."}
	if growth_rec(r, k) < 1.0:
		return {"ok": false, "err": "Not ripe yet: %d min left." % int(ceil(secs_left_rec(r, k) / 60.0))}
	var cr := crop_of(seed_id)
	var y: Array = cr.get("yield", [2, 3])
	var n := randi_range(int(y[0]), int(y[1]))
	var crop := str(cr.get("crop", ""))
	# Housing v2 P3: watering bonus / pest penalty / cross-breeding (server/garden2.gd)
	var G = load("res://scripts/server/garden2.gd")
	var mod: Dictionary = G.harvest_mod(r, k, n, float(cr.get("seed_back", 0.3)))
	n = int(mod["n"])
	if not c.can_carry(crop, n):
		return {"ok": false, "err": "Your bag is too heavy."}
	c.add_item(crop, n)
	var back := randf() < float(mod["seed_back"])
	if back:
		c.add_item(seed_id, 1)
	var bred: String = G.try_breed(c, r, k)
	if bool(mod["bonus"]):
		c.msg("Well watered: a bonus harvest!  ·  รดน้ำดี ได้ผลผลิตเพิ่ม", Color(0.6, 0.9, 1))
	if bool(mod["pest"]):
		c.msg("Pests ate half of it...  ·  แมลงกินไปครึ่งหนึ่ง", Color(1, 0.7, 0.5))
	g[k] = {}
	var Home = load("res://scripts/server/home.gd")
	Home.progress(c, "harvest")
	Home.add_coins(c, int(Housing.H2().get("harvest_coins", 3)))
	c.msg("Harvested %s x%d%s.  ·  เก็บเกี่ยวแล้ว" % [GameData.item_name(crop), n, " and got a seed back" if back else ""], Color(0.7, 1, 0.5))
	pe._fx("ring", {"at": garden_world(pi, int(r["lv"]), k), "r": 1.3, "color": Color(0.7, 1, 0.4), "time": 0.5})
	return {"ok": true, "crop": crop, "n": n, "seed_back": back, "bonus": bool(mod["bonus"]), "pest": bool(mod["pest"]), "bred": bred}


# ---------------------------------------------------------------- walls (server/map_colliders.gd hook)

## v1 Homestead walls: [size, centre] pairs (old map, kept so old saves standing there load).
static func wall_boxes() -> Array:
	var rm: Dictionary = H().get("room", {})
	var x0 := float(rm.get("x", [-9, 9])[0])
	var x1 := float(rm.get("x", [-9, 9])[1])
	var z0 := float(rm.get("z", [-16, -4])[0])
	var z1 := float(rm.get("z", [-16, -4])[1])
	var d0 := float(rm.get("door", [-1.5, 1.5])[0])
	var d1 := float(rm.get("door", [-1.5, 1.5])[1])
	var t := 0.5
	var out: Array = []
	out.append([Vector3(x1 - x0 + t, 3.0, t), Vector3((x0 + x1) / 2.0, 1.5, z0)])
	out.append([Vector3(t, 2.0, z1 - z0), Vector3(x0, 1.0, (z0 + z1) / 2.0)])
	out.append([Vector3(t, 2.0, z1 - z0), Vector3(x1, 1.0, (z0 + z1) / 2.0)])
	out.append([Vector3(d0 - x0, 1.0, t), Vector3((x0 + d0) / 2.0, 0.5, z1)])
	out.append([Vector3(x1 - d1, 1.0, t), Vector3((d1 + x1) / 2.0, 0.5, z1)])
	return out


## map_colliders.gd: the v1 Homestead walls, or every house of a community map.
static func colliders(root: Node3D, map_id: String) -> void:
	if Housing.is_community(map_id):
		Housing.colliders(root, map_id)
		return
	if map_id != home_map():
		return
	for b in wall_boxes():
		var sb := StaticBody3D.new()
		sb.name = "HomeWall"
		var cs := CollisionShape3D.new()
		var bs := BoxShape3D.new()
		bs.size = b[0]
		cs.shape = bs
		sb.add_child(cs)
		root.add_child(sb)
		sb.position = b[1]
