-- Helper functions that are useful to customize the openspace.cfg loading

-- ####################################################################################### 
-- ##                          Public functions                                         ##
-- ####################################################################################### 

-- SGCT related functions
sgct = {}
sgct.config = {}

-- Creates a configuration file similar to the default 'single.xml':
-- The parameter is a table and can contain the follow attributes:

-- first argument: horizontal window size {default: 1280}
-- second argument: vertical window size {default: 720}
-- res: A table containing the horizontal and vertical resolution [example: res={3840, 2160}]
-- pos: The position of the window on the screen [example: pos={50, 100}] {default: {50, 50}}
-- fullScreen: Whether the application should run in exclusive full screen [example: fullScreen=true] {default: false}
-- border: Whether the application should have window decorations (aka. border) [example: border=false] {default: true}
-- monitor: Determines the monitor on which the application is started [example: monitor=2] {default: 0}
-- shared: Determines whether the contents of the window should be shared using the SPOUT library [example: shared=true] {default: false}

-- Expert settings:
-- vsync: Whether the rendering speed is locked to the refreshrate [example: vsync=true] {default: false}
-- refreshRate: If vsync is enabled, this is the target framerate [example: refreshRate=30] {default: infinity}
-- stereo: Select the stereo rendering mode as supported by SGCT [example: stereo='anaglyph_red_cyan'] {default: 'none'}
-- msaa: The multisampling anti-aliasing factor [example: msaa=8] {default: 4}
-- scene: Global settings to all scene objects (offset, orientation, scaling; each optional)
--      [example: scene = {offset = {x = 1.0, y = 1.0, z = 2.0}, orientation = { yaw = 120, pitch = 15, roll = 0.0 }, scale = 10.0}]
-- sgctDebug: Determines whether a higher debug level in SGCT is enabled [example: sgctDebug=true] {default: false}
-- fov: The field of view settings [example: fov={ left=20, right=30, up=10, down=50}] {default: { left=40.0, right=40.0, up=22.5, down=22.5}}
-- tracked: Determines whether the aspect ratio of the camera defined at application startup should persist when the window is resized [example: tracked=false] {default: true}

-- Thus this function can be called the following ways:
-- sgct.config.single() -> Leading to a 1280x720 resolution window
-- sgct.config.single(1920, 1080) -> Leading to a 1920x1080 resolution window
-- sgct.config.single(640, 360, res={3840, 2160}) -> 640x360 window with 4K rendering resolution
-- sgct.config.single(msaa=1) -> 1280x720 resolution without multisampling
function sgct.config.single(arg) end



-- Creates a configuration file similar to the default 'single_fisheye.xml'
-- The parameter is a table and can contain the follow attributes:

-- first argument: horizontal window size {default: 1280}
-- second argument: vertical window size {default: 720}
-- res: A table containing the horizontal and vertical resolution [example: res={3840, 2160}]
-- pos: The position of the window on the screen [example: pos={50, 100}] {default: {50, 50}}
-- fullScreen: Whether the application should run in exclusive full screen [example: fullScreen=true] {default: false}
-- border: Whether the application should have window decorations (aka. border) [example: border=false] {default: true}
-- monitor: Determines the monitor on which the application is started [example: monitor=2] {default: 0}
-- shared: Determines whether the contents of the window should be shared using the SPOUT library [example: shared=true] {default: false}

-- Expert settings:
-- vsync: Whether the rendering speed is locked to the refreshrate [example: vsync=true] {default: true}
-- refreshRate: If vsync is enabled, this is the target framerate [example: refreshRate=30] {default: infinity}
-- stereo: Select the stereo rendering mode as supported by SGCT [example: stereo='anaglyph_red_cyan'] {default: 'none'}
-- msaa: The multisampling anti-aliasing factor [example: msaa=8] {default: 4}
-- scene: Global settings to all scene objects (offset, orientation, scaling; each optional)
--      [example: scene = {offset = {x = 1.0, y = 1.0, z = 2.0}, orientation = { yaw = 120, pitch = 15, roll = 0.0 }, scale = 10.0}]
-- sgctDebug: Determines whether a higher debug level in SGCT is enabled [example: sgctDebug=true] {default: false}
-- fov: The field of view for the fisheye [example: fov=360] {default: 180}
-- quality: The quality setting for the cubemap textures [example: quality="4k"] {default: "1k"}
-- tilt: The forwards tilt of the fisheye, relative to the center [example: tilt=90] {default: 0.0}
-- background: The background color used outside of the fisheye rendering [example: backgruound={r=1.0, g=0.25, b=0.25, a=1.0}] {default: {r=0.1, g=0.1, b=0.1, a=1.0}}

-- Thus this function can be called the following ways:
-- sgct.config.fisheye() -> Leading to a 1280x720 resolution window
-- sgct.config.fisheye(640, 640) -> Leading to a 640x650 resolution window
-- sgct.config.fisheye(640, 360, res={3840, 3840}) -> 640x360 window with 4K rendering resolution
-- sgct.config.fisheye(msaa=1) -> 1280x720 resolution without multisampling
function sgct.config.fisheye(arg) end

-- Global variable storing the name of the config function called at initialization
sgctconfiginitializeString = ""




--[[
########################################################################################## 
                            Internal helper functions
########################################################################################## 
]]--

function generateSingleViewportFOV(down, up, left, right, tracked)
  local result = {}

  table.insert(result,   [[            {]])

  if tracked then
    table.insert(result, [[              "tracked": ]] .. tostring(tracked) .. [[,]])
  end

  table.insert(result,   [[              "projection": {]])
  table.insert(result,   [[                "type": "PlanarProjection",]])
  table.insert(result,   [[                "fov": {]])
  table.insert(result,   [[                  "left": ]] .. left .. [[,]])
  table.insert(result,   [[                  "right": ]] .. right .. [[,]])
  table.insert(result,   [[                  "up": ]] .. up .. [[,]])
  table.insert(result,   [[                  "down": ]] .. down)
  table.insert(result,   [[                }]])
  table.insert(result,   [[              }]])
  table.insert(result,   [[            }]])

  return table.concat(result, "\n")
end



function generateFisheyeViewport(fov, quality, tilt, background, crop, offset, tracked)
  local result = {}

  table.insert(result,   [[           {]])

  if tracked then
    table.insert(result, [[             "tracked": ]] .. tostring(tracked) .. [[,]])
  end

  table.insert(result,   [[             "projection": {]])
  table.insert(result,   [[               "type": "FisheyeProjection",]])

  if fov then
    table.insert(result, [[               "fov": ]] .. fov .. [[,]])
  end
  table.insert(result,   [[               "quality": "]] .. quality .. [[",]])
  table.insert(result,   [[               "tilt": ]] .. tilt .. [[,]])

  if crop then
    table.insert(result, [[               "crop": {]])
    table.insert(result, [[                 "left": ]] .. crop["left"] .. [[,]])
    table.insert(result, [[                 "right": ]] .. crop["right"] .. [[,]])
    table.insert(result, [[                 "top": ]] .. crop["top"] .. [[,]])
    table.insert(result, [[                 "bottom": ]] .. crop["bottom"])
    table.insert(result, [[               },]])
  end

  if offset then
    table.insert(result, [[               "offset": {]])
    table.insert(result, [[                 "x": ]] .. offset["x"] .. [[,]])
    table.insert(result, [[                 "y": ]] .. offset["y"] .. [[,]])
    table.insert(result, [[                 "z": ]] .. offset["z"])
    table.insert(result, [[               },]])
  end

  table.insert(result,   [[               "background": {]])
  table.insert(result,   [[                 "r": ]] .. background["r"] .. [[,]])
  table.insert(result,   [[                 "g": ]] .. background["g"] .. [[,]])
  table.insert(result,   [[                 "b": ]] .. background["b"] .. [[,]])
  table.insert(result,   [[                 "a": ]] .. background["a"])
  table.insert(result,   [[               }]])
  table.insert(result,   [[             }]])
  table.insert(result,   [[           }]])

  return table.concat(result, "\n")
end



function generateWindow(result, fullScreen, msaa, border, monitor, tags, stereo, pos,
                        size, res, viewport)
  table.insert(result,   [[          "name": "OpenSpace",]])

  if fullScreen then
    table.insert(result, [[          "fullscreen": ]] .. tostring(fullScreen) .. [[,]])
  end

  if msaa then
    table.insert(result, [[          "msaa": ]] .. msaa .. [[,]])
  end

  if border then
    table.insert(result, [[          "border": ]] .. tostring(border) .. [[,]])
  end

  if monitor then
    table.insert(result, [[          "monitor": ]] .. monitor .. [[,]])
  end

  if #(tags) > 0 then
    for i, v in ipairs(tags) do
      tags[i] = "\"" .. v .. "\""
    end
    local t = table.concat(tags, [[,]])
    table.insert(result, [[          "tags": [ ]] .. t .. [[ ], ]])
  end

  if stereo then
    table.insert(result, [[          "stereo": "]] .. stereo .. [[",]])
  end

  local px = pos[1]
  local py = pos[2]
  table.insert(result,   [[          "pos": { "x": ]] .. px .. [[, "y": ]] .. py .. [[ },]])

  local sx = size[1]
  local sy = size[2]
  table.insert(result,   [[          "size": { "x": ]] .. sx .. [[, "y": ]] .. sy .. [[ },]])

  if res then
    res[1] = res[1] or size[1]
    res[2] = res[2] or size[2]
    table.insert(result, [[          "res": {]])
    table.insert(result, [[            "x": ]] .. res[1] .. [[,]])
    table.insert(result, [[            "y": ]] .. res[2])
    table.insert(result, [[          },]])
  end

  table.insert(result,   [[          "viewports": []])

  table.insert(result, viewport)
  table.insert(result,   [[          ] ]])
end



function generateUser(result)
  table.insert(result, [[    {]])
  table.insert(result, [[      "eyeseparation": 0.06,]])
  table.insert(result, [[      "pos": { "x": 0.0, "y": 0.0, "z": 0.0 }]])
  table.insert(result, [[    }]])
end



function generateScene(result, scene)
  if scene == nil then
    return nil
  end

  local offset = scene["offset"] or { x = 0.0, y = 0.0, z = 0.0 }
  local orientation = scene["orientation"] or { yaw = 0.0, pitch = 0.0, roll = 0.0 }
  local scale = scene["scale"] or 1.0

  table.insert(result, [[{]])
  table.insert(result, [[  "offset": {]])
  table.insert(result, [[    "x": ]] .. offset["x"] .. [[,]])
  table.insert(result, [[    "y": ]] .. offset["y"] .. [[,]])
  table.insert(result, [[    "z": ]] .. offset["z"] .. [[,]])
  table.insert(result, [[  },]])
  table.insert(result, [[  "orientation": {]])
  table.insert(result, [[    "yaw": ]] .. orientation["yaw"] .. [[,]])
  table.insert(result, [[    "pitch": ]] .. orientation["pitch"] .. [[,]])
  table.insert(result, [[    "roll": ]] .. orientation["roll"] .. [[,]])
  table.insert(result, [[  },]])
  table.insert(result, [[  "scale": ]] .. scale)
  table.insert(result, [[}]])
end



function generateSettings(result, refreshRate, vsync)
  table.insert(result,   [[    "display": {]])

  if (refreshRate) then
    table.insert(result, [[      "refreshrate": ]] .. refreshRate .. [[,]])
  end

  if vsync then
    table.insert(result, [[      "swapinterval": 1]])
  else
    table.insert(result, [[      "swapinterval": 0]])
  end

  table.insert(result,   [[    }]])
end



function generateCluster(arg, viewport)
  local result = {}

  table.insert(result,   [[{]])
  table.insert(result,   [[  "version": 1,]])
  table.insert(result,   [[  "masteraddress": "127.0.0.1",]])

  if arg["sgctDebug"] then
    table.insert(result, [[  "debug": ]] .. tostring(arg["sgctdebug"]) .. [[,]])
  end

  table.insert(result, [[  "settings": {]])
  generateSettings(result, arg["refreshRate"], arg["vsync"])
  table.insert(result, [[  },]])

  if arg["scene"] then
    table.insert(result, [[  "scene": {]])
    generateScene(result, arg["scene"])
    table.insert(result, [[  },]])
  end

  table.insert(result,   [[  "nodes": []])
  table.insert(result,   [[    {]])
  table.insert(result,   [[      "address": "127.0.0.1",]])
  table.insert(result,   [[      "port": 20401,]])
  table.insert(result,   [[      "windows": []])
  table.insert(result,   [[        {]])
  generateWindow(result, arg["fullScreen"], arg["msaa"], arg["border"], arg["monitor"],
    arg["tags"], arg["stereo"], arg["pos"], arg["size"], arg["res"], viewport)
  table.insert(result,   [[        }]])
  table.insert(result,   [[      ] ]])
  table.insert(result,   [[    }]])
  table.insert(result,   [[  ],]])
  table.insert(result,   [[  "users": []])
  generateUser(result)
  table.insert(result,   [[  ] ]])
  table.insert(result,   [[}]])

  return table.concat(result, "\n")
end



function check(type_str, arg, param, subparam_type)
  local t = type(arg[param])
  assert(t == type_str or t == "nil", param .. " must be a " .. type_str .. " or nil")

  if type_str == "table" and subparam_type and arg[param] then
    for k, v in pairs(arg[param]) do
      assert(
        type(v) == subparam_type,
        param .. "[" .. k .. "] must be a " .. subparam_type
      )
    end    
  end
end



function generateSingleWindowConfig(arg, viewport)
  check("table", arg, "res", "number")
  check("table", arg, "tags", "string")
  check("table", arg, "pos", "number")
  check("boolean", arg, "shared")
  check("boolean", arg, "fullScreen")
  check("boolean", arg, "border")
  check("boolean", arg, "vsync")
  check("boolean", arg, "sgctDebug")
  check("number", arg, "monitor")
  check("number", arg, "msaa")
  check("number", arg, "refreshRate")
  check("string", arg, "stereo")

  check("table", arg, "scene")
  if arg["scene"] then
    check("table", arg["scene"], "offset", "number")
    check("table", arg["scene"], "orientation", "number")
    check("number", arg["scene"], "scale")
  end
  
  if arg["shared"] then
    local t = arg["tags"]
    t[#t + 1] = "Spout"
  end

  return generateCluster(arg, viewport)
end



function sgct.makeConfig(config)
  local configFile = os.tmpname() .. ".json"
  local file = io.open(configFile, "w+")
  file:write(config)
  io.close(file)
  return configFile
end



function sgct.config.single(arg)
  arg = arg or {}

  if type(arg[1]) == "number" and type(arg[2]) == "number" then
    arg["size"] = { arg[1], arg[2] }
    arg[1] = nil
    arg[2] = nil
  else
    -- No numbers specified, therefore we want to use the screen resolution of the primary
    -- monitor to derive the resolution
    -- ScreenResolution is a variable that got injected into the openspace.cfg by the
    -- OpenSpace code prior to loading this file

    local scale_factor = 2.0/3.0;
    arg["size"] = { ScreenResolution.x * scale_factor, ScreenResolution.y * scale_factor }
  end

  check("table", arg, "size", "number")
  check("table", arg, "fov", "number")

  arg["vsync"] = arg["vsync"] or false
  arg["tags"] = arg["tags"] or {}
  arg["pos"] = arg["pos"] or { 50, 50 }
  arg["size"] = arg["size"] or { 1280, 720 }

  if (not arg["fov"]) then
    local tanDefaultFov = math.tan(math.rad(40.0))

    local tanHorizontalFov
    local tanVerticalFov
    local aspectRatio = arg["size"][1] / arg["size"][2]
    if (aspectRatio > 1) then
      tanHorizontalFov = tanDefaultFov
      tanVerticalFov = tanDefaultFov / aspectRatio
    else
      tanHorizontalFov = tanDefaultFov * aspectRatio
      tanVerticalFov = tanDefaultFov
    end

    arg["fov"] = {
      down = math.deg(math.atan(tanVerticalFov)),
      up = math.deg(math.atan(tanVerticalFov)),
      left = math.deg(math.atan(tanHorizontalFov)),
      right = math.deg(math.atan(tanHorizontalFov))
    }
  end

  check("boolean", arg, "tracked")
  sgctconfiginitializeString = "sgct.config.single"

  arg["tracked"] = arg["tracked"] or true

  local viewport = generateSingleViewportFOV(arg["fov"]["down"], arg["fov"]["up"], 
    arg["fov"]["left"], arg["fov"]["right"], arg["tracked"])
 
  return sgct.makeConfig(generateSingleWindowConfig(arg, viewport))
end



function sgct.config.fisheye(arg)
  arg = arg or {}

  check("number", arg, 1)
  check("number", arg, 2)

  if type(arg[1]) == "number" and type(arg[2]) == "number" then
    arg["size"] = { arg[1], arg[2] }
    arg[1] = nil
    arg[2] = nil
  else
    -- No numbers specified, therefore we want to use the screen resolution of the primary
    -- monitor to derive the resolution
    -- ScreenResolution is a variable that got injected into the openspace.cfg by the
    -- OpenSpace code prior to loading this file

    local scale_factor = 2.0/3.0;
    arg["size"] = { ScreenResolution.x * scale_factor, ScreenResolution.y * scale_factor }    
  end

  check("number", arg, "fov")
  check("number", arg, "tilt")
  check("string", arg, "quality")
  check("table", arg, "background", "number")
  check("table", arg, "crop", "number")
  check("table", arg, "offset", "number")

  sgctconfiginitializeString = "sgct.config.fisheye"

  arg["vsync"] = arg["vsync"] or false
  arg["tags"] = arg["tags"] or {}
  arg["pos"] = arg["pos"] or { 50, 50 }
  arg["size"] = arg["size"] or { 1024, 1024 }

  arg["quality"] = arg["quality"] or "1k"
  arg["tilt"] = arg["tilt"] or 90.0
  arg["background"] = arg["background"] or { r = 0.0, g = 0.0, b = 0.0, a = 1.0 }
  arg["tracked"] = arg["tracked"] or false

  local viewport = generateFisheyeViewport(arg["fov"], arg["quality"], arg["tilt"],
    arg["background"], arg["crop"], arg["offset"], arg["tracked"])

  return sgct.makeConfig(generateSingleWindowConfig(arg, viewport))
end
