-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
--
-- S_Bot, our beloved Standard Bot
--
-- Works on all game modes, some better than others
--
-- Authors:
--  - Unknown    (original QuickBot v2 code)
--  - sam686     (AI code for gametypes, other improvements)
--  - watusimoto (maintenance, upgrades)
--  - raptor     (maintenance, upgrades)
--  - Fordcars   (AI improvements)
--
-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
-------------------------------------------------------------------------------


profiles = {
    {
        name = "S_Bot",
        difficulty = 0.5,
        aggression = 0.5,
        defense = 0,
        speed = 1.0,
        accuracy = 0.90,
        dodging = 0.25
    },
    {
        name = "Meany",
        difficulty = 1.0,
        aggression = 1.0,
        defense = 0,
        speed = 1.0,
        accuracy = 1.0,
        dodging = 1.0
    },
    {
        name = "Barnside",
        difficulty = 0.5,
        aggression = 1.0,
        defense = 0,
        speed = 0.5,
        accuracy = 0.1,
        dodging = 0.1
    }
}

-- Worst possible angle is multiplied by the accuracy
WORST_ACCURACY = 0.25 * math.tau  -- quarter-circle...  really bad!


goalPt = point.new(0,0)

prevtarget = nil

gotoPositionWasNil = true

o = point.new(0,0)

botPos = nil        -- Bot's location, will be updated when onTick() is run

-- This function gets run once during setup.  It is the only time we can declare variables
-- and have them be defined as globals.
function main()
    -- Set up this bots profile
    
    -- Choose a random profile (for now)
    profileIndex = math.random(#profiles)
    profile = profiles[profileIndex]

    difficulty    = profile["difficulty"]
    aggression    = profile["aggression"]
    defense       = profile["defense"]
    speed         = profile["speed"]
    accuracy      = profile["accuracy"]
    dodging       = profile["dodging"]

    -- Set up other variables
    botRadius = bot:getRad()
    pathTimerMax = 250
    pathTimer = pathTimerMax
    dirToGo = 0
    
    game = bf:getGameInfo()
    gameType = game:getGameType()
    
    averageShieldIndex = 1
    averageShieldMaxTicks = 20

    averageShieldArray = { }
    for i = 1, averageShieldMaxTicks do
        averageShieldArray[i] = false
    end

    myOrbitalDirection = 1
    myObjective = math.random(0, 10)
end


items = { }     -- Global variable, reusable container for bf:findAllObjects.  Reusing this table avoids costs of
                -- constructing and destructing it every time we call bf:findAllObjects().  Be sure to clear the
                -- table before reusing it!


function shieldSelf()
    table.clear(items)
    bot:findVisibleObjects(items, ObjType.Bullet, ObjType.Seeker, ObjType.Asteroid, ObjType.Mine)

    local distToShieldAt = botRadius * 2 + (1 - difficulty) * 100
    if (items ~= nil) then
        for i,bullet in ipairs(items) do
            local bulletPos = bullet:getPos()
            local bulletVel = bullet:getVel()
            local angleDiff = math.abs(angleDifference(point.angleTo(o, bulletVel), point.angleTo(bulletPos, botPos)))
            --logprint(angleDiff)

            local bulletFromTeam = false
            if (game:isTeamGame() == true) and (bullet:getTeamIndex() == bot:getTeamIndex()) then -- If bullet is from team
                bulletFromTeam = true                               -- bullet from team is true
            end

            if (bulletFromTeam == false) and (point.distanceTo(bulletPos, botPos) < distToShieldAt + bullet:getRad() + point.distanceTo(o, bulletVel) * 50 and angleDiff < math.pi / 4) then
                bot:fireModule(Module.Shield)
                return(true)
            end
        end
    end
end


function angleDifference(angleA, angleB)
    return (math.mod(math.mod(angleA - angleB, math.pi * 2) + math.pi * 3, math.pi * 2) - math.pi)
end


function fireAtObjects()
    table.clear(items)
    bot:findVisibleObjects(items, ObjType.Robot, ObjType.Turret, ObjType.Ship, ObjType.Asteroid, 
        ObjType.ForceFieldProjector, ObjType.SpyBug, ObjType.Core)

    -- Cycle through list of potential items until we find one that we can attack
    for index, enemy in ipairs(items) do
        if(fireAtObject(enemy, Weapon.Phaser)) then
            break
        end
    end
end


-- Fires at the specified object with the specified weapon if the obj is a good target.
-- Does not fire if object is on the same team or if there is something in the way.
-- Returns whether it fired or not.
function fireAtObject(obj, weapon)
    local classId = obj:getObjType()

    if(classId == ObjType.Turret or classId == ObjType.ForceFieldProjector) then
        -- Ignore all same-team engineered objects...  even in single-team games
        if obj:getTeamIndex() == bot:getTeamIndex() then
            return false                                 -- Cancel fireAtObject function
        end

        if obj:getHealth() < .1  then                                             -- If item is essentially dead
            return false                                                          
        end
    end
    
    
    -- No shooting various team related objects
    if (obj:getTeamIndex() == bot:getTeamIndex() and game:isTeamGame()) or  -- Same team
        obj:getTeamIndex() == Team.Neutral then                            -- Neutral team
        if (classId == ObjType.Ship    or   classId == ObjType.Robot   or    -- Turrets and FFs handled above
            classId == ObjType.Core    or   classId == ObjType.SpyBug) then
            return false
        end
    end
    
    -- No shooting non-flag carriers in single player rabbit
    if gameType == GameType.Rabbit and not game:isTeamGame() and 
        (classId == ObjType.Ship or classId == ObjType.Robot) and 
        not bot:hasFlag() and not obj:hasFlag() then
        return false
    end

    -- We made it here!  We have a valid target..
    local angle = getFiringSolution(obj)
    if angle ~= nil and bot:hasWeapon(weapon) then
        local offset = ((math.random() * 2) - 1) * WORST_ACCURACY * (1 - accuracy)
        bot:setAngle(angle + offset)
        bot:fireWeapon(weapon)
        --logprint("bot:fireWeapon() called!");
        return(true)
    end
    --logprint("Firing solution not found.");
    return(false)
end


function getName()
    return profile["name"] .. " [s_bot]"
end


-- Each tick we see if we should shield
-- We store if we've shielded for 'averageShieldMaxTicks' ticks and average it
function shield()
    averageShieldArray[averageShieldIndex] = shieldSelf()
    averageShieldIndex = math.mod(averageShieldIndex,averageShieldMaxTicks) + 1

    local shieldTickCount = 0

    -- Find ratio of ticks shielded
    for i = 1, averageShieldMaxTicks do
        if(averageShieldArray[averageShieldIndex]) then
            shieldTickCount = shieldTickCount + 1
        end
    end

    -- Set ratio
    local shieldTickRatio = shieldTickCount / averageShieldMaxTicks

    -- Reverse orbital direction if our shielding ratio is greater than the 
    -- dodging threshold - this means the bot will appear to strafe more if
    -- 'dodging' is higher
    if(shieldTickRatio > 1 - dodging) then
        myOrbitalDirection = -myOrbitalDirection
        
        -- Reset shielding average if we had to change direction
        for i = 1, averageShieldMaxTicks do
            averageShieldArray[i] = false
        end
    end
end


function orbitPoint(pt, dir, inputDist, inputStrictness)
    local distAway = botRadius * 7
    local strictness = 2
    local direction = 1

    if(dir ~= nil) then 
        direction = dir 
    end


    if(inputDist ~= nil) then 
        distAway = inputDist 
    end

    if(inputStrictness ~= nil) then 
        strictness = inputStrictness 
    end

    if(pt ~= nil) then
        local dist = point.distanceTo(pt, botPos)
        local deltaDistance = (dist - distAway) * strictness / distAway
        local sign = 1
        if(deltaDistance > 0) then
            sign = 1
        elseif(deltaDistance < 0) then
            sign = -1
        end

        local changeInAngle = (math.abs(deltaDistance)/(deltaDistance + sign)) * math.pi/2
        local angleToPoint = point.angleTo(pt, bot:getPos())
        dirToGo = angleToPoint + (math.pi/2 + changeInAngle)*direction
        --bot:setThrust(speed, dirToGo)
    end
end

function gotoPosition(pt)
    if pt ~= nil then
        gotoPositionWasNil = false
        if pathTimer < .01 then
            goalPt = bot:getWaypoint(pt)
            if(goalPt ~= nil) then
                dirToGo = point.angleTo(botPos, goalPt)
            end
        end
    end
end

function gotoAndOrbitPosition(pt)
    if pt ~= nil then
        gotoPositionWasNil = false
        if not bot:canSeePoint(pt) then
            gotoPositionWasNil = false
            gotoPosition(pt)
        else
            gotoPositionWasNil = false
            orbitPoint(pt, myOrbitalDirection, botRadius * 5, 2)
        end
    end
end


-- TODO Use aggression in a different manner since I have no idea what this is doing
function setAggressiveAttackTarget(target, aggression)
    if target ~= nil then
        local targetPos = target:getPos()
      
        -- local dist    = point.distanceTo(botPos, targetPos)
        local myPow   = bot:getEnergy() + bot:getHealth()

        table.clear(items)
        bot:findVisibleObjects(items, ObjType.Ship, ObjType.Robot)
      
        local otherPow = target:getEnergy() + target:getHealth() * #items
      
        --advantage is between -1 and 1, -1 meaning an extreme disadvantage and 1 meaning an extreme advantage
        local advantage = (myPow - otherPow) / math.max(myPow, otherPow)
        
        if((advantage * 0.5) + .5 > 1 - aggression) then
            prevtarget = targetPos
        end
    end
end


-- Returns the objective for the bot, in the form of an object the bot can navigate towards.  This makes bots choose different defending locations.
-- If onTeam is true, will only return items on specified team.  If onTeam is false, will return items *not* on 
-- specified team.  If called with fewer than three args, will ignore team altogether.
function getObjective(objType, team, onTeam)

    table.clear(items)
    bf:findAllObjects(items, objType)       -- Returns a list of all items of type objType in the game

    local itemsOnMyTeam = {}
    local currentIndex = 1

    for index, item in ipairs(items) do         -- Iterate through all found items
        local itemTeamIndex = item:getTeamIndex()

        if (objType == ObjType.Flag) and (gameType == GameType.Nexus) then
            if not item:isOnShip() then
                itemsOnMyTeam[currentIndex] = item
                currentIndex = currentIndex + 1
            end
        elseif (objType == ObjType.Flag) and 
              ((gameType == GameType.HTF) or (gameType == GameType.Retrieve)) and 
              item:isInCaptureZone() then
                --logprint(item:getCaptureZone():getTeamIndex());
            if item:getCaptureZone():getTeamIndex() ~= bot:getTeamIndex() then
                itemsOnMyTeam[currentIndex] = item
                currentIndex = currentIndex + 1
            end
        elseif (objType == ObjType.GoalZone) and (gameType == GameType.HTF or gameType == GameType.Retrieve) then
            if onTeam == nil or ((itemTeamIndex == team) == onTeam) then
                if not item:hasFlag() then
                    itemsOnMyTeam[currentIndex] = item
                    currentIndex = currentIndex + 1
                end
            end
        else
            if (itemTeamIndex == Team.Neutral ) and (objType ~= ObjType.GoalZone or gameType ~= GameType.ZC) then
                itemTeamIndex = team   -- anything Neutral is on our team (except zone control neutral goal zone)
            end
            if onTeam == nil or ((itemTeamIndex == team) == onTeam) then
                itemsOnMyTeam[currentIndex] = item
                currentIndex = currentIndex + 1
            end
        end
    end
    local listMax = 0
    --find max
    if itemsOnMyTeam[1] ~= nil then
        for index,item in ipairs(itemsOnMyTeam) do
            if(item ~= nil) then
                listMax = listMax + 1
            end
        end
        local targetNum = math.mod(myObjective, listMax) + 1
        return(itemsOnMyTeam[targetNum])
    else
        return(nil)
    end

--  local closestitem = 0 --itemsOnMyTeam[1]
--  local cur = 1
--  local closestdist = 99999999
--  while itemsOnMyTeam[cur] ~= nil do
--      local item1 = itemsOnMyTeam[cur]
--      if item1 ~= nil then
--          local loc = item1.getPos()
--          if loc ~= nil then
--              local dist = point.distanceTo(botPos, loc)
--              if dist < closestdist then
--                  closestdist = dist
--                  closesetitem = item1
--              end
--          end
--      end
--      cur=cur+1
--  end
--  return(closestitem)
end


function doObjective(closestEnemy)
    gotoPositionWasNil = true
    if(gameType == GameType.Bitmatch) then
        --Nothing to do here.           
    elseif(gameType == GameType.Nexus) then
        -- Grab any flags that are found, and go to nexus when it opens
        local otherFlag = getObjective(ObjType.Flag)            -- Find any flags
        if otherFlag ~= nil then                                -- If there is a flag avalible
            gotoPosition(otherFlag:getPos()) 
        end

        --  If bot has more than 4 flags and the nexus is open or we're within 10 seconds of opening
        if bot:getFlagCount() > 4 and (game:isNexusOpen() or game:getNexusTimeLeft() < 10) then  --  Need to know if nexus is open
            local nexusOrFlag = getObjective(ObjType.Nexus)  -- unimplemented push function error.
            if nexusOrFlag ~= nil then 
                gotoPosition(nexusOrFlag:getPos()) 
            end
        end
    elseif(gameType == GameType.Rabbit) then
        --Grab a flag, or go after the flag.            
        if not bot:hasFlag() then
            local otherFlag = getObjective(ObjType.Flag, bot:getTeamIndex(), true)       -- Find flags on our team
            gotoPosition(otherFlag:getPos())
        end
    elseif(gameType == GameType.HTF or gameType == GameType.Retrieve) then
        -- Grab the flag and put it into goal zones
        -- Robot keeps trying to pick up the flags that is already in the goalZones
        if bot:hasFlag() then                                                           -- If the bot has the flag
            local otherFlag = getObjective(ObjType.GoalZone, bot:getTeamIndex(), true)   -- Find an avalible GoalZone on our team
            if otherFlag ~= nil then                                                    -- If there is an avalible GoalZone
                gotoPosition(otherFlag:getPos())                                        -- Go to it
            end
        else                                                                            -- If the bot doesn't have the flag
            local otherFlag = getObjective(ObjType.Flag, bot:getTeamIndex(), true)       -- Find flags on our team
            if otherFlag ~= nil then                                                    -- If there is a flag avalible
                gotoPosition(otherFlag:getPos())                                        -- Go to it
            end
        end
    elseif(gameType == GameType.CTF) then
        --defend the flag
        local myFlag    = getObjective(ObjType.Flag, bot:getTeamIndex(), true)    -- Find flags on our team
        local otherFlag = getObjective(ObjType.Flag, bot:getTeamIndex(), false)   -- Find flags not on our team

        if(defense < .5) then                                                    -- If bot doesn't defend allot (default is 0)
            if bot:hasFlag() then                                                -- If the bot has a flag
                if not myFlag:isOnShip() then                                    -- If my flag is not on a ship
                    gotoPosition(myFlag:getPos())                                -- Go to position of my flag
                else
                    gotoAndOrbitPosition(myFlag:getPos())                        -- Otherwise, go and orbit the flag on enemy
                end
                --gotoPosition(myFlag:getPos())
            else                                                                 -- If the bot doesn't have the flag
                local retrievingFlag = false
                if myFlag ~= nil then
                    if not myFlag:isInInitLoc() and not myFlag:isOnShip() and                     -- If my flag is not in its initial location and my flag is not on a ship
                            point.distSquared(myFlag:getPos(), bot:getPos()) <= 2000 * 2000 then  -- .. and we're within some sane range of the flag
                        gotoPosition(myFlag:getPos())                                             -- Go to and return my flag
                        retrievingFlag = true
                    end
                end

                if otherFlag ~= nil then                                         -- If there is an enemy flag
                    if not retrievingFlag then                                   -- .. and this bot isn't already retrieving a flag
                        if myFlag:isOnShip() then                                -- If my flag is on a ship
                            gotoPosition(myFlag:getPos())                        -- Go to position of my flag
                        elseif not otherFlag:isOnShip() then                     -- If enemy flag is not on a ship
                            gotoPosition(otherFlag:getPos())                     -- Go to that flag
                        else
                            gotoAndOrbitPosition(otherFlag:getPos())             -- Go and orbit our team flag's carrier
                        end
                    end
                end
            end
        else
            if bot:hasFlag() then                                                -- If the bot has a flag and has more than .5 defense
                gotoPosition(myFlag:getPos())                                    -- Go to team flag
            elseif myFlag:isInInitLoc() then                                     -- If the bot doesn't have a flag and the team flag is in intial location
                gotoAndOrbitPosition(myFlag:getPos())                            -- Go and orbit team flag
            else
                if myFlag:isOnShip() then                                        -- If team flag is on a ship
                    gotoAndOrbitPosition(myFlag:getPos())                        -- Go and orbit team flag
                else
                    gotoPosition(myFlag:getPos())                                -- If team flag is not on a ship, go to team flag
                end
            end
        end
    elseif(gameType == GameType.Soccer) then
        --grab soccer and put into enemy goal
        -- How do we know if we are holding soccer ? (not supported when cannot pickup soccer (016)
        if bot:getMountedItems(ObjType.SoccerBallItem)[1] ~= nil then
            local otherFlag = getObjective(ObjType.GoalZone, bot:getTeamIndex(), false)   -- Find GoalZones not on our team
            if otherFlag ~= nil then 
                gotoPosition(otherFlag:getPos()) 
            end           
        else
            local otherFlag = getObjective(ObjType.SoccerBallItem)                        -- Find SoccerBall
            if otherFlag ~= nil then 
                gotoPosition(otherFlag:getPos()) 
            end
        end
    elseif(gameType == GameType.ZC) then
        -- Grab flag, then go after zones that is not ours.
        if not bot:hasFlag() then
            local otherFlag = getObjective(ObjType.Flag, bot:getTeamIndex(), true)           -- Find flags on our team
            if otherFlag ~= nil then
                if otherFlag:isOnShip() then
                    gotoAndOrbitPosition(otherFlag:getPos())
                else
                    gotoPosition(otherFlag:getPos())
                end
            end
        else
            local otherFlag = getObjective(ObjType.GoalZone, bot:getTeamIndex(), false)      -- Find GoalZones on our team
            if otherFlag then
                gotoPosition(otherFlag:getPos())
            end
        end

    elseif(gameType == GameType.Core) then
        local obj = getObjective(ObjType.Core, bot:getTeamIndex(), false)                    -- Find enemy Core
        if obj ~= nil then
            gotoAndOrbitPosition(obj:getPos())
        end
    end

    -- If we have no where to go, go to nearest enemy.
    if gotoPositionWasNil then
        if(closestEnemy ~= nil) then
            prevtarget = closestEnemy:getPos()
        end
        if(prevtarget ~= nil) then 
            gotoAndOrbitPosition(prevtarget) 
        end
    end
end


function goInDirection()
    bot:setThrust(speed, dirToGo)
end


-- This function gets called every game tick; deltaTime is the time that has elapsed since it was last called
function onTick(deltaTime)
    botPos = bot:getPos()
    pathTimer = pathTimer - deltaTime
    assert(bot:getPos() ~= nil)

    local closestEnemy = bot:findClosestEnemy()
    
    setAggressiveAttackTarget(closestEnemy, aggression)
    doObjective(closestEnemy)   -- Set bot's objective

    goInDirection()             -- Move the ship
    fireAtObjects()             -- Fire weapons
    shield()                    -- Apply shield

    if(pathTimer < 0) then
        pathTimer = pathTimerMax + math.random(0, pathTimerMax)
    end
end
