--***********************************************************
--**                    ROBERT JOHNSON                     **
--***********************************************************

require "TimedActions/ISBaseTimedAction"

ISFishingAction = ISBaseTimedAction:derive("ISFishingAction");

function ISFishingAction:isValidStart()
    return self.tile ~= nil and IsoUtils.DistanceTo(self.tile:getX(), self.tile:getY(), self.character:getX(), self.character:getY()) < 10
end

function ISFishingAction:isValid()
	local actionQueue = ISTimedActionQueue.getTimedActionQueue(self.character)
	local lastAction = actionQueue.queue[#actionQueue.queue]
	if lastAction and (lastAction.Type ~= "ISFishingAction") then
		return false
    end
    
	if self.rod ~= self.character:getPrimaryHandItem() then return false end
	return self.lure == self.character:getSecondaryHandItem() or not self.lure;
end

function ISFishingAction:update()
    self.rod:setJobDelta(self:getJobDelta());
    -- Don't start a new strike animation close to the end of the action.
    if self:getJobDelta() < 0.75 then
        self.splashTimer = self.splashTimer - (getGameTime():getMultiplier() / 1.6);
    end
    if self.splashTimer <= 0 then
        if not self.usingSpear then
            if not self.firstCast then
                self.character:setVariable("FishingStage", "strike");
            end
            self.character:playSound("LureHitWater");
            addSound(self.character, self.character:getX(), self.character:getY(), self.character:getZ(), 20, 1)
            --        getSoundManager():PlayWorldSound("waterSplash", false, self.character:getSquare(), 1, 20, 1, false)
            self.splashTimer = ZombRand(150, 300);
        else
            self.character:setVariable("FishingStage", "spearStrike");
            self.character:playSound("StrikeWithFishingSpear");
            addSound(self.character, self.character:getX(), self.character:getY(), self.character:getZ(), 20, 1)
            self.splashTimer = ZombRand(150, 300);
        end
        self.firstCast = false;
    end

    -- add some boredom if you're not skilled
    if self.fishingLvl < 3 then
        self.character:getBodyDamage():setBoredomLevel(self.character:getBodyDamage():getBoredomLevel() + (ZomboidGlobals.BoredomDecrease * 0.01 * getGameTime():getMultiplier()))
    end

    self.character:setMetabolicTarget(Metabolics.UsingTools);
end


function ISFishingAction:start()
    self.rod:setJobType(getText("ContextMenu_Fishing"));
    self.rod:setJobDelta(0.0);
    self.pole = self.rod;
    self.usingSpear = false;
    if WeaponType.getWeaponType(self.rod) == WeaponType.spear then
        self.usingSpear = true;
        self.splashTimer = ZombRand(50, 150);
        self.character:setVariable("FishingStage", (self.stage == "cast") and "spearCast" or self.stage);
        -- The previous action did a strike at the very end.
        if self.stage == "spearStrike" then
            self.character:playSound("StrikeWithFishingSpear");
        end
    end
    if self.character:isSitOnGround() and self.usingSpear then
        self.character:setVariable("sitonground", false);
        self.character:setVariable("forceGetUp", true);
    end
    if not self.usingSpear then
        --self.character:PlayAnimUnlooped("Attack_" .. self.pole:getSwingAnim());
        --self.character:SetAnimFrame(0, false);
        --self.character:getSpriteDef():setFrameSpeedPerFrame(0.1);
        if self.stage == "cast" then
            self.splashTimer = 40 + ZombRand(10); -- splash timer is the SFX delay
            addSound(self.character, self.character:getX(), self.character:getY(), self.character:getZ(), 20, 1)
        else
            -- Finish "reel" from previous action.
            self.character:setVariable("ReelDone", false)
            self.castAfterReel = true;
            self.splashTimer = 10000;
        end
--        getSoundManager():PlayWorldSound("waterSplash", false, self.character:getSquare(), 1, 20, 1, false)
--        self.character:playSound("CastFishingLine");
        self.character:SetVariable("FishingStage", self.stage);
    end
    self.character:faceThisObject(self.tile);

    if self.character:getCurrentState()~=FishingState.instance() then
        self.character:reportEvent("EventFishing");
    end
end

function ISFishingAction:stop()
    self.character:PlayAnim("Idle");
    self.rod:setJobDelta(0.0);
    self.character:clearVariable("FishingStage")
    self.character:clearVariable("ReelDone")
    self.character:SetVariable("FishingFinished","true");
    ISBaseTimedAction.stop(self);
end

function ISFishingAction:perform()

    self.rod:setJobDelta(0.0);

    self.character:PlayAnim("Idle");

    if self.usingSpear then
        self.splashTimer = 0;
    end

    -- get the fishing zone to see how many fishes left
    local updateZone = self:getFishingZone();
    if updateZone then
        local fishLeft = tonumber(updateZone:getName());
        if getGametimeTimestamp() - updateZone:getLastActionTimestamp() > 20000 then
            fishLeft = math.max(ZombRand(10,25) + self.fishingZoneIncrease, 0);
            updateZone:setName(tostring(fishLeft));
            updateZone:setOriginalName(tostring(fishLeft));
        end
        if fishLeft == 0 then
            self.character:SetVariable("FishingFinished","true");
            -- needed to remove from queue / start next.
            ISBaseTimedAction.perform(self);
           return;
        end
    end


    local caughtFish = false;
    if self:attractFish() then -- caught something !
        local fish = self:getFish();
        if updateZone then
            local fishLeft = tonumber(updateZone:getName());
            updateZone:setName(tostring(fishLeft - 1));
            updateZone:setLastActionTimestamp(getGametimeTimestamp());
            if isClient() then updateZone:sendToServer() end
        end
        caughtFish = true;
    else
        if ZombRand(9) == 0 then -- give some xp even for a fail
            self.character:getXp():AddXP(Perks.Fishing, 1);
        end
        if self.lureProperties and ZombRand(100) <= self.lureProperties.chanceOfBreak then -- maybe remove the lure
            self.character:getSecondaryHandItem():Use();
            self.character:setSecondaryHandItem(nil);
        end
    end

    if not updateZone then -- register a new fishing zone
        local nbrOfFish = math.max(ZombRand(10,25) + self.fishingZoneIncrease, 0);
        local x,y,z = self.tile:getSquare():getX(), self.tile:getSquare():getY(), self.tile:getSquare():getZ()
        local updateZone = getWorld():registerZone(tostring(nbrOfFish), "Fishing", x - 20, y - 20, z, 40, 40);
        updateZone:setOriginalName(tostring(nbrOfFish));
        updateZone:setLastActionTimestamp(getGametimeTimestamp());
        if isClient() then updateZone:sendToServer() end
    end
    
    if self.fishingUI then
        self.fishingUI:updateZoneProgress(updateZone);
    end

    local newAction = nil;
    if not self.usingSpear then
        local lure = ISWorldObjectContextMenu.getFishingLure(self.character, self.rod)
        if lure then
            ISWorldObjectContextMenu.equip(self.character, self.character:getSecondaryHandItem(), lure:getType(), false);
            newAction = ISFishingAction:new(self.character, self.tile, self.rod, lure, self.fishingUI);
        end
    else
        newAction = ISFishingAction:new(self.character, self.tile, self.rod, nil, self.fishingUI);
    end

    if newAction then
        ISTimedActionQueue.add(newAction);
    end

    if not self.usingSpear then
        if newAction then
            if caughtFish then
                newAction.stage = "reel";
--                print(" - TRIGGER: strike (newcast & caughtfish)")
            else
                newAction.stage = "cast";
--                print(" - TRIGGER: cast (newcast & nocaught)")
            end
        else
            if caughtFish then
                self.character:SetVariable("FishingStage","strikeEnd");
--                print(" - TRIGGER: strikeEnd (nonewcast & caughtfish)")
            else
                self.character:SetVariable("FishingFinished","true");
--                print(" - TRIGGER: FishingFinished = true (nonewcast & nocaught)")
            end
        end
    else
        if newAction then
            if caughtFish then
                newAction.stage = "spearStrike";
                --                print(" - TRIGGER: strike (newcast & caughtfish)")
            else
                newAction.stage = "spearIdle";
                --                print(" - TRIGGER: cast (newcast & nocaught)")
            end
        else
            if caughtFish then
                self.character:SetVariable("FishingStage","spearStrike");
                --                print(" - TRIGGER: strikeEnd (nonewcast & caughtfish)")
            else
                self.character:SetVariable("FishingFinished","true");
                --                print(" - TRIGGER: FishingFinished = true (nonewcast & nocaught)")
            end
        end
    end

    -- needed to remove from queue / start next.
    ISBaseTimedAction.perform(self);
end

function ISFishingAction:getFishingZone()
    ISFishingAction.zones_array:clear();
    local zones = getWorld():getMetaGrid():getZonesAt(self.tile:getSquare():getX(), self.tile:getSquare():getY(), self.tile:getSquare():getZ(), ISFishingAction.zones_array);
    if zones then
        for i=0,zones:size()-1 do
            if zones:get(i):getType() == "Fishing" then
                return zones:get(i);
            end
        end
    end
    return nil
end

ISFishingAction.zones_array = ArrayList.new()

function ISFishingAction.getFishingZoneFixed(x, y, z)
    ISFishingAction.zones_array:clear();
    local zones = getWorld():getMetaGrid():getZonesAt(x, y, z, ISFishingAction.zones_array);
    if zones then
        for i=0,zones:size()-1 do
            if zones:get(i):getType() == "Fishing" then
                return zones:get(i);
            end
        end
    end
    return nil
end

-- get a fish by the number
-- if plastic lure : 15/100 it's a big, 25/100 medium and 60/100 it's a little/lure fish
-- if living lure : 20/100 it's a big, 30/100 it's a medium and 50/100 it's a little/lure fish
function ISFishingAction:getFish()
    local fishItem = nil;
    local fishSizeNumber = ZombRand(100);
    local fish = {};
    -- we gonna determine the fish size and give player's xp
    -- first, if we have a plastic lure
    if self.plasticLure then
        if fishSizeNumber <= 15 then
            fish.size = "Big";
            self.character:getXp():AddXP(Perks.Fishing, 7);
        elseif fishSizeNumber <= 25 then
            fish.size = "Medium";
            self.character:getXp():AddXP(Perks.Fishing, 5);
        else
            fish.size = "Small";
            self.character:getXp():AddXP(Perks.Fishing, 3);
        end
    else -- living lure size
        if fishSizeNumber <= 20 then
            fish.size = "Big";
            self.character:getXp():AddXP(Perks.Fishing, 7);
        elseif fishSizeNumber <= 30 then
            fish.size = "Medium";
            self.character:getXp():AddXP(Perks.Fishing, 5);
        else
            fish.size = "Small";
            self.character:getXp():AddXP(Perks.Fishing, 3);
        end
    end
    fish.fish = self:getFishByLure();
    if fish.fish.name then -- if no name then it's a "trash" item
    -- then we may broke our line
        if not self:brokeLine(fish) then
            -- we gonna create our fish
            fishItem = self:createFish(fish, fish.fish);
--            getSoundManager():PlayWorldSound("getFish", false, self.character:getSquare(), 1, 20, 1, false)
            self.character:playSound("CatchFish");
            addSound(self.character, self.character:getX(), self.character:getY(), self.character:getZ(), 20, 1)
        end
    else
        fishItem = InventoryItemFactory.CreateItem(fish.fish.item);
        local inv = self:getUsedInventory(fishItem);
        inv:AddItem(fishItem);
        if not self.usingSpear then
--            getSoundManager():PlayWorldSound("getFish", false, self.character:getSquare(), 1, 20, 1, false)
            self.character:playSound("CatchTrashWithRod");
            addSound(self.character, self.character:getX(), self.character:getY(), self.character:getZ(), 20, 1)
        end
    end

    -- remove the lure
    if not self.plasticLure and self.character:getSecondaryHandItem() then
        self.character:getSecondaryHandItem():Use();
        self.character:setSecondaryHandItem(nil);
    end
    
    if self.fishingUI then
        self.fishingUI:setFish(fishItem);
    end
    
    return fish;
end

function ISFishingAction:animEvent(event, parameter)
    if event == "ReelDone" then
        if self.castAfterReel then
            self.castAfterReel = false
            self.character:setVariable("FishingStage", "cast")
            self.splashTimer = 40 + ZombRand(10);
        elseif not self.character:isVariable("FishingStage", "cast") then
            self.character:setVariable("FishingStage", "idle")
        end
    end
end

-- Visual ratio of fish caught and trash caught using player level
function ISFishingAction:getFishByLure()
    local item = 0;
    local MaxTrashRate = 0.4;
    local MinTrashRate = 0.15;
    local DampingConstant = 0.3;
    local trashRate = MaxTrashRate;
    for i = 0,self.fishingLvl do
        trashDelta = trashRate - MinTrashRate
        trashRate = trashRate - (trashDelta*DampingConstant)
    end
    if ZombRandFloat(0.0,1.0) < trashRate then
        item = Fishing.trashItems[ZombRand(#Fishing.trashItems) + 1];
    else
        item = Fishing.fishes[ZombRand(#Fishing.fishes) + 1];
    end

    for i,v in ipairs(item.lure) do
        if (self.lure and v == self.lure:getType()) or self.usingSpear then
            return item;
        end
    end
    return self:getFishByLure(); -- (could cause stack overflow if caught in infinite loop when lure is invalid for any fish)
end

-- create the fish we just get
-- we randomize is weight and size according to his size
-- then we set his new name
function ISFishingAction:createFish(fishType, fish)
    --    local fish = Fishing.fishes[fishType.fishType];
    local fishToCreate = InventoryItemFactory.CreateItem(fish.item);
    local baseWeightLb = fishToCreate:getActualWeight();
    local size = nil;
    local maxSize = nil;
    local minSize = nil;
    local weightKg = nil;
    local baseScale = 1;
    -- now we set the size (for the name) and weight (for hunger) according to his size (little, medium and big)
    if fishType.size == "Small" then
        size = ZombRand(fish.little.minSize, fish.little.maxSize);
        maxSize = fish.little.maxSize;
        minSize = fish.little.minSize;
        weightKg = size / fish.little.weightChange;
    elseif fishType.size == "Medium" then
        size = ZombRand(fish.medium.minSize, fish.medium.maxSize);
        maxSize = fish.medium.maxSize;
        minSize = fish.medium.minSize;
        weightKg = size / fish.medium.weightChange;
        baseScale = 1.2;
    else
        size = ZombRand(fish.big.minSize, fish.big.maxSize);
        maxSize = fish.big.maxSize;
        minSize = fish.big.minSize;
        weightKg = size / fish.big.weightChange;
        baseScale = 1.4;
    end
    local scaleMod = (((size - minSize) + 1) / ((maxSize - minSize) + 1) / 2);
--    print("got a fish ", fishType.size, size, minSize, maxSize, " % ", scaleMod, "full scale: ", (baseScale + scaleMod));
    local nutritionFactor = 2.2 * weightKg / baseWeightLb;
    fishToCreate:setCalories(fishToCreate:getCalories() * nutritionFactor);
    fishToCreate:setLipids(fishToCreate:getLipids() * nutritionFactor);
    fishToCreate:setCarbohydrates(fishToCreate:getCarbohydrates() * nutritionFactor);
    fishToCreate:setProteins(fishToCreate:getProteins() * nutritionFactor);
    fishToCreate:setWorldScale(scaleMod + baseScale);

    -- the fish name is like : Big Trout - 26cm
    if not fish.noNameChange then
        fishToCreate:setName(getText("IGUI_Fish_" .. fishType.size) .. " " .. getText("IGUI_Fish_" .. string.gsub(fish.name, "%s+", "")) .. " - " .. size .. "cm");
    end

    -- hunger reduction is weight of the fish div by 6, and set it to negative
    fishToCreate:setBaseHunger(- weightKg / 6);
    fishToCreate:setHungChange(fishToCreate:getBaseHunger());
    -- weight is kg * 2.2 (in pound)
    fishToCreate:setActualWeight(weightKg * 2.2);
    fishToCreate:setCustomWeight(true)
    local inv = self:getUsedInventory(fishToCreate);
    inv:AddItem(fishToCreate);
    
    return fishToCreate;
end

function ISFishingAction:getUsedInventory(item)
    if not self.fishingUI or (self.fishingUI:getSelectedBag() == self.character:getInventory()) then return self.character:getInventory(); end
    --[[for i=1,self.fishingUI.bagOptions:getNumOptions() do
        local container = self.fishingUI.bagOptions:getOptionData(i)
        if self.fishingUI.bagOptions:isSelected(i) and (container:getCapacityWeight() + item:getWeight()) <= container:getCapacity() then
            return container;
        end
    end--]]
    local container = self.fishingUI:getSelectedBag():getItemContainer();
    if(container ~= nil and (container:getCapacityWeight() + item:getWeight()) <= container:getCapacity()) then
        return container;
    else return self.character:getInventory(); end
end

------ broken line risk : (every skills pts lower by 1 this number)
-------- * big fish 9/100 risk to broke the line
-------- * medium fish 6/100
-------- * little fish 2/100
function ISFishingAction:brokeLine(fish)
    local brokenLineNumber = ZombRand(100);
    local breakRodeNumber = 0;
    if fish.size == "Small" then
        breakRodeNumber = 8 - self.fishingLvl;
    elseif fish.size == "Medium" then
        breakRodeNumber = 12 - self.fishingLvl;
    else
        breakRodeNumber = 22 - self.fishingLvl;
    end
    if not string.match(self.pole:getType(), "TwineLine") then -- a rod with twine line have more chance to break
        breakRodeNumber = breakRodeNumber - 2;
    end
    if breakRodeNumber < 0 then
        breakRodeNumber = 0;
    end
    if self.pole:getType() == "CraftedFishingRod" then -- a crafted rode have more chance to break
        breakRodeNumber = breakRodeNumber + 3;
    end
    if self.usingSpear then -- spear have more chance to break
        breakRodeNumber = breakRodeNumber + 5;
    end
    if brokenLineNumber <= breakRodeNumber then
        return self:brokeThisLine();
    end
    return false;
end

function ISFishingAction:brokeThisLine()
    if self.pole:getType() == "CraftedFishingRod" or self.pole:getType() == "CraftedFishingRodTwineLine" then
        self.character:getInventory():AddItem("Base.WoodenStick");
        self.character:playSound("BreakFishingLine")
    elseif self.pole:getType() == "FishingRod" or self.pole:getType() == "FishingRodTwineLine" then
        self.character:getInventory():AddItem("Base.FishingRodBreak");
        self.character:playSound("BreakFishingLine")
    elseif self.usingSpear then
        self.pole:setCondition(self.pole:getCondition() - 1)
        if self.pole:isBroken() then
            self.character:removeFromHands(self.pole);
            self.character:getInventory():setDrawDirty(true);
            self:forceStop();
            return true
        end
        return false
    end
    self.character:removeFromHands(self.pole);
    self.character:getInventory():Remove(self.pole);
    if self.lure then
        self.character:getInventory():Remove(self.lure);
        self.character:setSecondaryHandItem(nil);
    end
    self:forceStop();
--    getSoundManager():PlaySound("waterSplash", false, 1, 0)
    return true;
end

-- Depend on what lure you used :
-- Living lure is easier but can escape and always removed after getting something
-- Plastic lure are for good fisherman, almost never disapear but harder to get something
function ISFishingAction:attractFish()
    local attractNumber = ZombRand(self.attractNumber);
    -- a bit more chance during dawn and dusk
    local currentHour = math.floor(math.floor(GameTime:getInstance():getTimeOfDay() * 3600) / 3600);
    if (currentHour >= 4 and currentHour <= 6) or (currentHour >= 18 and currentHour <= 20) then
        attractNumber = attractNumber - 10;
    end
    if self.usingSpear then -- less chance to catch something with a spear
        attractNumber = attractNumber + 10;
    end
    -- harder chance of getting fish during winter
    if (getGameTime():getMonth() + 1) >= 11 or (getGameTime():getMonth() + 1) <= 2 then
        attractNumber = attractNumber + 20;
    end
    -- start with plastic lure
    if self.plasticLure and attractNumber <= (10 + (self.fishingLvl * 2.5)) then
        return true;
    elseif not self.plasticLure and attractNumber <= (20 + (self.fishingLvl * 1.5)) then
        return true;
    end
--    return true;
    return false;
end

function ISFishingAction:new(character, tile, rod, lure, fishingUI)
	local o = {}
	setmetatable(o, self)
	self.__index = self
	o.character = character;
	o.stopOnWalk = true;
	o.stopOnRun = true;
    o.fishingUI = fishingUI;
    o.fishingLvl = character:getPerkLevel(Perks.Fishing);
    o.rod = rod
    o.lure = lure
    if o.lure then
        o.lureProperties = Fishing.lure[o.lure:getType()];
        o.plasticLure = o.lureProperties.plastic;
    else
        o.plasticLure = true;
    end
    if not o.lure then -- spear fishing
        o.maxTime = 300 + ZombRand(300) - (o.fishingLvl * 5)
    else
        if o.plasticLure then
            o.maxTime = 700 + ZombRand(300) - (o.fishingLvl * 5);
        else
            o.maxTime = 500 + ZombRand(300) - (o.fishingLvl * 5);
        end
    end
    o.tile = tile;
    o.stage = "cast";
    o.firstCast = true;
    o.splashTimer = 0;

    -- sandbox settings
    o.fishingZoneIncrease = 0;
    if SandboxVars.NatureAbundance == 1 then -- very poor
        o.fishingZoneIncrease = -10;
    elseif SandboxVars.NatureAbundance == 2 then -- poor
        o.fishingZoneIncrease = -5;
    elseif SandboxVars.NatureAbundance == 4 then -- abundant
        o.fishingZoneIncrease = 5;
    elseif SandboxVars.NatureAbundance == 5 then -- very abundant
        o.fishingZoneIncrease = 10;
    end

    o.attractNumber = 100;
    if SandboxVars.NatureAbundance == 1 then -- very poor
        o.attractNumber = 140;
    elseif SandboxVars.NatureAbundance == 2 then -- poor
        o.attractNumber = 120;
    elseif SandboxVars.NatureAbundance == 4 then -- abundant
        o.attractNumber = 80;
    elseif SandboxVars.NatureAbundance == 5 then -- very abundant
        o.attractNumber = 60;
    end

    o.caloriesModifier = 1.2;

    return o;
end
