require "TimedActions/ISBaseTimedAction"
require "W900_CisternParts"

ISTakeFluidFromW900Cistern = ISBaseTimedAction:derive("ISTakeFluidFromW900Cistern")
local RUBBER_HOSE_TYPE = "RubberHose"

local function is_inventory_target(target)
	if not target then
		return false
	end

	if target.kind ~= nil then
		return target.kind == "inventoryItem"
	end

	return target.getContainer ~= nil and target.getFluidContainer ~= nil
end

local function is_world_object_target(target)
	return target and target.kind == "worldObject"
end

local function get_target_ref(target)
	if not target then
		return nil
	end

	if target.ref ~= nil then
		return target.ref
	end

	return target
end

local function get_target_fluid_container(target)
	local ref = get_target_ref(target)
	if not ref or not ref.getFluidContainer then
		return nil
	end

	local ok, fluid_container = pcall(function()
		return ref:getFluidContainer()
	end)
	if ok then
		return fluid_container
	end

	return nil
end

local function transmit_world_object(target)
	local obj = is_world_object_target(target) and get_target_ref(target) or nil
	if not obj then
		return
	end

	if obj.transmitModData then
		obj:transmitModData()
	end

	local square = obj.getSquare and obj:getSquare() or nil
	if square and square.transmitModdata then
		square:transmitModdata()
	end
end

local function set_world_object_amount(target, fluid_type, amount)
	local obj = is_world_object_target(target) and get_target_ref(target) or nil
	if not obj then
		return
	end

	amount = math.max(0, amount or 0)
	if obj.emptyFluid then
		obj:emptyFluid()
	end

	if amount > 0 and obj.addFluid then
		local mapped_fluid = fluid_type == Fluid.Water and FluidType.Water or FluidType.Petrol
		obj:addFluid(mapped_fluid, amount)
		return
	end

	local fluid_container = get_target_fluid_container(target)
	if not fluid_container then
		return
	end

	if fluid_container:isEmpty() then
		fluid_container:addFluid(fluid_type, amount)
	else
		fluid_container:adjustAmount(amount)
	end
end

local function targetIsEmpty(item)
	local fluid_container = get_target_fluid_container(item)
	return fluid_container and fluid_container:isEmpty()
end

local function getPartFluidType(part)
	if not part then
		return nil
	end

	local contentType = part:getContainerContentType()
	if contentType == "Gasoline" then
		return Fluid.Petrol
	end
	if contentType == "Water" then
		return Fluid.Water
	end

	return nil
end

local function partIsUsable(part)
	return part
		and W900CisternParts.isInstalledPart(part)
		and part:getContainerContentAmount() > 0
end

local function targetIsUsable(target, fluidType)
	local fluid_container = get_target_fluid_container(target)
	if not fluid_container or fluid_container:getFreeCapacity() <= 0 or fluidType == nil then
		return false
	end

	if fluid_container:canAddFluid(fluidType) then
		return true
	end

	if not is_world_object_target(target) then
		return false
	end

	local ref = get_target_ref(target)
	local amount = fluid_container.getAmount and fluid_container:getAmount() or 0
	return amount > 0 and ref and ref.emptyFluid and ref.addFluid
end

function ISTakeFluidFromW900Cistern:isValid()
	local fluidType = getPartFluidType(self.part)
	local inventory_item = is_inventory_target(self.target) and get_target_ref(self.target) or nil
	return (not inventory_item or (not self.character:hasFullInventory() and self.character:getInventory():contains(inventory_item, true)))
		and self.character:getInventory():containsTypeRecurse(RUBBER_HOSE_TYPE)
		and partIsUsable(self.part)
		and fluidType ~= nil
		and targetIsUsable(self.target, fluidType)
end

function ISTakeFluidFromW900Cistern:waitToStart()
	self.character:faceThisObject(self.vehicle)
	return self.character:shouldBeTurning()
end

function ISTakeFluidFromW900Cistern:update()
	local progress
	if isServer() then
		progress = self.netAction:getProgress()
	else
		progress = self:getJobDelta()
	end

	if not isServer() then
		self.character:faceThisObject(self.vehicle)
		if self.item then
			self.item:setJobDelta(progress)
			self.item:setJobType(self.jobType)
		end
	end

	if not isClient() then
		local fluidType = getPartFluidType(self.part)
		if not fluidType then
			return
		end

		local litres = self.tankStart + (self.tankTarget - self.tankStart) * progress
		litres = math.ceil(litres)
		if litres ~= self.amountSent then
			self.part:setContainerContentAmount(litres)
			self.amountSent = litres
		end

		local litresTaken = self.tankStart - litres
		if is_world_object_target(self.target) then
			set_world_object_amount(self.target, fluidType, self.itemStart + litresTaken)
		elseif targetIsEmpty(self.target) then
			self.fluidCont:addFluid(fluidType, litresTaken)
		else
			self.fluidCont:adjustAmount(self.itemStart + litresTaken)
		end
		if self.item then
			self.item:syncItemFields()
		end
	end

	self.character:setMetabolicTarget(Metabolics.HeavyDomestic)
end

function ISTakeFluidFromW900Cistern:animEvent(event, parameter)
	if isServer() and event == "update" then
		self:update()
	end
end

function ISTakeFluidFromW900Cistern:serverStart()
	local period = 1000
	emulateAnimEvent(self.netAction, period, "update", nil)
end

function ISTakeFluidFromW900Cistern:start()
	self:setActionAnim("TakeGasFromVehicle")
	if self.item then
		self:setOverrideHandModels(nil, self.item:getStaticModel())
	end
	self.sound = self.character:playSound(self.soundName)
end

function ISTakeFluidFromW900Cistern:stop()
	self.character:stopOrTriggerSound(self.sound)
	if self.item then
		self.item:setJobDelta(0)
	end
	ISBaseTimedAction.stop(self)
end

function ISTakeFluidFromW900Cistern:perform()
	self.character:stopOrTriggerSound(self.sound)
	if self.item then
		self.item:setJobDelta(0)
	end
	local item = self:nextItem()
	if item then
		self:runAgain(item)
	end
	ISBaseTimedAction.perform(self)
end

function ISTakeFluidFromW900Cistern:runAgain(intoItem)
	if not intoItem then return end
	ISTimedActionQueue.addAfter(self, ISTakeFluidFromW900Cistern:new(self.character, self.part, intoItem, self.otherItems, self.fluidType))
	if is_inventory_target(intoItem) then
		local item = get_target_ref(intoItem)
		ISTimedActionQueue.addAfter(self, ISEquipWeaponAction:new(self.character, item, 50, false, false))
		if item:getContainer() ~= self.character:getInventory() then
			ISTimedActionQueue.addAfter(self, ISInventoryTransferAction:new(self.character, item, item:getContainer(), self.character:getInventory()))
		end
	end
end

function ISTakeFluidFromW900Cistern:nextItem()
	if not self.otherItems then return nil end
	while #self.otherItems > 0 do
		local item = table.remove(self.otherItems, 1)
		if item then
			local fluidType = getPartFluidType(self.part)
			if is_inventory_target(item) then
				local inv_item = get_target_ref(item)
				if inv_item and self.character:getInventory():contains(inv_item, true) and targetIsUsable(item, fluidType) then
					return item
				end
			elseif targetIsUsable(item, fluidType) then
				return item
			end
		end
	end
	return nil
end

function ISTakeFluidFromW900Cistern:complete()
	local fluidType = getPartFluidType(self.part)
	if not fluidType then
		return false
	end

	if is_world_object_target(self.target) then
		set_world_object_amount(self.target, fluidType, self.itemTarget)
	elseif targetIsEmpty(self.target) then
		self.fluidCont:addFluid(fluidType, self.itemTarget)
	else
		self.fluidCont:adjustAmount(self.itemTarget)
	end
	if self.vehicle and self.part then
		self.part:setContainerContentAmount(self.tankTarget)
		self.vehicle:transmitPartModData(self.part)
	end
	if self.item then
		self.item:syncItemFields()
	else
		transmit_world_object(self.target)
	end
	return true
end

function ISTakeFluidFromW900Cistern:serverStop()
	if self.item then
		self.item:syncItemFields()
	else
		transmit_world_object(self.target)
	end
	self.vehicle:transmitPartModData(self.part)
end

function ISTakeFluidFromW900Cistern:getDuration()
	self.itemStart = self.fluidCont:getAmount()
	self.tankStart = self.part:getContainerContentAmount()
	local add = self.fluidCont:getFreeCapacity()
	if self.item then
		local freeInventoryCapacity = self.character:getFreeInventoryCapacity() / ZomboidGlobals.EquippedOrWornEncumbranceMultiplier
		add = math.min(add, freeInventoryCapacity)
	end
	local take = math.min(add, self.tankStart)
	self.tankTarget = self.tankStart - take
	self.itemTarget = self.itemStart + take
	self.amountSent = math.ceil(self.tankStart)
	return take * 50
end

function ISTakeFluidFromW900Cistern:new(character, part, item, otherItems, fluidType)
	local o = ISBaseTimedAction.new(self, character)
	o.vehicle = part:getVehicle()
	o.part = part
	o.target = item
	o.item = is_inventory_target(item) and get_target_ref(item) or nil
	o.otherItems = otherItems
	o.fluidCont = get_target_fluid_container(item)
	o.fluidType = fluidType or getPartFluidType(part)
	o.soundName = o.fluidType == Fluid.Water and "TransferLiquid" or "CanisterAddFuelSiphon"
	o.jobType = o.fluidType == Fluid.Water and "Drain Cistern" or getText("ContextMenu_VehicleSiphonGas")
	o.maxTime = o:getDuration()
	return o
end