--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
--
--  file:    unit_metal_maker.lua
--  brief:   controls metal makers to minimize energy stalls and metal leaks
--  original author:  Owen Martindell
--
--  Copyright (C) 2008.
--  Licensed under the terms of the GNU GPL, v2 or later.
--
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------

function widget:GetInfo()
  return {
    name      = "Improved MetalMakers",
    desc      = "Controls metal makers - Disable other MM widgets before use (v2.70)",
    author    = "TheFatController, [LCC]jK",
    date      = "October 21, 2008",
    license   = "GNU GPL, v2 or later",
    layer     = 0,
    enabled   = true -- loaded by default?
  }
end

--------------------------------------------------------------------------------
--------------------------------------------------------------------------------

-- global vars (so other widget (i.e. guis) can interact with the widget)

mmw_update         = 1.0   -- Seconds per update, do not set this too low
mmw_etargetPercent = 0.5   -- Percent of energy bar to aim for
mmw_estallPercent  = 0.3   -- Percent of "mmw_etargetPercent", when energy stalling begins (30% of 50% -> 15%)
mmw_forceStall     = false

--------------------------------------------------------------------------------
--------------------------------------------------------------------------------

-- speed up

local GetMyTeamID        = Spring.GetMyTeamID
local GetMyAllyTeamID    = Spring.GetMyAllyTeamID
local GetTeamUnits       = Spring.GetTeamUnits
local GetUnitDefID       = Spring.GetUnitDefID
local GetUnitHealth      = Spring.GetUnitHealth
local GiveOrderToUnitMap = Spring.GiveOrderToUnitMap
local GetTeamResources   = Spring.GetTeamResources
local GetTeamUnits       = Spring.GetTeamUnits
local GetUnitResources   = Spring.GetUnitResources
local GetTeamList        = Spring.GetTeamList
local GetSpectatingState = Spring.GetSpectatingState
local IsReplay           = Spring.IsReplay

--------------------------------------------------------------------------------
--------------------------------------------------------------------------------

local myTeamID    = 0
local myAllyID    = 0
local eMaxRec     = 0
local timeCounter = 0
local minUpdate   = 0
local oldInc      = 0
local oldUse      = 0
local eStable     = 1

local metalMakers    = {}
local nonMetalMakers = {}

--------------------------------------------------------------------------------
--------------------------------------------------------------------------------

local function maker(unitID, efficiency, drain, state)
  return { unitID = unitID, efficiency = efficiency, drain = drain, state = state }
end


function widget:Initialize()
  if (CheckForSpec()) then return false end
  UpdateTeamUnits()
  minUpdate = mmw_update - (mmw_update / 4)
end


function widget:TeamDied(teamID)
  CheckForSpec()
end

function isMetalMaker(inUnit)
  if (inUnit.isBuilding) and (inUnit.onOffable) and (inUnit.makesMetal > 0) and (inUnit.energyUpkeep > 0) then
    return true
  else
    return false
  end
end

function CheckForSpec()
  if GetSpectatingState() or IsReplay() then
    Spring.SendCommands({"echo MetalMakers widget disabled for spectators"})
    widgetHandler:RemoveWidget()
    return true
  end
end


function UpdateTeamUnits(teamID)
  metalMakers    = {}
  nonMetalMakers = {}

  if (CheckForSpec()) then return false end
  
  myAllyID = GetMyAllyTeamID()
  myTeamID = GetMyTeamID()
  
  local teamUnits = GetTeamUnits(myTeamID)
  for _,unitID in ipairs(teamUnits) do
    local _, _, _, _, buildProgress = GetUnitHealth(unitID)
    if (buildProgress == 1) then
      local unitDefID  = GetUnitDefID(unitID)
      local unitDef    = UnitDefs[unitDefID]
      if isMetalMaker(unitDef) then
        table.insert(metalMakers, maker(unitID,(unitDef.energyUpkeep / unitDef.makesMetal), unitDef.energyUpkeep, 0))
      else
        table.insert(nonMetalMakers, unitID)
      end
    end
  end

  table.sort(metalMakers, function(m1,m2) return m1.efficiency < m2.efficiency; end)
  
end


function widget:UnitFinished(unitID, unitDefID, unitTeam)

  myAllyID = GetMyAllyTeamID()
  myTeamID = GetMyTeamID()
  
  if (unitTeam ~= myTeamID) then
    return
  end
    
  local unitDef = UnitDefs[unitDefID]
  if isMetalMaker(unitDef) then
    table.insert(metalMakers, maker(unitID,(unitDef.energyUpkeep / unitDef.makesMetal), unitDef.energyUpkeep, 0))
    table.sort(metalMakers, function(m1,m2) return m1.efficiency < m2.efficiency; end)
  else
    table.insert(nonMetalMakers, unitID)
  end
end


function widget:TextCommand(command)
  if (string.find(command, 'energyhover ') == 1) then
    local inVar = tonumber(string.sub(command, 13))
    if (inVar > 1) then
      if (inVar <= 100) then
        inVar = (inVar / 100)
      else
        inVar = 1
      end
    elseif (inVar < 0) then
      inVar = 0
    end
    mmw_etargetPercent = inVar
    if (timeCounter < minUpdate) then 
      timeCounter = mmw_update
    end
  elseif (string.find(command, 'forcestall') == 1) then
    mmw_forceStall = true
    timeCounter = mmw_update
  end
end

function widget:UnitDestroyed(unitID, unitDefID, unitTeam)
  if (unitTeam ~= myTeamID) then
    return
  end

  local unitDef = UnitDefs[unitDefID]
  if isMetalMaker(unitDef) then 
    for makerIndex,makerStats in ipairs(metalMakers) do
      if (unitID == makerStats.unitID) then
        table.remove(metalMakers,makerIndex)
        break
      end
    end
  else
    for nonMakerIndex,nonMakerID in ipairs(nonMetalMakers) do
      if (unitID == nonMakerID) then
         table.remove(nonMetalMakers,nonMakerIndex)
         break
      end
    end
  end
end


function widget:UnitTaken(unitID, unitDefID, unitTeam)
  widget:UnitDestroyed(unitID, unitDefID, unitTeam)
end


function widget:UnitGiven(unitID, unitDefID, unitTeam)
  widget:UnitFinished(unitID, unitDefID, unitTeam)
end

--------------------------------------------------------------------------------
--------------------------------------------------------------------------------


local function updateMakers(eFree, eMax, eRemain)
  local checkOnce = false
  local onMakers = {}
  local offMakers = {}
  for _,makerStats in ipairs(metalMakers) do
    eFree = (eFree - makerStats.drain)
    local makerState = -1

    if (eFree > 0) then
      makerState = 1
    elseif (not checkOnce) and (((eRemain + eFree) / eMax) > mmw_etargetPercent) then
      makerState = 1
      checkOnce = true
    end
    
    if (makerStats.state ~= makerState) then
      makerStats.state = makerState
      if (makerState == 1) then
        onMakers[makerStats.unitID] = true
      else
        offMakers[makerStats.unitID] = true
      end
    end
  end
  GiveOrderToUnitMap(onMakers, CMD.ONOFF, { 1 }, { })
  GiveOrderToUnitMap(offMakers, CMD.ONOFF, { 0 }, { })  
end


function widget:Update(deltaTime)
  
  if (next(metalMakers) == nil) then return end
  
  if (myTeamID ~= GetMyTeamID()) then UpdateTeamUnits() end
  
  local eCur, eMax, eUse, eInc, _, _, _, eRec = GetTeamResources(myTeamID, "energy")
  
  if (eMaxRec < eRec) then eMaxRec = eRec end
  
  local ePercent = (eCur/eMax)
  
  if (math.max(math.abs(oldInc-eInc), math.abs(oldUse-eUse)) > (eMax * 0.20)) then
    if (timeCounter < minUpdate) then 
      timeCounter = minUpdate
    end
  elseif (ePercent < 0.15) and (timeCounter < minUpdate) then 
    timeCounter = minUpdate 
  end
  
  if (eStable == 0) and (math.abs(ePercent - mmw_etargetPercent) <= 0.05) then
    eStable = 1
    timeCounter = mmw_update
  elseif (eStable == 1) then
    if (math.abs(ePercent - mmw_etargetPercent) > 0.2) then
      eStable = 0
    end
  end
    
  oldInc = eInc
  oldUse = eUse

  if (not mmw_forceStall) and (ePercent < (mmw_etargetPercent * mmw_estallPercent)) then
    updateMakers(0, eMax, eCur) -- turn all metal makers off
  elseif (timeCounter >= mmw_update) then
    local mCur, mMax, mUse, _, _, mShare = GetTeamResources(myTeamID, "metal")

    local nonMakerDrain = 0
    
    for _,unitID in ipairs(nonMetalMakers) do
      local _, _, _, eUse = GetUnitResources(unitID)
      if eUse then
        nonMakerDrain = nonMakerDrain + eUse
      end
    end
            
    local eAvail = ((eInc + eMaxRec) - nonMakerDrain)
    
    if (ePercent < (mmw_etargetPercent - 0.05)) then
      eAvail = (eAvail - metalMakers[1].drain)
    elseif (ePercent > (mmw_etargetPercent + 0.05)) then
      local eCounting = 0
      local mCounting = 0
      local eEff      = metalMakers[1].efficiency
      for _,makerStats in ipairs(metalMakers) do
        if (makerStats.efficiency == eEff) then
          eCounting = eCounting + makerStats.drain
          mCounting = mCounting + 1
        else
          break
        end
      end
      if ((eMax * 0.2) > (metalMakers[1].drain * mCounting)) then
        eAvail = math.max(eCounting,(eAvail + metalMakers[1].drain))
      else
        if (ePercent > (mmw_etargetPercent + 0.2)) then
          eAvail = eAvail + metalMakers[1].drain
        end
      end
    end
    
    if mmw_forceStall then
      eAvail = eMax
      if (ePercent < 0.15) then
        mmw_forceStall = false
      end
    elseif ((mCur > (mMax-7)) and (mShare < 1) and (mUse == 0)) then
      eAvail = 0
    end
     
    updateMakers(eAvail, eMax, eCur)
     
    timeCounter = 0
        
  else
    timeCounter = timeCounter + deltaTime
  end
end

--------------------------------------------------------------------------------
--------------------------------------------------------------------------------