local socket = require "socket"

local debugFont
local debugBuffer={}
local debugTextEnabled=false
local maxDebugBufferSize=3
local appLoaded=false
local isHost=false
local playerName="player"
local networkTimer=0
local networkUpdateRate=0.1
local hostID="0"
local clientID="0"
local portNum=12345
local hostAddress="localhost"
local appPath
local getTextMode=false

function log(msg)
  if debugTextEnabled == true then
    print(msg)
    table.insert(debugBuffer,msg)
    for k,v in pairs(debugBuffer) do
      if k > maxDebugBufferSize then
        table.remove(debugBuffer,1)
        break
      end
    end
  end
end

function makeUID()
  math.randomseed(os.time())
  local uid = math.random(999999)
  while uid <= 0 do
    uid = math.random(999999)
  end
  str = string.format("%d",uid)
  return str
end

--host broadcasts available game name and version number and uid
--client replies with uid to connect
--server gives client new uid to confim connection
--client can then request game data or play game


function love.load()
    clientID = makeUID()

    udp = socket.udp()
    udp:settimeout(0)
    
    if arg[2] == "-h" then
      --host a game
      isHost=true
      hostID = makeUID()
      if arg[3] ~= nil then
        appPath = arg[3]
        if arg[4] ~= nil then playerName=arg[4] end
        if arg[5] ~= nil then hostAddress = arg[5] end
        loadApp(appPath,playerName)
      else
        log("Error: missing app parameter")
      end
      udp:setsockname('*', portNum)
    else
      --find a game
      if arg[3] ~= nil then appPath = arg[3] end
      if arg[4] ~= nil then playerName = arg[4] end
      if arg[5] ~= nil then hostAddress = arg[5] end
      log("CLIENT CMD: " .. appPath .. " " .. playerName .. " " .. hostAddress)
      udp:setpeername(hostAddress, portNum)
      --broadcast connect message and wait
      --on response read game name and load
      --Note: need to check if game name exists
      --and download if it doesnt
      --also verify version number
    end
    
    debugFont = love.graphics.newFont(love._vera_ttf, 14)
end

function love.update(dt)
    if love.keyboard.isDown("x") then
      --os.exit()
  	end
  	
  	--Do network tick
  	networkTimer = networkTimer + dt
  	if networkTimer > networkUpdateRate then 
  	  networkTimer=0
  	  processMessages(dt)
  	end
  	
  	--Do game tick
  	if appLoaded == true then 
  	  appupdate(dt) 
  	end
end

function love.draw()
  love.graphics.setFont(debugFont) 
  love.graphics.setColor( 255,255,255 )
  for k,v in pairs(debugBuffer) do
    love.graphics.print(v, 0, 420+(k*15));
  end
  if appLoaded == true then appdraw() end
end

function loadApp(path,playerName)
  package.path = path .. "/?.lua"
  require("appmain")
  if appname ~= nil then
    if appversion ~= nil then
      if appload ~= nil then
        if appupdate ~= nil then
          if appdraw ~= nil then
            --App loaded!!
            appload(playerName)
            appLoaded=true
            log("Loaded: " .. appname .. " v" .. appversion)
          else
            log("Error: mainapp doesnt define 'appdraw'")
          end
        else
          log("Error: mainapp doesnt define 'appupdate'")
        end
      else
        log("Error: mainapp doesnt define 'appload'")
      end
    else
      log("Error: mainapp doesnt define 'appversion'")
    end
  else
    log("Error: mainapp doesnt define 'appname'")
  end
end

local shift=false
local textBuffer=""
function love.keyreleased(k)
  if k=="lshift" or k=="rshift" then
    shift=false
  end
end
function love.keypressed(k)

  if k=="lshift" or k=="rshift" then
    shift=true
  end

  if getTextMode == true then
    if k == "backspace" then
      local len = string.len(textBuffer)
      textBuffer = string.sub(textBuffer,1,len-1)
    elseif k == "return" then
      getTextMode=false
    else
      local letter = k
      if string.len(letter) == 1 then
        if shift==true then
          if letter == '1' then letter = '!'
          elseif letter == '2' then letter = '"'
          elseif letter == '3' then letter = '£'
          elseif letter == '4' then letter = '$'
          elseif letter == '5' then letter = '%'
          elseif letter == '6' then letter = '^'
          elseif letter == '7' then letter = '&'
          elseif letter == '8' then letter = '*'
          elseif letter == '9' then letter = '('
          elseif letter == '0' then letter = ')'
          elseif letter == '/' then letter = '?'
          end
        end
        if textBuffer == nil then textBuffer = "" end
        textBuffer = textBuffer .. letter
      end
    end
  end
end

function textInputOn()
  getTextMode=true
end

function getTextInputBuffer()
  return textBuffer
end

function clearTextInputBuffer()
  textBuffer=""
end

function isTextInputOn()
  return getTextMode
end

--Client Functions--

local sendMessageList={}
local receiveMessageList={} 
local clientList={} --clientID clientIP clientPort lastPing

function composeMessage( hID, cID, msgType, msgData )
  assert(hID)
  assert(cID)
  assert(msgType)
  assert(msgData)
  local dg = string.format("%s %s %s %s",hID,cID,msgType,msgData)
  return dg
end

function clientPushMessage(messageType, messageData)
  --Note: messageData MUST be a string!
  --Because messages maybe delayed in sending we only want to keep the most
  --recent version of a particular message type. Use clientPushMessageUnique if
  --a unique copy of the message type is required
  local found = false
  for k,v in pairs(sendMessageList) do
    if v[1] == messageType then
      found=true
      v[2]=messageData
    end
  end
  if found == false then
    clientPushMessageUnique(messageType,messageData)
  end
end

function clientPushMessageUnique(messageType, messageData)
  table.insert(sendMessageList,{messageType,messageData})
end

function clientPopMessage()
  local msg = receiveMessageList[1]
  if msg ~= nil then
    table.remove(receiveMessageList,1)
  end
  return msg
end

function processMessages(dt)  
  --update last ping time
  for k,v in pairs(clientList) do
    v[4] = v[4] + dt
    --TODO handle timedout connections
  end
  
  --host send message to all clients
  --client send to host
  --common message structure (hostID clientID MESSAGE)
  
  if isHost == false and appLoaded == false then
    if hostID == "0" then
      --send getinfo packet
      local dg = composeMessage(hostID,clientID,"GETINFO","XXX")
      udp:send(dg)
    else
      --send connect packet
      local dg = composeMessage(hostID,clientID,"CONNECT","XXX")
      udp:send(dg)
    end
  end
  
  if appLoaded == true then
    for k,v in pairs(sendMessageList) do
      if isHost == true then
        for l,w in pairs(clientList) do
          local dg = composeMessage(hostID,w[1],v[1],v[2])
          udp:sendto(dg,w[2],w[3])
        end
      else
        if hostID ~= "0" then
          local dg = composeMessage(hostID,clientID,v[1],v[2])
          udp:send(dg)
        end
      end
    end
    sendMessageList={}
    if isHost == false and hostID ~= "0" then
      local dg = composeMessage(hostID,clientID,"PING","XXX")
      udp:send(dg)
    end
  end
 
  
  if isHost == true and appLoaded == true then
    repeat
      data, msg_or_ip, port_or_nil = udp:receivefrom()
      if data then
        local recHostID, recClientID, recMessage = data:match("^(%-?[%d.e]*) (%-?[%d.e]*) (.*)")
        if recMessage ~= nil then
          local recMessageType, recMessageData = recMessage:match("^(%S*) (.*)")
          if recMessageType == "GETINFO" then
            --Send game info to client
            local dg = composeMessage(hostID,recClientID,"INFO",appname .. appversion)
            udp:sendto(dg,msg_or_ip,port_or_nil)
          else
            --Only process messages for this host (not from it!)
            if recHostID == hostID and recClientID ~= clientID then
              if recMessageType == "CONNECT" then
                --Connect a new client
                local found=false
                for k,v in pairs(clientList) do
                  if v[1] == recClientID then 
                    found = true 
                    break
                  end
                end
                if found == false then
                  table.insert(clientList,{recClientID,msg_or_ip,port_or_nil,0})
                  local dg = composeMessage(hostID,recClientID,"CONNECTED","XXX")
                  udp:sendto(dg,msg_or_ip,port_or_nil)
                end
              elseif recMessageType == "PING" then
                --Received ping message from client
                for k,v in pairs(clientList) do
                  if v[1] == recClientID then
                    v[4]=0
                    break
                  end
                end
              else
                --Relay game messages to clients
                for k,v in pairs(clientList) do
                  if v[1] ~= recClientID then
                    local dg = composeMessage(hostID,v[1],recMessageType,recMessageData)
                    udp:sendto(dg,v[2],v[3])
                  end
                end
                --Save game message for local player
                table.insert(receiveMessageList,{recMessageType, recMessageData})
              end
            end
          end
        end
      end
    until not data
  end
  
  if isHost == false then
    repeat
      data, msg = udp:receive()
      --TODO handle disconnected server
      if data then
        local recHostID, recClientID, recMessage = data:match("^(%-?[%d.e]*) (%-?[%d.e]*) (.*)")
        if recMessage ~= nil then
          local recMessageType, recMessageData = recMessage:match("^(%S*) (.*)")
          if appLoaded == false then
            if recClientID == clientID then
              if recMessageType == "INFO" then
                --TODO check info against existing apps
                hostID = recHostID
              elseif recMessageType == "CONNECTED" then
                loadApp(appPath,playerName)
              end
            end
          else
            --Save game message for local player
            table.insert(receiveMessageList,{recMessageType, recMessageData})
          end
        end
      end
    until not data
  end
end

function getIsHost()
  return isHost
end

