/*
 * Copyright (c) 2016-2017, Abel Briggs
 * Copyright (c) 2017, Kronos <https://github.com/KronosDesign>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package net.runelite.client.plugins.idlenotifier;

import com.google.common.collect.ImmutableSet;
import com.google.inject.Provides;
import java.awt.TrayIcon;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import javax.inject.Inject;
import net.runelite.api.Actor;
import net.runelite.api.AnimationID;
import static net.runelite.api.AnimationID.*;
import net.runelite.api.Client;
import net.runelite.api.Constants;
import net.runelite.api.GameState;
import net.runelite.api.GraphicID;
import net.runelite.api.Hitsplat;
import net.runelite.api.InventoryID;
import net.runelite.api.Item;
import net.runelite.api.ItemContainer;
import net.runelite.api.NPC;
import net.runelite.api.NPCDefinition;
import net.runelite.api.Player;
import net.runelite.api.Skill;
import net.runelite.api.SkullIcon;
import net.runelite.api.VarPlayer;
import net.runelite.api.Varbits;
import net.runelite.api.WallObject;
import net.runelite.api.WorldType;
import net.runelite.api.coords.WorldPoint;
import net.runelite.api.events.AnimationChanged;
import net.runelite.api.events.GameStateChanged;
import net.runelite.api.events.GameTick;
import net.runelite.api.events.HitsplatApplied;
import net.runelite.api.events.InteractingChanged;
import net.runelite.api.events.ItemContainerChanged;
import net.runelite.api.events.PlayerSpawned;
import net.runelite.api.events.SpotAnimationChanged;
import net.runelite.api.events.WallObjectSpawned;
import net.runelite.client.Notifier;
import net.runelite.client.config.ConfigManager;
import net.runelite.client.eventbus.Subscribe;
import net.runelite.client.game.FriendChatManager;
import net.runelite.client.game.Sound;
import net.runelite.client.game.SoundManager;
import net.runelite.client.plugins.Plugin;
import net.runelite.client.plugins.PluginDescriptor;
import net.runelite.client.plugins.PluginType;
import net.runelite.client.util.PvPUtil;
import org.apache.commons.lang3.ArrayUtils;
import org.pf4j.Extension;

@Extension
@PluginDescriptor(
	name = "Idle Notifier",
	description = "Send a notification when going idle, or when HP/Prayer reaches a threshold",
	tags = {"health", "hitpoints", "notifications", "prayer", "pvp", "pker"},
	type = PluginType.MISCELLANEOUS
)
public class IdleNotifierPlugin extends Plugin
{
	// This must be more than 500 client ticks (10 seconds) before you get AFK kicked
	private static final int LOGOUT_WARNING_MILLIS = (4 * 60 + 40) * 1000; // 4 minutes and 40 seconds
	private static final int COMBAT_WARNING_MILLIS = 19 * 60 * 1000; // 19 minutes
	private static final int LOGOUT_WARNING_CLIENT_TICKS = LOGOUT_WARNING_MILLIS / Constants.CLIENT_TICK_LENGTH;
	private static final int COMBAT_WARNING_CLIENT_TICKS = COMBAT_WARNING_MILLIS / Constants.CLIENT_TICK_LENGTH;

	private static final int HIGHEST_MONSTER_ATTACK_SPEED = 8; // Except Scarab Mage, but they are with other monsters
	private static final Duration SIX_HOUR_LOGOUT_WARNING_AFTER_DURATION = Duration.ofMinutes(340);

	private static final String FISHING_SPOT = "Fishing spot";

	private static final int RESOURCE_AREA_REGION = 12605;

	private static final Set<Integer> nominalAnimations = new ImmutableSet.Builder<Integer>()
		.addAll(
			Arrays.asList(
				/* Woodcutting */
				WOODCUTTING_BRONZE,
				WOODCUTTING_IRON,
				WOODCUTTING_STEEL,
				WOODCUTTING_BLACK,
				WOODCUTTING_MITHRIL,
				WOODCUTTING_ADAMANT,
				WOODCUTTING_RUNE,
				WOODCUTTING_GILDED,
				WOODCUTTING_DRAGON,
				WOODCUTTING_INFERNAL,
				WOODCUTTING_3A_AXE,
				WOODCUTTING_CRYSTAL,
				/* Cooking(Fire, Range) */
				COOKING_FIRE,
				COOKING_RANGE,
				COOKING_WINE,
				/* Crafting(Gem Cutting, Glassblowing, Spinning, Battlestaves, Pottery) */
				GEM_CUTTING_OPAL,
				GEM_CUTTING_JADE,
				GEM_CUTTING_REDTOPAZ,
				GEM_CUTTING_SAPPHIRE,
				GEM_CUTTING_EMERALD,
				GEM_CUTTING_RUBY,
				GEM_CUTTING_DIAMOND,
				GEM_CUTTING_AMETHYST,
				CRAFTING_GLASSBLOWING,
				CRAFTING_SPINNING,
				CRAFTING_BATTLESTAVES,
				CRAFTING_LEATHER,
				CRAFTING_POTTERS_WHEEL,
				CRAFTING_POTTERY_OVEN,
				/* Fletching(Cutting, Stringing, Adding feathers and heads) */
				FLETCHING_BOW_CUTTING,
				FLETCHING_STRING_NORMAL_SHORTBOW,
				FLETCHING_STRING_OAK_SHORTBOW,
				FLETCHING_STRING_WILLOW_SHORTBOW,
				FLETCHING_STRING_MAPLE_SHORTBOW,
				FLETCHING_STRING_YEW_SHORTBOW,
				FLETCHING_STRING_MAGIC_SHORTBOW,
				FLETCHING_STRING_NORMAL_LONGBOW,
				FLETCHING_STRING_OAK_LONGBOW,
				FLETCHING_STRING_WILLOW_LONGBOW,
				FLETCHING_STRING_MAPLE_LONGBOW,
				FLETCHING_STRING_YEW_LONGBOW,
				FLETCHING_STRING_MAGIC_LONGBOW,
				FLETCHING_ATTACH_FEATHERS_TO_ARROWSHAFT,
				FLETCHING_ATTACH_HEADS,
				FLETCHING_ATTACH_BOLT_TIPS_TO_BRONZE_BOLT,
				FLETCHING_ATTACH_BOLT_TIPS_TO_IRON_BROAD_BOLT,
				FLETCHING_ATTACH_BOLT_TIPS_TO_BLURITE_BOLT,
				FLETCHING_ATTACH_BOLT_TIPS_TO_STEEL_BOLT,
				FLETCHING_ATTACH_BOLT_TIPS_TO_MITHRIL_BOLT,
				FLETCHING_ATTACH_BOLT_TIPS_TO_ADAMANT_BOLT,
				FLETCHING_ATTACH_BOLT_TIPS_TO_RUNE_BOLT,
				FLETCHING_ATTACH_BOLT_TIPS_TO_DRAGON_BOLT,
				/* Smithing(Anvil, Furnace, Cannonballs */
				SMITHING_ANVIL,
				SMITHING_SMELTING,
				SMITHING_CANNONBALL,
				/* Fishing */
				FISHING_CRUSHING_INFERNAL_EELS,
				FISHING_CUTTING_SACRED_EELS,
				FISHING_BIG_NET,
				FISHING_NET,
				FISHING_POLE_CAST,
				FISHING_CAGE,
				FISHING_HARPOON,
				FISHING_BARBTAIL_HARPOON,
				FISHING_DRAGON_HARPOON,
				FISHING_INFERNAL_HARPOON,
				FISHING_OILY_ROD,
				FISHING_KARAMBWAN,
				FISHING_BAREHAND,
				FISHING_PEARL_ROD,
				FISHING_PEARL_FLY_ROD,
				FISHING_PEARL_BARBARIAN_ROD,
				FISHING_PEARL_ROD_2,
				FISHING_PEARL_FLY_ROD_2,
				FISHING_PEARL_BARBARIAN_ROD_2,
				FISHING_PEARL_OILY_ROD,
				/* Mining(Normal) */
				MINING_BRONZE_PICKAXE,
				MINING_IRON_PICKAXE,
				MINING_STEEL_PICKAXE,
				MINING_BLACK_PICKAXE,
				MINING_MITHRIL_PICKAXE,
				MINING_ADAMANT_PICKAXE,
				MINING_RUNE_PICKAXE,
				MINING_GILDED_PICKAXE,
				MINING_DRAGON_PICKAXE,
				MINING_DRAGON_PICKAXE_UPGRADED,
				MINING_DRAGON_PICKAXE_OR,
				MINING_INFERNAL_PICKAXE,
				MINING_3A_PICKAXE,
				MINING_CRYSTAL_PICKAXE,
				DENSE_ESSENCE_CHIPPING,
				DENSE_ESSENCE_CHISELING,
				/* Mining(Motherlode) */
				MINING_MOTHERLODE_BRONZE,
				MINING_MOTHERLODE_IRON,
				MINING_MOTHERLODE_STEEL,
				MINING_MOTHERLODE_BLACK,
				MINING_MOTHERLODE_MITHRIL,
				MINING_MOTHERLODE_ADAMANT,
				MINING_MOTHERLODE_RUNE,
				MINING_MOTHERLODE_GILDED,
				MINING_MOTHERLODE_DRAGON,
				MINING_MOTHERLODE_DRAGON_UPGRADED,
				MINING_MOTHERLODE_DRAGON_OR,
				MINING_MOTHERLODE_INFERNAL,
				MINING_MOTHERLODE_3A,
				MINING_MOTHERLODE_CRYSTAL,
				/* Herblore */
				HERBLORE_PESTLE_AND_MORTAR,
				HERBLORE_POTIONMAKING,
				HERBLORE_MAKE_TAR,
				/* Magic */
				MAGIC_CHARGING_ORBS,
				MAGIC_LUNAR_PLANK_MAKE,
				MAGIC_LUNAR_STRING_JEWELRY,
				MAGIC_MAKE_TABLET,
				MAGIC_ENCHANTING_JEWELRY,
				MAGIC_ENCHANTING_AMULET_1,
				MAGIC_ENCHANTING_AMULET_2,
				MAGIC_ENCHANTING_AMULET_3,
				MAGIC_ENCHANTING_BOLTS,
				/* Prayer */
				USING_GILDED_ALTAR,
				/* Farming */
				FARMING_MIX_ULTRACOMPOST,
				FARMING_HARVEST_BUSH,
				FARMING_HARVEST_HERB,
				FARMING_HARVEST_FRUIT_TREE,
				FARMING_HARVEST_FLOWER,
				FARMING_HARVEST_ALLOTMENT,
				/* Misc */
				PISCARILIUS_CRANE_REPAIR,
				HOME_MAKE_TABLET,
				SAND_COLLECTION
			)
		).build();

	@Inject
	private Notifier notifier;

	@Inject
	private Client client;

	@Inject
	private SoundManager soundManager;

	@Inject
	private IdleNotifierConfig config;

	@Inject
	private FriendChatManager friendChatManager;

	private Instant lastAnimating;
	private int lastAnimation = AnimationID.IDLE;
	private Instant lastInteracting;
	private Actor lastInteract;
	private Instant lastMoving;
	private WorldPoint lastPosition;
	private boolean notifyPosition = false;
	private boolean notifyHitpoints = true;
	private boolean notifyPrayer = true;
	private boolean notifyOxygen = true;
	private boolean notifyIdleLogout = true;
	private boolean notify6HourLogout = true;
	private int lastSpecEnergy = 1000;
	private int lastCombatCountdown = 0;
	private Instant sixHourWarningTime;
	private boolean ready;
	private Instant lastTimeItemsUsedUp;
	private List<Integer> itemIdsPrevious = new ArrayList<>();
	private List<Integer> itemQuantitiesPrevious = new ArrayList<>();
	private final List<Integer> itemQuantitiesChange = new ArrayList<>();
	private boolean lastInteractWasCombat;
	private boolean interactingNotified;
	private SkullIcon lastTickSkull = null;
	private boolean isFirstTick = true;
	private boolean resourceDoorReady = false;

	@Provides
	IdleNotifierConfig provideConfig(ConfigManager configManager)
	{
		return configManager.getConfig(IdleNotifierConfig.class);
	}

	@Subscribe
	void onAnimationChanged(AnimationChanged event)
	{
		if (client.getGameState() != GameState.LOGGED_IN)
		{
			return;
		}

		Player localPlayer = client.getLocalPlayer();
		if (localPlayer != event.getActor())
		{
			return;
		}

		int graphic = localPlayer.getSpotAnimation();
		int animation = localPlayer.getAnimation();

		if (nominalAnimations.contains(animation) || (animation == MAGIC_LUNAR_SHARED && graphic == GraphicID.BAKE_PIE))
		{
			resetTimers();
			lastAnimation = animation;
			lastAnimating = Instant.now();
			interactingNotified = false;
		}

		else if (animation == IDLE)
		{
			lastAnimating = Instant.now();
			interactingNotified = false;
		}

		// On unknown animation simply assume the animation is invalid and dont throw notification
		else
		{
			lastAnimation = IDLE;
			lastAnimating = null;
		}
	}

	@Subscribe
	private void onPlayerSpawned(PlayerSpawned event)
	{
		final Player p = event.getPlayer();
		if (config.notifyPkers() && p != null && p != client.getLocalPlayer()
			&& PvPUtil.isAttackable(client, p) && !client.isFriended(p.getName(), false)
			&& !friendChatManager.isMember(p.getName()))
		{
			String playerName = p.getName();
			int combat = p.getCombatLevel();
			notifier.notify("PK'er warning! A level " + combat + " player named " + playerName +
				" appeared!", TrayIcon.MessageType.WARNING);
		}
	}

	@Subscribe
	private void onWallObjectSpawned(WallObjectSpawned event)
	{
		WallObject wall = event.getWallObject();

		if (regionCheck())
		{
			if (config.notifyResourceDoor() && wall.getId() == 83 && resourceDoorReady)
			{
				notifier.notify("Door warning! The resource area door has been opened!");
			}
		}
	}

	@Subscribe
	private void onItemContainerChanged(ItemContainerChanged event)
	{
		ItemContainer itemContainer = event.getItemContainer();

		if (itemContainer != client.getItemContainer(InventoryID.INVENTORY) || !config.outOfItemsIdle())
		{
			return;
		}

		Item[] items = itemContainer.getItems();
		ArrayList<Integer> itemQuantities = new ArrayList<>();
		ArrayList<Integer> itemIds = new ArrayList<>();

		// Populate list of items in inventory without duplicates
		for (Item value : items)
		{
			int itemId = OutOfItemsMapping.mapFirst(value.getId());
			if (itemIds.indexOf(itemId) == -1) // -1 if item not yet in list
			{
				itemIds.add(itemId);
			}
		}

		// Populate quantity of each item in inventory
		for (int j = 0; j < itemIds.size(); j++)
		{
			itemQuantities.add(0);
			for (Item item : items)
			{
				if (itemIds.get(j) == OutOfItemsMapping.mapFirst(item.getId()))
				{
					itemQuantities.set(j, itemQuantities.get(j) + item.getQuantity());
				}
			}
		}

		itemQuantitiesChange.clear();

		// Calculate the quantity of each item consumed by the last action
		if (!itemIdsPrevious.isEmpty())
		{
			for (int i = 0; i < itemIdsPrevious.size(); i++)
			{
				int id = itemIdsPrevious.get(i);
				int currentIndex = itemIds.indexOf(id);
				int currentQuantity;
				if (currentIndex != -1) // -1 if item is no longer in inventory
				{
					currentQuantity = itemQuantities.get(currentIndex);
				}
				else
				{
					currentQuantity = 0;
				}
				itemQuantitiesChange.add(currentQuantity - itemQuantitiesPrevious.get(i));
			}
		}
		else
		{
			itemIdsPrevious = itemIds;
			itemQuantitiesPrevious = itemQuantities;
			return;
		}

		// Check we have enough items left for another action.
		for (int i = 0; i < itemQuantitiesPrevious.size(); i++)
		{
			if (-itemQuantitiesChange.get(i) * 2 > itemQuantitiesPrevious.get(i))
			{
				lastTimeItemsUsedUp = Instant.now();
				return;
			}
		}
		itemIdsPrevious = itemIds;
		itemQuantitiesPrevious = itemQuantities;
	}

	@Subscribe
	void onInteractingChanged(InteractingChanged event)
	{
		final Actor source = event.getSource();
		if (source != client.getLocalPlayer())
		{
			return;
		}

		final Actor target = event.getTarget();

		// Reset last interact
		if (target != null)
		{
			lastInteract = null;
		}
		else
		{
			lastInteracting = Instant.now();
		}

		final boolean isNpc = target instanceof NPC;

		// If this is not NPC, do not process as we are not interested in other entities
		if (!isNpc)
		{
			return;
		}

		final NPC npc = (NPC) target;
		final NPCDefinition npcComposition = npc.getDefinition();
		final List<String> npcMenuActions = Arrays.asList(npcComposition.getActions());

		if (npcMenuActions.contains("Attack"))
		{
			// Player is most likely in combat with attack-able NPC
			resetTimers();
			lastInteract = target;
			lastInteracting = Instant.now();
			lastInteractWasCombat = true;
		}
		else if (target.getName() != null && target.getName().contains(FISHING_SPOT))
		{
			// Player is fishing
			resetTimers();
			lastInteract = target;
			lastInteracting = Instant.now();
			lastInteractWasCombat = false;
		}
	}

	@Subscribe
	void onGameStateChanged(GameStateChanged gameStateChanged)
	{
		lastInteracting = null;

		GameState state = gameStateChanged.getGameState();

		switch (state)
		{
			case LOGIN_SCREEN:
				resetTimers();
				isFirstTick = true;
				break;
			case HOPPING:
				isFirstTick = true;
				ready = true;
				break;
			case LOGGING_IN:
			case CONNECTION_LOST:
				ready = true;
				break;
			case LOGGED_IN:
				if (ready)
				{
					sixHourWarningTime = Instant.now().plus(SIX_HOUR_LOGOUT_WARNING_AFTER_DURATION);
					ready = false;
					resetTimers();
				}
				resourceDoorReady = true;

				break;
		}
	}

	@Subscribe
	void onHitsplatApplied(HitsplatApplied event)
	{
		if (event.getActor() != client.getLocalPlayer())
		{
			return;
		}

		final Hitsplat hitsplat = event.getHitsplat();

		if (hitsplat.getHitsplatType() == Hitsplat.HitsplatType.DAMAGE_ME
			|| hitsplat.getHitsplatType() == Hitsplat.HitsplatType.BLOCK_ME)
		{
			lastCombatCountdown = HIGHEST_MONSTER_ATTACK_SPEED;
		}
	}

	@Subscribe
	private void onSpotAnimationChanged(SpotAnimationChanged event)
	{
		Actor actor = event.getActor();

		if (actor != client.getLocalPlayer())
		{
			return;
		}

		if (actor.getSpotAnimation() == GraphicID.SPLASH)
		{
			lastCombatCountdown = HIGHEST_MONSTER_ATTACK_SPEED;
		}
	}

	@Subscribe
	void onGameTick(GameTick event)
	{
		skullNotifier();

		final Player local = client.getLocalPlayer();
		final Duration waitDuration = Duration.ofMillis(config.getIdleNotificationDelay());
		lastCombatCountdown = Math.max(lastCombatCountdown - 1, 0);

		if (client.getGameState() != GameState.LOGGED_IN
			|| local == null
			// If user has clicked in the last second then they're not idle so don't send idle notification
			|| System.currentTimeMillis() - client.getMouseLastPressedMillis() < 1000
			|| client.getKeyboardIdleTicks() < 10)
		{
			resetTimers();
			resetOutOfItemsIdleChecks();
			return;
		}

		if (config.logoutIdle() && checkIdleLogout())
		{
			notifyWith(local, "is about to log out from idling too long!");
		}

		if (check6hrLogout())
		{
			notifyWith(local, "is about to log out from being online for 6 hours!");
		}

		if (config.outOfItemsIdle() && checkOutOfItemsIdle(waitDuration))
		{
			notifyWith(local, "has run out of items!");
			// If this triggers, don't also trigger animation idle notification afterwards.
			lastAnimation = IDLE;
		}

		if (config.movementIdle() && checkMovementIdle(waitDuration, local))
		{
			notifier.notify("[" + local.getName() + "] has stopped moving!");
		}

		if (config.interactionIdle() && checkInteractionIdle(waitDuration, local))
		{
			if (lastInteractWasCombat)
			{
				notifyWith(local, "is now out of combat!");
				if (config.outOfCombatSound())
				{
					soundManager.playSound(Sound.OUT_OF_COMBAT);
				}
			}
			else
			{
				notifyWith(local, "is now idle!");
				if (config.interactionIdleSound())
				{
					soundManager.playSound(Sound.IDLE);
				}
			}
			interactingNotified = true;
		}

		if (config.animationIdle() && checkAnimationIdle(waitDuration, local))
		{
			notifyWith(local, "is now idle!");
			if (config.animationIdleSound())
			{
				soundManager.playSound(Sound.IDLE);
			}
		}

		if (checkLowHitpoints())
		{
			notifyWith(local, "has low hitpoints!");
			if (config.getPlayHealthSound())
			{
				soundManager.playSound(Sound.LOW_HEATLH);
			}
		}

		if (checkLowPrayer())
		{
			notifyWith(local, "has low prayer!");
			if (config.getPlayPrayerSound())
			{
				soundManager.playSound(Sound.LOW_PRAYER);
			}
		}

		if (checkLowOxygen())
		{
			notifyWith(local, "has low oxygen!");
		}

		if (checkFullSpecEnergy())
		{
			notifyWith(local, "has restored spec energy!");
			if (config.getSpecSound())
			{
				soundManager.playSound(Sound.RESTORED_SPECIAL_ATTACK);
			}
		}
	}

	private boolean checkFullSpecEnergy()
	{
		int currentSpecEnergy = client.getVar(VarPlayer.SPECIAL_ATTACK_PERCENT);

		int threshold = config.getSpecEnergyThreshold() * 10;
		if (threshold == 0)
		{
			lastSpecEnergy = currentSpecEnergy;
			return false;
		}

		// Check if we have regenerated over the threshold, and that the
		// regen was small enough.
		boolean notify = lastSpecEnergy < threshold && currentSpecEnergy >= threshold && currentSpecEnergy - lastSpecEnergy <= 100;

		notify = (notify) || ((config.getOverSpecEnergy()) && (currentSpecEnergy >= threshold) && (currentSpecEnergy != lastSpecEnergy) && (currentSpecEnergy - lastSpecEnergy <= 100));

		lastSpecEnergy = currentSpecEnergy;
		return notify;
	}

	private boolean checkLowOxygen()
	{
		if (config.getOxygenThreshold() == 0)
		{
			return false;
		}
		if (config.getOxygenThreshold() >= client.getVar(Varbits.OXYGEN_LEVEL) * 0.1)
		{
			if (!notifyOxygen)
			{
				notifyOxygen = true;
				return true;
			}
		}
		else
		{
			notifyOxygen = false;
		}
		return false;
	}

	private boolean checkLowHitpoints()
	{
		if (config.getHitpointsThreshold() == 0)
		{
			return false;
		}
		if (client.getRealSkillLevel(Skill.HITPOINTS) > config.getHitpointsThreshold())
		{
			if (client.getBoostedSkillLevel(Skill.HITPOINTS) + client.getVar(Varbits.NMZ_ABSORPTION) <= config.getHitpointsThreshold())
			{
				if (!notifyHitpoints)
				{
					notifyHitpoints = true;
					return true;
				}
			}
			else
			{
				notifyHitpoints = false;
			}
		}

		return false;
	}

	private boolean checkLowPrayer()
	{
		if (config.getPrayerThreshold() == 0)
		{
			return false;
		}
		if (client.getRealSkillLevel(Skill.PRAYER) > config.getPrayerThreshold())
		{
			if (client.getBoostedSkillLevel(Skill.PRAYER) <= config.getPrayerThreshold())
			{
				if (!notifyPrayer)
				{
					notifyPrayer = true;
					return true;
				}
			}
			else
			{
				notifyPrayer = false;
			}
		}

		return false;
	}

	private boolean checkInteractionIdle(Duration waitDuration, Player local)
	{
		if (lastInteract == null)
		{
			return false;
		}

		final Actor interact = local.getInteracting();

		if (interact == null)
		{
			if (lastInteracting != null
				&& Instant.now().compareTo(lastInteracting.plus(waitDuration)) >= 0
				&& lastCombatCountdown == 0)
			{
				lastInteract = null;
				lastInteracting = null;

				// prevent animation notifications from firing too
				lastAnimation = IDLE;
				lastAnimating = null;

				return true;
			}
		}
		else
		{
			lastInteracting = Instant.now();
		}

		return false;
	}

	private boolean checkIdleLogout()
	{
		// Check clientside AFK first, because this is required for the server to disconnect you for being first
		int idleClientTicks = client.getKeyboardIdleTicks();
		if (client.getMouseIdleTicks() < idleClientTicks)
		{
			idleClientTicks = client.getMouseIdleTicks();
		}

		if (idleClientTicks < LOGOUT_WARNING_CLIENT_TICKS)
		{
			notifyIdleLogout = true;
			return false;
		}

		// If we are not receiving hitsplats then we can be afk kicked
		if (lastCombatCountdown <= 0)
		{
			boolean warn = notifyIdleLogout;
			notifyIdleLogout = false;
			return warn;
		}

		// We are in combat, so now we have to check for the timer that knocks you out of combat
		// I think there are other conditions that I don't know about, because during testing I just didn't
		// get removed from combat sometimes.
		final long lastInteractionAgo = System.currentTimeMillis() - client.getMouseLastPressedMillis();
		if (lastInteractionAgo < COMBAT_WARNING_MILLIS || client.getKeyboardIdleTicks() < COMBAT_WARNING_CLIENT_TICKS)
		{
			notifyIdleLogout = true;
			return false;
		}

		boolean warn = notifyIdleLogout;
		notifyIdleLogout = false;
		return warn;
	}

	private boolean check6hrLogout()
	{
		if (sixHourWarningTime == null)
		{
			return false;
		}

		if (Instant.now().compareTo(sixHourWarningTime) >= 0)
		{
			if (notify6HourLogout)
			{
				notify6HourLogout = false;
				return true;
			}
		}
		else
		{
			notify6HourLogout = true;
		}

		return false;
	}

	private boolean checkAnimationIdle(Duration waitDuration, Player local)
	{
		if (lastAnimation == IDLE || interactingNotified)
		{
			return false;
		}

		final int animation = local.getAnimation();

		if (animation == IDLE)
		{
			if (lastAnimating != null && Instant.now().compareTo(lastAnimating.plus(waitDuration)) >= 0)
			{
				lastAnimation = IDLE;
				lastAnimating = null;

				// prevent interaction notifications from firing too
				lastInteract = null;
				lastInteracting = null;

				return true;
			}
		}
		else
		{
			lastAnimating = Instant.now();
		}

		return false;
	}

	private boolean checkOutOfItemsIdle(Duration waitDuration)
	{
		if (lastTimeItemsUsedUp == null)
		{
			return false;
		}

		if (Instant.now().compareTo(lastTimeItemsUsedUp.plus(waitDuration)) >= 0)
		{
			resetTimers();
			resetOutOfItemsIdleChecks();
			return true;
		}
		return false;
	}

	private boolean checkMovementIdle(Duration waitDuration, Player local)
	{
		if (lastPosition == null)
		{
			lastPosition = local.getWorldLocation();
			return false;
		}

		WorldPoint position = local.getWorldLocation();

		if (lastPosition.equals(position))
		{
			if (notifyPosition
				&& local.getAnimation() == IDLE
				&& Instant.now().compareTo(lastMoving.plus(waitDuration)) >= 0)
			{
				notifyPosition = false;
				// Return true only if we weren't just breaking out of an animation
				return lastAnimation == IDLE;
			}
		}
		else
		{
			notifyPosition = true;
			lastPosition = position;
			lastMoving = Instant.now();
		}
		return false;
	}

	private void resetTimers()
	{
		final Player local = client.getLocalPlayer();

		// Reset animation idle timer
		lastAnimating = null;
		if (client.getGameState() == GameState.LOGIN_SCREEN || local == null || local.getAnimation() != lastAnimation)
		{
			lastAnimation = IDLE;
		}

		// Reset interaction idle timer
		lastInteracting = null;
		if (client.getGameState() == GameState.LOGIN_SCREEN || local == null || local.getInteracting() != lastInteract)
		{
			lastInteract = null;
		}
	}

	private void resetOutOfItemsIdleChecks()
	{
		lastTimeItemsUsedUp = null;
		itemQuantitiesChange.clear();
		itemIdsPrevious.clear();
		itemQuantitiesPrevious.clear();
	}

	private void skullNotifier()
	{
		final Player local = client.getLocalPlayer();
		SkullIcon currentTickSkull = local.getSkullIcon();
		EnumSet worldTypes = client.getWorldType();
		if (!(worldTypes.contains(WorldType.DEADMAN)))
		{
			if (!isFirstTick)
			{
				if (config.showSkullNotification() && lastTickSkull == null && currentTickSkull == SkullIcon.SKULL)
				{
					notifyWith(local, "is now skulled!");
				}
				else if (config.showUnskullNotification() && lastTickSkull == SkullIcon.SKULL && currentTickSkull == null)
				{
					notifyWith(local, "is now unskulled!");
				}
			}
			else
			{
				isFirstTick = false;
			}

			lastTickSkull = currentTickSkull;
		}
	}

	private boolean regionCheck()
	{
		return ArrayUtils.contains(client.getMapRegions(), RESOURCE_AREA_REGION);
	}

	private void notifyWith(Player local, String message)
	{
		notifier.notify("[" + local.getName() + "] " + message);
	}
}