HIRELING_CREDITS = {
	Developer = 'Leonardo "Leu" Pereira (jlcvp)',
	Version = "1.0-CoronaVaires",
	Date = "29/04/2020"
}

local DEBUG = true -- print debug to console

HIRELING_CACHE_STORAGE = {}
HIRELINGS = {}
PLAYER_HIRELINGS = {}
HIRELING_OUTFIT_CHANGING = {}

function DebugPrint(str)
	if DEBUG == true then
		Spdlog.debug(str)
	end
end

function printTable(t)
	local str = '{'

		for k,v in pairs(t) do
			str = str .. string.format( "\n %s = %s",tostring(k), tostring(v))
		end
	str = str.. '\n}'
	Spdlog.debug(str)
end

-- [[ Constants and ENUMS ]]

HIRELING_SKILLS = {
	BANKER = 1, -- 1<<0
	COOKING = 2, -- 1<<1
	STEWARD = 4, -- 1<<2
	TRADER = 8 -- 1<<3
}

HIRELING_SEX = {
	FEMALE = 0,
	MALE = 1
}

HIRELING_OUTFIT_DEFAULT = { name = "Citizen", female = 1107, male = 1108 }

HIRELING_OUTFITS = {
	BANKER = 1, -- 1<<0
	COOKING = 2, -- 1<<1
	STEWARD = 4, -- 1<<2
	TRADER = 8, -- 1<<3 ...
	SERVANT = 16,
	HYDRA = 32,
	FERUMBRAS = 64,
	BONELORD = 128,
	DRAGON = 256
}

HIRELING_OUTFITS_TABLE = {
	BANKER = {name = "Banker Dress", female = 1109, male = 1110},
	BONELORD = {name = "Bonelord Dress", female = 1123, male = 1124},
	COOKING = {name = "Cook Dress", female = 1113, male = 1114},
	DRAGON = {name = "Dragon Dress", female = 1125, male = 1126},
	FERUMBRAS = {name = "Ferumbras Dress", female = 1131, male = 1132},
	HYDRA = {name = "Hydra Dress", female = 1129, male = 1130},
	SERVANT = {name = "Servant Dress", female = 1117, male = 1118},
	STEWARD = {name = "Stewart Dress", female = 1115, male = 1116},
	TRADER = {name = "Trader Dress", female = 1111, male = 1112}
}

HIRELING_STORAGE = {
	SKILL = 28800,
	OUTFIT = 28900
}

HIRELING_LAMP_ID = 29432
HIRELING_ATTRIBUTE = "HIRELING_ID"

HIRELING_FOODS_BOOST = {
	MAGIC = 29410,
	MELEE = 29411,
	SHIELDING = 29408,
	DISTANCE = 35173,
}

HIRELING_FOODS = { -- only the non-skill ones
	29412, 29413, 29414, 29415, 29416
}

-- [[ LOCAL FUNCTIONS AND UTILS ]]

local function checkHouseAccess(hireling)
	--check if owner still have access to the house
	if not hireling or hireling.active == 0 then
		return false
	end

	local tile = hireling:getPosition():getTile()
	if not tile then
		return false
	end
	local house = tile:getHouse()
	if not house then
		return false
	end
	local player = Player(hireling:getOwnerId())
	if not player then
		player = Game.getOfflinePlayer(hireling:getOwnerId())
	end

	if house:getOwnerGuid() == hireling:getOwnerId() then return true end

	-- player is not invited anymore, return to lamp
	Spdlog.info("Returning Hireling:" .. hireling:getName() .. " to owner Inbox")
	local inbox = player:getSlotItem(CONST_SLOT_STORE_INBOX)
	-- Using FLAG_NOLIMIT to avoid losing the hireling after being kicked out of the house and having no slots available in the store inbox
	local lamp = inbox:addItem(HIRELING_LAMP_ID, 1, INDEX_WHEREEVER, FLAG_NOLIMIT)
	lamp:setAttribute(ITEM_ATTRIBUTE_DESCRIPTION, "This mysterious lamp summons your very own personal hireling.\nThis item cannot be traded.\nThis magic lamp is the home of " .. hireling:getName() .. ".")
	lamp:setSpecialAttribute(HIRELING_ATTRIBUTE, hireling:getId()) --save hirelingId on item
	player:save()
	hireling.active = 0
	hireling.cid = -1
	hireling:setPosition({x=0,y=0,z=0})

end

local function spawnNPCs()
	Spdlog.info("Spawning Hirelings")
	local hireling
	for i=1,#HIRELINGS do
		hireling = HIRELINGS[i]

		if checkHouseAccess(hireling) then
			hireling:spawn()
		end
	end
end

local function addStorageCacheValue(player_id, storage, value)
	if not HIRELING_CACHE_STORAGE[player_id] then
		HIRELING_CACHE_STORAGE[player_id] = {}
	end
	HIRELING_CACHE_STORAGE[player_id][storage] = value
end

local function initStorageCache()
	local sql = string.format("SELECT `player_id`, `key`, `value` FROM `player_storage` "..
	"WHERE `key` IN (%d,%d)", HIRELING_STORAGE.SKILL, HIRELING_STORAGE.OUTFIT)

	local resultId = db.storeQuery(sql)
	if resultId ~= false then
		local player_id, key, value
		repeat
			player_id = Result.getNumber(resultId,"player_id")
			key = Result.getNumber(resultId,"key")
			value = Result.getNumber(resultId,"value")

			addStorageCacheValue(player_id, key, value)
		until not Result.next(resultId)
		Result.free(resultId)
	end
end

local function getStorageForPlayer(player_id, storage)
	local player = Player(player_id)
	if player then
		return player:getStorageValue(storage)
	else
		return HIRELING_CACHE_STORAGE[player_id] and HIRELING_CACHE_STORAGE[player_id][storage] or -1
	end
end

-- [[ DEFINING HIRELING CLASS ]]

Hireling = {
	id = -1,
	player_id = -1,
	name = 'hireling',
	skills = 0,
	active = 0,
	sex = 0,
	posx = 0,
	posy = 0,
	posz = 0,
	lookbody = 34,
	lookfeet = 116,
	lookhead = 97,
	looklegs = 3,
	looktype = 0,
	cid = -1
}

function Hireling:new(o)
	o = o or {}
	setmetatable(o, self)
	self.__index = self
	return o
end

function Hireling:getOwnerId()
	return self.player_id
end

function Hireling:getId()
	return self.id
end

function Hireling:getName()
	return self.name
end

function Hireling:canTalkTo(player)
	if not player then return false end

	local tile = Tile(player:getPosition())
	if not tile then return false end
	local house = tile:getHouse()
	if not house then return false end

	local hirelingHouse = Tile(self:getPosition()):getHouse()

	return house:getId() == hirelingHouse:getId()
end

function Hireling:getPosition()
	return Position(self.posx,self.posy, self.posz)
end

function Hireling:setPosition(pos)
	self.posx = pos.x
	self.posy = pos.y
	self.posz = pos.z
end

function Hireling:getOutfit()
	local outfit = 	{
		lookType=self.looktype,
		lookHead=self.lookhead,
		lookAddons=0,
		lookMount=0,
		lookLegs=self.looklegs,
		lookBody=self.lookbody,
		lookFeet=self.lookfeet
	}

	return outfit
end

function Hireling:getAvailableOutfits()

	local flags = getStorageForPlayer(self:getOwnerId(),HIRELING_STORAGE.OUTFIT)
	local sex = (self.sex == HIRELING_SEX.FEMALE) and 'female' or 'male'

	local outfits = {}
	-- add default outfit
	table.insert(outfits, { name = HIRELING_OUTFIT_DEFAULT.name, lookType = HIRELING_OUTFIT_DEFAULT[sex] })
	if flags >0 then
		local outfit
		for key, value in pairs(HIRELING_OUTFITS) do
			if hasBitSet(value, flags) then
				outfit = {
					name = HIRELING_OUTFITS_TABLE[key].name,
					lookType = HIRELING_OUTFITS_TABLE[key][sex]
				}
				table.insert(outfits, outfit)
			end
		end
	end

	return outfits
end

function Hireling:requestOutfitChange()
	local player = Player(self:getOwnerId())

	HIRELING_OUTFIT_CHANGING[self:getOwnerId()] = self:getId()
	player:sendHirelingOutfitWindow(self)
end

function Hireling:hasOutfit(lookType)
	local outfits = self:getAvailableOutfits()
	local found = false
	for _,outfit in ipairs(outfits) do
		if outfit.lookType == lookType then
			return true
		end
	end

	return found
end

function Hireling:setOutfit(outfit)
	self.looktype = outfit.lookType
	self.lookhead = outfit.lookHead
	self.lookbody = outfit.lookBody
	self.looklegs = outfit.lookLegs
	self.lookfeet = outfit.lookHead
	self.lookAddons = outfit.lookAddons
end

function Hireling:changeOutfit(outfit)
	HIRELING_OUTFIT_CHANGING[self:getOwnerId()] = nil --clear flag

	if not self:hasOutfit(outfit.lookType) then return end

	local npc = Npc(self.cid)
	local creature = Creature(npc) --maybe self.cid works here too

	creature:setOutfit(outfit)
	self:setOutfit(outfit)
end


function Hireling:hasSkill(SKILL)
	local skills = getStorageForPlayer(self:getOwnerId(), HIRELING_STORAGE.SKILL)
	if skills <= 0 then
		return false
	else
		return hasBitSet(SKILL, skills)
	end
end

function Hireling:setCreature(cid)
	self.cid = cid
end

function Hireling:save()
	local sql = "UPDATE `player_hirelings` SET"
	sql = sql .. " `name`=" .. db.escapeString(self.name)
	sql = sql .. ", `active`=" .. tostring(self.active)
	sql = sql .. ", `sex`=" .. tostring(self.sex)
	sql = sql .. ", `posx`=" .. tostring(self.posx)
	sql = sql .. ", `posy`=" .. tostring(self.posy)
	sql = sql .. ", `posz`=" .. tostring(self.posz)
	sql = sql .. ", `lookbody`=" .. tostring(self.lookbody)
	sql = sql .. ", `lookfeet`=" .. tostring(self.lookfeet)
	sql = sql .. ", `lookhead`=" .. tostring(self.lookhead)
	sql = sql .. ", `looklegs`=" .. tostring(self.looklegs)
	sql = sql .. ", `looktype`=" .. tostring(self.looktype)

	sql = sql .. " WHERE `id`=" .. tostring(self.id)

	db.query(sql)
end

function Hireling:spawn()
	self.active = 1
	-- Creating new hireling with player choose name
	createHirelingType(self:getName())

	local npc = Npc(Game.generateNpc(self:getName()))
	npc:setName(self:getName())
	local creature = Creature(npc)
	creature:setOutfit(self:getOutfit())
	npc:setSpeechBubble(7)

	npc:place(self:getPosition())
	creature:getPosition():sendMagicEffect(CONST_ME_TELEPORT)
	self:setCreature(npc:getId())
end

function Hireling:returnToLamp(player_id)
	if self.active ~= 1 then
		return
	end

	local player = Player(player_id)
	if self:getOwnerId() ~= player_id then
		player:getPosition():sendMagicEffect(CONST_ME_POFF)
		return player:sendTextMessage(MESSAGE_FAILURE, "You are not the master of this hireling.")
	end

	self.active = 0
	addEvent(function(npcId, ownerGuid, hirelingId)
		local npc = Npc(npcId)
		if not npc then
			return Spdlog.error("[Hireling:returnToLamp] - Npc not found or is nil.")
		end

		local owner = Player(ownerGuid)
		if not owner then
			return
		end

		local lampType = ItemType(HIRELING_LAMP_ID)
		if owner:getFreeCapacity() < lampType:getWeight(1) then
			owner:getPosition():sendMagicEffect(CONST_ME_POFF)
			return owner:sendTextMessage(MESSAGE_FAILURE, "You do not have enough capacity.")
		end

		local inbox = owner:getSlotItem(CONST_SLOT_STORE_INBOX)
		if not inbox or inbox:getEmptySlots() < 1 then
			owner:getPosition():sendMagicEffect(CONST_ME_POFF)
			return owner:sendTextMessage(MESSAGE_FAILURE, "You don't have enough room in your inbox.")
		end

		local hireling = getHirelingById(hirelingId)
		if not hireling then
			return Spdlog.error("[Hireling:returnToLamp] - Hireling not found or is nil.")
		end

		npc:say("As you wish!",	TALKTYPE_PRIVATE_NP, false, owner, npc:getPosition())
		local lamp = inbox:addItem(HIRELING_LAMP_ID, 1, INDEX_WHEREEVER, FLAG_NOLIMIT)
		npc:getPosition():sendMagicEffect(CONST_ME_PURPLESMOKE)
		npc:remove() --remove hireling
		lamp:setAttribute(ITEM_ATTRIBUTE_DESCRIPTION, "This mysterious lamp summons your very own personal hireling.\nThis item cannot be traded.\nThis magic lamp is the home of " .. self:getName() .. ".")
		lamp:setSpecialAttribute(HIRELING_ATTRIBUTE, hirelingId) --save hirelingId on item
		hireling:setPosition({x=0,y=0,z=0})
	end, 1000, self.cid, player:getGuid(), self.id)
end
-- [[ END CLASS DEFINITION ]]

-- [[ GLOBAL FUNCTIONS DEFINITIONS ]]

function SaveHirelings()
	for _, hireling in ipairs(HIRELINGS) do
		hireling:save()
	end
end
function getHirelingById(id)
	local hireling
	for i = 1, #HIRELINGS do
		hireling = HIRELINGS[i]
		if hireling:getId() == id then
			return hireling
		end
	end
	return nil
end

function getHirelingByPosition(position)
	local hireling
	for i = 1, #HIRELINGS do
		hireling = HIRELINGS[i]
		if hireling.posx == position.x and hireling.posy == position.y and hireling.posz == position.z then
			return hireling
		end
	end
	return nil
end

function HirelingsInit()
	local rows = db.storeQuery("SELECT * FROM `player_hirelings`")

	if rows then
		repeat
			local player_id = Result.getNumber(rows, "player_id")

			if not PLAYER_HIRELINGS[player_id] then
				PLAYER_HIRELINGS[player_id] = {}
			end

			local hireling = Hireling:new()
			hireling.id = Result.getNumber(rows, "id")
			hireling.player_id = player_id
			hireling.name = Result.getString(rows, "name")
			hireling.active = Result.getNumber(rows, "active")
			hireling.sex = Result.getNumber(rows, "sex")
			hireling.posx = Result.getNumber(rows, "posx")
			hireling.posy = Result.getNumber(rows, "posy")
			hireling.posz = Result.getNumber(rows, "posz")
			hireling.lookbody = Result.getNumber(rows, "lookbody")
			hireling.lookfeet = Result.getNumber(rows, "lookfeet")
			hireling.lookhead = Result.getNumber(rows, "lookhead")
			hireling.looklegs = Result.getNumber(rows, "looklegs")
			hireling.looktype = Result.getNumber(rows, "looktype")

			table.insert(PLAYER_HIRELINGS[player_id], hireling)
			table.insert(HIRELINGS, hireling)
		until not Result.next(rows)
		Result.free(rows)

		spawnNPCs()
		initStorageCache()

	end
end

function PersistHireling(hireling)

	db.query(string.format("INSERT INTO `player_hirelings` (`player_id`,`name`,`active`,`sex`,`posx`,`posy`,`posz`,`lookbody`,`lookfeet`,`lookhead`,`looklegs`,`looktype`) VALUES (%d, %s, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d)",
	hireling.player_id, db.escapeString(hireling.name), hireling.active, hireling.sex, hireling.posx, hireling.posy, hireling.posz, hireling.lookbody, hireling.lookfeet, hireling.lookhead, hireling.looklegs, hireling.looktype)
	)

	local hirelings = PLAYER_HIRELINGS[hireling.player_id] or {}
	local ids = ""
	for i=1,#hirelings do
		if i > 1 then
			ids = ids .. "','"
		end
		ids = ids .. tostring(hirelings[i].id)
	end
	local query = string.format("SELECT `id` FROM `player_hirelings` WHERE `player_id`= %d and `id` NOT IN ('%s')", hireling.player_id, ids)
	local resultId = db.storeQuery(query)

	if resultId then
		local id = Result.getNumber(resultId, 'id')
		hireling.id = id
		return true
	else
		return false
	end
end

-- [[ END GLOBAL FUNCTIONS ]]

-- [[ Player extension ]]
function Player:getHirelings()
	return PLAYER_HIRELINGS[self:getGuid()] or {}
end

function Player:getHirelingsCount()
	local hirelings = self:getHirelings()
	return #hirelings
end

function Player:addNewHireling(name, sex)
	local hireling = Hireling:new()
	hireling.name = name
	hireling.player_id = self:getGuid()
	if sex == HIRELING_SEX.FEMALE then
		hireling.looktype=136 -- citizen female
		hireling.sex = HIRELING_SEX.FEMALE
	else
		hireling.looktype=128 -- citizen male
		hireling.sex = HIRELING_SEX.MALE
	end

	local lampType = ItemType(HIRELING_LAMP_ID)
	if self:getFreeCapacity() < lampType:getWeight(1) then
		self:getPosition():sendMagicEffect(CONST_ME_POFF)
		self:sendTextMessage(MESSAGE_FAILURE, "You do not have enough capacity.")
		return false
	end

	local inbox = self:getSlotItem(CONST_SLOT_STORE_INBOX)
	if not inbox or inbox:getEmptySlots() < 1 then
		self:getPosition():sendMagicEffect(CONST_ME_POFF)
		self:sendTextMessage(MESSAGE_FAILURE, "You don't have enough room in your inbox.")
		return false
	end

	local saved = PersistHireling(hireling)
	if not saved then
		DebugPrint('Error saving Hireling:' .. name .. ' - player:' .. self:getName())
		return false
	else
		if not PLAYER_HIRELINGS[self:getGuid()] then
			PLAYER_HIRELINGS[self:getGuid()] = {}
		end
		table.insert(PLAYER_HIRELINGS[self:getGuid()], hireling)
		table.insert(HIRELINGS, hireling)
		local lamp = inbox:addItem(HIRELING_LAMP_ID, 1, INDEX_WHEREEVER, FLAG_NOLIMIT)
		lamp:setAttribute(ITEM_ATTRIBUTE_DESCRIPTION, "This mysterious lamp summons your very own personal hireling.\nThis item cannot be traded.\nThis magic lamp is the home of " .. hireling:getName() .. ".")
		lamp:setSpecialAttribute(HIRELING_ATTRIBUTE, hireling:getId()) --save hirelingId on item
		hireling.active = 0
		return hireling
	end
end

function Player:isChangingHirelingOutfit()
	return HIRELING_OUTFIT_CHANGING[self:getGuid()] and HIRELING_OUTFIT_CHANGING[self:getGuid()] > 0 or false
end

function Player:getHirelingChangingOutfit()
	local id = HIRELING_OUTFIT_CHANGING[self:getGuid()]
	if not id then
		return nil
	end

	return getHirelingById(id)
end

function Player:sendHirelingOutfitWindow(hireling)
	local outfit = hireling:getOutfit()
	local msg = NetworkMessage()
	msg:addByte(0xC8) -- 'ProtocolGame::sendOutfitWindow()'' header
	msg:addU16(outfit.lookType)

	if outfit.lookType == 0 then
		msg:addU16(outfit.lookTypeEx)
	else
		msg:addByte(outfit.lookHead)
		msg:addByte(outfit.lookBody)
		msg:addByte(outfit.lookLegs)
		msg:addByte(outfit.lookFeet)
		msg:addByte(outfit.lookAddons)
	end
	msg:addU16(outfit.lookMount)

	msg:addByte(0x00) -- Mount head
	msg:addByte(0x00) -- Mount body
	msg:addByte(0x00) -- Mount legs
	msg:addByte(0x00) -- Mount feet
	msg:addU16(0x00) -- Familiar

	local availableOutfits = hireling:getAvailableOutfits()
	msg:addU16(#availableOutfits)
	for _,outfit in ipairs(availableOutfits) do
		msg:addU16(outfit.lookType)
		msg:addString(outfit.name)
		msg:addByte(0x00) -- addons
		msg:addByte(0x00) -- Store bool
	end

	msg:addU16(0x00) -- Mounts count
	msg:addU16(0x00) -- Familiar count
	msg:addByte(0x00) -- Try outfit bool
	msg:addByte(0x00) -- Is mounted bool
	msg:addByte(0x00) -- Random outfit bool

	msg:sendToPlayer(self)
end

function Player:hasHirelings()
	return PLAYER_HIRELINGS[self:getGuid()] and #PLAYER_HIRELINGS[self:getGuid()] > 0 or false
end

function Player:copyHirelingStorageToCache()
	if(self:hasHirelings()) then
		local storageSkill = self:getStorageValue(HIRELING_STORAGE.SKILL)
		local storageOutfit = self:getStorageValue(HIRELING_STORAGE.OUTFIT)
		addStorageCacheValue(self:getGuid(),HIRELING_STORAGE.SKILL, storageSkill)
		addStorageCacheValue(self:getGuid(),HIRELING_STORAGE.OUTFIT, storageOutfit)
	end
end

function Player:findHirelingLamp(hirelingId)
	local inbox = self:getSlotItem(CONST_SLOT_STORE_INBOX)
	if not inbox then return nil end

	local lastIndex = inbox:getSize() - 1
	for i=0,lastIndex do
		local item = inbox:getItem(i)
		if item and item:getId() == HIRELING_LAMP_ID and item:getSpecialAttribute(HIRELING_ATTRIBUTE) == hirelingId then
			return item
		end
	end
	return nil
end

function Player:sendHirelingSelectionModal(title, message, callback, data)
	-- callback(playerId, data, hireling)
	-- get all hireling list
	local hirelings = self:getHirelings()
	local modal = ModalWindow {
		title = title,
		message = message
	}
	local hireling
	for i=1,#hirelings do
		hireling = hirelings[i]
		local choice = modal:addChoice(string.format('#%d - %s', i, hireling:getName()))
		choice.hireling = hireling
	end

	local playerId = self:getId()
	local internalConfirm = function(button, choice)
		local hrlng = choice and choice.hireling or nil
		callback(playerId, data, hrlng)
	end

	local internalCancel = function(btn, choice) callback(playerId, data, nil) end

	modal:addButton('Select',internalConfirm)
	modal:setDefaultEnterButton('Select')
	modal:addButton('Cancel',internalCancel)
	modal:setDefaultEscapeButton('Cancel')

	modal:sendToPlayer(self)
end

function Player:showInfoModal(title, message, buttonText)
	local modal = ModalWindow {
		title = title,
		message = message
	}
	buttonText = buttonText or 'Close'
	modal:addButton(buttonText,function()end)
	modal:setDefaultEscapeButton(buttonText)

	modal:sendToPlayer(self)
end

function Player:hasHirelingSkill(SKILL)
	local skills = self:getStorageValue(HIRELING_STORAGE.SKILL)
	if skills <= 0 then
		return false
	else
		return hasBitSet(SKILL, skills)
	end
end

function Player:enableHirelingSkill(SKILL)
	local skills = self:getStorageValue(HIRELING_STORAGE.SKILL)
	if skills < 0 then skills = 0 end
	skills = setFlag(SKILL, skills)
	self:setStorageValue(HIRELING_STORAGE.SKILL, skills)
	self:copyHirelingStorageToCache()
end

function Player:hasHirelingOutfit(OUTFIT)
	local outfits = self:getStorageValue(HIRELING_STORAGE.OUTFIT)
	if outfits <= 0 then
		return false
	else
		return hasBitSet(OUTFIT, outfits)
	end
end

function Player:enableHirelingOutfit(OUTFIT)
	local outfits = self:getStorageValue(HIRELING_STORAGE.OUTFIT)
	if outfits < 0 then outfits = 0 end
	outfits = setFlag(OUTFIT, outfits)
	self:setStorageValue(HIRELING_STORAGE.OUTFIT, outfits)
	self:copyHirelingStorageToCache()
end
-- [[ END PLAYER EXTENSION ]]
