--[[
	Mortha - proste automaty pomocnicze.

	Wszystko dziala WYLACZNIE po stronie klienta i wysyla do serwera dokladnie
	te same komunikaty, co gracz klikajacy recznie.

	Wymagania, ktore juz raz kosztowaly caly wieczor:
	  * klient MUSI byc zbudowany z -DBOT_PROTECTION=OFF, bo inaczej
	    Game::checkBotProtection anuluje kazde wywolanie g_game.* z timera,
	  * klient MUSI miec wczytane items.otb (patrz game_things), bo bez tego
	    item:getName() zwraca pusty napis i filtrowanie lupu po nazwie nie ma
	    z czym porownywac.

	Okno pokazuje na dole, co bot ROBI I NA CO CZEKA. Poprzednia wersja
	milczala, przez co "czekam, bo many jest 15% a prog to 80%" wygladalo
	dokladnie tak samo jak "zepsute".
]]

morthabotWindow = nil
morthabotButton = nil

local events = {}
local ui = {}
local pendingCorpses = {}
local lastAction = ''

-- Czary do wypalania many. Lista zamiast pola tekstowego - literowka
-- w nazwie czaru po cichu wylaczala caly trening.
local SPELLS = {
	'exura', 'exura vita', 'exura gran', 'exura san',
	'utani hur', 'utani gran hur', 'utevo lux', 'utamo vita',
}

local cfg = {
	manaEnabled  = false,
	manaSpell    = 'exura',
	manaPercent  = 20,       -- rzucaj, gdy many jest CO NAJMNIEJ tyle %
	manaInterval = 1500,

	afkEnabled   = false,
	afkInterval  = 60000,

	foodEnabled  = false,
	foodInterval = 30000,

	lootEnabled  = false,
	lootValuables = true,
	lootList     = '',
}

-- UWAGA na numery przedmiotow. Silnik wysyla do klienta `clientId` z items.otb,
-- a NIE numer serwerowy z items.xml (NetworkMessage::AddItem). Dla zlota jest
-- to 3031, nie 2148 - wpisane na sztywno numery serwerowe nie pasowaly do
-- niczego i dlatego ani jedzenie, ani zbieranie monet nie moglo zadzialac.
-- Dlatego listy trzymamy w numerach SERWEROWYCH (takich jak w items.xml)
-- i tlumaczymy je na klienckie po wejsciu do gry, przez wczytane items.otb.
--
-- Jedzenie: caly zakres 2666-2696 bez maki (2692), ciasta (2693) i pszenicy
-- (2694), ktorych sie nie je.
local FOOD_SERVER_IDS = {
	2666, 2667, 2668, 2669, 2670, 2671, 2672, 2673, 2674, 2675, 2676, 2677,
	2678, 2679, 2680, 2681, 2682, 2683, 2684, 2685, 2686, 2687, 2688, 2689,
	2690, 2691, 2695, 2696,
}

-- To, co zbiera sie zawsze przy wlaczonej opcji "monety i klejnoty".
local VALUABLE_SERVER_IDS = {
	2148, 2152, 2160,                          -- gold, platinum, crystal coin
	2145, 2146, 2147, 2149, 2150,              -- small diamond/sapphire/ruby/emerald/amethyst
	2143, 2144,                                -- white pearl, black pearl
	2151, 2153, 2154, 2157, 2158,              -- talon, violet/yellow gem, gold nugget, blue gem
}

local FOOD_IDS = {}          -- [clientId] = true, wypelniane po wejsciu do gry
local VALUABLE_IDS = {}

local function inList(tab, value)
	for _, v in ipairs(tab) do
		if v == value then return true end
	end
	return false
end

--- Tlumaczy numery serwerowe na klienckie przez wczytane items.otb.
local function resolveIds(serverIds)
	local out, missing = {}, 0
	for _, sid in ipairs(serverIds) do
		local it = g_things.getItemType(sid)
		local cid = it and it:getClientId() or 0
		if cid ~= 0 then
			out[cid] = true
		else
			missing = missing + 1
		end
	end
	return out, missing
end

local function stopEvents()
	for name, ev in pairs(events) do
		if ev then removeEvent(ev) end
		events[name] = nil
	end
	pendingCorpses = {}
end


-- ------------------------------------------------------------ trening many
local function manaTick()
	events.mana = nil
	if not cfg.manaEnabled then return end

	if g_game.isOnline() then
		local player = g_game.getLocalPlayer()
		if player then
			local maxMana = player:getMaxMana()
			local pct = maxMana > 0 and (player:getMana() / maxMana) * 100 or 0
			if maxMana <= 0 then
				lastAction = tr('brak many')
			elseif pct >= cfg.manaPercent then
				g_game.talk(cfg.manaSpell)
				lastAction = string.format('%s (%d%%)', cfg.manaSpell, pct)
			else
				-- To zdanie jest tu po to, zeby "nic sie nie dzieje" mialo powod.
				lastAction = string.format(tr('czekam na mane %d%% z %d%%'),
				                           pct, cfg.manaPercent)
			end
			updateStatus()
		end
	end
	events.mana = scheduleEvent(manaTick, cfg.manaInterval)
end


-- ------------------------------------------------------------------ anti-AFK
local function afkTick()
	events.afk = nil
	if not cfg.afkEnabled then return end

	-- Sam obrot wystarcza serwerowi za aktywnosc, a nie rusza postaci z miejsca.
	if g_game.isOnline() and g_game.getLocalPlayer() then
		local dirs = { North, East, South, West }
		g_game.turn(dirs[math.random(1, #dirs)])
	end
	events.afk = scheduleEvent(afkTick, cfg.afkInterval)
end


-- ------------------------------------------------------------------ jedzenie
local function foodTick()
	events.food = nil
	if not cfg.foodEnabled then return end

	if g_game.isOnline() then
		for _, container in pairs(g_game.getContainers()) do
			for _, item in ipairs(container:getItems()) do
				if FOOD_IDS[item:getId()] then
					g_game.use(item)
					events.food = scheduleEvent(foodTick, cfg.foodInterval)
					return
				end
			end
		end
	end
	events.food = scheduleEvent(foodTick, cfg.foodInterval)
end


-- ------------------------------------------------------------------ autoloot
local function wantedNames()
	local out = {}
	for name in string.gmatch(cfg.lootList:lower(), '([^,]+)') do
		name = name:gsub('^%s*(.-)%s*$', '%1')
		if #name > 0 then table.insert(out, name) end
	end
	return out
end

local function shouldLoot(item)
	if cfg.lootValuables and VALUABLE_IDS[item:getId()] then
		return true
	end
	local wanted = wantedNames()
	if #wanted == 0 then return false end

	-- Nazwa jest dostepna tylko wtedy, gdy klient wczytal items.otb.
	local nm = (item:getName() or ''):lower()
	if nm == '' then return false end
	for _, w in ipairs(wanted) do
		if string.find(nm, w, 1, true) then return true end
	end
	return false
end

--- Zapamietuje miejsce, gdzie padl potwor - zwloki pojawiaja sie chwile pozniej.
function onCreatureDeath(creature)
	if not cfg.lootEnabled or not creature then return end
	if creature:isLocalPlayer() then return end

	local pos = creature:getPosition()
	if pos then
		table.insert(pendingCorpses, { x = pos.x, y = pos.y, z = pos.z, tries = 0 })
	end
end

--- Otwiera zwloki, ktore leza w zasiegu reki. Bez tego autoloot dzialal tylko
--- na pojemnikach otwartych recznie - czyli w praktyce wcale.
local function corpseTick()
	events.corpse = nil
	if not cfg.lootEnabled then return end

	if g_game.isOnline() and #pendingCorpses > 0 then
		local player = g_game.getLocalPlayer()
		local ppos = player and player:getPosition()
		if ppos then
			local keep = {}
			for _, c in ipairs(pendingCorpses) do
				c.tries = c.tries + 1
				local opened = false
				local inReach = c.z == ppos.z
				               and math.abs(c.x - ppos.x) <= 1
				               and math.abs(c.y - ppos.y) <= 1
				if inReach then
					local tile = g_map.getTile({ x = c.x, y = c.y, z = c.z })
					if tile then
						for _, item in ipairs(tile:getItems()) do
							if item:isContainer() then
								g_game.open(item, nil)
								opened = true
								break
							end
						end
					end
				end
				-- Zwloki potrafia dojsc z opoznieniem, ale nie czekamy w nieskonczonosc.
				if not opened and c.tries < 16 then
					table.insert(keep, c)
				end
			end
			pendingCorpses = keep
		end
	end
	events.corpse = scheduleEvent(corpseTick, 500)
end

--- Wywolywane, gdy w grze otworzy sie jakikolwiek pojemnik (m.in. zwloki).
function onContainerOpen(container)
	if not cfg.lootEnabled then return end

	-- Serwer dosyla zawartosc osobnym pakietem, wiec zaraz po otwarciu
	-- lista bywa jeszcze pusta.
	scheduleEvent(function()
		if not cfg.lootEnabled or not g_game.isOnline() then return end
		local player = g_game.getLocalPlayer()
		if not player then return end
		local dest = player:getInventoryItem(InventorySlotBack)
		if not dest then return end

		local taken = 0
		for _, item in ipairs(container:getItems()) do
			if shouldLoot(item) then
				g_game.move(item, dest:getPosition(), item:getCount())
				taken = taken + 1
			end
		end
		if taken > 0 then
			lastAction = string.format(tr('zebrano %d rzeczy'), taken)
			updateStatus()
		end
	end, 300)
end


-- ------------------------------------------------------------------ interfejs
function updateStatus()
	if not ui.statusLabel then return end

	local on = {}
	if cfg.manaEnabled then table.insert(on, tr('mana')) end
	if cfg.afkEnabled  then table.insert(on, tr('anti-AFK')) end
	if cfg.foodEnabled then table.insert(on, tr('jedzenie')) end
	if cfg.lootEnabled then table.insert(on, tr('loot')) end

	if #on == 0 then
		ui.statusLabel:setText(tr('Wszystko wylaczone.'))
		return
	end
	local text = table.concat(on, ', ')
	if lastAction ~= '' then
		text = text .. '\n' .. lastAction
	end
	ui.statusLabel:setText(text)
end

local function saveSettings()
	g_settings.set('morthabot-mana',      cfg.manaEnabled)
	g_settings.set('morthabot-spell',     cfg.manaSpell)
	g_settings.set('morthabot-percent',   cfg.manaPercent)
	g_settings.set('morthabot-afk',       cfg.afkEnabled)
	g_settings.set('morthabot-food',      cfg.foodEnabled)
	g_settings.set('morthabot-loot',      cfg.lootEnabled)
	g_settings.set('morthabot-loot-val',  cfg.lootValuables)
	g_settings.set('morthabot-loot-list', cfg.lootList)
end

--- Czyta okno, restartuje timery i zapisuje ustawienia. Wolane po KAZDEJ
--- zmianie - nie ma przycisku "Zapisz", bo zaznaczenie kratki i tak znaczy
--- "wlacz to teraz".
function apply()
	cfg.manaEnabled   = ui.manaBox:isChecked()
	cfg.manaSpell     = ui.manaSpell:getCurrentOption().text
	cfg.manaPercent   = ui.manaPercent:getValue()
	cfg.afkEnabled    = ui.afkBox:isChecked()
	cfg.foodEnabled   = ui.foodBox:isChecked()
	cfg.lootEnabled   = ui.lootBox:isChecked()
	cfg.lootValuables = ui.lootGoldBox:isChecked()
	cfg.lootList      = ui.lootList:getText()

	stopEvents()
	lastAction = ''
	if cfg.manaEnabled then events.mana   = scheduleEvent(manaTick, 500) end
	if cfg.afkEnabled  then events.afk    = scheduleEvent(afkTick, cfg.afkInterval) end
	if cfg.foodEnabled then events.food   = scheduleEvent(foodTick, 2000) end
	if cfg.lootEnabled then events.corpse = scheduleEvent(corpseTick, 500) end

	updateStatus()
	saveSettings()
end

function toggle()
	if morthabotButton:isOn() then
		morthabotWindow:close()
		morthabotButton:setOn(false)
	else
		morthabotWindow:open()
		morthabotButton:setOn(true)
	end
end

function onMiniWindowClose()
	morthabotButton:setOn(false)
end

--- Numery przedmiotow tlumaczymy dopiero po wejsciu do gry: items.otb wczytuje
--- sie razem z Tibia.dat przy wyborze wersji klienta, czyli PO zaladowaniu
--- tego modulu.
function onGameStart()
	local missFood, missVal
	FOOD_IDS, missFood = resolveIds(FOOD_SERVER_IDS)
	VALUABLE_IDS, missVal = resolveIds(VALUABLE_SERVER_IDS)
	if not g_things.isOtbLoaded() then
		g_logger.warning('MorthaBot: brak items.otb - autoloot i jedzenie nie beda dzialac')
	elseif missFood > 0 or missVal > 0 then
		g_logger.info(('MorthaBot: nie rozwiazalem %d numerow jedzenia i %d kosztownosci')
		              :format(missFood, missVal))
	end
end

function init()
	morthabotWindow = g_ui.loadUI('morthabot', modules.game_interface.getRightPanel())

	for _, id in ipairs({ 'manaBox', 'manaSpell', 'manaPercent', 'afkBox',
	                      'foodBox', 'lootBox', 'lootGoldBox', 'lootList',
	                      'statusLabel' }) do
		ui[id] = morthabotWindow:recursiveGetChildById(id)
	end

	for _, spell in ipairs(SPELLS) do
		ui.manaSpell:addOption(spell)
	end
	ui.manaPercent:setMinimum(0)
	ui.manaPercent:setMaximum(100)

	cfg.manaEnabled   = g_settings.getBoolean('morthabot-mana')
	cfg.manaSpell     = g_settings.getString('morthabot-spell')
	cfg.manaPercent   = g_settings.getNumber('morthabot-percent')
	cfg.afkEnabled    = g_settings.getBoolean('morthabot-afk')
	cfg.foodEnabled   = g_settings.getBoolean('morthabot-food')
	cfg.lootEnabled   = g_settings.getBoolean('morthabot-loot')
	cfg.lootList      = g_settings.getString('morthabot-loot-list')
	cfg.lootValuables = not g_settings.exists('morthabot-loot-val')
	                    or g_settings.getBoolean('morthabot-loot-val')
	if not inList(SPELLS, cfg.manaSpell) then cfg.manaSpell = SPELLS[1] end
	if cfg.manaPercent == nil or cfg.manaPercent <= 0 then cfg.manaPercent = 20 end

	ui.manaBox:setChecked(cfg.manaEnabled)
	ui.manaSpell:setCurrentOption(cfg.manaSpell, true)
	ui.manaPercent:setValue(cfg.manaPercent)
	ui.afkBox:setChecked(cfg.afkEnabled)
	ui.foodBox:setChecked(cfg.foodEnabled)
	ui.lootBox:setChecked(cfg.lootEnabled)
	ui.lootGoldBox:setChecked(cfg.lootValuables)
	ui.lootList:setText(cfg.lootList)
	updateStatus()

	-- Kazda zmiana dziala natychmiast.
	for _, id in ipairs({ 'manaBox', 'afkBox', 'foodBox', 'lootBox', 'lootGoldBox' }) do
		connect(ui[id], { onCheckChange = apply })
	end
	connect(ui.manaSpell,   { onOptionChange = apply })
	connect(ui.manaPercent, { onValueChange = apply })
	connect(ui.lootList,    { onTextChange = apply })

	morthabotButton = modules.client_topmenu.addRightGameToggleButton(
		'morthabotButton', tr('MorthaBot'), '/images/topbuttons/morthabot', toggle)

	morthabotWindow:setup()
	-- Przy pierwszym uruchomieniu okno stoi zamkniete, zeby nie zabieralo
	-- miejsca w panelu. Kolejne uruchomienia pamietaja wybor gracza.
	local saved = g_settings.getNode('MiniWindows')
	if not saved or not saved['morthabotWindow'] then
		morthabotWindow:close()
	end
	morthabotButton:setOn(morthabotWindow:isVisible())

	connect(g_game, {
		onGameStart = onGameStart,
		onGameEnd = stopEvents,
		onContainerOpen = onContainerOpen,
	})
	connect(Creature, { onDeath = onCreatureDeath })

	if g_game.isOnline() then
		onGameStart()
	end

	if cfg.manaEnabled or cfg.afkEnabled or cfg.foodEnabled or cfg.lootEnabled then
		apply()
	end
end

function terminate()
	stopEvents()
	disconnect(g_game, {
		onGameEnd = stopEvents,
		onContainerOpen = onContainerOpen,
	})
	disconnect(Creature, { onDeath = onCreatureDeath })
	if morthabotButton then morthabotButton:destroy() end
	if morthabotWindow then morthabotWindow:destroy() end
end
