require("luamods/command")
require("luamods/layout")
require("luamods/cgi")

local xml = require("pl.xml")
local pretty = require("pl.pretty")
local stringx = require("pl.stringx")
local tablex = require("pl.tablex")
local List = require("pl.List")

stringx.import()

header = layout.getHeader()
footer = layout.getFooter()

local configPath = "/etc/sam2/pipes.xml"

-- parsed dictionary of SAM2 config out of the config file.
local udpServer = {}
local pipes = {}

-- Any errors or warnings collected from the parsing.
local errors = {}
local warnings = {}

-- Record any error to display to user.
local frontendErr = nil

-- convenience function to safely set a depply nested table.
-- property should be a pl.List.
local function deepSet(obj, property, val)
    if #property > 1 then
        if not obj[property[1]] then
            obj[property[1]] = {}
        end

        obj = deepSet(obj[property[1]], property:slice(2, #property), val)
        return obj
    elseif #property == 1 then
        obj[property[1]] = val
        return obj
    else
        return obj
    end
end

-- convenience function to safely get from a deeply nested table.
local function deepGet(obj, property)
    if not obj then
        return nil
    end

    if #property > 1 then
        local objNextLevel = obj[property[1]] or nil
        if objNextLevel then
            return deepGet(objNextLevel, property:slice(2, #property))
        else
            return nil
        end
    else
        return obj[property[1]] or nil
    end
end

-- Handle a post if we have one.
local postPipes = nil -- map representation of pipes after parsing from post table.
if cgi.post and cgi.post.submitButton then
    -- There is a post, so we can populate the new details for each pipe.
    postPipes = {}

    -- The post is a flat map of keys... so we go through them in
    -- the effort to make it logically nested.
    for key, val in pairs(cgi.post) do
        local keyList = List.new((key):split("."))

        if keyList[1] == "pipe" then
            if not postPipes[keyList[2]] then
                postPipes[keyList[2]] = {}
            end

            -- The property is everything after "pipe.<idx>".
            -- Set the property for the pipe in the nested object.
            local property = keyList:slice(3, keyList:len())
            deepSet(postPipes[keyList[2]], property, val)
        elseif keyList[1] == "udp_server" then
          udpServer[keyList[2]] = val  
        end
    end

    local fixCheckbox = function(pipe, key)
        if not pipe[key] then
            pipe[key] = "false"
        elseif pipe[key] == "on" then
            pipe[key] = "true"
        else
            pipe[key] = "false"
        end
    end

    -- Fix checkbox values.
    for _, pipe in pairs(postPipes) do
        fixCheckbox(pipe, 'enabled')
        fixCheckbox(pipe, 'udp_push')
    end

    -- Index by the only unique key. The serial port device.
    local indexedPipes = {}
    for _, pipe in pairs(postPipes) do
        indexedPipes[pipe.serial_port.device] = pipe
    end
    postPipes = indexedPipes
end

-- pipe a serial port def in a pipe defintion... in the sam2 config.
local function getFromSerialPortTag(tag, postPipe)
    local serial_port = tag

    serial_port = tablex.deepcopy(tag.attr)

    for serialTag in tag:childtags() do
        if serialTag.tag == "baud" then
            serial_port.baud = deepGet(postPipe, List.new({'serial_port', 'baud'})) or serialTag[1]
            serialTag[1] = serial_port.baud
        elseif serialTag.tag == "data" then
            serial_port.data = deepGet(postPipe, List.new({'serial_port', 'data'})) or serialTag[1]
            serialTag[1] = serial_port.data
        elseif serialTag.tag == "stop" then
            serial_port.stop = deepGet(postPipe, List.new({'serial_port', 'stop'})) or serialTag[1]
            serialTag[1] = serial_port.stop
        elseif serialTag.tag == "mode" then
            serial_port.mode = deepGet(postPipe, List.new({'serial_port', 'mode'})) or serialTag[1]
            serialTag[1] = serial_port.mode
        else
            table.insert(errors, "Unexpected tag in serial port definition... "..pipeTag.tag)
        end
    end

    return serial_port
end

-- parse a pipe def from the sam2 config.
local function getPipe(tag)
    local pipe = {}

    -- Lookup the serial port so we can populate the entries
    -- if this execution is in fact a post.
    local postPipe = nil
    local serialTags = tag:get_elements_with_name("serial_port", true)
    if not serialTags or #serialTags == 0 then
        table.insert(errors, "No serial_port tag for pipe.")
    else
        if postPipes then
            postPipe = postPipes[serialTags[1].attr.device]
        end
    end

    for pipeTag in tag:childtags() do
        if pipeTag.tag == "enabled" then
            pipe.enabled = deepGet(postPipe, List.new({'enabled'})) or pipeTag[1]
            pipeTag[1] = pipe.enabled
        elseif pipeTag.tag == "label" then
            pipe.label = deepGet(postPipe, List.new({'label'})) or pipeTag[1]
            pipeTag[1] = pipe.label
        elseif pipeTag.tag == "tcp_port" then
            pipe.tcp_port = deepGet(postPipe, List.new({'tcp_port'})) or pipeTag[1]
            pipeTag[1] = pipe.tcp_port
        elseif pipeTag.tag == "udp_push" then
            pipe.udp_push = deepGet(postPipe, List.new({'udp_push'})) or pipeTag[1]
            pipeTag[1] = pipe.udp_push
        elseif pipeTag.tag == "serial_port" then
            pipe.serial_port = getFromSerialPortTag(pipeTag, postPipe)
        else
            table.insert(errors, "Unexpected tag in pipe definition... "..pipeTag.tag)
        end
    end

    return pipe
end

-- parse the sam2 config XML document. You must provide the penlight XML document.
-- If a post is being handled... the original document will be updated with the new
-- values, allowing rewrite.
local function parseSam2Config(doc)
    for tag in doc:childtags() do
        if tag.tag == "udp_server" then
            udpServer.host = udpServer.host or tag.attr.host
            tag.attr.host = udpServer.host
            udpServer.port = udpServer.port or tag.attr.port
            tag.attr.port = udpServer.port
        elseif tag.tag == "pipe" then
            table.insert(pipes, getPipe(tag))
        else
            table.insert(warnings, "Unexpected tag in sam2 definition... "..pipeTag.tag)
        end
    end
end

userLevel = db.getPriv(layout.username) or 1

if (userLevel == 1) then
    buttonControl = " disabled"
    inputControl = " readonly"
else
    buttonControl = ""
    inputControl = ""
end

local doc, docErr = xml.parse(configPath, true)
if not doc then
    docErr = configPath .. " has invalid XML."
end

content = ""

content = content .. [[
                    <div id="info">
                        <div>
                            <center><h1>SAM2 Configuration</h1></center>
                        </div>
                    </div>
]]

if (docErr) then
    -- Bail on error here
    content = [[
                    <div id="info">
                        <center>
                            <h2><img src="images/warning.png"/>&nbsp; Invalid SAM2 configuration</h2>
                            Config file, "]]..configPath..[[", is invalid or unreadable.<br><br>
                            <a href="home.lua">Back to Home</a>
                        </center>
                    </div>
]]
    command.log(docErr)
    html = header..content..footer
    print(html)
    os.exit()
end

parseSam2Config(doc)

-- write new config file if this is a post.
if postPipes then
    local pipesContent = "<!-- /etc/sam2/pipes.xml generated by webconfig -->\n"
    pipesContent = pipesContent .. xml.tostring(doc, "", "        ")
    pipesContent = pipesContent .. "\n<!-- end of file -->\n"
    local f, err = io.open(configPath, "w+")
    if err then
        command.log("There was a problem saving the changes. " .. err)
        frontendErr = "There was a problem saving the changes."
    else
        f:write(pipesContent)
        f:close()

        -- Restart the service.
        local out, status = command.ex("systemctl restart sccd 2>&1")
        if not status then
            command.log("Failed to restart sccd. " .. out)
            frontendErr = "Failed to restart sccd service."
        end
    end
end

if (userLevel == 1) then
    content = content .. [[
                    <center><b>Note:</b> Editing this section requires administrator privileges.</center><br>
]]
end

pipesContent = "<table class=\"onetwo\">"
-- pipe column headings.
pipesContent = pipesContent .. [[
    <tr>
        <th>Enable</th>
        <th>Physical port</th>
        <th>Port description</th>
        <th>Baud rate</th>
        <th>TCP port</th>
        <th>Enable UDP push</th>
    </tr>
]]
-- render each pipe row.
for pipeIdx, pipe in ipairs(pipes) do
    local isChecked = function(condition)
        return condition and "checked" or ""
    end

    -- Convenience for populating baud rate select options..
    local baudOption = function(value, baud)
        if value == baud then
            return [[<option value="]] .. value .. [[" selected>]] .. value .. [[</option>]]
        else
            return [[<option value="]] .. value .. [[">]] .. value .. [[</option>]]
        end
    end

    pipesContent = pipesContent .. [[
        <tr>
            <td style="text-align:center">
                <input type="checkbox" name="pipe.]] .. tostring(pipeIdx) .. [[.enabled"
                    ]] .. isChecked(pipe.enabled == "true") .. [[                    
                />
            </td>

            <td>
                <input type="text" name="pipe.]] .. tostring(pipeIdx) .. [[.serial_port.device"
                    value="]] .. pipe.serial_port.device .. [[" readonly style="background: none; border: none; cursor: default;"
                />
            </td>

            <td>
                <input type="text" name="pipe.]] .. tostring(pipeIdx) .. [[.label"
                    value="]] .. pipe.label .. [["
                />
            </td>

            <td>
                <select name="pipe.]] .. tostring(pipeIdx) .. [[.serial_port.baud">
                    ]] .. baudOption("115200", pipe.serial_port.baud) .. [[
                    ]] .. baudOption("57600",  pipe.serial_port.baud) .. [[
                    ]] .. baudOption("38400",  pipe.serial_port.baud) .. [[
                    ]] .. baudOption("19200",  pipe.serial_port.baud) .. [[
                    ]] .. baudOption("9600",   pipe.serial_port.baud) .. [[
                </select>
            </td>

            <td>
            <input type="number" min="1" max="65535" name="pipe.]] .. tostring(pipeIdx) .. [[.tcp_port"
                value="]] .. pipe.tcp_port .. [["
            />
        </td>

            <td style="text-align:center">
                <input type="checkbox" name="pipe.]] .. tostring(pipeIdx) .. [[.udp_push"
                    ]] .. isChecked(pipe.udp_push == "true") .. [[                    
                />
            </td>
        </tr>
    ]]
end
-- end table of pipes.
pipesContent = pipesContent .. [[
    </table>
]]

-- Read the config after any edits for the current state.
local configViewContent = ""
local confFile, err = io.open(configPath, "r")
if err then
    frontendErr = "Failed to read config file, " .. configPath
    command.log(frontendErr .. ". Orig err: " .. err)
else
    local configContent = confFile:read("*all")
    confFile:close()

    configViewContent = configViewContent .. [[
        <script>
            function toggleVis(target)
            {
                if (target.style.display == "none")
                {
                    target.style.display = "inline-block";
                }
                else
                {
                    target.style.display = "none";
                }
                return false;
            }
        </script>

        <br>
        <img src="images/magnify.png"/>
        <a href="javascript:void(0)" onclick="toggleVis(document.getElementById('conf'));">View ]]..configPath..[[</a>
        <br>

        <div id="conf" style="display: none; border: 1px solid black; border-radius: 4px; padding: 1px; width: 100%;">
            <xmp style="padding: 5px;">
]]..configContent..[[
            </xmp>
        </div>
    ]]
end

content = content .. [[
    <div class="formLayoutfull">
        <form action="sam2_config.lua" method="post">
            <h3>UDP Server</h3>
            <table class="onetwo" style="">
                <tr>
                    <td style="padding-left: 5px;">Host:</td>
                    <td>
                        <input type="text" name="udp_server.host" value=]] .. udpServer.host .. [[ />
                    </td>
                </tr>
                <tr>
                    <td style="padding-left: 5px;">Port:</td>
                    <td>
                        <input type="number" name="udp_server.port" value=]] .. udpServer.port .. [[ />
                    </td>
                </tr>
            </table>

            <hr>
            <h3>Device Tunnels</h3>
            ]] .. pipesContent .. [[

            <br>]]

if frontendErr then
    content = content .. [[<b><img src="images/warning.png"/>&nbsp; ]] .. frontendErr .. [[</b><br><br>]]
else
    if postPipes then
        content = content .. [[<b><img src="images/ok.png"/> Changes saved.</b><br><br>]]
    end
end

content = content .. [[
             <input type="submit" name="submitButton" value="Submit"]]..buttonControl..[[/>
        </form>]]

if configViewContent then
    content = content .. configViewContent
end

content = content .. [[
    </div>
]]

print(header..content..footer)
