extends RefCounted
## Housing v2 P3 (docs/HOUSING_V2.md), server side - the garden becomes a little game of its own:
##  - WATERING (garden_water): up to 2 times per planting (once before half-grown, once after); each water = +25 % chance
##    of a bonus harvest (+2 crops / +1 flower), +15 % seed-back, and helps cross-breeding.
##  - PESTS: 35 % of plantings get a pest that shows up between 30 % and 80 % growth (decided at planting, so server and
##    client agree without ticking); a pest left on halves the harvest; shooing it (garden_pest) pays 2 Home Coins.
##  - FLOWERS: Rose / Tulip / Daisy in 8 colours (red / yellow / white -> pink / orange -> purple / blue -> golden);
##    harvesting a ripe flower next to a ripe flower of the same kind may give a seed of the child colour (breed table,
##    25 % + 10 % per water on the two beds, x the pair's rarity). Planted flowers add Beauty by colour tier (home.gd).
##  - FLOWER MARKET (Florist Posy on every plaza): crops + flowers bought / sold for gold at prices that drift every
##    day (0.6x .. 1.7x of the base, the same for everyone, deterministic per day + item); buying costs 30 % more, rare
##    colours are only bought. Base-colour seeds sold at a fixed price.
##  - TREASURE BOXES: 6 boxes a day sit at seeded spots of every lane (streets, plaza ring, walkways); each character
##    opens up to 6 a day (any lanes): coins, seeds, materials, sometimes a hybrid seed. Saved in c.life.boxes.

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

## Tests / shots: a fixed market day (0 = today).
static var test_day := 0


static func G2() -> Dictionary:
	return Life.data().get("garden2", {})


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


static func day() -> int:
	if test_day != 0:
		return test_day
	var d := Time.get_date_dict_from_unix_time(int(Life.unix()))
	return int(d["year"]) * 10000 + int(d["month"]) * 100 + int(d["day"])


## The day `n` days before day d (calendar aware).
static func day_back(d: int, n: int) -> int:
	var t := Time.get_unix_time_from_datetime_dict({"year": d / 10000, "month": (d / 100) % 100, "day": d % 100, "hour": 12, "minute": 0, "second": 0})
	var b := Time.get_date_dict_from_unix_time(int(t) - n * 86400)
	return int(b["year"]) * 10000 + int(b["month"]) * 100 + int(b["day"])


# ---------------------------------------------------------------- saved state

static func ensure(c) -> void:
	var L: Dictionary = c.life
	if not L.has("boxes") or not (L["boxes"] is Dictionary):
		L["boxes"] = {"day": 0, "open": []}


static func from_save(d: Dictionary, out: Dictionary) -> void:
	var b = d.get("boxes", {})
	if b is Dictionary and not b.is_empty():
		var op: Array = []
		for k in b.get("open", []):
			op.append(str(k))
		out["boxes"] = {"day": int(b.get("day", 0)), "open": op}


# ---------------------------------------------------------------- beds

## A new planting: decide its pest now (both sides read the same record).
static func on_plant(e: Dictionary) -> void:
	e["w"] = 0
	if randf() < float(G2().get("pest_chance", 0.35)):
		var pa: Array = G2().get("pest_at", [0.3, 0.8])
		e["pa"] = snappedf(randf_range(float(pa[0]), float(pa[1])), 0.01)


static func _bed(r: Dictionary, k: int) -> Dictionary:
	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 {}
	return g[k]


static func pest_on(r: Dictionary, k: int) -> bool:
	var e := _bed(r, k)
	if e.is_empty() or not e.has("pa") or bool(e.get("po", false)):
		return false
	var LifeHome = load("res://scripts/server/life_home.gd")
	return LifeHome.growth_rec(r, k) >= float(e["pa"])


## "" = this bed can take water now.
static func water_block(r: Dictionary, k: int) -> String:
	var e := _bed(r, k)
	if e.is_empty():
		return "Nothing is planted there."
	var LifeHome = load("res://scripts/server/life_home.gd")
	var gr: float = LifeHome.growth_rec(r, k)
	if gr >= 1.0:
		return "It is ripe - harvest it!"
	var w := int(e.get("w", 0))
	if w >= int(G2().get("water_max", 2)):
		return "Watered enough for this planting."
	if w == 1 and (gr < 0.5 or float(e.get("wt", 0.0)) >= 0.5):
		return "Water it again once it is half grown."
	return ""


static func can_water(r: Dictionary, k: int) -> bool:
	return water_block(r, k) == ""


static func water(pe, r: Dictionary, k: int) -> Dictionary:
	var err := water_block(r, k)
	if err != "":
		return {"ok": false, "err": err}
	var LifeHome = load("res://scripts/server/life_home.gd")
	var e := _bed(r, k)
	e["w"] = int(e.get("w", 0)) + 1
	e["wt"] = snappedf(LifeHome.growth_rec(r, k), 0.01)
	load("res://scripts/server/home.gd").progress(pe.ch, "water")
	pe.ch.msg("Watered.  ·  รดน้ำแล้ว (%d/%d)" % [int(e["w"]), int(G2().get("water_max", 2))], Color(0.6, 0.85, 1))
	return {"ok": true, "w": int(e["w"])}


static func clear_pest(pe, r: Dictionary, k: int) -> Dictionary:
	if not pest_on(r, k):
		return {"ok": false, "err": "No pests there."}
	_bed(r, k)["po"] = true
	var Home = load("res://scripts/server/home.gd")
	Home.add_coins(pe.ch, int(G2().get("pest_coins", 2)), "garden pest shooed away")
	Home.progress(pe.ch, "pest")
	return {"ok": true}


## Harvest modifiers of bed k: {"n": final count, "bonus": bool, "pest": bool, "seed_back": chance}.
static func harvest_mod(r: Dictionary, k: int, n: int, seed_back: float) -> Dictionary:
	var e := _bed(r, k)
	var w := int(e.get("w", 0))
	var pest := pest_on(r, k)
	var bonus := false
	var fl := str(crop(str(e.get("seed", ""))).get("flower", "")) != ""
	# P5: what neighbours picked is gone (server/social.gd)
	n = maxi(1, n - (e.get("pk", []) as Array).size())
	if pest:
		n = maxi(1, n / 2)
	elif w > 0 and randf() < float(G2().get("water_bonus", 0.25)) * w:
		bonus = true
		n += 1 if fl else int(G2().get("bonus_crops", 2))
	return {"n": n, "bonus": bonus, "pest": pest, "seed_back": seed_back + float(G2().get("water_seed", 0.15)) * w}


## The child colour of two colours (table order does not matter), [colour, chance mult] or [].
static func child(a: String, b: String) -> Array:
	for p in G2().get("breed", []):
		if (str(p[0]) == a and str(p[1]) == b) or (str(p[0]) == b and str(p[1]) == a):
			return [str(p[2]), float(p[3])]
	return []


## Neighbour beds of k (within 4 m in the plot frame).
static func neighbours(lv: int, k: int) -> Array:
	var gp: Array = Housing.garden_plots(lv)
	var out: Array = []
	if k < 0 or k >= gp.size():
		return out
	for j in gp.size():
		if j != k and (gp[j] as Vector2).distance_to(gp[k]) <= 4.0:
			out.append(j)
	return out


## Cross-breeding chance + child seed for harvesting flower bed k: {"seed", "chance", "with"} or {}.
static func breed_option(r: Dictionary, k: int) -> Dictionary:
	var e := _bed(r, k)
	var cr := crop(str(e.get("seed", "")))
	if str(cr.get("flower", "")) == "":
		return {}
	var LifeHome = load("res://scripts/server/life_home.gd")
	var best := {}
	for j in neighbours(int(r.get("lv", 1)), k):
		var e2 := _bed(r, j)
		if e2.is_empty() or LifeHome.growth_rec(r, j) < 1.0:
			continue
		var c2 := crop(str(e2["seed"]))
		if str(c2.get("flower", "")) != str(cr["flower"]) or str(c2.get("fcol", "")) == str(cr.get("fcol", "")):
			continue
		var ch := child(str(cr["fcol"]), str(c2["fcol"]))
		if ch.is_empty():
			continue
		var chance := (float(G2().get("breed_base", 0.25)) + float(G2().get("breed_water", 0.1)) * (int(e.get("w", 0)) + int(e2.get("w", 0)))) * float(ch[1])
		if best.is_empty() or chance > float(best["chance"]):
			best = {"seed": "seed_%s_%s" % [str(cr["flower"]), str(ch[0])], "chance": chance, "with": j}
	return best


## Test hook: force the next cross-breed roll (-1 = random).
static var force_breed := -1


static func try_breed(c, r: Dictionary, k: int) -> String:
	var o := breed_option(r, k)
	if o.is_empty():
		return ""
	var hit := randf() < float(o["chance"]) if force_breed < 0 else force_breed == 1
	if not hit or GameData.item(str(o["seed"])).is_empty():
		return ""
	c.add_item(str(o["seed"]), 1)
	c.msg("Cross-pollination! A seed of %s.  ·  ได้เมล็ดสีใหม่!" % GameData.item_name(str(o["seed"])), Color(1, 0.75, 0.95))
	return str(o["seed"])


# ---------------------------------------------------------------- the flower market

static func market() -> Dictionary:
	return G2().get("market", {})


## Price multiplier of an item on day d (same for every player, changes at midnight).
static func mult(id: String, d: int = 0) -> float:
	if d == 0:
		d = day()
	var m: Array = G2().get("market_mult", [0.6, 1.7])
	var u := float(absi(hash("%d:%s" % [d, id])) % 10007) / 10006.0
	# a gentle bell: most days near the base, some great days, some poor ones
	var v := (u + float(absi(hash("%s:%d" % [id, d])) % 10007) / 10006.0) / 2.0
	return float(m[0]) + (float(m[1]) - float(m[0])) * v


static func sell_price(id: String, d: int = 0) -> int:
	var e: Dictionary = market().get(id, {})
	if e.is_empty():
		return 0
	return maxi(1, int(round(float(e.get("base", 1)) * mult(id, d))))


static func buy_price(id: String, d: int = 0) -> int:
	var e: Dictionary = market().get(id, {})
	if not e.is_empty():
		if int(e.get("tier", 0)) > int(G2().get("market_sell_tier", 1)):
			return 0
		return maxi(1, int(round(sell_price(id, d) * float(G2().get("market_spread", 1.3)))))
	# fixed-price seeds of the florist
	return int(GameData.item(id).get("price", 0)) if florist_sells(id) else 0


static func florist_sells(id: String) -> bool:
	for n in GameData.npcs:
		if str(n.get("life", "")) == "market":
			return (n.get("sells", []) as Array).has(id)
	return false


static func florist_near(pe) -> bool:
	if pe.map == null:
		return false
	for n in pe.map.npcs:
		if str(n.def.get("life", "")) == "market" and pe.flat_dist(n.pos) <= float(G2().get("market_reach", 5.0)):
			return true
	return false


static func market_cmd(pe, t: String, cmd: Dictionary) -> Dictionary:
	var c = pe.ch
	if not florist_near(pe):
		return {"ok": false, "err": "Talk to Florist Posy on the lane plaza.  ·  ไปหาแม่ค้าดอกไม้"}
	var id := str(cmd.get("id", ""))
	var n := clampi(int(cmd.get("n", 1)), 1, 999)
	match t:
		"market_sell":
			var p := sell_price(id)
			if p <= 0:
				return {"ok": false, "err": "Posy does not buy that."}
			n = mini(n, int(c.count(id)))
			if n <= 0:
				return {"ok": false, "err": "You have none."}
			c.remove_item(id, n)
			c.zeny += p * n
			load("res://scripts/server/home.gd").progress(c, "sell", n)
			c.msg("Sold %s x%d for %d gold.  ·  ขายแล้ว" % [GameData.item_name(id), n, p * n], Color(1, 0.9, 0.5))
			c.changed.emit()
			return {"ok": true, "gold": p * n}
		"market_buy":
			var p2 := buy_price(id)
			if p2 <= 0:
				return {"ok": false, "err": "Posy does not sell that."}
			if int(c.zeny) < p2 * n:
				return {"ok": false, "err": "You need %d gold." % (p2 * n)}
			if not c.can_carry(id, n):
				return {"ok": false, "err": "Your bag is too heavy."}
			c.zeny -= p2 * n
			c.add_item(id, n)
			c.msg("Bought %s x%d for %d gold." % [GameData.item_name(id), n, p2 * n], Color(1, 0.9, 0.6))
			c.changed.emit()
			return {"ok": true, "gold": p2 * n}
	return {"ok": false, "err": "unknown market command"}


# ---------------------------------------------------------------- treasure boxes

## Today's box spots of a lane (world x/z), seeded per lane + day.
static func box_spots(map_id: String, d: int = 0) -> Array:
	if d == 0:
		d = day()
	var B: Dictionary = G2().get("boxes", {})
	var spots: Array = (B.get("spots", []) as Array).duplicate()
	var rng := RandomNumberGenerator.new()
	rng.seed = hash("%s:%d:boxes" % [map_id, d])
	var out: Array = []
	for i in mini(int(B.get("count", 6)), spots.size()):
		var j := rng.randi_range(0, spots.size() - 1)
		out.append(Vector3(float(spots[j][0]), 0, float(spots[j][1])))
		spots.remove_at(j)
	return out


static func box_key(map_id: String, i: int) -> String:
	return "%s:%d" % [map_id, i]


static func boxes_today(c) -> Dictionary:
	ensure(c)
	var b: Dictionary = c.life["boxes"]
	if int(b.get("day", 0)) != day():
		b["day"] = day()
		b["open"] = []
	return b


static func box_open(c, map_id: String, i: int) -> bool:
	var b: Dictionary = c.life.get("boxes", {})
	return int(b.get("day", 0)) == day() and (b.get("open", []) as Array).has(box_key(map_id, i))


## Test hook: force a loot kind ("" = roll).
static var force_loot := ""


static func open_box(pe, i: int) -> Dictionary:
	var c = pe.ch
	if pe.map == null or not Housing.is_community(pe.map.map_id):
		return {"ok": false, "err": ""}
	var mid: String = pe.map.map_id
	var sp := box_spots(mid)
	if i < 0 or i >= sp.size():
		return {"ok": false, "err": "No box there."}
	var B: Dictionary = G2().get("boxes", {})
	if pe.flat_dist(sp[i]) > float(B.get("reach", 2.2)) + 0.6:
		return {"ok": false, "err": "Walk up to the box first."}
	var b := boxes_today(c)
	if (b["open"] as Array).has(box_key(mid, i)):
		return {"ok": false, "err": "You already opened this one today."}
	if (b["open"] as Array).size() >= int(B.get("per_day", 6)):
		return {"ok": false, "err": "That is enough treasure for today - new boxes appear tomorrow.  ·  พรุ่งนี้มีใหม่"}
	var loot := _roll(B)
	var got := ""
	match str(loot[0]):
		"coins":
			var n := randi_range(int(loot[2]), int(loot[3]))
			load("res://scripts/server/home.gd").add_coins(c, n, "treasure box")
			got = "%d Home Coins" % n
		_:
			var pool := _pool(str(loot[0]))
			var id := str(pool[randi() % pool.size()])
			var n2 := randi_range(int(loot[2]), int(loot[3]))
			if not c.can_carry(id, n2):
				return {"ok": false, "err": "Your bag is too heavy."}
			c.add_item(id, n2)
			got = "%s x%d" % [GameData.item_name(id), n2]
	(b["open"] as Array).append(box_key(mid, i))
	load("res://scripts/server/home.gd").progress(c, "box")
	c.msg("Treasure box: %s!  ·  กล่องสมบัติ" % got, Color(1, 0.85, 0.4))
	pe._fx("ring", {"at": sp[i], "r": 1.4, "color": Color(1, 0.85, 0.35), "time": 0.6})
	Life.ev(pe, "box", {"comm": mid, "i": i})
	c.changed.emit()
	return {"ok": true, "got": got, "kind": str(loot[0])}


static func _roll(B: Dictionary) -> Array:
	var L: Array = B.get("loot", [])
	if force_loot != "":
		for e in L:
			if str(e[0]) == force_loot:
				return e
	var tot := 0
	for e in L:
		tot += int(e[1])
	var x := randi() % maxi(1, tot)
	for e in L:
		x -= int(e[1])
		if x < 0:
			return e
	return L[0]


static func _pool(kind: String) -> Array:
	var out: Array = []
	for s in Life.garden().get("crops", {}):
		var cr: Dictionary = Life.garden()["crops"][s]
		var fl := str(cr.get("flower", "")) != ""
		match kind:
			"flower_seed":
				if fl and int(cr.get("tier", 0)) == 0:
					out.append(s)
			"rare_seed":
				if fl and int(cr.get("tier", 0)) in [1, 2]:
					out.append(s)
			"crop_seed":
				if not fl:
					out.append(s)
	if kind == "material":
		out = ["timber_plank", "linen_cloth", "clay_brick", "glass_pane", "brass_fittings"]
	return out if not out.is_empty() else ["timber_plank"]


# ---------------------------------------------------------------- commands (Life.cmd)

static func command(pe, t: String, cmd: Dictionary) -> Dictionary:
	if t == "lane_box":
		return open_box(pe, int(cmd.get("i", -1)))
	return market_cmd(pe, t, cmd)
