extends RefCounted
## UI v4 quest book (owner 2026-09-30, "painted storybook"): Active / Available / Done tabs, a list of illustrated
## quest rows (the zone's painted map as a thumbnail, recommended level, a status pill), and the selected quest
## below: the giver's portrait, the objective with a progress bar, the zone picture, reward cards and the
## auto-quest button. Hook: hud.gd _fill_quests -> Quest4.fill(hud) while UiSkin.v4().

const UiSkin = preload("res://scripts/ui/ui_skin.gd")
const Icons = preload("res://scripts/icons.gd")
const MapPaint = preload("res://scripts/map_paint.gd")
const AutoPlay = preload("res://scripts/auto_play.gd")

const W := 520.0

static var tab := "active"
static var sel := ""


static var _sig := ""


static func fill(hud) -> void:
	var body: VBoxContainer = hud.bodies["quests"]
	var ps = PlayerState
	var ql = ps.quests
	# rebuild only when something shown changes (HP ticks refresh the HUD: a rebuild mid-click eats the click)
	var au = hud._auto()
	var sig := JSON.stringify([tab, sel, ql.active, ql.done, ps.inventory, ps.job, ps.base_lv, au != null and au.active, str(au.quest_id) if au != null else ""])
	if sig == _sig and body.get_child_count() > 0:
		return
	_sig = sig
	hud._clear(body)
	var active: Array = (ql.active as Dictionary).keys()
	var avail: Array = []
	for q in GameData.quest_list:
		var qid := str(q["id"])
		if ql.can_accept(ps, qid) == "":
			avail.append(qid)
	var done: Array = (ql.done as Array).duplicate()
	done.reverse()
	var lists := {"active": active, "avail": avail, "done": done}
	# tabs
	var tr := HBoxContainer.new()
	tr.add_theme_constant_override("separation", 6)
	body.add_child(tr)
	for t in [["active", "In progress"], ["avail", "Available"], ["done", "Done"]]:
		var k := str(t[0])
		var b: Button = hud._tab_button("%s  %d" % [hud.tr(str(t[1])), (lists[k] as Array).size()], tab == k, func():
			tab = k
			sel = ""
			hud.dirty = true)
		b.name = "QTab_" + k
		tr.add_child(b)
	var ids: Array = lists[tab]
	if sel == "" or not (sel in ids):
		sel = str(ids[0]) if not ids.is_empty() else ""
	# list
	var scroll := ScrollContainer.new()
	scroll.name = "QuestList"
	scroll.custom_minimum_size = Vector2(W, mini(3, maxi(1, ids.size())) * 64 + 8)
	scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
	body.add_child(UiSkin.boxed(scroll, 6.0))
	var list := VBoxContainer.new()
	list.add_theme_constant_override("separation", 4)
	list.size_flags_horizontal = Control.SIZE_EXPAND_FILL
	scroll.add_child(list)
	if ids.is_empty():
		list.add_child(UiSkin.label(hud.tr("No quests here yet."), 14, Color("#7a6248")))
	for id in ids:
		list.add_child(_row(hud, str(id), ql, ps))
	# detail
	if sel != "":
		body.add_child(_detail(hud, sel, ql, ps))
	var foot := HBoxContainer.new()
	body.add_child(foot)
	foot.add_child(UiSkin.label("%s: %d / %d" % [hud.tr("Completed"), done.size(), GameData.quest_list.size()], 13, Color("#8a7052")))


static func _goal_map(id: String) -> String:
	var g: Dictionary = AutoPlay.quest_goal(id)
	return str(g.get("map", ""))


static func _status(ql, ps, id: String) -> Array:
	if ql.is_done(id):
		return ["Done", Color("#8a7052")]
	if ql.is_active(id):
		return ["Complete!", Color("#3f9a4a")] if ql.is_complete(ps, id) else ["In progress", Color("#e0932c")]
	return ["Available", Color("#3c78c8")]


static func _row(hud, id: String, ql, ps) -> Control:
	var q: Dictionary = GameData.quest(id)
	var b := Button.new()
	b.name = "QRow_" + id
	b.focus_mode = Control.FOCUS_NONE
	b.custom_minimum_size = Vector2(0, 60)
	b.size_flags_horizontal = Control.SIZE_EXPAND_FILL
	var on := id == sel
	for st in ["normal", "hover", "pressed", "disabled"]:
		b.add_theme_stylebox_override(st, UiSkin.card(on or st == "hover", 4.0))
	b.add_theme_stylebox_override("focus", StyleBoxEmpty.new())
	b.pressed.connect(func():
		sel = id
		hud.dirty = true)
	var h := HBoxContainer.new()
	h.add_theme_constant_override("separation", 8)
	h.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
	h.offset_left = 6
	h.offset_right = -8
	h.offset_top = 5
	h.offset_bottom = -5
	h.mouse_filter = Control.MOUSE_FILTER_IGNORE
	b.add_child(h)
	var th := Thumb.new()
	th.map_id = _goal_map(id)
	th.custom_minimum_size = Vector2(84, 50)
	th.mouse_filter = Control.MOUSE_FILTER_IGNORE
	h.add_child(th)
	var v := VBoxContainer.new()
	v.add_theme_constant_override("separation", -2)
	v.size_flags_horizontal = Control.SIZE_EXPAND_FILL
	v.mouse_filter = Control.MOUSE_FILTER_IGNORE
	h.add_child(v)
	var nm: Label = UiSkin.label(load("res://scripts/ui/msg_th.gd").name_th(str(q.get("name", id))), 15, Color("#4a2e16"), "SemiBold")
	nm.clip_text = true
	nm.mouse_filter = Control.MOUSE_FILTER_IGNORE
	v.add_child(nm)
	var mid := _goal_map(id)
	var lv := MapPaint.lv_text(GameData.map(mid)) if mid != "" else ""
	var sub: Label = UiSkin.label(("%s  ·  %s" % [GameData.map_name(mid), lv]) if mid != "" else ql.npc_name(ql.giver(id)), 12, Color("#8a7052"))
	sub.clip_text = true
	sub.mouse_filter = Control.MOUSE_FILTER_IGNORE
	v.add_child(sub)
	var stt := _status(ql, ps, id)
	var pill := Label.new()
	pill.text = hud.tr(str(stt[0]))
	pill.add_theme_stylebox_override("normal", UiSkin.badge(stt[1]))
	pill.add_theme_color_override("font_color", Color("#ffffff"))
	pill.add_theme_font_override("font", UiSkin.font("SemiBold"))
	pill.add_theme_font_size_override("font_size", 12)
	pill.size_flags_vertical = Control.SIZE_SHRINK_CENTER
	pill.mouse_filter = Control.MOUSE_FILTER_IGNORE
	h.add_child(pill)
	return b


static func _detail(hud, id: String, ql, ps) -> Control:
	var q: Dictionary = GameData.quest(id)
	var box := VBoxContainer.new()
	box.name = "QuestDetail"
	box.add_theme_constant_override("separation", 6)
	var top := HBoxContainer.new()
	box.add_child(top)
	var t: Label = UiSkin.label(load("res://scripts/ui/msg_th.gd").name_th(str(q.get("name", id))), 20, Color("#4a2e16"), "Bold")
	t.size_flags_horizontal = Control.SIZE_EXPAND_FILL
	top.add_child(t)
	var mid := _goal_map(id)
	if mid != "":
		top.add_child(UiSkin.label("%s  %s" % [GameData.map_name(mid), MapPaint.lv_text(GameData.map(mid))], 12, Color("#8a7052"), "Medium"))
	# giver portrait + story line + the zone picture
	var story := HBoxContainer.new()
	story.add_theme_constant_override("separation", 8)
	box.add_child(story)
	var giver := str(ql.giver(id))
	var pt := Portrait.new()
	pt.job = str(_npc(giver).get("job", ""))
	pt.custom_minimum_size = Vector2(68, 78)
	story.add_child(pt)
	var sv := VBoxContainer.new()
	sv.size_flags_horizontal = Control.SIZE_EXPAND_FILL
	story.add_child(sv)
	sv.add_child(UiSkin.label(ql.npc_name(giver), 13, Color("#5a3a1a"), "SemiBold"))
	var txt: String = load("res://scripts/ui/th_text.gd").lines(str(q.get("desc", q.get("text", ""))) if q.has("desc") or q.has("text") else ql.objective(ps, id))  # th-pass3
	var dl: Label = UiSkin.label(txt, 13, Color("#5b4128"))
	dl.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
	dl.custom_minimum_size = Vector2(220, 0)
	sv.add_child(dl)
	var big := Thumb.new()
	big.map_id = mid
	big.custom_minimum_size = Vector2(210, 118)
	story.add_child(big)
	# objective + progress
	var ob := HBoxContainer.new()
	ob.add_theme_constant_override("separation", 8)
	box.add_child(UiSkin.boxed(ob, 7.0, 1))
	var fin: bool = ql.is_active(id) and ql.is_complete(ps, id)
	var ck: Label = UiSkin.label("✔" if fin or ql.is_done(id) else "○", 16, Color("#3f9a4a") if fin else Color("#b8a27a"), "Bold")
	ob.add_child(ck)
	var ol: Label = UiSkin.label(load("res://scripts/ui/msg_th.gd").tr_msg(ql.objective(ps, id)), 13, Color("#3b2a1a"))
	ol.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
	ol.size_flags_horizontal = Control.SIZE_EXPAND_FILL
	ol.custom_minimum_size = Vector2(200, 0)
	ob.add_child(ol)
	if ql.is_active(id):
		var g: int = maxi(1, ql.goal(id))
		var pb: ProgressBar = hud._bar(Color("#58b84c"), 16)
		hud._set_bar(pb, ql.progress(ps, id), g, "%d / %d" % [ql.progress(ps, id), g])
		pb.custom_minimum_size = Vector2(120, 16)
		pb.size_flags_vertical = Control.SIZE_SHRINK_CENTER
		ob.add_child(pb)
	# rewards
	box.add_child(UiSkin.label(hud.tr("Rewards"), 14, Color("#5a3a1a"), "SemiBold"))
	var rr := HBoxContainer.new()
	rr.add_theme_constant_override("separation", 6)
	box.add_child(rr)
	var rw: Dictionary = q.get("rewards", {})
	var items: Dictionary = rw.get("items", {})
	for k in items:
		rr.add_child(_reward(Icons.item_tile(str(k), 52.0, hud.item_frame(str(k)), 2), "%s x%d" % [GameData.item_name(str(k)), int(items[k])]))
	if int(rw.get("zeny", 0)) > 0:
		var c: Control = load("res://scripts/ui/shop_list.gd")._coin(40.0)
		rr.add_child(_reward(c, "%s z" % hud._fmt(int(rw["zeny"]))))
	if int(rw.get("base_exp", 0)) > 0:
		var ex: Label = UiSkin.label("EXP", 15, Color("#2f7d2a"), "Bold")
		ex.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
		rr.add_child(_reward(ex, "%s" % hud._fmt(int(rw["base_exp"] * GameData.rate("exp_rate")))))
	# actions
	var ar := HBoxContainer.new()
	ar.add_theme_constant_override("separation", 8)
	box.add_child(ar)
	if ql.is_active(id):
		var go: Button = hud._quest_go_button(id, 15)
		go.custom_minimum_size = Vector2(150, 40)
		# the main action: green (Stop stays red)
		if go.text == "Go":
			go.text = hud.tr("Track quest")
			var st: Array = UiSkin.buttons(Color("#58b84c"))
			for i in 4:
				go.add_theme_stylebox_override(["normal", "hover", "pressed", "disabled"][i], st[i])
			for c in ["font_color", "font_hover_color", "font_pressed_color"]:
				go.remove_theme_color_override(c)
			go.add_theme_constant_override("outline_size", 4)
			go.add_theme_color_override("font_outline_color", Color(0.12, 0.34, 0.12, 0.85))
		ar.add_child(go)
		var sp := Control.new()
		sp.size_flags_horizontal = Control.SIZE_EXPAND_FILL
		ar.add_child(sp)
		ar.add_child(UiSkin.label("%s: %s" % [hud.tr("Report to"), load("res://scripts/ui/msg_th.gd").name_th(ql.npc_name(ql.turn_in_npc(id)))], 13, Color("#3f9a4a") if fin else Color("#8a7052"), "Medium"))
	elif not ql.is_done(id):
		ar.add_child(UiSkin.label("%s: %s" % [hud.tr("Talk to"), load("res://scripts/ui/msg_th.gd").name_th(ql.npc_name(giver))], 13, Color("#3c78c8"), "Medium"))
	return box


static func _reward(icon: Control, text: String) -> Control:
	var p := PanelContainer.new()
	p.add_theme_stylebox_override("panel", UiSkin.card(false, 5.0))
	var v := VBoxContainer.new()
	v.add_theme_constant_override("separation", 1)
	p.add_child(v)
	icon.custom_minimum_size = Vector2(52, 52)
	icon.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
	v.add_child(icon)
	var l: Label = UiSkin.label(text, 11, Color("#5b4128"), "Medium")
	l.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	l.custom_minimum_size = Vector2(92, 0)
	l.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
	v.add_child(l)
	return p


static func _npc(npc_id: String) -> Dictionary:
	for n in GameData.npcs:
		if str(n.get("id", "")) == npc_id:
			return n
	return {}


## A zone picture (the map's painted texture) in a rounded frame.
class Thumb extends Control:
	var map_id := ""

	func _draw() -> void:
		var MP = load("res://scripts/map_paint.gd")
		# painted zone art (assets/ui/v4/zones/<map>.png, tools/ui4_apply_flow.py) when there is one, else the map
		var zone: Texture2D = load("res://scripts/ui/skin4.gd").tex("zones/" + map_id) if map_id != "" else null
		var t: Texture2D = zone if zone else (MP.texture(map_id) if map_id != "" else null)
		var r := Rect2(Vector2.ZERO, size)
		var rr := 8.0
		var pts := PackedVector2Array()
		for c in [[Vector2(rr, rr), PI], [Vector2(size.x - rr, rr), PI * 1.5], [size - Vector2(rr, rr), 0.0], [Vector2(rr, size.y - rr), PI * 0.5]]:
			for i in 6:
				var a: float = float(c[1]) + PI * 0.5 * float(i) / 5.0
				pts.append((c[0] as Vector2) + Vector2(cos(a), sin(a)) * rr)
		if t:
			# a centred crop of the square map picture
			var uvs := PackedVector2Array()
			var asp := size.x / size.y
			for p in pts:
				var u := p / size
				if zone:
					# 16:9 art: fill the frame, cropping the long side
					var za := 16.0 / 9.0
					uvs.append(Vector2(0.5 + (u.x - 0.5) * minf(1.0, asp / za), 0.5 + (u.y - 0.5) * minf(1.0, za / asp)))
				else:
					uvs.append(Vector2(0.5 + (u.x - 0.5) * 0.7, 0.5 + (u.y - 0.5) * 0.7 / asp))
			draw_colored_polygon(pts, Color.WHITE, uvs, t)
		else:
			draw_colored_polygon(pts, Color("#b9d99a"))
		draw_polyline(pts + PackedVector2Array([pts[0]]), Color("#c9ad7a"), 2.0, true)
		draw_polyline(pts + PackedVector2Array([pts[0]]), Color(1, 1, 1, 0.5), 1.0, true)
		if r.size.x > 100.0:
			draw_rect(Rect2(2, size.y - 16, size.x - 4, 14), Color(0, 0, 0, 0.0))


## The quest giver's portrait on a soft disc.
class Portrait extends Control:
	var job := ""

	func _draw() -> void:
		var c := Vector2(size.x * 0.5, size.y * 0.55)
		var r := minf(size.x, size.y) * 0.46
		draw_circle(c, r + 2.0, Color("#c9ad7a"))
		draw_circle(c, r, Color("#fdf3dc"))
		var tex: Texture2D = null
		if job != "":
			tex = load("res://scripts/ui/equip_window.gd").portrait(job)
		if tex:
			var ts := tex.get_size()
			var h := size.y
			var wd := h * ts.x / ts.y
			draw_texture_rect(tex, Rect2(Vector2((size.x - wd) * 0.5, 0), Vector2(wd, h)), false)
		else:
			# no portrait art for this NPC: the painted speech-bubble badge
			var ic: Texture2D = load("res://scripts/ui/skin4.gd").tex("hdr_npc")
			if ic:
				var d := r * 1.5
				draw_texture_rect(ic, Rect2(c - Vector2(d, d) * 0.5, Vector2(d, d)), false)
