package core.api

import com.moandjiezana.toml.Toml
import content.data.Quests
import content.data.consumables.*
import content.data.skill.SkillingTool
import content.global.handlers.iface.ge.StockMarket
import content.global.skill.construction.decoration.pohstorage.StorableFamily
import content.global.skill.construction.decoration.pohstorage.StorageInterface
import content.global.skill.slayer.SlayerEquipmentFlags
import content.global.skill.slayer.SlayerFlags
import content.global.skill.slayer.SlayerManager
import content.global.skill.slayer.Tasks
import content.global.skill.summoning.familiar.BurdenBeast
import core.ServerConstants
import core.api.utils.PlayerStatsCounter
import core.api.utils.Vector
import core.cache.def.impl.AnimationDefinition
import core.cache.def.impl.ItemDefinition
import core.cache.def.impl.SceneryDefinition
import core.cache.def.impl.VarbitDefinition
import core.game.activity.Cutscene
import core.game.component.Component
import core.game.consumable.*
import core.game.container.impl.EquipmentContainer
import core.game.dialogue.DialogueFile
import core.game.dialogue.SkillDialogueHandler
import core.game.diary.DiaryLevel
import core.game.ge.GrandExchangeRecords
import core.game.interaction.*
import core.game.node.Node
import core.game.node.entity.Entity
import core.game.node.entity.combat.CombatSwingHandler
import core.game.node.entity.combat.ImpactHandler
import core.game.node.entity.impl.Animator
import core.game.node.entity.impl.ForceMovement
import core.game.node.entity.impl.Projectile
import core.game.node.entity.npc.NPC
import core.game.node.entity.player.Player
import core.game.node.entity.player.info.LogType
import core.game.node.entity.player.info.PlayerMonitor
import core.game.node.entity.player.link.HintIconManager
import core.game.node.entity.player.link.IronmanMode
import core.game.node.entity.player.link.TeleportManager
import core.game.node.entity.player.link.audio.Audio
import core.game.node.entity.player.link.diary.DiaryType
import core.game.node.entity.player.link.emote.Emotes
import core.game.node.entity.player.link.prayer.PrayerType
import core.game.node.entity.player.link.quest.Quest
import core.game.node.entity.player.link.quest.QuestRepository
import core.game.node.entity.skill.Skills
import core.game.node.item.GroundItem
import core.game.node.item.GroundItemManager
import core.game.node.item.Item
import core.game.node.scenery.Scenery
import core.game.node.scenery.SceneryBuilder
import core.game.requirement.*
import core.game.shops.Shops
import core.game.system.config.ItemConfigParser
import core.game.system.config.ServerConfigParser
import core.game.system.task.Pulse
import core.game.system.timer.*
import core.game.system.timer.impl.*
import core.game.world.GameWorld
import core.game.world.GameWorld.Pulser
import core.game.world.map.Direction
import core.game.world.map.Location
import core.game.world.map.RegionManager
import core.game.world.map.RegionManager.getRegionChunk
import core.game.world.map.path.Pathfinder
import core.game.world.map.zone.MapZone
import core.game.world.map.zone.ZoneBorders
import core.game.world.map.zone.ZoneBuilder
import core.game.world.repository.Repository
import core.game.world.update.flag.*
import core.game.world.update.flag.chunk.AnimateObjectUpdateFlag
import core.game.world.update.flag.context.*
import core.net.packet.PacketRepository
import core.net.packet.context.DefaultContext
import core.net.packet.context.MusicContext
import core.net.packet.out.AudioPacket
import core.net.packet.out.MusicPacket
import core.tools.*
import org.rs09.consts.Items
import org.rs09.consts.NPCs
import org.rs09.consts.Sounds
import java.io.*
import java.util.regex.*
import kotlin.math.*

/**
 * Gets a skilling tool which the player has the level to use and is in their inventory.
 * @param player the player to get the tool for
 * @param pickaxe whether or not we are trying to get a pickaxe.
 * @return the tool which meets the requirements or null if none.
 */
fun getTool(player: Player, pickaxe: Boolean): SkillingTool? {
    return if (pickaxe) SkillingTool.getPickaxe(player) else SkillingTool.getHatchet(player)
}

/**
 * Check if a player has a given level (buffable/debuffable)
 * @param player the player to check the stat for
 * @param skill the Skill to check. There is an enum for this called Skills. Example: Skills.STRENGTH
 * @param level the level to check against
 * @return true if the check succeeds, false otherwise
 */
fun hasLevelDyn(player: Player, skill: Int, level: Int): Boolean {
    return player.skills.getLevel(skill) >= level
}

/**
 * Check if a player's static skill level meets a certain value
 * @param player the player to check the stat for
 * @param skill the Skill to check. There is an enum for this called Skills. Example: Skills.STRENGTH
 * @param level the level to check against
 * @return true if the check succeeds, false otherwise
 */
fun hasLevelStat(player: Player, skill: Int, level: Int): Boolean {
    return player.skills.getStaticLevel(skill) >= level
}

/**
 * Check the amount of a given item in the player's inventory
 * @param player the player whose inventory to check
 * @param id the ID of the item to check for the amount of
 * @return the amount of the given ID in the player's inventory
 */
fun amountInInventory(player: Player, id: Int): Int {
    return player.inventory.getAmount(id)
}

/**
 * Check the amount of a given item in the player's bank
 * @param player the player to check
 * @param id the ID of the item to check for
 * @param includeSecondary if the secondary bank should be included in the search
 * @return the amount of the ID in the player's bank.
 */
fun amountInBank(player: Player, id: Int, includeSecondary: Boolean = true): Int {
    return getAmountInBank(player, id) + if (includeSecondary) getAmountInBank(player, id, true) else 0
}

/**
 * More performant way to check the amount of a given item in the player's bank
 * Uses the alwaysStack property of banks to short circuit the search.
 * @param player the player to check
 * @param id the ID of the item to check for
 * @param secondary if the secondary bank should be searched instead
 * @return the amount of the ID in the player's bank.
 */
private fun getAmountInBank(player: Player, id: Int, secondary: Boolean = false): Int {
    val bank = if (secondary) player.bankSecondary.toArray() else player.bankPrimary.toArray()
    bank.forEach { if (it?.id == id) return it.amount }
    return 0
}

/**
 * Check the amount of a given item in the player's equipment slots
 * @param player the player to check
 * @param id the ID of the item to check for
 * @return the amount of the ID in the player's equipment.
 */
fun amountInEquipment(player: Player, id: Int): Int {
    val slot = itemDefinition(id).getConfiguration(ItemConfigParser.EQUIP_SLOT, -1)
    val equipped = player.equipment[slot] ?: return 0
    return if (equipped.id == id) equipped.amount else 0
}

/**
 * Check if an item exists in a player's inventory
 * @param player the player whose inventory to check
 * @param id the ID of the item to check for
 * @param amount the amount to check for
 * @return true if the player has >= the given item in the given amount, false otherwise.
 */
fun inInventory(player: Player, id: Int, amount: Int = 1): Boolean {
    return player.inventory.contains(id, amount)
}

/**
 * Check if an item exists in a player's bank
 * @param player the player whose bank to check
 * @param id the ID of the item to check for
 * @param amount the amount to check for, defaults to 1
 * @return true if the item exists in the given amount in the player's bank
 */
fun inBank(player: Player, id: Int, amount: Int = 1): Boolean {
    return amountInBank(player, id) >= amount
}

/**
 * Check if an item exists in a player's equipment
 * @param player the player whose equipment to check
 * @param id the ID of the item to check for
 * @param amount the amount to check for, defaults to 1
 * @return true if the item exists in the given amount in the player's equipment
 */
fun inEquipment(player: Player, id: Int, amount: Int = 1): Boolean {
    return amountInEquipment(player, id) >= amount
}

/**
 * Check if any item is in a player's inventory
 * @param player the player
 * @param ids the set of item ids to check
 * @return true if the player has at least one of the items in their inventory, false if none are present
 */
fun anyInInventory(player: Player, vararg ids: Int): Boolean {
    return ids.any{ id ->
        inInventory(player, id)
    }
}

/**
 * Check if an item exists in a player's equipment or inventory
 * @param player the player whose equipment to check
 * @param id the ID of the item to check for
 * @param amount the amount to check for, defaults to 1
 * @return true if the item exists in the given amount in the player's equipment or inventory
 */
fun inEquipmentOrInventory(player: Player, id: Int, amount: Int = 1): Boolean {
    // Proper, but slower implementation. Use faster unless need to check amounts split between equip/inv
    //return amountInEquipment(player, id) + amountInInventory(player, id) >= amount
    return inEquipment(player, id, amount) || inInventory(player, id, amount)
}

/**
 * Check that a set of items is equipped by the given player
 */
fun allInEquipment(player: Player, vararg ids: Int): Boolean {
    return ids.all { id ->
        inEquipment(player, id)
    }
}

/**
 * Check that at least one item from a set of items is equipped by the given player
 * @param player the player
 * @param ids the set of item ids to check
 * @return true if the player has at least one of the items equipped, false if none are equipped
 */
fun anyInEquipment(player: Player, vararg ids: Int): Boolean {
    return ids.any { id ->
        inEquipment(player, id)
    }
}

/**
 * Gets the item in the given equipment slot for the given player
 * @param player the player whose equipment to pull from
 * @param slot the Equipment slot to use, EquipmentSlot enum contains the options.
 * @return the Item in the given slot, or null if none.
 */
fun getItemFromEquipment(player: Player, slot: EquipmentSlot): Item? {
    return player.equipment.get(slot.ordinal)
}

class ContainerisedItem(val container: core.game.container.Container?, val itemId: Int) {
    fun remove() : Boolean {
        return this.container?.remove(this.itemId.asItem()) ?: false
    }
    fun exists() : Boolean {
        return this.container != null && this.itemId > -1
    }
}

/**
 * Check if player has the specified item ID equipped, in inventory, or in their bank
 * @param id The item ID to check
 * @return A ContainerisedItem containing the container and the item ID if found, otherwise ContainerisedItem(null, -1) if not found
 */
fun hasAnItem(player: Player, id: Int): ContainerisedItem {
    return hasAnItem(player, arrayOf(id), false)
}

/**
 * Check if player has the specified item ID equipped, in inventory, or in their bank
 * @param id The item ID to check
 * @param checkSecondBank Whether to check the player's second bank.
 * @return A ContainerisedItem containing the container and the item ID if found, otherwise ContainerisedItem(null, -1) if not found
 */
fun hasAnItem(player: Player, id: Int, checkSecondBank: Boolean): ContainerisedItem {
    return hasAnItem(player, arrayOf(id), checkSecondBank)
}

/**
 * Check if player has any of the specified item IDs equipped, in inventory, or in their bank
 * @param ids An array of item IDs to check
 * @param checkSecondBank Whether to check the player's second bank.
 * @return A ContainerisedItem containing the container and the item ID if found, otherwise ContainerisedItem(null, -1) if not found
 */
fun hasAnItem(player: Player, ids: Array<Int>, checkSecondBank: Boolean): ContainerisedItem {
    // Search bank/equip/inv and return if item is found there.
    val searchSpace = if (checkSecondBank) {
        arrayOf(player.inventory, player.equipment, player.bankPrimary, player.bankSecondary)
    } else {
        arrayOf(player.inventory, player.equipment, player.bankPrimary)
    }
    for (container in searchSpace) {
        for (id in ids) {
            if (container.containItems(id)) {
                return ContainerisedItem(container, id)
            }
        }
    }

    // Check all costume storage containers and return if item is found there.
    for (id in ids) {
        if (StorableFamily.values().any { family ->
                player.getPOHStorageState()
                    .getContainer(family)
                    .contains(id)
            }) {
            // Note that this proxy container is a fake container just used to get the correct return type.
            return ContainerisedItem(player.POHStorageProxyContainer, id)
        }
    }

    // Item is not found.
    return ContainerisedItem(null, -1)
}

/**
 * Check if a player has an item equipped which corresponds to the given God
 * @param player the player to check
 * @param god the God whose equipment we are checking for
 * @return true if the player has an item corresponding to the given god, false otherwise
 */
fun hasGodItem(player: Player, god: God): Boolean {
    god.validItems.forEach { if (inEquipment(player, it)) return true }
    return false
}

/**
 * Check if the given player should have the "remove nothing" RoW effect active.
 * @param player the player we are checking
 * @return whether we should ignore nothings
 */
fun shouldRemoveNothings(player: Player) : Boolean {
    val ring = getItemFromEquipment(player, EquipmentSlot.RING)
    return ring != null && ring.id in Items.RING_OF_WEALTH_14638..Items.RING_OF_WEALTH4_14646
}

/**
 * Remove an item from a player's inventory
 * @param player the player whose inventory to remove the item from
 * @param item the ID or Item object to remove from the player's inventory
 * @param container the Container to remove the items from. An enum exists for this in the api package called Container. Ex: api.Container.BANK
 */
fun <T> removeItem(player: Player, item: T, container: Container = Container.INVENTORY): Boolean {
    item ?: return false
    val it = when (item) {
        is Item -> item
        is Int -> Item(item)
        else -> throw IllegalStateException("Invalid value passed for item")
    }

    return when (container) {
        Container.INVENTORY -> player.inventory.remove(it)
        Container.BANK -> player.bank.remove(it) || player.bankSecondary.remove(it)
        Container.EQUIPMENT -> player.equipment.remove(it)
        Container.BoB -> {
            if (player.familiarManager.hasFamiliar() && player.familiarManager.familiar.isBurdenBeast)
                (player.familiarManager.familiar as BurdenBeast).container.remove(it)
            else
                false
        }
    }
}

/**
 * Remove items in a players inventory
 * Checks that the player has enough first
 * @param player
 * @param items the items to remove
 */
fun removeItemsIfPlayerHasEnough(player: Player, vararg items: Item): Boolean {
    for (item in items) {
        if (amountInInventory(player, item.id) >= 1) continue
        return false
    }
    for (item in items) {
        if (removeItem(player, item)) continue
        return false
    }
    return true
}

/**
 * Add an item to the given player's given container
 * @param player the player whose container to modify
 * @param id the ID of the item to add
 * @param amount the amount of the item to add
 * @param container the Container to modify
 * @return true if the item was successfully added
 */
fun addItem(player: Player, id: Int, amount: Int = 1, container: Container = Container.INVENTORY): Boolean {
    val cont = when (container) {
        Container.INVENTORY -> player.inventory
        Container.BANK -> player.bank
        Container.EQUIPMENT -> player.equipment
        Container.BoB -> {
            if(player.familiarManager.hasFamiliar() && player.familiarManager.familiar.isBurdenBeast)
                (player.familiarManager.familiar as BurdenBeast).container
            else
                return false
        }
    }

    return cont.add(Item(id, amount))
}

/**
 * Replaces the item in the given slot in the given container with the given item.
 * @param player the player whose container to modify
 * @param slot the slot to use
 * @param item the item to replace the slot with
 * @param currentItem the current item that is being replaced
 * @param container the Container to modify
 * @return the item that was previously in the slot, or null if none.
 */
fun replaceSlot(player: Player, slot: Int, item: Item, currentItem: Item? = null, container: Container = Container.INVENTORY): Item? {
    val cont = when (container) {
        Container.INVENTORY -> player.inventory
        Container.EQUIPMENT -> player.equipment
        Container.BANK -> player.bank
        Container.BoB -> (player.familiarManager.familiar as BurdenBeast).container
    }

    if (item.id == 65535 || item.amount <= 0) {
        return cont.replace(null, slot)
    }

    if (currentItem == null) {
        return cont.replace(item, slot)
    }

    if (cont.remove(currentItem, slot, true)) {
        return cont.replace(item, slot) ?: currentItem
    }

    PlayerMonitor.log(player, LogType.DUPE_ALERT, "Potential slot-replacement-based dupe attempt, slot: $slot, item: $item")
    val other = when (container) {
        Container.INVENTORY -> Container.EQUIPMENT
        else -> Container.INVENTORY
    }
    if (removeItem(player, currentItem, other)) {
        return cont.replace(item, slot) ?: currentItem
    }
    return null
}

/**
 * Replaces all items a player owns anywhere (equipment, inventory, bank, second bank)
 * @param player the player whose inventory to remove the item from
 * @param itemId the item ID to replace
 * @param replaceId the replacement item ID
 * @author Player Name
 */
fun replaceAllItems(player: Player, itemId: Int, replaceId: Int) {
    val item = Item(itemId)
    val containers = if (player.familiarManager.hasFamiliar() && player.familiarManager.familiar.isBurdenBeast()) {
        arrayOf(player.inventory, player.equipment, player.bankPrimary, player.bankSecondary, (player.familiarManager.familiar as BurdenBeast).container)
    } else {
        arrayOf(player.inventory, player.equipment, player.bankPrimary, player.bankSecondary)
    }
    for (container in containers) {
        val hasItems = container.getAll(item)
        if (!item.definition.isStackable && container != player.bankPrimary && container != player.bankSecondary) {
            // just replace
            for (target in hasItems) {
                val newItem = Item(replaceId, target.amount)
                container.replace(newItem, target.slot, true)
            }
        } else {
            // add to existing stack if possible
            if (hasItems.size > 0) {
                val target = hasItems[0]
                var count = 0
                for (x in hasItems) {
                    count += x.amount
                }
                val newItem = Item(replaceId, count)
                container.replace(newItem, target.slot, true)
            }
            if (hasItems.size > 1) {
                for (i in 1 until hasItems.size) {
                    container.remove(hasItems[i], hasItems[i].slot, true)
                }
            }
        }
    }
}

/**
 * Add an item with a variable quantity or drop it if a player does not have enough space
 * @param player the player whose inventory to add to
 * @param id the ID of the item to add to the player's inventory
 * @param amount the amount of the ID to add to the player's inventory, defaults to 1
 */
fun addItemOrDrop(player: Player, id: Int, amount: Int = 1) {
    val item = Item(id, amount)
    if(amount == 1 || item.definition.isStackable) {
        if (!player.inventory.add(item)) GroundItemManager.create(item, player)
    } else {
        val singleItem = Item(id, 1)
        for(i in 0 until amount) {
            if(!player.inventory.add(singleItem)) GroundItemManager.create(singleItem, player)
        }
    }
}

/**
 * Add an item with a variable quantity or bank it if a player does not have enough space, or drop it if that still doesn't work
 * @param player the player whose inventory to add to
 * @param id the ID of the item to add to the player's inventory
 * @param amount the amount of the ID to add to the player's inventory, defaults to 1
 */
fun addItemOrBank(player: Player, id: Int, amount: Int = 1) {
    val item = Item(id, amount)
    if (!player.inventory.add(item)) {
        if (player.ironmanManager.mode != IronmanMode.ULTIMATE && player.bankPrimary.add(item)) {
            sendMessage(player, colorize("%RThe ${item.name} has been sent to your bank."))
        } else if (player.ironmanManager.mode != IronmanMode.ULTIMATE && player.bankSecondary.add(item)) {
            sendMessage(player, colorize("%RThe ${item.name} has been sent to your secondary bank."))
        } else {
            GroundItemManager.create(item, player)
            sendMessage(player, colorize("%RAs your inventory and bank account(s) are all full, the ${item.name} has been placed on the ground under your feet. Don't forget to grab it. (Also consider cleaning out some stuff, maybe? I mean, Jesus!)"))
        }
    }
}

/**
 * Clears an NPC with the "poof" smoke graphics commonly seen with random event NPCs.
 * @param npc the NPC object to initialize
 */
fun poofClear(npc: NPC) {
    submitWorldPulse(object : Pulse() {
        var counter = 0
        override fun pulse(): Boolean {
            when (counter++) {
                2 -> {
                    npc.isInvisible = true; Graphics.send(Graphics(86), npc.location)
                }
                3 -> npc.clear().also { return true }
            }
            return false
        }
    })
}

/**
 * Get number of free slots in a player's inventory
 * @param player the player to check
 * @return the number of free slots in the player's inventory
 */
fun freeSlots(player: Player): Int {
    return player.inventory.freeSlots()
}

/**
 * Get an animation by ID.
 * @param id the ID of the animation to use
 * @return an Animation object with the given ID.
 */
fun getAnimation(id: Int): Animation {
    return Animation(id)
}

/**
 * Get an animation by ID with priority
 * @param id the ID of the animation to get
 * @param priority the Animator.Priority enum instance to represent the desired priority
 * @return an Animation object with the given ID and priority
 */
fun getAnimationWithPriority(id: Int, priority: Animator.Priority): Animation {
    return Animation(id, Animator.Priority.values()[priority.ordinal])
}

/**
 * Reset a player's animator
 * @param player the player whose animator to reset
 */
fun resetAnimator(player: Player) {
    player.animator.animate(Animation(-1, Animator.Priority.VERY_HIGH))
}

/**
 *  Get the number of ticks an animation lasts
 *  @param animation the Animation object to check the duration of
 *  @return the number of ticks the given animation lasts for
 */
fun animationDuration(animation: Animation): Int {
    return animation.definition.durationTicks
}

fun animationCycles (animation: Int) : Int {
    val def = AnimationDefinition.forId(animation)
    return def.cycles
}

/**
 * Give a player some amount of experience in a specific skill
 * @param player the player to award XP to
 * @param skill the Skill ID to reward XP for. There is a Skills enum you can use for this. Example: Skills.STRENGTH
 * @param amount the amount, including decimal place, of experience to award
 */
fun rewardXP(player: Player, skill: Int, amount: Double) {
    player.skills.addExperience(skill, amount)
}

/**
 * Replace an object with the given revert timer
 * @param toReplace the Scenery instance we are replacing
 * @param with the ID of the Scenery we wish to replace toReplace with
 * @param forTicks the number of ticks the object should be replaced for. Use -1 for permanent.
 * @param loc the location to move the new object to if necessary. Defaults to null.
 */
fun replaceScenery(toReplace: Scenery, with: Int, forTicks: Int, loc: Location? = null) {
    val newLoc = when (loc) {
        null -> toReplace.location
        else -> loc
    }
    if (forTicks == -1) {
        SceneryBuilder.replace(toReplace, toReplace.transform(with, toReplace.rotation, newLoc))
    } else {
        SceneryBuilder.replace(toReplace, toReplace.transform(with, toReplace.rotation, newLoc), forTicks)
    }
    toReplace.isActive = false
}

/** 
 * Add a scenery to the world
 * @param sceneryId the ID of the scenery to add
 * @param location the location to place it at
 * @param rotation the rotation of the scenery (default 0)
 * @param type the type of the scenery (default 22)
 * @return the created scenery
*/
fun addScenery (sceneryId: Int, location: Location, rotation: Int = 0, type: Int = 22) : Scenery {
    val scenery = Scenery(sceneryId, location, type, rotation)
    SceneryBuilder.add(scenery)
    return scenery
}

fun addScenery (scenery: Scenery) {
    SceneryBuilder.add(scenery)
}

/**
 * Remove a scenery from the world
 * @param scenery the Scenery object to remove.
*/
fun removeScenery (scenery: Scenery) {
    SceneryBuilder.remove(scenery)
}

/**
 * Replace an object with the given revert timer with the given rotation
 * @param toReplace the Scenery instance we are replacing
 * @param with the ID of the Scenery we wish to replace toReplace with
 * @param forTicks the number of ticks the object should be replaced for. Use -1 for permanent.
 * @Param rotation the Direction of the rotation it should use. Direction.NORTH, Direction.SOUTH, etc
 * @param loc the location to move the new object to if necessary. Defaults to null.
 */
fun replaceScenery(toReplace: Scenery, with: Int, forTicks: Int, rotation: Direction, loc: Location? = null) {
    val newLoc = when (loc) {
        null -> toReplace.location
        else -> loc
    }
    val rot = when (rotation) {
        Direction.NORTH_WEST -> 0
        Direction.NORTH -> 1
        Direction.NORTH_EAST -> 2
        Direction.EAST -> 4
        Direction.SOUTH_EAST -> 7
        Direction.SOUTH -> 6
        Direction.SOUTH_WEST -> 5
        Direction.WEST -> 3
    }
    if (forTicks == -1) {
        SceneryBuilder.replace(toReplace, toReplace.transform(with, rot, newLoc))
    } else {
        SceneryBuilder.replace(toReplace, toReplace.transform(with, rot, newLoc), forTicks)
    }
    toReplace.isActive = false
}

/**
 * Gets the name of an item.
 * @param id the ID of the item to get the name of
 * @return the name of the item
 */
fun getItemName(id: Int): String {
    return ItemDefinition.forId(id).name
}

/**
 * Removes a ground item
 * @param node the GroundItem object to remove
 */
fun removeGroundItem(node: GroundItem) {
    GroundItemManager.destroy(node)
}

/**
 * Checks if a ground item is valid/still exists/should exist
 * @param node the GroundItem object to check the validity of
 * @return true if the node is valid, false otherwise
 */
fun isValidGroundItem(node: GroundItem): Boolean {
    return GroundItemManager.getItems().contains(node)
}

/**
 * Checks if a player has space for an item
 * @param player the player whose inventory to check
 * @param item the Item to check against
 * @return true if the player's inventory has space for the item
 */
fun hasSpaceFor(player: Player, item: Item): Boolean {
    return player.inventory.hasSpaceFor(item)
}

/**
 * Get the number of ticks passed since server startup
 */
fun getWorldTicks(): Int {
    return GameWorld.ticks
}

/**
 * Plays a jingle by id
 * @param player the player to play the jingle for
 * @param jingleId the jingle to play
 */
fun playJingle(player: Player, jingleId: Int) {
    PacketRepository.send(MusicPacket::class.java, MusicContext(player, jingleId, true))
}

/**
 * Impact an enemy with the given amount of damage and the given hitsplat type
 * @param entity the entity to damage
 * @param amount the amount of damage to deal
 * @param type the type of hit splat to use, ImpactHandler.HitsplatType is an enum containing these options.
 */
fun impact(entity: Entity, amount: Int, type: ImpactHandler.HitsplatType = ImpactHandler.HitsplatType.NORMAL) {
    entity.impactHandler.manualHit(entity, amount, type)
    if (entity is Player) playHurtAudio(entity)
}

/**
 * Get an item's definition
 * @param id the ID of the item to get the definition of
 * @return the ItemDefinition for the given ID.
 */
fun itemDefinition(id: Int): ItemDefinition {
    return ItemDefinition.forId(id)
}

/**
 * Check whether a node contains an interaction option.
 *
 * @param node: An NPC, a scenery object or an item to inspect.
 * @param option: The name of the interaction option to check for existence of.
 *
 * @return A Boolean value indicating the presence of the specified option in the given node.
 *
 * @author vddCore
 */
fun hasOption(node: Node, option: String): Boolean {
    return when (node) {
        is NPC -> node.definition.hasAction(option)
        is Scenery -> node.definition.hasAction(option)
        is Item -> node.definition.hasAction(option)
        else -> throw IllegalArgumentException("Expected an NPC, Scenery or an Item, got ${node.javaClass.simpleName}.")
    }
}

/**
 * Send an object animation
 */
fun animateScenery(player: Player, obj: Scenery, animationId: Int, global: Boolean = false) {
    player.packetDispatch.sendSceneryAnimation(obj, getAnimation(animationId), global)
}

/**
 * Send an object animation independent of a player
 */
fun animateScenery(obj: Scenery, animationId: Int) {
    val animation = Animation(animationId)
    animation.setObject(obj)
    getRegionChunk(obj.location).flag(AnimateObjectUpdateFlag(animation))
}

/**
 * Produce a ground item owned by the player
 */
fun produceGroundItem(player: Player, item: Int) {
    GroundItemManager.create(Item(item), player)
}

/**
 * Produces a ground item with the given ID and amount at the given location
 * @param owner the owner of the ground item, use null for none.
 * @param id the id of the item.
 * @param amount the amount of the item.
 * @param location the location of the item.
 * @return the created ground item.
 */
fun produceGroundItem(owner: Player?, id: Int, amount: Int, location: Location) : GroundItem {
   return GroundItemManager.create(Item(id, amount), location, owner)
}

/**
 * Spawns a projectile
 */
fun spawnProjectile(source: Entity, dest: Entity, projectileId: Int) {
    Projectile.create(source, dest, projectileId).send()
}

/**
 * Spawns a projectile with more advanced parameters
 * @param source the initial Location of the projectile
 * @param dest the final Location of the projectile
 * @param projectile the ID of the gfx used for the projectile
 * @param startHeight the height the projectile spawns at
 * @param endHeight the height the projectile ends at
 * @param delay the delay before the projectile spawns
 * @param speed the speed the projectile travels at
 * @param angle the angle the projectile travels at
 */
fun spawnProjectile(
    source: Location,
    dest: Location,
    projectile: Int,
    startHeight: Int,
    endHeight: Int,
    delay: Int,
    speed: Int,
    angle: Int
) {
    Projectile.create(
        source,
        dest,
        projectile,
        startHeight,
        endHeight,
        delay,
        speed,
        angle,
        source.getDistance(dest).toInt()
    ).send()
}

/**
 * Causes the given entity to face the given toFace
 * @param entity the entity you wish to face something
 * @param toFace the thing to face
 * @param duration how long you wish to face the thing for
 */
fun face(entity: Entity, toFace: Node, duration: Int = -1) {
    if (duration == -1) {
        when (toFace) {
            is Location -> entity.faceLocation(toFace)
            is Entity -> entity.face(toFace)
        }
    } else {
        when (toFace) {
            is Location -> entity.faceTemporary(toFace.asNpc(), duration)
            else -> entity.faceTemporary(toFace as Entity, duration)
        }
    }
}

/**
 * Causes the given entity to reset its face direction.
 * @param entity the entity to reset.
*/
fun resetFace (entity: Entity) {
    entity.face(null)
    entity.faceLocation(entity.location)
}

/**
 * Opens the given interface for the given player
 * @param player the player to open the interface for
 * @param id the ID of the interface to open
 */
fun openInterface(player: Player, id: Int) {
    player.interfaceManager.open(Component(id))
}

/**
 * Opens the given interface as an overlay for the given player
 * @param player the player to open the interface for
 * @param id the ID of the interface to open
 */
fun openOverlay(player: Player, id: Int) {
    player.interfaceManager.openOverlay(Component(id))
}

/**
 * Closes any open overlays for the given player
 * @param player the player to close for
 */
fun closeOverlay(player: Player) {
    player.interfaceManager.closeOverlay()
}

/**
 * Runs the given Emote for the given Entity
 * @param entity the entity to run the emote on
 * @param emote the Emotes enum entry to run
 */
fun emote(entity: Entity, emote: Emotes) {
    entity.animate(emote.animation)
}

/**
 * Sends a message to the given player.
 *
 * The message will be split on word boundaries into multiple lines so
 * that none of the lines will overflow the player's message box.
 *
 * @param player the player to send the message to.
 * @param message the message to send.
 */
fun sendMessage(player: Player, message: String) {
    player.sendMessages(*splitLines(message, 86))
}

/**
 * Forces an above-head chat message for the given entity
 * @param entity the entity to send the chat for
 * @param message the message to display
 */
fun sendChat(entity: Entity, message: String, delay: Int = -1) {
    if (delay > -1) {
        queueScript(entity, delay, QueueStrength.SOFT) {
            entity.sendChat(message)
            return@queueScript stopExecuting(entity)
        }
    } else entity.sendChat(message)
}

/**
 * Sends a message to a player's dialogue box
 * @param player the player to send the dialogue to
 * @param message the message to send, lines are split automatically.
 */
fun sendDialogue(player: Player, message: String) {
    player.dialogueInterpreter.sendDialogue(*splitLines(message))
}

/**
 * Sends a message to the player's dialogue box
 * @param player the player to send the dialogue to
 * @param lines the lines of dialogue to send. No automatic splitting.
 */
fun sendDialogueLines(player: Player, vararg message: String) {
    player.dialogueInterpreter.sendDialogue(*message)
}

/**
 * Sends options dialogue to the given player.
 * @param player the player to send the options to.
 * @param title the title of the dialogue options
 * @param options the options to present to the player
 */
fun sendDialogueOptions(player: Player, title: String, vararg options: String) {
    player.dialogueInterpreter.sendOptions(title, *options)
}

/**
 * Plays an animation on the entity
 * @param entity the entity to animate
 * @param anim the animation to play, can be an ID or an Animation object.
 * @param forced whether or not to force the animation (usually not necessary)
 */
fun <T> animate(entity: Entity, anim: T, forced: Boolean = false) {
    val animation = when (anim) {
        is Int -> Animation(anim)
        is Animation -> anim
        else -> throw IllegalStateException("Invalid value passed for anim")
    }

    if (forced) {
        entity.animator.forceAnimation(animation)
    } else {
        entity.animator.animate(animation)
    }
}

/**
 * Plays audio for the player
 * @param player the player to play the defined audio for
 * @param id the audio id to play
 * @param delay the delay in client cycles (50 cycles = 1 second)
 * @param loops the number of times to loop audio (for some audio ids it is used to define how many times to loop the audio)
 * @param location the location where the audio will play from (The sound will fade with distance)
 * @param radius the distance the audio can be heard from the defined location (default = 8 tiles if undefined)
 */
@JvmOverloads
fun playAudio(player: Player, id: Int, delay: Int = 0, loops: Int = 1, location: Location? = null, radius: Int = Audio.defaultAudioRadius) {
    PacketRepository.send(AudioPacket::class.java, DefaultContext(player, Audio(id, delay, loops, radius), location))
}

/**
 * Plays audio for players near a defined location
 * @param location the location where the audio will play from (The sound will fade with distance)
 * @param id the audio id to play
 * @param delay the delay in client cycles (50 cycles = 1 second)
 * @param loops the number of times to loop audio (for some audio ids it is used to define how many times to loop the audio)
 * @param radius the distance the audio can be heard from the defined location (default = 8 tiles if undefined)
 */
@JvmOverloads
fun playGlobalAudio(location: Location, id: Int, delay: Int = 0, loops: Int = 1, radius: Int = Audio.defaultAudioRadius) {
    val nearbyPlayers = RegionManager.getLocalPlayers(location, radius)
    for (player in nearbyPlayers) {
        PacketRepository.send(AudioPacket::class.java, DefaultContext(player, Audio(id, delay, loops, radius), location))
    }
}

/**
 * Plays a random hurt audio for the player based on gender
 * @param player the player to play hurt audio for
 * @param delay the delay in client cycles (50 cycles = 1 second)
 */
fun playHurtAudio(player: Player, delay: Int = 0) {
    val maleHurtAudio = intArrayOf(Sounds.HUMAN_HIT4_516, Sounds.HUMAN_HIT5_517, Sounds.HUMAN_HIT_518, Sounds.HUMAN_HIT_6_522)
    val femaleHurtAudio = intArrayOf(Sounds.FEMALE_HIT_506, Sounds.FEMALE_HIT_507, Sounds.FEMALE_HIT2_508, Sounds.FEMALE_HIT_2_510)
    if (player.isMale) {
        playAudio(player, maleHurtAudio.random(), delay)
    } else {
        playAudio(player, femaleHurtAudio.random(), delay)
    }
}

/**
 * Opens a dialogue with the given dialogue key or dialogue file, depending which is passed.
 * @param player the player to open the dialogue for
 * @param dialogue either the dialogue key or an instance of a DialogueFile
 * @param args various args to pass to the opened dialogue
 */
fun openDialogue(player: Player, dialogue: Any, vararg args: Any) {
    player.dialogueInterpreter.close()
    when (dialogue) {
        is Int -> player.dialogueInterpreter.open(dialogue, *args)
        is DialogueFile -> player.dialogueInterpreter.open(dialogue, *args)
        is SkillDialogueHandler -> dialogue.open()
        else -> log(ContentAPI::class.java, Log.ERR, "Invalid object type passed to openDialogue() -> ${dialogue.javaClass.simpleName}")
    }
}

/**
 * Closes any opened dialogue.
 */
fun closeDialogue(player: Player) {
    player.dialogueInterpreter.close()
    player.interfaceManager.closeChatbox()
}

/**
 * Gets an NPC with the given ID from the repository.
 * @param id the ID of the NPC to locate
 * @returns an NPC instance matching the ID if it finds one, null otherwise
 */
fun findNPC(id: Int): NPC? {
    return Repository.findNPC(id)
}

/**
 * Gets the spawned scenery from the world map using the given coordinates
 * @param x the X coordinate to use
 * @param y the Y coordinate to use
 * @param z the Z coordinate to use
 */
fun getScenery(x: Int, y: Int, z: Int): Scenery? {
    return RegionManager.getObject(z, x, y)
}

/**
 * Gets the spawned scenery from the world map using the given Location object.
 * @param loc the Location object to use.
 */
fun getScenery(loc: Location): Scenery? {
    return RegionManager.getObject(loc)
}

/**
 * Gets an NPC within render distance of the refLoc that matches the given ID
 * @param refLoc the Location to find the closes NPC to
 * @param id the ID of the NPC to locate
 * @returns an NPC instance matching the ID if it finds one, null otherwise
 */
fun findNPC(refLoc: Location, id: Int): NPC? {
    return Repository.npcs.firstOrNull { it.id == id && it.location.withinDistance(refLoc) }
}

/**
 * Gets an NPC with the given ID in the same general area  as the given Entity
 * @param entity the entity to search around
 * @param id the ID of the NPC to locate
 * @returns an NPC matching the given ID or null if none is found
 */
fun findLocalNPC(entity: Entity, id: Int): NPC? {
    return RegionManager.getLocalNpcs(entity).firstOrNull { it.id == id }
}

/**
 * Gets an NPC with the given ID in the same general area  as the given Entity
 * @param location The location to search around.
 * @param distance The maximum distance to the entity.
 * @returns an NPC matching the given ID or null if none is found
 */
fun findLocalNPCs(location: Location, distance: Int): MutableList<NPC> {
    return RegionManager.getLocalNpcs(location, distance)
}


/**
 * Gets a list of nearby NPCs that match the given IDs.
 * @param entity the entity to check around
 * @param ids the IDs of the NPCs to look for
 */
fun findLocalNPCs(entity: Entity, ids: IntArray): List<NPC> {
    return RegionManager.getLocalNpcs(entity).filter { it.id in ids }.toList()
}

/**
 * Gets a list of nearby NPCs that match the given IDs.
 * @param entity the entity to check around
 * @param ids the IDs of the NPCs to look for
 * @param distance The maximum distance to the entity.
 */
fun findLocalNPCs(entity: Entity, ids: IntArray, distance: Int): List<NPC> {
    return RegionManager.getLocalNpcs(entity, distance).filter { it.id in ids }.toList()
}

/**
 * @param regionId the ID of the region
 * @return a [ZoneBorders] encapsulating the entire region indicated by the provided regionId
 */
fun getRegionBorders(regionId: Int): ZoneBorders {
    return ZoneBorders.forRegion(regionId)
}

/**
 * Gets the value of an attribute key from the Entity's attributes store
 * @param entity the entity to get the attribute from
 * @param attribute the attribute key to use
 * @param default the default value to return if the attribute does not exist
 */
fun <T> getAttribute(entity: Entity, attribute: String, default: T): T {
    return entity.getAttribute(attribute, default)
}

/**
 * Sets an attribute key to the given value in an Entity's attribute store
 * @param entity the entity to set the attribute for
 * @param attribute the attribute key to use
 * @param value the value to set the attribute to
 */
fun <T> setAttribute(entity: Entity, attribute: String, value: T) {
    entity.setAttribute(attribute, value)
}

fun removeAttribute(entity: Entity, attribute: String) {
    entity.removeAttribute(attribute.replace("/save:",""))
}

fun removeAttributes(entity: Entity, vararg attributes: String) {
    for (attribute in attributes) removeAttribute(entity, attribute)
}

/**
 * Adds the given timer to the entity's active timers.
 * @param entity the entity whose timers are being added to
 * @param timer the timer being added
**/
fun registerTimer (entity: Entity, timer: RSTimer?) {
    if (timer == null) return
    entity.timers.registerTimer (timer)
}

/**
 * Used to fetch the existing, active, non-abstract and non-anonymous timer with the given identifier, or start a new timer if none exists and return that.
 * @param entity the entity whose timers we're retrieving
 * @param identifier the identifier of the timer, refer to the individual timer class for this token.
 * @param args various args to pass to the initialization of the timer, if applicable.
 * @return Either the existing active timer, or a new timer initialized with the passed args if none exists yet.
**/
fun getOrStartTimer (entity: Entity, identifier: String, vararg args: Any) : RSTimer? {
    val existing = getTimer (entity, identifier)
    if (existing != null)
        return existing
    return spawnTimer (identifier, *args).also { registerTimer (entity, it) }
}

/**
 * Used to fetch the existing, active, non-abstract and non-anonymous timer with the given type, or start a new timer if none exists and return that.
 * @param entity the entity whose timer we're retrieving
 * @param T the type of timer we are fetching
 * @param args the various args to pass to the initialization of the timer, if applicable.
 * @return Either the existing, active timer or a new timer initialized with the passed args if none exists yet.
**/
inline fun <reified T: RSTimer> getOrStartTimer (entity: Entity, vararg args: Any) : T {
    val existing = getTimer <T> (entity)
    if (existing != null)
        return existing
    return spawnTimer <T> (*args).also { registerTimer (entity, it) }
}

/**
 * Used to fetch a new instance of a registered (see: non-anonymous, non-abstract) timer with the given configuration args
 * @param identifier the string identifier for the timer. e.g. poison's is "poison"
 * @param args various arbitrary arguments to be passed to the timer's constructor. Refer to the timer in question for what the args are expected to be.
 * @return a timer instance configured with your given args, or null if no timer with the given key exists in the registry.
**/
fun spawnTimer (identifier: String, vararg args: Any) : RSTimer? {
    return TimerRegistry.getTimerInstance (identifier, *args)
}

/**
 * Used to fetch a new instance of a registered (see: non-anonymous, non-abstract) timer with the given configuration args
 * @param T the type of the timer you're trying to retrieve. The timer registry will be searched for a timer of this type.
 * @param args various arbitrary arguments to be passed to the timer's constructor. Refer to the timer in question for what the args are expected to be.
 * @return a timer instance configured with your given args, or null if the timer is not listed in the registry (if this happens, your timer is either abstract or anonymous.)
**/
inline fun <reified T: RSTimer> spawnTimer (vararg args: Any) : T {
    return TimerRegistry.getTimerInstance <T> (*args)!!
}

/**
 * Used to check if a timer of the given type is registered and active in the entity's timers.
 * @param T the type of timer
 * @param entity the entity whose timers are being checked
 * @return true if there is a timer registered and active with the given type.
**/
inline fun <reified T: RSTimer> hasTimerActive (entity: Entity) : Boolean {
    return getTimer<T>(entity) != null
}

/**
 * Used to get the active instance of a timer with the given type from the entity's timers.
 * @param T the type of timer
 * @param entity the entity whose timers we are checking
 * @return the active instance of the given type in the entity's timers, or null.
*/
inline fun <reified T: RSTimer> getTimer (entity: Entity) : T? {
    return entity.timers.getTimer<T>()
}

/**
 * Removes any active timers of the given type from the entity's active timers. This will remove ALL matching instances.
 * @param T the type of timer
 * @param entity the entity whose timers are being checked
**/
inline fun <reified T: RSTimer> removeTimer (entity: Entity) {
    entity.timers.removeTimer<T>()
}

/**
 * Used to check if a timer with the given identifier string is registered and active in the entity's timers.
 * @param identifier the identifier token assigned to the timer. You'll have to refer to the actual timer class for this string.
 * @param entity the entity whose timers are being checked
 * @return true if there's a timer with the given identifier active in the entity's timers
**/
fun hasTimerActive (entity: Entity, identifier: String) : Boolean {
    return getTimer (entity, identifier) != null
}

/**
 * Used to get the active instance of a timer with the given identifier from the entity's timers.
 * @param identifier the string identifier of the timer we are looking for. You'll have to refer to the actual timer class for this string.
 * @param entity the entity whose timers are being checked
 * @return the instance of the active timer if found, null otherwise.
**/
fun getTimer (entity: Entity, identifier: String) : RSTimer? {
    return entity.timers.getTimer(identifier)
}

/**
 * Removes any active timers with the given identifier from the entity's active timers. This will remove ALL matching instances.
 * @param identifier the identifier token assigned to the timer. You'll have to refer to the actual timer class for this string.
 * @param entity the entity whose timers are being checked
**/
fun removeTimer (entity: Entity, identifier: String) {
    entity.timers.removeTimer(identifier)
}

/**
 * Removes the given timer from the entity's active timers. Note this variant will only work with a reference to the same exact timer you want to remove.
 * @param entity the entity whose timers we are modifying
 * @param timer the timer to remove
**/
fun removeTimer (entity: Entity, timer: RSTimer) {
    entity.timers.removeTimer(timer)
}

/**
 * Locks the given entity for the given number of ticks
 * @param entity the entity to lock
 * @param duration the number of ticks to lock for
 */
fun lock(entity: Entity, duration: Int) {
    entity.lock(duration)
}

/**
 * Locks specifically an entity's interactions, allowing movement still
 * @param entity the entity to lock
 * @param duration the duration in ticks to lock for
 */
fun lockInteractions(entity: Entity, duration: Int) {
    entity.locks.lockInteractions(duration)
}

/**
 * Unlocks the given entity
 * @param entity the entity to unlock
 */
fun unlock(entity: Entity) {
    entity.unlock()
}

/**
 * Transforms an NPC for the given number of ticks
 * @param npc the NPC object to transform
 * @param transformTo the ID of the NPC to turn into
 * @param restoreTicks the number of ticks until the NPC returns to normal
 */
fun transformNpc(npc: NPC, transformTo: Int, restoreTicks: Int) {
    npc.transform(transformTo)
    Pulser.submit(object : Pulse(restoreTicks) {
        override fun pulse(): Boolean {
            npc.reTransform()
            return true
        }
    })
}

/**
 * Transforms an NPC for the given number of ticks with hp carryover
 * @param npc the NPC object to transform
 * @param transformTo the ID of the NPC to turn into
 * @param restoreTicks the number of ticks until the NPC returns to normal
 */
fun transformNpcWithHpCarryover(npc: NPC, transformTo: Int, restoreTicks: Int) {
    npc.transformWithHpCarryover(transformTo)
    queueScript(npc, 0, QueueStrength.SOFT) { stage: Int ->
        when (stage) {
            0 -> return@queueScript delayScript(npc, restoreTicks)
            else -> {
                npc.reTransform()
                return@queueScript stopExecuting(npc)
            }
        }
    }
}

/**
 * Produces a Location object using the given x,y,z values
 */
fun location(x: Int, y: Int, z: Int): Location {
    return Location.create(x, y, z)
}

/**
 * Checks if the given entity is within the given ZoneBorders
 */
fun inBorders(entity: Entity, borders: ZoneBorders): Boolean {
    return borders.insideBorder(entity)
}

/**
 * Checks if the given entity is within the given borders
 */
fun inBorders(entity: Entity, swX: Int, swY: Int, neX: Int, neY: Int): Boolean {
    return ZoneBorders(swX, swY, neX, neY).insideBorder(entity)
}

/**
 * AHeals the given entity for the given number of hitpoints
 */
fun heal(entity: Entity, amount: Int) {
    entity.skills.heal(amount)
}

fun getVarp (player: Player, varpIndex: Int) : Int {
    return player.varpMap[varpIndex] ?: 0
}

fun getVarbit (player: Player, def: VarbitDefinition) : Int {
    val mask = def.mask
    val current = getVarp (player, def.varpId)
    return (current shr def.startBit) and mask
}

fun getVarbit (player: Player, varbitId: Int) : Int {
    val def = VarbitDefinition.forId(varbitId)
    return getVarbit (player, def)
}

@JvmOverloads
fun setVarp (player: Player, varpIndex: Int, value: Int, save: Boolean = false) {
    player.varpMap[varpIndex] = value
    if (player.saveVarp[varpIndex] != true && save)
        player.saveVarp[varpIndex] = true //only set if we're choosing to save. Prevents accidental unsaving. if you REALLY want to unsave a varp, use unsaveVarp.
    player.packetDispatch.sendVarp(varpIndex, value)
}

fun saveVarp (player: Player, varpIndex: Int) {
    player.saveVarp[varpIndex] = true
}

fun unsaveVarp (player: Player, varpIndex: Int) {
    player.saveVarp.remove(varpIndex)
}

@JvmOverloads
fun setVarbit (player: Player, def: VarbitDefinition, value: Int, save: Boolean = false) {
    val mask = def.mask
    val current = getVarp (player, def.varpId) and (mask shl def.startBit).inv()
    val newValue = (value and mask) shl def.startBit
    setVarp (player, def.varpId, current or newValue, save)
}

@JvmOverloads
fun setVarbit (player: Player, varbitId: Int, value: Int, save: Boolean = false) {
    val def = VarbitDefinition.forId(varbitId)

    if (def == null) {
        logWithStack (ContentAPI::class.java, Log.ERR, "Trying to setVarbit $varbitId, which doesn't seem to exist.")
        return
    }
    
    setVarbit (player, def, value, save)
}

fun setVarc (player: Player, varc: Int, value: Int)
{
    player.packetDispatch.sendVarcUpdate(varc.toShort(), value)
}

fun reinitVarps (player: Player) {
    for ((index, value) in player.varpMap) {
        setVarp(player, index, value, player.saveVarp[index] ?: false)
    }
}

/**
 * Force an entity to walk to a given destination.
 * @param entity the entity to forcewalk
 * @param dest the Location object to walk to
 * @param type the type of pathfinder to use. "smart" for the SMART pathfinder, "clip" for the noclip pathfinder, anything else for DUMB.
 */
fun forceWalk(entity: Entity, dest: Location, type: String) {
    if (type == "clip") {
        ForceMovement(entity, dest, 10, 10).run()
        return
    }
    val pathfinder = when (type) {
        "smart" -> Pathfinder.SMART
        else -> Pathfinder.DUMB
    }
    val path = Pathfinder.find(entity, dest, true, pathfinder)
    path.walk(entity)
}

/**
 * Returns a location truncated to the appropriate pathfinding limit
**/
fun truncateLoc (mover: Entity, destination: Location) : Pair<Boolean,Location> {
    val vector = Vector.betweenLocs(mover.location, destination)
    val normVec = vector.normalized()
    val mag = vector.magnitude()

    var multiplier = if (mover is NPC) 14.0 else ServerConstants.MAX_PATHFIND_DISTANCE.toDouble()
    var clampedMultiplier = min(multiplier, mag)

    var truncated = multiplier == clampedMultiplier

    return Pair(truncated, mover.location.transform(normVec * clampedMultiplier))
}

/**
 * Force a player to move from the start location to the dest location
 * @param player the player we are moving
 * @param start the start location
 * @param dest the end location
 * @param startArrive the number of client cycles to take moving the player to the start location
 * @param destArrive the number of client cycles to take moving the player to the end location from the start location
 * @param direction (optional) the direction to face the player during the movement
 * @param anim (optional) the animation to use throughout the movement
 * @param callback (optional) a callback called when the forced  movement completes
 * @see NOTE: There are 30 client cycles per second.
*/
fun forceMove (player: Player, start: Location, dest: Location, startArrive: Int, destArrive: Int, dir: Direction? = null, anim: Int = 819, callback: (()->Unit)? = null) {
    var direction: Direction

    if (dir == null) {
        var delta = Location.getDelta(start, dest)
        var x = abs(delta.x)
        var y = abs(delta.y)

        if (x > y) 
            direction = Direction.getDirection(delta.x, 0) 
        else 
            direction = Direction.getDirection(0, delta.y)
    } else direction = dir

    val startLoc = Location.create(start)
    val destLoc = Location.create(dest)
    var startArriveTick = getWorldTicks() + cyclesToTicks (startArrive) + 1
    var destArriveTick = startArriveTick + cyclesToTicks (destArrive)
    var maskSet = false

    delayEntity(player, (destArriveTick - getWorldTicks()) + 1)
    queueScript (player, 0, QueueStrength.SOFT) { stage: Int ->
        if (!finishedMoving(player))
            return@queueScript keepRunning(player)
        if (!maskSet) {
            var ctx = ForceMoveCtx (startLoc, destLoc, startArrive, destArrive, direction)
            player.updateMasks.register(EntityFlag.ForceMove, ctx)
            maskSet = true
        }

        var tick = getWorldTicks()
        if (tick < startArriveTick) {
            return@queueScript keepRunning(player)
        } else if (tick < destArriveTick) {
            if (animationFinished(player))
                animate (player, anim)
            return@queueScript keepRunning(player)
        } else if (tick >= destArriveTick) {
            try {
                callback?.invoke()
            } catch (e: Exception) {
                e.printStackTrace()
            }
            player.properties.teleportLocation = dest
            return@queueScript stopExecuting(player)
        }
        return@queueScript stopExecuting(player)
    }
}

/**
 * Interrupts a given entity's walking queue
 * @param entity the entity to interrupt
 */
fun stopWalk(entity: Entity) {
    entity.walkingQueue.reset()
}

/**
 * Returns a list of all valid children IDs for a given scenery ID
 */
fun getChildren(scenery: Int): IntArray {
    return SceneryDefinition.forId(scenery).getChildrenIds().filter { it != -1 }.toIntArray()
}

/**
 * Adjusts the charge for the given node.
 * @param node the node to adjust the charge of
 * @param amount the amount to adjust by
 */
fun adjustCharge(node: Node, amount: Int) {
    when (node) {
        is Item -> node.charge += amount
        is Scenery -> node.charge += amount
        else -> log(ContentAPI::class.java, Log.ERR, "Attempt to adjust the charge of invalid type: ${node.javaClass.simpleName}")
    }
}

/**
 * Get the current charge of the given node
 * @param node the node whose charge to check
 * @return amount of charges the node has, or -1 if the node does not accept charges.
 */
fun getCharge(node: Node): Int {
    when (node) {
        is Item -> return node.charge
        is Scenery -> return node.charge
        else -> log(ContentAPI::class.java, Log.ERR, "Attempt to get charge of invalid type: ${node.javaClass.simpleName}")
            .also { return -1 }
    }
}

/**
 * Set the charge of the given node to the given amount.
 * @param node the node to set the charge for
 * @param charge the amount to set the node's charge to (default is 1000)
 */
fun setCharge(node: Node, charge: Int) {
    when (node) {
        is Item -> node.charge = charge
        is Scenery -> node.charge = charge
        else -> log(ContentAPI::class.java, Log.ERR, "Attempt to set the charge of invalid type: ${node.javaClass.simpleName}")
    }
}

/**
 * Gets the used option in the context of an interaction.
 * @param player the player to get the used option for.
 * @return the option the player used
 */
fun getUsedOption(player: Player): String {
    return player.getAttribute("interact:option", "INVALID")
}

/**
 * Used to play both an Animation and Graphics object simultaneously.
 * Use -1 in place of anim or graphics if not needed.
 * @param entity the entity to perform this on
 * @param anim the Animation object to use, can also be an ID.
 * @param gfx the Graphics object to use, can also be an ID.
 */
fun <A, G> visualize(entity: Entity, anim: A, gfx: G) {
    val animation = when (anim) {
        is Int -> Animation(anim)
        is Animation -> anim
        else -> throw IllegalStateException("Invalid parameter passed for animation.")
    }

    val graphics = when (gfx) {
        is Int -> Graphics(gfx)
        is Graphics -> gfx
        else -> throw IllegalStateException("Invalid parameter passed for graphics.")
    }

    entity.visualize(animation, graphics)
}

/**
 * Used to submit a pulse to the GameWorld's Pulser.
 * @param pulse the Pulse object to submit
 */
fun submitWorldPulse(pulse: Pulse) {
    Pulser.submit(pulse)
}

/**
 * Used to submit a pulse to the GameWorld's Pulser, albeit with a cleaner syntax.
 * @param task an anonymous function that will be run in the Pulse
 * @return the newly submitted Pulse
 */
fun runWorldTask(task: () -> Unit): Pulse {
    val pulse = object : Pulse() {
        override fun pulse(): Boolean {
            task.invoke()
            return true
        }
    }

    submitWorldPulse(pulse)
    return pulse
}

/**
 * Teleports or "instantly moves" an entity to a given Location object.
 * @param entity the entity to move
 * @param loc the Location object to move them to
 * @param type the teleport type to use (defaults to instant). An enum exists as TeleportManager.TeleportType.
 */
fun teleport(entity: Entity, loc: Location, type: TeleportManager.TeleportType = TeleportManager.TeleportType.INSTANT) : Boolean {
    if (type == TeleportManager.TeleportType.INSTANT) {
        entity.properties.teleportLocation = loc
        return true
    }
    else return entity.teleporter.send(loc, type)
}

/**
 * Sets the dynamic or "temporary" (restores) level of a skill.
 * @param entity the entity to set the level for
 * @param skill the Skill to set. A Skills enum exists that can be used. Ex: Skills.STRENGTH
 * @param level the level to set the skill to
 */
fun setTempLevel(entity: Entity, skill: Int, level: Int) {
    entity.skills.setLevel(skill, level)
}

/**
 * Gets the static (unchanging/max) level of an entity's skill
 * @param entity the entity to get the level for
 * @param skill the Skill to get the level of. A Skills enum exists that can be used. Ex: Skills.STRENGTH
 * @return the static level of the skill
 */
fun getStatLevel(entity: Entity, skill: Int): Int {
    return entity.skills.getStaticLevel(skill)
}

/**
 * Gets the dynamic (boostable/debuffable/restoring) level of an entity's skill
 * @param entity the entity to get the level for
 * @param skill the Skill to get the level of. A Skills enum exists that can be used. Ex: Skills.STRENGTH
 * @return the dynamic level of the skill
 */
fun getDynLevel(entity: Entity, skill: Int): Int {
    return entity.skills.getLevel(skill)
}

/**
 * Adjusts (buffs/debuffs) the given Skill by the amount given.
 * @param entity the entity to adjust the skill for
 * @param skill the Skill to adjust. A Skills enum exists that can be used. Ex: Skills.STRENGTH
 * @param amount the amount to adjust the skill by. Ex-Buff: 5, Ex-Debuff: -5
 */
fun adjustLevel(entity: Entity, skill: Int, amount: Int) {
    entity.skills.setLevel(skill, entity.skills.getStaticLevel(skill) + amount)
}

/**
 * Remove all of a given item from the given container
 * @param player the player to remove the item from
 * @param item the item to remove. Can be an Item object or an ID.
 * @param container the Container to remove the item from. An enum exists for this called Container. Ex: Container.BANK
 */
fun <T> removeAll(player: Player, item: T, container: Container = Container.INVENTORY): Boolean {
    item ?: return false
    val it = when (item) {
        is Item -> item.id
        is Int -> item
        else -> throw IllegalStateException("Invalid value passed as item")
    }

    return when (container) {
        Container.EQUIPMENT -> player.equipment.remove(Item(it, amountInEquipment(player, it)))
        Container.BANK -> {
            val amountInPrimary = amountInBank(player, it, false)
            val amountInSecondary = amountInBank(player, it, true) - amountInPrimary
            player.bank.remove(Item(it, amountInPrimary)) && player.bankSecondary.remove(Item(it, amountInSecondary))
        }
        Container.INVENTORY -> player.inventory.remove(Item(it, amountInInventory(player, it)))
        Container.BoB -> {
            if (player.familiarManager.hasFamiliar() && player.familiarManager.familiar.isBurdenBeast){
                val cont = (player.familiarManager.familiar as BurdenBeast).container
                cont.remove(Item(it, cont.getAmount(it)))
            }
            else
                false
        }
    }
}

/**
 * Sends a string to a specific interface child
 * @param player the player to send the packet to
 * @param string the string to send to the child
 * @param iface the ID of the interface to use
 * @param child the index of the child to send the string to
 */
fun setInterfaceText(player: Player, string: String, iface: Int, child: Int) {
    player.packetDispatch.sendString(string, iface, child)
}

/**
 * Allows you to hide or show specific children in an interface
 * @param player the player to send the packet to
 * @param iface the ID of the interface to use
 * @param child the index of the child to hide or show
 * @param hide if the child should be hidden or not
 */
fun setComponentVisibility(player: Player, iface: Int, child: Int, hide: Boolean) {
    player.packetDispatch.sendInterfaceConfig(iface, child, hide)
}

/**
 * Closes any open (non-chat) interfaces for the player
 * @param player the player to close the interface for
 */
fun closeInterface(player: Player) {
    player.interfaceManager.close()
}

/**
 * Closes any opened tab interfaces for the player
 * @param player the player to close the tab for
 */
fun closeTabInterface(player: Player) {
    player.interfaceManager.closeSingleTab()
}

/**
 * Closes any open (both chat and non-chat) interfaces for the player
 * @param player the player to close the interfaces for
 */
fun closeAllInterfaces(player: Player) {
    player.interfaceManager.close()
    player.interfaceManager.closeChatbox()
    player.dialogueInterpreter.close()
}

/**
 * Sends a dialogue that uses the player's chathead.
 * @param player the player to send the dialogue to
 * @param msg the message to send.
 * @param expr the FacialExpression to use. An enum exists for these called FacialExpression. Defaults to FacialExpression.FRIENDLY
 * @param hide should the continue button be hidden?
 */
fun sendPlayerDialogue(player: Player, msg: String, expr: core.game.dialogue.FacialExpression = core.game.dialogue.FacialExpression.FRIENDLY, hide: Boolean = false) {
    player.dialogueInterpreter.sendDialogues(player, expr, hide, *splitLines(msg))
}

/**
 * Sends a player model to a specific interface child
 * @param player the player to send the packet to and whose model to use
 * @param iface the ID of the interface to send it to
 * @param child the index of the child on the interface to send the model to
 */
fun sendPlayerOnInterface(player: Player, iface: Int, child: Int) {
    player.packetDispatch.sendPlayerOnInterface(iface, child)
}

/**
 * Sends a dialogue that uses the npc's chathead.
 * @param player the player to send the dialogue to
 * @param npc the ID of the NPC to use for the chathead
 * @param msg the message to send.
 * @param expr the FacialExpression to use. An enum exists for these called FacialExpression. Defaults to FacialExpression.FRIENDLY
 * @param hide should the continue button be hidden?
 */
fun sendNPCDialogue(player: Player, npc: Int, msg: String, expr: core.game.dialogue.FacialExpression = core.game.dialogue.FacialExpression.FRIENDLY,
                    hide: Boolean = false) {
    player.dialogueInterpreter.sendDialogues(npc, expr, hide,  *splitLines(msg))
}

/**
 * Sends a dialogue that uses the npc's chathead.
 * @param player the player to send the dialogue to
 * @param npc the ID of the NPC to use for the chathead
 * @param expr the FacialExpression to use. An enum exists for these called FacialExpression.
 * @param msg the message to send.
 */
fun sendNPCDialogueLines(player: Player, npc: Int, expr: core.game.dialogue.FacialExpression, hideContinue: Boolean, vararg msgs: String) {
    val dialogueComponent = player.dialogueInterpreter.sendDialogues(npc, expr, *msgs)
    player.packetDispatch.sendInterfaceConfig(dialogueComponent.id, msgs.size + 4, hideContinue)
}

/**
 * Sends an animation to a specific interface child
 * @param player the player to send the packet to
 * @param anim the ID of the animation to send to the interface
 * @param iface the ID of the interface to send the animation to
 * @param child the index of the child on the interface to send the model to
 */
fun sendAnimationOnInterface(player: Player, anim: Int, iface: Int, child: Int) {
    player.packetDispatch.sendAnimationInterface(anim, iface, child)
}

/**
 * Register a logout listener to a player. Logout listeners are methods that run when a player logs out.
 * @param player the player to register the listener for
 * @param handler the method to run when the listener is invoked (when the player logs out)
 */
fun registerLogoutListener(player: Player, key: String, handler: (p: Player) -> Unit) {
    player.logoutListeners[key] = handler
}

/**
 * Removes a logout listener based on the key from a player
 * @param player the player to remove the logout listner from
 * @param key the key of the logout listener to remove.
 */
fun clearLogoutListener(player: Player, key: String) {
    player.logoutListeners.remove(key)
}

/**
 * Sends an item to a specific child on an interface
 * @param player the player to send the packet to
 * @param iface the ID of the interface to send the item onto
 * @Param child the index of the child on the interface to send the item onto
 * @param item the ID of the item to send
 * @param amount the amount of the item to send - defaults to 1
 */
fun sendItemOnInterface(player: Player, iface: Int, child: Int, item: Int, amount: Int = 1) {
    player.packetDispatch.sendItemOnInterface(item, amount, iface, child)
}

/**
 * Sends a zoomed item to a specific child on an interface
 * @param player the player to send the packet to
 * @param iface the ID of the interface to send the item onto
 * @Param child the index of the child on the interface to send the item onto
 * @param item the ID of the item to send
 * @param zoom the amount of zoom to apply to the item - defaults to 230
 */
fun sendItemZoomOnInterface(player: Player, iface: Int, child: Int, item: Int, zoom: Int = 230) {
    player.packetDispatch.sendItemZoomOnInterface(item, zoom, iface, child)
}

/**
 * Sends a dialogue box with a single item and some text
 * @param player the player to send it to
 * @param item the ID of the item to show
 * @param message the text to display
 */
fun sendItemDialogue(player: Player, item: Any, message: String) {
    val dialogueItem = when (item) {
        is Item -> item
        is Int -> Item(item)
        else -> {
            throw java.lang.IllegalArgumentException("Expected an Item or an Int, got ${item::class.java.simpleName}.")
        }
    }

    player.dialogueInterpreter.sendItemMessage(dialogueItem, *splitLines(message))
}

/**
 * Sends a dialogue box with two items and some text
 * @param player the player to send it to
 * @param item1 the ID of the first item to show
 * @param item2 the ID of the second item to show
 * @param message the text to display
 */
fun sendDoubleItemDialogue(player: Player, item1: Any, item2: Any, message: String) {
    when (item1) {
        is Item -> player.dialogueInterpreter.sendDoubleItemMessage(item1, item2 as Item, *splitLines(message))
        is Int -> player.dialogueInterpreter.sendDoubleItemMessage(item1, item2 as Int, *splitLines(message))
    }
}

/**
 * Send an input dialogue to retrieve a specified value from the player
 * @param player the player to send the input dialogue to
 * @param numeric whether or not the input is numeric
 * @param prompt what to prompt the player
 * @param handler the method that handles the value gained from the input dialogue
 */
fun sendInputDialogue(player: Player, numeric: Boolean, prompt: String, handler: (value: Any) -> Unit) {
    if (numeric) sendInputDialogue(player, InputType.NUMERIC, prompt, handler)
    else sendInputDialogue(player, InputType.STRING_SHORT, prompt, handler)
}

/**
 * Send input dialogues based on type. Some dialogues are special and can't be covered by the other sendInputDialogue method
 * @param player the player to send the input dialogue to
 * @param type the input type to send - an enum is available for this called InputType
 * @param prompt what to prompt the player
 * @param handler the method that handles the value from the input dialogue
 */
fun sendInputDialogue(player: Player, type: InputType, prompt: String, handler: (value: Any) -> Unit) {
    when (type) {
        InputType.AMOUNT -> {
            player.setAttribute("parseamount", true)
            player.dialogueInterpreter.sendInput(true, prompt)
        }

        InputType.NUMERIC, InputType.STRING_SHORT -> player.dialogueInterpreter.sendInput(
            type != InputType.NUMERIC,
            prompt
        )

        InputType.STRING_LONG -> player.dialogueInterpreter.sendLongInput(prompt)
        InputType.MESSAGE -> player.dialogueInterpreter.sendMessageInput(prompt)
    }

    player.setAttribute("runscript", handler)
    player.setAttribute("input-type", type)
}

/**
 * Forces an NPC to "flee" from a player or other entity
 * @param entity the entity to make flee
 * @param from the entity to flee from
 */
fun flee(entity: Entity, from: Entity) {
    lock(entity, 5)
    face(entity, from, 5)

    val diffX = entity.location.x - from.location.x
    val diffY = entity.location.y - from.location.y

    forceWalk(entity, entity.location.transform(diffX, diffY, 0), "DUMB")
}

/**
 * Submits an individual or "weak" pulse to a specific entity's pulse manager. Pulses submitted this way can be overridden by other pulses.
 * @param entity the entity to submit the pulse to
 * @param pulse the pulse to submit
 */
fun submitIndividualPulse(entity: Entity, pulse: Pulse) {
    entity.pulseManager.run(pulse)
}

/**
 * Similar to submitIndividualPulse, but for non-repeating tasks, with a cleaner syntax.
 */
fun runTask(entity: Entity, delay: Int = 0, repeatTimes: Int = 1, task: () -> Unit) {
    var cycles = repeatTimes
    entity.pulseManager.run(object : Pulse(delay) {
        override fun pulse(): Boolean {
            task.invoke()
            return --cycles <= 0
        }
    })
}

/**
 * Gets the number of QP a player has
 * @param player the player to get the QP for
 * @return the number of QP the player has
 */
fun getQuestPoints(player: Player): Int {
    return player.questRepository.points
}

/**
 * Gets the stage for the given quest for the given player
 */
fun getQuestStage(player: Player, quest: Quests): Int {
    return player.questRepository.getStage(quest)
}

/**
 * Sets the stage for the given quest for the given player
 */
fun setQuestStage(player: Player, quest: Quests, stage: Int) {
    player.questRepository.setStage(QuestRepository.getQuests()[quest]!!, stage)
    player.questRepository.syncronizeTab(player)
}

/**
 * Check if a quests' stage for a given player is (inclusively) between the provided start and end stage
 */
fun isQuestInProgress(player: Player, quest: Quests, startStage: Int, endStage: Int): Boolean {
    return player.questRepository.getStage(quest) in startStage..endStage
}

fun isQuestStarted(player: Player, quest: Quests): Boolean {
    return getQuestStage(player, quest) > 0
}

/**
 * Check if a quest is complete
 */
fun isQuestComplete(player: Player, quest: Quests): Boolean {
    return player.questRepository.getStage(quest) == 100
}

/**
 * Get the quest for a player.
 * @param player The player to get the quest for.
 * @param quest The quest name string
 * @return the quest object
 */
fun getQuest(player: Player, quest: Quests): Quest {
    return player.questRepository.getQuest(quest)
}


/**
 * Check if a player meets the requirements to start a quest, and then starts it if they do. Returns success bool
 */
fun startQuest(player: Player, quest: Quests): Boolean {
    val quest = player.questRepository.getQuest(quest)
    val canStart = quest.hasRequirements(player)
    if (!canStart) return false
    quest.start(player)
    return true
}

/**
 * Finishes a quest, gives rewards, marks as completed, etc
 */
fun finishQuest(player: Player, quest: Quests) {
    player.questRepository.getQuest(quest).finish(player)
}

/**
 * Gets a scenery definition from the given ID
 * @param id the ID of the scenery to get the definition for.
 * @return the scenery definition
 */
fun sceneryDefinition(id: Int): SceneryDefinition {
    return SceneryDefinition.forId(id)
}

/**
 * Register a map zone
 * @param zone the zone to register
 * @param borders the ZoneBorders that compose the zone
 */
fun registerMapZone(zone: MapZone, borders: ZoneBorders) {
    ZoneBuilder.configure(zone)
    zone.register(borders)
}

/**
 * Animates a component of an interface.
 * @param player the player to animate the interface for.
 * @param iface the ID of the interface to animate.
 * @param child the child on the interface to animate.
 * @param anim the ID of the animation to use.
 */
fun animateInterface(player: Player, iface: Int, child: Int, anim: Int) {
    player.packetDispatch.sendAnimationInterface(anim, iface, child)
}

/**
 * Adds a climb destination to the ladder handler.
 * @param ladderLoc the location of the ladder/stairs object you want to climb.
 * @param dest the destination for the climb.
 */
fun addClimbDest(ladderLoc: Location, dest: Location) {
    core.game.global.action.SpecialLadders.add(ladderLoc, dest)
}

/**
 * Sends a news announcement in game chat.
 * @param message the message to announce
 */
fun sendNews(message: String) {
    Repository.sendNews(message, 12, "CC6600")
}

/**
 * Sends a given Graphics object, or graphics ID, to the given location.
 * @param gfx the Graphics object, or the Integer ID of the graphics, to send. Either works.
 * @param location the location to send it to
 */
fun <G> sendGraphics(gfx: G, location: Location) {
    when (gfx) {
        is Int -> Graphics.send(Graphics(gfx), location)
        is Graphics -> Graphics.send(gfx, location)
    }
}

/**
 * Instructs the given player's client to execute a given CS2 script with the given arguments
 * @param player the Player
 * @param scriptId the ID of the CS2 script to execute
 * @param arguments the various arguments passed to the script. 
**/
fun runcs2 (player: Player, scriptId: Int, vararg arguments: Any) {
    var typeString = StringBuilder()
    var finalArgs = Array<Any?> (arguments.size) { null }
    try {
        for (i in 0 until arguments.size) {
            val arg = arguments[i]
            if (arg is Int)
                typeString.append("i")
            else if (arg is String)
                typeString.append("s")
            else
                throw IllegalArgumentException("Argument at index $i ($arg) is not an acceptable type! Only string and int are accepted.")
            finalArgs[arguments.size - i - 1] = arg
        }
        player.packetDispatch.sendRunScript(scriptId, typeString.toString(), *finalArgs)
    } catch (e: Exception) {
        e.printStackTrace()
    }
}

/**
 * Opens a generic item selection prompt with a glowing background, with your own callback to handle the selection.
 * @param player the player we are openinig the prompt for
 * @param options the right-click options the items should have
 * @param keepAlive whether or not the selection prompt should remain open for multiple interactions
 * @param callback a callback to handle the selection. The parameters passed to the callback are the slot in the inventory of the selected item, and the 0-9 index of the option clicked.
**/
@JvmOverloads
fun sendItemSelect(player: Player, vararg options: String, keepAlive: Boolean = false, callback: (slot: Int, optionIndex: Int) -> Unit) {
    player.interfaceManager.openSingleTab(Component(12))
    val scriptArgs = arrayOf ((12 shl 16) + 18, 93, 4, 7, 0, -1, "", "", "", "", "", "", "", "", "")
    for (i in 0 until min(9, options.size))
        scriptArgs[6 + i] = options[i]
    runcs2(player, 150, *scriptArgs)
    val settings = IfaceSettingsBuilder()
        .enableOptions(0 until 9)
        .build()
    player.packetDispatch.sendIfaceSettings(settings, 18, 12, 0, 28)
    setAttribute(player, "itemselect-callback", callback)
    setAttribute(player, "itemselect-keepalive", keepAlive)
}

fun announceIfRare(player: Player, item: Item) {
    if (item.definition.getConfiguration(ItemConfigParser.RARE_ITEM, false)) {
        sendNews("${player.username} has just received: ${item.amount} x ${item.name}.")
        PlayerStatsCounter.incrementRareDrop(player, item)
    }
}

fun hasRequirement(player: Player, req: QuestReq, message: Boolean = true) : Boolean {
    var (isMet, unmetReqs) = req.evaluate(player)
    val messageList = ArrayList<String>()

    var totalSoftQp = 0
    var totalHardQp = 0

    for (req in unmetReqs) {
        when (req) {
            is QPCumulative -> totalSoftQp += req.amount
            is QPReq -> if (req.amount > totalHardQp) totalHardQp = req.amount
        }
    }

    var neededQp = min(max(totalSoftQp, totalHardQp), player.questRepository.getAvailablePoints())

    isMet = isMet && neededQp <= player.questRepository.getPoints()

    if (isMet) return true
    
    if (unmetReqs.size == 2 && unmetReqs[0] is QuestReq) {
        messageList.add ("This requires completion of ${(unmetReqs[0] as QuestReq).questReq.quest} to access.")
    } else {
        messageList.add ("You need the pre-reqs for ${req.questReq.quest} to access this.")
        messageList.add ("Please check the page in your quest journal for more info.")
    }

    if (message)
        for (message in messageList) 
            sendMessage(player, message)

    return false
}

@JvmOverloads
fun hasRequirement(player: Player, quest: Quests, message: Boolean = true) : Boolean {
    val questReq = QuestRequirements.values().firstOrNull { it.quest == quest } ?: return false
    return hasRequirement(player, QuestReq(questReq), message)
}

/**
 * Generates a list of skill names which the player has mastered
 * @param player the player
 * @return a List<String> of skill names
 */
fun getMasteredSkillNames(player: Player): List<String> {
    val hasMastered = player.getSkills().masteredSkills > 0
    val masteredSkills = ArrayList<String>()

    if (hasMastered) {
        for ((skillId, skillName) in Skills.SKILL_NAME.withIndex()) {
            //phil you were looping every skill and performing a string comparison (another loop) for every one here.
            //that's some real shit code bro.
            //you were also performing a dynamic level check rather than a static.
            if (hasLevelStat(player, skillId, 99)) {
                masteredSkills.add(skillName)
            }
        }
    }
    return masteredSkills
}

/**
 * Empties the provided container into the player's bank.
 * @param player the player
 * @param container the container to be emptied.
 * @return The amount of items which were successfully transferred to the bank.
 *
 * @author ceik
 * @author James Triantafylos
 * @author vddCore
 */
fun dumpContainer(player: Player, container: core.game.container.Container): Int {
    val bank = player.bank
    var dumpedCount = 0

    run beginDepositing@{
        if (hasIronmanRestriction(player, IronmanMode.ULTIMATE)) {
            return@beginDepositing
        }

        container.toArray().filterNotNull().forEach { item ->
            if (!bank.hasSpaceFor(item)) {
                sendMessage(player, "You have no more space in your bank.")
                return@beginDepositing
            }

            if (!bank.canAdd(item)) {
                sendMessage(player, "A magical force prevents you from banking the ${item.name}.")
                return@forEach
            } else {
                if (container is EquipmentContainer) {
                    if (!InteractionListeners.run(item.id, player, item, false)) {
                        sendMessage(player, "A magical force prevents you from removing your ${item.name}.")
                        return@forEach
                    }
                    if (SlayerEquipmentFlags.isSlayerEq(item.id)) {
                        SlayerEquipmentFlags.updateFlags(player)
                    }
                }

                container.remove(item)
                bank.add(unnote(item), false)
                dumpedCount++
            }
        }
    }

    container.update()
    bank.update()

    return dumpedCount
}

/**
 * Attempts to empty the player's Beast of Burden's inventory
 * into the player's bank.
 *
 * @param player The player whose Beast of Burden's inventory to empty.
 *
 * @author vddCore
 */
fun dumpBeastOfBurden(player: Player) {
    val famMan = player.familiarManager

    if (!famMan.hasFamiliar()) {
        sendMessage(player, "You don't have a familiar.")
        return
    }

    if (famMan.familiar !is BurdenBeast) {
        sendMessage(player, "Your familiar is not a Beast of Burden.")
        return
    }

    val beast: BurdenBeast = (famMan.familiar as BurdenBeast)

    if (beast.container.isEmpty) {
        sendMessage(player, "Your familiar's inventory is empty.")
        return
    }

    val itemCount = beast.container.itemCount()
    val dumpedCount = dumpContainer(player, beast.container)


    when {
        dumpedCount == itemCount -> sendMessage(player, "Your familiar's inventory was deposited into your bank.")
        dumpedCount > 0 -> {
            val remainPhrase = when {
                (itemCount - dumpedCount == 1) -> "item remains"
                else -> "items remain"
            }

            sendMessage(player, "${itemCount - dumpedCount} $remainPhrase in your familiar's inventory.")
        }
    }
}

/**
 * Gets the player's familiar boost in the given skill
 *
 * @param player The player who owns the familiar.
 * @param skill The skill to check boost of.
 * @return The amount of skill boost gained from the player's familiar.
 *
 * @author bushtail
 */
fun getFamiliarBoost(player: Player, skill: Int): Int {
    return player.familiarManager.getBoost(skill)
}

/**
 * Converts an item into its noted representation.
 *
 * @param item The item to convert.
 * @return Noted form of the item or the item itself if it's already, or cannot be, noted.
 *
 * @author vddCore
 */
fun note(item: Item): Item {
    if (!item.definition.isUnnoted)
        return item

    if (item.definition.noteId < 0)
        return item

    return Item(item.definition.noteId, item.amount, item.charge)
}

/**
 * Converts a noted item into its unnoted representation.
 *
 * @param item The item to convert.
 * @return Unnoted form of the item, or the item itself if it's already unnoted.
 *
 * @author vddCore
 */
fun unnote(item: Item): Item {
    if (item.definition.isUnnoted)
        return item

    return Item(item.noteChange, item.amount, item.charge)
}

/**
 * Checks if the player has the Seal of Passage equipped or if it's present in the inventory.
 *
 * @param player The player to inspect for item's presence.
 * @return True if Seal of Passage present, false otherwise.
 */
fun hasSealOfPassage(player: Player): Boolean {
    return inEquipmentOrInventory(player, Items.SEAL_OF_PASSAGE_9083)
}

/**
 * Returns a boolean indicating if the player has a house.
 * @param player the player.
 * @return boolean indicating presence of house.
 */
fun hasHouse(player: Player): Boolean {
    return player.houseManager.hasHouse()
}

fun Player.getCutscene(): Cutscene? {
    return getAttribute<Cutscene?>(this, Cutscene.ATTRIBUTE_CUTSCENE, null)
}

/**
 * Stores a book in the POH storage
 * If this is the first time reading the book, store it in POH bookcase if eligible
 * @param player the player
 * @param itemID the ID of the book that was interacted with
 * @return True if the item was identified and processed (whether read or not)
 *         False if the node was not a book or did not have a bitIndex
 */
fun storeBookInHouse(player: Player, itemId: Int): Boolean {
    // find the corresponding Storable object
    val storable = StorageInterface.findStorableByDisplayId(itemId) ?: return false

    // check if the storable is tracked
    if (storable.bitIndex == -1) {
        return true
    }

    // retrieve bitfields where the values of already-read books are stored, and if the book has been read previously
    val bits = StorageInterface.BookcaseBitfields.get(player)
    val isRead = StorageInterface.BookcaseBitfields.isBitSet(bits, storable.bitIndex)

    // special check for games book. It isn't normally available and is only obtainable from the bookcase. You can't read it without first having it, and if you lose it you can't go back to somewhere else to get a new copy. Thus, it is always spawned in.
    if(itemId == Items.GAME_BOOK_7681) {
        val updatedBits = StorageInterface.BookcaseBitfields.setBit(bits, storable.bitIndex)
        val container = StorageInterface.getStorageContainer(player, StorableFamily.BOOKCASE)
        if (!isRead) {
            StorageInterface.BookcaseBitfields.set(player, updatedBits)
            container.addItem(itemId)
        } else {
            container.addItem(itemId)
        }

        return true
    }

    // if book is not read, store it in house
    if (!isRead) {
        // update bitfields
        val updatedBits = StorageInterface.BookcaseBitfields.setBit(bits, storable.bitIndex)
        StorageInterface.BookcaseBitfields.set(player, updatedBits)

        // add to the POH container
        val container = StorageInterface.getStorageContainer(player, StorableFamily.BOOKCASE)
        container.addItem(itemId)
    }
    return true
}

/**
 * Stores a book in the POH storage
 * If this is the first time reading the book, store it in POH bookcase if eligible
 * Used inside interaction listeners ("read") on nodes (books)
 * @param player the player
 * @param node the node (book) that was interacted with
 * @return True if the item was identified and processed (whether read or not)
 *         False if the node was not a book or did not have a bitIndex
 */
fun storeBookInHouse(player: Player, node: Node): Boolean {
    val bookId = (node as? Item)?.id ?: return false
    return storeBookInHouse(player, bookId)
}

fun Player.getCutsceneStage(): Int {
    return getAttribute(this, Cutscene.ATTRIBUTE_CUTSCENE_STAGE, 0)
}

fun getServerConfig(): Toml {
    return ServerConfigParser.tomlData ?: Toml()
}

fun getPathableRandomLocalCoordinate(target: Entity, radius: Int, center: Location, maxAttempts: Int = 3): Location {
    var maxRadius = Vector.deriveWithEqualComponents(ServerConstants.MAX_PATHFIND_DISTANCE.toDouble()).x - 1
    var effectiveRadius = min(radius, maxRadius.toInt())
    val swCorner = center.transform(-effectiveRadius, -effectiveRadius, center.z)
    val neCorner = center.transform(effectiveRadius, effectiveRadius, center.z)
    val borders = ZoneBorders(swCorner.x, swCorner.y, neCorner.x, neCorner.y, center.z)

    var attempts = maxAttempts
    var success: Boolean
    while (attempts-- > 0) {
        val dest = borders.randomLoc
        val path = Pathfinder.find(center, dest, target.size())
        success = path.isSuccessful && !path.isMoveNear
        if (success) return dest
    }

    return target.location
}

/**
 * Returns a pathable cardinal location in a 1 tile radius in order of west, south, east, north.
 * @param entity the entity used to check if the loc is pathable
 * @param center the center location
 */
fun getPathableCardinal(target: Entity, center: Location) : Location {
    var tiles = center.cardinalTiles

    for (tile in tiles) {
        val path = Pathfinder.find(center, tile, target.size())
        if (path.isSuccessful && !path.isMoveNear)
            return tile
    }

    return center
}

/**
 * Returns the player's active slayer task.
 * @author bushtail
 * @param player the player whose task we are checking.
 * @return the slayer task.
 */
fun getSlayerTask(player: Player): Tasks? {
    return SlayerManager.getInstance(player).activeTask
}

/**
 * Returns the name of the player's active slayer task.
 * @author bushtail
 * @param player the player whose task we are checking.
 * @return the name of the slayer task.
 */
fun getSlayerTaskName(player: Player): String {
    return SlayerManager.getInstance(player).taskName
}

/**
 * Returns the player's remaining kills for their active slayer task.
 * @author bushtail
 * @param player the player whose task we are checking.
 * @return the remaining kills of the slayer task.
 */
fun getSlayerTaskKillsRemaining(player: Player): Int {
    return SlayerManager.getInstance(player).amount
}

/**
 * Returns the player's slayer master as npc.
 * @author bushtail
 * @param player the player whose master we are checking.
 * @return the slayer master as NPC.
 */
fun getSlayerMaster(player: Player): NPC {
    return findNPC(SlayerManager.getInstance(player).master?.npc as Int) as NPC
}

/**
 * Returns the player's slayer master location as string.
 * @author bushtail
 * @param player the player whose master we are checking.
 * @return the slayer master location as String.
 */
fun getSlayerMasterLocation(player: Player): String {
    return when (getSlayerMaster(player).id) {
        NPCs.CHAELDAR_1598 -> "Zanaris"
        NPCs.DURADEL_8275 -> "Shilo Village"
        NPCs.MAZCHNA_8274 -> "Canifis"
        NPCs.TURAEL_8273 -> "Taverley"
        NPCs.VANNAKA_1597 -> "Edgeville Dungeon"
        else -> "The Backrooms"
    }
}

/**
 * Returns the player's slayer task tip as String.
 * @author bushtail
 * @param player the player whose task tip we are checking.
 * @return the task tip as String.
 */
fun getSlayerTip(player: Player): Array<out String> {
    return if (hasSlayerTask(player)) {
        getSlayerTask(player)?.tip!!
    } else {
        arrayOf("You need something new to hunt.")
    }
}

/**
 * Returns the player's slayer task flags as Int.
 * @author bushtail
 * @param player the player whose task flags we are checking.
 * @return the task flags as Int.
 */
fun getSlayerTaskFlags(player: Player): Int {
    return getSlayerFlags(player).taskFlags
}

fun getSlayerFlags(player: Player): SlayerFlags {
    return SlayerManager.getInstance(player).flags
}

/**
 * Returns whether or not the player has a task.
 * @author bushtail
 * @param player the player whose task we are checking.
 * @return has task as Boolean.
 */
fun hasSlayerTask(player: Player): Boolean {
    return SlayerManager.getInstance(player).hasTask()
}

/**
 * Checks whether a player plays in a specific Ironman mode.
 *
 * @param player Player whose Ironman status to check.
 * @param restriction The Ironman restriction level to check the player against.
 * @return Whether the player is restricted to the provided Ironman mode.
 */
fun hasIronmanRestriction(player: Player, restriction: IronmanMode): Boolean {
    return player.ironmanManager.isIronman
        && player.ironmanManager.mode.ordinal >= restriction.ordinal
}

/**
 * Conditionally executes an action based on the player's Ironman status.
 *
 * @param player Player whose action to restrict.
 * @param restriction Ironman mode that will be used as the restriction criterion.
 * @param action The action to be restricted.
 */
fun restrictForIronman(player: Player, restriction: IronmanMode, action: () -> Unit) {
    if (!player.ironmanManager.checkRestriction(restriction)) {
        action()
    }
}

/**
 * Opens the given player's Grand Exchange interface.
 *
 * @author vddCore
 * @param player The player whose Grand Exchange interface to open.
 */
fun openGrandExchange(player: Player) {
    restrictForIronman(player, IronmanMode.ULTIMATE) {
        StockMarket.openFor(player)
    }
}

/**
 * Checks if the player has any items to collect from the Grand Exchange
 *
 * @author vddCore
 * @param player The player whose Grand Exchange collection box to inspect.
 */
fun hasAwaitingGrandExchangeCollections(player: Player): Boolean {
    val records = GrandExchangeRecords.getInstance(player)

    for (record in records.offerRecords) {
        val offer = records.getOffer(record)

        return offer != null
            && offer.withdraw[0] != null
    }

    return false
}

/**
 * Opens the given player's Grand Exchange collection box.
 *
 * It will send a prohibition message to Ultimate Ironmen.
 *
 * @author vddCore
 * @param player The player whose collection box to open.
 */
fun openGrandExchangeCollectionBox(player: Player) {
    restrictForIronman(player, IronmanMode.ULTIMATE) {
        GrandExchangeRecords.getInstance(player).openCollectionBox()
    }
}

/**
 * Opens the given player's bank account. If the player has a PIN set,
 * the PIN interface will be shown first.
 *
 * It will send a prohibition message to Ultimate Ironmen.
 *
 * @author vddCore
 * @param player The player whose bank account to open.
 */
fun openBankAccount(player: Player) {
    restrictForIronman(player, IronmanMode.ULTIMATE) {
        player.bank.open()
    }
}

/**
 * Opens the given player's bank deposit box interface.
 *
 * It will send a prohibition message to Ultimate Ironmen.
 *
 * @author vddCore
 * @param player The player whose bank account to open.
 */
fun openDepositBox(player: Player) {
    restrictForIronman(player, IronmanMode.ULTIMATE) {
        player.bank.openDepositBox()
    }
}

/**
 * Opens the given player's bank PIN settings interface.
 *
 * It will send a prohibition message to Ultimate Ironmen.
 *
 * @author vddCore
 * @param player The player whose PIN settings to open.
 */
fun openBankPinSettings(player: Player) {
    restrictForIronman(player, IronmanMode.ULTIMATE) {
        player.bankPinManager.openSettings()
    }
}

enum class SecondaryBankAccountActivationResult {
    SUCCESS,
    ALREADY_ACTIVE,
    NOT_ENOUGH_MONEY,
    INTERNAL_FAILURE
}
/**
 * Activates the secondary bank account and handles all the fees required.
 *
 * @author vddCore
 * @param player The player whose secondary bank account to activate.
 * @returns Whether the operation was successful or not.
 */
fun activateSecondaryBankAccount(player: Player): SecondaryBankAccountActivationResult {
    if (hasIronmanRestriction(player, IronmanMode.ULTIMATE)) {
        return SecondaryBankAccountActivationResult.INTERNAL_FAILURE
    }

    if (hasActivatedSecondaryBankAccount(player)) {
        return SecondaryBankAccountActivationResult.ALREADY_ACTIVE
    }

    val cost = 5000000
    val coinsInInventory = amountInInventory(player, Items.COINS_995)
    val coinsInBank = amountInBank(player, Items.COINS_995)
    val coinsTotal = coinsInInventory + coinsInBank

    if (cost > coinsTotal) {
        return SecondaryBankAccountActivationResult.NOT_ENOUGH_MONEY
    }

    val operationResult = if (cost > coinsInInventory) {
        val amountToTakeFromBank = cost - coinsInInventory

        removeItem(player, Item(Items.COINS_995, coinsInInventory), Container.INVENTORY)
                && removeItem(player, Item(Items.COINS_995, amountToTakeFromBank), Container.BANK)
    } else {
        removeItem(player, Item(Items.COINS_995, cost))
    }

    return if (operationResult) {
        setAttribute(player, "/save:UnlockedSecondaryBank", true)
        SecondaryBankAccountActivationResult.SUCCESS
    } else {
        sendMessage(player, "$cost;$coinsInInventory;$coinsInBank;$coinsTotal")
        SecondaryBankAccountActivationResult.INTERNAL_FAILURE
    }
}

/**
 * Checks if the player has unlocked their secondary bank account.
 *
 * @author vddCore
 * @param player The player whose secondary bank activation status to inspect.
 * @return Secondary bank activation status.
 */
fun hasActivatedSecondaryBankAccount(player: Player): Boolean {
    return getAttribute(player, "UnlockedSecondaryBank", false)
}

/**
 * Toggles between the player's primary and/or secondary bank account.
 * Has no effect if the secondary bank account hasn't been activated.
 *
 * It will send a prohibition message to Ultimate Ironmen.
 *
 * @author vddCore
 * @param player The player whose bank accounts to toggle.
 */
fun toggleBankAccount(player: Player) {
    restrictForIronman(player, IronmanMode.ULTIMATE) {
        if (!hasActivatedSecondaryBankAccount(player)) {
            return@restrictForIronman
        }

        player.useSecondaryBank = !player.useSecondaryBank
    }
}

/**
 * Checks if a player is currentl using their secondary bank account.
 *
 * @author vddCore
 * @param player The player whose bank account toggle state to inspect.
 */
fun isUsingSecondaryBankAccount(player: Player): Boolean {
    return player.useSecondaryBank
}

/**
 * Returns 'primary' or 'secondary' strings based on which
 * bank account is activated for the given user.
 *
 * @author vddCore
 * @param player The player whose bank account name to retrieve.
 * @param invert Whether to invert the return value.
 * @return Bank account name according to the given criteria.
 */
fun getBankAccountName(player: Player, invert: Boolean = false): String {
    return if (isUsingSecondaryBankAccount(player)) {
        if (invert) "primary" else "secondary"
    } else {
        if (invert) "secondary" else "primary"
    }
}

/**
 * Opens a shop for the given NPC in the provided player's context.
 *
 * @author vddCore
 * @param player The player serving as the shop context.
 * @param npc The NPC ID whose shop to open.
 */
fun openNpcShop(player: Player, npc: Int): Boolean {
    val shop = Shops.shopsByNpc[npc]

    if (shop != null) {
        shop.openFor(player)
        return true
    }

    return false
}

/**
 * Skill Dialogue builder.
 * @param player the player to send the dialogue for.
 * Use like:
 * sendSkillDialogue(player) {
 *    withItems(Items.EXAMPLE_0, Items.EXAMPLE_1)
 *    create { id, amount ->
 *       doSomethingWith(id, amount)
 *    }
 *    calculateMaxAmount { id ->
 *       return someAmount
 *    }
 * }
 */
fun sendSkillDialogue(player: Player, init: SkillDialogueBuilder.() -> Unit) {
    val builder = SkillDialogueBuilder()
    builder.player = player
    builder.init()

    if (builder.items.size !in 1..5) {
        throw IllegalStateException("Invalid number of items passed to skill dialogue (min 1, max 5): ${builder.items.size}")
    }

    val type = when (builder.items.size) {
        1 -> SkillDialogueHandler.SkillDialogue.ONE_OPTION
        2 -> SkillDialogueHandler.SkillDialogue.TWO_OPTION
        3 -> SkillDialogueHandler.SkillDialogue.THREE_OPTION
        4 -> SkillDialogueHandler.SkillDialogue.FOUR_OPTION
        5 -> SkillDialogueHandler.SkillDialogue.FIVE_OPTION
        else -> null
    }

    object : SkillDialogueHandler(player, type, *builder.items) {
        override fun create(amount: Int, index: Int) {
            builder.creationCallback(builder.items[index].id, amount)
        }

        override fun getAll(index: Int): Int {
            return builder.totalAmountCallback(builder.items[index].id)
        }
    }.open()
}

class SkillDialogueBuilder {
    internal lateinit var player: Player
    internal var items: Array<Item> = arrayOf<Item>()
    internal var creationCallback: (itemId: Int, amount: Int) -> Unit = {_,_ -> }
    internal var totalAmountCallback: (itemId: Int) -> Int = {id -> amountInInventory(player, id)}

    fun withItems(vararg item: Item) {
        items = arrayOf(*item)
    }

    fun withItems(vararg item: Int) {
        items = item.map { Item(it) }.toTypedArray()
    }

    fun create(method: (itemId: Int, amount: Int) -> Unit) {
        creationCallback = method
    }

    fun calculateMaxAmount(method: (itemId: Int) -> Int) {
        totalAmountCallback = method
    }
}

/**
 * Registers a hint icon with the given height at the given location
 * @param player the player to register the hint icon for
 * @param height the height of the hint icon
 * @param location the location of the hint icon
 */
fun registerHintIcon(player: Player, location: Location, height: Int) {
    setAttribute(player, "hinticon", HintIconManager.registerHintIcon(player, location, 1, -1, player.hintIconManager.freeSlot(), height, 3))
}

/**
 * Registers a hint icon for the given Node.
 * @param player the player to show a hint icon to.
 * @param node the Node to register a hint icon for.
 */
fun registerHintIcon(player: Player, node: Node) {
    if (getAttribute(player, "hinticon", null) != null)
        return
    setAttribute(player, "hinticon", HintIconManager.registerHintIcon(player, node))
}

/**
 * Clears the active ContentAPI-originated hint icon
 * @param player the player to clear the active hint icon for
 */
fun clearHintIcon(player: Player) {
    val slot = getAttribute(player, "hinticon", -1)
    player.removeAttribute("hinticon")
    HintIconManager.removeHintIcon(player, slot)
}

/**
 * Gets the equipment slot the item belongs to
 * @param item the Id of the item to check
 * @return the EquipmentSlot, or null if the item cannot be equipped, or has no slot defined.
 */
fun equipSlot(item: Int) : EquipmentSlot? {
    return EquipmentSlot
        .values()
        .getOrNull(itemDefinition(item).getConfiguration(ItemConfigParser.EQUIP_SLOT, -1))
}

/**
 * Adjusts the user's credits by the given amount
 * @param player the player whose credits to adjust
 * @param amt the amount to adjust by - add with positive, remove with negative.
 * @return true if successful. Success is defined as always true when adding, but can be false if we are trying to remove, and it would put the amount below 0.
 */
fun updateCredits(player: Player, amount: Int) : Boolean {
    val creds = getCredits(player) + amount

    if (creds < 0)
        return false
    else
        player.details.accountInfo.credits = creds

    return true
}

/**
 * Gets the number of credits a user has.
 * @param player the player to check
 * @return the number of credits they have
 */
fun getCredits(player: Player) : Int {
    return player.details.accountInfo.credits
}

/**
 * Asserts that a quest is required, and sends the player "You must have completed the $quest quest $message"
 * @param player the player we are checking
 * @param quest the quest we are checking for
 * @param message the text appended to "You must have completed the $quest quest ..." if the quest is not complete.
 * @return whether or not the quest has been completed
 */
fun requireQuest(player: Player, quest: Quests, message: String) : Boolean {
    if (!isQuestComplete(player, quest)) {
        sendMessage(player, "You must have completed the $quest quest $message")
        return false
    }
    return true
}


/**
 * Determines whether or not specified node is a player
 * @param entity the node whom we are checking
 * @return whether or not the entity is a player
 */
fun isPlayer(node: Node) : Boolean {
    return (node is Player)
}

/**
 * Adds a dialogue action to the player's dialogue interpreter
 * @param player the player to add the dialogue action to
 * @param action the dialogue action to add
 */
fun addDialogueAction(player: Player, action: core.game.dialogue.DialogueAction) {
    player.dialogueInterpreter.addAction(action)
}

/**
 * Logs a message to the server console
 * @param origin simply put (Kotlin) this::class.java or (Java) this.getClass()
 * @param type the type of log: Log.FINE (default, visible on VERBOSE), Log.INFO (visible on DETAILED), Log.WARN (visible on CAUTIOUS), Log.ERR (always visible)
 * @param message the actual message to log.
 */
fun log(origin: Class<*>, type: Log, message: String) {
    SystemLogger.processLogEntry(origin, type, message)
}

/**
 * Logs a message to the server console along with a stack trace leading up to it.
 * @param origin simply put (Kotlin) this::class.java or (Java) this.getClass()
 * @param type the type of log: Log.FINE (default, visible on VERBOSE), Log.INFO (visible on DETAILED), Log.WARN (visible on CAUTIOUS), Log.ERR (always visible)
 * @param message the actual message to log.
*/
fun logWithStack(origin: Class<*>, type: Log, message: String) {
    try {
        throw Exception(message)
    } catch (e: Exception) {
        log(origin, type, "${exceptionToString(e)}")
    }
}

/**
 * Takes an exception as an argument, and sends back the complete exception with stack trace as a standard string. Useful for various things.
 * @param e The exception you wish to convert.
 * @return a string with the full stack trace of the exception
**/
fun exceptionToString (e: Exception) : String {
    val sw = StringWriter()
    val pw = PrintWriter(sw)
    e.printStackTrace(pw)
    return sw.toString()
}

/**
 * Used by content handlers to check if the entity is done moving yet
 */
fun finishedMoving(entity: Entity) : Boolean {
    return entity.clocks[Clocks.MOVEMENT] < GameWorld.ticks
}


/**
 * Delay the execution of the currently running script
 */
fun delayScript(entity: Entity, ticks: Int): Boolean {
    entity.scripts.getActiveScript()?.let { it.nextExecution = GameWorld.ticks + ticks }
    return false
}

/**
 * Set the global delay for the entity, pausing execution of all queues/scripts until passed.
 */
fun delayEntity(entity: Entity, ticks: Int) {
    entity.scripts.delay = GameWorld.ticks + ticks
    lock(entity, ticks) //TODO: REMOVE WHEN EVERYTHING IMPORTANT USES PROPER QUEUES - THIS IS INCORRECT BEHAVIOR
}

fun apRange(entity: Entity, apRange: Int) {
    entity.scripts.apRange = apRange
    entity.scripts.apRangeCalled = true
}

fun hasLineOfSight(entity: Entity, target: Node) : Boolean {
    return CombatSwingHandler.isProjectileClipped (entity, target, false)
}

fun animationFinished(entity: Entity) : Boolean {
    return entity.clocks[Clocks.ANIMATION_END] < GameWorld.ticks
}

fun clearScripts(entity: Entity) : Boolean {
    entity.scripts.reset()
    return true
}

fun restartScript(entity: Entity) : Boolean {
    if (entity.scripts.getActiveScript()?.persist != true) {
        log(entity.scripts.getActiveScript()!!::class.java, Log.ERR, "Tried to call restartScript on a non-persistent script! Either use stopExecuting() or make the script persistent.")
        return clearScripts(entity)
    }
    return true
}

fun keepRunning(entity: Entity) : Boolean {
    entity.scripts.getActiveScript()?.nextExecution = getWorldTicks() + 1
    return false
}

fun stopExecuting(entity: Entity) : Boolean {
    if (entity.scripts.getActiveScript()?.persist == true) {
        log(entity.scripts.getActiveScript()!!::class.java, Log.ERR, "Tried to call stopExecuting() on a persistent script! To halt execution of a persistent script, you MUST call clearScripts()!")
        return clearScripts(entity)
    }
    return true
}

fun queueScript(entity: Entity, delay: Int = 1, strength: QueueStrength = QueueStrength.WEAK, persist: Boolean = false, script: (stage: Int) -> Boolean) {
    val s = QueuedScript(script, strength, persist)
    s.nextExecution = getWorldTicks() + delay
    entity.scripts.addToQueue(s, strength)
}

/**
 * Sets the clock to the value of WORLD_TICKS + ticks.
 * @param entity the entity whose clock we are updating
 * @param clock the clock we are updating. Please use [core.game.interaction.Clocks] for this argument. 
 * @param ticks the number of ticks to delay by
 * @param delayScript whether to delay the script by the number of ticks passed in.
 * @return always returns false so this can be used as a script return value.
**/
fun delayClock(entity: Entity, clock: Int, ticks: Int, delayScript: Boolean = false) : Boolean {
    entity.clocks[clock] = getWorldTicks() + ticks
    if (delayScript) {
        return delayScript(entity, ticks)
    }
    return false
}

/**
 * Checks if a clock is ready (have we elapsed any delay put into it)
 * @param entity the entity whose clock we are checking
 * @param clock the clock we are checking. Please use [core.game.interaction.Clocks] for this argument.
 * @return true if we have elapsed the clock's wait
**/
fun clockReady(entity: Entity, clock: Int) : Boolean {
    return entity.clocks[clock] <= getWorldTicks()
}

fun delayAttack(entity: Entity, ticks: Int) {
    entity.properties.combatPulse.delayNextAttack(3)
    entity.clocks[Clocks.NEXT_ATTACK] = getWorldTicks() + ticks
}

fun stun(entity: Entity, ticks: Int) {
    stun(entity, ticks, true)
}

fun stun(entity: Entity, ticks: Int, sendMessage: Boolean) {
    entity.walkingQueue.reset()
    entity.pulseManager.clear()
    entity.locks.lockMovement(ticks)
    entity.clocks[Clocks.STUN] = getWorldTicks() + ticks
    entity.graphics(Graphics(80, 96))
    if (entity is Player) {
        playAudio(entity.asPlayer(), Sounds.STUNNED_2727)
        entity.animate(Animation(424, Animator.Priority.VERY_HIGH))
        if (sendMessage) {
            sendMessage(entity, "You have been stunned!")
        }
    }
}

fun isStunned(entity: Entity) : Boolean {
    return entity.clocks[Clocks.STUN] >= getWorldTicks()
}

/**
 * Applies poison to the target. (In other words, creates and starts a poison timer.)
 * @param entity the entity who will be receiving the poison damage.
 * @param source the entity to whom credit for the damage should be awarded (the attacker.) You should award credit to the victim if the poison is sourceless (e.g. from a trap or plant or something)
 * @param severity the severity of the poison damage. Severity is not a 1:1 representation of damage, rather the formula `floor((severity + 4) / 5)` is used. Severity is decreased by 1 with each application of the poison, and ends when it reaches 0.
 * @see To those whe ask "why severity instead of plain damage?" to which the answer is: severity is how it works authentically, and allows for scenarios where, e.g. a poison should hit 6 once, and then drop to 5 immediately.
**/
fun applyPoison (entity: Entity, source: Entity, severity: Int) {
    if(hasTimerActive<PoisonImmunity>(entity)) {
        return
    }
    if(entity.isPoisonImmune()) {
        return
    }
    val existingTimer = getTimer<Poison>(entity)

    if (existingTimer != null) {
        if (existingTimer.severity > severity) {
            return
        } else {
            existingTimer.severity = severity
            existingTimer.damageSource = source
        }
    } else {
        registerTimer(entity, spawnTimer<Poison>(source, severity))
    }
}

fun isPoisoned (entity: Entity) : Boolean {
    return getTimer<Poison>(entity) != null
}

fun curePoison (entity: Entity) {
    if (!hasTimerActive<Poison>(entity))
        return
    removeTimer<Poison>(entity)
    if (entity is Player)
        sendMessage(entity, "Your poison has been cured.")
}

fun setCurrentScriptState(entity: Entity, state: Int) {
    val script = entity.scripts.getActiveScript()
    if (script == null) {
        log(ContentAPI::class.java, Log.WARN, "Tried to set a script state when no script was being ran!")
        if (GameWorld.settings?.isDevMode != true) return
        throw IllegalStateException("Script execution mistake - check stack trace and above warning log!")
    }
    script.state = state - 1 //set it to 1 below the state so that on next execution the state is at the expected value.
}

/**
 * Modifies prayer points by value
 * @param player the player to modify prayer points
 * @param amount the amount of points to modify by (positive for increment, negative for decrement)
 */
fun modPrayerPoints(player: Player, amount: Double) {
    if(amount > 0) player.skills.incrementPrayerPoints(amount)
    else if(amount < 0) player.skills.decrementPrayerPoints(amount.absoluteValue)
    else return
}

/**
 * Iterates a container and "decants" every potion type in the container. (Decanting is the process of condensing multiple variable-dose potions into the minimum number of max-dose potions)
 * @param container the container to iterate
 * @return a pair where the first element is the list of items to be removed, and the second element is the list of items to add.
*/
fun decantContainer (container: core.game.container.Container) : Pair<List<Item>, List<Item>> {
    val doseCount = HashMap<Consumable,Int>()
    val toRemove = ArrayList<Item>()
    val toAdd = ArrayList<Item>()
    val doseRegex = Pattern.compile("(\\([0-9]\\))") //Matches "(2)" in "Defense potion (2)"

    for (item in container.toArray()) {
        if (item == null) continue
        val potionType = Consumables.getConsumableById(item.id)?.consumable as? Potion ?: continue
        val matcher = doseRegex.matcher(item.name)
        if (!matcher.find()) continue
        val dosage = matcher.group(1).replace("(","").replace(")","").toIntOrNull() ?: continue
        doseCount[potionType] = (doseCount[potionType] ?: 0) + dosage
        toRemove.add(item)
    }

    for ((consumable, count) in doseCount) {
        val maxDose = consumable.ids.size
        val totalMaxDosePotions = count / maxDose
        val remainderDose = count % maxDose
        if (totalMaxDosePotions > 0)
            toAdd.add(Item(consumable.ids[0], totalMaxDosePotions))
        if (remainderDose > 0)
            toAdd.add(Item(consumable.ids[consumable.ids.size - remainderDose]))
    }

    val emptyVials = toRemove.size - toAdd.sumBy { it.amount }
    if (emptyVials > 0)
        toAdd.add(Item(229, emptyVials))
    return Pair<List<Item>,List<Item>>(toRemove, toAdd)
}

/** 
 * Checks whether the user has a valid anti-dragonfire shield equipped.
 * @param player the player whose shield we are checking.
 * @param wyvern an optional parameter (default false) whether or not we are checking against wyvern fire, which ignores normal anti-dragon-shields, but accepts elemental/mind shields, etc.
 * @return whether or not the player is protected by their shield. 
 * @see <a href=https://runescape.wiki/w/Dragonfire?oldid=2068032>Source</a> 
**/
fun hasDragonfireShieldProtection(player: Player, wyvern: Boolean = false): Boolean {
    val shield = getItemFromEquipment(player, EquipmentSlot.SHIELD) ?: return false
    return when (shield.id) {
        Items.ELEMENTAL_SHIELD_2890, Items.MIND_SHIELD_9731 -> wyvern
        Items.ANTI_DRAGON_SHIELD_1540 -> !wyvern
        Items.DRAGONFIRE_SHIELD_11283, Items.DRAGONFIRE_SHIELD_11284 -> true
        else -> false
    }
}

/**
 * Calculates the expected max hit of dragonfire after checking/applying valid protections.
 * @param entity the entity whose protection we are checking.
 * @param maxDamage the max damage the dragonfire can do without protection.
 * @param wyvern an optional parameter (default false) that defines whether or not this is wyvern fire. Wyvern fire ignores anti-fire potions and regular anti-dragon shield.
 * @param unprotectableDamage an optional parameter (default 0) that defines how much damage cannot be protected against. Think of it as a minimum-max-hit.
 * @param sendMessage an optional parameter (default false) whether or not to send a message notifying the player of the damage being blocked.
 * @return the maximum amount of damage that can be done to the player.
 * @see <a href=https://runescape.wiki/w/Dragonfire?oldid=2068032>Source</a> 
**/
fun calculateDragonfireMaxHit(entity: Entity, maxDamage: Int, wyvern: Boolean = false, unprotectableDamage: Int = 0, sendMessage: Boolean = false): Int {
    val hasShield: Boolean
    val hasPotion: Boolean
    val hasPrayer: Boolean

    if (entity is Player) {
        hasShield = hasDragonfireShieldProtection(entity, wyvern)
        hasPotion = !wyvern && hasTimerActive<DragonFireImmunity>(entity)
        hasPrayer = entity.prayer.get(PrayerType.PROTECT_FROM_MAGIC)

        if (sendMessage) {
            var message = "You are horribly burnt by the ${if (wyvern) "icy" else "fiery"} breath."
            if (hasShield && hasPotion)
                message = "Your potion and shield fully absorb the ${if (wyvern) "icy" else "fiery"} breath."
            else if (hasShield)
                message = "Your shield absorbs most of the ${if (wyvern) "icy" else "fiery"} breath."
            else if (hasPotion)
                message = "Your potion absorbs some of the fiery breath."
            else if (hasPrayer)
                message = "Your prayer absorbs some of the ${if (wyvern) "icy" else "fiery"} breath."
            sendMessage(entity, message)
        }
    } else {
        val dragonfireTokens = entity.getDragonfireProtection(!wyvern)
        hasShield = dragonfireTokens and 0x2 != 0
        hasPotion = dragonfireTokens and 0x4 != 0
        hasPrayer = dragonfireTokens and 0x8 != 0
    }

    var effectiveDamage = maxDamage.toDouble()
    if (hasPrayer && !hasShield && !hasPotion)
        effectiveDamage -= 0.6 * maxDamage
    else {
        if (hasShield)
            effectiveDamage -= 0.9 * maxDamage
        if (hasPotion)
            effectiveDamage -= 0.1 * maxDamage
    }

    return Math.max(unprotectableDamage, effectiveDamage.toInt())
}

/**
 * Opens a single interface tab from given ID.
 * @param player the player to open interface for
 * @param component the component ID to open
 */
fun openSingleTab(player: Player, component: Int) {
    player.interfaceManager.openSingleTab(Component(component))
}

/**
 * Finish an achievement diary task for the player.
 * @param player the player who just finished the task.
 * @param diary the diary the task belongs to.
 * @param level the level of the task.
 * @param task The task that was just finished.
 */
fun finishTask(player: Player, diary: DiaryType, level: DiaryLevel, task: Int) {
    player.achievementDiaryManager.finishTask(player, diary, getDiaryLevelIndex(diary, level), task)
}

/**
 * Resolves a [DiaryLevel] to an index for a specific diary.
 * @param diary The diary the task belongs to.
 * @param level The task level in the diary.
 * @throws IllegalArgumentException if the level is not present in the diary.
 * @return the index of the level that can be used with [core.game.node.entity.player.link.diary.AchievementDiaryManager.finishTask].
 */
fun getDiaryLevelIndex(diary: DiaryType, level: DiaryLevel): Int {
    val levelName = level.name.lowercase().replaceFirstChar { c -> c.uppercase() }
    val levelIndex = diary.levelNames.indexOf(levelName)

    if (levelIndex < 0) {
        throw IllegalArgumentException("'$levelName' was not found in diary '$diary'.")
    }

    return levelIndex
}

private class ContentAPI
