-- LUALOCALS < ---------------------------------------------------------
local include, nodecore, minetest
    = include, nodecore, minetest
-- LUALOCALS > ---------------------------------------------------------

local modname = minetest.get_current_modname()
local S = minetest.get_translator(modname)

ctf = {}

ctf.S = S

local cfg = include("config")
local teams = cfg.teams

local CRYSTALS_PER_TEAM = cfg.CRYSTALS_PER_TEAM
local PLAYERS_PER_TEAM = cfg.PLAYERS_PER_TEAM
local SPAWNPOINT = cfg.SPAWNPOINT
local SPAWN_OFFSET = cfg.SPAWN_OFFSET
local FIELD_RADIUS = cfg.FIELD_RADIUS
local BORDER_RADIUS = cfg.BORDER_RADIUS
local SPAWN_DIST = cfg.SPAWN_DIST
local TEAM_DIST = cfg.TEAM_DIST
local PLAYER_DIST = cfg.PLAYER_DIST
local SPAWNFALL_LIMIT = cfg.SPAWNFALL_LIMIT
local TELEPORT_ROOM = cfg.TELEPORT_ROOM
local TELEPORT_ROOM_RADIUS = cfg.TELEPORT_ROOM_RADIUS

ctf.player_count = 0
ctf.voted_count = 0
ctf.players = {}
ctf.spectators = {}
ctf.teams = {}
ctf.started = false
ctf.starting = false
ctf.ended = false

do
  
local f = math.floor

local function sign(x)
  return x > 0 and 1 or (x < 0 and -1 or 0)
end

local function tspawn(name,pl)
  if pl.team then
    local pls = {}
    for k,v in pairs(ctf.teams[pl.team].players) do
      if v and k ~= name then
        table.insert(pls,minetest.get_player_by_name(k):get_pos())
      end
    end
    if #pls > 0 then
      ctf.spawn(name,pls[math.random(1,#pls)])
    else
      ctf.spawn(name,vector.add(ctf.spawnpoint,SPAWN_OFFSET))
    end
  else
    ctf.spawn(name,vector.add(ctf.spawnpoint,SPAWN_OFFSET))
  end
end

function ctf.spectate(ref)
  local name = ref:get_player_name()
  if ctf.players[name] then
    local hadteam = false
    if ctf.players[name].team and ctf.teams[ctf.players[name].team] then
      ctf.teams[ctf.players[name].team].players[name] = nil
      hadteam = true
    end
    ctf.players[name].team = nil
    if ctf.players[name].voted then
      ctf.voted_count = ctf.voted_count-1
    end
    ctf.players[name].voted = false
    if ctf.starting == math.huge then
      ctf.starting = -math.huge
    end
    ctf.spectators[name] = ctf.players[name]
    ctf.players[name] = nil
    ctf.explode_inv(ref)
    ctf.player_count = ctf.player_count - 1
    ctf.set_text(name,S"You're a spectator")
    if not hadteam then
      tspawn(name,ctf.spectators[name])
    end
    return true
  end
  return false
end

function ctf.unspectate(ref)
  local name = ref:get_player_name()
  if ctf.spectators[name] then
    ctf.players[name] = ctf.spectators[name]
    ctf.spectators[name] = nil
    ctf.player_count = ctf.player_count + 1
    ctf.explode_inv(ref)
    ctf.spectators[name] = nil
    if ctf.started then
      ctf.dist_player(name)
      ctf.set_team_text(name)
    else
      ctf.set_text(name,S"The game hasn't started yet".."\n"..S"/vote to end the game")
    end
    tspawn(name,ctf.players[name])
    return true
  end
  return false
end

function ctf.spawn(name,pos,call)
  local ref = minetest.get_player_by_name(name)
  local pl = ctf.players[name] or ctf.spectators[name]
  local over = pl.over or ref:get_physics_override()
  ref:set_pos(vector.add(TELEPORT_ROOM,{x=math.random(-8,8),y=math.random(-2,2),z=math.random(-8,8)}))
  local n = math.random()
  if pl.transid then
    minetest.chat_send_player(name,S"<TELEPORT> Cancelling previous transportation")
  else
    do
      pl.over = over
      ref:set_physics_override({
        gravity = 1/16,
      })
      ref:set_sky(0x000000, "plain", nil, false)
      local r = TELEPORT_ROOM_RADIUS
      local tr = TELEPORT_ROOM
      local v = {x=r,y=r,z=r}
      local p1,p2 = vector.subtract(tr,v),vector.add(tr,v)
      p1 = {x=f(p1.x/16),y=f(p1.y/16),z=f(p1.z/16)}
      p2 = {x=f(p2.x/16),y=f(p2.y/16),z=f(p2.z/16)}
      for x=p1.x,p2.x do
        for y=p1.y,p2.y do
          for z=p1.z,p2.z do
            ref:send_mapblock({x=x,y=y,z=z})
          end
        end
      end
    end
  end
  pl.transid = n
  pos = {x=f(pos.x),y=f(pos.y),z=f(pos.z)}
  call = call or function()end
  local limit = SPAWNFALL_LIMIT
  minetest.chat_send_player(name,S"<TELEPORT> Configuring transportation processor...")
  --minetest.after(0,function()
    minetest.emerge_area({x=pos.x,y=pos.y-limit,z=pos.z},{x=pos.x,y=pos.y+limit,z=pos.z},function(bp,act,crem)
      if crem > 0 then return end
      pl = ctf.players[name] or ctf.spectators[name]
      if not pl then return end
      if pl.transid ~= n then
        return
      end
      pl.transid = nil
      pl.over = nil
      minetest.chat_send_player(name,S"<TELEPORT> Done. Sending transport coordinates...")
      ref:set_sky(0x000000, "regular")
      ref:set_physics_override(over)
      local off = 0
      while off > -limit do
        if minetest.get_node({x=pos.x,y=pos.y+off,z=pos.z}).name ~= "air" then
          off = off + 1
          break
        end
        off = off - 1
      end
      while off < limit do
        if minetest.get_node({x=pos.x,y=pos.y+off,z=pos.z}).name == "air" then
          break
        end
        off = off + 1
      end
      local apos = {x=pos.x,y=pos.y+off,z=pos.z}
      local ref = minetest.get_player_by_name(name)
      if ref then
        ref:set_pos(apos)
        call(ref,apos)
      end
    end)
  --end)
end

end

minetest.register_on_chat_message(function(name, message)
  local team = ctf.players[name].team
  if team and ctf.teams[team] and ctf.started and not ctf.ended then
    if message:sub(1,1) ~= "." then
      local msg = minetest.format_chat_message(name,message)
      minetest.chat_send_all(msg)
    else
      local msg = minetest.colorize(teams[team].colorstr, minetest.format_chat_message(name,message:sub(2)))
      for k,v in pairs(ctf.teams[team].players) do
        minetest.chat_send_player(k,msg)
      end
    end
    return true
  end
end)

minetest.register_chatcommand("vote",{
  desc = S"Vote to do things",
  func = function(name,param)
    if ctf.ended then
      minetest.chat_send_player(name,S"<DEMOCRACY> Nothing to vote for")
      return
    end
    if not ctf.started and ctf.player_count < 2 then
      minetest.chat_send_player(name,S"<DEMOCRACY> Not enough players")
      return
    end
    local pl = ctf.players[name]
    pl.voted = not pl.voted
    ctf.voted_count = pl.voted and (ctf.voted_count+1) or (ctf.voted_count-1)
    if ctf.started then
      if pl.voted then
        minetest.chat_send_player(name,S"<DEMOCRACY> You vote to end the game")
      else
        minetest.chat_send_player(name,S"<DEMOCRACY> You vote to NOT end the game")
      end
    else
      if pl.voted then
        minetest.chat_send_player(name,S"<DEMOCRACY> You vote to start the game")
      else
        minetest.chat_send_player(name,S"<DEMOCRACY> You vote to NOT start the game")
      end
    end
  end
})

minetest.register_chatcommand("spectate",{
  desc = S"Start spectating",
  func = function(name,param)
    ctf.spectate(minetest.get_player_by_name(name))
  end
})

minetest.register_chatcommand("unspectate",{
  desc = S"Stop spectating",
  func = function(name,param)
    ctf.unspectate(minetest.get_player_by_name(name))
  end
})

include("node")
ctf.crystals(teams)

local bar_c = minetest.get_content_id(modname..":barrier")
local tbar_c = minetest.get_content_id(modname..":telebarrier")
local air_c = minetest.get_content_id("air")

local abs = math.abs
local function distance(a,b)
  return math.max(abs(a.x-b.x),abs(a.z-b.z))
end
local function rdistance(a,b)
  return math.max(abs(a.x-b.x),abs(a.z-b.z),abs(a.y-b.y))
end
local f = math.floor

minetest.register_on_generated(function(minp,maxp,seed)
  local vm,mip,map = minetest.get_mapgen_object("voxelmanip")
  local d = vm:get_data()
  local ar = VoxelArea:new{MinEdge=mip,MaxEdge=map}
  if vector.distance(TELEPORT_ROOM,vector.divide(vector.add(minp,maxp),2)) < 4000 then
    local tr = TELEPORT_ROOM
    for x=minp.x,maxp.x do
      for y=minp.y,maxp.y do
        for z=minp.z,maxp.z do
          local i = ar:index(x,y,z)
          if tr.y-y > 32 or
            (f(y/16) < f(tr.y/16) and rdistance({x=x,y=0,z=z},{x=tr.x,y=0,z=tr.z}) <= (f(tr.y/16)-f(y/16)+0.5)*16)then
            d[i] = tbar_c
          else
            d[i] = air_c
          end
        end
      end
    end
  else
    if distance(vector.divide(vector.add(minp,maxp),2),ctf.spawnpoint) > FIELD_RADIUS then
      for x=minp.x,maxp.x do
        for y=minp.y,maxp.y do
          for z=minp.z,maxp.z do
            local i = ar:index(x,y,z)
            d[i] = bar_c
          end
        end
      end
    end
  end
  vm:set_data(d)
  vm:calc_lighting()
  vm:write_to_map(false)
end)

function ctf.set_team_text(name)
  local pl = ctf.players[name]
  assert(pl)
  local team = teams[pl.team]
  ctf.set_text(name,S("You're member of @1 team",tostring(team.desc)).."\n"..S"/vote to end the game",team.colornum)
end

function ctf.join(team,name)
  if ctf.players[name].team and ctf.teams[ctf.players[name].team].players[name] then
    ctf.teams[ctf.players[name].team].players[name] = nil
  end
  ctf.teams[team].players[name] = true
  ctf.players[name].team = team
end

function ctf.dist_player(name)
  local m,t = math.huge,nil
  for team,teamd in pairs(ctf.teams) do
    local c = 0
    for namee,bool in pairs(teamd.players) do
      if bool and namee ~= name then
        c=c+1
      end
    end
    if c < m or (c == m and math.random()>0.5) then
      m = c
      t = team
    end
  end
  ctf.join(t,name)
end

minetest.register_on_joinplayer(function(ref)
  local name = ref:get_player_name()
  local id = ref:hud_add{
    name = "stat",
    text = S"The game hasn't started yet".."\n"..S"/vote to start the game",
    number = 0xFFFFFF,
    alignment = {x=-1,y=1},
    offset = {x=-20,y=20},
    position = {x=1,y=0},
  }
  ctf.spectators[name] = {}
  ctf.spectators[name].hud = {stat = id}
  ctf.explode_inv(ref)
  
  ctf.unspectate(ref)
end)

function ctf.explode_inv(ref)
  local inv = ref:get_inventory()
  local pos = ref:get_pos()
  local list = inv:get_list("main")
  inv:set_list("main",{})
  for k,v in pairs(list) do
    if not nodecore.item_is_virtual(v) and not v:is_empty() then
      nodecore.item_eject(pos,v,5)
    end
  end
end

function ctf.set_text(name,text,colornum)
  local pl = ctf.players[name] or ctf.spectators[name]
  if pl and pl.hud then
    local ref = minetest.get_player_by_name(name)
    ref:hud_change(pl.hud.stat,"text",text)
    ref:hud_change(pl.hud.stat,"number",colornum or 0xFFFFFF)
  end
end

minetest.register_on_leaveplayer(function(ref)
  local name = ref:get_player_name()
  ctf.spectate(ref)
  ctf.spectators[name] = nil
end)

function ur(n)
  return (math.random()-0.5)*n
end

do
    local z = SPAWN_DIST
    local pos = vector.add(SPAWNPOINT,{x=ur(z),y=0,z=ur(z)})
    ctf.spawnpoint = pos
end
  
function ctf.start()
  if ctf.ended or ctf.started then
    return
  end
  if ctf.starting and ctf.starting ~= -math.huge and (ctf.voted_count/ctf.player_count) < 0.5 then
    minetest.chat_send_all(S"<INFO + DEMOCRACY> Aborting")
    ctf.starting = -math.huge
    return
  end
  ctf.ended = false
  local tc = math.max(2,math.floor(ctf.player_count/PLAYERS_PER_TEAM))
  if ctf.player_count >= 2 and not ctf.starting and (ctf.voted_count/ctf.player_count) >= 0.5 then
    ctf.starting = minetest.get_server_uptime()+10
    minetest.chat_send_all(S"<INFO + DEMOCRACY> Most players are ready. Starting the game in 10...")
    do
      local n = 10
      local function f()
        if not ctf.starting or ctf.starting == -math.huge then ctf.starting = false return end
        local n2=math.ceil(ctf.starting-minetest.get_server_uptime())
        if n2 ~= n and n2 > 0 then
          n=n2
          minetest.chat_send_all(S("<INFO> @1...",n))
        end
        if n2 > 0 then
          minetest.after(0.2,f)
        end
      end
      f()
    end
  elseif ctf.starting and not (ctf.player_count >= 2) then
    ctf.starting = false
    minetest.chat_send_all(S"<INFO> Not enough players now. Aborting.")
  end
  if ctf.starting and minetest.get_server_uptime() >= ctf.starting then
    ctf.starting = math.huge
    minetest.chat_send_all(S"<TELEPORT + INFO> CONFIGURING TRANSPORTATION PROCCESSORS FOR EVERYONE . . .")
    local c = 0
    ctf.teams = {}
    for k,v in pairs(teams) do
      c=c+1
      ctf.teams[k] = {players = {},score = CRYSTALS_PER_TEAM}
      if c >= tc then
        break
      end
    end
    for name,pl in pairs(ctf.players) do
      ctf.explode_inv(minetest.get_player_by_name(name))
      ctf.dist_player(name)
      ctf.set_team_text(name)
    end
    local z = SPAWN_DIST
    local pos = ctf.spawnpoint
    pos = vector.add(pos,SPAWN_OFFSET)
    local pc = 0
    local call = function(ref,pos)
      pc = pc + 1
      if pc < ctf.player_count then return end
      if ctf.starting ~= math.huge then
        minetest.chat_send_all(S"<TELEPORT + INFO> SOMETHING HAPPENED. ABORTING")
        ctf.teams = {}
        for k,v in pairs(ctf.players) do
          v.team = nil
          ctf.set_text(k,S"The game hasn't started yet".."\n"..S"/vote to start the game")
        end 
        ctf.starting = false
        ctf.started = false
        ctf.ended = false
        return
      end
      ctf.starting = false
      ctf.started = true
      for k,v in pairs(ctf.players) do
        if v.voted then
          v.voted = false
          ctf.voted_count = ctf.voted_count - 1
        end
      end
      minetest.chat_send_all(S"<INFO> The game begins!")
      for k,v in pairs(ctf.players) do
        ctf.explode_inv(minetest.get_player_by_name(k))
      end 
      for tname,team in pairs(ctf.teams) do
        local crystals = CRYSTALS_PER_TEAM
        while crystals > 0 do
          local b = false
          for name in pairs(team.players) do
            local ref = minetest.get_player_by_name(name)
            if crystals > 0 then
              crystals = crystals - 1
              b=true
              ref:get_inventory():add_item("main",modname..":crystal_"..tname)
            else
              break
            end
          end
          if not b then
            break
          end
        end
      end
    end
    for tname,team in pairs(ctf.teams) do
      local x = TEAM_DIST
      local pos = vector.add(pos,{x=ur(x),y=0,z=ur(x)})
      for name in pairs(team.players) do
        local y = PLAYER_DIST
        local pos = vector.add(pos,{x=ur(y),y=0,z=ur(y)})
        local ref = minetest.get_player_by_name(name)
        ctf.explode_inv(minetest.get_player_by_name(name))
        ctf.spawn(name,pos,call)
      end
    end
  end
end

do
  local f
  local emerging = false
  local t = {
    [minetest.EMERGE_CANCELLED]="CANCEL",
    [minetest.EMERGE_ERRORED]="ERROR",
    [minetest.EMERGE_FROM_MEMORY]="MEMORY",
    [minetest.EMERGE_FROM_DISK]="DISK",
    [minetest.EMERGE_GENERATED]="GENERATED",
  }
  function f()
    local r = TELEPORT_ROOM_RADIUS
    local tr = TELEPORT_ROOM
    local v = {x=r,y=r,z=r}
    if not emerging then
      emerging = true
      local gen = false
      minetest.emerge_area(vector.subtract(tr,v),vector.add(tr,v),function(bp,act,crem)
        if crem == 0 then
          if gen then
            --print("loaded teleport_room "..(t[act] or "UNK"))
          end
          emerging = false
        else
          if t[act] ~= "MEMORY" and not gen then
            --print("loading teleport_room "..(t[act] or "UNK"))
            gen = true
          end
        end
      end)
    end
    if #minetest.get_connected_players() == 0 and not (ctf.started or ctf.ended or ctf.starting) then
      local z = SPAWN_DIST
      local pos = vector.add(SPAWNPOINT,{x=ur(z),y=0,z=ur(z)})
      ctf.spawnpoint = pos
    end
    minetest.after(5,f)
  end
  minetest.after(0,f)
end

do
  local f
  function f()
    ctf.start()
    for name,pl in pairs(ctf.players) do
      if not minetest.check_player_privs(name,{server = true}) then
        if not pl.team then
          ctf.explode_inv(minetest.get_player_by_name(name))
          local privs = minetest.get_player_privs(name)
          privs.fast = true
          privs.fly = true
          privs.interact = nil
          minetest.set_player_privs(name,privs)
        else
          local privs = minetest.get_player_privs(name)
          privs.fast = nil
          privs.fly = nil
          privs.interact = true
          minetest.set_player_privs(name,privs)
        end
      end
    end
    for name,pl in pairs(ctf.spectators) do
      if not minetest.check_player_privs(name,{server = true}) then
        ctf.explode_inv(minetest.get_player_by_name(name))
        local privs = minetest.get_player_privs(name)
        privs.fast = true
        privs.fly = true
        privs.interact = nil
        minetest.set_player_privs(name,privs)
      end
    end
    if ctf.started and not ctf.ended then
      ctf.check_win()
    end
    minetest.after(1,f)
  end
  minetest.after(1,f)
end

function ctf.check_win()
  for name,team in pairs(ctf.teams) do
    if team.score <= 0 then
      ctf.teams[name] = nil
      for pname,pl in pairs(team.players) do
        ctf.set_text(pname,S":(".."\n"..S"Your team lost.",teams[name].colornum)
      end
      minetest.chat_send_all(S("<INFO> Team @1 lost!",teams[name].desc))
    end
  end
  local c = 0
  for k,v in pairs(ctf.teams) do if v then c=c+1 end end
  local vout = (ctf.voted_count/ctf.player_count) >= 0.5
  if c == 1 or vout then
    ctf.ended = true
    if c == 1 then
      local name,team = next(ctf.teams)
      minetest.chat_send_all(S("<INFO> Team @1 won!",teams[name].desc))
      for pname,pl in pairs(team.players) do
        ctf.set_text(pname,S":D".."\n"..S"Your team won!",teams[name].colornum)
      end
    elseif vout then
      minetest.chat_send_all(S"<INFO> Majority of players voted to end the game")
      for k,v in pairs(ctf.players) do
        ctf.set_text(k,S"The game hasn't started yet".."\n"..S"/vote to end the game")
      end
    end
    ctf.teams = {}
    do
      local z = SPAWN_DIST
      local pos = vector.add(SPAWNPOINT,{x=ur(z),y=0,z=ur(z)})
      ctf.spawnpoint = pos
    end
    for k,v in pairs(ctf.players) do
      if v.voted then
        v.voted = false
        ctf.voted_count = ctf.voted_count - 1
      end
      v.team = nil
      ctf.explode_inv(minetest.get_player_by_name(k))
      ctf.spawn(k,vector.add(ctf.spawnpoint,SPAWN_OFFSET))
    end
    for k,v in pairs(ctf.spectators) do
      ctf.spawn(k,vector.add(ctf.spawnpoint,SPAWN_OFFSET))
    end
    minetest.after(5,function()
        minetest.chat_send_all(S"<INFO> The game is over")
        ctf.started = false
        ctf.ended = false
      end)
  end
end
