local TABAS_Utils = {}

local TABAS_Sprites = require("TABAS_Sprites")
local TABAS_Compat = require("TABAS_Compat")

 ----------------- For Objects -----------------

function TABAS_Utils.getBathSprites(specific)
    local sprites = {}
    for k, v in pairs(TABAS_Sprites.Bathtub) do
        for j, dir in pairs(v) do
            if dir and dir.faucet then
                if specific then
                    table.insert(sprites, dir[specific])
                else
                    table.insert(sprites, dir.faucet)
                    table.insert(sprites, dir.tub)
                end
            end
        end
    end
    return sprites
end

function TABAS_Utils.getShowerSprites()
    local sprites = {}
    for k, v in pairs(TABAS_Sprites.Shower) do
        for j, sprite in pairs(v) do
            if sprite ~= nil then
                table.insert(sprites, sprite)
            end
        end
    end
    return sprites
end

local function findObjectOnSquare(_square, targetSprites)
    for i=1, _square:getObjects():size() do
        local object = _square:getObjects():get(i-1)
        if object:getSprite() and object:getSpriteName() then
            for j=1, #targetSprites do
                if object:getSpriteName() == targetSprites[j] then
                    return object
                end
            end
        end
    end
end

function TABAS_Utils.getBathingObjectFromWorldObjects(worldObjects, targetSprites)
    if not worldObjects then return end
    local square
    local object
    for i=1, #worldObjects do
        if worldObjects[i] and worldObjects[i]:getSquare() then
            square = worldObjects[i]:getSquare()
            object = findObjectOnSquare(square, targetSprites)
            if object then
                return object
            end
        end
    end
end

function TABAS_Utils.getObjectFacing(object)
    local props = object:getSprite():getProperties()
    local facing = props:Is("Facing") and props:Val("Facing") or nil
    if not facing then
        facing = object:getModData().facing
    end
    return facing
end

function TABAS_Utils.getFullyBathObject(object)
    local linkedObj = TABAS_Utils.getGridExtensionBath(object)
    if TABAS_Utils.isBathFaucet(object) then
        return object, linkedObj
    else
        return linkedObj, object
    end
end

function TABAS_Utils.getBathObjectOnSquare(_square, _facing)
    for i=1,_square:getObjects():size() do
        local object = _square:getObjects():get(i-1)
        if TABAS_Utils.isBathObject(object) then
            local facing = TABAS_Utils.getObjectFacing(object)
            if facing == _facing then
                return object
            end
        end
    end
    return nil
end

function TABAS_Utils.isBathObject(object)
    if not object then return false end
    if not instanceof(object, "IsoObject") or not object:getSprite() then return false end
    local sprite = object:getSpriteName()
    for _, v in pairs(TABAS_Sprites.Bathtub) do
        for _, dir in pairs(v) do
            if sprite == dir.faucet or sprite == dir.tub then
                return true
            end
        end
    end
    return false
end

function TABAS_Utils.getGridExtensionBath(object, _facing)
    if not TABAS_Utils.isBathObject(object) then return end
    local props = object:getSprite():getProperties()
    local facing = _facing or props:Is("Facing") and props:Val("Facing")
    local isExtension = props:Is("IsGridExtensionTile")
    local dir
    if facing == "S" then
        if isExtension then dir = IsoDirections.N else dir = IsoDirections.S end
    elseif facing == "E" then
        if isExtension then dir = IsoDirections.W else dir = IsoDirections.E end
    elseif facing == "W" then
        if isExtension then dir = IsoDirections.E else dir = IsoDirections.W end
    elseif facing == "N" then
        if isExtension then dir = IsoDirections.S else dir = IsoDirections.N end
    end
    local square = object:getSquare():getAdjacentSquare(dir)
    for i=1, square:getObjects():size() do
        local innerObject = square:getObjects():get(i-1)
        if TABAS_Utils.isBathObject(innerObject) then
            return innerObject, square
        end
    end
    return nil, nil
end

function TABAS_Utils.isBathFaucet(object)
    if not object:getSprite() or not object:getSpriteName() then return false end
    local spriteName = object:getSpriteName()
    local facing = TABAS_Utils.getObjectFacing(object)
    local spriteDir = "sprite" .. facing
    for _, v in pairs(TABAS_Sprites.Bathtub) do
        if v[spriteDir] then
            local faucetSprite = v[spriteDir].faucet
            if spriteName == faucetSprite then
                return true
            end
        end
    end
    return false
end

function TABAS_Utils.getBathOrShower(object)
    local isBathObj = TABAS_Utils.isBathObject(object)
    local square = object:getSquare()
    local targetSprites
    if isBathObj then
        targetSprites = TABAS_Utils.getShowerSprites()
    else
        targetSprites = TABAS_Utils.getBathSprites("faucet")
    end
    return findObjectOnSquare(square, targetSprites)
end

function TABAS_Utils.canHot(object)
    if not SandboxVars.TakeABathAndShower.WaterTemperatureConcept then
        return true
    end
    local square = object:getSquare()
    local roomID = square:getRoomID() or 1
    if roomID > 1 then
        return getWorld():isHydroPowerOn() or square:haveElectricity()
    else
        return square:haveElectricity()
    end
end

function TABAS_Utils.removeTileObject(square, object)
    if not object then return end
    square:RemoveTileObject(object)
    -- triggerEvent("OnObjectAboutToBeRemoved", object)
    if isClient() then square:transmitRemoveItemFromSquare(object) end
    if isServer() then square:transmitRemoveItemFromSquareOnClients(object) end
end


 ----------------- For Tub Fluid Conatainer -----------------

function TABAS_Utils.isTfcObject(object)
    if not instanceof(object, "IsoObject") or not object:getName() then return false end
    return object:getName() == "TubFluidContainer"
end

function TABAS_Utils.getTfcBase(x, y, z, facing)
    local TFC_Bathtub = require("TABAS_TfcBathtub")
    local tfcBath = TFC_Bathtub:new(x, y, z, facing)
    if tfcBath then
        return tfcBath
    end
    return nil
end

function TABAS_Utils.getTfcBaseOnBathObject(object)
    if not TABAS_Utils.isBathObject(object) then return nil end
    local square = object:getSquare()
    local facing = TABAS_Utils.getObjectFacing(object)
    local TFC_Base = TABAS_Utils.getTfcBase(square:getX(), square:getY(), square:getZ(), facing)
    if not TFC_Base then
        object = TABAS_Utils.getGridExtensionBath(object, facing)
        if object then
            square = object:getSquare()
            TFC_Base = TABAS_Utils.getTfcBase(square:getX(), square:getY(), square:getZ(), facing)
        end
    end
    return TFC_Base
end

 function TABAS_Utils.getTfcObjectOnSquare(_square, _facing)
    for i=1,_square:getObjects():size() do
        local object = _square:getObjects():get(i-1)
        if TABAS_Utils.isTfcObject(object) then
            if _facing then
                local facing = object:getModData().facing or nil
                if facing and facing == _facing then
                    return object
                end
            else
                return object
            end
        end
    end
    return nil
end

function TABAS_Utils.getTfcObjectOnBathObject(bathObject)
    if not bathObject then return nil end
    local square = bathObject:getSquare()
    local facing = TABAS_Utils.getObjectFacing(bathObject)
    return TABAS_Utils.getTfcObjectOnSquare(square, facing)
end

 ----------------- For Characters -----------------

function TABAS_Utils.setPlayerPosition(playerObj, x, y)
    playerObj:setX(x)
    playerObj:setY(y)
end

function TABAS_Utils.setPlayerPositionC(playerObj, square)
    playerObj:setX(square:getX() + 0.5)
    playerObj:setY(square:getY() + 0.5)
end

function TABAS_Utils.getCurrentPlayerPosition(playerObj)
    local x = playerObj:getX()
    local y = playerObj:getY()
    local z = playerObj:getZ()
    return x, y, z
end

function TABAS_Utils.getBodyBloodAndDirt(playerObj)
    local visual = playerObj:getHumanVisual()
    local bodyBlood = 0
    local bodyDirt = 0
    local maxIndex = BloodBodyPartType.MAX:index()
    for i=1, maxIndex do
        local part = BloodBodyPartType.FromIndex(i-1)
        bodyBlood = bodyBlood + visual:getBlood(part)
        bodyDirt = bodyDirt + visual:getDirt(part)
    end
    bodyBlood = math.ceil(bodyBlood / BloodBodyPartType.MAX:index() * 100)
    bodyDirt = math.ceil(bodyDirt / BloodBodyPartType.MAX:index() * 100)
    return bodyBlood, bodyDirt
end

function TABAS_Utils.getBodyGrime(playerObj)
    local grime = playerObj:getModData().BodyGrime or 0
    return math.ceil(grime)
end

function TABAS_Utils.getRequiredSoap(playerObj)
	local units = 0
	local visual = playerObj:getHumanVisual()
    local grimes = TABAS_Utils.getBodyGrime(playerObj)
    if grimes then
        units = grimes * 0.5
    end
	for i=1,BloodBodyPartType.MAX:index() do
		local part = BloodBodyPartType.FromIndex(i-1)
		-- Soap is used for blood but not for dirt.
		if visual:getBlood(part) > 0 then
			units = units + 0.5
		end
	end
	return math.ceil(units)
end


 ----------------- For Items -----------------

local soapTypes = {"Soap2", "BodyShampoo"}

function TABAS_Utils.getSoapList(playerObj, object)
    local soapList = {}
    local items = object:getSquare():getWorldObjects()
    for i=0, items:size()-1 do
        local item = items:get(i):getItem()
        if item and instanceof(item, "DrainableComboItem") then
            for j=1, #soapTypes do
                local soap = soapTypes[j]
                if item:getType() == soap then
                    table.insert(soapList, item)
                end
            end
        end
    end
    items = playerObj:getInventory():getItems()
    for i=0, items:size()-1 do
        local item = items:get(i)
        if item and instanceof(item, "DrainableComboItem") then
            for j=1, #soapTypes do
                local soap = soapTypes[j]
                if item:getType() == soap then
                    table.insert(soapList, item)
                end
            end
        end
    end
    return soapList
end

function TABAS_Utils.getSoapRemaining(soapList)
    if not soapList or #soapList < 1 then return 0 end

    local remaining = ISWashClothing.GetSoapRemaining(soapList)
    return remaining
end

local function predicateAvailableTowel(item)
    return not item:isBroken() and instanceof(item, "Clothing") and item:getWetness() < 25
end

local function predicateDrainableComboTowel(item)
    return not item:isBroken() and instanceof(item, "DrainableComboItem")
end

function TABAS_Utils.getAvailableTowel(playerObj)
    local playerInv = playerObj:getInventory()
    local towel = playerInv:getFirstTagEvalRecurse("Wipeable", predicateAvailableTowel)
    if not towel then
        towel = playerInv:getFirstTypeEvalRecurse("BathTowel", predicateDrainableComboTowel)
    end
    return towel
end

 ----------------- For Clothes -----------------

function TABAS_Utils.isNotExcludedClothing(item, excludeBathWear, ignoreOptions)
    local bodyLocation = item:getBodyLocation()
    if not bodyLocation then
        return false
    end
    if item:getDisplayName() == item:getFullType() then
        return false
    end
    if excludeBathWear then
        local excludeItemType = TABAS_Compat.Exclude.BathWear
        for i=1, #excludeItemType do
            if item:getFullType() == excludeItemType[i] then
                return false
            end
        end
    end
    local excludeBodyLocations = TABAS_Compat.Exclude.BodyLocations
    for i=1, #excludeBodyLocations do
        if bodyLocation == excludeBodyLocations[i] then
            return false
        end
    end
    if instanceof(item, "AlarmClockClothing") then
        if (TABAS_Utils.ModOptionsValue("NotTakeoff_Watches") or ignoreOptions) then
            return false
        end
    elseif (item:getAttachmentsProvided() and item:getActualWeight() <= 0.5) and (TABAS_Utils.ModOptionsValue("NotTakeOff_Belts") or ignoreOptions) then
        return false
    else
        if TABAS_Utils.ModOptionsValue("NotTakeOff_Accessories") or ignoreOptions then
            local excludeAccessorylocations = TABAS_Compat.Exclude.AccessoryLocations
            for i=1, #excludeAccessorylocations do
                if bodyLocation == excludeAccessorylocations[i] then
                    return false
                end
            end
        end
    end
    return true
end

function TABAS_Utils.getWornClothesCountExcluded(items, excludeBathWear)
    local count = 0
    for i=0, items:size()-1 do
        local item = items:get(i):getItem()
        if item and instanceof(item, "Clothing") then
            if TABAS_Utils.isNotExcludedClothing(item, excludeBathWear, true) then
                count = count + 1
            end
        end
    end
    return count
end

 ----------------- For TimedActions -----------------

local MakeUpLocations = {"MakeUp_FullFace", "MakeUp_Eyes", "MakeUp_EyesShadow", "MakeUp_Lips"}

function TABAS_Utils.removeAllMakeup(character)
    local makeup
    local removed = 0
    for i=1, #MakeUpLocations do
        makeup = character:getWornItem(MakeUpLocations[i])
        if makeup then
            character:removeWornItem(makeup)
            character:getInventory():Remove(makeup)
            removed = removed + 1
        end
    end
    return removed
end

function TABAS_Utils.cleaningBody(character, pct, factor)
    local visual = character:getHumanVisual()
    local decrease = 0
    local bloodTotal = 0
    local dirtTotal = 0
    local decreaseTotal = 0
    for i=1, BloodBodyPartType.MAX:index() do
        local part = BloodBodyPartType.FromIndex(i-1)
        local blood = visual:getBlood(part)
        if blood > 0 then
            decrease =  blood * pct * factor
            visual:setBlood(part, blood - decrease)
            bloodTotal = bloodTotal + decrease
            decreaseTotal = decreaseTotal + decrease
        end
        local dirt = visual:getDirt(part)
        if dirt > 0 then
            decrease = dirt * pct * factor
            visual:setDirt(part, dirt - decrease)
            dirtTotal = dirtTotal + decrease
            decreaseTotal = decreaseTotal + decrease
        end
    end
    -- print("[TABAS] CleaningBody decreased: blood >> ".. bloodTotal .. ", dirt >> " .. dirtTotal)
    character:resetModelNextFrame()
    sendVisual(character)
    return decreaseTotal
end

function TABAS_Utils.cleaningGrime(character, pct, factor)
    local modData = character:getModData()
    local grime = modData.BodyGrime
    local decrease = 0
    if grime and grime > 0 then
        decrease = grime * pct * factor
        modData.BodyGrime = grime - decrease
        -- print("[TABAS] CleaningGrime decreased: grime >> ".. decrease)
    end
    return decrease
end

function TABAS_Utils.cleaningWornItems(character, wornItems, pct, factor)
    local item
    local value
    local decrease = 0
    local decreaseTotal = 0
    for i=0, wornItems:size()-1 do
        item = wornItems:get(i):getItem()
        if item:getDisplayName() ~= item:getFullType() then
            if instanceof(item, "Clothing") then
                local coveredParts = BloodClothingType.getCoveredParts(item:getBloodClothingType())
                if coveredParts then
                    for j=0,coveredParts:size()-1 do
                        value = item:getBlood(coveredParts:get(j))
                        if value > 0 then
                            decrease = value * pct * factor
                            item:setBlood(coveredParts:get(j), value - decrease)
                            decreaseTotal = decreaseTotal + decrease
                        end
                        value = item:getDirt(coveredParts:get(j))
                        if value > 0 then
                            decrease = value * pct * factor
                            item:setDirt(coveredParts:get(j), value - decrease)
                            decreaseTotal = decreaseTotal + decrease
                        end
                    end
                end
                item:setWetness(100)
                value = item:getDirtyness()
                decrease = value * pct * factor
                item:setDirtyness(value - decrease)
                decreaseTotal = decreaseTotal + decrease * 0.01
            end
            value = item:getBloodLevel()
            decrease = value * pct * factor
            item:setBloodLevel(value - decrease)
            decreaseTotal = decreaseTotal + decrease * 0.01
            --sync Wetness, Dirtyness, BloodLevel
            syncItemFields(character, item)
        end
    end
    syncVisuals(character)
    triggerEvent("OnClothingUpdated", character)
    return decreaseTotal
end

local function removeFakeItem(character, item)
    local srcContainer = item:getContainer()
    local playerInv = character:getInventory()
    playerInv:Remove(item)
    sendRemoveItemFromContainer(srcContainer, item)
    character:removeWornItem(item, false)
    triggerEvent("OnClothingUpdated", character)
end

function TABAS_Utils.addFakeWornItem(character, itemType, location)
    local item = instanceItem(itemType)
    if not item then return end

    local bodyLocation = location or item:getBodyLocation()
    local oldItem = character:getWornItem(bodyLocation)
    if oldItem then
        removeFakeItem(character, oldItem)
    end
    character:getInventory():AddItem(item)
    sendAddItemToContainer(character:getInventory(), item)

    character:setWornItem(bodyLocation, item)
    sendClothing(character, bodyLocation, item)
end

function TABAS_Utils.removeFakeWornItem(character, itemType, location)
    local item = instanceItem(itemType)
    if not item then return end

    local bodyLocation = location or item:getBodyLocation()
    local oldItem = character:getWornItem(bodyLocation)
    if oldItem then
        removeFakeItem(character, oldItem)
    end
end

function TABAS_Utils.switchObjectSolidtrans(object, unset)
    if not object:getSprite() then return end
    local prop = object:getSprite():getProperties()
    if unset and prop:Is(IsoFlagType.solidtrans) then
        prop:UnSet(IsoFlagType.solidtrans)
    else
        prop:Set(IsoFlagType.solidtrans)
    end
    object:getSquare():RecalcPropertiesIfNeeded()
end

 ----------------- Misc -----------------

function TABAS_Utils.ModOptionsValue(option)
    return PZAPI.ModOptions:getOptions("TakeABathAndShower"):getOption(option):getValue()
end

function TABAS_Utils.formatedCelsiusOrFahrenheit(_temperature, _decimal)
    local decimal = _decimal or 0
	local temperature = round(_temperature, decimal)
    if getCore():isCelsius() then
        return tostring(temperature) .. " C"
    else
        local f = math.floor(temperature * 9 / 5 + 32 + 0.5)
		 return tostring(f) .. " F"
	end
end

function TABAS_Utils.getDifferentialTime(prevTime)
    if not prevTime then return 0,0,0 end
    local TABAS_GameTimes = require("TABAS_GameTimes")
    local currentTime = TABAS_GameTimes.Calender:getTimeInMillis()
    if not currentTime or currentTime == prevTime then return 0,0,0 end
    local dt = math.abs(currentTime - prevTime)
    -- print("Calculated Time Difference: ",dt)
    local minMod, hourMod, dayMod = 1000*60, 1000*60*60, 1000*60*60*24
    local days = math.floor(dt/dayMod)
    local output = "Diff Time: "
    if days ~= 0 then
        output = output .. tostring(days) .. "d, "
        dt = dt % dayMod
    end
    local hours = math.floor(dt/hourMod)
    if hours ~= 0 then
        output = output .. tostring(hours) .. "h, "
        dt = dt % hourMod
    end
    local minutes = math.floor(dt/minMod)
    if minutes ~= 0 then
        output = output .. tostring(minutes) .. "m"
        dt = dt - minutes*minMod
    end
    -- print(output)
    return minutes, hours, days
end

return TABAS_Utils