-- test script for ELua/Lunatic Interpreter

--do return end

-- error=nil -- must not affect "require"
local string = require("string")
local bit = require("bit")
local math = require("math")

local pcall = pcall
local DBG_ = DBG_

print('---=== ELua Test script ===---')

local function printf(fmt, ...)
    print(string.format(fmt, ...))
end

local function checkfail(funcstr, expectedmsg)
    local status, errmsg = pcall(DBG_.loadstring(funcstr))
    if (status) then
        print('^21ERROR:^O '..funcstr.." DIDN'T fail")
    else
        if (expectedmsg==nil or string.find(errmsg, expectedmsg, 1, true)) then
            print("^11SUCCESS:^O "..funcstr.." failed: "..errmsg)
        else
            -- XXX: beginning with "^10" is counted as error in OSD_Printf()
            print("^10ERROR*:^O "..funcstr.." failed: "..errmsg..
                  ", but expected error message was: "..expectedmsg)
        end
    end
end


---- check serialization ----
gamevar("ourvar")
gamevar("ourvar2")

-- test nans, infs, precision, subnorms, booleans
ourvar2 = { "asd"; 0/0, 1/0, -1/0, 0.12345678901234567, 1e-314, true }
ourvar = { ourvar2; 1, 2, 3, "qwe"; [true]=0, [false]=1 }
ourvar[#ourvar+1] = ourvar;

local gvstr = DBG_.serializeGamevars()
ourvar = -1

print("========== attempting to load string: ==========")
print(gvstr)
print("---------- (end string to load) ----------")

-- XXX: need to think about fully restoring state
DBG_.loadGamevarsString(gvstr)
print("ourvar[4]="..ourvar[4])


print('tweaking sector pals')
print('numsectors: ' .. gv.numsectors .. ' of ' .. gv.MAXSECTORS)

---[[
for i = 0, gv.numsectors/2 do
    sector[i].floorpal = 1;
    sector[i].ceilingpal = 2;
end

local vol, lev = gv.currentEpisode()+1, gv.currentLevel()+1
printf('volume=%d, level=%d', vol, lev)

if (vol==1 and lev==1) then  -- E1L1
    print('tweaking some sprites 2')
    local i = 562
    spriteext[i].alpha = 0.5;
    sprite[i].cstat = bit.bor(sprite[i].cstat, 2+512);
    spriteext[i].pitch = 128;
    spriteext[i].roll = 256;

    i = 107  -- pistol ammo at rooftop
    spriteext[i].pitch = 128;
    spriteext[i].roll = 256;

    for spr in spritesofsect(307) do  -- some fence sprites in E1L1
        printf('spr %d', spr)
        sprite[spr].pal = 6
    end

    --this is a problem
    --actor = {}
    actor[562].flags = bit.bor(actor[562].flags, 2);   -- pal 6 with goggles on front SEENINE
end

--[[
-- TODO: better API for all TROR things?
if (vol==1 and lev==8) then
    print('tweaking bunch 1');
    -- trueror1.map
    for i in sectorsofbunch(1, gv.CEILING) do
        sector[i].ceilingz = sector[i].ceilingz - 3*1024;
    end
    for i in sectorsofbunch(1, gv.FLOOR) do
        sector[i].floorz = sector[i].floorz - 3*1024;
    end
end
--]]

local unsafe = pcall(function() string.UNSAFE=true; end)

--]]
--tostring = nil  -- REMEMBER
--DBG_.printkv('_G in test.elua', _G)

-- direct gv array access forbidden
checkfail('gv.sprite[0].yrepeat = 100', "access forbidden")

checkfail('print(sprite[100000].ceilingpal)', "out-of-bounds sprite[] read access")

checkfail('print(gv.sprite[0])', "access forbidden")

-- set metatable forbidden
checkfail('setmetatable(sprite, {})', "attempt to read undeclared variable 'setmetatable'")

-- OOB write access.
-- Note that indexing ("reading") sector fails, even if the user wants to
-- assign to a sector member. Potentially confusing error message.
checkfail('sector[-1].ceilingpal = 4', "out-of-bounds sector[] read access")

-- wallnum member is read-only
checkfail('sector[0].wallnum = 0', "attempt to write to constant location")  -- this comes from LJ/FFI

-- gv.numsectors is read-only
checkfail('gv.numsectors = 4', "attempt to write to constant location")

-- cannot create new fields in 'gv'
checkfail('gv.QWE = 4', "write access forbidden")

-- direct sector write access forbidden
checkfail('sector[4] = sector[6]', "cannot write directly to sector[]")

-- that would be horrible...
checkfail('sprite._nextspritesect[4] = -666', "cannot write directly to nextspritesect[]")

-- we're indexing a plain array!
checkfail('print(sprite._nextspritesect[4].whatfield)', "attempt to index a number value")

-- creating new keys forbidden... handled by LuaJIT
checkfail('wall[4].QWE = 123', "has no member named 'QWE'")

-- our 'require' has only safe stuff
--checkfail("require('os')")

-- we must declare globals with 'gamevar'
checkfail("new_global = 345", "attempt to write to undeclared variable 'new_global'")

-- can't redefine constants in 'gv'
checkfail('gv.CEILING = 3', "attempt to write to constant location")

-- string.dump is unavailable
checkfail('local s=require[[string]]; local tmp=s.dump(gameevent)',
          "attempt to call field 'dump' (a nil value)")

if (not unsafe) then
    -- changing base module tables is disallowed
    checkfail('local s=require[[string]]; s.format=nil', "modifying base module table forbidden")
else
    print('WARNING: RUNNING WITH UNPROTECTED BASE MODULES')
end

print('')
-- This is problematic, even though pretty much every access will yield a
-- "missing declaration" error.
-- See http://luajit.org/ext_ffi_api.html#ffi_C about what stuff ffi.C contains.
checkfail('gv.luaJIT_setmode(nil, 0, 0)', "missing declaration for symbol 'luaJIT_setmode'")

checkfail('gv.luaJIT_BC_con_lang', "attempt to call a nil value")
checkfail('gv.yax_getbunch(0,0)', "access forbidden")
checkfail('gv.gethitickms = nil', "attempt to write to constant location")

-- actor[].t_data[] is not accessible for now
checkfail('local i = actor[0].t_data[15]', "has no member named 't_data'")

-- no pointer arithmetic!
checkfail('local spr = sprite[0]; local x=spr+1', "attempt to perform arithmetic on")

checkfail('gameactor(1680, 0)', "invalid last argument to gameactor: must be a function")

checkfail("do local bt=require'bittest'; bt.QWE=1; end", "modifying module table forbidden")
-- the cdata returned by player[] can't be made into a pointer!
checkfail("do local pl=player[0]; i=pl[1]; end")
checkfail("do local ud=gv.ud.camerasprite; end", "access forbidden")  -- test for proper decl()
checkfail("sprite[0]:set_picnum(-10)", "invalid tile number")
checkfail("gv.g_sizes_of=nil; print(gv.g_sizes_of[0])", "write access forbidden")
checkfail("gv.cam.sect=-1", "invalid sector number")
checkfail("local flag=gv.SFLAG_NULL", "missing declaration")

player[0].wackedbyactor = -1  -- should succeed
checkfail("player[0].curr_weapon = -1", "Invalid weapon ID")
player[0].curr_weapon = 1

printf('ceilingbunch of sector 0: %d', getbunch(0, gv.CEILING))

gameevent(gv.EVENT_JUMP,
          function(actori, playeri, dist)
              printf("jump i=%d p=%d d=%d", actori, playeri, dist)
              error("greetings from EVENT_JUMP")
          end
         )

gameevent("PROCESSINPUT",
          -- Input test.
          -- NOTE: I don't think that exposing g_player[].sync (aka "input") is a good idea...
          function(actori, playeri, dist)
              local IB = player.INPUT_BITS
              local input = player[playeri]._input
              if (bit.band(input.bits, IB.JUMP) ~= 0) then
                  print("JUMPED")
                  -- ... because for example this doesn't work
                  -- (P_HandleSharedKeys, where the JETPACK bit is tested, runs
                  -- before P_ProcessInput):
                  input.bits = bit.bor(input.bits, IB.JETPACK)
              end
          end
         )

-- test event chaining
gameevent("JUMP", actor.FLAGS.chain_beg,
          function(actori, playeri, dist)
              local ps = player[playeri]
              print("I'm first!")
--              DBG_.oom()
              ps.weapon.PISTOL.shoots = 2605 -- RPG
              ps.weapon[gv.PISTOL_WEAPON].firesound = 351 -- thunder

              -- XXX: provide either named constants or methods?
              ps.pipebombControl = 2
              ps.tripbombControl = 2

              -- Test of INTERNAL member _pals.
              -- NOTE: setting colors partially is bad! E.g. after an item is
              -- picked up, col[0] and col[1] remain and tint everything greenish.
              ps._pals[2] = 20
              ps._pals.f = 30
          end
         )

gameevent(gv.EVENT_ENTERLEVEL,
          function()
              if (gv._DEBUG_LUNATIC) then
                  DBG_.testmembread()
              end

              -- NOTE: times are for helixhorned (Core2Duo 3GHz)
              local i
              local N = 1e6
              local t = gv.gethitickms()

              for i=3,N do
                  gv.gethitickms()
              end

              t = gv.gethitickms()-t

              -- x86_64: 35ns/call, x86: 280 ns/call
              -- Windows 32-bit: about 1 us/call?
              printf("%d gethitickms() calls took %.03f ms (%.03f us/call)",
                     N, t, (t*1000)/N)

              local sum=0
              t = gv.gethitickms()
              for i=1,N do sum = sum+gv.ksqrt(i) end
              t = gv.gethitickms()-t
              -- x86_64: 14ns/call
              printf("%d ksqrt() calls took %.03f ms (%.03f us/call) [sum=%f]",
                     N, t, (t*1000)/N, sum)

              sum=0
              t = gv.gethitickms()
              for i=1,N do sum = sum+math.sqrt(i) end
              t = gv.gethitickms()-t
              -- x86_64: 7ns/call
              printf("%d math.sqrt() calls took %.03f ms (%.03f us/call) [sum=%f]",
                     N, t, (t*1000)/N, sum)

              printf("sqrt(0xffffffff) = %f(ksqrt) %f(math.sqrt)",
                     gv.ksqrt(0xffffffff), math.sqrt(0xffffffff))

              local pl = player[0]
              -- MAX < current is "allowed"
              pl.max_ammo_amount[gv.RPG_WEAPON] = 17
              pl:give_weapon(gv.RPG_WEAPON)
              pl.ammo_amount[gv.RPG_WEAPON] = 54

              pl:give_weapon(gv.SHRINKER_WEAPON)
              -- This looks much prettier:
              pl.ammo_amount.SHRINKER = 2

              -- MORTER2 from test/weaponvars.con
              player[0].weapon.SHOTGUN.shoots = 1653
              local proj = projectile[1653]
              proj.drop = 0
              proj:set_trail(2329)  -- SMALLSMOKE

              t = gv.gethitickms()
              local N=1
              for n=1,N do
                  for i=0,gv.MAXSPRITES-1 do
                      sprite[i].filler = 1
                  end
                  for i=gv.MAXSPRITES-1,0,-1 do
                      sprite[i].shade = 1
                  end
                  for i=0,gv.MAXSPRITES-1 do
                      sprite[i].xoffset = 0
                  end
                  for i=gv.MAXSPRITES-1,0,-1 do
                      sprite[i].yoffset = 1
                  end
              end
              t = gv.gethitickms()-t
              printf("%d x four 0..MAXSPRITES-1 iterations took %.03f us per outer iteration", N, (1000*t)/N)
              -- Results on x86:
              -- N=1: 480-1000 us (too large variance)
              -- N=10: 190-210 us * 10 = 1.9-2.1 ms
              -- N=100: about 160 us * 100 = about 16 ms

              -- Make the DUKECAR in E1L1 into a zombie actor (temporarily)
              if (sprite[24].picnum==2491) then
                  sprite.changestat(24, gv.STAT_ZOMBIEACTOR)
              end

              checkfail("gameevent('GAME', function() print('qwe') end)",
                        "must be called from top level")
          end
         )

local geom = require "geom"

gameevent("LOADACTOR", function(i)
    local spr = sprite[i]
    if (i==614 and spr.picnum==930) then
        -- "police line" ribbon in E1L1
        -- clear the hitag so that it doesn't spawn as FALLER from premap
        spr.hitag = 0
    end
end)

gameactor(930, nil, 1, function(i)  -- "police line" ribbon
    local spr = sprite[i]
    local r = math.random
    local d = 20
    -- NOTE: __add metamethod is called because of the RHS:
    local v = spr + geom.vec3(r(-d,d), r(-d,d))
    spr:setpos(v)
end)

local stat = require("stat")
local hs = stat.new()

local con = require("con")
local AC, MV = con.AC, con.MV
con.action("TROOPSTAND",0,1,5,1,1)
con.action("TROOPFLINTCH", 50, 1, 1, 1, 6)
con.move("SHRUNKVELS", 32)
con.ai("AITEMP", AC.TROOPFLINTCH, MV.SHRUNKVELS, 0)  -- TODO: test
-- This should work as well. In fact, it's exactly the same, but I prefer the
-- above version for clarity.  NOTE: we'll need to think whether it's OK to
-- redefine composites (moves/actions/ais) during gameplay (probably not).
-- Or will we allow only one definition per label ever?
con.ai("AITEMP", "TROOPFLINTCH", "SHRUNKVELS", 0)

local TROOPSTRENGTH = 30

local AF = actor.FLAGS
local CS = sprite.CSTAT

-- Also test actor code chaining: strength is doubled.
gameactor(1680, AF.chain_end+AF.enemy, 2*TROOPSTRENGTH, "TROOPSTAND", -- LIZTROOP
          function(i, playeri, dist)
              spriteext[i]:make_animated()

              sprite[i].pal = math.random(32)
--              sprite[i].ang = bit.band(sprite[i].ang-20, 2047)

              local spr = sprite[i]

              local t = gv.gethitickms()
              local hit = hitscan(spr, spr.sectnum, 10, 10, 0, gv.CLIPMASK0)

              hs:add(1000*(gv.gethitickms()-t))

              if (hs.n == 300) then
                  printf("hitscan: %s", hs:getstatstr())
                  hs:reset()
                  error("greetings from LIZTROOP actor")
              end

              local actr = actor[i]
              if (actr:get_count() % 30 == 0) then
                  spr.cstatx:flip(CS.YFLIP)
              end

              actr.flagsx:test(AF.NVG)  -- test of bitint's ":test()" for actor[].flags

              if (dist < 4096) then
                  -- Duke Vader / Anakin Nukewalker?
                  actor[i]:set_action("TROOPFLINTCH")
                  actor[i]:set_move("SHRUNKVELS")

                  if (dist < 1024) then
                      con.killit()
                  end
              end
          end
         )

gameevent("DISPLAYROOMS",
          function()
              local cam = gv.cam
              cam.pos.z = cam.pos.z + 64*16*math.sin(gv.totalclock/30)

              local ps = player[0]
              if (ps.cursectnum >= 0) then
                  local hit = sector[ps.cursectnum]:zrangeat(cam.pos)
                  if (gv.totalclock%200==0) then
                      printf("hit %s %d at z=%d, %s %d at z=%d",
                             hit.c.spritep and "sprite" or "sector", hit.c.num, hit.c.z,
                             hit.f.spritep and "sprite" or "sector", hit.f.num, hit.f.z)
                  end
              end
          end
         )

gameevent("DISPLAYREST", function()
    for i=0,10 do
        for j=1,100 do
            -- XXX: This is slower in the Polymodes than with classic!
--            con._gametext(2822, j, i, 12, 0, 0, 16, 0,0,gv.xdim,gv.ydim,8192)
        end
    end

    -- Clear showing every sector with the pavement floor tile. (Unless we're
    -- in such a sector or an adjoining one.)
    -- XXX: We need a better place to do this, maybe an event in
    -- G_DisplayRest() where show2dsector[] is tweaked?
    local show2dsector = sector.showbitmap
    for i=0,gv.numsectors-1 do
        if (sector[i].floorpicnum==815) then
            show2dsector:set0(i)
        end
    end
end)

-- APLAYER
gameactor(1405, actor.FLAGS.chain_beg, function(aci, pli)
    if (con._squished(aci, pli)) then
        printf("Squished LunaCON test")
    end
end)


gameevent("ANIMATESPRITES",
function(aci)
    local tspr = atsprite[aci]
    if (tspr:getspr().picnum==1680) then
        local tspr2 = tspr:dup()
        if (tspr2) then
            tspr2.x = tspr2.x + 512*math.cos(gv.totalclock/60)
            tspr2.y = tspr2.y + 512*math.sin(gv.totalclock/60)
            tspr2.cstatx:set(CS.TRANSLUCENT_BOTH_BITS)
        end
    end
end)

printf("EVENT_INIT = %d", gv.EVENT_INIT)  -- tests default defines

local bittest = require "bittest"
bittest.sieve()

require("test/test_geom")
require("test/test_rotspr")

do
    -- Test ksin vs. sinb
    local xmath = require "xmath"
    local sum = 0

    local N = 1000
    local t = gv.gethitickms()
    for i=0,N*2048-1 do
        sum = sum+xmath.ksin(i)
    end
    t = gv.gethitickms()-t
    sum = sum*1e12
    printf("ksin: sum*1e12=%.03f, %.03fus per 0-2047 cycle", sum, t)

    sum = 0
    local t = gv.gethitickms()
    for i=0,N*2048-1 do
        sum = sum+xmath.sinb(i)
    end
    t = gv.gethitickms()-t
    sum = sum*1e12
    printf("sinb: sum*1e12=%.03f, %.03fus per 0-2047 cycle", sum, t)

    -- Results (helixhorned x86):
    -- ksin: sum*1e12=0.000, 3.801us per 0-2047 cycle
    -- sinb: sum*1e12=0.052, 2.886us per 0-2047 cycle
end

print('---=== END TEST SCRIPT ===---')

--[[
function check_sector_idx()
    error("bla")
end
spritesofsect(0)
--]]

--DBG_.oom()

-- This will complain about wrong usage of 'error'. In particular,
-- the nil must not propagate to C!
error(nil)