-- authors: Luxinia Dev (Eike Decker & Christoph Kubisch)
---------------------------------------------------------

local cgbinpath = os.getenv("CG_BIN_PATH")
local cgprofile 
local cgglsles 

return cgbinpath and {
  fninit = function(frame,menuBar)
    cgprofile = ide.config.cgprofile or "gp5"
    cgglsles = ide.config.cgglsles
    cgbinpath = ide.config.path.cgbin or os.getenv("CG_BIN_PATH")

    local myMenu = wx.wxMenu{
      { ID "cg.profile.arb", "&ARB VP/FP", "ARB program profile", wx.wxITEM_CHECK },
      { ID "cg.profile.glsl", "ARB &GLSL", "ARB GLSL program profile", wx.wxITEM_CHECK },
      { ID "cg.profile.nv40", "NV VP/FP&40", "NV program sm3 profile", wx.wxITEM_CHECK },
      { ID "cg.profile.gp4", "NV &GP4", "NV program sm4 profile", wx.wxITEM_CHECK },
      { ID "cg.profile.gp5", "NV &GP5", "NV program sm5 profile", wx.wxITEM_CHECK },
      { ID "cg.profile.dx_2x", "DX SM&2_x", "DirectX sm2_x profile", wx.wxITEM_CHECK },
      { ID "cg.profile.dx_3", "DX SM&3_0", "DirectX sm3_0 profile", wx.wxITEM_CHECK },
      { ID "cg.profile.dx_4", "DX SM&4_0", "DirectX sm4_0 profile", wx.wxITEM_CHECK },
      { ID "cg.profile.dx_5", "DX SM&5_0", "DirectX sm5_0 profile", wx.wxITEM_CHECK },
      { },
      { ID "cg.compile.input", "&Custom Args\tCtrl-L", "when set a popup for custom compiler args will be envoked", wx.wxITEM_CHECK },
      { ID "cg.compile.gles", "GLSL-ES", "When GLSL file is source use GLSL-ES path", wx.wxITEM_CHECK },
      { },
      { ID "cg.compile.vertex", "Compile &Vertex\tCtrl-1", "Compile Vertex program (select entry word)" },
      { ID "cg.compile.fragment", "Compile &Fragment\tCtrl-2", "Compile Fragment program (select entry word)" },
      { ID "cg.compile.geometry", "Compile &Geometry\tCtrl-3", "Compile Geometry program (select entry word)" },
      { ID "cg.compile.tessctrl", "Compile T.Ctrl\tCtrl-4", "Compile T.Ctrl program (select entry word)" },
      { ID "cg.compile.tesseval", "Compile T.Eval\tCtrl-5", "Compile T.Eval program (select entry word)" },
      { ID "cg.compile.compute", "Compile Compute\tCtrl-6", "Compile Compute program (select entry word)" },
      { },
      { ID "cg.format.asm", "Annotate ASM", "indent and add comments to Cg ASM output" },
      { ID "cg.format.master", "Build from master", "Creates a new cg file from a master containing special include instrctions." },
    }
    menuBar:Append(myMenu, "&Cg")

    local data = {}
    data.customarg = false
    data.custom = ""
    data.profid = ID ("cg.profile."..cgprofile)
    data.gles = cgglsles and true or false
    data.domains = {
      [ID "cg.compile.vertex"] = 1,
      [ID "cg.compile.fragment"] = 2,
      [ID "cg.compile.geometry"] = 3,
      [ID "cg.compile.tessctrl"] = 4,
      [ID "cg.compile.tesseval"] = 5,
      [ID "cg.compile.compute"] = 6,
    }
    data.profiles = {
      [ID "cg.profile.arb"] = {"arbvp1","arbfp1",false,false,false,false,ext=".glp", asm=true,},
      [ID "cg.profile.glsl"] = {"glslv","glslf","glslg",false,false,false,ext=".glsl"},
      [ID "cg.profile.nv40"] = {"vp40","fp40",false,false,false,false,ext=".glp",nvperf=true, asm=true,},
      [ID "cg.profile.gp4"] = {"gp4vp","gp4fp","gp4gp",false,false,false,ext=".glp",nvperf=true, asm=true,},
      [ID "cg.profile.gp5"] = {"gp5vp","gp5fp","gp5gp","gp5tcp","gp5tep","gp5cp",ext=".glp", asm=true,},
      [ID "cg.profile.dx_2x"] = {"vs_2_0","ps_2_x",false,false,false,false,ext=".txt"},
      [ID "cg.profile.dx_3"] = {"vs_3_0","ps_3_0",false,false,false,false,ext=".txt"},
      [ID "cg.profile.dx_4"] = {"vs_4_0","ps_4_0","gs_4_0",false,false,false,ext=".txt"},
      [ID "cg.profile.dx_5"] = {"vs_5_0","ps_5_0","gs_5_0","ds_5_0","hs_5_0",false,ext=".txt"},
    }
    data.domaindefs = {
      " -D_VERTEX_ ",
      " -D_FRAGMENT_ ",
      " -D_GEOMETRY_ ",
      " -D_TESS_CONTROL_ ",
      " -D_TESS_EVAL_ ",
      " -D_COMPUTE_ ",
    }
    -- Profile related
    menuBar:Check(data.profid, true)

    local function selectProfile (id)
      for id,profile in pairs(data.profiles) do
        menuBar:Check(id, false)
      end
      menuBar:Check(id, true)
      data.profid = id
    end

    menuBar:Check(ID "cg.compile.gles", data.gles)

    local function evSelectProfile (event)
      local chose = event:GetId()
      selectProfile(chose)
    end

    for id,profile in pairs(data.profiles) do
      frame:Connect(id,wx.wxEVT_COMMAND_MENU_SELECTED,evSelectProfile)
    end

    -- check for NvPerf
    local perfexe = "/NVShaderPerf.exe"
    local fn = wx.wxFileName(cgbinpath..perfexe)
    local hasperf = fn:FileExists()

    -- master file generator

    local function buildFromMaster(filenamein, filenameout)
      local path = GetPathWithSep(filenamein)
      if (not filenameout) then
        local name = filenamein:GetName()
        name = name:match("(.+).master$")
        if (not name) then return end
        filenameout = path..name.."."..filenamein:GetExt()
      end

      local masterfile = io.open(filenamein:GetFullPath(), "rb")
      local outfile = io.open(filenameout, "wb")

      local function out(str)
        --str = string.match(str,"
        return str
      end

      local function handleInclude(fname,defs)
        local defcnt = 0
        for i,v in pairs(defs) do
          defcnt = defcnt + 1
        end

        local incfile = io.open(path..fname, "rb")
        if (defcnt > 0) then
          local write = nil
          for line in FileLines(incfile) do
            if (write) then
              local cap = string.match(line,"#endif%s+//%s*([%w_]+)")
              if (cap == write) then
                outfile:write("//$"..write.." END$\n")
                break
              end
              outfile:write(line)
            else
              local cap = string.match(line,"#ifdef%s+([%w_]+)")
              if (cap and defs[cap]) then
                write = cap
                outfile:write("//$"..write.." BEGIN$\n")
              end
            end
          end
        else
          for line in FileLines(incfile) do
            outfile:write(line)
          end
        end

        incfile:close()
      end

      DisplayOutput("Cg Master Generating...\n")
      local master = nil
      for line in FileLines(masterfile) do
        local masterbegin = string.find(line,'//$MASTER-INCLUDE-BEGIN$',nil, true)
        local masterend = string.find(line,'//$MASTER-INCLUDE-END$', nil, true)
        if (masterbegin) then
          master = {}
          outfile:write(line)
        elseif(masterend) then
          master = nil
        end

        if (master) then
          local linein = " "..line
          local defadd = string.match(linein,'[^/]#define ([_%w]+)')
          local defrem = string.match(linein,'[^/]#undef ([_%w]+)')
          if (defadd) then master[defadd] = true end
          if (defrem) then master[defrem] = nil end
          DisplayOutput(defadd,defrem, "\n")

          local incfile = string.match(linein,'[^/]#include "(.+)"')
          if (incfile) then
            handleInclude(incfile, master, "\n")
          end
        else
          outfile:write(line)
        end
      end
      DisplayOutput("Written:",filenameout,"\n")
      outfile:close()
      masterfile:close()
    end

    local function beautifyAsm(tx)
      local newtx = ""
      local indent = 0
      local maxindent = 0
      local startindent = {
        ["IF"]=true,["REP"]=true,["ELSE"]=true,["LOOP"]=true,["BB"]=true,
      }
      local endindent = {
        ["ENDIF"]=true,["ENDREP"]=true,["ELSE"]=true,["ENDLOOP"]=true,["END"]=true,["RET"]=true,
      }

      local function checknesting(str,tab)
        local res
        local chk = str:match("%s*(BB)%d+.*:")
        chk = chk or str:match("%s*(%w+)")
        res = chk and tab[chk] and chk

        return res
      end

      local argregistry = {}
      local argbuffersfixed = false

      local registercc
      local registermem

      local function fixargbuffers()
        if (argbuffersfixed) then return end

        local argnew = {}
        for i,v in pairs(argregistry) do
          local buf,bufstart = string.match(i,"buf(%d+)%[(%d+)%]")
          if (buf and bufstart) then
            bufstart = tonumber(bufstart)/16
            argnew["buf"..buf.."["..tostring(bufstart).."]"] = v
          else
            argnew[i] = v
          end
        end
        argregistry = argnew
        argbuffersfixed = true
      end

      local function checkregistry(w)
        local regsuccess = true

        local vtype,vname,sem,resource,pnum,pref = string.match(w,"#var ([_%w]+) ([%[%]%._%w]+) : ([^%:]*) : ([^%:]*) : ([^%:]*) : (%d*)")
        local funcname,subroutine = string.match(w,"#function %d+ ([_%w]+)%((%d+)%)")
        if (pref == "1") then
          local descriptor = vtype.." "..vname

          -- check if resource is array
          local resstart,rescnt = string.match(resource,"c%[(%d+)%], (%d+)")
          resstart = tonumber(resstart)
          rescnt = tonumber(rescnt)

          -- check if resource is buffer/buffer array
          local buf,bufstart,bufcnt = string.match(resource,"buffer%[(%d+)%]%[(%d+)%],? ?(%d*)")
          buf = tonumber(buf)
          bufstart = tonumber(bufstart)
          bufcnt = tonumber(bufcnt)

          -- check if texture
          local texnum = string.match(resource,"texunit (%d+)")

          local argnames = {}
          if (rescnt) then
            for i=0,(rescnt-1) do
              table.insert(argnames,"c["..tostring(resstart + i).."]")
            end
          elseif (texnum) then
            table.insert(argnames,"texture["..tostring(texnum).."]")
            table.insert(argnames,"texture"..tostring(texnum))
          elseif (buf) then
            table.insert(argnames,"buf"..tostring(buf).."["..tostring(bufstart).."]")
          else
            table.insert(argnames,resource)
          end

          for i,v in ipairs(argnames) do
            argregistry[v] = descriptor
          end
        elseif (funcname and subroutine) then
          argregistry["SUBROUTINENUM("..subroutine..")"] = "function "..funcname
        elseif string.find(w,"BUFFER4") then
          fixargbuffers()
        elseif string.find(w,"TEMP") then
          --TEMP R0, R1, R2, R3, R4, R5, R6, R7, R8, R9, R10, R11;
          --TEMP RC, HC;
          --TEMP lmem[9];
          registercc = registercc or 0
          for i in string.gmatch(w,"C") do
            registercc = registercc + 1
          end
          registermem = tonumber(string.match(w,"lmem%[(%d+)%]"))
        else
          regsuccess = false
        end

        return regsuccess
      end

      local function checkargs(str)
        local comment = "#"
        local declared = {}
        for i in string.gmatch(str,"([%[%]%(%)%w]+)") do
          local descr = argregistry[i]
          if (descr and not declared[i]) then
            comment = comment.." "..i.." = "..descr
            declared[i] = true
          end
        end

        return comment ~= "#" and comment
      end

      local registerlevels = {{}}
      local function checkregisters(str,indent)
        if (indent < 0) then return end
        local cur = registerlevels[indent+1]
        for i in string.gmatch(str,"R(%d+)") do
          cur[i] = true
        end
      end

      local function clearregisters(indent)
        registerlevels[math.max(0,indent)+1] = {}
      end

      local function outputregisters(indent)
        if (indent < 0) then return "" end
        local tab = registerlevels[indent+1]
        local out = {}
        for i,v in pairs(tab) do
          table.insert(out,i)
        end
        table.sort(out)
        local cnt = #out
        if (cnt < 1) then return "" end

        local str = string.rep(" ",indent).."# "..tostring(cnt).." R used: "
        for i,v in ipairs(out) do
          str = str..tostring(v)..((i==cnt) and "" or ", ")
        end
        return str.."\n"
      end

      -- check declarations
      local lastline = ""
      for w in string.gmatch(tx, "[^\n]*\n") do
        if (not checkregistry(w)) then

          if (checknesting(w,endindent)) then
            newtx = newtx..outputregisters(indent)
            if (indent == 0) then clearregisters(indent) end
            indent = math.max(0,indent - 1)
          end

          local firstchar = string.sub(w,1,1)
          local indentstr = (firstchar ~= " " and firstchar ~= "\t" and string.rep(" ",indent) or "")
          local linestr = indentstr..w
          local argcomment = (firstchar ~= "#") and checkargs(w)

          checkregisters(w,indent)

          newtx = newtx..(argcomment and (indentstr..argcomment.."\n") or "")
          newtx = newtx..linestr

          if (checknesting(w,startindent)) then
            indent = indent + 1
            maxindent = math.max(maxindent,indent)
            clearregisters(indent)
          end
        else
          newtx = newtx..w
        end
        lastline = w
      end

      local registers = tonumber(string.match(lastline, "(%d+) R%-regs")) or 0
      registermem = registermem or 0
      registercc = registercc or 0
      local stats = "# "..tostring(registercc).." C-regs, "..tostring(registermem).." L-regs\n"
      stats = stats.."# "..tostring(registercc + registermem + registers).." maximum registers\n"
      stats = stats.."# "..maxindent.." maximum nesting level\n"
      newtx = newtx..stats

      return newtx,lastline..stats
    end

    local function beautifyAsmFile(filePath)
      local file_text = ""
      local statlines = ""
      local handle = io.open(filePath, "rb")
      if handle then
        file_text = handle:read("*a")
        file_text,statlines = beautifyAsm(file_text)
        handle:close()
      end

      if (file_text == "") then return end

      local handle = io.open(filePath, "wb")
      if handle then
        handle:write(file_text)
        handle:close()
      end
      return statlines
    end

    -- Compile Arg
    frame:Connect(ID "cg.compile.input",wx.wxEVT_COMMAND_MENU_SELECTED,
      function(event)
        data.customarg = event:IsChecked()
      end)

    frame:Connect(ID "cg.compile.gles",wx.wxEVT_COMMAND_MENU_SELECTED,
      function(event)
        data.gles = event:IsChecked()
      end)
    -- Compile
    local function evCompile(event)
      local filename,info = GetEditorFileAndCurInfo()
      local editor = GetEditor()
      local glsl = editor and editor.spec and editor.spec.apitype and editor.spec.apitype == "glsl"
      local entryname = (glsl and "main" or info.selword)

      if (not (filename and entryname and cgbinpath)) then
        DisplayOutput("Error: Cg Compile: Insufficient parameters (nofile / not selected entry function!\n")
        return
      end

      local domain = data.domains[event:GetId()]
      local profile = data.profiles[data.profid]
      if (not profile[domain]) then return end

      -- popup for custom input
      data.custom = data.customarg and wx.wxGetTextFromUser("Compiler Args","Cg",data.custom) or data.custom
      local args = data.customarg and data.custom or ""
      args = args:len() > 0 and args or nil

      local fullname  = filename:GetFullPath()
      local outname = fullname.."."..entryname.."^"
      outname = args and outname..args:gsub("%s*[%-%/]",";-")..";^" or outname
      outname = outname..profile[domain]..profile.ext
      outname = '"'..outname..'"'

      local cmdglsl = data.gles and "-ogles -glslWerror -DGL_ES" or "-oglsl -glslWerror -po PaBO2 "
      local cmdline = ' "'..fullname..'" -profile '..profile[domain].." "
      cmdline = glsl and cmdline..cmdglsl or cmdline
      cmdline = glsl and (data.profid == (ID "cg.profile.gp5")) and cmdline.."-po NV_shader_atomic_float -po NV_bindless_texture " or cmdline
      cmdline = args and cmdline..args.." " or cmdline
      cmdline = cmdline..data.domaindefs[domain]
      cmdline = cmdline.."-o "..outname.." "
      cmdline = cmdline.."-entry "..entryname

      cmdline = cgbinpath.."/cgc.exe"..cmdline

      local function nvperfcallback(str)
        local pixels = string.match(str,"([,%d]+) pixels/s")
        pixels = pixels and string.gsub(pixels,",","")
        pixels = tonumber(pixels)
        local function tostr(num)
          return string.format("%.2f",num)
        end

        -- delete .cgbin file
        local binname,ext = fullname:match("(.*)%.([a-zA-Z_0-9]+)$")
        binname = binname..".cgbin"
        wx.wxRemoveFile(binname)

        if (pixels ~= nil) then
          local str = string.match(str,("(.* pixels/s)"))
          local info = "1920x1080: "..tostr(pixels/(1920*1080)).." Hz\n"
          info = info.."1280x1024: "..tostr(pixels/(1280*1024)).." Hz\n"
          str = (str.."\n"..info)
          return str
        else
          return str.."\n"
        end
      end

      local function compilecallback(str)
        local postfunc
        -- check for errors, if none, launch nvperf
        -- and indentation
        if (string.find(str," 0 errors.")) then
          postfunc = function()
            -- beautify asm
            if (profile.asm) then
              local statlines = beautifyAsmFile(outname:sub(2,-2))
              DisplayOutput(statlines)
            end

            -- optionally run perf process
            local cgperfgpu = ide.config.cgperfgpu or "G80"
            local profiletypes = {
              ["G70"] = {},
              ["G80"] = {
                ["vp40"] = " -profile vp40",
                ["fp40"] = " -profile fp40"},
            }
            if (hasperf and (not glsl) and profile.nvperf and (domain == 1 or domain == 2)
              and profiletypes[cgperfgpu])
            then
              local domaintypes = {"cg_vp","cg_fp",}
              local cmdline = " -gpu "..cgperfgpu.." -type "..domaintypes[domain]
              cmdline = cmdline.." -function "..info.selword
              cmdline = cmdline..(profiletypes[cgperfgpu][profile[domain]] or "")
              cmdline = cmdline..' "'..fullname..'"'

              cmdline = cgbinpath..perfexe..cmdline
              CommandLineRun(cmdline,nil,true,nil,nvperfcallback)
            end
          end
        end

        return str,postfunc
      end

      -- run compiler process
      CommandLineRun(cmdline,nil,true,nil,compilecallback)

    end

    frame:Connect(ID "cg.compile.vertex",wx.wxEVT_COMMAND_MENU_SELECTED,evCompile)
    frame:Connect(ID "cg.compile.fragment",wx.wxEVT_COMMAND_MENU_SELECTED,evCompile)
    frame:Connect(ID "cg.compile.geometry",wx.wxEVT_COMMAND_MENU_SELECTED,evCompile)
    frame:Connect(ID "cg.compile.tessctrl",wx.wxEVT_COMMAND_MENU_SELECTED,evCompile)
    frame:Connect(ID "cg.compile.tesseval",wx.wxEVT_COMMAND_MENU_SELECTED,evCompile)
    frame:Connect(ID "cg.compile.compute",wx.wxEVT_COMMAND_MENU_SELECTED,evCompile)

    -- indent asm
    frame:Connect(ID "cg.format.asm", wx.wxEVT_COMMAND_MENU_SELECTED,
      function(event)
        local curedit = GetEditor()
        local newtx = beautifyAsm( curedit:GetText() )

        curedit:SetText(newtx)
      end)

    -- master file
    frame:Connect(ID "cg.format.master", wx.wxEVT_COMMAND_MENU_SELECTED,
      function(even)
        local filename,info = GetEditorFileAndCurInfo()
        buildFromMaster(filename)
      end)
  end,
}
