--[[---------------------------------------------
-------------------------------------------------
--
-- ISBaseIcon
--
-- eris
--
-------------------------------------------------
--]]---------------------------------------------
require "Foraging/forageSystem";
require "ISUI/ISPanel";
ISBaseIcon = ISPanel:derive("ISBaseIcon");
-------------------------------------------------
-------------------------------------------------
ISBaseIcon.managedMarkers = {
	isoMarker	= "isoMarkers",
	worldMarker	= "worldMarkers",
};

ISBaseIcon.updateEvents          = {
	{ method = "updateViewDistance",        tick = 5,     breakTick = false },
	{ method = "updatePerkLevel",           tick = 20,    breakTick = false },
	{ method = "updateModifiers",           tick = 30,    breakTick = false },
};
-------------------------------------------------
-------------------------------------------------
local pinIconBlank      = getTexture("media/textures/Foraging/pinIconBlank.png");
local pinIconUnknown    = getTexture("media/textures/Foraging/pinIconUnknown.png");
local poisonIcon        = getTexture("media/ui/SkullPoison.png");
-------------------------------------------------
-------------------------------------------------
local math = math;
local getTimestampMs = getTimestampMs;
-------------------------------------------------
-------------------------------------------------
local function iterList(_list)
	local list = _list;
	local size = list:size() - 1;
	local i = -1;
	return function()
		i = i + 1;
		if i <= size and not list:isEmpty() then
			return list:get(i), i;
		end;
	end;
end

local function clamp(_value, _min, _max)
	if _min > _max then _min, _max = _max, _min; end;
	return math.min(math.max(_value, _min), _max);
end
-------------------------------------------------
-------------------------------------------------
function ISBaseIcon:isValid() 				return true; 					end;
function ISBaseIcon:onRightMouseUp() 		return false; 					end;
function ISBaseIcon:onRightMouseDown()		return false;					end;
function ISBaseIcon:doPickup()				return false;					end;
-------------------------------------------------
-------------------------------------------------
function ISBaseIcon:getAlpha()		 		return self.textureColor.a; 	end;
function ISBaseIcon:getColor()			 	return self.textureColor; 		end;
function ISBaseIcon:setAlpha(_a)			self.textureColor.a = _a;		end;
function ISBaseIcon:setColor(_rgba) 		self.textureColor = _rgba;		end;
-------------------------------------------------
-------------------------------------------------
function ISBaseIcon:prerender() 											end;
function ISBaseIcon:renderWorldIcon() 										end;
-------------------------------------------------
-------------------------------------------------
function ISBaseIcon:getGridSquare()			return self:initGridSquare();	end;
-------------------------------------------------
-------------------------------------------------
function ISBaseIcon:doSearchFocusCheck()									end;
function ISBaseIcon:setCanRollForSearchFocus()								end;
-------------------------------------------------
-------------------------------------------------
function ISBaseIcon:setIsBeingRemoved(_isBeingRemoved)
	self.isBeingRemoved = _isBeingRemoved;
end;
-------------------------------------------------
-------------------------------------------------
function ISBaseIcon:getGameSpeed()
	if UIManager and UIManager.getSpeedControls then
		return UIManager.getSpeedControls():getCurrentGameSpeed();
	end;
	return 1;
end
-------------------------------------------------
-------------------------------------------------
--todo: refactor - move grab menu constructor to a new method
function ISBaseIcon:doContextMenu(_context)
	if self:getIsSeen() and self:getAlpha() > 0 then
		self:getGridSquare();
		if (not self.square) then return; end;
		--
		local contextMenu;
		if _context then
			local forageSubOption = _context:addOptionOnTop(getText("IGUI_perks_Foraging"), self);
			contextMenu = ISContextMenu:getNew(_context);
			contextMenu:addSubMenu(forageSubOption, contextMenu);
		else
			contextMenu = ISContextMenu.get(self.player, getMouseX(), getMouseY());
		end
		--
		if not contextMenu then return; end;
		--
		local plInventory = self.character:getInventory();
		local plInventoryHasSpace = (plInventory:getCapacityWeight() <= plInventory:getEffectiveCapacity(self.character));
		local contextName = getText("IGUI_Pickup").." "..getText("UI_foraging_UnknownItem");
		--
		if self.identified then contextName = getText("IGUI_Pickup").." "..self.itemObj:getDisplayName(); end;
		--
		local contextOption = contextMenu:addOption(contextName, self, nil, contextMenu, plInventory);
		local subMenu = ISContextMenu:getNew(contextMenu);
		local bpList = getPlayerInventory(self.player).backpacks;
		--
		local plInvOption = subMenu:addOption(getText("ContextMenu_PutInContainer", getText("ContextMenu_MoveToInventory")), self, self.onClickContext, 0, 0, contextMenu, plInventory, {self.itemObj});
		if plInventory:hasRoomFor(self.character, self.itemObj) and plInventoryHasSpace then
			if self.itemObjTable then
				local itemTable = {};
				for _, itemObj in pairs(self.itemObjTable) do
					if itemObj and itemObj:getWorldItem() then
						table.insert(itemTable, itemObj);
					end;
				end;
				if #itemTable > 1 then
					plInvOption.onSelect = nil
					local subMenuGrab = ISContextMenu:getNew(contextMenu);
					contextMenu:addSubMenu(plInvOption, subMenuGrab);
					subMenuGrab:addOption(getText("ContextMenu_Grab_one"), self, self.onClickContext, 0, 0, contextMenu, plInventory, {self.itemObj});
					if #itemTable > 2 then
						subMenuGrab:addOption(getText("ContextMenu_Grab_half"), self, self.onClickContext, 0, 0, contextMenu, plInventory, {unpack(itemTable, 1, math.ceil(#itemTable / 2))});
					end;
					subMenuGrab:addOption(getText("ContextMenu_Grab_all"), self, self.onClickContext, 0, 0, contextMenu, plInventory, itemTable);
				end;
			end;
		else
			plInvOption.onSelect = nil;
			plInvOption.notAvailable = true;
		end;
		for _, backpack in ipairs(bpList) do
			local bpItem = backpack and backpack.inventory and backpack.inventory:getContainingItem();
			if bpItem then
				if not backpack.inventory:getOnlyAcceptCategory() or (self.itemObj:getCategory() == backpack.inventory:getOnlyAcceptCategory()) then
					local backPackOption = subMenu:addOption(getText("ContextMenu_PutInContainer", bpItem:getDisplayName()), self, self.onClickContext, 0, 0, contextMenu, backpack.inventory, {self.itemObj});
					if (not backpack.inventory:hasRoomFor(self.character, self.itemObj)) or (not plInventoryHasSpace) then
						backPackOption.onSelect = nil;
						backPackOption.notAvailable = true;
					else
						if self.itemObjTable then
							local itemTable = {};
							for _, itemObj in pairs(self.itemObjTable) do
								if itemObj and itemObj:getWorldItem() then
									table.insert(itemTable, itemObj);
								end;
							end;
							--
							if #itemTable > 1 then
								backPackOption.onSelect = nil
								local subMenuGrab = ISContextMenu:getNew(contextMenu);
								contextMenu:addSubMenu(backPackOption, subMenuGrab);
								subMenuGrab:addOption(getText("ContextMenu_Grab_one"), self, self.onClickContext, 0, 0, contextMenu, backpack.inventory, {self.itemObj});
								if #itemTable > 2 then
									subMenuGrab:addOption(getText("ContextMenu_Grab_half"), self, self.onClickContext, 0, 0, contextMenu, backpack.inventory, {unpack(itemTable, 1, math.ceil(#itemTable / 2))});
								end;
								subMenuGrab:addOption(getText("ContextMenu_Grab_all"), self, self.onClickContext, 0, 0, contextMenu, backpack.inventory, itemTable);
							end;
						end;
					end;
				end;
			end;
		end;
		contextMenu:addSubMenu(contextOption, subMenu);
		if self.onClickDiscard then
			if self.identified then
				contextName = getText("UI_foraging_DiscardItem").." "..self.itemObj:getDisplayName();
			else
				contextName = getText("UI_foraging_DiscardItem").." "..getText("UI_foraging_UnknownItem");
			end;
			contextOption = contextMenu:addOption(contextName, self, self.onClickDiscard, contextMenu);
		end;
		--
		triggerEvent("onFillSearchIconContextMenu", contextMenu, self);
		return false;
	end;
	return false;
end
-------------------------------------------------
-------------------------------------------------
function ISBaseIcon:render3DItem()
	if self.itemObj and self.square then
		Render3DItem(self.itemObj, self.square, self.xCoord, self.yCoord, self.zCoord, self.itemRotation);
	end;
end

function ISBaseIcon:renderAltWorldTexture()
	self:updateZoom();
	self:updateAlpha();
	local dx, dy = self:getScreenDelta();
	self:setX(isoToScreenX(self.player, self.xCoord, self.yCoord, self.zCoord) + dx);
	self:setY(isoToScreenY(self.player, self.xCoord, self.yCoord, self.zCoord) + dy);
	--local tc = self.textureColor;
	--for _, texture in ipairs(self.altWorldTexture) do
	--	self:drawTextureScaled(texture, -(texture:getWidth() / 2 / self.zoom), -(texture:getHeight() / self.zoom), texture:getWidth() / self.zoom, texture:getHeight() / self.zoom, tc.a, tc.r, tc.g, tc.b);  --render world texture
	--end;
end

function ISBaseIcon:renderWorldItemTexture()
	self:updateZoom();
	self:updateAlpha();
	local dx, dy = self:getScreenDelta();
	self:setX(isoToScreenX(self.player, self.xCoord, self.yCoord, self.zCoord) - self.itemTexture:getWidth() / 2 / self.zoom + dx);
	self:setY(isoToScreenY(self.player, self.xCoord, self.yCoord, self.zCoord) + dy);
	--local tc = self.textureColor;
	--self:drawTextureScaled(self.itemTexture, 0, 0, self.itemTexture:getWidth() / self.zoom, self.itemTexture:getHeight() / self.zoom, tc.a, tc.r, tc.g, tc.b);  --render world icon
end

function ISBaseIcon:renderPinIcon()
	local tc = self.textureColor;
	if self.itemTexture and self.identified and self.renderItemTexture then
		local textureCenter = (self.itemTexture:getWidth() / 1.5 / self.zoom) / 2;
		local pinCenter = self.width / 2;
		self:drawTextureScaled(self.texture, 0, 0, self.width, self.height, self.pinAlpha, tc.r, tc.g, tc.b);
		self:drawTextureScaled(self.itemTexture, (pinCenter - textureCenter), 5 / self.zoom, self.itemTexture:getWidth() / 1.5 / self.zoom, self.itemTexture:getHeight() / 1.5 / self.zoom, self.pinAlpha, tc.r, tc.g, tc.b);
		if self.isKnownPoison then
			self:drawTextureScaled(poisonIcon, pinCenter, 15 / self.zoom, poisonIcon:getWidth() / self.zoom, poisonIcon:getHeight() / self.zoom, self.pinAlpha, tc.r, tc.g, tc.b)
		end;
	else
		self:drawTextureScaled(pinIconUnknown, 0, 0, self.width, self.height, self.pinAlpha, tc.r, tc.g, tc.b);
	end;
	if false then
		-- Draw the 30-pixel gap between the pin icon and item texture.
		local wh = 30 / self.zoom
		self:drawRectBorder(self.width / 2 - wh / 2, self.height, wh, wh, 1, 1, 1, 1)
	end
end

function ISBaseIcon:render()
	if self:getIsSeen() then
		self:updateBounce();
		self:updatePinIconPosition();
		self:updatePinIconSize();
		self:renderPinIcon();
	else
		self:resetBounce();
	end;
end
-------------------------------------------------
-------------------------------------------------
function ISBaseIcon:getAngleOffset2D(_angle1, _angle2)
	return 180 - math.abs(math.abs(_angle1 - _angle2) - 180);
end

function ISBaseIcon:getAngle2D(_x1, _y1, _x2, _y2)
	local angle = math.atan2(_x1 - _x2, -(_y1 - _y2));
	if angle < 0 then angle = math.abs(angle) else angle = 2 * math.pi - angle end;
	return math.deg(angle);
end

function ISBaseIcon:isCenterView(_bonusAngle)
	local objAngle1 = math.deg(self.character:getForwardDirection():getDirection() + math.pi / 2); --rotate 90 degrees CW
	if objAngle1 < 0 then objAngle1 = math.abs(360 + objAngle1); end; --fix to 0-360
	local objAngle2 = self:getAngle2D(self.xCoord, self.yCoord, self.character:getX(), self.character:getY()); --find angle to item
	objAngle2 = math.abs(objAngle2 - 360); --invert direction
	local offset2D = self:getAngleOffset2D(objAngle1, objAngle2); --find difference between angles
	if offset2D >= -(self.viewAngle + _bonusAngle + self.expandView) and offset2D <= (self.viewAngle + _bonusAngle + self.expandView) then
		return true;
	end;
	return false;
end
-------------------------------------------------
-------------------------------------------------
function ISBaseIcon:getDistance3D(_x1, _y1, _z1, _x2, _y2, _z2)
	local absX = math.abs(_x2 - _x1);
	local absY = math.abs(_y2 - _y1);
	local absZ = math.abs(_z2 - _z1) * self.zSize;
	return math.sqrt(absX^2 + absY^2) + absZ;
end

function ISBaseIcon:isInRangeOfPlayer(_range)
	return self:getDistance3D(self.character:getX(), self.character:getY(), self.character:getZ(), self.xCoord, self.yCoord, self.zCoord) <= _range;
end

function ISBaseIcon:isInRangeForUpdate()
	return 	((self.distanceToPlayer <= self.manager.radius and (not self.isDarknessCapped))
			or  (self.distanceToPlayer <= self.viewDistance));
end
-------------------------------------------------
-------------------------------------------------
function ISBaseIcon:getIsSeen()			 			return self.isSeen; 				end;
function ISBaseIcon:setIsSeen(_isSeen)				self.isSeen = _isSeen; 				end;
function ISBaseIcon:getIsSeenThisUpdate() 			return self.isSeenThisUpdate; 		end;
function ISBaseIcon:setIsSeenThisUpdate(_isSeen) 	self.isSeenThisUpdate = _isSeen; 	end;
function ISBaseIcon:getIsNoticed()					return self.isNoticed; 			    end;
function ISBaseIcon:setIsNoticed(_isNoticed)		self.isNoticed = _isNoticed;		end;

function ISBaseIcon:getCanSeeThisUpdate()
	if (self.square and self.character) then
		if self.square:getZ() == self.character:getZ() then

			local currentSquare = self.character:getCurrentSquare();
			local canSeeSquare = self.square:isCanSee(self.player);
			local isOnSquare = self.distanceToPlayer <= self.onSquareDistance;
			local isBlockedTo = self.square:isBlockedTo(currentSquare);
			local lightPenalty = 1 - forageSystem.getLightLevelPenalty(self.character, self.square, true);
			local squareDarkMulti = self.square:getDarkMulti(self.player);

			if ISSearchManager.showDebug then
				self.visionData = {
					canSeeSquare = canSeeSquare and "true" or "false",
					isOnSquare = isOnSquare and "true" or "false",
					isBlockedTo = isBlockedTo and "true" or "false",
					lightPenalty = lightPenalty,
					squareDarkMulti = squareDarkMulti,
					itemSize = self.itemSize,
				};
			end;

			if isBlockedTo then return false; end;
			if isOnSquare then return true; end;

			if lightPenalty >= (forageSystem.lightPenaltyCutoff / 100) then
				--check for lit surface where the floor is dark - e.g. tables
				if squareDarkMulti <= 2.0 then
					return false;
				end;
			end;

			if self.perkLevel >= 5 then
				--also check adjacent squares
				for _, square in pairs(self.adjacentSquares) do
					canSeeSquare = square:isCanSee(self.player);
					if canSeeSquare and not square:isBlockedTo(currentSquare) then break; end;
				end;
			end;

			return canSeeSquare;
		end;
	end;
	return false;
end

function ISBaseIcon:updateModifiers()
	local character = self.character;
	self.modifiers.levelBonus			= forageSystem.getLevelVisionBonus(self.perkLevel);
	self.modifiers.traitBonus			= forageSystem.getTraitVisionBonus(character);
	self.modifiers.professionBonus		= forageSystem.getProfessionVisionBonus(character);
	self.modifiers.panic 				= forageSystem.getPanicPenalty(character);
	self.modifiers.body					= forageSystem.getBodyPenalty(character);
	self.modifiers.exhaustion			= forageSystem.getExhaustionPenalty(character);
	self.modifiers.clothing				= forageSystem.getClothingPenalty(character);
	self.modifiers.difficulty 			= forageSystem.getDifficultyPenalty(self.perkLevel);
	self.modifiers.size 				= forageSystem.getItemSizePenalty(self.itemSize);
	self.modifiers.weather				= forageSystem.getWeatherPenalty(character, self.square);
	self.modifiers.categoryBonus		= forageSystem.getCategoryBonus(character, self.catDef);
	self.modifiers.hungerBonus			= forageSystem.getHungerBonus(character, self.itemDef);
end

function ISBaseIcon:doVisionCheck()
	local character, itemDef = self.character, self.itemDef;
	--
	local maxRadiusCap = self.maxRadiusCap;
	local maxRadius = self.maxRadius;
	local minRadius = self.minRadius;
	--
	local levelBonus        = self.modifiers.levelBonus;
	local traitBonus        = self.modifiers.traitBonus;
	local professionBonus   = self.modifiers.professionBonus;
	--
	local viewDistance = minRadius;
	--
	maxRadius       = maxRadius + professionBonus + traitBonus;
	viewDistance    = viewDistance + levelBonus;
	viewDistance    = viewDistance + traitBonus;
	viewDistance    = viewDistance + professionBonus;
	viewDistance    = clamp(viewDistance, minRadius, maxRadius);
	--
	local modifiers = {
		panic 		= self.modifiers.panic,
		body		= self.modifiers.body,
		exhaustion	= self.modifiers.exhaustion,
		clothing	= self.modifiers.clothing,
		difficulty 	= self.modifiers.difficulty,
		size 		= self.modifiers.size,
		weather 	= self.modifiers.weather,
	};
	--
	for _, modifier in pairs(modifiers) do viewDistance = viewDistance * modifier; end;
	--
	--only the highest modifier applies for aim or sneak
	local aiming    = math.max(forageSystem.getAimVisionBonus(character) * self.manager.aimMulti, 1);
	local sneaking  = math.max(forageSystem.getSneakVisionBonus(character) * self.manager.sneakMulti, 1);

	local categoryBonus = self.modifiers.categoryBonus;
	local hungerBonus   = self.modifiers.hungerBonus;

	local visionBonus = math.max(aiming, sneaking) * categoryBonus * hungerBonus;

	viewDistance = viewDistance * visionBonus;
	maxRadius = maxRadius * visionBonus;
	minRadius = minRadius * visionBonus;
	maxRadius = math.min(maxRadius, maxRadiusCap);
	--
	local lightLevelPenalty = forageSystem.getLightLevelPenalty(character, self.square, true);
	if 1 - lightLevelPenalty >= (forageSystem.lightPenaltyCutoff / 100) then
		--if it is not well lit, you need to be on the square to see it
		viewDistance = self.darkVisionRadius;
		self.isDarknessCapped = true;
	else
		--otherwise the minimum is our normal minimum distance
		viewDistance = viewDistance * lightLevelPenalty;
		self.isDarknessCapped = false;
	end;
	--
	if itemDef and self.checkIsIdentified then
		self.identifyDistance = math.max((0.25 + ((self.perkLevel * 0.05)) * viewDistance), self.onSquareDistance);
		self:checkIsIdentified();
	end;
	--
	return clamp(viewDistance, minRadius, maxRadius);
end
-------------------------------------------------
-------------------------------------------------
function ISBaseIcon:remove() self.manager:removeIcon(self); end;

function ISBaseIcon:reset()
	self:setIsNoticed(false);
	self:setIsSeen(false);
	self:setIsSeenThisUpdate(false);
	self:setAlpha(0);
	self.alphaTarget = 0;
	self:resetBounce();
	self:removeIsoMarker();
	self:removeWorldMarker();
end
-------------------------------------------------
-------------------------------------------------
function ISBaseIcon:getScreenDelta() return -getPlayerScreenLeft(self.player), -getPlayerScreenTop(self.player); end;

function ISBaseIcon:updateZoom() self.zoom = getCore():getZoom(self.player); end;

function ISBaseIcon:updateAlpha()
	self.alphaTarget = clamp(1 - self.distanceToPlayer / self.maxRadius, 0, 1);
	--
	if self:getAlpha() ~= self.alphaTarget then
		self:setAlpha(self:getAlpha() + (self.alphaTarget - self:getAlpha()) / 60);
	end;
	if self.pinAlpha ~= self.alphaTarget then
		self.pinAlpha = (self.pinAlpha + (self.alphaTarget - self.pinAlpha ) / 60);
	end;
	--
	if self.isoMarker then
		if self:getIsSeen() then
			self.isoMarker:setAlpha(self:getAlpha());
		else
			self.isoMarker:setAlpha(self:getAlpha() / 2);
		end;
	end;
	if self:getAlpha() <= 0 then self:reset(); end;
end

function ISBaseIcon:updatePinIconPosition()
	self:updateZoom();
	self:updateAlpha();
	local dx, dy = self:getScreenDelta();
	self:setX(isoToScreenX(self.player, self.xCoord, self.yCoord, self.zCoord) + dx - self.width / 2); --for item texture
	self:setY(isoToScreenY(self.player, self.xCoord, self.yCoord, self.zCoord) + dy + (self.pinOffset / self.zoom));
	self:setY(self.y - (30 / self.zoom) - (self.height) + (math.sin(self.bounceStep) * self.bounceHeight));
end

function ISBaseIcon:updatePinIconSize()
	self:setWidth(self.baseWidth / self.zoom);
	self:setHeight(self.baseHeight / self.zoom);
end
-------------------------------------------------
-------------------------------------------------
function ISBaseIcon:initItem()
	if (not self.itemObj) then
		if self.icon then
			if self.icon.itemObj then
				self.itemObj = self.icon.itemObj;
			elseif self.icon.itemType then
				self.itemObj = InventoryItemFactory.CreateItem(self.icon.itemType);
			else
				print("[ISBaseIcon][initItem] no item or type for "..self.icon.id);
			end;
		end;
	end;
	if self.itemObj and (not self.itemTexture) then
		self.itemTexture = self.itemObj:getTexture();
		self:findTextureCenter();
		self:findPinOffset();
	end;
end

function ISBaseIcon:initItemCount()
	if self.itemDef then
		if self.itemDef.minCount == self.itemDef.maxCount then
			self.itemCount = self.itemDef.minCount;
		else
			self.itemCount = ZombRand(self.itemDef.minCount, self.itemDef.maxCount) + 1;
		end;
	end;
end

function ISBaseIcon:getItemList()
	if self.itemDef and (not self.itemList) then
		if ISSearchManager.iconItems[self.iconID] then
			self.itemList = ISSearchManager.iconItems[self.iconID];
		else
			self.itemList = ArrayList.new();
			--
			for _ = 1, self.itemCount do
				self.itemList:add(InventoryItemFactory.CreateItem(self.itemDef.type));
			end;
			--
			if self.itemDef.spawnFuncs then
				for _, spawnFunc in ipairs(self.itemDef.spawnFuncs) do
					self.itemList = spawnFunc(self.character, self.character:getInventory(), self.itemDef, self.itemList) or self.itemList;
				end;
			end;
			--
			ISSearchManager.iconItems[self.iconID] = self.itemList;
		end;
		--
		for item in iterList(self.itemList) do
			if item:IsFood() and item:getHerbalistType() and item:getHerbalistType() ~= "" then
				if item:getPoisonPower() > 0 and self.character:isRecipeKnown("Herbalist") then
					self.isKnownPoison = true;
					break;
				end;
			end;
		end;
	end;
end
-------------------------------------------------
-------------------------------------------------
function ISBaseIcon:updateTimestamp()
	self.currentTimestamp = getTimestampMs();
	if self.lastTimestamp <= 0 then self.lastTimestamp = self.currentTimestamp; end;
	--spot items faster if aiming/crouching
	local aiming = math.max(forageSystem.getAimVisionBonus(self.character) * self.manager.aimMulti, 1);
	local sneaking = math.max(forageSystem.getSneakVisionBonus(self.character) * self.manager.sneakMulti, 1);
	self.timeDelta = (self.currentTimestamp - self.lastTimestamp) * math.max(aiming, sneaking, 1);
	self.lastTimestamp = self.currentTimestamp;
end
-------------------------------------------------
-------------------------------------------------
function ISBaseIcon:checkIsSpotted()
	if self:getSpotTimer() >= self.spotTimerMax then self:spotIcon(); end;
end

function ISBaseIcon:updateSpotTimerMax()
	self.spotTimerMax = 300 + ((10 - self.perkLevel + 1) * 200);
end

function ISBaseIcon:spotIcon()
	if (not self:getIsSeen()) then
		self:resetBounce();
		self.spotTimer = self.spotTimerMax;
		self:setIsNoticed(true);
		self:updateAlpha();
		self:setIsSeen(true);
		self:setIsSeenThisUpdate(true);
		self.manager:spotIcon(self);
		self:addIsoMarker();
		self:initItem();
		self:initItemCount();
		self:getItemList();
		if self.canRollForSearchFocus then
			self:setCanRollForSearchFocus(false);
		end;
	end;
end

function ISBaseIcon:updateSpotTimer()
	self:updateSpotTimerMax();
	if self.distanceToPlayer <= self.viewDistance and self.manager.isSearchMode and (not self:getIsSeen()) then
		self.manager.isSpotting = self;
		if self.distanceToPlayer <= self.minRadius or self:isCenterView((self.perkLevel or 0) * 6) then
			self:setIsNoticed(true);
			self.spotTimer = math.max(self.spotTimer + self.timeDelta, 0);
			self.stareVal = math.max(self.spotTimer / self.spotTimerMax, self.manager:getAlpha());
			self.manager:setAlpha(math.min(self.stareVal, 1));
			self.manager:resetForceFindSystem();
		else
			--anti frustration, the view will expand gradually until the item is spotted
			self.expandView = self.expandView + self.expandViewStep;
			self.manager:setAlpha(math.max(self.manager.spotAlpha, self.manager:getAlpha()));
		end;
	else
		self.expandView = 0;
		self.spotTimer = math.max(self.spotTimer - self.timeDelta, 0);
	end;
end
-------------------------------------------------
-------------------------------------------------
function ISBaseIcon:getIsSearchModeActive()
	if (self.manager.isSearchMode) and (not self.character:getVehicle()) then return true; end;
	self:reset();
	return false;
end

function ISBaseIcon:updateViewDistance()
	self.viewDistance = self:doVisionCheck();
end

function ISBaseIcon:updateDistanceToPlayer()
	self.distanceToPlayer = self:getDistance3D(self.character:getX(), self.character:getY(), self.character:getZ(), self.xCoord, self.yCoord, self.zCoord);
end
-------------------------------------------------
-------------------------------------------------
function ISBaseIcon:removeIsoMarker()
	if self.isoMarker then self.isoMarker:remove(); self.isoMarker = nil; end;
end

function ISBaseIcon:addIsoMarker()
	self:updateManagerMarkers();
	if self.isBeingRemoved then return; end;
	if not (self.itemObj and self.itemTexture) then self:initItem(); end;
	if self.iconClass and self.iconClass == "forageIcon" and (not self.isoMarker) then
		if self.altWorldTexture then
			local altTextures = {};
			local altName;
			for _, texture in ipairs(self.altWorldTexture) do
				altName = texture and texture:getName() or false;
				if altName then table.insert(altTextures, altName); end;
			end;
			if #altTextures > 0 then
				if self.itemDef.doIsoMarkerSprite then
					self.isoMarker = getIsoMarkers():addIsoMarker(self.itemDef.doIsoMarkerSprite, self.square, 1, 1, 1, false, self.itemDef.doIsoMarkerObject);
				else
					self.isoMarker = getIsoMarkers():addIsoMarker(altTextures, { }, self.square, 1, 1, 1, false, self.itemDef.doIsoMarkerObject);
				end;
			else
				self.isoMarker = getIsoMarkers():addIsoMarker({self.itemTexture:getName()}, {}, self.square, 1, 1, 1, false, false);
			end;
		else
			self.isoMarker = getIsoMarkers():addIsoMarker({self.itemTexture:getName()}, {}, self.square, 1, 1, 1, false, false);
		end;
	end;
end

function ISBaseIcon:addWorldMarker()
	self:updateManagerMarkers();
	if self.isBeingRemoved then return; end;
	self.worldMarker = getWorldMarkers():addPlayerHomingPoint(self.character, self.xCoord, self.yCoord, 0.8, 0.8, 0.8, 1);
	self:setWorldMarkerPosition();
end

function ISBaseIcon:setWorldMarkerPosition()
	self.worldMarker:setX(self.xCoord);
	self.worldMarker:setY(self.yCoord);
	if self.altWorldTexture then
		self.worldMarker:setHomeOnOffsetX(IsoUtils.XToScreen(0.0, 0.0, 0, 0));
		self.worldMarker:setHomeOnOffsetY(IsoUtils.YToScreen(0.0, 0.0, 0, 0));
	elseif self.itemTexture then
		self.worldMarker:setHomeOnOffsetX(IsoUtils.XToScreen(0.5, 0.5, 0, 0));
		self.worldMarker:setHomeOnOffsetY(IsoUtils.YToScreen(0.5, 0.5, 0, 0) - self.itemTexture:getHeight() / 2);
	end
end

function ISBaseIcon:removeWorldMarker()
	if self.worldMarker then self.worldMarker:remove(); self.worldMarker = nil; end;
end

function ISBaseIcon:updateWorldMarker()
	if
		(self.iconClass == "forageIcon" or self.iconClass == "worldObject")
		and self.manager.disableTick == 0
		and self:isNearby()
		and self:getIsSeenThisUpdate()
		and self:checkIsForageable()
		and self:isInRangeForUpdate()
	then
		if self.canRollForSearchFocus then
			self:doSearchFocusCheck();
		end;
		if (not self.isNoticed) then
			self:setIsNoticed(true);
		end;
		if (not self.worldMarker) then
			self:addWorldMarker();
		else
			self:setWorldMarkerPosition();
		end;
		if (not self.isoMarker) then
			self:addIsoMarker();
		end;
	else
		self:removeWorldMarker();
		if (not self:getIsSeen()) then
			self:removeIsoMarker();
		end;
	end;
end
-------------------------------------------------
-------------------------------------------------
function ISBaseIcon:getSpotTimer()			return self.spotTimer; 							        end;
function ISBaseIcon:setSpotTimer(_time)		self.spotTimer = _time; 						        end;
function ISBaseIcon:isNearby()				return self.distanceToPlayer <= self.maxRadius;         end;
-------------------------------------------------
-------------------------------------------------
function ISBaseIcon:checkIsForageable()     return self.isForageable;                               end;
-------------------------------------------------
-------------------------------------------------
function ISBaseIcon:updateManagerMarkers()
	local manager = self.manager;
	local managedMarkers = self.managedMarkers;
	for markerType, managedType in pairs(managedMarkers) do
		if self[markerType] then
			if manager[managedType][self.iconID] then
				if manager[managedType][self.iconID] ~= self[markerType] then
					manager[managedType][self.iconID]:remove();
					manager[managedType][self.iconID] = self[markerType];
				end;
			else
				manager[managedType][self.iconID] = self[markerType];
			end;
		else
			if manager[managedType][self.iconID] then
				manager[managedType][self.iconID]:remove();
				manager[managedType][self.iconID] = nil;
			end;
		end;
	end;
end

function ISBaseIcon:updatePerkLevel()
	if self.itemDef then
		self.perkLevel = forageSystem.getPerkLevel(self.character, self.itemDef);
	else
		self.perkLevel = self.character:getPerkLevel(Perks.PlantScavenging);
	end;
end

function ISBaseIcon:checkIsPlayerRunning()
	return ((self.character:isRunning() or self.character:isSprinting()) and self.character:isPlayerMoving());
end

function ISBaseIcon:updateLastSeen()
	self.lastSeenHours = forageSystem.getWorldAge();
end

function ISBaseIcon:doUpdateEvents(_force)
	self.updateTick = self.updateTick + 1;
	--
	for i, event in ipairs(self.updateEvents) do
		if (_force or self.updateTick % event.tick == 0) and self[event.method] then
			self[event.method](self);
			if event.breakTick then break; end;
		end;
	end;
	--
	if self.updateTick > self.updateTickMax then self.updateTick = 0; end;
end

function ISBaseIcon:update()
	self:setIsSeenThisUpdate(false);
	--
	if (not self:getGridSquare()) or (not self:getIsSearchModeActive()) then
		self:removeIsoMarker();
		self:removeWorldMarker();
		return;
	end;
	--
	if (not self:isValid()) then self:remove(); return; end;
	--
	self:updateDistanceToPlayer();
	--
	if self:isNearby() then
		self:updateTimestamp();
		self:doUpdateEvents();
		if self.manager:getIsSeen(self) then self:spotIcon(); end;
		if (not self.isValidSquare) then self.manager:doChangePosition(self); return; end;
		self:setIsSeenThisUpdate(self:getCanSeeThisUpdate());
		if (not self:checkIsPlayerRunning()) then
			if self:getIsSeenThisUpdate() then
				if self:checkIsForageable() then
					self:updateSpotTimer();
					self:checkIsSpotted();
					self:updateLastSeen();
				end;
			end;
		else
			self:setSpotTimer(0);
		end;
	end;
	--
	self:updateWorldMarker();
	self:updateManagerMarkers();
end
-------------------------------------------------
-------------------------------------------------
function ISBaseIcon:findPinOffset()
	if self.altWorldTexture then
		-- If a forage icon is drawn without an IsoObject, the texture is drawn above the square center.
		if not self.itemDef.doIsoMarkerObject then
			local tallestTexture = 0;
			for _, texture in ipairs(self.altWorldTexture) do
				if tallestTexture < texture:getHeight() then tallestTexture = texture:getHeight() end;
			end;
			local tileHeight = 32 * Core.getTileScale()
			self.pinOffset = tileHeight / 2 - tallestTexture;
			return
		end
		-- If a forage icon is drawn using an IsoObject, the texture is drawn above the south-east corner of the square.
		local tallestTexture = 0;
		for _, texture in ipairs(self.altWorldTexture) do
			local top = texture:getHeightOrig() - texture:getOffsetY()
			if tallestTexture < top then tallestTexture = top end;
		end;
		local tileHeight = 32 * Core.getTileScale()
		self.pinOffset = tileHeight - tallestTexture;
		return
	end;
	-- If a forage icon is drawn without an IsoObject, the texture is drawn above the square center.
	if self.itemTexture then
		local tileHeight = 32 * Core.getTileScale()
		self.pinOffset = tileHeight / 2 - self.itemTexture:getHeight();
	end
end

function ISBaseIcon:findTextureCenter()
	if self.altWorldTexture then
		local widestTexture = 0;
		for _, texture in ipairs(self.altWorldTexture) do
			if widestTexture < texture:getWidth() then widestTexture = texture:getWidth(); end;
		end;
		self.textureCenter = widestTexture / 2;
	elseif self.itemTexture then
		self.textureCenter = self.itemTexture:getWidth() / 2;
	else
		self.textureCenter = 0;
	end;
end
-------------------------------------------------
-------------------------------------------------
function ISBaseIcon:resetBounce()
	self.bounce = true;
	self.bounceStep = 1 * math.pi;
	self.bounceMax = 2 * math.pi;
	self.bounceHeight = 24;
	self.bounceSpeed = 0.1;
end

function ISBaseIcon:updateBounce()
	if self.bounce then
		self.bounceStep = self.bounceStep + self.bounceSpeed;
		if self.bounceStep >= self.bounceMax then
			self.bounce = false;
		end;
	end;
end
-------------------------------------------------
-------------------------------------------------

function ISBaseIcon:initGridSquare()
	local cell = getCell();
	if cell then
		local square = (getCell():getGridSquare(self.icon.x, self.icon.y, self.icon.z));
		if square then
			self.square = square;
			self.adjacentSquares = {
				north   = square:getN(),
				south   = square:getS(),
				east	= square:getE(),
				west	= square:getW(),
			};
			return self.square;
		end;
	end;
	return false;
end

function ISBaseIcon:initAltTexture()
	if type(self.altWorldTexture) == 'string' then
		self.altWorldTexture = {getTexture(texture)};
	elseif type(self.altWorldTexture) == 'table' then
		if type(self.altWorldTexture[1]) == 'table' then
			if #self.altWorldTexture > 1 then
				self.altWorldTexture = self.altWorldTexture[ZombRand(#self.altWorldTexture) + 1];
			else
				self.altWorldTexture = self.altWorldTexture[1];
			end;
		end;
		for i, texture in ipairs(self.altWorldTexture) do
			if type(texture) == 'string' then
				self.altWorldTexture[i] = getTexture(texture);
			end;
		end;
	else
		self.altWorldTexture = {self.altWorldTexture};
	end;
end

function ISBaseIcon:initialise()
	ISPanel.initialise(self);
	self:setAlpha(0);
	self:initGridSquare();
	self:setVisible(true);
	self:setFollowGameWorld(true);
	self:setRenderThisPlayerOnly(self.player);
end
-------------------------------------------------
-------------------------------------------------
function ISBaseIcon:new(_manager, _icon)
	local forageSystem = forageSystem;
	local o = {};
	o = ISPanel:new(0, 0, 30, 45);
	setmetatable(o, self)
	self.__index = self;

	o.width                 = 30;
	o.height                = 45;
	o.baseWidth             = o.width;
	o.baseHeight            = o.height;

	o.iconClass             = "ISBaseIcon";

	o.manager               = _manager;
	o.character             = _manager.character;
	o.player                = _manager.character:getPlayerNum();
	o.perkLevel             = _manager.perkLevel;

	o.icon                  = _icon;
	o.iconID                = _icon.id;
	o.xCoord                = _icon.x or 0;
	o.yCoord                = _icon.y or 0;
	o.zCoord                = _icon.z or 0;

	o.zSize                 = 10;

	o.texture               = pinIconBlank;
	o.textureColor          = {r = 1, g = 1, b = 1, a = 0};
	o.alphaTarget           = 0;
	o.pinAlpha              = 1;

	o.square                = nil;
	o.adjacentSquares       = {};

	o.spotTimer             = 0;
	o.spotTimerMax          = 10000;
	o.lastTimestamp         = 0;
	o.currentTimestamp      = 0;

	o.lastSeenHours         = 0;
	o.onSquareDistance      = 1.5;
	o.darkVisionRadius      = forageSystem.darkVisionRadius;
	o.minRadius             = forageSystem.minVisionRadius;
	o.maxRadius             = forageSystem.maxVisionRadius;
	o.maxRadiusCap          = forageSystem.visionRadiusCap;

	o.distanceToPlayer      = 0;
	o.viewDistance          = 0;
	o.identifyDistance      = 0;

	o.isDarknessCapped      = false;

	o.isNoticed             = false;
	o.isSeen                = false;
	o.isSeenThisUpdate      = false;

	o.identified            = true;

	o.isBeingRemoved        = false;

	o.renderItemTexture     = true;
	o.isBonusIcon           = false;
	o.isForageable          = true;

	o.canMoveVertical       = false;

	o.posChanges            = 0;
	o.maxPosChanges         = 10;

	o.itemRotation          = ZombRand(360);

	o.onMouseDoubleClick    = ISBaseIcon.doPickup;

	o.modifiers				= {
		levelBonus			= 0,
		traitBonus			= 0,
		professionBonus		= 0,
		panic 				= 0,
		body				= 0,
		exhaustion			= 0,
		clothing			= 0,
		difficulty 			= 0,
		size 				= 0,
		weather				= 0,
		categoryBonus		= 0,
		hungerBonus			= 0,
	};

	o.viewAngle             = 30;
	o.expandView            = 0;
	o.expandViewStep        = 0.5;

	o.textureCenter         = 0;
	o.pinOffset             = 0;

	o.itemType              = _icon.itemType;
	o.itemSize              = _icon.itemSize or 1.0;

	o.isMover               = false;
	o.moveState             = "idle";
	o.moveTargetX           = o.xCoord;
	o.moveTargetY           = o.yCoord;

	o.visionData            = {};

	o.managedMarkers        = ISBaseIcon.managedMarkers;
	o.worldMarker           = nil;
	o.isoMarker             = nil;

	o.itemList              = nil;
	o.itemCount             = 1;
	o.isKnownPoison         = false;

	o.updateTick			= 0;
	o.updateTickMax			= 200;
	o.updateEvents          = ISBaseIcon.updateEvents;

	o.bounce				= true;
	o.bounceStep			= 1 * math.pi;
	o.bounceMax				= 2 * math.pi;
	o.bounceHeight			= 24;
	o.bounceSpeed			= 0.1;

	o.canRollForSearchFocus	= false;

	o:initialise();
	return o;
end
