--- **Wrapper** - CLIENT wraps DCS Unit objects acting as a __Client__ or __Player__ within a mission.
-- 
-- ===
-- 
-- ### Author: **FlightControl**
-- ### Contributions: **funkyfranky**
-- 
-- ===
-- 
-- @module Wrapper.Client
-- @image Wrapper_Client.JPG


--- The CLIENT class
-- @type CLIENT
-- @field #string ClassName Name of the class.
-- @field #string ClientName Name of the client.
-- @field #string ClientBriefing Briefing.
-- @field #function ClientCallBack Callback function.
-- @field #table ClientParameters Parameters of the callback function.
-- @field #number ClientGroupID Group ID of the client.
-- @field #string ClientGroupName Group name.
-- @field #boolean ClientAlive Client alive.
-- @field #boolean ClientAlive2 Client alive 2.
-- @field #table Players Player table.
-- @field Core.Point#COORDINATE SpawnCoord Spawn coordinate from the template.
-- @extends Wrapper.Unit#UNIT


--- Wrapper class of those **Units** defined within the Mission Editor that have the skillset defined as __Client__ or __Player__.
-- 
-- Note that clients are NOT the same as Units, they are NOT necessarily alive.
-- The CLIENT class is a wrapper class to handle the DCS Unit objects that have the skillset defined as __Client__ or __Player__:
-- 
--  * Wraps the DCS Unit objects with skill level set to Player or Client.
--  * Support all DCS Unit APIs.
--  * Enhance with Unit specific APIs not in the DCS Group API set.
--  * When player joins Unit, execute alive init logic.
--  * Handles messages to players.
--  * Manage the "state" of the DCS Unit.
-- 
-- Clients are being used by the @{Tasking.Mission#MISSION} class to follow players and register their successes.
--  
-- ## CLIENT reference methods
-- 
-- For each DCS Unit having skill level Player or Client, a CLIENT wrapper object (instance) will be created within the global _DATABASE object (an instance of @{Core.Database#DATABASE}).
-- This is done at the beginning of the mission (when the mission starts).
--  
-- The CLIENT class does not contain a :New() method, rather it provides :Find() methods to retrieve the object reference
-- using the DCS Unit or the DCS UnitName.
-- 
-- Another thing to know is that CLIENT objects do not "contain" the DCS Unit object. 
-- The CLIENT methods will reference the DCS Unit object by name when it is needed during API execution.
-- If the DCS Unit object does not exist or is nil, the CLIENT methods will return nil and log an exception in the DCS.log file.
--  
-- The CLIENT class provides the following functions to retrieve quickly the relevant CLIENT instance:
-- 
--  * @{#CLIENT.Find}(): Find a CLIENT instance from the global _DATABASE object (an instance of @{Core.Database#DATABASE}) using a DCS Unit object.
--  * @{#CLIENT.FindByName}(): Find a CLIENT instance from the global _DATABASE object (an instance of @{Core.Database#DATABASE}) using a DCS Unit name.
--  
-- **IMPORTANT: ONE SHOULD NEVER SANITIZE these CLIENT OBJECT REFERENCES! (make the CLIENT object references nil).**
-- 
-- @field #CLIENT
CLIENT = {
  ClassName = "CLIENT",
  ClientName = nil,
  ClientAlive = false,
  ClientTransport = false,
  ClientBriefingShown = false,
  _Menus = {},
  _Tasks = {},
  Messages = {},
  Players = {},
}


--- Finds a CLIENT from the _DATABASE using the relevant DCS Unit.
-- @param #CLIENT self
-- @param DCS#Unit DCSUnit The DCS unit of the client.
-- @param #boolean Error Throw an error message.
-- @return #CLIENT The CLIENT found in the _DATABASE.
function CLIENT:Find(DCSUnit, Error)

  local ClientName = DCSUnit:getName()
  
  local ClientFound = _DATABASE:FindClient( ClientName )
  
  if ClientFound then
    ClientFound:F( ClientName )
    return ClientFound
  end
  
  if not Error then
    error( "CLIENT not found for: " .. ClientName )
  end
end

--- Finds a CLIENT from the _DATABASE using the relevant player name.
-- @param #CLIENT self
-- @param #string Name Name of the player
-- @return #CLIENT or nil if not found
function CLIENT:FindByPlayerName(Name)
  
  local foundclient = nil
  _DATABASE:ForEachClient(
    function(client)
      if client:GetPlayerName() == Name then
        foundclient = client
      end
    end
  )
  return foundclient
end

--- Finds a CLIENT from the _DATABASE using the relevant Client Unit Name.
-- As an optional parameter, a briefing text can be given also.
-- @param #CLIENT self
-- @param #string ClientName Name of the DCS **Unit** as defined within the Mission Editor.
-- @param #string ClientBriefing Text that describes the briefing of the mission when a Player logs into the Client.
-- @param #boolean Error A flag that indicates whether an error should be raised if the CLIENT cannot be found. By default an error will be raised.
-- @return #CLIENT
-- @usage
-- -- Create new Clients.
--  local Mission = MISSIONSCHEDULER.AddMission( 'Russia Transport Troops SA-6', 'Operational', 'Transport troops from the control center to one of the SA-6 SAM sites to activate their operation.', 'Russia' )
--  Mission:AddGoal( DeploySA6TroopsGoal )
--
--  Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*HOT-Deploy Troops 1' ):Transport() )
--  Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*RAMP-Deploy Troops 3' ):Transport() )
--  Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*HOT-Deploy Troops 2' ):Transport() )
--  Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*RAMP-Deploy Troops 4' ):Transport() )
function CLIENT:FindByName( ClientName, ClientBriefing, Error )

  -- Client
  local ClientFound = _DATABASE:FindClient( ClientName )

  if ClientFound then
    ClientFound:F( { ClientName, ClientBriefing } )
    
    ClientFound:AddBriefing(ClientBriefing)
    
    ClientFound.MessageSwitch = true

    return ClientFound
  end
  
  if not Error then
    error( "CLIENT not found for: " .. ClientName )
  end
end

--- Transport defines that the Client is a Transport. Transports show cargo.
-- @param #CLIENT self
-- @param #string ClientName Name of the client unit.
-- @return #CLIENT self
function CLIENT:Register(ClientName)

  -- Inherit unit.
  local self = BASE:Inherit( self, UNIT:Register(ClientName )) -- #CLIENT
  
  -- Set client name.
  self.ClientName = ClientName
  
  -- Message switch.
  self.MessageSwitch = true
  
  -- Alive2.
  self.ClientAlive2 = false

  return self
end


--- Transport defines that the Client is a Transport. Transports show cargo.
-- @param #CLIENT self
-- @return #CLIENT self
function CLIENT:Transport()
  self.ClientTransport = true
  return self
end

--- Adds a briefing to a CLIENT when a player joins a mission.
-- @param #CLIENT self
-- @param #string ClientBriefing is the text defining the Mission briefing.
-- @return #CLIENT self
function CLIENT:AddBriefing( ClientBriefing )

  self.ClientBriefing = ClientBriefing
  self.ClientBriefingShown = false
  
  return self
end

--- Add player name.
-- @param #CLIENT self
-- @param #string PlayerName Name of the player.
-- @return #CLIENT self
function CLIENT:AddPlayer(PlayerName)

  table.insert(self.Players, PlayerName)
  
  return self
end

--- Get number of associated players.
-- @param #CLIENT self
-- @return #number Count
function CLIENT:CountPlayers()
  return #self.Players or 0
end

--- Get player name(s).
-- @param #CLIENT self
-- @return #table List of player names or an empty table `{}`.
function CLIENT:GetPlayers()
  return self.Players
end

--- Get name of player.
-- @param #CLIENT self
-- @return #string Player name or `nil`.
function CLIENT:GetPlayer()
  if #self.Players>0 then
    return self.Players[1]
  end
  return nil
end

--- Remove player.
-- @param #CLIENT self
-- @param #string PlayerName Name of the player.
-- @return #CLIENT self
function CLIENT:RemovePlayer(PlayerName)

  for i,playername in pairs(self.Players) do
    if PlayerName==playername then
      table.remove(self.Players, i)
      break
    end
  end

  return self
end

--- Remove all players.
-- @param #CLIENT self
-- @return #CLIENT self
function CLIENT:RemovePlayers()
  self.Players={}
  return self
end


--- Show the briefing of a CLIENT.
-- @param #CLIENT self
-- @return #CLIENT self
function CLIENT:ShowBriefing()

  if not self.ClientBriefingShown then
    self.ClientBriefingShown = true
    local Briefing = ""
    if self.ClientBriefing and self.ClientBriefing ~= "" then
      Briefing = Briefing .. self.ClientBriefing
      self:Message( Briefing, 60, "Briefing" )
    end
  end

  return self
end

--- Show the mission briefing of a MISSION to the CLIENT.
-- @param #CLIENT self
-- @param #string MissionBriefing
-- @return #CLIENT self
function CLIENT:ShowMissionBriefing( MissionBriefing )
  self:F( { self.ClientName } )

  if MissionBriefing then
    self:Message( MissionBriefing, 60, "Mission Briefing" )
  end

  return self
end

--- Resets a CLIENT.
-- @param #CLIENT self
-- @param #string ClientName Name of the Group as defined within the Mission Editor. The Group must have a Unit with the type Client.
function CLIENT:Reset( ClientName )
  self:F()
  self._Menus = {}
end

-- Is Functions

--- Checks if the CLIENT is a multi-seated UNIT.
-- @param #CLIENT self
-- @return #boolean true if multi-seated.
function CLIENT:IsMultiSeated()
  self:F( self.ClientName )

  local ClientMultiSeatedTypes = { 
    ["Mi-8MT"]  = "Mi-8MT", 
    ["UH-1H"]   = "UH-1H", 
    ["P-51B"]   = "P-51B" 
  }
  
  if self:IsAlive() then
    local ClientTypeName = self:GetClientGroupUnit():GetTypeName()
    if ClientMultiSeatedTypes[ClientTypeName] then
      return true
    end
  end
  
  return false
end

--- Checks for a client alive event and calls a function on a continuous basis. Does **NOT** work for dynamic spawn client slots!
-- @param #CLIENT self
-- @param #function CallBackFunction Create a function that will be called when a player joins the slot.
-- @param ... (Optional) Arguments for callback function as comma separated list.
-- @return #CLIENT
function CLIENT:Alive( CallBackFunction, ... )
  self:F()
  
  self.ClientCallBack = CallBackFunction
  self.ClientParameters = arg

  self.AliveCheckScheduler = SCHEDULER:New( self, self._AliveCheckScheduler, { "Client Alive " .. self.ClientName }, 0.1, 5, 0.5 )
  self.AliveCheckScheduler:NoTrace()

  return self
end

-- @param #CLIENT self
function CLIENT:_AliveCheckScheduler( SchedulerName )
  self:T2( { SchedulerName, self.ClientName, self.ClientAlive2, self.ClientBriefingShown, self.ClientCallBack } )

  if self:IsAlive() then
   
    if self.ClientAlive2 == false then
    
      -- Show briefing.
      self:ShowBriefing()
      
      -- Callback function.
      if self.ClientCallBack then
        self:T("Calling Callback function")
        self.ClientCallBack( self, unpack( self.ClientParameters ) )
      end
      
      -- Alive.
      self.ClientAlive2 = true
    end
    
  else
  
    if self.ClientAlive2 == true then
      self.ClientAlive2 = false
    end
    
  end
  
  return true
end

--- Return the DCSGroup of a Client.
-- This function is modified to deal with a couple of bugs in DCS 1.5.3
-- @param #CLIENT self
-- @return DCS#Group The group of the Client.
function CLIENT:GetDCSGroup()
  self:F3()

--  local ClientData = Group.getByName( self.ClientName )
--  if ClientData and ClientData:isExist() then
--    self:T( self.ClientName .. " : group found!" )
--    return ClientData
--  else
--    return nil
--  end
  
  local ClientUnit = Unit.getByName( self.ClientName )

  local CoalitionsData = { AlivePlayersRed = coalition.getPlayers( coalition.side.RED ), AlivePlayersBlue = coalition.getPlayers( coalition.side.BLUE ) }
  
  for CoalitionId, CoalitionData in pairs( CoalitionsData ) do
    self:T3( { "CoalitionData:", CoalitionData } )
    for UnitId, UnitData in pairs( CoalitionData ) do
      self:T3( { "UnitData:", UnitData } )
      if UnitData and UnitData:isExist() then

        --self:F(self.ClientName)
        if ClientUnit then
        
          local ClientGroup = ClientUnit:getGroup()
          
          if ClientGroup then
            self:T3( "ClientGroup = " .. self.ClientName )
            
            if ClientGroup:isExist() and UnitData:getGroup():isExist() then
             
              if ClientGroup:getID() == UnitData:getGroup():getID() then
                self:T3( "Normal logic" )
                self:T3( self.ClientName .. " : group found!" )
                self.ClientGroupID = ClientGroup:getID()
                self.ClientGroupName = ClientGroup:getName()
                return ClientGroup
              end
              
            else
            
              -- Now we need to resolve the bugs in DCS 1.5 ...
              -- Consult the database for the units of the Client Group. (ClientGroup:getUnits() returns nil)
              self:T3( "Bug 1.5 logic" )
              
              local ClientGroupTemplate = _DATABASE.Templates.Units[self.ClientName].GroupTemplate
              
              self.ClientGroupID = ClientGroupTemplate.groupId
              
              self.ClientGroupName = _DATABASE.Templates.Units[self.ClientName].GroupName
              
              self:T3( self.ClientName .. " : group found in bug 1.5 resolvement logic!" )
              return ClientGroup
              
            end
  --        else
  --          error( "Client " .. self.ClientName .. " not found!" )
          end
        else
          --self:F( { "Client not found!", self.ClientName } )
        end
      end
    end
  end

  -- For non player clients
  if ClientUnit then
    local ClientGroup = ClientUnit:getGroup()
    if ClientGroup then
      self:T3( "ClientGroup = " .. self.ClientName )
      if ClientGroup:isExist() then 
        self:T3( "Normal logic" )
        self:T3( self.ClientName .. " : group found!" )
        return ClientGroup
      end
    end
  end
  
  -- Nothing could be found :(
  self.ClientGroupID = nil
  self.ClientGroupName = nil
  
  return nil
end 


--- Get the group ID of the client.
-- @param #CLIENT self
-- @return #number DCS#Group ID.
function CLIENT:GetClientGroupID()

  -- This updates the ID.
  self:GetDCSGroup()

  return self.ClientGroupID
end


--- Get the name of the group of the client.
-- @param #CLIENT self
-- @return #string
function CLIENT:GetClientGroupName()

  -- This updates the group name.
  self:GetDCSGroup()
  
  return self.ClientGroupName
end

--- Returns the UNIT of the CLIENT.
-- @param #CLIENT self
-- @return Wrapper.Unit#UNIT The client UNIT or `nil`.
function CLIENT:GetClientGroupUnit()
  self:F2()
  
  local ClientDCSUnit = Unit.getByName( self.ClientName )

  self:T( self.ClientDCSUnit )
  
  if ClientDCSUnit and ClientDCSUnit:isExist() then
    local ClientUnit=_DATABASE:FindUnit( self.ClientName )
    return ClientUnit
  end
  
  return nil
end

--- Returns the DCSUnit of the CLIENT.
-- @param #CLIENT self
-- @return DCS#Unit
function CLIENT:GetClientGroupDCSUnit()
  self:F2()

  local ClientDCSUnit = Unit.getByName( self.ClientName )
  
  if ClientDCSUnit and ClientDCSUnit:isExist() then
    self:T2( ClientDCSUnit )
    return ClientDCSUnit
  end
end


--- Evaluates if the CLIENT is a transport.
-- @param #CLIENT self
-- @return #boolean true is a transport.
function CLIENT:IsTransport()
  self:F()
  return self.ClientTransport
end

--- Shows the @{AI.AI_Cargo#CARGO} contained within the CLIENT to the player as a message.
-- The @{AI.AI_Cargo#CARGO} is shown using the @{Core.Message#MESSAGE} distribution system.
-- @param #CLIENT self
function CLIENT:ShowCargo()
  self:F()

  local CargoMsg = ""
  
  for CargoName, Cargo in pairs( CARGOS ) do
    if self == Cargo:IsLoadedInClient() then
      CargoMsg = CargoMsg .. Cargo.CargoName .. " Type:" ..  Cargo.CargoType .. " Weight: " .. Cargo.CargoWeight .. "\n"
    end
  end
  
  if CargoMsg == "" then
    CargoMsg = "empty"
  end
  
  self:Message( CargoMsg, 15, "Co-Pilot: Cargo Status", 30 )

end

--- The main message driver for the CLIENT.
-- This function displays various messages to the Player logged into the CLIENT through the DCS World Messaging system.
-- @param #CLIENT self
-- @param #string Message is the text describing the message.
-- @param #number MessageDuration is the duration in seconds that the Message should be displayed.
-- @param #string MessageCategory is the category of the message (the title).
-- @param #number MessageInterval is the interval in seconds between the display of the @{Core.Message#MESSAGE} when the CLIENT is in the air.
-- @param #string MessageID is the identifier of the message when displayed with intervals.
function CLIENT:Message( Message, MessageDuration, MessageCategory, MessageInterval, MessageID )
  self:F( { Message, MessageDuration, MessageCategory, MessageInterval } )

  if self.MessageSwitch == true then
    if MessageCategory == nil then
      MessageCategory = "Messages"
    end
    if MessageID ~= nil then
      if self.Messages[MessageID] == nil then
        self.Messages[MessageID] = {}
        self.Messages[MessageID].MessageId = MessageID
        self.Messages[MessageID].MessageTime = timer.getTime()
        self.Messages[MessageID].MessageDuration = MessageDuration
        if MessageInterval == nil then
          self.Messages[MessageID].MessageInterval = 600
        else
          self.Messages[MessageID].MessageInterval = MessageInterval
        end
        MESSAGE:New( Message, MessageDuration, MessageCategory ):ToClient( self )
      else
        if self:GetClientGroupDCSUnit() and not self:GetClientGroupDCSUnit():inAir() then
          if timer.getTime() - self.Messages[MessageID].MessageTime >= self.Messages[MessageID].MessageDuration + 10 then
            MESSAGE:New( Message, MessageDuration , MessageCategory):ToClient( self )
            self.Messages[MessageID].MessageTime = timer.getTime()
          end
        else
          if timer.getTime() - self.Messages[MessageID].MessageTime  >= self.Messages[MessageID].MessageDuration + self.Messages[MessageID].MessageInterval then
            MESSAGE:New( Message, MessageDuration, MessageCategory ):ToClient( self )
            self.Messages[MessageID].MessageTime = timer.getTime()
          end
        end
      end
    else
      MESSAGE:New( Message, MessageDuration, MessageCategory ):ToClient( self )
    end
  end
end

--- [Multi-Player Server] Get UCID from a CLIENT.
-- @param #CLIENT self
-- @return #string UCID
function CLIENT:GetUCID()
  local PID = NET.GetPlayerIDByName(nil,self:GetPlayerName())
  return net.get_player_info(tonumber(PID), 'ucid')
end

--- [Multi-Player Server] Return a table of attributes from CLIENT. If optional attribute is present, only that value is returned. 
-- @param #CLIENT self
-- @param #string Attribute (Optional) The attribute to obtain. List see below.
-- @return #table PlayerInfo or nil if it cannot be found
-- @usage
-- Returned table holds these attributes:
--
--          'id'    : player ID
--          'name'  : player name
--          'side'  : 0 - spectators, 1 - red, 2 - blue
--          'slot'  : slot ID of the player or 
--          'ping'  : ping of the player in ms
--          'ipaddr': IP address of the player, SERVER ONLY
--          'ucid'  : Unique Client Identifier, SERVER ONLY
--
function CLIENT:GetPlayerInfo(Attribute)
  local PID = NET.GetPlayerIDByName(nil,self:GetPlayerName())
  if PID then
    return net.get_player_info(tonumber(PID), Attribute)
  else
    return nil
  end
end
