-- lx-bundle:true

-- This file is part of Lx, created by Liqube <www.liqube.com>
-- Copyright Liqube Audio. All rights reserved.
-- Lx Interim License. See LICENSE for details.

local core = require("core")
local common = require("core.common")
local config = require("core.config")
local style = require("core.style")
local Object = require("core.object")

local Scrollbar = Object:extend()

function Scrollbar:__tostring()
	return "Scrollbar"
end

function Scrollbar:new(opts)
	opts = opts or {}
	self.direction = opts.direction or "v" -- "v" | "h"
	self.alignment = opts.alignment or "e" -- "e" | "s"
	self.force_status = opts.force_status or false

	self.contracted_size = opts.contracted_size
	self.expanded_size = opts.expanded_size
	self.minimum_thumb_size = opts.minimum_thumb_size
	self.contracted_margin = opts.contracted_margin
	self.expanded_margin = opts.expanded_margin

	self.rect = { x = 0, y = 0, w = 0, h = 0, scrollable = 0 }
	self.normal_rect = { across = 0, along = 0, across_size = 0, along_size = 0, scrollable = 0 }
	self.percent = 0
	self.expand_percent = 0
	self.dragging = false
	self.drag_start_offset = 0
	self.hovering = { track = false, thumb = false }

	if self.force_status == "expanded" then
		self.expand_percent = 1
	end
end

function Scrollbar:set_forced_status(status)
	self.force_status = status
	if self.force_status == "expanded" then
		self.expand_percent = 1
	elseif self.force_status == "contracted" then
		self.expand_percent = 0
	end
end

function Scrollbar:real_to_normal(x, y, w, h)
	x, y, w, h = x or 0, y or 0, w or 0, h or 0
	if self.direction == "v" then
		if self.alignment == "s" then
			x = (self.rect.x + self.rect.w) - x - w
		end
		return x, y, w, h
	end
	if self.alignment == "s" then
		y = (self.rect.y + self.rect.h) - y - h
	end
	return y, x, h, w
end

function Scrollbar:normal_to_real(x, y, w, h)
	x, y, w, h = x or 0, y or 0, w or 0, h or 0
	if self.direction == "v" then
		if self.alignment == "s" then
			x = (self.rect.x + self.rect.w) - x - w
		end
		return x, y, w, h
	end
	if self.alignment == "s" then
		x = (self.rect.y + self.rect.h) - x - w
	end
	return y, x, h, w
end

local function get_expanded_size(sb)
	return sb.expanded_size or style.expanded_scrollbar_size or style.scrollbar_size
end

local function get_contracted_size(sb)
	return sb.contracted_size or style.scrollbar_size
end

local function get_min_thumb_size(sb)
	return sb.minimum_thumb_size or style.minimum_thumb_size or 20
end

local function get_expanded_margin(sb)
	return sb.expanded_margin or style.expanded_scrollbar_margin or 0
end

local function get_contracted_margin(sb)
	return sb.contracted_margin or style.contracted_scrollbar_margin or 0
end

function Scrollbar:_get_thumb_rect_normal()
	local nr = self.normal_rect
	local sz = nr.scrollable
	if sz == math.huge or sz <= nr.along_size then
		return 0, 0, 0, 0
	end
	local contracted = get_contracted_size(self)
	local expanded = get_expanded_size(self)
	local across = contracted + (expanded - contracted) * self.expand_percent
	local along = math.max(get_min_thumb_size(self), nr.along_size * nr.along_size / sz)
	return nr.across + nr.across_size - across, nr.along + self.percent * (nr.along_size - along), across, along
end

function Scrollbar:get_thumb_rect()
	return self:normal_to_real(self:_get_thumb_rect_normal())
end

function Scrollbar:_get_track_rect_normal()
	local nr = self.normal_rect
	local sz = nr.scrollable
	if sz <= nr.along_size or sz == math.huge then
		return 0, 0, 0, 0
	end
	local contracted = get_contracted_size(self)
	local expanded = get_expanded_size(self)
	local across = contracted + (expanded - contracted) * self.expand_percent
	return nr.across + nr.across_size - across, nr.along, across, nr.along_size
end

function Scrollbar:get_track_rect()
	return self:normal_to_real(self:_get_track_rect_normal())
end

function Scrollbar:_overlaps_normal(x, y)
	local sx, sy, sw, sh = self:_get_thumb_rect_normal()
	local margin = self.expand_percent * get_expanded_margin(self)
		+ (1 - self.expand_percent) * get_contracted_margin(self)
	if x >= sx - margin and x <= sx + sw and y >= sy and y <= sy + sh then
		return "thumb"
	end
	sx, sy, sw, sh = self:_get_track_rect_normal()
	if x >= sx - margin and x <= sx + sw and y >= sy and y <= sy + sh then
		return "track"
	end
end

function Scrollbar:overlaps(x, y)
	x, y = self:real_to_normal(x, y)
	return self:_overlaps_normal(x, y)
end

function Scrollbar:on_mouse_down(button, x, y)
	if button ~= "left" then
		return
	end
	x, y = self:real_to_normal(x, y)
	local hit = self:_overlaps_normal(x, y)
	if not hit then
		return
	end
	local _, along, _, along_size = self:_get_thumb_rect_normal()
	self.dragging = true
	if hit == "thumb" then
		self.drag_start_offset = along - y
		return true
	end
	local nr = self.normal_rect
	self.drag_start_offset = -along_size / 2
	return common.clamp((y - nr.along - along_size / 2) / math.max(1, nr.along_size - along_size), 0, 1)
end

function Scrollbar:_update_hover_status_normal(x, y)
	local hit = self:_overlaps_normal(x, y)
	self.hovering.thumb = hit == "thumb"
	self.hovering.track = self.hovering.thumb or hit == "track"
	return self.hovering.track or self.hovering.thumb
end

function Scrollbar:on_mouse_up(button, x, y)
	if button ~= "left" then
		return
	end
	self.dragging = false
	x, y = self:real_to_normal(x, y)
	return self:_update_hover_status_normal(x, y)
end

function Scrollbar:on_mouse_move(x, y, dx, dy)
	x, y = self:real_to_normal(x, y)
	dx, dy = self:real_to_normal(dx, dy)
	if self.dragging then
		local nr = self.normal_rect
		local _, _, _, along_size = self:_get_thumb_rect_normal()
		return common.clamp((y - nr.along + self.drag_start_offset) / math.max(1, nr.along_size - along_size), 0, 1)
	end
	return self:_update_hover_status_normal(x, y)
end

function Scrollbar:on_mouse_left()
	self.hovering.track = false
	self.hovering.thumb = false
end

function Scrollbar:set_size(x, y, w, h, scrollable)
	self.rect.x, self.rect.y, self.rect.w, self.rect.h = x, y, w, h
	self.rect.scrollable = scrollable
	local nr = self.normal_rect
	nr.across, nr.along, nr.across_size, nr.along_size = self:real_to_normal(x, y, w, h)
	nr.scrollable = scrollable
end

function Scrollbar:set_percent(percent)
	self.percent = percent
end

function Scrollbar:update()
	if not self.force_status then
		local dest = (self.hovering.track or self.dragging) and 1 or 0
		local diff = math.abs(self.expand_percent - dest)
		if
			not config.transitions
			or diff < 0.05
			or (config.disabled_transitions and config.disabled_transitions["scroll"])
		then
			self.expand_percent = dest
		else
			local rate = 0.3
			if config.fps ~= 60 or config.animation_rate ~= 1 then
				local dt = 60 / config.fps
				rate = 1 - common.clamp(1 - rate, 1e-8, 1 - 1e-8) ^ (config.animation_rate * dt)
			end
			self.expand_percent = common.lerp(self.expand_percent, dest, rate)
		end
		if diff > 1e-8 then
			core.redraw = true
		end
	elseif self.force_status == "expanded" then
		self.expand_percent = 1
	elseif self.force_status == "contracted" then
		self.expand_percent = 0
	end
end

function Scrollbar:draw_track()
	if not (self.hovering.track or self.dragging) and self.expand_percent == 0 then
		return
	end
	local track = style.scrollbar_track or style.background2 or style.background
	local color = { track[1], track[2], track[3], (track[4] or 255) * self.expand_percent }
	local x, y, w, h = self:get_track_rect()
	renderer.draw_rect(x, y, w, h, color)
end

function Scrollbar:draw_thumb()
	local highlight = self.hovering.thumb or self.dragging
	local color = highlight and style.scrollbar2 or style.scrollbar
	local x, y, w, h = self:get_thumb_rect()
	renderer.draw_rect(x, y, w, h, color)
end

function Scrollbar:draw()
	self:draw_track()
	self:draw_thumb()
end

return Scrollbar
