-- lx-bundle:true

local core = require("core")
local config = require("core.config")
-- Needed for Shift+Wheel horizontal scrolling
local keymap = require("core.keymap")
local style = require("core.style")
local common = require("core.common")
local Object = require("core.object")
local Scrollbar = require("core.ui.scrollbar")

local View = Object:extend()
local MOUSE_INTERACTION_REDRAW_SECONDS = 2

View.LaneIDs = {
	MINIMAP = "minimap", -- a vertical scrollbar
	HSCROLL = "hscrollbar",
	VSCROLL = "vscrollbar",
}
View.LaneOrders = {
	MINIMAP = 100,
	HSCROLL = 200,
	VSCROLL = 300,
}

-- Build a deterministic lane list ordered by lane priority and id
local function lane_list_sorted(side_lanes)
	local list = {}
	for id, lane in pairs(side_lanes or {}) do
		local sz = math.max(0, tonumber(lane.size) or 0)
		if sz > 0 then
			list[#list + 1] = {
				id = id,
				order = tonumber(lane.order) or 0,
				size = sz,
			}
		end
	end
	table.sort(list, function(a, b)
		if a.order ~= b.order then
			return a.order < b.order
		end
		return tostring(a.id) < tostring(b.id)
	end)
	return list
end

-- Pick translucent debug overlay color by lane side
local function lane_debug_color(side)
	if side == "right" then
		return { 90, 160, 255, 80 }
	end
	return { 120, 220, 120, 80 }
end

-- Check whether a point lies inside a rectangle
local function point_in_rect(px, py, x, y, w, h)
	return px >= x and py >= y and px < x + w and py < y + h
end

--- Initialize base view geometry, scrolling, and cursor state
function View:new()
	self.position = {
		x = 0,
		y = 0,
	}
	self.size = {
		x = 0,
		y = 0,
	}
	self.scroll = {
		x = 0,
		y = 0,
		to = { x = 0, y = 0 },
	}
	self.vscrollbar = Scrollbar(
		{ direction = "v", alignment = "e" }
	)
	self.hscrollbar = Scrollbar(
		{ direction = "h", alignment = "e" }
	)
	self.mouse_cursor = "arrow"
	self.scrollable = false
	self._mouse_pan = nil
	self._cached_scroll_max_x = nil
	self._cached_scroll_max_y = nil
	self._interaction_redraw_until = 0

	-- Shared lane reservation/layout state used by all view subclasses
	self._lane_reservations = {
		right = {},
		bottom = {},
	}
	self._lane_input_handlers = {
		right = {},
		bottom = {},
	}
	self._lane_input_handler_count = 0
	self._active_lane_input = nil
	self._hover_lane_input = nil
	self._lane_layout_dirty = true

	self._lane_layout_cache = {
		right = {
			list = {},
			rects = {},
			total = 0,
		},
		bottom = {
			list = {},
			rects = {},
			total = 0,
		},
	}
	self._lane_layout_last = {
		x = nil,
		y = nil,
		w = nil,
		h = nil,
	}
end

-- Smoothly move a numeric field toward a destination value
function View:move_towards(t, k, dest, rate)
	if type(t) ~= "table" then
		rate = dest
		dest = k
		k = t
		t = self
	end
	local val = t[k]
	if math.abs(val - dest) < 0.5 then
		t[k] = dest
	else
		t[k] = common.lerp(val, dest, rate or 0.5)
	end
	if val ~= dest then
		core.redraw = true
	end
end

-- Attempt to close this view
function View:try_close(do_close)
	do_close()
end

-- Return the tab/display name for this view
function View:get_name()
	return "---"
end

-- Return total vertical scrollable size for this view
function View:get_scrollable_size()
	return math.huge
end

function View:get_scroll_max_x()
	return math.huge
end

function View:get_scroll_max_y()
	local scrollable_size = self:get_scrollable_size()
	local viewport_height = self:get_scroll_viewport_height()
	local max_scroll_y = math.max(0, scrollable_size - viewport_height)

	return max_scroll_y
end

--- Keep rendering responsive for a short time after direct mouse input
-- @param duration number|nil redraw window in seconds
function View:keep_mouse_interaction_active(duration)
	local redraw_seconds = math.max(0, tonumber(duration) or MOUSE_INTERACTION_REDRAW_SECONDS)
	self._interaction_redraw_until = math.max(
		self._interaction_redraw_until or 0,
		system.get_time() + redraw_seconds
	)
	core.redraw = true
end

--- Return whether cached scrollbar geometry owns one pointer position
-- This deliberately avoids content measurement when a drag begins
-- @param x number pointer x
-- @param y number pointer y
-- @return boolean overlaps
function View:mouse_pan_overlaps_scrollbar(x, y)
	local vertical_overlap = self.vscrollbar and self.vscrollbar:overlaps(x, y) ~= nil
	local horizontal_overlap = self.hscrollbar and self.hscrollbar:overlaps(x, y) ~= nil

	return vertical_overlap or horizontal_overlap
end

--- Start direct mouse panning from geometry cached by normal view updates
-- @param button string mouse button
-- @param x number pointer x
-- @param y number pointer y
-- @return boolean started
function View:start_mouse_pan(button, x, y)
	if not self.scrollable or self:mouse_pan_overlaps_scrollbar(x, y) then
		return false
	end

	-- An already visible view has fresh limits; the open fallback avoids measuring on input
	local maximum_x = self._cached_scroll_max_x or math.huge
	local maximum_y = self._cached_scroll_max_y or math.huge
	self._mouse_pan = {
		x = x,
		y = y,
		button = button,
		previous_cursor = self.mouse_cursor,
		maximum_x = maximum_x,
		maximum_y = maximum_y,
	}
	self.scroll.to.x = self.scroll.x or 0
	self.scroll.to.y = self.scroll.y or 0
	self.mouse_cursor = "grabbing"
	self:keep_mouse_interaction_active()

	return true
end

--- Move active mouse panning without measuring or rebuilding view content
-- @param x number pointer x
-- @param y number pointer y
-- @return boolean handled
function View:update_mouse_pan(x, y)
	local pan = self._mouse_pan

	if not pan then
		return false
	end

	local step_x = x - pan.x
	local step_y = y - pan.y
	pan.x = x
	pan.y = y

	local current_x = self.scroll.to.x or self.scroll.x or 0
	local current_y = self.scroll.to.y or self.scroll.y or 0
	local target_x = common.clamp(current_x - step_x, 0, pan.maximum_x)
	local target_y = common.clamp(current_y - step_y, 0, pan.maximum_y)

	self.scroll.x = target_x
	self.scroll.to.x = target_x
	self.scroll.y = target_y
	self.scroll.to.y = target_y
	self.mouse_cursor = "grabbing"
	self:keep_mouse_interaction_active()

	return true
end

--- Finish active mouse panning for the released button
-- @param button string mouse button
-- @return boolean finished
function View:finish_mouse_pan(button)
	local pan = self._mouse_pan

	if not pan or (button and button ~= pan.button) then
		return false
	end

	self._mouse_pan = nil
	self.mouse_cursor = pan.previous_cursor or "arrow"
	self:keep_mouse_interaction_active()

	return true
end

function View:get_scroll_viewport_height()
	local bottom = 0
	if self.get_bottom_lane_height then
		bottom = math.max(0, self:get_bottom_lane_height() or 0)
	end
	return math.max(1, self.size.y - bottom)
end

-- Compute the scrollbar rectangle for the current scroll state
function View:get_scrollbar_rect()
	local sz = self:get_scrollable_size()
	local viewport_h = self:get_scroll_viewport_height()
	local x, y, w, h = self.position.x, self.position.y, self.size.x, viewport_h
	local show = sz > viewport_h
	if self.get_lane_size and self:get_lane_size("right", View.LaneIDs.MINIMAP) > 0 then
		show = false
	end
	if self.set_lane_size and self.get_lane_rect then
		local lane_w = math.max(1, style.scrollbar_size or 12)
		self:set_lane_size("right", View.LaneIDs.VSCROLL, show and lane_w or 0, View.LaneOrders.VSCROLL)
		local lx, ly, lw, lh = self:get_lane_rect("right", View.LaneIDs.VSCROLL)
		if lx and ly and lw and lh then
			x, y, w, h = lx, ly, lw, lh
		end
	end
	local scrollbar_sz = show and sz or viewport_h
	self.vscrollbar:set_size(x, y, w, h, scrollbar_sz)
	local max_scroll = math.max(0, sz - viewport_h)
	local t = (max_scroll > 0) and common.clamp((self.scroll.y or 0) / max_scroll, 0, 1) or 0
	self.vscrollbar:set_percent(t)
	return self.vscrollbar:get_track_rect()
end

-- Return normalized scrollbar drag state for a pointer y-position
-- 'drag_off' is the pointer offset into the thumb (0..thumb height)
function View:get_scrollbar_drag_state(y, drag_off)
	self:get_scrollbar_rect()
	local _, track_y, _, track_h = self.vscrollbar:get_track_rect()
	local _, _, _, thumb_h = self.vscrollbar:get_thumb_rect()
	local sz = self:get_scrollable_size()
	local viewport_h = self:get_scroll_viewport_height()
	local max_scroll = math.max(0, sz - viewport_h)
	if thumb_h <= 0 or track_h <= 0 or max_scroll <= 0 then
		return nil
	end
	local track = math.max(1, track_h - thumb_h)
	local off = common.clamp(drag_off or (thumb_h * 0.5), 0, thumb_h)
	local thumb_top = common.clamp(y - off, track_y, track_y + track)
	local t = (thumb_top - track_y) / track
	return {
		t = common.clamp(t, 0, 1),
		thumb_size = thumb_h,
		track_size = track,
		max_scroll = max_scroll,
		drag_offset = off,
	}
end

-- Convert pointer y-position to vertical scroll target for current scrollbar
function View:get_scrollbar_scroll_target(y, drag_off)
	local st = self:get_scrollbar_drag_state(y, drag_off)
	if not st then
		return 0, nil
	end
	local target = common.clamp(st.t * st.max_scroll, 0, st.max_scroll)
	return target, st
end

-- Return whether a point overlaps the scrollbar hit area
function View:scrollbar_overlaps_point(x, y)
	self:get_scrollbar_rect()
	return self.vscrollbar:overlaps(x, y) ~= nil
end

-- Return whether a point overlaps the scrollbar thumb itself
function View:scrollbar_thumb_overlaps_point(x, y)
	self:get_scrollbar_rect()
	return self.vscrollbar:overlaps(x, y) == "thumb"
end

-- Start scrollbar dragging when pressed over the scrollbar
function View:on_mouse_down(button, x, y, clicks)
	self:get_scrollbar_rect()
	local pressed = self.vscrollbar:on_mouse_down(button, x, y, clicks)
	self.dragging_scrollbar = self.vscrollbar.dragging or false
	if pressed then
		if type(pressed) == "number" then
			local max_scroll = math.max(0, self:get_scrollable_size() - self:get_scroll_viewport_height())
			local target = common.clamp(pressed * max_scroll, 0, max_scroll)
			self.scroll.to.y = target
			self.scroll.y = target
		end
		return true
	end
end

--- Stop active panning and scrollbar dragging on mouse release
-- @param button string mouse button
-- @param x number pointer x
-- @param y number pointer y
function View:on_mouse_up(button, x, y)
	self:finish_mouse_pan(button)
	self.vscrollbar:on_mouse_up(button, x, y)
	self.dragging_scrollbar = self.vscrollbar.dragging or false
end

--- Update direct panning, scrollbar dragging, and scrollbar hover state
-- @param x number pointer x
-- @param y number pointer y
-- @param dx number pointer delta x
-- @param dy number pointer delta y
-- @return boolean|nil handled
function View:on_mouse_move(x, y, dx, dy)
	if self:update_mouse_pan(x, y) then
		return true
	end

	self:get_scrollbar_rect()
	local moved = self.vscrollbar:on_mouse_move(x, y, dx, dy)
	if type(moved) == "number" then
		local max_scroll = math.max(0, self:get_scrollable_size() - self:get_scroll_viewport_height())
		local target = common.clamp(moved * max_scroll, 0, max_scroll)
		self.scroll.to.y = target
		self.scroll.y = target
	end
	self.dragging_scrollbar = self.vscrollbar.dragging or false
	if self.scrollable and self:scrollbar_overlaps_point(x, y) then
		self.mouse_cursor = "arrow"
	end
end

--- Stop active panning and scrollbar hover when the pointer leaves
function View:on_mouse_left()
	self:finish_mouse_pan()
	self.vscrollbar:on_mouse_left()
	self.dragging_scrollbar = false
end

-- Handle text input for this view (no-op by default)
function View:on_text_input(text)
	-- no-op
end

--- Handle unfinished native text composition for this view
-- @param text string current composition text
-- @param start number selected composition character offset
-- @param length number selected composition character count
function View:on_text_editing(text, start, length)
	-- Views without editable text deliberately ignore native composition
end

-- Handle wheel scrolling for vertical or shift-horizontal scrolling
function View:on_mouse_wheel(a, b, c, d)
	-- Prefer vertical delta when available
	local y = (d ~= nil and d) or (c ~= nil and c) or (b ~= nil and b) or a
	if self.scrollable then
		-- Prefer line-based wheel delta when configured and supported by the view
		local wheel_delta = config.mouse_wheel_scroll
		local lines = config.mouse_wheel_scroll_lines
		if lines and self.get_line_height then
			wheel_delta = self:get_line_height() * lines
		end
		-- Shift+Wheel scrolls horizontally; plain wheel remains vertical
		if keymap.modkeys["shift"] then
			self.scroll.to.x = math.max(0, self.scroll.to.x + y * -wheel_delta)
		else
			self.scroll.to.y = self.scroll.to.y + y * -wheel_delta
		end
	end
end

-- Return visible content bounds in scroll-space coordinates
function View:get_content_bounds()
	local x = self.scroll.x
	local y = self.scroll.y
	return x, y, x + self.size.x, y + self:get_scroll_viewport_height()
end

-- Return on-screen content origin adjusted by scroll offset
function View:get_content_offset()
	local x = common.round(self.position.x - self.scroll.x)
	local y = common.round(self.position.y - self.scroll.y)
	return x, y
end

-- Reserve or release a named lane region on a given side
function View:set_lane_size(side, id, size, order)
	local sides = self._lane_reservations
	local side_lanes = sides and sides[side]
	if not side_lanes or not id then
		return
	end
	local sz = math.max(0, tonumber(size) or 0)
	local existing = side_lanes[id]
	if sz <= 0 then
		if existing then
			self._lane_layout_dirty = true
			side_lanes[id] = nil
			local active = self._active_lane_input
			if active and active.side == side and active.id == id then
				self._active_lane_input = nil
			end
			local hover = self._hover_lane_input
			if hover and hover.side == side and hover.id == id then
				self._hover_lane_input = nil
			end
		end
		return
	end

	local prev_size = existing and math.max(0, tonumber(existing.size) or 0) or nil
	local prev_order = existing and (tonumber(existing.order) or 0) or nil
	local next_order = tonumber(order)
	if next_order == nil then
		next_order = (existing and prev_order) or 0
	end

	if existing and prev_size == sz and prev_order == next_order then
		return
	end

	side_lanes[id] = { size = sz, order = next_order }
	self._lane_layout_dirty = true
end

-- Return the reserved size for a single lane id
function View:get_lane_size(side, id)
	local side_lanes = self._lane_reservations and self._lane_reservations[side]
	local lane = side_lanes and side_lanes[id]
	return lane and math.max(0, tonumber(lane.size) or 0) or 0
end

-- Recompute lane rectangles when geometry or reservations changed
function View:refresh_lane_layout_cache()
	local last = self._lane_layout_last
	local x = self.position.x
	local y = self.position.y
	local w = self.size.x
	local h = self.size.y
	if not self._lane_layout_dirty and last.x == x and last.y == y and last.w == w and last.h == h then
		return
	end

	last.x, last.y, last.w, last.h = x, y, w, h
	self._lane_layout_dirty = false

	local cache = self._lane_layout_cache
	local right = cache.right
	local bottom = cache.bottom

	right.list = lane_list_sorted(self._lane_reservations.right)
	bottom.list = lane_list_sorted(self._lane_reservations.bottom)
	right.rects = {}
	bottom.rects = {}

	right.total = 0
	for _, lane in ipairs(right.list) do
		right.total = right.total + lane.size
	end
	bottom.total = 0
	for _, lane in ipairs(bottom.list) do
		bottom.total = bottom.total + lane.size
	end

	local rx = x + w
	local ry = y
	local rh = math.max(1, h - bottom.total)
	for _, lane in ipairs(right.list) do
		rx = rx - lane.size
		right.rects[lane.id] = { rx, ry, lane.size, rh }
	end

	local bx = x
	local by = y + h
	local bw = math.max(1, w)
	for _, lane in ipairs(bottom.list) do
		by = by - lane.size
		bottom.rects[lane.id] = { bx, by, bw, lane.size }
	end
end

-- Return total reserved thickness for a side ("right" or "bottom")
function View:get_reserved_lane_size(side)
	self:refresh_lane_layout_cache()
	local side_cache = self._lane_layout_cache and self._lane_layout_cache[side]
	return side_cache and side_cache.total or 0
end

-- Return the rectangle of a specific reserved lane
function View:get_lane_rect(side, id)
	self:refresh_lane_layout_cache()
	local side_cache = self._lane_layout_cache and self._lane_layout_cache[side]
	local r = side_cache and side_cache.rects and side_cache.rects[id]
	if not r then
		return nil
	end
	return r[1], r[2], r[3], r[4]
end

-- Attach or remove a pointer-input handler for a lane
function View:set_lane_input_handler(side, id, handler)
	local sides = self._lane_input_handlers
	local side_handlers = sides and sides[side]
	if not side_handlers or not id then
		return
	end
	if handler then
		if not side_handlers[id] then
			self._lane_input_handler_count = self._lane_input_handler_count + 1
		end
		side_handlers[id] = handler
	else
		if side_handlers[id] then
			self._lane_input_handler_count = math.max(0, self._lane_input_handler_count - 1)
		end
		side_handlers[id] = nil
		local active = self._active_lane_input
		if active and active.side == side and active.id == id then
			self._active_lane_input = nil
		end
	end
end

-- Return the lane input handler registered for side/id
function View:get_lane_input_handler(side, id)
	local side_handlers = self._lane_input_handlers and self._lane_input_handlers[side]
	return side_handlers and side_handlers[id] or nil
end

-- Resolve the top-most lane handler under the given pointer position
function View:resolve_lane_input_handler_at(x, y)
	if (self._lane_input_handler_count or 0) == 0 then
		return nil
	end
	self:refresh_lane_layout_cache()
	for _, side in ipairs({ "right", "bottom" }) do
		local side_cache = self._lane_layout_cache[side]
		local list = side_cache and side_cache.list or {}
		local rects = side_cache and side_cache.rects or {}
		for _, lane in ipairs(list) do
			local handler = self:get_lane_input_handler(side, lane.id)
			if handler then
				local r = rects[lane.id]
				if r and point_in_rect(x, y, r[1], r[2], r[3], r[4]) then
					return side, lane.id, handler
				end
			end
		end
	end
	return nil
end

-- Draw optional lane debug overlays for reserved regions
function View:draw_debug_lanes()
	if not config.debug_lanes then
		return
	end
	self:refresh_lane_layout_cache()
	local font = style.font
	for _, side in ipairs({ "right", "bottom" }) do
		local list = (self._lane_layout_cache[side] and self._lane_layout_cache[side].list) or {}
		for _, lane in ipairs(list) do
			local x, y, w, h = self:get_lane_rect(side, lane.id)
			if x and y and w and h and w > 0 and h > 0 then
				local c = lane_debug_color(side)
				renderer.draw_rect(x, y, w, h, c)
				renderer.draw_text(
					font,
					string.format("%s:%s", side, tostring(lane.id)),
					x + 2 * SCALE,
					y + 2 * SCALE,
					style.text
				)
			end
		end
	end
end

-- Convenience helper for total reserved width on the right side
function View:get_right_lane_width()
	return self:get_reserved_lane_size("right")
end

-- Convenience helper for total reserved height on the bottom side
function View:get_bottom_lane_height()
	return self:get_reserved_lane_size("bottom")
end

-- Return the drawable viewport excluding reserved lane areas
function View:get_content_viewport_rect()
	local right = math.max(0, self:get_right_lane_width() or 0)
	local bottom = math.max(0, self:get_bottom_lane_height() or 0)
	local w = math.max(1, self.size.x - right)
	local h = math.max(1, self.size.y - bottom)
	return self.position.x, self.position.y, w, h
end

--- Clamp scroll positions and cache the limits used by direct mouse panning
function View:clamp_scroll_position()
	local max_y = self:get_scroll_max_y()
	local max_x = self:get_scroll_max_x()
	self._cached_scroll_max_y = max_y
	self._cached_scroll_max_x = max_x

	self.scroll.to.y = common.clamp(self.scroll.to.y or 0, 0, max_y)
	self.scroll.y = common.clamp(self.scroll.y or 0, 0, max_y)

	if max_x == math.huge then
		self.scroll.to.x = math.max(0, self.scroll.to.x or 0)
		self.scroll.x = math.max(0, self.scroll.x or 0)
	else
		self.scroll.to.x = common.clamp(self.scroll.to.x or 0, 0, max_x)
		self.scroll.x = common.clamp(self.scroll.x or 0, 0, max_x)
	end
end

--- Update scrolling while direct mouse panning keeps ownership of its position
function View:update()
	if self._mouse_pan then
		self:keep_mouse_interaction_active()
		return
	end

	self:clamp_scroll_position()
	self:move_towards(self.scroll, "x", self.scroll.to.x, 0.3)
	self:move_towards(self.scroll, "y", self.scroll.to.y, 0.3)
	self:clamp_scroll_position()

	if (self._interaction_redraw_until or 0) > system.get_time() then
		core.redraw = true
	end
end

-- Fill the view background rectangle with a color
function View:draw_background(color)
	local x, y = self.position.x, self.position.y
	local w, h = self.size.x, self.size.y
	renderer.draw_rect(x, y, w + x % 1, h + y % 1, color)
end

-- Draw the scrollbar using normal or hover/drag colors
function View:draw_scrollbar()
	self:get_scrollbar_rect()
	self.vscrollbar:draw()
end

-- Draw this view
function View:draw() end

return View
